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 00000000..ab0c15a9 Binary files /dev/null and b/seed/catalog/FIL-9920-4780-9379.png differ 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.*" },