Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ✨ add check_properties() #967

Merged
merged 85 commits into from
Jan 21, 2025
Merged
Show file tree
Hide file tree
Changes from 60 commits
Commits
Show all changes
85 commits
Select commit Hold shift + click to select a range
55c03c6
refactor: :recycle: use custom CheckError in checks
martonvago Dec 18, 2024
958bb18
feat: :sparkles: improve CheckError
martonvago Dec 19, 2024
a619ded
test: :white_check_mark: add tests for helper functions
martonvago Dec 19, 2024
3586c51
refactor: :recycle: return early from function
martonvago Dec 19, 2024
d3de146
docs: :memo: update test docstrings
martonvago Dec 19, 2024
05e7d41
apply suggestions from code review
martonvago Jan 10, 2025
52d3a24
chore(pre-commit): :pencil2: automatic fixes
pre-commit-ci[bot] Jan 10, 2025
64d7a87
Merge branch 'main' into feat/check-error
martonvago Jan 10, 2025
8196dc0
feat: :sparkles: put required fields into constants
martonvago Jan 10, 2025
8c701d8
feat: :sparkles: add simple helper functions
martonvago Jan 10, 2025
b6a5a86
feat: :sparkles: add functions to check required and blank fields
martonvago Jan 10, 2025
e9da123
feat: :sparkles: add functions for collecting Sprout-specific package…
martonvago Jan 10, 2025
6133ac9
feat: :sparkles: add check_resource_properties
martonvago Jan 10, 2025
b179dbe
feat: :sparkles: add check_package_properties
martonvago Jan 10, 2025
166958b
feat: :sparkles: add check_properties
martonvago Jan 10, 2025
dabbb75
docs: :memo: fix docstring
martonvago Jan 10, 2025
a08ce8b
apply suggestions from code review
martonvago Jan 13, 2025
1dd1e1e
refactor: :recycle: rename function to validation_errors_to_check_errors
martonvago Jan 13, 2025
c595aa9
refactor: :recycle: rename file to validation_errors_to_check_errors
martonvago Jan 13, 2025
cb49bd1
docs: :memo: add more detail to docstring
martonvago Jan 13, 2025
d603d82
refactor: :recycle: rename constant to PACKAGE_RECOMMENDED_FIELDS
martonvago Jan 14, 2025
abfc4c5
fix: :bug: include `data` in resource required fields
martonvago Jan 14, 2025
6611019
refactor: :recycle: make structure of PACKAGE_SPROUT_REQUIRED_FIELDS …
martonvago Jan 14, 2025
fea4a5f
refactor: :recycle: drop fields using pop
martonvago Jan 14, 2025
1cbec8a
apply suggestions from code review
martonvago Jan 14, 2025
c5b784f
refactor: :recycle: rename function to get_sprout_specific_resource_e…
martonvago Jan 14, 2025
c854a2d
refactor: :recycle: rename file to get_sprout_specific_resource_errors
martonvago Jan 14, 2025
ee5edf0
docs: :memo: update test names and docstrings
martonvago Jan 14, 2025
ee05805
Merge branch 'feat/check-error' into feat/sprout-checks-1-required-fi…
martonvago Jan 14, 2025
e43d13e
Merge branch 'feat/sprout-checks-1-required-fields' into feat/sprout-…
martonvago Jan 14, 2025
b174133
Merge branch 'feat/sprout-checks-2-simple-helper-functions' into feat…
martonvago Jan 14, 2025
562f93b
Merge branch 'feat/sprout-checks-3-required-and-blank-checks' into fe…
martonvago Jan 14, 2025
2391eed
Merge branch 'feat/sprout-checks-4-sprout-specific-package-and-resour…
martonvago Jan 14, 2025
569b947
refactor: :recycle: rename function to exclude_non_sprout_resource_er…
martonvago Jan 14, 2025
87418fb
refactor: :recycle: rename file to exclude_non_sprout_resource_errors
martonvago Jan 14, 2025
72db9d8
Merge branch 'feat/sprout-checks-2-simple-helper-functions' into feat…
martonvago Jan 14, 2025
99c783d
Merge branch 'feat/sprout-checks-3-required-and-blank-checks' into fe…
martonvago Jan 14, 2025
56d4cee
Merge branch 'feat/sprout-checks-4-sprout-specific-package-and-resour…
martonvago Jan 14, 2025
8bbdd4d
refactor: :recycle: correct function name after rename
martonvago Jan 14, 2025
accee38
Merge branch 'feat/sprout-checks-5-check-resource-properties' into fe…
martonvago Jan 14, 2025
0df3b4d
Merge branch 'feat/sprout-checks-6-check-package-properties' into fea…
martonvago Jan 14, 2025
9270df6
refactor: :recycle: correct function name after rename
martonvago Jan 14, 2025
144b12f
apply suggestions from code review
martonvago Jan 15, 2025
0e3a772
refactor: :recycle: update test names and docstrings
martonvago Jan 15, 2025
9e49ebb
apply suggestions from code review
martonvago Jan 15, 2025
cad7d43
docs: :memo: update test names and docstrings
martonvago Jan 17, 2025
f017c19
Merge branch 'feat/sprout-checks-3-required-and-blank-checks' into fe…
martonvago Jan 17, 2025
33d0062
Merge branch 'feat/sprout-checks-4-sprout-specific-package-and-resour…
martonvago Jan 17, 2025
6c90df7
refactor: :recycle: use ExceptionGroup instead of custom summary error
martonvago Jan 17, 2025
0a00856
docs: :memo: update test names and docstrings
martonvago Jan 17, 2025
02abdb5
docs: :memo: update test names and docstrings
martonvago Jan 17, 2025
a052837
apply suggestions from code review
martonvago Jan 17, 2025
5c1735a
Merge branch 'feat/sprout-checks-5-check-resource-properties' into fe…
martonvago Jan 17, 2025
7bfe5e6
docs: :memo: update test names and docstrings
martonvago Jan 17, 2025
e86e2c3
Merge branch 'feat/sprout-checks-5-check-resource-properties' into fe…
martonvago Jan 17, 2025
7bb74db
refactor: :recycle: use ExceptionGroup instead of custom summary error
martonvago Jan 17, 2025
eed70aa
docs: :memo: update test names and docstrings
martonvago Jan 17, 2025
42e40b6
apply suggestions from code review
martonvago Jan 17, 2025
4771b86
Merge branch 'feat/sprout-checks-6-check-package-properties' into fea…
martonvago Jan 17, 2025
4d0b812
refactor: :recycle: use ExceptionGroup instead of custom summary error
martonvago Jan 17, 2025
5350b2e
docs: :memo: update test names and docstrings
martonvago Jan 17, 2025
b33c387
Merge branch 'main' into feat/sprout-checks-2-simple-helper-functions
lwjohnst86 Jan 20, 2025
f0510d8
Merge branch 'main' into feat/sprout-checks-2-simple-helper-functions
lwjohnst86 Jan 20, 2025
7c19388
refactor: :recycle: rename functions
martonvago Jan 20, 2025
abc47e9
refactor: :recycle: rename files
martonvago Jan 20, 2025
a0d4897
refactor: :recycle: update error message and assertions
martonvago Jan 20, 2025
fd6282c
refactor: :recycle: pull out type error message into constant
martonvago Jan 20, 2025
7881d41
Merge branch 'feat/sprout-checks-2-simple-helper-functions' into feat…
martonvago Jan 20, 2025
f531760
refactor: :recycle: pull out error messages into constants
martonvago Jan 20, 2025
e5cefa8
refactor: :recycle: rename tests
martonvago Jan 20, 2025
cc9f4ba
Merge branch 'feat/sprout-checks-3-required-and-blank-checks' into fe…
martonvago Jan 20, 2025
8e6a91e
refactor: :recycle: fix imports after rename
martonvago Jan 20, 2025
9cbff8b
Merge branch 'feat/sprout-checks-4-sprout-specific-package-and-resour…
martonvago Jan 20, 2025
4039200
apply suggestions from code review
martonvago Jan 20, 2025
62ab91f
style: :art: fix linter
martonvago Jan 20, 2025
b8c7d1c
test: :white_check_mark: don't check error message in test
martonvago Jan 20, 2025
54e3a6c
Merge branch 'feat/sprout-checks-5-check-resource-properties' into fe…
martonvago Jan 20, 2025
cdbc133
apply suggestions from code review
martonvago Jan 20, 2025
845d9f2
style: :art: fix linter
martonvago Jan 20, 2025
2cfd170
Merge branch 'feat/sprout-checks-6-check-package-properties' into fea…
martonvago Jan 20, 2025
e06afa4
apply suggestions from code review
martonvago Jan 20, 2025
14d5376
Merge branch 'main' into feat/sprout-checks-7-check-properties
lwjohnst86 Jan 21, 2025
7335c8b
docs: :memo: update docstrings
martonvago Jan 21, 2025
feae835
revert: :rewind: commitizen seems to have updating other package vers…
lwjohnst86 Jan 21, 2025
4b1b833
Merge branch 'feat/sprout-checks-7-check-properties' of https://githu…
lwjohnst86 Jan 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions ruff.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Support Python 3.10+
target-version = "py310"
# Support Python 3.12+
target-version = "py312"

