From 576074c5ead087f3c58bf1c7876d5f921a473ade Mon Sep 17 00:00:00 2001 From: coffeepy Date: Thu, 8 May 2025 10:34:23 -0500 Subject: [PATCH 1/3] feat: implement metadata field models and mutations in MetadataSpec --- docs/CHANGELOG.md | 18 ++ pythonik/models/__init__.py | 21 ++ pythonik/models/metadata/__init__.py | 16 ++ pythonik/models/metadata/fields.py | 73 +++++++ pythonik/specs/metadata.py | 75 ++++++- pythonik/tests/test_metadata.py | 281 +++++++++++++++++++++++++++ 6 files changed, 483 insertions(+), 1 deletion(-) create mode 100644 pythonik/models/metadata/fields.py diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 01f3e03..6d993bc 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## 2025-05-08 "Metadata Field Management & Type Safety" - version 1.11.0 + +### Added +- **Metadata Field Management (in `pythonik.specs.metadata.MetadataSpec`):** + - `create_metadata_field()`: Added method to create new metadata fields. + - `update_metadata_field()`: Added method to update existing metadata fields by name. + - `delete_metadata_field()`: Added method to delete metadata fields by name. +- **Type Safety & Modeling (in `pythonik.models.metadata.fields`):** + - Defined `IconikFieldType` Enum to represent all known metadata field types from the Iconik API. + - Integrated `IconikFieldType` into `FieldCreate`, `FieldUpdate`, and `Field` Pydantic models for robust type validation and clarity. +- **Testing:** + - Added comprehensive test coverage for metadata field type handling and new CRUD operations: + - Parameterized test (`test_create_metadata_field_for_all_types`) for every defined `IconikFieldType`. + - Test (`test_create_metadata_field_with_unknown_type_raises_validation_error`) for API returning unrecognized `field_type`. + +### Technical Details +This update introduces full programmatic management of metadata fields, including creation, update, and deletion capabilities. A dedicated Enum (`IconikFieldType`) enhances type safety and developer experience when working with these fields. This Enum is integrated throughout the relevant Pydantic models and the new `MetadataSpec` methods. Comprehensive tests ensure correct handling of all defined field types, robust behavior against unknown types from the API, and functionality of the new CRUD operations. + ## 2025-05-02 "Segment Deletion" - version 1.10.0 ### Added diff --git a/pythonik/models/__init__.py b/pythonik/models/__init__.py index e69de29..1f4d703 100644 --- a/pythonik/models/__init__.py +++ b/pythonik/models/__init__.py @@ -0,0 +1,21 @@ +from .metadata import ( + Field, + FieldCreate, + FieldUpdate, + FieldOption, + CreateViewRequest, + UpdateViewRequest, + View, + ViewMetadata, +) + +__all__ = [ + "Field", + "FieldCreate", + "FieldUpdate", + "FieldOption", + "CreateViewRequest", + "UpdateViewRequest", + "View", + "ViewMetadata", +] \ No newline at end of file diff --git a/pythonik/models/metadata/__init__.py b/pythonik/models/metadata/__init__.py index e69de29..168a8c9 100644 --- a/pythonik/models/metadata/__init__.py +++ b/pythonik/models/metadata/__init__.py @@ -0,0 +1,16 @@ +# pythonik/models/metadata/__init__.py +from .fields import Field, FieldCreate, FieldUpdate, FieldOption +from .views import CreateViewRequest, UpdateViewRequest, View, ViewMetadata + +__all__ = [ + # From fields.py + "Field", + "FieldCreate", + "FieldUpdate", + "FieldOption", + # From views.py + "CreateViewRequest", + "UpdateViewRequest", + "View", + "ViewMetadata", +] \ No newline at end of file diff --git a/pythonik/models/metadata/fields.py b/pythonik/models/metadata/fields.py new file mode 100644 index 0000000..ed65729 --- /dev/null +++ b/pythonik/models/metadata/fields.py @@ -0,0 +1,73 @@ +# pythonik/models/metadata/fields.py +from typing import List, Optional +from datetime import datetime +from enum import Enum +from pydantic import BaseModel, HttpUrl + +class IconikFieldType(str, Enum): + """Known Iconik metadata field types based on documentation. + Actual values sent to/received from API. + """ + STRING = "string" # General short text + TEXT = "text" # Longer text (potentially multi-line in UI but distinct type) + TEXT_AREA = "text_area" # Explicitly for larger amounts of text data + INTEGER = "integer" + FLOAT = "float" + BOOLEAN = "boolean" # For Yes/No fields + DATE = "date" + DATETIME = "datetime" # For Date Time fields + DROPDOWN = "drop_down" # For fields with predefined options + EMAIL = "email" + TAG_CLOUD = "tag_cloud" # For free-form tag collections + URL = "url" + +class FieldOption(BaseModel): + """Represents an option for a metadata field (e.g., for dropdowns).""" + label: Optional[str] = None + value: Optional[str] = None + +class _FieldConfigurable(BaseModel): + """Base model for common configurable attributes of metadata fields.""" + label: Optional[str] = None + field_type: Optional[IconikFieldType] = None + description: Optional[str] = None + options: Optional[List[FieldOption]] = None + required: Optional[bool] = None + auto_set: Optional[bool] = None + hide_if_not_set: Optional[bool] = None + is_block_field: Optional[bool] = None + is_warning_field: Optional[bool] = None + multi: Optional[bool] = None + read_only: Optional[bool] = None + representative: Optional[bool] = None + sortable: Optional[bool] = None + use_as_facet: Optional[bool] = None + min_value: Optional[float] = None + max_value: Optional[float] = None + external_id: Optional[str] = None + source_url: Optional[HttpUrl] = None + +class FieldCreate(_FieldConfigurable): + """Data Transfer Object for creating a new metadata field.""" + name: str + label: str + field_type: IconikFieldType + +class FieldUpdate(_FieldConfigurable): + """ + Data Transfer Object for updating an existing metadata field. + All fields are optional to support partial updates. + 'name' is specified in the URL path for updates, not in the body. + """ + pass + +class Field(_FieldConfigurable): + """Represents a metadata field as returned by the API.""" + id: str + name: str + label: str + field_type: IconikFieldType + + date_created: Optional[datetime] = None + date_modified: Optional[datetime] = None + mapped_field_name: Optional[str] = None diff --git a/pythonik/specs/metadata.py b/pythonik/specs/metadata.py index c30970e..8544e25 100644 --- a/pythonik/specs/metadata.py +++ b/pythonik/specs/metadata.py @@ -10,6 +10,7 @@ UpdateMetadata, UpdateMetadataResponse, ) +from pythonik.models.metadata.fields import Field, FieldCreate, FieldUpdate from pythonik.specs.base import Spec from typing import Literal, Union, Dict, Any, List @@ -27,6 +28,10 @@ UPDATE_VIEW_PATH = GET_VIEW_PATH DELETE_VIEW_PATH = GET_VIEW_PATH +# Field paths +FIELDS_BASE_PATH = "fields/" +FIELD_BY_NAME_PATH = "fields/{field_name}/" + ObjectType = Literal["segments"] @@ -383,7 +388,7 @@ def delete_view(self, view_id: str, **kwargs) -> Response: - can_delete_metadata_views Returns: - Response: Empty response with 204 status code + Response: An empty response, expecting HTTP 204 No Content on success. Raises: - 400 Bad request @@ -392,3 +397,71 @@ def delete_view(self, view_id: str, **kwargs) -> Response: """ resp = self._delete(DELETE_VIEW_PATH.format(view_id=view_id), **kwargs) return self.parse_response(resp, None) + + # Metadata Field Management + # ------------------------- + + def create_metadata_field( + self, + field_data: FieldCreate, + exclude_defaults: bool = True, + **kwargs, + ) -> Response: + """Create a new metadata field. + + Args: + field_data: The data for the new field. + exclude_defaults: Whether to exclude default values when dumping Pydantic models. + **kwargs: Additional kwargs to pass to the request. + + Returns: + Response: The created metadata field. + """ + json_data = self._prepare_model_data( + field_data, exclude_defaults=exclude_defaults + ) + resp = self._post(FIELDS_BASE_PATH, json=json_data, **kwargs) + return self.parse_response(resp, Field) + + def update_metadata_field( + self, + field_name: str, + field_data: FieldUpdate, + exclude_defaults: bool = True, + **kwargs, + ) -> Response: + """Update an existing metadata field by its name. + + Args: + field_name: The name of the field to update. + field_data: The data to update the field with. + exclude_defaults: Whether to exclude default values when dumping Pydantic models. + **kwargs: Additional kwargs to pass to the request. + + Returns: + Response: The updated metadata field. + """ + json_data = self._prepare_model_data( + field_data, exclude_defaults=exclude_defaults + ) + endpoint = FIELD_BY_NAME_PATH.format(field_name=field_name) + resp = self._put(endpoint, json=json_data, **kwargs) + return self.parse_response(resp, Field) + + def delete_metadata_field( + self, + field_name: str, + **kwargs, + ) -> Response: + """Delete a metadata field by its name. + + Args: + field_name: The name of the field to delete. + **kwargs: Additional kwargs to pass to the request. + + Returns: + Response: An empty response, expecting HTTP 204 No Content on success. + """ + endpoint = FIELD_BY_NAME_PATH.format(field_name=field_name) + resp = self._delete(endpoint, **kwargs) + return self.parse_response(resp) diff --git a/pythonik/tests/test_metadata.py b/pythonik/tests/test_metadata.py index 96834fc..10d65f8 100644 --- a/pythonik/tests/test_metadata.py +++ b/pythonik/tests/test_metadata.py @@ -4,6 +4,7 @@ from requests import HTTPError from pythonik.models.base import ObjectType from loguru import logger +import json from pythonik.models.metadata.views import ( FieldValue, @@ -31,8 +32,15 @@ UPDATE_VIEW_PATH, DELETE_VIEW_PATH, GET_VIEW_PATH, + FIELDS_BASE_PATH, + FIELD_BY_NAME_PATH, ) +from pythonik.models import Field, FieldCreate, FieldUpdate, FieldOption +import pytest +from pythonik.models.metadata.fields import IconikFieldType +from pydantic import ValidationError + def test_get_asset_metadata(): with requests_mock.Mocker() as m: @@ -1323,3 +1331,276 @@ def test_get_view_alternate_base_url(): logger.info(m.last_request.url) logger.info(mock_address) assert m.last_request.url == mock_address + + +def test_create_metadata_field(requests_mock): + app_id = str(uuid.uuid4()) + auth_token = str(uuid.uuid4()) + client = PythonikClient(app_id=app_id, auth_token=auth_token, timeout=3) # Client instance + spec_instance = client.metadata() # MetadataSpec instance + + field_name = "new_test_field_create" + field_create_payload = FieldCreate( + name=field_name, + label="New Test Field Create", + field_type="string", + options=[FieldOption(label="Option 1", value="opt1_val_create")] + ) + expected_field_response = Field( + id="generated_uuid_for_new_field_create", + name=field_name, + label="New Test Field Create", + field_type="string", + options=[FieldOption(label="Option 1", value="opt1_val_create")], + date_created="2025-01-01T00:00:00Z", + date_modified="2025-01-01T00:00:00Z", + ) + + # mock_address = MetadataSpec.gen_url(FIELDS_BASE_PATH) # Old way (classmethod, relative URL) + mock_address = spec_instance.gen_url(FIELDS_BASE_PATH) # New way (instance method, absolute URL) + requests_mock.post( + mock_address, json=json.loads(expected_field_response.model_dump_json()), status_code=201 + ) + + # client = PythonikClient(app_id=app_id, auth_token=auth_token, timeout=3) # Already created above + result = spec_instance.create_metadata_field(field_create_payload) # Use the same spec_instance + + assert result.response.ok + assert result.response.status_code == 201 + assert result.data.name == expected_field_response.name + assert result.data.label == expected_field_response.label + assert len(result.data.options) == 1 + assert result.data.options[0].label == "Option 1" + + +def test_update_metadata_field(requests_mock): + app_id = str(uuid.uuid4()) + auth_token = str(uuid.uuid4()) + field_to_update_name = "existing_field_for_update_test" + field_update_payload = FieldUpdate( + label="Updated Label for Field Test", + description="Updated description test.", + ) + expected_field_response = Field( + id="uuid_of_existing_field_update_test", + name=field_to_update_name, + label="Updated Label for Field Test", + description="Updated description test.", + field_type="string", + options=[], + date_created="2024-01-01T00:00:00Z", + date_modified="2025-01-01T00:00:00Z", + ) + + mock_address = MetadataSpec.gen_url( + FIELD_BY_NAME_PATH.format(field_name=field_to_update_name) + ) + requests_mock.put( + mock_address, json=json.loads(expected_field_response.model_dump_json()), status_code=200 + ) + + client = PythonikClient(app_id=app_id, auth_token=auth_token, timeout=3) + result = client.metadata().update_metadata_field(field_to_update_name, field_update_payload) + + assert result.response.ok + assert result.response.status_code == 200 + assert result.data.name == field_to_update_name + assert result.data.label == "Updated Label for Field Test" + assert result.data.description == "Updated description test." + + +def test_delete_metadata_field(requests_mock): + app_id = str(uuid.uuid4()) + auth_token = str(uuid.uuid4()) + field_to_delete_name = "field_marked_for_deletion_test" + + mock_address = MetadataSpec.gen_url( + FIELD_BY_NAME_PATH.format(field_name=field_to_delete_name) + ) + requests_mock.delete(mock_address, status_code=204) + + client = PythonikClient(app_id=app_id, auth_token=auth_token, timeout=3) + result = client.metadata().delete_metadata_field(field_to_delete_name) + + assert result.response.ok + assert result.response.status_code == 204 + + +def test_create_metadata_field_conflict_error(requests_mock): + app_id = str(uuid.uuid4()) + auth_token = str(uuid.uuid4()) + field_create_payload = FieldCreate(name="existing_name", label="Fail Label", field_type="string") + mock_address = MetadataSpec.gen_url(FIELDS_BASE_PATH) + requests_mock.post(mock_address, json={"error": "conflict"}, status_code=409) + client = PythonikClient(app_id=app_id, auth_token=auth_token, timeout=3) + result = client.metadata().create_metadata_field(field_create_payload) + assert not result.response.ok + assert result.response.status_code == 409 + + +def test_update_metadata_field_not_found_error(requests_mock): + app_id = str(uuid.uuid4()) + auth_token = str(uuid.uuid4()) + field_update_payload = FieldUpdate(label="NonExistent Update") + non_existent_field = "non_existent_field_name" + mock_address = MetadataSpec.gen_url(FIELD_BY_NAME_PATH.format(field_name=non_existent_field)) + requests_mock.put(mock_address, json={"error": "not found"}, status_code=404) + client = PythonikClient(app_id=app_id, auth_token=auth_token, timeout=3) + result = client.metadata().update_metadata_field(non_existent_field, field_update_payload) + assert not result.response.ok + assert result.response.status_code == 404 + + +def test_delete_metadata_field_not_found_error(requests_mock): + app_id = str(uuid.uuid4()) + auth_token = str(uuid.uuid4()) + non_existent_field = "another_non_existent_field" + mock_address = MetadataSpec.gen_url(FIELD_BY_NAME_PATH.format(field_name=non_existent_field)) + requests_mock.delete(mock_address, json={"error": "not found"}, status_code=404) + client = PythonikClient(app_id=app_id, auth_token=auth_token, timeout=3) + result = client.metadata().delete_metadata_field(non_existent_field) + assert not result.response.ok + assert result.response.status_code == 404 + + +@pytest.mark.parametrize("field_type_enum", list(IconikFieldType)) +def test_create_metadata_field_for_all_types(requests_mock, field_type_enum: IconikFieldType): + """Test creating a metadata field for each IconikFieldType.""" + app_id = str(uuid.uuid4()) + auth_token = str(uuid.uuid4()) + client = PythonikClient(app_id=app_id, auth_token=auth_token, timeout=3) + spec_instance = client.metadata() + + field_name = f"test_sdk_{field_type_enum.value.lower()}_{str(uuid.uuid4())[:8]}" + field_label = f"Test SDK {field_type_enum.name.replace('_', ' ').title()} Field" + + field_create_payload = { + "name": field_name, + "label": field_label, + "field_type": field_type_enum, # Use the Enum member directly + } + + if field_type_enum == IconikFieldType.DROPDOWN: + field_create_payload["options"] = [FieldOption(label="Opt1", value="val1")] + + # Prepare Pydantic model for request + field_data_model = FieldCreate(**field_create_payload) + + # Expected response from API (API returns string value for field_type) + expected_response_json = { + "id": str(uuid.uuid4()), + "name": field_name, + "label": field_label, + "field_type": field_type_enum.value, # API returns the string value + "date_created": "2023-01-01T12:00:00Z", + "date_modified": "2023-01-01T12:00:00Z", + # ... other fields with default/null values as per Field model + "description": None, + "options": field_create_payload.get("options", []), + "required": False, + "auto_set": False, + "hide_if_not_set": False, + "is_block_field": False, + "is_warning_field": False, + "multi": False, + "read_only": False, + "representative": False, + "sortable": False, + "use_as_facet": False, + "min_value": None, + "max_value": None, + "external_id": None, + "source_url": None, + "mapped_field_name": None, + } + if field_type_enum == IconikFieldType.DROPDOWN: + # Pydantic model for options will be list of FieldOption objects + # API mock should return list of dicts + expected_response_json["options"] = [{"label": "Opt1", "value": "val1"}] + else: + expected_response_json["options"] = [] + + + create_url = spec_instance.gen_url(FIELDS_BASE_PATH) + requests_mock.post(create_url, json=expected_response_json, status_code=201) + + # Call the method under test + response = spec_instance.create_metadata_field(field_data=field_data_model) + + assert response.response.ok + assert response.response.status_code == 201 + assert response.data is not None + assert response.data.name == field_name + assert response.data.label == field_label + assert response.data.field_type == field_type_enum # Pydantic model converts back to Enum + + # Optional: Mock and test deletion for cleanup (good practice) + delete_url = spec_instance.gen_url(FIELD_BY_NAME_PATH.format(field_name=field_name)) + requests_mock.delete(delete_url, status_code=204) + delete_response = spec_instance.delete_metadata_field(field_name) + assert delete_response.response.ok + assert delete_response.response.status_code == 204 + + +def test_create_metadata_field_with_unknown_type_raises_validation_error(requests_mock): + """ + Test that fetching a field with an unknown field_type string + from the API raises a Pydantic ValidationError during response parsing. + """ + app_id = str(uuid.uuid4()) + auth_token = str(uuid.uuid4()) + client = PythonikClient(app_id=app_id, auth_token=auth_token, timeout=3) + spec_instance = client.metadata() + + field_name = f"test_sdk_unknown_type_{str(uuid.uuid4())[:8]}" + field_label = "Test SDK Unknown Type Field" + unknown_type_string = "some_future_unrecognized_type" + + # Data for creating the field - this part uses a valid IconikFieldType for the request + field_data_model = FieldCreate( + name=field_name, + label=field_label, + field_type=IconikFieldType.STRING # A valid type for the request itself + ) + + # Mocked API response containing the unknown field_type in its body + mock_api_response_json = { + "id": str(uuid.uuid4()), + "name": field_name, + "label": field_label, + "field_type": unknown_type_string, # This is the unexpected value from API + "date_created": "2023-01-01T12:00:00Z", + "date_modified": "2023-01-01T12:00:00Z", + # Fill in other required fields for Field model based on its definition + "description": None, + "options": [], + "required": False, + "auto_set": False, + "hide_if_not_set": False, + "is_block_field": False, + "is_warning_field": False, + "multi": False, + "read_only": False, + "representative": False, + "sortable": False, + "use_as_facet": False, + "min_value": None, + "max_value": None, + "external_id": None, + "source_url": None, + "mapped_field_name": None, + } + + create_url = spec_instance.gen_url(FIELDS_BASE_PATH) + # We mock a successful creation (201) but with a problematic field_type in the response body + requests_mock.post(create_url, json=mock_api_response_json, status_code=201) + + # The ValidationError should be raised when parse_response (called by create_metadata_field) + # tries to fit unknown_type_string into IconikFieldType. + with pytest.raises(ValidationError) as excinfo: + spec_instance.create_metadata_field(field_data=field_data_model) + + # Check that the error message mentions 'field_type' and the unknown value + error_str = str(excinfo.value).lower() + assert "field_type" in error_str + assert f"'{unknown_type_string}'" in error_str or unknown_type_string in error_str From 1db5415daa43ae95214ee6284b4e69d4518e933f Mon Sep 17 00:00:00 2001 From: coffeepy Date: Thu, 8 May 2025 10:38:08 -0500 Subject: [PATCH 2/3] remove init py conveniences for now --- pythonik/models/__init__.py | 21 --------------------- pythonik/models/metadata/__init__.py | 16 ---------------- 2 files changed, 37 deletions(-) diff --git a/pythonik/models/__init__.py b/pythonik/models/__init__.py index 1f4d703..e69de29 100644 --- a/pythonik/models/__init__.py +++ b/pythonik/models/__init__.py @@ -1,21 +0,0 @@ -from .metadata import ( - Field, - FieldCreate, - FieldUpdate, - FieldOption, - CreateViewRequest, - UpdateViewRequest, - View, - ViewMetadata, -) - -__all__ = [ - "Field", - "FieldCreate", - "FieldUpdate", - "FieldOption", - "CreateViewRequest", - "UpdateViewRequest", - "View", - "ViewMetadata", -] \ No newline at end of file diff --git a/pythonik/models/metadata/__init__.py b/pythonik/models/metadata/__init__.py index 168a8c9..e69de29 100644 --- a/pythonik/models/metadata/__init__.py +++ b/pythonik/models/metadata/__init__.py @@ -1,16 +0,0 @@ -# pythonik/models/metadata/__init__.py -from .fields import Field, FieldCreate, FieldUpdate, FieldOption -from .views import CreateViewRequest, UpdateViewRequest, View, ViewMetadata - -__all__ = [ - # From fields.py - "Field", - "FieldCreate", - "FieldUpdate", - "FieldOption", - # From views.py - "CreateViewRequest", - "UpdateViewRequest", - "View", - "ViewMetadata", -] \ No newline at end of file From 44c6f3fec45ae54f0bf379a8b0d60fa1c63e4c23 Mon Sep 17 00:00:00 2001 From: coffeepy Date: Thu, 8 May 2025 10:41:50 -0500 Subject: [PATCH 3/3] removed imports --- pythonik/tests/test_metadata.py | 67 ++++++++++++++++++++++----------- 1 file changed, 45 insertions(+), 22 deletions(-) diff --git a/pythonik/tests/test_metadata.py b/pythonik/tests/test_metadata.py index 10d65f8..9c9f0e3 100644 --- a/pythonik/tests/test_metadata.py +++ b/pythonik/tests/test_metadata.py @@ -36,7 +36,7 @@ FIELD_BY_NAME_PATH, ) -from pythonik.models import Field, FieldCreate, FieldUpdate, FieldOption +from pythonik.models.metadata.fields import Field, FieldCreate, FieldUpdate, FieldOption import pytest from pythonik.models.metadata.fields import IconikFieldType from pydantic import ValidationError @@ -1336,18 +1336,20 @@ def test_get_view_alternate_base_url(): def test_create_metadata_field(requests_mock): app_id = str(uuid.uuid4()) auth_token = str(uuid.uuid4()) - client = PythonikClient(app_id=app_id, auth_token=auth_token, timeout=3) # Client instance - spec_instance = client.metadata() # MetadataSpec instance + client = PythonikClient( + app_id=app_id, auth_token=auth_token, timeout=3 + ) # Client instance + spec_instance = client.metadata() # MetadataSpec instance field_name = "new_test_field_create" field_create_payload = FieldCreate( name=field_name, label="New Test Field Create", field_type="string", - options=[FieldOption(label="Option 1", value="opt1_val_create")] + options=[FieldOption(label="Option 1", value="opt1_val_create")], ) expected_field_response = Field( - id="generated_uuid_for_new_field_create", + id="generated_uuid_for_new_field_create", name=field_name, label="New Test Field Create", field_type="string", @@ -1355,15 +1357,21 @@ def test_create_metadata_field(requests_mock): date_created="2025-01-01T00:00:00Z", date_modified="2025-01-01T00:00:00Z", ) - + # mock_address = MetadataSpec.gen_url(FIELDS_BASE_PATH) # Old way (classmethod, relative URL) - mock_address = spec_instance.gen_url(FIELDS_BASE_PATH) # New way (instance method, absolute URL) + mock_address = spec_instance.gen_url( + FIELDS_BASE_PATH + ) # New way (instance method, absolute URL) requests_mock.post( - mock_address, json=json.loads(expected_field_response.model_dump_json()), status_code=201 + mock_address, + json=json.loads(expected_field_response.model_dump_json()), + status_code=201, ) - + # client = PythonikClient(app_id=app_id, auth_token=auth_token, timeout=3) # Already created above - result = spec_instance.create_metadata_field(field_create_payload) # Use the same spec_instance + result = spec_instance.create_metadata_field( + field_create_payload + ) # Use the same spec_instance assert result.response.ok assert result.response.status_code == 201 @@ -1396,11 +1404,15 @@ def test_update_metadata_field(requests_mock): FIELD_BY_NAME_PATH.format(field_name=field_to_update_name) ) requests_mock.put( - mock_address, json=json.loads(expected_field_response.model_dump_json()), status_code=200 + mock_address, + json=json.loads(expected_field_response.model_dump_json()), + status_code=200, ) client = PythonikClient(app_id=app_id, auth_token=auth_token, timeout=3) - result = client.metadata().update_metadata_field(field_to_update_name, field_update_payload) + result = client.metadata().update_metadata_field( + field_to_update_name, field_update_payload + ) assert result.response.ok assert result.response.status_code == 200 @@ -1429,7 +1441,9 @@ def test_delete_metadata_field(requests_mock): def test_create_metadata_field_conflict_error(requests_mock): app_id = str(uuid.uuid4()) auth_token = str(uuid.uuid4()) - field_create_payload = FieldCreate(name="existing_name", label="Fail Label", field_type="string") + field_create_payload = FieldCreate( + name="existing_name", label="Fail Label", field_type="string" + ) mock_address = MetadataSpec.gen_url(FIELDS_BASE_PATH) requests_mock.post(mock_address, json={"error": "conflict"}, status_code=409) client = PythonikClient(app_id=app_id, auth_token=auth_token, timeout=3) @@ -1443,10 +1457,14 @@ def test_update_metadata_field_not_found_error(requests_mock): auth_token = str(uuid.uuid4()) field_update_payload = FieldUpdate(label="NonExistent Update") non_existent_field = "non_existent_field_name" - mock_address = MetadataSpec.gen_url(FIELD_BY_NAME_PATH.format(field_name=non_existent_field)) + mock_address = MetadataSpec.gen_url( + FIELD_BY_NAME_PATH.format(field_name=non_existent_field) + ) requests_mock.put(mock_address, json={"error": "not found"}, status_code=404) client = PythonikClient(app_id=app_id, auth_token=auth_token, timeout=3) - result = client.metadata().update_metadata_field(non_existent_field, field_update_payload) + result = client.metadata().update_metadata_field( + non_existent_field, field_update_payload + ) assert not result.response.ok assert result.response.status_code == 404 @@ -1455,7 +1473,9 @@ def test_delete_metadata_field_not_found_error(requests_mock): app_id = str(uuid.uuid4()) auth_token = str(uuid.uuid4()) non_existent_field = "another_non_existent_field" - mock_address = MetadataSpec.gen_url(FIELD_BY_NAME_PATH.format(field_name=non_existent_field)) + mock_address = MetadataSpec.gen_url( + FIELD_BY_NAME_PATH.format(field_name=non_existent_field) + ) requests_mock.delete(mock_address, json={"error": "not found"}, status_code=404) client = PythonikClient(app_id=app_id, auth_token=auth_token, timeout=3) result = client.metadata().delete_metadata_field(non_existent_field) @@ -1464,7 +1484,9 @@ def test_delete_metadata_field_not_found_error(requests_mock): @pytest.mark.parametrize("field_type_enum", list(IconikFieldType)) -def test_create_metadata_field_for_all_types(requests_mock, field_type_enum: IconikFieldType): +def test_create_metadata_field_for_all_types( + requests_mock, field_type_enum: IconikFieldType +): """Test creating a metadata field for each IconikFieldType.""" app_id = str(uuid.uuid4()) auth_token = str(uuid.uuid4()) @@ -1477,7 +1499,7 @@ def test_create_metadata_field_for_all_types(requests_mock, field_type_enum: Ico field_create_payload = { "name": field_name, "label": field_label, - "field_type": field_type_enum, # Use the Enum member directly + "field_type": field_type_enum, # Use the Enum member directly } if field_type_enum == IconikFieldType.DROPDOWN: @@ -1491,7 +1513,7 @@ def test_create_metadata_field_for_all_types(requests_mock, field_type_enum: Ico "id": str(uuid.uuid4()), "name": field_name, "label": field_label, - "field_type": field_type_enum.value, # API returns the string value + "field_type": field_type_enum.value, # API returns the string value "date_created": "2023-01-01T12:00:00Z", "date_modified": "2023-01-01T12:00:00Z", # ... other fields with default/null values as per Field model @@ -1520,7 +1542,6 @@ def test_create_metadata_field_for_all_types(requests_mock, field_type_enum: Ico else: expected_response_json["options"] = [] - create_url = spec_instance.gen_url(FIELDS_BASE_PATH) requests_mock.post(create_url, json=expected_response_json, status_code=201) @@ -1532,7 +1553,9 @@ def test_create_metadata_field_for_all_types(requests_mock, field_type_enum: Ico assert response.data is not None assert response.data.name == field_name assert response.data.label == field_label - assert response.data.field_type == field_type_enum # Pydantic model converts back to Enum + assert ( + response.data.field_type == field_type_enum + ) # Pydantic model converts back to Enum # Optional: Mock and test deletion for cleanup (good practice) delete_url = spec_instance.gen_url(FIELD_BY_NAME_PATH.format(field_name=field_name)) @@ -1560,7 +1583,7 @@ def test_create_metadata_field_with_unknown_type_raises_validation_error(request field_data_model = FieldCreate( name=field_name, label=field_label, - field_type=IconikFieldType.STRING # A valid type for the request itself + field_type=IconikFieldType.STRING, # A valid type for the request itself ) # Mocked API response containing the unknown field_type in its body