From 403cccd2cf690d548c7a0720716f70475024c17b Mon Sep 17 00:00:00 2001 From: Albert Sola Date: Mon, 8 Dec 2025 15:36:10 +0000 Subject: [PATCH] Update seeding for Catalog --- mpt_api_client/resources/catalog/products.py | 12 +- pyproject.toml | 5 +- seed/accounts/__init__.py | 0 seed/accounts/api_tokens.py | 14 +- seed/accounts/buyer.py | 22 +- seed/accounts/licensee.py | 20 +- seed/accounts/module.py | 12 +- seed/accounts/seller.py | 19 +- seed/accounts/user_group.py | 14 +- seed/catalog/authorization.py | 39 +++ seed/catalog/catalog.py | 32 +- seed/catalog/item.py | 135 -------- seed/catalog/item_group.py | 87 ----- seed/catalog/listing.py | 46 +++ seed/catalog/price_list.py | 30 ++ seed/catalog/product.py | 298 +++++++++++++----- seed/catalog/product_parameters.py | 99 ------ seed/catalog/product_parameters_group.py | 82 ----- seed/container.py | 28 +- seed/context.py | 4 +- seed/defaults.py | 11 - seed/helper.py | 84 +++++ seed/seed_api.py | 19 +- .../FIL-9920-4780-9379.png | Bin seed/static/__init__.py | 0 seed/static/empty.pdf | Bin 0 -> 3840 bytes seed/{data => static}/logo.png | Bin seed/static/static.py | 7 + tests/seed/catalog/test_authorization.py | 40 +++ tests/seed/catalog/test_catalog.py | 44 +-- tests/seed/catalog/test_item.py | 127 -------- tests/seed/catalog/test_item_group.py | 98 ------ tests/seed/catalog/test_listing.py | 43 +++ tests/seed/catalog/test_price_list.py | 35 ++ tests/seed/catalog/test_product.py | 187 +++++++---- tests/seed/catalog/test_product_parameters.py | 110 ------- .../catalog/test_product_parameters_group.py | 103 ------ tests/seed/test_context.py | 17 + tests/seed/test_helper.py | 50 +++ 39 files changed, 800 insertions(+), 1173 deletions(-) create mode 100644 seed/accounts/__init__.py create mode 100644 seed/catalog/authorization.py delete mode 100644 seed/catalog/item.py delete mode 100644 seed/catalog/item_group.py create mode 100644 seed/catalog/listing.py create mode 100644 seed/catalog/price_list.py delete mode 100644 seed/catalog/product_parameters.py delete mode 100644 seed/catalog/product_parameters_group.py delete mode 100644 seed/defaults.py create mode 100644 seed/helper.py rename seed/{catalog => static}/FIL-9920-4780-9379.png (100%) create mode 100644 seed/static/__init__.py create mode 100644 seed/static/empty.pdf rename seed/{data => static}/logo.png (100%) create mode 100644 seed/static/static.py create mode 100644 tests/seed/catalog/test_authorization.py delete mode 100644 tests/seed/catalog/test_item.py delete mode 100644 tests/seed/catalog/test_item_group.py create mode 100644 tests/seed/catalog/test_listing.py create mode 100644 tests/seed/catalog/test_price_list.py delete mode 100644 tests/seed/catalog/test_product_parameters.py delete mode 100644 tests/seed/catalog/test_product_parameters_group.py create mode 100644 tests/seed/test_context.py create mode 100644 tests/seed/test_helper.py diff --git a/mpt_api_client/resources/catalog/products.py b/mpt_api_client/resources/catalog/products.py index 73f868b8..cfe04dde 100644 --- a/mpt_api_client/resources/catalog/products.py +++ b/mpt_api_client/resources/catalog/products.py @@ -65,9 +65,9 @@ class ProductsServiceConfig: class ProductsService( - CreateFileMixin[Model], - UpdateFileMixin[Model], - PublishableMixin[Model], + CreateFileMixin[Product], + UpdateFileMixin[Product], + PublishableMixin[Product], GetMixin[Product], DeleteMixin, CollectionMixin[Product], @@ -128,9 +128,9 @@ def update_settings(self, product_id: str, settings: ResourceData) -> Product: class AsyncProductsService( - AsyncCreateFileMixin[Model], - AsyncUpdateFileMixin[Model], - AsyncPublishableMixin[Model], + AsyncCreateFileMixin[Product], + AsyncUpdateFileMixin[Product], + AsyncPublishableMixin[Product], AsyncGetMixin[Product], AsyncDeleteMixin, AsyncCollectionMixin[Product], diff --git a/pyproject.toml b/pyproject.toml index 15df09c5..aed35e18 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -131,8 +131,11 @@ per-file-ignores = [ "tests/e2e/accounts/*.py: WPS430 WPS202", "tests/e2e/catalog/*.py: WPS202 WPS421", "tests/e2e/catalog/items/*.py: WPS110 WPS202", + "tests/seed/catalog/test_product.py: WPS202 WPS204 WPS219", "tests/*: WPS432 WPS202", - "seed/accounts/*.py: WPS453", + "seed/*: WPS404", + "seed/accounts/*.py: WPS204 WPS404 WPS453", + "seed/catalog/product.py: WPS202 WPS204 WPS217 WPS201 WPS213 WPS404" ] [tool.ruff] diff --git a/seed/accounts/__init__.py b/seed/accounts/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/seed/accounts/api_tokens.py b/seed/accounts/api_tokens.py index 9e21e4a0..4dfd984f 100644 --- a/seed/accounts/api_tokens.py +++ b/seed/accounts/api_tokens.py @@ -1,20 +1,20 @@ import logging import os -from dependency_injector.wiring import inject +from dependency_injector.wiring import Provide, inject from mpt_api_client import AsyncMPTClient from mpt_api_client.resources.accounts.api_tokens import ApiToken +from seed.container import Container from seed.context import Context -from seed.defaults import DEFAULT_CONTEXT, DEFAULT_MPT_OPERATIONS logger = logging.getLogger(__name__) @inject async def get_api_token( - context: Context = DEFAULT_CONTEXT, - mpt_ops: AsyncMPTClient = DEFAULT_MPT_OPERATIONS, + context: Context = Provide[Container.context], + mpt_ops: AsyncMPTClient = Provide[Container.mpt_operations], ) -> ApiToken | None: """Get API token from context or fetch from API.""" api_token_id = context.get_string("accounts.api_token.id") @@ -34,7 +34,7 @@ async def get_api_token( @inject def build_api_token_data( - context: Context = DEFAULT_CONTEXT, + context: Context = Provide[Container.context], ) -> dict[str, object]: """Get API token data dictionary for creation.""" account_id = os.getenv("CLIENT_ACCOUNT_ID") @@ -50,8 +50,8 @@ def build_api_token_data( @inject async def init_api_token( - context: Context = DEFAULT_CONTEXT, - mpt_ops: AsyncMPTClient = DEFAULT_MPT_OPERATIONS, + context: Context = Provide[Container.context], + mpt_ops: AsyncMPTClient = Provide[Container.mpt_operations], ) -> ApiToken: """Get or create API token.""" api_token = await get_api_token(context=context, mpt_ops=mpt_ops) diff --git a/seed/accounts/buyer.py b/seed/accounts/buyer.py index 44b95f9f..c45294a8 100644 --- a/seed/accounts/buyer.py +++ b/seed/accounts/buyer.py @@ -1,23 +1,21 @@ import logging import os -import pathlib -from dependency_injector.wiring import inject +from dependency_injector.wiring import Provide, inject from mpt_api_client import AsyncMPTClient from mpt_api_client.resources.accounts.buyers import Buyer +from seed.container import Container from seed.context import Context -from seed.defaults import DEFAULT_CONTEXT, DEFAULT_MPT_OPERATIONS +from seed.static.static import ICON logger = logging.getLogger(__name__) -icon = pathlib.Path("seed/data/logo.png").resolve() - @inject async def get_buyer( - context: Context = DEFAULT_CONTEXT, - mpt_operations: AsyncMPTClient = DEFAULT_MPT_OPERATIONS, + context: Context = Provide[Container.context], + mpt_operations: AsyncMPTClient = Provide[Container.mpt_operations], ) -> Buyer | None: """Get buyer from context or fetch from API.""" buyer_id = context.get_string("accounts.buyer.id") @@ -36,7 +34,7 @@ async def get_buyer( @inject -def build_buyer_data(context: Context = DEFAULT_CONTEXT) -> dict[str, object]: +def build_buyer_data(context: Context = Provide[Container.context]) -> dict[str, object]: """Build buyer data dictionary for creation.""" buyer_account_id = os.getenv("CLIENT_ACCOUNT_ID") if not buyer_account_id: @@ -65,16 +63,16 @@ def build_buyer_data(context: Context = DEFAULT_CONTEXT) -> dict[str, object]: @inject async def init_buyer( - context: Context = DEFAULT_CONTEXT, - mpt_operations: AsyncMPTClient = DEFAULT_MPT_OPERATIONS, + context: Context = Provide[Container.context], + mpt_operations: AsyncMPTClient = Provide[Container.mpt_operations], ) -> Buyer: """Get or create buyer.""" buyer = await get_buyer(context=context, mpt_operations=mpt_operations) if buyer is None: buyer_data = build_buyer_data(context=context) logger.debug("Creating buyer ...") - with open(str(icon), "rb") as icon_file: # noqa: PTH123 - created = await mpt_operations.accounts.buyers.create(buyer_data, file=icon_file) + with ICON.open("rb") as icon_fd: + created = await mpt_operations.accounts.buyers.create(buyer_data, file=icon_fd) if isinstance(created, Buyer): context.set_resource("accounts.buyer", created) context["accounts.buyer.id"] = created.id diff --git a/seed/accounts/licensee.py b/seed/accounts/licensee.py index ebb3e416..56c0f5e5 100644 --- a/seed/accounts/licensee.py +++ b/seed/accounts/licensee.py @@ -1,23 +1,21 @@ import logging import os -import pathlib -from dependency_injector.wiring import inject +from dependency_injector.wiring import Provide, inject from mpt_api_client import AsyncMPTClient from mpt_api_client.resources.accounts.licensees import Licensee +from seed.container import Container from seed.context import Context -from seed.defaults import DEFAULT_CONTEXT, DEFAULT_MPT_CLIENT +from seed.static.static import ICON logger = logging.getLogger(__name__) -icon = pathlib.Path("seed/data/logo.png").resolve() - @inject async def get_licensee( - context: Context = DEFAULT_CONTEXT, - mpt_client: AsyncMPTClient = DEFAULT_MPT_CLIENT, + context: Context = Provide[Container.context], + mpt_client: AsyncMPTClient = Provide[Container.mpt_client], ) -> Licensee | None: """Get licensee from context or fetch from API.""" licensee_id = context.get_string("accounts.licensee.id") @@ -37,7 +35,7 @@ async def get_licensee( @inject def build_licensee_data( # noqa: WPS238 - context: Context = DEFAULT_CONTEXT, + context: Context = Provide[Container.context], ) -> dict[str, object]: """Get licensee data dictionary for creation.""" account_id = os.getenv("CLIENT_ACCOUNT_ID") @@ -76,15 +74,15 @@ def build_licensee_data( # noqa: WPS238 @inject async def init_licensee( - context: Context = DEFAULT_CONTEXT, - mpt_client: AsyncMPTClient = DEFAULT_MPT_CLIENT, + context: Context = Provide[Container.context], + mpt_client: AsyncMPTClient = Provide[Container.mpt_client], ) -> Licensee: """Get or create licensee.""" licensee = await get_licensee(context=context, mpt_client=mpt_client) if licensee is None: licensee_data = build_licensee_data(context=context) logger.debug("Creating licensee ...") - with open(str(icon), "rb") as icon_file: # noqa: PTH123 + with ICON.open("rb") as icon_file: created = await mpt_client.accounts.licensees.create(licensee_data, file=icon_file) if isinstance(created, Licensee): context.set_resource("accounts.licensee", created) diff --git a/seed/accounts/module.py b/seed/accounts/module.py index 7fc705b4..2c0cd8d0 100644 --- a/seed/accounts/module.py +++ b/seed/accounts/module.py @@ -1,20 +1,20 @@ import logging -from dependency_injector.wiring import inject +from dependency_injector.wiring import Provide, inject from mpt_api_client import AsyncMPTClient from mpt_api_client.resources.accounts.modules import Module from mpt_api_client.rql.query_builder import RQLQuery +from seed.container import Container from seed.context import Context -from seed.defaults import DEFAULT_CONTEXT, DEFAULT_MPT_OPERATIONS logger = logging.getLogger(__name__) @inject async def get_module( - context: Context = DEFAULT_CONTEXT, - mpt_operations: AsyncMPTClient = DEFAULT_MPT_OPERATIONS, + context: Context = Provide[Container.context], + mpt_operations: AsyncMPTClient = Provide[Container.mpt_operations], ) -> Module | None: """Get module from context or fetch from API.""" module_id = context.get_string("accounts.module.id") @@ -34,8 +34,8 @@ async def get_module( @inject async def refresh_module( - context: Context = DEFAULT_CONTEXT, - mpt_operations: AsyncMPTClient = DEFAULT_MPT_OPERATIONS, + context: Context = Provide[Container.context], + mpt_operations: AsyncMPTClient = Provide[Container.mpt_operations], ) -> Module | None: """Refresh module in context (always fetch).""" module = await get_module(context=context, mpt_operations=mpt_operations) diff --git a/seed/accounts/seller.py b/seed/accounts/seller.py index 2194f6a5..61599185 100644 --- a/seed/accounts/seller.py +++ b/seed/accounts/seller.py @@ -1,21 +1,20 @@ -# mypy: disable-error-code=unreachable import logging import uuid -from dependency_injector.wiring import inject +from dependency_injector.wiring import Provide, inject from mpt_api_client import AsyncMPTClient from mpt_api_client.resources.accounts.sellers import Seller +from seed.container import Container from seed.context import Context -from seed.defaults import DEFAULT_CONTEXT, DEFAULT_MPT_OPERATIONS logger = logging.getLogger(__name__) @inject async def get_seller( - context: Context = DEFAULT_CONTEXT, - mpt_operations: AsyncMPTClient = DEFAULT_MPT_OPERATIONS, + context: Context = Provide[Container.context], + mpt_operations: AsyncMPTClient = Provide[Container.mpt_operations], ) -> Seller | None: """Get seller from context or fetch from API.""" seller_id = context.get_string("accounts.seller.id") @@ -33,7 +32,6 @@ async def get_seller( return seller -@inject def build_seller_data(external_id: str | None = None) -> dict[str, object]: """Get seller data dictionary for creation.""" if external_id is None: @@ -54,11 +52,11 @@ def build_seller_data(external_id: str | None = None) -> dict[str, object]: @inject async def init_seller( - context: Context = DEFAULT_CONTEXT, - mpt_operations: AsyncMPTClient = DEFAULT_MPT_OPERATIONS, + context: Context = Provide[Container.context], + mpt_operations: AsyncMPTClient = Provide[Container.mpt_operations], ) -> Seller | None: """Get or create seller. Returns Seller if successful, None otherwise.""" - seller = await get_seller(context=context, mpt_operations=mpt_operations) + seller = await get_seller() if seller is None: logger.debug("Creating seller ...") seller_data = build_seller_data() @@ -68,13 +66,12 @@ async def init_seller( context["accounts.seller.id"] = created.id logger.info("Seller created: %s", created.id) return created - logger.warning("Seller creation failed") + logger.warning("Seller creation failed") # type: ignore[unreachable] return None logger.info("Seller already exists: %s", seller.id) return seller -@inject async def seed_seller() -> None: """Seed seller.""" logger.debug("Seeding seller ...") diff --git a/seed/accounts/user_group.py b/seed/accounts/user_group.py index 203498cb..db064ca3 100644 --- a/seed/accounts/user_group.py +++ b/seed/accounts/user_group.py @@ -2,20 +2,20 @@ import logging import os -from dependency_injector.wiring import inject +from dependency_injector.wiring import Provide, inject from mpt_api_client import AsyncMPTClient from mpt_api_client.resources.accounts.user_groups import UserGroup +from seed.container import Container from seed.context import Context -from seed.defaults import DEFAULT_CONTEXT, DEFAULT_MPT_OPERATIONS logger = logging.getLogger(__name__) @inject async def get_user_group( - context: Context = DEFAULT_CONTEXT, - mpt_operations: AsyncMPTClient = DEFAULT_MPT_OPERATIONS, + context: Context = Provide[Container.context], + mpt_operations: AsyncMPTClient = Provide[Container.mpt_operations], ) -> UserGroup | None: """Get user group from context or fetch from API.""" user_group_id = context.get_string("accounts.user_group.id") @@ -35,7 +35,7 @@ async def get_user_group( @inject def build_user_group_data( - context: Context = DEFAULT_CONTEXT, + context: Context = Provide[Container.context], ) -> dict[str, object]: """Get user group data dictionary for creation.""" account_id = os.getenv("CLIENT_ACCOUNT_ID") @@ -54,8 +54,8 @@ def build_user_group_data( @inject async def init_user_group( - context: Context = DEFAULT_CONTEXT, - mpt_operations: AsyncMPTClient = DEFAULT_MPT_OPERATIONS, + context: Context = Provide[Container.context], + mpt_operations: AsyncMPTClient = Provide[Container.mpt_operations], ) -> UserGroup | None: """Get or create user group.""" user_group = await get_user_group(context=context, mpt_operations=mpt_operations) diff --git a/seed/catalog/authorization.py b/seed/catalog/authorization.py new file mode 100644 index 00000000..c462e545 --- /dev/null +++ b/seed/catalog/authorization.py @@ -0,0 +1,39 @@ +import uuid + +from dependency_injector.wiring import Provide, inject + +from mpt_api_client import AsyncMPTClient +from mpt_api_client.resources.catalog.authorizations import Authorization +from seed.container import Container +from seed.context import Context +from seed.helper import init_resource, require_context_id + + +async def seed_authorization() -> None: + """Seed authorization.""" + await init_resource("catalog.authorization.id", create_authorization) + + +@inject +async def create_authorization( + operations: AsyncMPTClient = Provide[Container.mpt_operations], + context: Context = Provide[Container.context], +) -> Authorization: + """Creates an authorization.""" + product_id = require_context_id(context, "catalog.product.id", "Create authorization") + seller_id = require_context_id(context, "accounts.seller.id", "Create authorization") + account_id = require_context_id(context, "accounts.account.id", "Create authorization") + short_uuid = uuid.uuid4().hex[:8] + + authorization_data = { + "externalIds": {"operations": f"e2e-seeded-{short_uuid}"}, + "product": {"id": product_id}, + "owner": {"id": seller_id}, + "journal": {"firstInvoiceDate": "2025-12-01", "frequency": "1m"}, + "eligibility": {"client": True, "partner": True}, + "currency": "USD", + "notes": "E2E Seeded", + "name": "E2E Seeded", + "vendor": {"id": account_id}, + } + return await operations.catalog.authorizations.create(authorization_data) diff --git a/seed/catalog/catalog.py b/seed/catalog/catalog.py index 1624a71a..9117b2d5 100644 --- a/seed/catalog/catalog.py +++ b/seed/catalog/catalog.py @@ -1,39 +1,19 @@ -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.authorization import seed_authorization +from seed.catalog.listing import seed_listing +from seed.catalog.price_list import seed_price_list 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() + await seed_authorization() + await seed_price_list() + await seed_listing() logger.debug("Seeded catalog completed.") diff --git a/seed/catalog/item.py b/seed/catalog/item.py deleted file mode 100644 index 19d74a9a..00000000 --- a/seed/catalog/item.py +++ /dev/null @@ -1,135 +0,0 @@ -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 deleted file mode 100644 index c8922cb9..00000000 --- a/seed/catalog/item_group.py +++ /dev/null @@ -1,87 +0,0 @@ -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/listing.py b/seed/catalog/listing.py new file mode 100644 index 00000000..d8004994 --- /dev/null +++ b/seed/catalog/listing.py @@ -0,0 +1,46 @@ +from dependency_injector.wiring import Provide, inject + +from mpt_api_client import AsyncMPTClient +from mpt_api_client.resources.catalog.listings import Listing +from seed.container import Container +from seed.context import Context +from seed.helper import init_resource, require_context_id + + +async def seed_listing() -> None: + """Seed listing.""" + await init_resource("catalog.listing.id", create_listing) + + +@inject +async def create_listing( # noqa: WPS210 + operations: AsyncMPTClient = Provide[Container.mpt_operations], + context: Context = Provide[Container.context], +) -> Listing: + """Creates a listing.""" + product_id = require_context_id(context, "catalog.product.id", "Create listing") + seller_id = require_context_id(context, "accounts.seller.id", "Create listing") + authorization_id = require_context_id(context, "catalog.authorization.id", "Create listing") + account_id = require_context_id(context, "accounts.account.id", "Create listing") + price_list_id = require_context_id(context, "catalog.price_list.id", "Create listing") + + listing_data = { + "name": "e2e - please delete", + "authorization": { + "id": authorization_id, + }, + "product": { + "id": product_id, + }, + "vendor": { + "id": account_id, + }, + "seller": { + "id": seller_id, + }, + "priceList": {"id": price_list_id}, + "primary": False, + "notes": "", + "eligibility": {"client": True, "partner": False}, + } + return await operations.catalog.listings.create(listing_data) diff --git a/seed/catalog/price_list.py b/seed/catalog/price_list.py new file mode 100644 index 00000000..6f85a4ce --- /dev/null +++ b/seed/catalog/price_list.py @@ -0,0 +1,30 @@ +from dependency_injector.wiring import Provide, inject + +from mpt_api_client import AsyncMPTClient +from mpt_api_client.resources.catalog.price_lists import PriceList +from seed.container import Container +from seed.context import Context +from seed.helper import init_resource, require_context_id + + +async def seed_price_list() -> None: + """Seed price list.""" + await init_resource("catalog.price_list.id", create_price_list) + + +@inject +async def create_price_list( + operations: AsyncMPTClient = Provide[Container.mpt_operations], + context: Context = Provide[Container.context], +) -> PriceList: + """Creates a price list.""" + product_id = require_context_id(context, "catalog.product.id", "Create price list") + + price_list_data = { + "notes": "E2E Seeded", + "defaultMarkup": "20.0", + "product": {"id": product_id}, + "currency": "USD", + "default": False, + } + return await operations.catalog.price_lists.create(price_list_data) diff --git a/seed/catalog/product.py b/seed/catalog/product.py index 471061b0..7e256820 100644 --- a/seed/catalog/product.py +++ b/seed/catalog/product.py @@ -1,110 +1,234 @@ import logging -import pathlib +import uuid -from dependency_injector.wiring import inject +from dependency_injector.wiring import Provide, inject from mpt_api_client import AsyncMPTClient +from mpt_api_client.resources.catalog.items import Item +from mpt_api_client.resources.catalog.product_term_variants import TermVariant +from mpt_api_client.resources.catalog.product_terms import Term from mpt_api_client.resources.catalog.products import Product +from mpt_api_client.resources.catalog.products_documents import Document +from mpt_api_client.resources.catalog.products_item_groups import ItemGroup +from mpt_api_client.resources.catalog.products_parameter_groups import ParameterGroup +from mpt_api_client.resources.catalog.products_parameters import Parameter +from mpt_api_client.resources.catalog.products_templates import Template +from mpt_api_client.resources.catalog.units_of_measure import UnitOfMeasure +from seed.container import Container 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" +from seed.helper import init_resource, require_context_id +from seed.static.static import ICON, PDF logger = logging.getLogger(__name__) -namespace = "catalog.product" + +@inject +async def create_product( + mpt_vendor: AsyncMPTClient = Provide[Container.mpt_vendor], +) -> Product: + """Creates a product.""" + logger.debug("Creating product ...") + with ICON.open("rb") as icon_fd: + return await mpt_vendor.catalog.products.create( + {"name": "E2E Seeded", "website": "https://www.example.com"}, file=icon_fd + ) + + +async def seed_product() -> None: + """Seed product data.""" + logger.debug("Seeding catalog.product ...") + await init_resource("catalog.product.id", create_product) + await init_resource("catalog.unit.id", create_unit_of_measure) + await init_resource("catalog.product.item_group.id", create_item_group) + await init_resource("catalog.product.item.id", create_product_item) + await init_resource("catalog.product.document.id", create_document) + await init_resource("catalog.product.parameter_group.id", create_parameter_group) + await init_resource("catalog.product.parameter.id", create_parameter) + await init_resource("catalog.product.template.id", create_template) + await init_resource("catalog.product.terms.id", create_terms) + await init_resource("catalog.product.terms.variant.id", create_terms_variant) + logger.debug("Seeded catalog.product completed.") @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") - logger.debug("Getting product: %s", product_id) - result: Product | None = None - if product_id: - try: - maybe = context.get_resource(namespace, product_id) - except ValueError: - maybe = None - if isinstance(maybe, Product): - result = maybe - else: - logger.debug("Refreshing product: %s", product_id) - refreshed = await mpt_vendor.catalog.products.get(product_id) - if isinstance(refreshed, Product): - context.set_resource(namespace, refreshed) - context[f"{namespace}.id"] = refreshed.id - result = refreshed - return result +async def create_terms_variant( + context: Context = Provide[Container.context], + mpt_vendor: AsyncMPTClient = Provide[Container.mpt_vendor], +) -> TermVariant: + """Creates a product terms variant.""" + term_variant_data = { + "name": "E2E seeding", + "description": "Test variant description", + "languageCode": "en-gb", + "type": "File", + "assetUrl": "", + } + product_id = require_context_id(context, "catalog.product.id", "creating product terms variant") + terms_id = require_context_id( + context, "catalog.product.terms.id", "creating product terms variant" + ) + with PDF.open("rb") as pdf_fd: + return ( + await mpt_vendor.catalog.products.terms(product_id) + .variants(terms_id) + .create(term_variant_data, file=pdf_fd) + ) @inject -async def init_product( - context: Context = DEFAULT_CONTEXT, - mpt_vendor: AsyncMPTClient = DEFAULT_MPT_VENDOR, -) -> Product: - """Get or create product.""" - product = await get_product(context=context, mpt_vendor=mpt_vendor) - if product is None: - logger.debug("Creating product ...") - with open(str(icon), "rb") as icon_file: # noqa: PTH123 - created = await mpt_vendor.catalog.products.create( - {"name": "E2E Seeded", "website": "https://www.example.com"}, file=icon_file - ) - if isinstance(created, Product): - context.set_resource(namespace, created) - context[f"{namespace}.id"] = created.id - logger.info("Product created: %s", created.id) - return created - logger.warning("Product creation failed") - raise ValueError("Product creation failed") - logger.info("Product found: %s", product.id) - return product +async def create_template( + context: Context = Provide[Container.context], + mpt_vendor: AsyncMPTClient = Provide[Container.mpt_vendor], +) -> Template: + """Creates a product template.""" + template_data = { + "name": "E2E Seeding", + "description": "A template for testing", + "content": "template content", + "type": "OrderProcessing", + } + product_id = require_context_id(context, "catalog.product.id", "creating product template") + return await mpt_vendor.catalog.products.templates(product_id).create(template_data) @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(context=context, mpt_vendor=mpt_vendor) - if not isinstance(product, Product) or product.status != "Draft": - return product - logger.debug("Reviewing product: %s", product.id) - reviewed = await mpt_vendor.catalog.products.review(product.id) - if isinstance(reviewed, Product): - context.set_resource(namespace, reviewed) - return reviewed - logger.warning("Product review failed") - return None +async def create_terms( + context: Context = Provide[Container.context], + mpt_vendor: AsyncMPTClient = Provide[Container.mpt_vendor], +) -> Term: + """Creates a product terms.""" + product_id = require_context_id(context, "catalog.product.id", "creating product terms") + return await mpt_vendor.catalog.products.terms(product_id).create({ + "name": "E2E seeded", + "description": "E2E seeded", + }) @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 isinstance(product, Product) or product.status != "Reviewing": - return product - logger.debug("Publishing product: %s", product.id) - published = await mpt_operations.catalog.products.publish(product.id) - if isinstance(published, Product): - context.set_resource(namespace, published) - return published - logger.warning("Product publish failed") - return None +async def create_parameter_group( + context: Context = Provide[Container.context], + mpt_vendor: AsyncMPTClient = Provide[Container.mpt_vendor], +) -> ParameterGroup: + """Creates a product parameter group.""" + product_id = require_context_id( + context, "catalog.product.id", "creating product parameter group" + ) + return await mpt_vendor.catalog.products.parameter_groups(product_id).create({ + "name": "E2E Seeded", + "label": "E2E Seeded", + "displayOrder": 100, + }) -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.") +@inject +async def create_parameter( + context: Context = Provide[Container.context], + mpt_vendor: AsyncMPTClient = Provide[Container.mpt_vendor], +) -> Parameter: + """Creates a product parameter.""" + parameter_group_id = require_context_id( + context, "catalog.product.parameter_group.id", "creating product parameter" + ) + parameter_data = { + "constraints": {"hidden": False, "readonly": False, "required": False}, + "description": "E2E seeded", + "displayOrder": 100, + "name": "E2E seeded", + "phase": "Order", + "scope": "Order", + "type": "SingleLineText", + "context": "Purchase", + "options": { + "hintText": "e2e seeded", + "defaultValue": "default value", + "placeholderText": "Place holder text", + }, + "group": {"id": parameter_group_id}, + } + product_id = require_context_id(context, "catalog.product.id", "creating product parameter") + return await mpt_vendor.catalog.products.parameters(product_id).create(parameter_data) + + +@inject +async def create_document( + context: Context = Provide[Container.context], + mpt_vendor: AsyncMPTClient = Provide[Container.mpt_vendor], +) -> Document: + """Creates a product document.""" + product_id = require_context_id(context, "catalog.product.id", "creating product document") + document_data = { + "name": "E2E Seeded", + "description": "E2E Seeded", + "language": "en-gb", + "url": "", + "documenttype": "File", + } + with PDF.open("rb") as pdf_fd: + return await mpt_vendor.catalog.products.documents(product_id).create( + document_data, file=pdf_fd + ) + + +@inject +async def create_item_group( + context: Context = Provide[Container.context], + mpt_vendor: AsyncMPTClient = Provide[Container.mpt_vendor], +) -> ItemGroup: + """Creates a product item group.""" + product_id = require_context_id(context, "catalog.product.id", "creating product item group") + item_group_data = { + "product": {"id": product_id}, + "name": "E2E Seeded", + "label": "E2E Seeded", + "description": "E2E Seeded", + "displayOrder": 100, + "default": False, + "multiple": True, + "required": True, + } + + return await mpt_vendor.catalog.products.item_groups(product_id).create(item_group_data) + + +@inject +async def create_unit_of_measure( + operations: AsyncMPTClient = Provide[Container.mpt_operations], +) -> UnitOfMeasure: + """Creates a new unit of measure in the vendor's catalog.""" + short_uuid = uuid.uuid4().hex[:8] + return await operations.catalog.units_of_measure.create({ + "name": f"e2e seeded {short_uuid}", + "description": "e2e seeded", + }) + + +@inject +async def create_product_item( + context: Context = Provide[Container.context], + mpt_vendor: AsyncMPTClient = Provide[Container.mpt_vendor], +) -> Item: + """Creates a product item.""" + short_uuid = uuid.uuid4().hex[:8] + + unit_id = require_context_id(context, "catalog.unit.id", "creating product item") + item_group_id = require_context_id( + context, "catalog.product.item_group.id", "creating product item" + ) + product_id = require_context_id(context, "catalog.product.id", "creating product item") + + product_item_data = { + "name": "e2e - please delete", + "description": "e2e - please delete", + "unit": { + "id": unit_id, + }, + "group": { + "id": item_group_id, + }, + "product": { + "id": product_id, + }, + "terms": {"model": "quantity", "period": "1m", "commitment": "1m"}, + "externalIds": {"vendor": f"e2e-delete-{short_uuid}"}, + } + return await mpt_vendor.catalog.items.create(product_item_data) diff --git a/seed/catalog/product_parameters.py b/seed/catalog/product_parameters.py deleted file mode 100644 index e8a36a20..00000000 --- a/seed/catalog/product_parameters.py +++ /dev/null @@ -1,99 +0,0 @@ -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.product.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.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.product.parameter_group.id") - if not parameter_group_id: - raise ValueError("Parameter group id is required.") - return { - "name": "e2e - seed", - "scope": "Order", - "phase": "Order", - "description": "e2e - seeded parameter", - "externalId": "e2e-seed-parameter", - "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.parameters(product_id).create(parameter_data) - logger.info("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=context, mpt_vendor=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 deleted file mode 100644 index 9b6bba75..00000000 --- a/seed/catalog/product_parameters_group.py +++ /dev/null @@ -1,82 +0,0 @@ -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.product.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": "e2e - seed", - "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 index 378a2393..ede91a66 100644 --- a/seed/container.py +++ b/seed/container.py @@ -9,7 +9,6 @@ class Container(containers.DeclarativeContainer): config = providers.Configuration() - # Client factories mpt_client = providers.Factory( AsyncMPTClient.from_config, api_token=config.mpt_api_token_client, @@ -28,14 +27,11 @@ class Container(containers.DeclarativeContainer): 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") @@ -44,26 +40,4 @@ class Container(containers.DeclarativeContainer): 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", - "seed.accounts", - "seed.accounts.accounts", - "seed.accounts.api_tokens", - "seed.accounts.buyer", - "seed.accounts.licensee", - "seed.accounts.module", - "seed.accounts.seller", - "seed.accounts.user_group", - ] - ) + container.wire(packages=["seed"]) diff --git a/seed/context.py b/seed/context.py index d086a2ef..6c98e47d 100644 --- a/seed/context.py +++ b/seed/context.py @@ -33,7 +33,7 @@ def set_resource(self, namespace: str, resource: Model) -> None: # noqa: WPS615 self[f"{namespace}[{resource.id}]"] = resource -def load_context(json_file: pathlib.Path, context: Context | None = None) -> Context: +def load_context(json_file: pathlib.Path, context: Context) -> None: """Load context from JSON file. Args: @@ -44,12 +44,10 @@ def load_context(json_file: pathlib.Path, context: Context | None = None) -> Con 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) logger.info("Context loaded: %s", context.items()) - return context def save_context(context: Context, json_file: pathlib.Path) -> None: diff --git a/seed/defaults.py b/seed/defaults.py deleted file mode 100644 index 3e9e23db..00000000 --- a/seed/defaults.py +++ /dev/null @@ -1,11 +0,0 @@ -"""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/helper.py b/seed/helper.py new file mode 100644 index 00000000..6cc4ae31 --- /dev/null +++ b/seed/helper.py @@ -0,0 +1,84 @@ +import logging +from collections.abc import Awaitable, Callable +from typing import Any + +from dependency_injector.wiring import Provide, inject + +from seed.container import Container +from seed.context import Context + +logger = logging.getLogger(__name__) + + +class ResourceRequiredError(Exception): + """Raised when a resource is required but not found.""" + + def __init__(self, context: Context, key: str, action: str): + super().__init__(f"Missing required resource '{key}' before {action}.") + self.context = context + self.key = key + self.action = action + + +def require_context_id(context: Context, key: str, action: str) -> str: + """Fetch an ID from context, ensuring it exists and is non-empty. + + Args: + context: The seeding context. + key: The expected context key where the id is stored. + action: A short description of what we are creating (for error message). + + Returns: + The id string stored in the context under the provided key. + + Raises: + ResourceRequiredError: If the id is missing or empty in context. + """ + resource_id = context.get_string(key) + if not resource_id: + raise ResourceRequiredError(context, key, action) + return resource_id + + +@inject +async def init_resource( + namespace: str, + resource_factory: Callable[[], Awaitable[Any]], + context: Context = Provide[Container.context], +) -> str: + """Initialize a resource on demand and cache its id in the Context. + + This helper reads an id from the provided Context at the given namespace. If the id + is missing or empty, it will call the provided async ``resource_factory`` to create + the resource, extract the ``id`` from the returned object, store it back into the + Context under ``namespace``, and return it. If the id already exists in the Context, + the factory is not called and the existing id is returned. + + Args: + namespace: The Context key where the resource id is stored (e.g., + "catalog.product.id"). + resource_factory: A zero-argument async callable that creates the resource when + needed. It must return an object with an ``id`` attribute (string-like). + context: The seeding Context used to read and persist the id. Defaults to + ``DEFAULT_CONTEXT``. + + Returns: + The resource id string, either retrieved from the Context or obtained from the + newly created resource. + + Notes: + - The factory is invoked only if the id is not already present in the Context. + - Any exceptions thrown by ``resource_factory`` will propagate to the caller. + - No additional validation of the returned id is performed beyond attribute access. + + Example: + In an async function/context you can do: + id_value = await init_resource("catalog.product.id", create_product, context) + """ + logger.debug("Initializing resource: %s", namespace) + id_value = context.get_string(namespace) + if not id_value: + resource = await resource_factory() + id_value = resource.id + context[namespace] = id_value + return id_value diff --git a/seed/seed_api.py b/seed/seed_api.py index 49ffe523..5175f3b4 100644 --- a/seed/seed_api.py +++ b/seed/seed_api.py @@ -1,13 +1,12 @@ -import asyncio import logging import pathlib -from dependency_injector.wiring import inject +from dependency_injector.wiring import Provide, inject from seed.accounts.accounts import seed_accounts from seed.catalog.catalog import seed_catalog +from seed.container import Container from seed.context import Context, load_context, save_context -from seed.defaults import DEFAULT_CONTEXT logger = logging.getLogger(__name__) @@ -15,18 +14,12 @@ @inject -async def seed_api(context: Context = DEFAULT_CONTEXT) -> None: +async def seed_api(context: Context = Provide[Container.context]) -> None: """Seed API.""" - tasks: list[asyncio.Task[object]] = [] - load_context(context_file, context) - - catalog_task = asyncio.create_task(seed_catalog()) - accounts_task = asyncio.create_task(seed_accounts()) - tasks.extend([catalog_task, accounts_task]) - - try: - await asyncio.gather(*tasks) + try: # noqa: WPS229 + await seed_accounts() + await seed_catalog() except Exception: logger.exception("Exception occurred during seeding.") finally: diff --git a/seed/catalog/FIL-9920-4780-9379.png b/seed/static/FIL-9920-4780-9379.png similarity index 100% rename from seed/catalog/FIL-9920-4780-9379.png rename to seed/static/FIL-9920-4780-9379.png diff --git a/seed/static/__init__.py b/seed/static/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/seed/static/empty.pdf b/seed/static/empty.pdf new file mode 100644 index 0000000000000000000000000000000000000000..5abbd77a4955ff1b3736f45e405394c832c35750 GIT binary patch literal 3840 zcmai%c{r5s8pj7y7$ud6RBy5tV>Vma2a_m7WvtE0*hXXQ$&w{ovL#AF5waAOXvmT! z9g}RSkQfq@$S&vAuYRX_XyY65I9Q2h7S`rU=a$(Hx^tt(7eGJ zap5dOM*?ogqlx7)HWskh9xicHPuM*|FI^9S02f7*VH9`rPkV5*z9r;uTeI}@0QX`> zaQyMlptv&u6!`fBpmc)bN%vxqD4qc3F9Az;XM*27f%T(-6OK{HG=d)87qCHqg0i|Y z9Hj;x2MJeGw}XLa`WbC~^{+&jG3X=<3KOsaL+Tp>c7PI=?n-A^co0YwU_Db6 zUxmyXYt8iyIDN4${8L5BGyUnSP56P{(MLVKj#Gg>l@PO)P-q>~iYpkCWhMN1yw-Nx z_SS}Z?wvwhf>vBl2dPvjZb6^BbA_dumUibzW7*ticrdx@twN2NjA!>+P=cf?R%#;z zX3(Xgab8B0!<}qV_Dl@527Nx8B9DqdqC~Rzv+qg{HI%r{<&7MyYPp&!Gn^%kZxefh zxe;4s)}Jmtm=g=lvb`$(-ZAGKFNI}kln&v{%XM@tTsMIrgwq^ejLKxWMxhLZ#Xd)U z<%aMjH>f{h2*xa`JKccz<~j~XL!*p{xm$a#(l{TRsY}yk7K=pW!Q}d5gBN{G*t6xE zE+IsNOXyb&xTNuJX-)}~MG}*f`0f#>#hTuMJ?AY}HJkmHPfp8Buy5$b-oOvIPTW;u zi9qJ0tgr1Zt4=uyDR^IthrWkN?^Ky;#U%c2Pn93%v&I_##JyoauqorQq zdgfEn>F|#y%|9mdhebJ6g=>UZU51lkVL@GkK|57sdBx6WxClFWIY0u#)y*o5c$?{( z5bS;)DR*su?!a*yAtbE1`iWCfLWyTQ%K!W5(p~}EX!k)AT}g|PxlQq8hIF=qSk6G0 zYc2P-%e>5+FiZ#>!L4v+vqUi3gQp*WgascF;<>^DKf+DZf!^2EdJTc;B42ZPUvTz- zbcXo9gdPu7@z`h!J^ggEhrp&_oDM9a))>LnA41~`{UWp@Tesk{$gM4Ddfh_1toi(~ zx@Ky^JkPLEmxU5_7GKKT*?HhXK^E3bb9O&dypqrE!qQFNmjGttDpz3G)~nndgw3xZ z4}*J44@>aj>RR9H`|h~TEBf^9;L!>ZEx|{1qJz7>Kor6q>XP_$n)!Iz4XyZgP3!Aj zwvyu7y3Zv+@)mc#LHcFETe}{|9Z5Jl%(F2vi*R+PNTb+LBtCYKc#6dWu|?f9OxIG3f)rc`Gt6HUo|X9B52P*{NBbI*~|RaP?yjx z`>p^|u2Pml;(Vigt6^K~Fy}lD^aGCuD&dmQsWYiVEk#L%2`(w)sR7ca$vvq)sSfZ=S%-Z=N5AK; zRB)Tu>eO0<XyqlgziC_gioev%lH_-LDHzRp~?HYY*lRh3})mpdx-s3rG#d`_C!8q@qqh*b19N3)D*6i_yUuY^e*A9i~_d;(@uo5 zj5EQx*Lk@MUKq#DWBc4-XEtCyjomL{G@m)rcqfe z-95b}os&L_X)1TJ7I*-)=2(wDut%h%eSTp{l`e}>&nhaO%u{UFy46>xSwy;1X62Z3 zysT}zT_W5@RritYBMTorQ)4A7C5IC8sVpt?%%aR&HMkm^pyy9t0&mX5=w7S6lNFs+ zL%2c686@p5NGNPSkYk;5xKWMt@ND$7){u=%X;g+$qETGSF^kuo{g!7c+2!m_E+bAd zm_p^MrtH#pc>G<%?wjiM8qMc&{H`q?6wr*Zipdb^6LCFoI(CRxHbC}Xhf&@@2EI+cZF5_Wz$VczqMeF68SQ5WD@-Hl$@6|D$W+LznEHb zI^lH5qr`dQl=D=c)*-DZtvIdi)!x;Gfl3R6Rqj>qrM{V-FQZF6%cf8@=w1Gguv<_= z=qX+;0TlnX4PMatTCUov(7j5p9HeYJMY#1=u-@Ss{K}i`b}V`Bgezd~%1roGob)~K z>jPCJ8t7vis*c1rziP(C#EBS-_!zd}o*8uEju`|Pq#78fS}EYmh7Dt_V}V>vk-fWi z1W0)^^5?ne+XAM;s;dx%rE&BeTQl!|tuSTV`68&nr`veY_m2y}w7S)GB{= z5iAPtF1CI9__&WhX9t!0kxymsZ2pILfX8_vrpk^PA?S)n}Z_rbZg|9_yJ8 zvJ~5F1YMuG?r@##qm~spzfG02Z~sPY{Sf`ZRQg@w_y9ie^kUzOxv)vr?YY~_KJUHx zeBSkqRnAzY5%KoDFN@#X6>(mC&HnnR>hwhQDT2+}Gh1l!V0+=189L|j0#t3E z`t*mUg}qZZHfi+wMF$p)$*yM3J$z{$ZJpKqt~=`CEfrdTNx;BZa7V`Ow%v-_*VfL> z#?GasmpykhW+$}mxPDDN)93tGy~gnAm?+W6L#LTqmF2^)GKx!!`&&a&OYEl~GxFcE zmRC-u7CIKI+Z_(f2xLu0tj2Zksk(8+Vd6Y(?7@^9Ys{l;^~zAurD~90m+Y36`sXSG zIhWQlcJah)T>1>J4p_5)r#TkbzuL~5zr|krg}>UW(<*6VC|ee0df-NZcF}6rLJu{c zH9K*$=M1~-qGNnS1y*S-WwFGxegiw7ebBJn@Dr{G$Btc$?eo7k`|L|=Y--0)c#Zt` zhw~}zg_-AlmRXL(mWD4b?c-_MNBi$ot1orWxs8%P4tMy_rgHP5`wOeJ=R#JqmPxZs z31btc3#KWVaoRrT_*cIA%D7QS2Ij+Nmj5K_byQtvTaagsuvk5UCxr~Gv+4=J?iaeQ z)ATR={TFO|{%?F<=VC7q#gTxLF^%jA^71;fgRZ|ZvDzQTSOSyaN_YGV=RJRN{J$Vt z<>!K5W`L3v-Wt^YLUwTQ_ny^%x27>&DS(^@nF=TZ6gLm1pF9|VK_`2Wz?MeNjX*NB z08UXDo;12UfKq`Y;7Gt$PS1LkUcvRT zK6rIBDC$TuhKNFuRLCl-R5djcfq*6 Context: + ctx = Context() + ctx["catalog.product.id"] = "prod-123" + ctx["accounts.seller.id"] = "seller-456" + ctx["accounts.account.id"] = "acct-321" + return ctx + + +async def test_create_authorization(mocker, operations_client, context_with_data): + create_mock = mocker.AsyncMock(return_value={"id": "auth-1"}) + operations_client.catalog.authorizations.create = create_mock + fake_uuid = mocker.Mock(hex="cafebabe12345678") + mocker.patch("uuid.uuid4", return_value=fake_uuid) + + result = await create_authorization(operations_client, context_with_data) + + assert result == {"id": "auth-1"} + args, _ = create_mock.await_args + payload = args[0] + assert payload["product"]["id"] == "prod-123" + assert payload["owner"]["id"] == "seller-456" + assert payload["vendor"]["id"] == "acct-321" + + +async def test_seed_authorization(mocker): + init_resource = mocker.patch( + "seed.catalog.authorization.init_resource", + new_callable=mocker.AsyncMock, + ) + + await seed_authorization() + + init_resource.assert_called_once_with("catalog.authorization.id", create_authorization) diff --git a/tests/seed/catalog/test_catalog.py b/tests/seed/catalog/test_catalog.py index 4cce3e8c..c5dc0e99 100644 --- a/tests/seed/catalog/test_catalog.py +++ b/tests/seed/catalog/test_catalog.py @@ -1,44 +1,20 @@ 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() # act - - 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() # act - - mock_items.assert_called_once() - mock_params.assert_called_once() +from seed.catalog.catalog import seed_catalog 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_product", new_callable=AsyncMock) as seed_product, patch( - "seed.catalog.catalog.seed_groups_and_group_params", new_callable=AsyncMock - ) as seed_groups_and_group_params, + "seed.catalog.catalog.seed_authorization", new_callable=AsyncMock + ) as seed_authorization, + patch("seed.catalog.catalog.seed_price_list", new_callable=AsyncMock) as seed_price_list, + patch("seed.catalog.catalog.seed_listing", new_callable=AsyncMock) as seed_listing, ): await seed_catalog() # act - mock_product.assert_called_once() - seed_items_and_params.assert_called_once() - seed_groups_and_group_params.assert_called_once() + seed_product.assert_called_once() + seed_authorization.assert_called_once() + seed_price_list.assert_called_once() + seed_listing.assert_called_once() diff --git a/tests/seed/catalog/test_item.py b/tests/seed/catalog/test_item.py deleted file mode 100644 index eecdbdf6..00000000 --- a/tests/seed/catalog/test_item.py +++ /dev/null @@ -1,127 +0,0 @@ -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 - - result = await get_item(context=context, mpt_vendor=vendor_client) - - assert result == resource_item - assert context.get(f"catalog.item[{resource_item.id}]") == resource_item - - -async def test_get_item_without_id(context: Context) -> None: - result = await get_item(context=context) - - assert result 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 - - result = await create_item(context=context, mpt_vendor=vendor_client) - - assert result == 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 - - result = await review_item(context=context, mpt_vendor=vendor_client) - - assert result == 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) # act - - 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 - - result = await publish_item(context=context, mpt_operations=operations_client) - - assert result == 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) # act - - 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) # act - - 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 deleted file mode 100644 index c043ce7c..00000000 --- a/tests/seed/catalog/test_item_group.py +++ /dev/null @@ -1,98 +0,0 @@ -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 - - result = await get_item_group(context=context, mpt_vendor=vendor_client) - - assert result == 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: - result = await get_item_group(context=context) - - assert result is None - - -def test_set_item_group(context, item_group) -> None: - result = set_item_group(item_group, context=context) - - assert result == 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" - - result = build_item_group(context=context) - - assert result["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, - ): - result = await init_item_group(context=context, mpt_vendor=vendor_client) - - assert result == 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() # act - - mock_create.assert_called_once() diff --git a/tests/seed/catalog/test_listing.py b/tests/seed/catalog/test_listing.py new file mode 100644 index 00000000..085c2c64 --- /dev/null +++ b/tests/seed/catalog/test_listing.py @@ -0,0 +1,43 @@ +import pytest + +from seed.catalog.listing import create_listing, seed_listing +from seed.context import Context + + +@pytest.fixture +def context_with_data() -> Context: + ctx = Context() + ctx["catalog.product.id"] = "prod-123" + ctx["accounts.seller.id"] = "seller-456" + ctx["catalog.authorization.id"] = "auth-789" + ctx["accounts.account.id"] = "acct-321" + ctx["catalog.price_list.id"] = "pl-654" + return ctx + + +async def test_create_listing(mocker, operations_client, context_with_data): # noqa: WPS218 + create_mock = mocker.AsyncMock(return_value={"id": "lst-1"}) + operations_client.catalog.listings.create = create_mock + + result = await create_listing(operations_client, context_with_data) + + assert result == {"id": "lst-1"} + args, _ = create_mock.await_args + payload = args[0] + assert payload["product"]["id"] == "prod-123" + assert payload["seller"]["id"] == "seller-456" + assert payload["authorization"]["id"] == "auth-789" + assert payload["vendor"]["id"] == "acct-321" + assert payload["priceList"]["id"] == "pl-654" + + +async def test_seed_listing(mocker, context_with_data): + init_resource = mocker.patch( + "seed.catalog.listing.init_resource", + new_callable=mocker.AsyncMock, + return_value=mocker.Mock(id="lst-999"), + ) + + await seed_listing() + + init_resource.assert_awaited_once_with("catalog.listing.id", create_listing) diff --git a/tests/seed/catalog/test_price_list.py b/tests/seed/catalog/test_price_list.py new file mode 100644 index 00000000..e62aefdf --- /dev/null +++ b/tests/seed/catalog/test_price_list.py @@ -0,0 +1,35 @@ +import pytest + +from seed.catalog.price_list import create_price_list, seed_price_list +from seed.context import Context + + +@pytest.fixture +def context_with_product(): + ctx = Context() + ctx["catalog.product.id"] = "prod-123" + return ctx + + +async def test_create_price_list(mocker, operations_client, context_with_product): + create_mock = mocker.AsyncMock(return_value={"id": "pl-1"}) + operations_client.catalog.price_lists.create = create_mock + + result = await create_price_list(operations_client, context_with_product) + + assert result == {"id": "pl-1"} + args, _ = create_mock.await_args + payload = args[0] + assert payload["product"]["id"] == "prod-123" + + +async def test_seed_price_list_create(mocker): + init_resource = mocker.patch( + "seed.catalog.price_list.init_resource", + new_callable=mocker.AsyncMock, + return_value=mocker.Mock(id="pl-999"), + ) + + await seed_price_list() + + init_resource.assert_awaited_once_with("catalog.price_list.id", create_price_list) diff --git a/tests/seed/catalog/test_product.py b/tests/seed/catalog/test_product.py index 7703093e..8679d862 100644 --- a/tests/seed/catalog/test_product.py +++ b/tests/seed/catalog/test_product.py @@ -1,12 +1,17 @@ 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 mpt_api_client.resources.catalog.products import Product +from seed.catalog.product import ( # noqa: WPS235 + create_document, + create_item_group, + create_parameter, + create_parameter_group, + create_product, + create_product_item, + create_template, + create_terms, + create_terms_variant, + create_unit_of_measure, ) from seed.context import Context @@ -17,86 +22,128 @@ def product(): @pytest.fixture -def products_service(mocker): - return mocker.Mock(spec=AsyncProductsService) +def context_with_product(): + context = Context() + context["catalog.product.id"] = "prod-123" + return context -async def test_get_product(context: Context, vendor_client, product, products_service): - context["catalog.product.id"] = product.id - products_service.get.return_value = product - vendor_client.catalog.products = products_service +async def test_create_product(mocker, context: Context, vendor_client, product): + mpt_vendor = vendor_client + mpt_vendor.catalog.products.create = mocker.AsyncMock(return_value=product) - result = await get_product(context=context, mpt_vendor=vendor_client) + result = await create_product(mpt_vendor) assert result == product - assert context.get_resource("catalog.product", product.id) == product -async def test_get_product_without_id(context: Context): - result = await get_product(context=context) +async def test_create_template(mocker, vendor_client, context_with_product): + context = context_with_product - assert result is None + create_mock = mocker.AsyncMock(return_value={"id": "tmpl-1"}) + vendor_client.catalog.products.templates.return_value.create = create_mock + result = await create_template(context, vendor_client) -async def test_get_or_create_product_create_new( # noqa: WPS211 - context: Context, vendor_client, products_service, product, fs, mocker -): - products_service.create.return_value = product - vendor_client.catalog.products = products_service - fs.create_file( - "/mpt_api_client/seed/catalog/FIL-9920-4780-9379.png", contents=b"fake_icon_bytes" - ) - mock_get_product = mocker.patch( - "seed.catalog.product.get_product", new_callable=mocker.AsyncMock - ) - mock_get_product.return_value = None - result = await init_product(context=context, mpt_vendor=vendor_client) - assert result == product - products_service.create.assert_called_once() + assert result == {"id": "tmpl-1"} + create_mock.assert_awaited_once() -async def test_review_product_draft_status( - context, products_service, vendor_client, product, mocker -): - product.status = "Draft" - products_service.review.return_value = product - vendor_client.catalog.products = products_service - mocker.patch("seed.catalog.product.get_product", return_value=product) - result = await review_product(context, mpt_vendor=vendor_client) - assert result == product - products_service.review.assert_called_once() +async def test_create_terms(mocker, vendor_client, context_with_product): + context = context_with_product + create_mock = mocker.AsyncMock(return_value={"id": "term-1"}) + vendor_client.catalog.products.terms.return_value.create = create_mock -async def test_review_product_non_draft_status(product, mocker): - product.status = "Published" - mocker.patch("seed.catalog.product.get_product", return_value=product) - result = await review_product() - assert result == product + result = await create_terms(context, vendor_client) + assert result == {"id": "term-1"} + create_mock.assert_awaited_once() -async def test_publish_product_reviewing_status(context, operations_client, product, mocker): - product.status = "Reviewing" - operations_client.catalog.products.publish = mocker.AsyncMock(return_value=product) - mocker.patch("seed.catalog.product.get_product", return_value=product) - result = await publish_product(context, mpt_operations=operations_client) - assert result == product - operations_client.catalog.products.publish.assert_called_once() +async def test_create_terms_variant(mocker, vendor_client, context_with_product): + context = context_with_product + context["catalog.product.terms.id"] = "terms-123" -async def test_publish_product_non_reviewing_status(product, mocker): - product.status = "Draft" - mocker.patch("seed.catalog.product.get_product", return_value=product) - result = await publish_product() - assert result == product + create_mock = mocker.AsyncMock(return_value={"id": "variant-1"}) + vendor_client.catalog.products.terms.return_value.variants.return_value.create = create_mock + + result = await create_terms_variant(context, vendor_client) + + assert result == {"id": "variant-1"} + + +async def test_create_parameter_group(mocker, vendor_client, context_with_product): + context = context_with_product + + create_mock = mocker.AsyncMock(return_value={"id": "pg-1"}) + vendor_client.catalog.products.parameter_groups.return_value.create = create_mock + + result = await create_parameter_group(context, vendor_client) + + assert result == {"id": "pg-1"} + create_mock.assert_awaited_once() + + +async def test_create_parameter(mocker, vendor_client, context_with_product): + context = context_with_product + context["catalog.product.parameter_group.id"] = "pg-1" + + create_mock = mocker.AsyncMock(return_value={"id": "param-1"}) + vendor_client.catalog.products.parameters.return_value.create = create_mock + + result = await create_parameter(context, vendor_client) + + assert result == {"id": "param-1"} + create_mock.assert_awaited_once() + + +async def test_create_document(mocker, vendor_client, context_with_product): + context = context_with_product + + create_mock = mocker.AsyncMock(return_value={"id": "doc-1"}) + vendor_client.catalog.products.documents.return_value.create = create_mock + + result = await create_document(context, vendor_client) + + assert result == {"id": "doc-1"} + + +async def test_create_item_group(mocker, vendor_client, context_with_product): + context = context_with_product + + create_mock = mocker.AsyncMock(return_value={"id": "ig-1"}) + vendor_client.catalog.products.item_groups.return_value.create = create_mock + + result = await create_item_group(context, vendor_client) + + assert result == {"id": "ig-1"} + args, _ = create_mock.await_args + payload = args[0] + assert payload["product"]["id"] == "prod-123" + + +async def test_create_unit_of_measure(mocker, vendor_client): + create_mock = mocker.AsyncMock(return_value={"id": "uom-1"}) + vendor_client.catalog.units_of_measure.create = create_mock + + result = await create_unit_of_measure(vendor_client) + + assert result == {"id": "uom-1"} + create_mock.assert_awaited_once() + + +async def test_create_product_item(mocker, vendor_client, context_with_product): + context_with_product["catalog.unit.id"] = "unit-1" + context_with_product["catalog.product.item_group.id"] = "ig-1" + create_mock = mocker.AsyncMock(return_value={"id": "item-1"}) + vendor_client.catalog.items.create = create_mock + result = await create_product_item(context_with_product, vendor_client) -async def test_seed_product_sequence(mocker): - mock_create = mocker.patch("seed.catalog.product.init_product", new_callable=mocker.AsyncMock) - mock_review = mocker.patch("seed.catalog.product.review_product", new_callable=mocker.AsyncMock) - mock_publish = mocker.patch( - "seed.catalog.product.publish_product", new_callable=mocker.AsyncMock - ) - await seed_product() # act - mock_create.assert_called_once() - mock_review.assert_called_once() - mock_publish.assert_called_once() + assert result == {"id": "item-1"} + args, _ = create_mock.await_args + payload = args[0] + assert payload["unit"]["id"] == "unit-1" + assert payload["group"]["id"] == "ig-1" + assert payload["product"]["id"] == "prod-123" diff --git a/tests/seed/catalog/test_product_parameters.py b/tests/seed/catalog/test_product_parameters.py deleted file mode 100644 index d603db3c..00000000 --- a/tests/seed/catalog/test_product_parameters.py +++ /dev/null @@ -1,110 +0,0 @@ -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.product.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.parameters.return_value = service - - result = await get_parameter(context=context, mpt_vendor=vendor_client) - - assert result == 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: - result = await get_parameter(context=context) - - assert result is None - - -def test_build_parameter(context: Context) -> None: - context["catalog.product.parameter_group.id"] = "group-123" - - result = build_parameter(context=context) - - assert result["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.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), - ): - result = await init_parameter(context=context, mpt_vendor=vendor_client) - - assert result == 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.product.parameter_group.id"] = "group-123" - service = AsyncMock(spec=AsyncParametersService) - service.create.return_value = parameter - vendor_client.catalog.products.parameters.return_value = service - - result = await create_parameter(context=context, mpt_vendor=vendor_client) - - assert result == parameter - assert context.get("catalog.product.parameter.id") == parameter.id - assert context.get(f"catalog.product.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 deleted file mode 100644 index d3dd5cc7..00000000 --- a/tests/seed/catalog/test_product_parameters_group.py +++ /dev/null @@ -1,103 +0,0 @@ -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.product.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 - - result = await get_parameter_group(context=context, mpt_vendor=vendor_client) - - assert result == parameter_group - assert context.get("catalog.product.parameter_group.id") == parameter_group.id - assert context.get(f"catalog.product.parameter_group[{parameter_group.id}]") == parameter_group - - -async def test_get_parameter_group_without_id(context: Context) -> None: - result = await get_parameter_group(context=context) - - assert result is None - - -def test_build_parameter_group(context: Context) -> None: - parameter_group_payload = build_parameter_group(context=context) - - result = isinstance(parameter_group_payload, dict) - - assert result is True - - -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.product.parameter_group.id") == parameter_group.id - assert context.get(f"catalog.product.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_context.py b/tests/seed/test_context.py new file mode 100644 index 00000000..a9daf6b7 --- /dev/null +++ b/tests/seed/test_context.py @@ -0,0 +1,17 @@ +import json +from pathlib import Path + +from seed.context import Context, load_context + + +def test_load_context(tmp_path): + context = Context({"keep": "yes", "overwrite": "old"}) + json_data = {"overwrite": "new", "added": 123} + json_file: Path = tmp_path / "context.json" + json_file.write_text(json.dumps(json_data), encoding="utf-8") + + load_context(json_file, context) # act + + assert context["keep"] == "yes" + assert context["overwrite"] == "new" + assert context["added"] == 123 diff --git a/tests/seed/test_helper.py b/tests/seed/test_helper.py new file mode 100644 index 00000000..21d23e30 --- /dev/null +++ b/tests/seed/test_helper.py @@ -0,0 +1,50 @@ +import pytest + +from seed.context import Context +from seed.helper import ResourceRequiredError, init_resource, require_context_id + + +def test_require_context_id_returns_value(): + context = Context() + context["catalog.product.id"] = "prod-123" + + result = require_context_id(context, "catalog.product.id", "creating product") + + assert result == "prod-123" + + +def test_require_context_id_raises_when_missing(): + context = Context() + key = "catalog.product.id" + action = "creating product" + + with pytest.raises(ResourceRequiredError) as exc_info: + require_context_id(context, key, action) + + assert exc_info.value.key == key + assert exc_info.value.action == action + assert exc_info.value.context is context + assert str(exc_info.value) == f"Missing required resource '{key}' before {action}." + + +async def test_init_resource_existing_id(mocker): + context = Context() + context["catalog.product.id"] = "prod-123" + factory = mocker.AsyncMock() + + result = await init_resource("catalog.product.id", factory, context) + + assert result == "prod-123" + factory.assert_not_called() + + +async def test_init_resource_creates(mocker): + context = Context() + resource = mocker.Mock(id="new-456") + factory = mocker.AsyncMock(return_value=resource) + + result = await init_resource("catalog.product.id", factory, context) + + assert result == "new-456" + factory.assert_awaited_once() + assert context.get_string("catalog.product.id") == "new-456"