# In addition to the default formatters/linters, add these as well.
[lint]
Expand Down
23 changes: 23 additions & 0 deletions seedcase_sprout/core/checks/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""Check functions and constants for the Frictionless Data Package standard."""

from .check_error import CheckError
from .check_package_properties import check_package_properties
from .check_properties import check_properties
from .check_resource_properties import check_resource_properties
from .required_fields import (
PACKAGE_RECOMMENDED_FIELDS,
PACKAGE_REQUIRED_FIELDS,
RESOURCE_REQUIRED_FIELDS,
RequiredFieldType,
)

__all__ = [
"CheckError",
"check_properties",
"check_package_properties",
"check_resource_properties",
"PACKAGE_RECOMMENDED_FIELDS",
"PACKAGE_REQUIRED_FIELDS",
"RESOURCE_REQUIRED_FIELDS",
"RequiredFieldType",
]
5 changes: 4 additions & 1 deletion seedcase_sprout/core/checks/add_package_recommendations.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from seedcase_sprout.core.checks.config import NAME_PATTERN, SEMVER_PATTERN
from seedcase_sprout.core.checks.required_fields import (
PACKAGE_RECOMMENDED_FIELDS,
)


def add_package_recommendations(schema: dict) -> dict:
Expand All @@ -12,7 +15,7 @@ def add_package_recommendations(schema: dict) -> dict:
Returns:
The updated Data Package schema.
"""
schema["required"].extend(["name", "id", "licenses"])
schema["required"].extend(PACKAGE_RECOMMENDED_FIELDS.keys())
schema["properties"]["name"]["pattern"] = NAME_PATTERN
schema["properties"]["version"]["pattern"] = SEMVER_PATTERN
schema["properties"]["contributors"]["items"]["required"] = ["title"]
Expand Down
89 changes: 89 additions & 0 deletions seedcase_sprout/core/checks/check_error.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
from functools import total_ordering


