Skip to content

Commit fd99d9e

Browse files
authored
MPT-14767 E2E seeding proof of concept (#99)
- Added seeding proof of concept - Fix create product request
2 parents 5a1f491 + cb8f666 commit fd99d9e

29 files changed

+1401
-1
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ dependencies = [
2626

2727
[dependency-groups]
2828
dev = [
29+
"dependency-injector>=4.48.2",
2930
"freezegun==1.5.*",
3031
"ipdb==0.13.*",
3132
"ipython==9.*",

seed/__init__.py

Whitespace-only changes.
84 KB
Loading

seed/catalog/__init__.py

Whitespace-only changes.

seed/catalog/catalog.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import asyncio
2+
import logging
3+
from typing import Any
4+
5+
from seed.catalog.item import seed_items
6+
from seed.catalog.item_group import seed_item_group
7+
from seed.catalog.product import seed_product
8+
from seed.catalog.product_parameters import seed_parameters
9+
from seed.catalog.product_parameters_group import seed_parameter_group
10+
11+
logger = logging.getLogger(__name__)
12+
13+
14+
async def seed_groups_and_group_params() -> None:
15+
"""Seed parallel tasks for item groups and parameter groups."""
16+
tasks: list[asyncio.Task[Any]] = [
17+
asyncio.create_task(seed_item_group()),
18+
asyncio.create_task(seed_parameter_group()),
19+
]
20+
await asyncio.gather(*tasks)
21+
22+
23+
async def seed_items_and_params() -> None:
24+
"""Seed final tasks for items and parameters."""
25+
tasks: list[asyncio.Task[Any]] = [
26+
asyncio.create_task(seed_items()),
27+
asyncio.create_task(seed_parameters()),
28+
]
29+
await asyncio.gather(*tasks)
30+
31+
32+
async def seed_catalog() -> None:
33+
"""Seed catalog data including products, item groups, and parameters."""
34+
logger.debug("Seeding catalog ...")
35+
await seed_product()
36+
await seed_groups_and_group_params()
37+
await seed_items_and_params()
38+
39+
logger.debug("Seeded catalog completed.")

seed/catalog/item.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import logging
2+
from typing import Any
3+
4+
from dependency_injector.wiring import inject
5+
6+
from mpt_api_client import AsyncMPTClient
7+
from mpt_api_client.resources.catalog.items import Item
8+
from seed.context import Context
9+
from seed.defaults import (
10+
DEFAULT_CONTEXT,
11+
DEFAULT_MPT_OPERATIONS,
12+
DEFAULT_MPT_VENDOR,
13+
)
14+
15+
logger = logging.getLogger(__name__)
16+
17+
18+
@inject
19+
async def refresh_item(
20+
context: Context = DEFAULT_CONTEXT,
21+
mpt_vendor: AsyncMPTClient = DEFAULT_MPT_VENDOR,
22+
) -> Item | None:
23+
"""Refresh item in context (always fetch)."""
24+
item_id = context.get_string("catalog.item.id")
25+
if not item_id:
26+
return None
27+
item_resource = await mpt_vendor.catalog.items.get(item_id)
28+
context["catalog.item.id"] = item_resource.id
29+
context.set_resource("catalog.item", item_resource)
30+
return item_resource
31+
32+
33+
@inject
34+
async def get_item(
35+
context: Context = DEFAULT_CONTEXT,
36+
mpt_vendor: AsyncMPTClient = DEFAULT_MPT_VENDOR,
37+
) -> Item | None:
38+
"""Get item from context or fetch from API if not cached."""
39+
item_id = context.get_string("catalog.item.id")
40+
if not item_id:
41+
return None
42+
try:
43+
catalog_item = context.get_resource("catalog.item", item_id)
44+
except ValueError:
45+
catalog_item = None
46+
if not isinstance(catalog_item, Item):
47+
logger.debug("Loading item: %s", item_id)
48+
catalog_item = await mpt_vendor.catalog.items.get(item_id)
49+
context["catalog.item.id"] = catalog_item.id
50+
context.set_resource("catalog.item", catalog_item)
51+
return catalog_item
52+
return catalog_item
53+
54+
55+
@inject
56+
def build_item(context: Context = DEFAULT_CONTEXT) -> dict[str, Any]:
57+
"""Build item data dictionary for creation."""
58+
product_id = context.get("catalog.product.id")
59+
item_group_id = context.get("catalog.item_group.id")
60+
return {
61+
"product": {"id": product_id},
62+
"parameters": [],
63+
"name": "Product Item 1",
64+
"description": "Product Item 1 - Description",
65+
"group": {"id": item_group_id},
66+
"unit": {
67+
"id": "UNT-1229",
68+
"name": "<string> 1",
69+
"revision": 1,
70+
"description": "<string>TEST",
71+
"statistics": {"itemCount": 34},
72+
},
73+
"terms": {"model": "quantity", "period": "1m", "commitment": "1m"},
74+
"quantityNotApplicable": False,
75+
"externalIds": {"vendor": "item_1"},
76+
}
77+
78+
79+
@inject
80+
async def create_item(
81+
context: Context = DEFAULT_CONTEXT,
82+
mpt_vendor: AsyncMPTClient = DEFAULT_MPT_VENDOR,
83+
) -> Item:
84+
"""Create item and cache in context."""
85+
item_data = build_item(context=context)
86+
catalog_item = await mpt_vendor.catalog.items.create(item_data)
87+
context["catalog.item.id"] = catalog_item.id
88+
context.set_resource("catalog.item", catalog_item)
89+
return catalog_item
90+
91+
92+
@inject
93+
async def review_item(
94+
context: Context = DEFAULT_CONTEXT,
95+
mpt_vendor: AsyncMPTClient = DEFAULT_MPT_VENDOR,
96+
) -> Item | None:
97+
"""Review item if in draft status and cache result."""
98+
logger.debug("Reviewing catalog.item ...")
99+
catalog_item = context.get_resource("catalog.item")
100+
if catalog_item.status != "Draft":
101+
return catalog_item # type: ignore[return-value]
102+
catalog_item = await mpt_vendor.catalog.items.review(catalog_item.id)
103+
context.set_resource("catalog.item", catalog_item)
104+
return catalog_item
105+
106+
107+
@inject
108+
async def publish_item(
109+
context: Context = DEFAULT_CONTEXT,
110+
mpt_operations: AsyncMPTClient = DEFAULT_MPT_OPERATIONS,
111+
) -> Item | None:
112+
"""Publish item if in reviewing status and cache result."""
113+
logger.debug("Publishing catalog.item ...")
114+
catalog_item = context.get_resource("catalog.item")
115+
if catalog_item.status != "Reviewing":
116+
return catalog_item # type: ignore[return-value]
117+
catalog_item = await mpt_operations.catalog.items.publish(catalog_item.id)
118+
context.set_resource("catalog.item", catalog_item)
119+
return catalog_item
120+
121+
122+
@inject
123+
async def seed_items(
124+
context: Context = DEFAULT_CONTEXT,
125+
mpt_vendor: AsyncMPTClient = DEFAULT_MPT_VENDOR,
126+
mpt_operations: AsyncMPTClient = DEFAULT_MPT_OPERATIONS,
127+
) -> None:
128+
"""Seed catalog items (create/review/publish)."""
129+
logger.debug("Seeding catalog.item ...")
130+
existing = await refresh_item(context=context, mpt_vendor=mpt_vendor)
131+
if not existing:
132+
await create_item(context=context, mpt_vendor=mpt_vendor)
133+
await review_item(context=context, mpt_vendor=mpt_vendor)
134+
await publish_item(context=context, mpt_operations=mpt_operations)
135+
logger.debug("Seeded catalog.item completed.")

seed/catalog/item_group.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import logging
2+
from typing import Any
3+
4+
from dependency_injector.wiring import inject
5+
6+
from mpt_api_client import AsyncMPTClient
7+
from mpt_api_client.resources.catalog.products_item_groups import ItemGroup
8+
from seed.context import Context
9+
from seed.defaults import DEFAULT_CONTEXT, DEFAULT_MPT_VENDOR
10+
11+
logger = logging.getLogger(__name__)
12+
13+
14+
@inject
15+
async def get_item_group(
16+
context: Context = DEFAULT_CONTEXT,
17+
mpt_vendor: AsyncMPTClient = DEFAULT_MPT_VENDOR,
18+
) -> ItemGroup | None:
19+
"""Get item group from context or fetch from API."""
20+
item_group_id = context.get_string("catalog.item_group.id")
21+
if not item_group_id:
22+
return None
23+
try:
24+
item_group = context.get_resource("catalog.item_group", item_group_id)
25+
except ValueError:
26+
item_group = None
27+
if not isinstance(item_group, ItemGroup):
28+
logger.debug("Refreshing item group: %s", item_group_id)
29+
product_id = context.get_string("catalog.product.id")
30+
item_group = await mpt_vendor.catalog.products.item_groups(product_id).get(item_group_id)
31+
return set_item_group(item_group, context=context)
32+
return item_group
33+
34+
35+
@inject
36+
def set_item_group(
37+
item_group: ItemGroup,
38+
context: Context = DEFAULT_CONTEXT,
39+
) -> ItemGroup:
40+
"""Set item group in context."""
41+
context["catalog.item_group.id"] = item_group.id
42+
context.set_resource("catalog.item_group", item_group)
43+
return item_group
44+
45+
46+
@inject
47+
def build_item_group(context: Context = DEFAULT_CONTEXT) -> dict[str, Any]:
48+
"""Build item group data dictionary."""
49+
product_id = context.get("catalog.product.id")
50+
return {
51+
"product": {"id": product_id},
52+
"name": "Items",
53+
"label": "Items",
54+
"description": "Default item group",
55+
"displayOrder": 100,
56+
"default": True,
57+
"multiple": True,
58+
"required": True,
59+
}
60+
61+
62+
@inject
63+
async def init_item_group(
64+
context: Context = DEFAULT_CONTEXT,
65+
mpt_vendor: AsyncMPTClient = DEFAULT_MPT_VENDOR,
66+
) -> ItemGroup:
67+
"""Get or create item group."""
68+
item_group = await get_item_group()
69+
70+
if not item_group:
71+
logger.debug("Creating item group ...")
72+
product_id = context.get_string("catalog.product.id")
73+
item_group_data = build_item_group()
74+
item_group = await mpt_vendor.catalog.products.item_groups(product_id).create(
75+
item_group_data
76+
)
77+
logger.debug("Item group created: %s", item_group.id)
78+
return set_item_group(item_group)
79+
logger.debug("Item group found: %s", item_group.id)
80+
return item_group
81+
82+
83+
async def seed_item_group() -> None:
84+
"""Seed item group."""
85+
logger.debug("Seeding catalog.item_group ...")
86+
await init_item_group()
87+
logger.debug("Seeded catalog.item_group completed.")

seed/catalog/product.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import logging
2+
import pathlib
3+
4+
from dependency_injector.wiring import inject
5+
6+
from mpt_api_client import AsyncMPTClient
7+
from mpt_api_client.resources.catalog.products import Product
8+
from seed.context import Context
9+
from seed.defaults import DEFAULT_CONTEXT, DEFAULT_MPT_OPERATIONS, DEFAULT_MPT_VENDOR
10+
11+
icon = pathlib.Path(__file__).parent / "FIL-9920-4780-9379.png"
12+
13+
logger = logging.getLogger(__name__)
14+
15+
namespace = "catalog.product"
16+
17+
18+
@inject
19+
async def get_product(
20+
context: Context = DEFAULT_CONTEXT,
21+
mpt_vendor: AsyncMPTClient = DEFAULT_MPT_VENDOR,
22+
) -> Product | None:
23+
"""Get product from context or fetch from API."""
24+
product_id = context.get_string(f"{namespace}.id")
25+
if not product_id:
26+
return None
27+
try:
28+
product = context.get_resource(namespace, product_id)
29+
except ValueError:
30+
product = None
31+
if not isinstance(product, Product):
32+
logger.debug("Refreshing product: %s", product_id)
33+
product = await mpt_vendor.catalog.products.get(product_id)
34+
context.set_resource(namespace, product)
35+
context[f"{namespace}.id"] = product.id
36+
return product
37+
return product
38+
39+
40+
@inject
41+
async def init_product(
42+
context: Context = DEFAULT_CONTEXT,
43+
mpt_vendor: AsyncMPTClient = DEFAULT_MPT_VENDOR,
44+
) -> Product:
45+
"""Get or create product."""
46+
product = await get_product()
47+
if not product:
48+
logger.debug("Creating product ...")
49+
with pathlib.Path.open(icon, "rb") as icon_file:
50+
product = await mpt_vendor.catalog.products.create(
51+
{"name": "Test Product", "website": "https://www.example.com"}, icon=icon_file
52+
)
53+
context.set_resource(namespace, product)
54+
context[f"{namespace}.id"] = product.id
55+
logger.debug("Product created: %s", product.id)
56+
return product
57+
58+
59+
@inject
60+
async def review_product(
61+
context: Context = DEFAULT_CONTEXT,
62+
mpt_vendor: AsyncMPTClient = DEFAULT_MPT_VENDOR,
63+
) -> Product | None:
64+
"""Review product if in draft status."""
65+
product = await get_product()
66+
if not product or product.status != "Draft":
67+
return product
68+
logger.debug("Reviewing product: %s", product.id)
69+
product = await mpt_vendor.catalog.products.review(product.id)
70+
context.set_resource(namespace, product)
71+
return product
72+
73+
74+
@inject
75+
async def publish_product(
76+
context: Context = DEFAULT_CONTEXT,
77+
mpt_operations: AsyncMPTClient = DEFAULT_MPT_OPERATIONS,
78+
) -> Product | None:
79+
"""Publish product if in reviewing status."""
80+
product = await get_product()
81+
if not product or product.status != "Reviewing":
82+
return product
83+
logger.debug("Publishing product: %s", product.id)
84+
product = await mpt_operations.catalog.products.publish(product.id)
85+
context.set_resource(namespace, product)
86+
return product
87+
88+
89+
async def seed_product() -> None:
90+
"""Seed product data."""
91+
logger.debug("Seeding catalog.product ...")
92+
await init_product()
93+
await review_product()
94+
await publish_product()
95+
logger.debug("Seeded catalog.product completed.")

0 commit comments

Comments
 (0)