From cb8f666f602e2ba19d28f150cf005ce4a3626e36 Mon Sep 17 00:00:00 2001 From: Albert Sola Date: Tue, 28 Oct 2025 11:08:15 +0000 Subject: [PATCH] MPT-14767 E2E seeding proof of concept - Added seeding proof of concept - Fix create product request Run: docker compose run --rm bash python seed/main.py --- pyproject.toml | 1 + seed/__init__.py | 0 seed/catalog/FIL-9920-4780-9379.png | Bin 0 -> 86054 bytes seed/catalog/__init__.py | 0 seed/catalog/catalog.py | 39 +++++ seed/catalog/item.py | 135 ++++++++++++++++++ seed/catalog/item_group.py | 87 +++++++++++ seed/catalog/product.py | 95 ++++++++++++ seed/catalog/product_parameters.py | 103 +++++++++++++ seed/catalog/product_parameters_group.py | 82 +++++++++++ seed/container.py | 61 ++++++++ seed/context.py | 59 ++++++++ seed/defaults.py | 11 ++ seed/main.py | 18 +++ seed/seed_api.py | 29 ++++ setup.cfg | 2 +- tests/seed/__init__.py | 1 + tests/seed/catalog/__init__.py | 0 tests/seed/catalog/conftest.py | 21 +++ tests/seed/catalog/test_catalog.py | 44 ++++++ tests/seed/catalog/test_item.py | 126 ++++++++++++++++ tests/seed/catalog/test_item_group.py | 98 +++++++++++++ tests/seed/catalog/test_product.py | 108 ++++++++++++++ tests/seed/catalog/test_product_parameters.py | 111 ++++++++++++++ .../catalog/test_product_parameters_group.py | 100 +++++++++++++ tests/seed/test_seed_api.py | 37 +++++ tests/unit/http/test_mixins.py | 1 + tests/unit/models/resource/test_resource.py | 16 +++ uv.lock | 17 +++ 29 files changed, 1401 insertions(+), 1 deletion(-) create mode 100644 seed/__init__.py create mode 100644 seed/catalog/FIL-9920-4780-9379.png create mode 100644 seed/catalog/__init__.py create mode 100644 seed/catalog/catalog.py create mode 100644 seed/catalog/item.py create mode 100644 seed/catalog/item_group.py create mode 100644 seed/catalog/product.py create mode 100644 seed/catalog/product_parameters.py create mode 100644 seed/catalog/product_parameters_group.py create mode 100644 seed/container.py create mode 100644 seed/context.py create mode 100644 seed/defaults.py create mode 100644 seed/main.py create mode 100644 seed/seed_api.py create mode 100644 tests/seed/__init__.py create mode 100644 tests/seed/catalog/__init__.py create mode 100644 tests/seed/catalog/conftest.py create mode 100644 tests/seed/catalog/test_catalog.py create mode 100644 tests/seed/catalog/test_item.py create mode 100644 tests/seed/catalog/test_item_group.py create mode 100644 tests/seed/catalog/test_product.py create mode 100644 tests/seed/catalog/test_product_parameters.py create mode 100644 tests/seed/catalog/test_product_parameters_group.py create mode 100644 tests/seed/test_seed_api.py diff --git a/pyproject.toml b/pyproject.toml index 5887cbc0..c4910e19 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,7 @@ dependencies = [ [dependency-groups] dev = [ + "dependency-injector>=4.48.2", "freezegun==1.5.*", "ipdb==0.13.*", "ipython==9.*", diff --git a/seed/__init__.py b/seed/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/seed/catalog/FIL-9920-4780-9379.png b/seed/catalog/FIL-9920-4780-9379.png new file mode 100644 index 0000000000000000000000000000000000000000..ab0c15a9d72ec793b48e0b734598ac85af8bec51 GIT binary patch literal 86054 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�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

4HLbG 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 literal 0 HcmV?d00001 diff --git a/seed/catalog/__init__.py b/seed/catalog/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/seed/catalog/catalog.py b/seed/catalog/catalog.py new file mode 100644 index 00000000..1624a71a --- /dev/null +++ b/seed/catalog/catalog.py @@ -0,0 +1,39 @@ +import asyncio +import logging +from typing import Any + +from seed.catalog.item import seed_items +from seed.catalog.item_group import seed_item_group +from seed.catalog.product import seed_product +from seed.catalog.product_parameters import seed_parameters +from seed.catalog.product_parameters_group import seed_parameter_group + +logger = logging.getLogger(__name__) + + +async def seed_groups_and_group_params() -> None: + """Seed parallel tasks for item groups and parameter groups.""" + tasks: list[asyncio.Task[Any]] = [ + asyncio.create_task(seed_item_group()), + asyncio.create_task(seed_parameter_group()), + ] + await asyncio.gather(*tasks) + + +async def seed_items_and_params() -> None: + """Seed final tasks for items and parameters.""" + tasks: list[asyncio.Task[Any]] = [ + asyncio.create_task(seed_items()), + asyncio.create_task(seed_parameters()), + ] + await asyncio.gather(*tasks) + + +async def seed_catalog() -> None: + """Seed catalog data including products, item groups, and parameters.""" + logger.debug("Seeding catalog ...") + await seed_product() + await seed_groups_and_group_params() + await seed_items_and_params() + + logger.debug("Seeded catalog completed.") diff --git a/seed/catalog/item.py b/seed/catalog/item.py new file mode 100644 index 00000000..19d74a9a --- /dev/null +++ b/seed/catalog/item.py @@ -0,0 +1,135 @@ +import logging +from typing import Any + +from dependency_injector.wiring import inject + +from mpt_api_client import AsyncMPTClient +from mpt_api_client.resources.catalog.items import Item +from seed.context import Context +from seed.defaults import ( + DEFAULT_CONTEXT, + DEFAULT_MPT_OPERATIONS, + DEFAULT_MPT_VENDOR, +) + +logger = logging.getLogger(__name__) + + +@inject +async def refresh_item( + context: Context = DEFAULT_CONTEXT, + mpt_vendor: AsyncMPTClient = DEFAULT_MPT_VENDOR, +) -> Item | None: + """Refresh item in context (always fetch).""" + item_id = context.get_string("catalog.item.id") + if not item_id: + return None + item_resource = await mpt_vendor.catalog.items.get(item_id) + context["catalog.item.id"] = item_resource.id + context.set_resource("catalog.item", item_resource) + return item_resource + + +@inject +async def get_item( + context: Context = DEFAULT_CONTEXT, + mpt_vendor: AsyncMPTClient = DEFAULT_MPT_VENDOR, +) -> Item | None: + """Get item from context or fetch from API if not cached.""" + item_id = context.get_string("catalog.item.id") + if not item_id: + return None + try: + catalog_item = context.get_resource("catalog.item", item_id) + except ValueError: + catalog_item = None + if not isinstance(catalog_item, Item): + logger.debug("Loading item: %s", item_id) + catalog_item = await mpt_vendor.catalog.items.get(item_id) + context["catalog.item.id"] = catalog_item.id + context.set_resource("catalog.item", catalog_item) + return catalog_item + return catalog_item + + +@inject +def build_item(context: Context = DEFAULT_CONTEXT) -> dict[str, Any]: + """Build item data dictionary for creation.""" + product_id = context.get("catalog.product.id") + item_group_id = context.get("catalog.item_group.id") + return { + "product": {"id": product_id}, + "parameters": [], + "name": "Product Item 1", + "description": "Product Item 1 - Description", + "group": {"id": item_group_id}, + "unit": { + "id": "UNT-1229", + "name": " 1", + "revision": 1, + "description": "TEST", + "statistics": {"itemCount": 34}, + }, + "terms": {"model": "quantity", "period": "1m", "commitment": "1m"}, + "quantityNotApplicable": False, + "externalIds": {"vendor": "item_1"}, + } + + +@inject +async def create_item( + context: Context = DEFAULT_CONTEXT, + mpt_vendor: AsyncMPTClient = DEFAULT_MPT_VENDOR, +) -> Item: + """Create item and cache in context.""" + item_data = build_item(context=context) + catalog_item = await mpt_vendor.catalog.items.create(item_data) + context["catalog.item.id"] = catalog_item.id + context.set_resource("catalog.item", catalog_item) + return catalog_item + + +@inject +async def review_item( + context: Context = DEFAULT_CONTEXT, + mpt_vendor: AsyncMPTClient = DEFAULT_MPT_VENDOR, +) -> Item | None: + """Review item if in draft status and cache result.""" + logger.debug("Reviewing catalog.item ...") + catalog_item = context.get_resource("catalog.item") + if catalog_item.status != "Draft": + return catalog_item # type: ignore[return-value] + catalog_item = await mpt_vendor.catalog.items.review(catalog_item.id) + context.set_resource("catalog.item", catalog_item) + return catalog_item + + +@inject +async def publish_item( + context: Context = DEFAULT_CONTEXT, + mpt_operations: AsyncMPTClient = DEFAULT_MPT_OPERATIONS, +) -> Item | None: + """Publish item if in reviewing status and cache result.""" + logger.debug("Publishing catalog.item ...") + catalog_item = context.get_resource("catalog.item") + if catalog_item.status != "Reviewing": + return catalog_item # type: ignore[return-value] + catalog_item = await mpt_operations.catalog.items.publish(catalog_item.id) + context.set_resource("catalog.item", catalog_item) + return catalog_item + + +@inject +async def seed_items( + context: Context = DEFAULT_CONTEXT, + mpt_vendor: AsyncMPTClient = DEFAULT_MPT_VENDOR, + mpt_operations: AsyncMPTClient = DEFAULT_MPT_OPERATIONS, +) -> None: + """Seed catalog items (create/review/publish).""" + logger.debug("Seeding catalog.item ...") + existing = await refresh_item(context=context, mpt_vendor=mpt_vendor) + if not existing: + await create_item(context=context, mpt_vendor=mpt_vendor) + await review_item(context=context, mpt_vendor=mpt_vendor) + await publish_item(context=context, mpt_operations=mpt_operations) + logger.debug("Seeded catalog.item completed.") diff --git a/seed/catalog/item_group.py b/seed/catalog/item_group.py new file mode 100644 index 00000000..c8922cb9 --- /dev/null +++ b/seed/catalog/item_group.py @@ -0,0 +1,87 @@ +import logging +from typing import Any + +from dependency_injector.wiring import inject + +from mpt_api_client import AsyncMPTClient +from mpt_api_client.resources.catalog.products_item_groups import ItemGroup +from seed.context import Context +from seed.defaults import DEFAULT_CONTEXT, DEFAULT_MPT_VENDOR + +logger = logging.getLogger(__name__) + + +@inject +async def get_item_group( + context: Context = DEFAULT_CONTEXT, + mpt_vendor: AsyncMPTClient = DEFAULT_MPT_VENDOR, +) -> ItemGroup | None: + """Get item group from context or fetch from API.""" + item_group_id = context.get_string("catalog.item_group.id") + if not item_group_id: + return None + try: + item_group = context.get_resource("catalog.item_group", item_group_id) + except ValueError: + item_group = None + if not isinstance(item_group, ItemGroup): + logger.debug("Refreshing item group: %s", item_group_id) + product_id = context.get_string("catalog.product.id") + item_group = await mpt_vendor.catalog.products.item_groups(product_id).get(item_group_id) + return set_item_group(item_group, context=context) + return item_group + + +@inject +def set_item_group( + item_group: ItemGroup, + context: Context = DEFAULT_CONTEXT, +) -> ItemGroup: + """Set item group in context.""" + context["catalog.item_group.id"] = item_group.id + context.set_resource("catalog.item_group", item_group) + return item_group + + +@inject +def build_item_group(context: Context = DEFAULT_CONTEXT) -> dict[str, Any]: + """Build item group data dictionary.""" + product_id = context.get("catalog.product.id") + return { + "product": {"id": product_id}, + "name": "Items", + "label": "Items", + "description": "Default item group", + "displayOrder": 100, + "default": True, + "multiple": True, + "required": True, + } + + +@inject +async def init_item_group( + context: Context = DEFAULT_CONTEXT, + mpt_vendor: AsyncMPTClient = DEFAULT_MPT_VENDOR, +) -> ItemGroup: + """Get or create item group.""" + item_group = await get_item_group() + + if not item_group: + logger.debug("Creating item group ...") + product_id = context.get_string("catalog.product.id") + item_group_data = build_item_group() + item_group = await mpt_vendor.catalog.products.item_groups(product_id).create( + item_group_data + ) + logger.debug("Item group created: %s", item_group.id) + return set_item_group(item_group) + logger.debug("Item group found: %s", item_group.id) + return item_group + + +async def seed_item_group() -> None: + """Seed item group.""" + logger.debug("Seeding catalog.item_group ...") + await init_item_group() + logger.debug("Seeded catalog.item_group completed.") diff --git a/seed/catalog/product.py b/seed/catalog/product.py new file mode 100644 index 00000000..e4a8c2ce --- /dev/null +++ b/seed/catalog/product.py @@ -0,0 +1,95 @@ +import logging +import pathlib + +from dependency_injector.wiring import inject + +from mpt_api_client import AsyncMPTClient +from mpt_api_client.resources.catalog.products import Product +from seed.context import Context +from seed.defaults import DEFAULT_CONTEXT, DEFAULT_MPT_OPERATIONS, DEFAULT_MPT_VENDOR + +icon = pathlib.Path(__file__).parent / "FIL-9920-4780-9379.png" + +logger = logging.getLogger(__name__) + +namespace = "catalog.product" + + +@inject +async def get_product( + context: Context = DEFAULT_CONTEXT, + mpt_vendor: AsyncMPTClient = DEFAULT_MPT_VENDOR, +) -> Product | None: + """Get product from context or fetch from API.""" + product_id = context.get_string(f"{namespace}.id") + if not product_id: + return None + try: + product = context.get_resource(namespace, product_id) + except ValueError: + product = None + if not isinstance(product, Product): + logger.debug("Refreshing product: %s", product_id) + product = await mpt_vendor.catalog.products.get(product_id) + context.set_resource(namespace, product) + context[f"{namespace}.id"] = product.id + return product + return product + + +@inject +async def init_product( + context: Context = DEFAULT_CONTEXT, + mpt_vendor: AsyncMPTClient = DEFAULT_MPT_VENDOR, +) -> Product: + """Get or create product.""" + product = await get_product() + if not product: + logger.debug("Creating product ...") + with pathlib.Path.open(icon, "rb") as icon_file: + product = await mpt_vendor.catalog.products.create( + {"name": "Test Product", "website": "https://www.example.com"}, icon=icon_file + ) + context.set_resource(namespace, product) + context[f"{namespace}.id"] = product.id + logger.debug("Product created: %s", product.id) + return product + + +@inject +async def review_product( + context: Context = DEFAULT_CONTEXT, + mpt_vendor: AsyncMPTClient = DEFAULT_MPT_VENDOR, +) -> Product | None: + """Review product if in draft status.""" + product = await get_product() + if not product or product.status != "Draft": + return product + logger.debug("Reviewing product: %s", product.id) + product = await mpt_vendor.catalog.products.review(product.id) + context.set_resource(namespace, product) + return product + + +@inject +async def publish_product( + context: Context = DEFAULT_CONTEXT, + mpt_operations: AsyncMPTClient = DEFAULT_MPT_OPERATIONS, +) -> Product | None: + """Publish product if in reviewing status.""" + product = await get_product() + if not product or product.status != "Reviewing": + return product + logger.debug("Publishing product: %s", product.id) + product = await mpt_operations.catalog.products.publish(product.id) + context.set_resource(namespace, product) + return product + + +async def seed_product() -> None: + """Seed product data.""" + logger.debug("Seeding catalog.product ...") + await init_product() + await review_product() + await publish_product() + logger.debug("Seeded catalog.product completed.") diff --git a/seed/catalog/product_parameters.py b/seed/catalog/product_parameters.py new file mode 100644 index 00000000..8549d38e --- /dev/null +++ b/seed/catalog/product_parameters.py @@ -0,0 +1,103 @@ +import logging +from typing import Any + +from dependency_injector.wiring import inject + +from mpt_api_client import AsyncMPTClient +from mpt_api_client.resources.catalog.products_parameters import Parameter +from seed.context import Context +from seed.defaults import DEFAULT_CONTEXT, DEFAULT_MPT_VENDOR + +logger = logging.getLogger(__name__) + +namespace = "catalog.parameter" + + +@inject +async def get_parameter( + context: Context = DEFAULT_CONTEXT, + mpt_vendor: AsyncMPTClient = DEFAULT_MPT_VENDOR, +) -> Parameter | None: + """Get parameter from context or fetch from API.""" + parameter_id = context.get_string(f"{namespace}.id") + if not parameter_id: + return None + try: + return context.get_resource(namespace, parameter_id) # type: ignore[return-value] + except ValueError: + logger.debug("Loading parameter: %s", parameter_id) + product_id = context.get_string("catalog.product.id") + parameter = await mpt_vendor.catalog.products.product_parameters(product_id).get( + parameter_id + ) + context.set_resource(namespace, parameter) + return parameter + + +@inject +def build_parameter(context: Context = DEFAULT_CONTEXT) -> dict[str, Any]: + """Build parameter data dictionary.""" + parameter_group_id = context.get_string("catalog.parameter_group.id") + if not parameter_group_id: + raise ValueError("Parameter group id is required.") + return { + "name": "Parameter Name", + "scope": "Order", + "phase": "Order", + "description": "Agreement identifier of the reseller", + "externalId": "RES-233-33-xx3", + "displayOrder": 100, + "context": "Purchase", + "constraints": {"hidden": True, "readonly": True, "required": False}, + "type": "SingleLineText", + "options": { + "name": "Agreement Id", + "placeholderText": "AGR-xxx-xxx-xxx", + "hintText": "Add agreement id", + "minChar": 15, + "maxChar": 15, + "defaultValue": None, + }, + "group": {"id": parameter_group_id}, + "status": "active", + } + + +@inject +async def create_parameter( + context: Context = DEFAULT_CONTEXT, mpt_vendor: AsyncMPTClient = DEFAULT_MPT_VENDOR +) -> Parameter: + """Create parameter and stores it in the context.""" + product_id = context.get_string("catalog.product.id") + if not product_id: + raise ValueError("Product id is required.") + parameter_data = build_parameter(context=context) + parameter = await mpt_vendor.catalog.products.product_parameters(product_id).create( + parameter_data + ) + logger.debug("Parameter created: %s", parameter.id) + context[f"{namespace}.id"] = parameter.id + context.set_resource(namespace, parameter) + return parameter + + +@inject +async def init_parameter( + context: Context = DEFAULT_CONTEXT, + mpt_vendor: AsyncMPTClient = DEFAULT_MPT_VENDOR, +) -> Parameter: + """Get or create parameter.""" + parameter = await get_parameter() + + if not parameter: + logger.debug("Creating parameter ...") + return await create_parameter(context, mpt_vendor) + logger.debug("Parameter found: %s", parameter.id) + return parameter + + +async def seed_parameters() -> None: + """Seed catalog parameters.""" + logger.debug("Seeding catalog.parameter ...") + await init_parameter() + logger.debug("Seeded catalog.parameter completed.") diff --git a/seed/catalog/product_parameters_group.py b/seed/catalog/product_parameters_group.py new file mode 100644 index 00000000..d0191692 --- /dev/null +++ b/seed/catalog/product_parameters_group.py @@ -0,0 +1,82 @@ +import logging +from typing import Any + +from dependency_injector.wiring import inject + +from mpt_api_client import AsyncMPTClient +from mpt_api_client.resources.catalog.products_parameter_groups import ParameterGroup +from seed.context import Context +from seed.defaults import DEFAULT_CONTEXT, DEFAULT_MPT_VENDOR + +logger = logging.getLogger(__name__) + +namespace = "catalog.parameter_group" + + +@inject +async def get_parameter_group( + context: Context = DEFAULT_CONTEXT, + mpt_vendor: AsyncMPTClient = DEFAULT_MPT_VENDOR, +) -> ParameterGroup | None: + """Get parameter group from context or fetch from API.""" + parameter_group_id = context.get_string(f"{namespace}.id") + if not parameter_group_id: + return None + try: + return context.get_resource(namespace, parameter_group_id) # type: ignore[return-value] + except ValueError: + logger.debug("Loading parameter group: %s", parameter_group_id) + product_id = context.get_string("catalog.product.id") + parameter_group = await mpt_vendor.catalog.products.parameter_groups(product_id).get( + parameter_group_id + ) + context[f"{namespace}.id"] = parameter_group.id + context.set_resource(namespace, parameter_group) + return parameter_group + + +@inject +def build_parameter_group(context: Context = DEFAULT_CONTEXT) -> dict[str, Any]: + """Build parameter group data dictionary.""" + return { + "name": "Parameter group", + "label": "Parameter group label", + "displayOrder": 100, + } + + +@inject +async def init_parameter_group( + context: Context = DEFAULT_CONTEXT, + mpt_vendor: AsyncMPTClient = DEFAULT_MPT_VENDOR, +) -> ParameterGroup: + """Get or create parameter group.""" + parameter_group = await get_parameter_group() + + if not parameter_group: + return await create_parameter_group(context, mpt_vendor) + logger.debug("Parameter group found: %s", parameter_group.id) + return parameter_group + + +async def create_parameter_group(context: Context, mpt_vendor: AsyncMPTClient) -> ParameterGroup: + """Create parameter group.""" + logger.debug("Creating parameter group ...") + product_id = context.get_string("catalog.product.id") + if not product_id: + raise ValueError("Product id is required.") + parameter_group_data = build_parameter_group() + parameter_group = await mpt_vendor.catalog.products.parameter_groups(product_id).create( + parameter_group_data + ) + logger.debug("Parameter group created: %s", parameter_group.id) + context[f"{namespace}.id"] = parameter_group.id + context.set_resource(namespace, parameter_group) + return parameter_group + + +async def seed_parameter_group() -> None: + """Seed parameter group.""" + logger.debug("Seeding %s ...", namespace) + await init_parameter_group() + logger.debug("Seeded %s completed.", namespace) diff --git a/seed/container.py b/seed/container.py new file mode 100644 index 00000000..6adfea5c --- /dev/null +++ b/seed/container.py @@ -0,0 +1,61 @@ +from dependency_injector import containers, providers + +from mpt_api_client import AsyncMPTClient +from seed.context import Context + + +class Container(containers.DeclarativeContainer): + """Dependency injection container for MPT clients.""" + + config = providers.Configuration() + + # Client factories + mpt_client = providers.Factory( + AsyncMPTClient.from_config, + api_token=config.mpt_api_token_client, + base_url=config.mpt_api_base_url, + ) + + mpt_vendor = providers.Factory( + AsyncMPTClient.from_config, + api_token=config.mpt_api_token_vendor, + base_url=config.mpt_api_base_url, + ) + + mpt_operations = providers.Factory( + AsyncMPTClient.from_config, + api_token=config.mpt_api_token_operations, + base_url=config.mpt_api_base_url, + ) + + # Context provider - stores application context as a singleton + context: providers.Singleton[Context] = providers.Singleton(Context) + + +# Create container instance +container = Container() + +# Configure from environment variables +container.config.mpt_api_base_url.from_env("MPT_API_BASE_URL") +container.config.mpt_api_token_client.from_env("MPT_API_TOKEN_CLIENT") +container.config.mpt_api_token_vendor.from_env("MPT_API_TOKEN_VENDOR") +container.config.mpt_api_token_operations.from_env("MPT_API_TOKEN_OPERATIONS") + + +def wire_container() -> None: + """Wire the dependency injection container.""" + container.wire( + modules=[ + "seed", + "seed.context", + "seed.defaults", + "seed.seed_api", + "seed.catalog", + "seed.catalog.catalog", + "seed.catalog.product", + "seed.catalog.item", + "seed.catalog.item_group", + "seed.catalog.product_parameters", + "seed.catalog.product_parameters_group", + ] + ) diff --git a/seed/context.py b/seed/context.py new file mode 100644 index 00000000..e89b2eca --- /dev/null +++ b/seed/context.py @@ -0,0 +1,59 @@ +import collections +import json +import pathlib +from typing import Any + +from mpt_api_client.models import Model + + +class Context(collections.UserDict[str, Any]): + """Application context.""" + + def get_string(self, key: str, default: str = "") -> str: + """Get string value from context.""" + return str(self.get(key, default)) + + def get_resource(self, namespace: str, resource_id: str | None = None) -> Model: # noqa: WPS615 + """Get resource from context. + + Raises: + ValueError: if resource not found or wrong type. + """ + resource_id = resource_id or self.get_string(f"{namespace}.id") + resource = self.get(f"{namespace}[{resource_id}]") + if not isinstance(resource, Model): + raise ValueError(f"Resource {resource_id} not found.") # noqa: TRY004 + return resource + + def set_resource(self, namespace: str, resource: Model) -> None: # noqa: WPS615 + """Save resource to context.""" + self[f"{namespace}[{resource.id}]"] = resource + + +def load_context(json_file: pathlib.Path, context: Context | None = None) -> Context: + """Load context from JSON file. + + Args: + json_file: JSON file path. + context: Context instance. + + Returns: + Context instance. + + """ + context = context or Context() + with json_file.open("r", encoding="utf-8") as fd: + existing_data = json.load(fd) + context.update(existing_data) + return context + + +def save_context(context: Context, json_file: pathlib.Path) -> None: + """Save context to JSON file. + + Args: + json_file: JSON file path. + context: Context instance. + """ + with json_file.open("w", encoding="utf-8") as fd: + json.dump(context.data, fd, indent=4, default=str) diff --git a/seed/defaults.py b/seed/defaults.py new file mode 100644 index 00000000..3e9e23db --- /dev/null +++ b/seed/defaults.py @@ -0,0 +1,11 @@ +"""Default dependency providers to avoid WPS404 violations.""" + +from dependency_injector.wiring import Provide + +from seed.container import Container + +# Constants for dependency injection to avoid WPS404 violations +DEFAULT_CONTEXT = Provide[Container.context] +DEFAULT_MPT_CLIENT = Provide[Container.mpt_client] +DEFAULT_MPT_VENDOR = Provide[Container.mpt_vendor] +DEFAULT_MPT_OPERATIONS = Provide[Container.mpt_operations] diff --git a/seed/main.py b/seed/main.py new file mode 100644 index 00000000..e346a948 --- /dev/null +++ b/seed/main.py @@ -0,0 +1,18 @@ +import asyncio +import logging + +from seed.container import wire_container +from seed.seed_api import seed_api + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +async def main() -> None: + """Main entry point for seeding.""" + wire_container() + await seed_api() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/seed/seed_api.py b/seed/seed_api.py new file mode 100644 index 00000000..747221ea --- /dev/null +++ b/seed/seed_api.py @@ -0,0 +1,29 @@ +import asyncio +import logging +import pathlib + +from dependency_injector.wiring import inject + +from seed.catalog.catalog import seed_catalog +from seed.context import Context, load_context, save_context +from seed.defaults import DEFAULT_CONTEXT + +logger = logging.getLogger(__name__) + +context_file: pathlib.Path = pathlib.Path(__file__).parent / "context.json" + + +@inject +async def seed_api(context: Context = DEFAULT_CONTEXT) -> None: + """Seed API.""" + tasks = [] + + load_context(context_file, context) + + tasks.append(asyncio.create_task(seed_catalog())) + try: + await asyncio.gather(*tasks) + except Exception: + logger.exception("Exception occurred during seeding.") + finally: + save_context(context, context_file) diff --git a/setup.cfg b/setup.cfg index e053ce2f..eb8d4599 100644 --- a/setup.cfg +++ b/setup.cfg @@ -48,7 +48,7 @@ per-file-ignores = tests/unit/resources/accounts/test_users.py: WPS204 WPS202 WPS210 tests/unit/test_mpt_client.py: WPS235 - tests/unit/*: + tests/*: # Allow magic strings. WPS432 # Found too many modules members. diff --git a/tests/seed/__init__.py b/tests/seed/__init__.py new file mode 100644 index 00000000..246a27c5 --- /dev/null +++ b/tests/seed/__init__.py @@ -0,0 +1 @@ +"""Tests for seed package.""" diff --git a/tests/seed/catalog/__init__.py b/tests/seed/catalog/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/seed/catalog/conftest.py b/tests/seed/catalog/conftest.py new file mode 100644 index 00000000..c1952c35 --- /dev/null +++ b/tests/seed/catalog/conftest.py @@ -0,0 +1,21 @@ +from unittest.mock import AsyncMock, MagicMock + +import pytest + +from mpt_api_client import AsyncMPTClient +from seed.context import Context + + +@pytest.fixture +def context() -> Context: + return Context() + + +@pytest.fixture +def vendor_client() -> AsyncMock: + return MagicMock(spec=AsyncMPTClient) + + +@pytest.fixture +def operations_client() -> AsyncMock: + return MagicMock(spec=AsyncMPTClient) diff --git a/tests/seed/catalog/test_catalog.py b/tests/seed/catalog/test_catalog.py new file mode 100644 index 00000000..9f3514bb --- /dev/null +++ b/tests/seed/catalog/test_catalog.py @@ -0,0 +1,44 @@ +from unittest.mock import AsyncMock, patch + +from seed.catalog.catalog import seed_catalog, seed_groups_and_group_params, seed_items_and_params + + +async def test_seed_catalog_stage1() -> None: + with ( + patch("seed.catalog.catalog.seed_item_group", new_callable=AsyncMock) as mock_item_group, + patch( + "seed.catalog.catalog.seed_parameter_group", new_callable=AsyncMock + ) as mock_param_group, + ): + await seed_groups_and_group_params() + + mock_item_group.assert_called_once() + mock_param_group.assert_called_once() + + +async def test_seed_catalog_stage2() -> None: + with ( + patch("seed.catalog.catalog.seed_items", new_callable=AsyncMock) as mock_items, + patch("seed.catalog.catalog.seed_parameters", new_callable=AsyncMock) as mock_params, + ): + await seed_items_and_params() + + mock_items.assert_called_once() + mock_params.assert_called_once() + + +async def test_seed_catalog() -> None: + with ( + patch("seed.catalog.catalog.seed_product", new_callable=AsyncMock) as mock_product, + patch( + "seed.catalog.catalog.seed_items_and_params", new_callable=AsyncMock + ) as seed_items_and_params, + patch( + "seed.catalog.catalog.seed_groups_and_group_params", new_callable=AsyncMock + ) as seed_groups_and_group_params, + ): + await seed_catalog() + + mock_product.assert_called_once() + seed_items_and_params.assert_called_once() + seed_groups_and_group_params.assert_called_once() diff --git a/tests/seed/catalog/test_item.py b/tests/seed/catalog/test_item.py new file mode 100644 index 00000000..2bfdb094 --- /dev/null +++ b/tests/seed/catalog/test_item.py @@ -0,0 +1,126 @@ +from unittest.mock import AsyncMock, patch + +import pytest + +from mpt_api_client.resources.catalog.items import AsyncItemsService, Item +from seed.catalog.item import ( + create_item, + get_item, + publish_item, + review_item, + seed_items, +) +from seed.context import Context + + +@pytest.fixture +def resource_item() -> Item: # noqa: WPS110 + return Item({"id": "item-123", "status": "Draft"}) + + +@pytest.fixture +def items_service(): + return AsyncMock(spec=AsyncItemsService) + + +async def test_get_item(context: Context, vendor_client, resource_item) -> None: # noqa: WPS110 + context["catalog.item.id"] = resource_item.id + service = AsyncMock(spec=AsyncItemsService) + service.get.return_value = resource_item + vendor_client.catalog.items = service + + fetched_item = await get_item(context=context, mpt_vendor=vendor_client) + + assert fetched_item == resource_item + assert context.get(f"catalog.item[{resource_item.id}]") == resource_item + + +async def test_get_item_without_id(context: Context) -> None: + missing_item = await get_item(context=context) + assert missing_item is None + + +async def test_create_item(context: Context, vendor_client, resource_item, items_service) -> None: # noqa: WPS110 + items_service.create.return_value = resource_item + vendor_client.catalog.items = items_service + + created = await create_item(context=context, mpt_vendor=vendor_client) + + assert created == resource_item + assert context.get("catalog.item.id") == resource_item.id + assert context.get(f"catalog.item[{resource_item.id}]") == resource_item + + +async def test_review_item_draft_status( + context: Context, vendor_client, resource_item, items_service +) -> None: # noqa: WPS110 + resource_item.status = "Draft" + items_service.review.return_value = resource_item + vendor_client.catalog.items = items_service + context.set_resource("catalog.item", resource_item) + context["catalog.item.id"] = resource_item.id + + reviewed_item = await review_item(context=context, mpt_vendor=vendor_client) + + assert reviewed_item == resource_item + items_service.review.assert_called_once() + + +async def test_review_item_non_draft_status( + context: Context, vendor_client, resource_item, items_service +) -> None: # noqa: WPS110 + resource_item.status = "Published" + + items_service.review.return_value = resource_item + vendor_client.catalog.items = items_service + context.set_resource("catalog.item", resource_item) + context["catalog.item.id"] = resource_item.id + + await review_item(context=context, mpt_vendor=vendor_client) + items_service.review.assert_not_called() + + +async def test_publish_item_reviewing_status( + context: Context, operations_client, resource_item, items_service +) -> None: # noqa: WPS110 + resource_item.status = "Reviewing" + items_service.publish.return_value = resource_item + operations_client.catalog.items = items_service + context.set_resource("catalog.item", resource_item) + context["catalog.item.id"] = resource_item.id + + published_item = await publish_item(context=context, mpt_operations=operations_client) + + assert published_item == resource_item + operations_client.catalog.items.publish.assert_called_once() + + +async def test_publish_item_non_reviewing_status( + context: Context, operations_client, resource_item, items_service +) -> None: # noqa: WPS110 + resource_item.status = "Draft" + items_service.publish.return_value = resource_item + operations_client.catalog.items = items_service + context.set_resource("catalog.item", resource_item) + context["catalog.item.id"] = resource_item.id + + await publish_item(context=context, mpt_operations=operations_client) + + operations_client.catalog.items.publish.assert_not_called() + + +async def test_seed_items(context: Context) -> None: + with ( + patch( + "seed.catalog.item.refresh_item", new_callable=AsyncMock, return_value=None + ) as mock_refresh, + patch("seed.catalog.item.create_item", new_callable=AsyncMock) as mock_create, + patch("seed.catalog.item.review_item", new_callable=AsyncMock) as mock_review, + patch("seed.catalog.item.publish_item", new_callable=AsyncMock) as mock_publish, + ): + await seed_items(context=context) + + mock_refresh.assert_called_once() + mock_create.assert_called_once() + mock_review.assert_called_once() + mock_publish.assert_called_once() diff --git a/tests/seed/catalog/test_item_group.py b/tests/seed/catalog/test_item_group.py new file mode 100644 index 00000000..63f7a791 --- /dev/null +++ b/tests/seed/catalog/test_item_group.py @@ -0,0 +1,98 @@ +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from mpt_api_client import AsyncMPTClient +from mpt_api_client.resources.catalog.products_item_groups import AsyncItemGroupsService, ItemGroup +from seed.catalog.item_group import ( + build_item_group, + get_item_group, + init_item_group, + seed_item_group, + set_item_group, +) + + +@pytest.fixture +def item_group(): + return ItemGroup({ + "id": "group-123", + }) + + +@pytest.fixture +def item_group_service(): + return AsyncMock(spec=AsyncItemGroupsService) + + +@pytest.fixture +def vendor_client() -> AsyncMock: + return MagicMock(spec=AsyncMPTClient) + + +@pytest.fixture +def mock_set_item_group(mocker): + return mocker.patch("seed.catalog.item_group.set_item_group", spec=set_item_group) + + +async def test_get_item_group(context, vendor_client, item_group, mock_set_item_group) -> None: + context["catalog.item_group.id"] = item_group.id + context["catalog.product.id"] = "product-123" + service = AsyncMock(spec=AsyncItemGroupsService) + service.get.return_value = item_group + vendor_client.catalog.products.item_groups.return_value = service + mock_set_item_group.return_value = item_group + + fetched_group = await get_item_group(context=context, mpt_vendor=vendor_client) + + assert fetched_group == item_group + service.get.assert_called_once_with(context["catalog.item_group.id"]) + service.create.assert_not_called() + + +async def test_get_item_group_without_id(context) -> None: + no_group = await get_item_group(context=context) + + assert no_group is None + + +def test_set_item_group(context, item_group) -> None: + stored_group = set_item_group(item_group, context=context) + + assert stored_group == item_group + assert context.get("catalog.item_group.id") == "group-123" + assert context.get("catalog.item_group[group-123]") == item_group + + +def test_build_item_group(context) -> None: + context["catalog.product.id"] = "product-123" + + item_group_payload = build_item_group(context=context) + + assert item_group_payload["product"]["id"] == "product-123" + + +async def test_get_or_create_item_group_create_new( + context, vendor_client, item_group_service, item_group +) -> None: + context["catalog.product.id"] = "product-123" + item_group_service.create.return_value = item_group + vendor_client.catalog.products.item_groups.return_value = item_group_service + + with ( + patch("seed.catalog.item_group.get_item_group", return_value=None), + patch("seed.catalog.item_group.build_item_group", return_value=item_group), + patch("seed.catalog.item_group.set_item_group", return_value=item_group) as set_item_group, + ): + created_group = await init_item_group(context=context, mpt_vendor=vendor_client) + + assert created_group == item_group + set_item_group.assert_called_once_with(item_group) + item_group_service.create.assert_called_once() + + +async def test_seed_item_group() -> None: + with patch("seed.catalog.item_group.init_item_group", new_callable=AsyncMock) as mock_create: + await seed_item_group() + + mock_create.assert_called_once() diff --git a/tests/seed/catalog/test_product.py b/tests/seed/catalog/test_product.py new file mode 100644 index 00000000..f22d057e --- /dev/null +++ b/tests/seed/catalog/test_product.py @@ -0,0 +1,108 @@ +import io +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from mpt_api_client.resources.catalog.products import AsyncProductsService, Product +from seed.catalog.product import ( + get_product, + init_product, + publish_product, + review_product, + seed_product, +) +from seed.context import Context + + +@pytest.fixture +def product(): + return Product({"id": "prod-123", "status": "Draft"}) + + +@pytest.fixture +def products_service(): + return AsyncMock(spec=AsyncProductsService) + + +async def test_get_product(context: Context, vendor_client, product, products_service) -> None: + context["catalog.product.id"] = product.id + products_service.get.return_value = product + vendor_client.catalog.products = products_service + + fetched_product = await get_product(context=context, mpt_vendor=vendor_client) + + assert fetched_product == product + assert context.get_resource("catalog.product", product.id) == product + + +async def test_get_product_without_id(context: Context) -> None: + product = await get_product(context=context) + assert product is None + + +async def test_get_or_create_product_create_new( + context: Context, vendor_client, products_service, product +) -> None: + products_service.create.return_value = product + vendor_client.catalog.products = products_service + fake_icon_bytes = io.BytesIO(b"fake image") + + with ( + patch("seed.catalog.product.get_product", return_value=None), + patch("seed.catalog.product.icon", new=MagicMock()), + patch("pathlib.Path.open", return_value=fake_icon_bytes), + ): + created = await init_product(context, mpt_vendor=vendor_client) + assert created == product + products_service.create.assert_called_once() + + +async def test_review_product_draft_status( + context, products_service, vendor_client, product +) -> None: + product.status = "Draft" + products_service.review.return_value = product + vendor_client.catalog.products = products_service + with ( + patch("seed.catalog.product.get_product", return_value=product), + ): + reviewed = await review_product(context, mpt_vendor=vendor_client) + assert reviewed == product + products_service.review.assert_called_once() + + +async def test_review_product_non_draft_status(product) -> None: + product.status = "Published" + with patch("seed.catalog.product.get_product", return_value=product): + unchanged = await review_product() + assert unchanged == product + + +async def test_publish_product_reviewing_status(context, operations_client, product) -> None: + product.status = "Reviewing" + operations_client.catalog.products.publish = AsyncMock(return_value=product) + with ( + patch("seed.catalog.product.get_product", return_value=product), + ): + published = await publish_product(context, mpt_operations=operations_client) + assert published == product + operations_client.catalog.products.publish.assert_called_once() + + +async def test_publish_product_non_reviewing_status(product) -> None: + product.status = "Draft" + with patch("seed.catalog.product.get_product", return_value=product): + unchanged = await publish_product() + assert unchanged == product + + +async def test_seed_product_sequence() -> None: + with ( + patch("seed.catalog.product.init_product", new_callable=AsyncMock) as mock_create, + patch("seed.catalog.product.review_product", new_callable=AsyncMock) as mock_review, + patch("seed.catalog.product.publish_product", new_callable=AsyncMock) as mock_publish, + ): + await seed_product() + mock_create.assert_called_once() + mock_review.assert_called_once() + mock_publish.assert_called_once() diff --git a/tests/seed/catalog/test_product_parameters.py b/tests/seed/catalog/test_product_parameters.py new file mode 100644 index 00000000..7d1de6b9 --- /dev/null +++ b/tests/seed/catalog/test_product_parameters.py @@ -0,0 +1,111 @@ +from typing import Any +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from mpt_api_client import AsyncMPTClient +from mpt_api_client.resources.catalog.products_parameters import AsyncParametersService, Parameter +from seed.catalog.product_parameters import ( + build_parameter, + create_parameter, + get_parameter, + init_parameter, + seed_parameters, +) +from seed.context import Context + +namespace = "catalog.parameter" + + +@pytest.fixture +def parameter() -> Parameter: + return Parameter({"id": "param-123"}) + + +@pytest.fixture +def parameters_service() -> AsyncMock: + return AsyncMock(spec=AsyncParametersService) + + +@pytest.fixture +def vendor_client() -> AsyncMock: + return MagicMock(spec=AsyncMPTClient) + + +async def test_get_parameter( + context: Context, vendor_client: AsyncMock, parameter: Parameter +) -> None: + context[f"{namespace}.id"] = parameter.id + context["catalog.product.id"] = "product-123" + service = AsyncMock(spec=AsyncParametersService) + service.get.return_value = parameter + vendor_client.catalog.products.product_parameters.return_value = service + + fetched_parameter = await get_parameter(context=context, mpt_vendor=vendor_client) + + assert fetched_parameter == parameter + assert context.get(f"{namespace}.id") == parameter.id + assert context.get(f"{namespace}[{parameter.id}]") == parameter + + +async def test_get_parameter_without_id(context: Context) -> None: + maybe_parameter = await get_parameter(context=context) + + assert maybe_parameter is None + + +def test_build_parameter(context: Context) -> None: + context["catalog.parameter_group.id"] = "group-123" + + parameter_payload: dict[str, Any] = build_parameter(context=context) + + assert parameter_payload["group"]["id"] == "group-123" + + +async def test_get_or_create_parameter_create_new( + context: Context, vendor_client: AsyncMock, parameters_service: AsyncMock, parameter: Parameter +) -> None: + context["catalog.product.id"] = "product-123" + parameters_service.create.return_value = parameter + vendor_client.catalog.products.product_parameters.return_value = parameters_service + + with ( + patch("seed.catalog.product_parameters.get_parameter", return_value=None), + patch("seed.catalog.product_parameters.build_parameter", return_value=parameter), + ): + created_parameter = await init_parameter(context=context, mpt_vendor=vendor_client) + + assert created_parameter == parameter + assert context.get(f"{namespace}.id") == parameter.id + assert context.get(f"{namespace}[{parameter.id}]") == parameter + + +async def test_seed_parameters() -> None: + with patch( + "seed.catalog.product_parameters.init_parameter", new_callable=AsyncMock + ) as mock_create: + await seed_parameters() + + mock_create.assert_called_once() + + +async def test_create_parameter_success( + context: Context, vendor_client: AsyncMock, parameter: Parameter +) -> None: + context["catalog.product.id"] = "product-123" + context["catalog.parameter_group.id"] = "group-123" + service = AsyncMock(spec=AsyncParametersService) + service.create.return_value = parameter + vendor_client.catalog.products.product_parameters.return_value = service + + created = await create_parameter(context=context, mpt_vendor=vendor_client) + + assert created == parameter + assert context.get("catalog.parameter.id") == parameter.id + assert context.get(f"catalog.parameter[{parameter.id}]") == parameter + + +async def test_create_parameter_missing_product(context: Context, vendor_client: AsyncMock) -> None: + context["catalog.parameter_group.id"] = "group-123" + with pytest.raises(ValueError): + await create_parameter(context=context, mpt_vendor=vendor_client) diff --git a/tests/seed/catalog/test_product_parameters_group.py b/tests/seed/catalog/test_product_parameters_group.py new file mode 100644 index 00000000..74e6adf6 --- /dev/null +++ b/tests/seed/catalog/test_product_parameters_group.py @@ -0,0 +1,100 @@ +from unittest.mock import AsyncMock, patch + +import pytest + +from mpt_api_client.resources.catalog.products_parameter_groups import ( + AsyncParameterGroupsService, + ParameterGroup, +) +from seed.catalog.product_parameters_group import ( + build_parameter_group, + create_parameter_group, + get_parameter_group, + init_parameter_group, + seed_parameter_group, +) +from seed.context import Context + + +@pytest.fixture +def parameter_group() -> ParameterGroup: + return ParameterGroup({"id": "param-group-123"}) + + +@pytest.fixture +def parameter_groups_service(): + return AsyncMock(spec=AsyncParameterGroupsService) + + +async def test_get_parameter_group( + context: Context, vendor_client, parameter_groups_service, parameter_group +) -> None: + context["catalog.parameter_group.id"] = parameter_group.id + context["catalog.product.id"] = "product-123" + parameter_groups_service.get.return_value = parameter_group + vendor_client.catalog.products.parameter_groups.return_value = parameter_groups_service + + fetched_parameter_group = await get_parameter_group(context=context, mpt_vendor=vendor_client) + + assert fetched_parameter_group == parameter_group + assert context.get("catalog.parameter_group.id") == parameter_group.id + assert context.get(f"catalog.parameter_group[{parameter_group.id}]") == parameter_group + + +async def test_get_parameter_group_without_id(context: Context) -> None: + maybe_parameter_group = await get_parameter_group(context=context) + + assert maybe_parameter_group is None + + +def test_build_parameter_group(context: Context) -> None: + parameter_group_payload = build_parameter_group(context=context) + assert isinstance(parameter_group_payload, dict) + + +async def test_get_or_create_parameter_group_create_new( + context: Context, vendor_client, parameter_groups_service, parameter_group +) -> None: + context["catalog.product.id"] = "product-123" + + with ( + patch( + "seed.catalog.product_parameters_group.get_parameter_group", return_value=None + ) as get_mock, + patch( + "seed.catalog.product_parameters_group.create_parameter_group", + return_value=parameter_group, + ) as create_mock, + ): + created_parameter_group = await init_parameter_group( + context=context, mpt_vendor=vendor_client + ) + + assert created_parameter_group == parameter_group + get_mock.assert_called_once() + create_mock.assert_called_once() + + +async def test_create_parameter_group_success( + context: Context, vendor_client, parameter_group +) -> None: + context["catalog.product.id"] = "product-123" + service = AsyncMock(spec=AsyncParameterGroupsService) + service.create.return_value = parameter_group + vendor_client.catalog.products.parameter_groups.return_value = service + + created = await create_parameter_group(context=context, mpt_vendor=vendor_client) + + assert created == parameter_group + assert context.get("catalog.parameter_group.id") == parameter_group.id + assert context.get(f"catalog.parameter_group[{parameter_group.id}]") == parameter_group + + +async def test_seed_parameter_group() -> None: + with patch( + "seed.catalog.product_parameters_group.init_parameter_group", + new_callable=AsyncMock, + ) as mock_create: + await seed_parameter_group() + + mock_create.assert_called_once() diff --git a/tests/seed/test_seed_api.py b/tests/seed/test_seed_api.py new file mode 100644 index 00000000..a139e32c --- /dev/null +++ b/tests/seed/test_seed_api.py @@ -0,0 +1,37 @@ +import pathlib +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from seed.context import Context +from seed.seed_api import seed_api + + +@pytest.fixture +def mock_context(): + context = MagicMock(spec=Context) + context.load = MagicMock() + context.save = MagicMock() + return context + + +@pytest.fixture +def context_file_path(tmp_path): + return tmp_path / "context.json" + + +async def test_seed_api_success(mock_context): + with ( + patch("seed.seed_api.seed_catalog", new_callable=AsyncMock) as mock_seed_catalog, + patch("seed.seed_api.context_file") as mock_context_file, + patch("seed.seed_api.load_context") as load, + patch("seed.seed_api.save_context") as save, + ): + mock_seed_catalog.return_value = None + mock_context_file.return_value = pathlib.Path("test_context.json") + + await seed_api(context=mock_context) + + load.assert_called_once() + mock_seed_catalog.assert_called_once() + save.assert_called_once() diff --git a/tests/unit/http/test_mixins.py b/tests/unit/http/test_mixins.py index 227525bb..f7e94a53 100644 --- a/tests/unit/http/test_mixins.py +++ b/tests/unit/http/test_mixins.py @@ -262,6 +262,7 @@ def test_sync_file_create_with_data(media_service): b"Content-Type: image/jpeg\r\n\r\n" b"Image content\r\n" in request.content ) + assert "multipart/form-data" in request.headers["Content-Type"] assert new_media.to_dict() == media_data diff --git a/tests/unit/models/resource/test_resource.py b/tests/unit/models/resource/test_resource.py index 1f504096..314999bf 100644 --- a/tests/unit/models/resource/test_resource.py +++ b/tests/unit/models/resource/test_resource.py @@ -54,3 +54,19 @@ def test_wrong_data_type(): response = Response(200, json=1) with pytest.raises(TypeError, match=r"Response data must be a dict."): Model.from_response(response) + + +def test_id_property_with_string_id(): + resource_data = {"id": "abc-123"} + resource = Model(resource_data) + + assert resource.id == "abc-123" + assert isinstance(resource.id, str) + + +def test_id_property_with_numeric_id(): + resource_data = {"id": 1024} + resource = Model(resource_data) + + assert resource.id == "1024" + assert isinstance(resource.id, str) diff --git a/uv.lock b/uv.lock index c337b3de..ad7ba5f2 100644 --- a/uv.lock +++ b/uv.lock @@ -284,6 +284,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, ] +[[package]] +name = "dependency-injector" +version = "4.48.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/a4/619df82de38ce6451cc1acb549237cbd9306c4bfbcec6e8e1fdbceb8c5f3/dependency_injector-4.48.2.tar.gz", hash = "sha256:9ce6089d75a5dd0b6191a243f41d2c2746802bb39550ad431242c15136fefd60", size = 1103335, upload-time = "2025-09-19T10:19:43.492Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/56/dce91cc7638a4be4d83e18d20edd3f9b295440b1897d972f7a8ce3ea240f/dependency_injector-4.48.2-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:54d0178be10f17b768afb5c0ed1c5c565abaa2d097b2bc5a529a31c580613df2", size = 1755919, upload-time = "2025-09-19T10:18:53.97Z" }, + { url = "https://files.pythonhosted.org/packages/67/80/c29f5cb5fd794ea453b240e6d6682a07cdc519a4bd76589c4b75a1bb7a91/dependency_injector-4.48.2-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:12a15979fd534b728b3061c8aa52fd55adb77574758817daae9df8a1c2eb830b", size = 1855277, upload-time = "2025-09-19T10:18:55.911Z" }, + { url = "https://files.pythonhosted.org/packages/79/fa/20f14684dfb822f4b72623d4c1250149ba2fcc95a831ae334605eff31b33/dependency_injector-4.48.2-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:85cdf4b423884d4a24a18b970abe73352fb210761302cd6b5ebc6e9a20dbe53f", size = 1760596, upload-time = "2025-09-19T10:18:57.636Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c0/06dddb1b21f64bd1e948244aed1743c8013ea7800fc6e3e470b0019dd93e/dependency_injector-4.48.2-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a9b457b95a400b7a2de0978a55768cdd104bd265953bf0ed06e7f25d18f35ed2", size = 1742442, upload-time = "2025-09-19T10:18:59.041Z" }, + { url = "https://files.pythonhosted.org/packages/63/23/32575e230f5baf8082eb776847756024441207eabbb03ada679c29061070/dependency_injector-4.48.2-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:417809f565c39800adb744d666dfe4d94eae510b73ec33f932d592415d7c46d0", size = 1842421, upload-time = "2025-09-19T10:19:01.446Z" }, + { url = "https://files.pythonhosted.org/packages/3c/99/6595e8235d8e120129098d7a56b0491be313bab5415fc9d107ad1ae2a967/dependency_injector-4.48.2-cp310-abi3-win32.whl", hash = "sha256:f014aa7bab427932802d59967d9fe0863a0001db66446177dcc62e47f3a6b234", size = 1512262, upload-time = "2025-09-19T10:19:03.029Z" }, + { url = "https://files.pythonhosted.org/packages/96/04/cf1d482d163bf8c7cfd886cb4cf8eed950b366c2723dea2b21874ef2201c/dependency_injector-4.48.2-cp310-abi3-win_amd64.whl", hash = "sha256:e3fcdeb8189f3e1f87fde9276061f8a6cc596c2fa139bc4b4d1f571035ebd645", size = 1640200, upload-time = "2025-09-19T10:19:04.549Z" }, +] + [[package]] name = "dill" version = "0.4.0" @@ -598,6 +613,7 @@ dependencies = [ [package.dev-dependencies] dev = [ + { name = "dependency-injector" }, { name = "freezegun" }, { name = "ipdb" }, { name = "ipython" }, @@ -627,6 +643,7 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ + { name = "dependency-injector", specifier = ">=4.48.2" }, { name = "freezegun", specifier = "==1.5.*" }, { name = "ipdb", specifier = "==0.13.*" }, { name = "ipython", specifier = "==9.*" },