@total_ordering
class CheckError(Exception):
"""Raised or returned when a properties object fails a single check."""

def __init__(
self,
message: str,
json_path: str,
validator: str,
):
"""Initialises CheckError.

Args:
message: The error message.
json_path: The path to the JSON field within the enclosing JSON object where
the error occurred.
validator: The name of the validator that failed.
"""
self.message = message
self.json_path = json_path
self.validator = validator
super().__init__(self.__str__())

def __eq__(self, other: object) -> bool:
"""Checks if this error is equal to an object.

Args:
other: The object to compare.

Returns:
If the objects are equal.
"""
if not isinstance(other, CheckError):
return NotImplemented
return (
self.message == other.message
and self.json_path == other.json_path
and self.validator == other.validator
)

def __lt__(self, other: object) -> bool:
"""Checks if this error is less than an object.

Args:
other: The object to compare.

Returns:
The result of the comparison.
"""
if not isinstance(other, CheckError):
return NotImplemented
return (self.json_path, self.validator, self.message) < (
other.json_path,
other.validator,
other.message,
)

def __hash__(self) -> int:
"""Returns a hash for this error.

Returns:
The hash.
"""
return hash((self.message, self.json_path, self.validator))

def __str__(self) -> str:
"""Returns a user-friendly string representation of the error.

Returns:
The string representation.
"""
return (
f"Error at `{self.json_path}` caused by `{self.validator}`: {self.message}"
)

