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

refactor: ♻️ user Properties classes in user facing functions #976

Merged
2 changes: 1 addition & 1 deletion docs/guide/packages.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ properties, you can use the `update_package_properties()` function:
``` {python}
package_properties = sp.edit_package_properties(
path=sp.path_package_properties(package_id=1),
properties=properties.compact_dict
properties=properties,
)
pprint(package_properties)
```
Expand Down
17 changes: 15 additions & 2 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ frictionless = {extras = ["sql"], version = "^5.17.0"}
requests = "^2.32.3"
platformdirs = "^4.3.2"
jsonschema = "^4.23.0"
dacite = "^1.8.1"

[tool.poetry.group.test.dependencies]
pytest = "^8.3.2"
Expand Down
10 changes: 8 additions & 2 deletions seedcase_sprout/core/create_resource_properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@
create_relative_resource_data_path,
)
from seedcase_sprout.core.edit_property_field import edit_property_field
from seedcase_sprout.core.properties import ResourceProperties
from seedcase_sprout.core.verify_properties_are_well_formed import (
verify_properties_are_well_formed,
)


def create_resource_properties(path: Path, properties: dict) -> dict:
def create_resource_properties(
path: Path, properties: ResourceProperties
) -> ResourceProperties:
"""Creates a valid properties object for the specified resource.

This function sets up and structures a new resource property by taking
Expand All @@ -37,7 +40,10 @@ def create_resource_properties(path: Path, properties: dict) -> dict:
NotPropertiesError: If properties are not correct Frictionless
resource properties.
"""
properties = properties.compact_dict
check_is_dir(path)
verify_properties_are_well_formed(properties, ResourceError.type)
data_path = create_relative_resource_data_path(path)
return edit_property_field(properties, "path", str(data_path))
edited_properties = edit_property_field(properties, "path", str(data_path))

return ResourceProperties.from_dict(edited_properties)
11 changes: 9 additions & 2 deletions seedcase_sprout/core/edit_package_properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from frictionless.errors import PackageError

from seedcase_sprout.core.check_is_file import check_is_file
from seedcase_sprout.core.properties import PackageProperties
from seedcase_sprout.core.read_json import read_json
from seedcase_sprout.core.verify_package_properties import (
verify_package_properties,
Expand All @@ -12,7 +13,9 @@
)


def edit_package_properties(path: Path, properties: dict) -> dict:
def edit_package_properties(
path: Path, properties: PackageProperties
) -> PackageProperties:
"""Edits the properties of an existing package.

Use this any time you want to edit the package's properties and particularly
Expand Down Expand Up @@ -47,6 +50,8 @@ def edit_package_properties(path: Path, properties: dict) -> dict:
package properties are not well-formed.
JSONDecodeError: If the `datapackage.json` file couldn't be read.
"""
properties = properties.compact_dict

check_is_file(path)
verify_properties_are_well_formed(properties, PackageError.type)

Expand All @@ -55,4 +60,6 @@ def edit_package_properties(path: Path, properties: dict) -> dict:

current_properties.update(properties)

return verify_package_properties(current_properties)
verify_package_properties(current_properties)

return PackageProperties.from_dict(current_properties)
15 changes: 15 additions & 0 deletions seedcase_sprout/core/properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
from typing import Any, Literal, Self
from uuid import uuid4

from dacite import from_dict

from seedcase_sprout.core.get_iso_timestamp import get_iso_timestamp


Expand Down Expand Up @@ -35,6 +37,19 @@ def compact_dict(self) -> dict:
},
)

@classmethod
def from_dict(cls: type[Self], data: dict) -> Self:
"""Creates an instance populated with data from a dictionary.

Args:
cls: The class to create an instance of.
data: The data to populate the instance with.

Returns:
An instance of the class with the properties from the dictionary.
"""
return from_dict(data_class=cls, data=data)


@dataclass
class ContributorProperties(Properties):
Expand Down
6 changes: 5 additions & 1 deletion seedcase_sprout/core/write_resource_properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@

from seedcase_sprout.core.check_data_path import check_data_path
from seedcase_sprout.core.check_is_file import check_is_file
from seedcase_sprout.core.properties import ResourceProperties
from seedcase_sprout.core.read_json import read_json
from seedcase_sprout.core.verify_package_properties import verify_package_properties
from seedcase_sprout.core.verify_resource_properties import verify_resource_properties
from seedcase_sprout.core.write_json import write_json


def write_resource_properties(path: Path, resource_properties: dict) -> Path:
def write_resource_properties(
path: Path, resource_properties: ResourceProperties
) -> Path:
"""Writes the specified resource properties to the `datapackage.json` file.

This functions verifies `resource_properties`, and if a
Expand All @@ -29,6 +32,7 @@ def write_resource_properties(path: Path, resource_properties: dict) -> Path:
they are incomplete or don't follow the Data Package specification.
JSONDecodeError: If the `datapackage.json` file couldn't be read.
"""
resource_properties = resource_properties.compact_dict
check_is_file(path)
verify_resource_properties(resource_properties)

Expand Down
42 changes: 23 additions & 19 deletions tests/core/test_create_resource_properties.py
Original file line number Diff line number Diff line change
@@ -1,40 +1,44 @@
from pathlib import Path

from pytest import raises
from pytest import fixture, raises

from seedcase_sprout.core.create_resource_properties import create_resource_properties
from seedcase_sprout.core.not_properties_error import NotPropertiesError
from seedcase_sprout.core.properties import ResourceProperties


def test_creates_properties_correctly(tmp_path):
@fixture
def resource_properties():
return ResourceProperties(
name="resource-name",
path="",
)


