Repository: frictionlessdata/frictionless-py

Issue:
Validate single resource with `datapackage.json`
Is it possible to validate an individual resource file (CSV) against the schema in `datapackage.json`.

I have separate processing scripts for each resource and like to run `goodtables` for each individually.

Setting `schema` didn't work (as `datapackage.json` is not a Table schema I assume).

Discussion:
shashigharti: Thank you! @roll I have made the changes. It is ready for the review.
roll: @shashigharti it seems to be failing

Also, can you please add the same tests to `tests/package/validate` tomorrow?
shashigharti: @roll thank you! I have added new tests and fixed failing tests. It is ready for the review!.

Patch:
diff --git a/frictionless/actions/validate.py b/frictionless/actions/validate.py
index d7c00654..9d155da9 100644
--- a/frictionless/actions/validate.py
+++ b/frictionless/actions/validate.py
@@ -47,6 +47,8 @@ def validate(source=None, type=None, **options):
     # We might resolve it when we convert Detector to be a metadata
     if type in ["inquiry", "schema"]:
         options.pop("detector", None)
+    if type != "package":
+        options.pop("resource_name", None)
     return validate(source, deprecate=False, **options)
 
 
@@ -107,6 +109,9 @@ def validate_package(
     package_options = {}
     signature = inspect.signature(validate_resource)
     for name, value in options.copy().items():
+        # Exclude resource_name from package_options
+        if name == "resource_name":
+            continue
         param = signature.parameters.get(name)
         if not param or param.kind != param.KEYWORD_ONLY:
             package_options[name] = options.pop(name)
@@ -115,6 +120,9 @@ def validate_package(
     try:
         native = isinstance(source, Package)
         package = source.to_copy() if native else Package(source, **package_options)
+        # For single resource validation
+        if "resource_name" in options:
+            return validate_resource(package.get_resource(options["resource_name"]))
         package_stats = []
         for resource in package.resources:
             package_stats.append({key: val for key, val in resource.stats.items() if val})
diff --git a/frictionless/package/validate.py b/frictionless/package/validate.py
index 70771307..c9bdfddb 100644
--- a/frictionless/package/validate.py
+++ b/frictionless/package/validate.py
@@ -32,6 +32,10 @@ def validate(package: "Package", original=False, parallel=False, **options):
 
     # Prepare package
     try:
+        # For single resource validation
+        if "resource_name" in options:
+            resource = package.get_resource(options["resource_name"])
+            return resource.validate()
         package_stats = []
         for resource in package.resources:
             package_stats.append({key: val for key, val in resource.stats.items() if val})
diff --git a/frictionless/program/common.py b/frictionless/program/common.py
index 75488ae5..12b78a80 100644
--- a/frictionless/program/common.py
+++ b/frictionless/program/common.py
@@ -279,3 +279,10 @@ csv = Option(
     default=False,
     help="Return in CSV format",
 )
+
+# Resource
+
+resource_name = Option(
+    default=None,
+    help="Name of resource to validate",
+)
diff --git a/frictionless/program/validate.py b/frictionless/program/validate.py
index 67aeadfd..e94751c5 100644
--- a/frictionless/program/validate.py
+++ b/frictionless/program/validate.py
@@ -71,6 +71,8 @@ def program_validate(
     parallel: bool = common.parallel,
     yaml: bool = common.yaml,
     json: bool = common.json,
+    # Resource
+    resource_name: str = common.resource_name,
 ):
     """
     Validate a data source.
@@ -192,6 +194,7 @@ def program_validate(
             limit_memory=limit_memory,
             original=original,
             parallel=parallel,
+            resource_name=resource_name,
         )
     )
 


Test patch:
diff --git a/tests/actions/validate/test_package.py b/tests/actions/validate/test_package.py
index 2249e5ac..eb9a9bba 100644
--- a/tests/actions/validate/test_package.py
+++ b/tests/actions/validate/test_package.py
@@ -528,7 +528,21 @@ def test_validate_package_with_diacritic_symbol_issue_905():
 
 def test_validate_package_with_resource_data_is_a_string_issue_977():
     report = validate(descriptor="data/issue-977.json", type="package")
-    print(report)
     assert report.flatten() == [
         [None, None, None, "package-error"],
     ]
+
+
+def test_validate_package_single_resource_221():
+    report = validate("data/datapackage.json", resource_name="number-two")
+    assert report.valid
+
+
+def test_validate_package_single_resource_wrong_resource_name_221():
+    report = validate("data/datapackage.json", resource_name="number-twoo")
+    assert report.flatten(["code", "message"]) == [
+        [
+            "package-error",
+            'The data package has an error: resource "number-twoo" does not exist',
+        ]
+    ]
diff --git a/tests/package/validate/test_general.py b/tests/package/validate/test_general.py
index 90d92bfe..296e01d3 100644
--- a/tests/package/validate/test_general.py
+++ b/tests/package/validate/test_general.py
@@ -294,3 +294,18 @@ def test_validate_package_with_resource_data_is_a_string_issue_977():
     assert report.flatten() == [
         [None, None, None, "package-error"],
     ]
+
+
+def test_validate_package_single_resource_221():
+    report = validate("data/datapackage.json", resource_name="number-two")
+    assert report.valid
+
+
+def test_validate_package_single_resource_wrong_resource_name_221():
+    report = validate("data/datapackage.json", resource_name="number-twoo")
+    assert report.flatten(["code", "message"]) == [
+        [
+            "package-error",
+            'The data package has an error: resource "number-twoo" does not exist',
+        ]
+    ]
diff --git a/tests/program/test_validate.py b/tests/program/test_validate.py
index b755f781..27eda6af 100644
--- a/tests/program/test_validate.py
+++ b/tests/program/test_validate.py
@@ -257,6 +257,22 @@ def test_program_validate_summary_1094():
     assert result.stdout.count("Total Time Taken (sec)")
 
 
+def test_program_validate_single_resource_221():
+    result = runner.invoke(
+        program, "validate data/datapackage.json --resource-name number-two"
+    )
+    assert result.exit_code == 0
+    assert result.stdout.count("valid: table-reverse.csv")
+
+
+def test_program_validate_single_invalid_resource_221():
+    result = runner.invoke(
+        program, "validate data/datapackage.json --resource-name number-twoo"
+    )
+    assert result.exit_code == 1
+    assert result.stdout.count("invalid: data/datapackage.json")
+
+
 # Helpers