def __repr__(self) -> str:
"""Returns a developer-friendly, unambiguous representation of the error.

Returns:
The developer-friendly representation.
"""
return (
f"CheckError(message={self.message!r}, "
f"json_path={self.json_path!r}, "
f"validator={self.validator!r})"
)
11 changes: 8 additions & 3 deletions seedcase_sprout/core/checks/check_object_against_json_schema.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
from jsonschema import Draft7Validator, FormatChecker, ValidationError
from jsonschema import Draft7Validator, FormatChecker

from seedcase_sprout.core.checks.check_error import CheckError
from seedcase_sprout.core.checks.validation_errors_to_check_errors import (
validation_errors_to_check_errors,
)


def check_object_against_json_schema(
json_object: dict, schema: dict
) -> list[ValidationError]:
) -> list[CheckError]:
"""Checks that `json_object` matches the given JSON schema.

Structural, type and format constraints are all checked. All schema violations are
Expand All @@ -21,4 +26,4 @@ def check_object_against_json_schema(
"""
Draft7Validator.check_schema(schema)
validator = Draft7Validator(schema, format_checker=FormatChecker())
return list(validator.iter_errors(json_object))
return validation_errors_to_check_errors(validator.iter_errors(json_object))
5 changes: 2 additions & 3 deletions seedcase_sprout/core/checks/check_package_properties.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
from jsonschema import ValidationError

from seedcase_sprout.core.checks.add_package_recommendations import (
add_package_recommendations,
)
from seedcase_sprout.core.checks.check_error import CheckError
from seedcase_sprout.core.checks.check_object_against_json_schema import (
check_object_against_json_schema,
)
Expand All @@ -14,7 +13,7 @@

def check_package_properties(
properties: dict, check_recommendations: bool = True
) -> list[ValidationError]:
) -> list[CheckError]:
"""Checks that `properties` matches the Data Package standard (v2.0).

Only package properties are checked. Schema constraints for resource properties are
Expand Down
5 changes: 2 additions & 3 deletions seedcase_sprout/core/checks/check_properties.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
from jsonschema import ValidationError

from seedcase_sprout.core.checks.add_package_recommendations import (
add_package_recommendations,
)
from seedcase_sprout.core.checks.add_resource_recommendations import (
add_resource_recommendations,
)
from seedcase_sprout.core.checks.check_error import CheckError
from seedcase_sprout.core.checks.check_object_against_json_schema import (
check_object_against_json_schema,
)
Expand All @@ -15,7 +14,7 @@

def check_properties(
properties: dict, check_recommendations: bool = True
) -> list[ValidationError]:
) -> list[CheckError]:
"""Checks that `properties` matches the Data Package standard (v2.0).

Both package and resource properties are checked. Structural, type and format
Expand Down
5 changes: 2 additions & 3 deletions seedcase_sprout/core/checks/check_resource_properties.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
from jsonschema import ValidationError

from seedcase_sprout.core.checks.add_resource_recommendations import (
add_resource_recommendations,
)
from seedcase_sprout.core.checks.check_error import CheckError
from seedcase_sprout.core.checks.check_object_against_json_schema import (
check_object_against_json_schema,
)
Expand All @@ -12,7 +11,7 @@

