Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions mpt_api_client/resources/catalog/products.py
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down Expand Up @@ -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],
Expand Down
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
48 changes: 37 additions & 11 deletions seed/accounts/api_tokens.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
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."""
"""
Retrieve the ApiToken from context if present; otherwise fetch it from the API and store it in context.

If the token is fetched, it is saved in the context resources under "accounts.api_token" and "accounts.api_token.id" is updated.

Returns:
`ApiToken` if found or fetched, `None` if no token id is present in context.
"""
api_token_id = context.get_string("accounts.api_token.id")
if not api_token_id:
return None
Expand All @@ -34,9 +41,21 @@ 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."""
"""
Builds the dictionary of fields required to create an API token used for end-to-end testing.

The returned mapping includes:
- `account`: `{"id": <CLIENT_ACCOUNT_ID environment variable>}`
- `name`: token display name
- `description`: token description
- `icon`: token icon (empty string when not set)
- `modules`: list containing a module object with `id` read from the context key "accounts.module.id"

Returns:
dict[str, object]: The payload suitable for API token creation.
"""
account_id = os.getenv("CLIENT_ACCOUNT_ID")
module_id = context.get_string("accounts.module.id")
return {
Expand All @@ -50,10 +69,17 @@ 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."""
"""
Ensure an API token exists for the account, creating and persisting one if necessary.

If an API token is not already available, creates a new token and stores it in the context resource "accounts.api_token" and updates "accounts.api_token.id".

Returns:
ApiToken: The existing or newly created API token instance.
"""
api_token = await get_api_token(context=context, mpt_ops=mpt_ops)
if api_token is None:
logger.debug("Creating API token ...")
Expand All @@ -72,4 +98,4 @@ async def seed_api_token() -> None:
"""Seed API token."""
logger.debug("Seeding API token ...")
await init_api_token()
logger.debug("Seeding API token completed.")
logger.debug("Seeding API token completed.")
53 changes: 41 additions & 12 deletions seed/accounts/buyer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,31 @@
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

logger = logging.getLogger(__name__)

icon = pathlib.Path("seed/data/logo.png").resolve()
icon = pathlib.Path(__file__).parent.parent / "data/logo.png"


@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."""
"""
Retrieve the buyer identified by "accounts.buyer.id" from the context or fetch it from the API and cache it.

If the context does not contain "accounts.buyer.id", returns `None`. If the id exists but no valid Buyer instance is cached, fetches the buyer from the API, stores it in the context under "accounts.buyer", updates "accounts.buyer.id" with the fetched buyer's id, and returns the Buyer.

Returns:
Buyer or `None` if "accounts.buyer.id" is not set in the context.
"""
buyer_id = context.get_string("accounts.buyer.id")
if not buyer_id:
return None
Expand All @@ -36,8 +43,22 @@ async def get_buyer(


@inject
def build_buyer_data(context: Context = DEFAULT_CONTEXT) -> dict[str, object]:
"""Build buyer data dictionary for creation."""
def build_buyer_data(context: Context = Provide[Container.context]) -> dict[str, object]:
"""
Builds the payload dictionary used to create a buyer in the MPT API.

Reads CLIENT_ACCOUNT_ID from the environment and `accounts.seller.id` from the provided context to populate required account and seller references.

Parameters:
context (Context): Application context used to read `accounts.seller.id`.

Returns:
dict[str, object]: Buyer data payload including name, account, sellers, contact, and address.

Raises:
ValueError: If CLIENT_ACCOUNT_ID environment variable is missing.
ValueError: If `accounts.seller.id` is not found in the context.
"""
buyer_account_id = os.getenv("CLIENT_ACCOUNT_ID")
if not buyer_account_id:
raise ValueError("CLIENT_ACCOUNT_ID environment variable is required")
Expand Down Expand Up @@ -65,10 +86,18 @@ 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."""
"""
Ensure a Buyer exists in the provided context, creating one via the API if none is present.

Returns:
Buyer: The existing or newly created Buyer instance.

Raises:
ValueError: If buyer creation via the API fails.
"""
buyer = await get_buyer(context=context, mpt_operations=mpt_operations)
if buyer is None:
buyer_data = build_buyer_data(context=context)
Expand All @@ -91,4 +120,4 @@ async def seed_buyer() -> None:
"""Seed buyer."""
logger.debug("Seeding buyer ...")
await init_buyer()
logger.debug("Seeding buyer completed.")
logger.debug("Seeding buyer completed.")
52 changes: 41 additions & 11 deletions seed/accounts/licensee.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
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

logger = logging.getLogger(__name__)

Expand All @@ -16,10 +16,17 @@

@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."""
"""
Retrieve the current licensee from the context or fetch it from the MPT API and cache it in the context.

If a licensee is not present in the context but a licensee id exists, the function fetches the licensee from the MPT API, stores the licensee as the context resource "accounts.licensee", and updates "accounts.licensee.id" in the context.

Returns:
Licensee | None: `Licensee` instance if found, `None` if no licensee id is present.
"""
licensee_id = context.get_string("accounts.licensee.id")
if not licensee_id:
return None
Expand All @@ -37,9 +44,22 @@ 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."""
"""
Constructs a licensee payload dictionary used to create a licensee.

Parameters:
context (Context): Context used to read required values: `accounts.seller.id`, `accounts.buyer.id`, and the `accounts.user_group` resource.

Returns:
dict[str, object]: A dictionary containing licensee fields required by the API (name, address, seller, buyer, account, eligibility, groups, type, status, and defaultLanguage).

Raises:
ValueError: If the environment variable `CLIENT_ACCOUNT_ID` is not set.
ValueError: If `accounts.seller.id` or `accounts.buyer.id` is missing from the context.
ValueError: If the `accounts.user_group` resource is missing from the context.
"""
account_id = os.getenv("CLIENT_ACCOUNT_ID")
if not account_id:
raise ValueError("CLIENT_ACCOUNT_ID environment variable is required")
Expand Down Expand Up @@ -76,10 +96,20 @@ 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."""
"""
Ensure a licensee exists for the current context, creating one if none is present.

If no licensee is found in the provided context, attempts to create one using the MPT client and the prepared licensee data, then stores the created licensee in the context.

Returns:
The Licensee instance associated with the context.

Raises:
ValueError: If licensee creation fails.
"""
licensee = await get_licensee(context=context, mpt_client=mpt_client)
if licensee is None:
licensee_data = build_licensee_data(context=context)
Expand All @@ -102,4 +132,4 @@ async def seed_licensee() -> None:
"""Seed licensee."""
logger.debug("Seeding licensee ...")
await init_licensee()
logger.info("Seeding licensee completed.")
logger.info("Seeding licensee completed.")
32 changes: 23 additions & 9 deletions seed/accounts/module.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
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."""
"""
Retrieve the Module stored in the context or fetch it from the API if not cached.

If the context does not contain "accounts.module.id", returns None. When a cached resource is absent or not a Module, the function fetches the module from the API and stores it in the context.

Returns:
Module if found or fetched, `None` if "accounts.module.id" is not set.
"""
module_id = context.get_string("accounts.module.id")
if not module_id:
return None
Expand All @@ -34,10 +41,17 @@ 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)."""
"""
Ensure the context contains a current "Access Management" Module by fetching it if missing.

If the context has no module, query the API for modules named "Access Management". Cache the first found Module in the context under "accounts.module" and store its id at "accounts.module.id". Logs a warning and returns None if no suitable Module is found.

Returns:
Module if available, None otherwise.
"""
module = await get_module(context=context, mpt_operations=mpt_operations)
if module is None:
filtered_modules = mpt_operations.accounts.modules.filter(
Expand Down Expand Up @@ -69,4 +83,4 @@ async def seed_module() -> Module:
raise ValueError("Could not seed module: no valid Module found.")
return refreshed
logger.debug("Seeding module completed.")
return existing_module
return existing_module
Loading
Loading