def test_creates_properties_correctly(tmp_path, resource_properties):
"""Given valid inputs, should create properties object correctly."""
resource_path = tmp_path / "resources" / "1"
resource_path.mkdir(parents=True)
properties = {
"name": "test",
"path": "",
}
signekb marked this conversation as resolved.
Show resolved Hide resolved
expected_properties = {
"name": "test",
"path": str(Path("resources", "1", "data.parquet")),
}

assert create_resource_properties(resource_path, properties) == expected_properties
expected_properties = ResourceProperties(
name="resource-name", path=str(Path("resources", "1", "data.parquet"))
)

assert (
create_resource_properties(resource_path, resource_properties)
== expected_properties
)


def test_rejects_path_if_invalid(tmp_path):
def test_rejects_path_if_invalid(tmp_path, resource_properties):
"""Given an invalid path input, should raise NotADirectoryError."""
resource_path = tmp_path / "nonexistent"
properties = {
"name": "test",
"path": "",
}

with raises(NotADirectoryError):
create_resource_properties(resource_path, properties)
create_resource_properties(resource_path, resource_properties)


def test_rejects_properties_if_incorrect(tmp_path):
"""Given an incorrect properties input, should raise NotPropertiesError."""
"""Given an incorrect/empty properties input, should raise NotPropertiesError."""
with raises(NotPropertiesError):
create_resource_properties(tmp_path, {})
create_resource_properties(tmp_path, ResourceProperties())
27 changes: 13 additions & 14 deletions tests/core/test_edit_package_properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def properties():
description="This is my package.",
version="2.0.0",
created="2024-05-14T05:00:01+00:00",
).compact_dict
)


@fixture
Expand Down Expand Up @@ -48,20 +48,22 @@ def test_edits_only_changed_package_properties(properties_path, properties):
current_properties = read_json(properties_path)

# When, Then
expected_properties = current_properties | properties
assert edit_package_properties(properties_path, properties) == expected_properties
expected_properties = current_properties | properties.compact_dict
assert edit_package_properties(
properties_path, properties
) == PackageProperties.from_dict(expected_properties)


def test_throws_error_if_path_points_to_dir(tmp_path):
"""Should throw FileNotFoundError if the path points to a folder."""
with raises(FileNotFoundError):
edit_package_properties(tmp_path, {})
edit_package_properties(tmp_path, PackageProperties())


def test_throws_error_if_path_points_to_nonexistent_file(tmp_path):
"""Should throw FileNotFoundError if the path points to a nonexistent file."""
with raises(FileNotFoundError):
edit_package_properties(tmp_path / "datapackage.json", {})
edit_package_properties(tmp_path / "datapackage.json", PackageProperties())


def test_throws_error_if_properties_file_cannot_be_read(tmp_path, properties):
Expand All @@ -83,22 +85,19 @@ def test_throws_error_if_current_package_properties_are_malformed(tmp_path, prop
edit_package_properties(path, properties)


def test_adds_custom_fields(
def test_throws_error_when_adding_custom_fields(
signekb marked this conversation as resolved.
Show resolved Hide resolved
properties_path,
):
"""Should add custom fields to properties."""
"""Should not accept custom fields to properties."""
# Given
current_properties = read_json(properties_path)
new_properties = {"custom-field": "custom-value"}
new_properties = PackageProperties.from_dict({"custom-field": "custom-value"})

# When, Then
assert (
edit_package_properties(properties_path, new_properties)
== current_properties | new_properties
)
with raises(NotPropertiesError):
assert edit_package_properties(properties_path, new_properties)
signekb marked this conversation as resolved.
Show resolved Hide resolved


def test_throws_error_if_new_properties_are_empty(properties_path):
"""Should throw NotPropertiesError if the new properties are empty."""
with raises(NotPropertiesError):
edit_package_properties(properties_path, {})
edit_package_properties(properties_path, PackageProperties())
38 changes: 38 additions & 0 deletions tests/core/test_properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,41 @@ def test_creates_package_properties_with_correct_defaults(mock_uuid):
assert properties.id == str(mock_uuid())
assert properties.version == "0.1.0"
assert properties.created == "2024-05-14T05:00:01+00:00"


@mark.parametrize(
"dict, expected_properties",
signekb marked this conversation as resolved.
Show resolved Hide resolved
[
({"family_name": "Doe"}, ContributorProperties(family_name="Doe")),
({"name": "a licence"}, LicenseProperties(name="a licence")),
({"title": "a source"}, SourceProperties(title="a source")),
({"header": True}, TableDialectProperties(header=True)),
({"resource": "a resource"}, ReferenceProperties(resource="a resource")),
({"fields": ["a field"]}, TableSchemaForeignKeyProperties(fields=["a field"])),
({"value": "NA"}, MissingValueProperties(value="NA")),
({"required": False}, ConstraintsProperties(required=False)),
({"name": "a field name"}, FieldProperties(name="a field name")),
({"fields_match": "exact"}, TableSchemaProperties(fields_match="exact")),
(
{
"name": "resource name",
"licenses": [{"name": "MIT"}],
},
ResourceProperties(
name="resource name", licenses=[LicenseProperties(name="MIT")]
),
),
(
{"version": "1.0.0", "contributors": [{"family_name": "Doe"}]},
PackageProperties(
version="1.0.0", contributors=[ContributorProperties(family_name="Doe")]
),
),
],
)
def test_transforms_dict_to_properties(dict, expected_properties):
"""Should transform a (nested) dictionary to a properties object."""
properties_cls = type(expected_properties)
properties = properties_cls.from_dict(dict)

assert properties == expected_properties
Loading
Loading