def check_resource_properties(
properties: dict, check_recommendations: bool = True
) -> list[ValidationError]:
) -> list[CheckError]:
"""Checks that the resource `properties` matches the Data Resource standard (v2.0).

This function expects an individual set of resource properties as input. Structural,
Expand Down
21 changes: 21 additions & 0 deletions seedcase_sprout/core/checks/get_full_json_path_from_error.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import re

from jsonschema import ValidationError


def get_full_json_path_from_error(error: ValidationError) -> str:
"""Returns the full `json_path` to the error.

For 'required' errors, the field name is extracted from the error message.

Args:
error: The error to get the full `json_path` for.

Returns:
The full`json_path` of the error.
"""
if error.validator == "required":
match = re.search("'(.*)' is a required property", error.message)
if match:
return f"{error.json_path}.{match.group(1)}"
return error.json_path
28 changes: 28 additions & 0 deletions seedcase_sprout/core/checks/required_fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from enum import Enum

# Data Package standard required fields and their types


class RequiredFieldType(str, Enum):
"""A class enumerating allowed types for required fields."""

str = "str"
list = "list"
any = "any"


PACKAGE_REQUIRED_FIELDS = {
"resources": RequiredFieldType.list,
}

PACKAGE_RECOMMENDED_FIELDS = {
"name": RequiredFieldType.str,
"id": RequiredFieldType.str,
"licenses": RequiredFieldType.list,
}

RESOURCE_REQUIRED_FIELDS = {
"name": RequiredFieldType.str,
"path": RequiredFieldType.str,
"data": RequiredFieldType.any,
}
18 changes: 18 additions & 0 deletions seedcase_sprout/core/checks/unwrap_errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from jsonschema import ValidationError


def unwrap_errors(errors: list[ValidationError]) -> list[ValidationError]:
"""Recursively extracts all errors into a flat list of errors.

Args:
errors: A nested list of errors.

Returns:
A flat list of errors.
"""
unwrapped = []
for error in errors:
unwrapped.append(error)
if error.context:
unwrapped.extend(unwrap_errors(error.context))
return unwrapped
41 changes: 41 additions & 0 deletions seedcase_sprout/core/checks/validation_errors_to_check_errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from typing import Iterator

from jsonschema import ValidationError

from seedcase_sprout.core.checks.check_error import CheckError
from seedcase_sprout.core.checks.get_full_json_path_from_error import (
get_full_json_path_from_error,
)
from seedcase_sprout.core.checks.unwrap_errors import unwrap_errors

COMPLEX_VALIDATORS = {"allOf", "anyOf", "oneOf"}


def validation_errors_to_check_errors(
validation_errors: Iterator[ValidationError],
) -> list[CheckError]:
"""Transforms `jsonschema.ValidationError`s to more compact `CheckError`s.

The list of errors is:

- flattened
- filtered for summary-type errors
- filtered for duplicates
- sorted by error location

Args:
validation_errors: The `jsonschema.ValidationError`s to transform.

Returns:
A list of `CheckError`s.
"""
check_errors = [
CheckError(
message=error.message,
json_path=get_full_json_path_from_error(error),
validator=error.validator,
)
for error in unwrap_errors(list(validation_errors))
if error.validator not in COMPLEX_VALIDATORS
]
return sorted(set(check_errors))
29 changes: 29 additions & 0 deletions seedcase_sprout/core/sprout_checks/check_data_path_string.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from seedcase_sprout.core.checks.check_error import CheckError
from seedcase_sprout.core.sprout_checks.get_json_path_to_resource_field import (
get_json_path_to_resource_field,
)


def check_data_path_string(
properties: dict, index: int | None = None
) -> list[CheckError]:
"""Checks that the `path` field of a set of resource properties is of type string.

Args:
properties: The resource properties.
index: The index of the resource properties. Defaults to None.

Returns:
A list of errors. An empty list if no error was found.
"""
path = properties.get("path", "")
if isinstance(path, str):
return []

return [
CheckError(
message=f"{path} is not of type 'string'",
json_path=get_json_path_to_resource_field("path", index),
validator="type",
)
]
Loading