mpt-api-client 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. mpt_api_client/__init__.py +4 -0
  2. mpt_api_client/exceptions.py +69 -0
  3. mpt_api_client/http/__init__.py +11 -0
  4. mpt_api_client/http/async_client.py +106 -0
  5. mpt_api_client/http/async_service.py +74 -0
  6. mpt_api_client/http/base_service.py +58 -0
  7. mpt_api_client/http/client.py +108 -0
  8. mpt_api_client/http/mixins.py +463 -0
  9. mpt_api_client/http/query_state.py +72 -0
  10. mpt_api_client/http/service.py +79 -0
  11. mpt_api_client/http/types.py +42 -0
  12. mpt_api_client/models/__init__.py +6 -0
  13. mpt_api_client/models/collection.py +34 -0
  14. mpt_api_client/models/file_model.py +55 -0
  15. mpt_api_client/models/meta.py +56 -0
  16. mpt_api_client/models/model.py +62 -0
  17. mpt_api_client/mpt_client.py +130 -0
  18. mpt_api_client/resources/__init__.py +21 -0
  19. mpt_api_client/resources/accounts/__init__.py +3 -0
  20. mpt_api_client/resources/accounts/account.py +76 -0
  21. mpt_api_client/resources/accounts/account_user_groups.py +38 -0
  22. mpt_api_client/resources/accounts/account_users.py +66 -0
  23. mpt_api_client/resources/accounts/accounts.py +145 -0
  24. mpt_api_client/resources/accounts/accounts_user_groups.py +38 -0
  25. mpt_api_client/resources/accounts/accounts_users.py +68 -0
  26. mpt_api_client/resources/accounts/api_tokens.py +41 -0
  27. mpt_api_client/resources/accounts/buyers.py +91 -0
  28. mpt_api_client/resources/accounts/cloud_tenants.py +38 -0
  29. mpt_api_client/resources/accounts/erp_links.py +49 -0
  30. mpt_api_client/resources/accounts/licensees.py +41 -0
  31. mpt_api_client/resources/accounts/mixins.py +272 -0
  32. mpt_api_client/resources/accounts/modules.py +32 -0
  33. mpt_api_client/resources/accounts/sellers.py +60 -0
  34. mpt_api_client/resources/accounts/user_groups.py +38 -0
  35. mpt_api_client/resources/accounts/users.py +111 -0
  36. mpt_api_client/resources/audit/__init__.py +3 -0
  37. mpt_api_client/resources/audit/audit.py +40 -0
  38. mpt_api_client/resources/audit/event_types.py +42 -0
  39. mpt_api_client/resources/audit/records.py +42 -0
  40. mpt_api_client/resources/billing/__init__.py +3 -0
  41. mpt_api_client/resources/billing/billing.py +101 -0
  42. mpt_api_client/resources/billing/credit_memo_attachments.py +42 -0
  43. mpt_api_client/resources/billing/credit_memos.py +64 -0
  44. mpt_api_client/resources/billing/custom_ledger_attachments.py +42 -0
  45. mpt_api_client/resources/billing/custom_ledger_charges.py +38 -0
  46. mpt_api_client/resources/billing/custom_ledger_upload.py +31 -0
  47. mpt_api_client/resources/billing/custom_ledgers.py +95 -0
  48. mpt_api_client/resources/billing/invoice_attachments.py +42 -0
  49. mpt_api_client/resources/billing/invoices.py +64 -0
  50. mpt_api_client/resources/billing/journal_attachments.py +42 -0
  51. mpt_api_client/resources/billing/journal_charges.py +38 -0
  52. mpt_api_client/resources/billing/journal_sellers.py +38 -0
  53. mpt_api_client/resources/billing/journal_upload.py +38 -0
  54. mpt_api_client/resources/billing/journals.py +107 -0
  55. mpt_api_client/resources/billing/ledger_attachments.py +42 -0
  56. mpt_api_client/resources/billing/ledger_charges.py +38 -0
  57. mpt_api_client/resources/billing/ledgers.py +78 -0
  58. mpt_api_client/resources/billing/manual_overrides.py +46 -0
  59. mpt_api_client/resources/billing/mixins.py +394 -0
  60. mpt_api_client/resources/billing/statement_charges.py +38 -0
  61. mpt_api_client/resources/billing/statements.py +63 -0
  62. mpt_api_client/resources/catalog/__init__.py +3 -0
  63. mpt_api_client/resources/catalog/authorizations.py +38 -0
  64. mpt_api_client/resources/catalog/catalog.py +104 -0
  65. mpt_api_client/resources/catalog/items.py +44 -0
  66. mpt_api_client/resources/catalog/listings.py +38 -0
  67. mpt_api_client/resources/catalog/mixins.py +130 -0
  68. mpt_api_client/resources/catalog/price_list_items.py +38 -0
  69. mpt_api_client/resources/catalog/price_lists.py +54 -0
  70. mpt_api_client/resources/catalog/pricing_policies.py +111 -0
  71. mpt_api_client/resources/catalog/pricing_policy_attachments.py +45 -0
  72. mpt_api_client/resources/catalog/product_term_variants.py +48 -0
  73. mpt_api_client/resources/catalog/product_terms.py +59 -0
  74. mpt_api_client/resources/catalog/products.py +214 -0
  75. mpt_api_client/resources/catalog/products_documents.py +45 -0
  76. mpt_api_client/resources/catalog/products_item_groups.py +38 -0
  77. mpt_api_client/resources/catalog/products_media.py +113 -0
  78. mpt_api_client/resources/catalog/products_parameter_groups.py +38 -0
  79. mpt_api_client/resources/catalog/products_parameters.py +38 -0
  80. mpt_api_client/resources/catalog/products_templates.py +38 -0
  81. mpt_api_client/resources/catalog/units_of_measure.py +38 -0
  82. mpt_api_client/resources/commerce/__init__.py +3 -0
  83. mpt_api_client/resources/commerce/agreements.py +94 -0
  84. mpt_api_client/resources/commerce/agreements_attachments.py +46 -0
  85. mpt_api_client/resources/commerce/commerce.py +51 -0
  86. mpt_api_client/resources/commerce/orders.py +220 -0
  87. mpt_api_client/resources/commerce/orders_subscription.py +38 -0
  88. mpt_api_client/resources/commerce/subscriptions.py +97 -0
  89. mpt_api_client/resources/notifications/__init__.py +3 -0
  90. mpt_api_client/resources/notifications/accounts.py +27 -0
  91. mpt_api_client/resources/notifications/batches.py +128 -0
  92. mpt_api_client/resources/notifications/categories.py +74 -0
  93. mpt_api_client/resources/notifications/contacts.py +54 -0
  94. mpt_api_client/resources/notifications/messages.py +38 -0
  95. mpt_api_client/resources/notifications/notifications.py +111 -0
  96. mpt_api_client/resources/notifications/subscribers.py +38 -0
  97. mpt_api_client/rql/__init__.py +3 -0
  98. mpt_api_client/rql/constants.py +7 -0
  99. mpt_api_client/rql/query_builder.py +519 -0
  100. mpt_api_client-0.0.1.dist-info/METADATA +19 -0
  101. mpt_api_client-0.0.1.dist-info/RECORD +103 -0
  102. mpt_api_client-0.0.1.dist-info/WHEEL +4 -0
  103. mpt_api_client-0.0.1.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,4 @@
1
+ from mpt_api_client.mpt_client import AsyncMPTClient, MPTClient
2
+ from mpt_api_client.rql import RQLQuery
3
+
4
+ __all__ = ["AsyncMPTClient", "MPTClient", "RQLQuery"] # noqa: WPS410
@@ -0,0 +1,69 @@
1
+ import json
2
+ from typing import override
3
+
4
+ from httpx import HTTPStatusError
5
+
6
+
7
+ class MPTError(Exception):
8
+ """Represents a generic MPT error."""
9
+
10
+
11
+ class MPTHttpError(MPTError):
12
+ """Represents an HTTP error."""
13
+
14
+ def __init__(self, status_code: int, message: str, body: str):
15
+ self.status_code = status_code
16
+ self.body = body
17
+ super().__init__(f"HTTP {status_code}: {message}")
18
+
19
+
20
+ class MPTAPIError(MPTHttpError):
21
+ """Represents an API error."""
22
+
23
+ def __init__(self, status_code: int, message: str, payload: dict[str, str]):
24
+ super().__init__(status_code, message, json.dumps(payload))
25
+ self.payload = payload
26
+ self.status: str | None = payload.get("status") or payload.get("statusCode")
27
+ self.title: str | None = payload.get("title") or payload.get("message")
28
+ self.detail: str | None = payload.get("detail") or message
29
+ self.trace_id: str | None = payload.get("traceId")
30
+ self.errors: str | None = payload.get("errors")
31
+
32
+ @override
33
+ def __str__(self) -> str:
34
+ base = f"{self.status} {self.title} - {self.detail} ({self.trace_id or 'no-trace-id'})" # noqa: WPS221 WPS237
35
+
36
+ if self.errors:
37
+ return f"{base}\n{json.dumps(self.errors, indent=2)}"
38
+ return base
39
+
40
+ @override
41
+ def __repr__(self) -> str:
42
+ return str(self.payload)
43
+
44
+
45
+ def transform_http_status_exception(http_status_exception: HTTPStatusError) -> MPTError:
46
+ """Transforms httpx exceptions into MPT exceptions.
47
+
48
+ Attempts to extract API related information from HTTPStatusError and
49
+ raises MPTAPIError or MPTHttpError.
50
+
51
+ Args:
52
+ http_status_exception: Native httpx exception
53
+
54
+ Returns:
55
+ MPTError
56
+ """
57
+ try:
58
+ return MPTAPIError(
59
+ status_code=http_status_exception.response.status_code,
60
+ message=http_status_exception.args[0],
61
+ payload=http_status_exception.response.json(),
62
+ )
63
+ except json.JSONDecodeError:
64
+ body = http_status_exception.response.content.decode()
65
+ return MPTHttpError(
66
+ status_code=http_status_exception.response.status_code,
67
+ message=http_status_exception.args[0],
68
+ body=body,
69
+ )
@@ -0,0 +1,11 @@
1
+ from mpt_api_client.http.async_client import AsyncHTTPClient
2
+ from mpt_api_client.http.async_service import AsyncService
3
+ from mpt_api_client.http.client import HTTPClient
4
+ from mpt_api_client.http.service import Service
5
+
6
+ __all__ = [ # noqa: WPS410
7
+ "AsyncHTTPClient",
8
+ "AsyncService",
9
+ "HTTPClient",
10
+ "Service",
11
+ ]
@@ -0,0 +1,106 @@
1
+ import os
2
+ from typing import Any
3
+
4
+ from httpx import (
5
+ AsyncClient,
6
+ AsyncHTTPTransport,
7
+ HTTPError,
8
+ HTTPStatusError,
9
+ )
10
+
11
+ from mpt_api_client.exceptions import MPTError, transform_http_status_exception
12
+ from mpt_api_client.http.types import (
13
+ HeaderTypes,
14
+ QueryParam,
15
+ RequestFiles,
16
+ Response,
17
+ )
18
+
19
+
20
+ class AsyncHTTPClient:
21
+ """Async HTTP client for interacting with SoftwareOne Marketplace Platform API."""
22
+
23
+ def __init__(
24
+ self,
25
+ *,
26
+ base_url: str | None = None,
27
+ api_token: str | None = None,
28
+ timeout: float = 5.0,
29
+ retries: int = 5,
30
+ ):
31
+ api_token = api_token or os.getenv("MPT_TOKEN")
32
+ if not api_token:
33
+ raise ValueError(
34
+ "API token is required. "
35
+ "Set it up as env variable MPT_TOKEN or pass it as `api_token` "
36
+ "argument to MPTClient."
37
+ )
38
+
39
+ base_url = base_url or os.getenv("MPT_URL")
40
+ if not base_url:
41
+ raise ValueError(
42
+ "Base URL is required. "
43
+ "Set it up as env variable MPT_URL or pass it as `base_url` "
44
+ "argument to MPTClient."
45
+ )
46
+ base_headers = {
47
+ "User-Agent": "swo-marketplace-client/1.0",
48
+ "Authorization": f"Bearer {api_token}",
49
+ "Accept": "application/json",
50
+ }
51
+ self.httpx_client = AsyncClient(
52
+ base_url=base_url,
53
+ headers=base_headers,
54
+ timeout=timeout,
55
+ transport=AsyncHTTPTransport(retries=retries),
56
+ )
57
+
58
+ async def request( # noqa: WPS211
59
+ self,
60
+ method: str,
61
+ url: str,
62
+ *,
63
+ files: RequestFiles | None = None,
64
+ json: Any | None = None,
65
+ query_params: QueryParam | None = None,
66
+ headers: HeaderTypes | None = None,
67
+ ) -> Response:
68
+ """Perform an HTTP request.
69
+
70
+ Args:
71
+ method: HTTP method.
72
+ url: URL to send the request to.
73
+ files: Request files.
74
+ json: Request JSON data.
75
+ query_params: Query parameters.
76
+ headers: Request headers.
77
+
78
+ Returns:
79
+ Response object.
80
+
81
+ Raises:
82
+ MPTError: If the request fails.
83
+ MPTApiError: If the response contains an error.
84
+ MPTHttpError: If the response contains an HTTP error.
85
+ """
86
+ try:
87
+ response = await self.httpx_client.request(
88
+ method,
89
+ url,
90
+ files=files,
91
+ json=json,
92
+ params=query_params,
93
+ headers=headers,
94
+ )
95
+ except HTTPError as err:
96
+ raise MPTError(f"HTTP Error: {err}") from err
97
+
98
+ try:
99
+ response.raise_for_status()
100
+ except HTTPStatusError as http_status_exception:
101
+ raise transform_http_status_exception(http_status_exception) from http_status_exception
102
+ return Response(
103
+ headers=dict(response.headers),
104
+ status_code=response.status_code,
105
+ content=response.content,
106
+ )
@@ -0,0 +1,74 @@
1
+ from urllib.parse import urljoin
2
+
3
+ from mpt_api_client.http.async_client import AsyncHTTPClient
4
+ from mpt_api_client.http.base_service import ServiceBase
5
+ from mpt_api_client.http.types import QueryParam, Response
6
+ from mpt_api_client.models import Model as BaseModel
7
+ from mpt_api_client.models import ResourceData
8
+ from mpt_api_client.models.collection import ResourceList
9
+
10
+
11
+ class AsyncService[Model: BaseModel](ServiceBase[AsyncHTTPClient, Model]): # noqa: WPS214
12
+ """Immutable Service for RESTful resource collections.
13
+
14
+ Examples:
15
+ active_orders_cc = order_collection.filter(RQLQuery(status="active"))
16
+ active_orders = active_orders_cc.order_by("created").iterate()
17
+ product_active_orders = active_orders_cc.filter(RQLQuery(product__id="PRD-1")).iterate()
18
+
19
+ new_order = order_collection.create(order_data)
20
+
21
+ """
22
+
23
+ async def _resource_do_request( # noqa: WPS211
24
+ self,
25
+ resource_id: str,
26
+ method: str = "GET",
27
+ action: str | None = None,
28
+ json: ResourceData | ResourceList | None = None,
29
+ query_params: QueryParam | None = None,
30
+ headers: dict[str, str] | None = None,
31
+ ) -> Response:
32
+ """Perform an action on a specific resource using.
33
+
34
+ Request with action: `HTTP_METHOD /endpoint/{resource_id}/{action}`.
35
+ Request without action: `HTTP_METHOD /endpoint/{resource_id}`.
36
+
37
+ Args:
38
+ resource_id: The resource ID to operate on.
39
+ method: The HTTP method to use.
40
+ action: The action name to use.
41
+ json: The updated resource data.
42
+ query_params: Additional query parameters.
43
+ headers: Additional headers.
44
+
45
+ Raises:
46
+ HTTPError: If the action fails.
47
+ """
48
+ resource_url = urljoin(f"{self.path}/", resource_id)
49
+ url = urljoin(f"{resource_url}/", action) if action else resource_url
50
+ return await self.http_client.request(
51
+ method, url, json=json, query_params=query_params, headers=headers
52
+ )
53
+
54
+ async def _resource_action(
55
+ self,
56
+ resource_id: str,
57
+ method: str = "GET",
58
+ action: str | None = None,
59
+ json: ResourceData | ResourceList | None = None,
60
+ query_params: QueryParam | None = None,
61
+ ) -> Model:
62
+ """Perform an action on a specific resource using `HTTP_METHOD /endpoint/{resource_id}`.
63
+
64
+ Args:
65
+ resource_id: The resource ID to operate on.
66
+ method: The HTTP method to use.
67
+ action: The action name to use.
68
+ json: The updated resource data.
69
+ query_params: Additional query parameters.
70
+ """
71
+ response = await self._resource_do_request(
72
+ resource_id, method, action, json=json, query_params=query_params
73
+ )
74
+ return self._model_class.from_response(response)
@@ -0,0 +1,58 @@
1
+ from typing import Any
2
+
3
+ from mpt_api_client.http.query_state import QueryState
4
+ from mpt_api_client.http.types import Response
5
+ from mpt_api_client.models import Collection, Meta
6
+ from mpt_api_client.models import Model as BaseModel
7
+
8
+
9
+ class ServiceBase[Client, Model: BaseModel]: # noqa: WPS214
10
+ """Service base with agnostic HTTP client."""
11
+
12
+ _endpoint: str
13
+ _model_class: type[Model]
14
+ _collection_key = "data"
15
+
16
+ def __init__(
17
+ self,
18
+ *,
19
+ http_client: Client,
20
+ query_state: QueryState | None = None,
21
+ endpoint_params: dict[str, str] | None = None,
22
+ ) -> None:
23
+ self.http_client = http_client
24
+ self.query_state = query_state or QueryState()
25
+ self.endpoint_params = endpoint_params or {}
26
+
27
+ @property
28
+ def path(self) -> str:
29
+ """Service endpoint URL."""
30
+ return self._endpoint.format(**self.endpoint_params)
31
+
32
+ def build_path(
33
+ self,
34
+ query_params: dict[str, Any] | None = None,
35
+ ) -> str:
36
+ """Builds the endpoint URL with all the query parameters.
37
+
38
+ Returns:
39
+ Complete URL with query parameters.
40
+ """
41
+ query = self.query_state.build(query_params)
42
+ return f"{self.path}?{query}" if query else self.path
43
+
44
+ @classmethod
45
+ def make_collection(cls, response: Response) -> Collection[Model]:
46
+ """Builds a collection from a response.
47
+
48
+ Args:
49
+ response: The response object.
50
+ """
51
+ meta = Meta.from_response(response)
52
+ return Collection(
53
+ resources=[
54
+ cls._model_class.new(resource, meta)
55
+ for resource in response.json().get(cls._collection_key)
56
+ ],
57
+ meta=meta,
58
+ )
@@ -0,0 +1,108 @@
1
+ import os
2
+ from typing import Any
3
+
4
+ from httpx import (
5
+ Client,
6
+ HTTPError,
7
+ HTTPStatusError,
8
+ HTTPTransport,
9
+ )
10
+
11
+ from mpt_api_client.exceptions import (
12
+ MPTError,
13
+ transform_http_status_exception,
14
+ )
15
+ from mpt_api_client.http.types import (
16
+ HeaderTypes,
17
+ QueryParam,
18
+ RequestFiles,
19
+ Response,
20
+ )
21
+
22
+
23
+ class HTTPClient:
24
+ """Sync HTTP client for interacting with SoftwareOne Marketplace Platform API."""
25
+
26
+ def __init__(
27
+ self,
28
+ *,
29
+ base_url: str | None = None,
30
+ api_token: str | None = None,
31
+ timeout: float = 5.0,
32
+ retries: int = 5,
33
+ ):
34
+ api_token = api_token or os.getenv("MPT_TOKEN")
35
+ if not api_token:
36
+ raise ValueError(
37
+ "API token is required. "
38
+ "Set it up as env variable MPT_TOKEN or pass it as `api_token` "
39
+ "argument to MPTClient."
40
+ )
41
+
42
+ base_url = base_url or os.getenv("MPT_URL")
43
+ if not base_url:
44
+ raise ValueError(
45
+ "Base URL is required. "
46
+ "Set it up as env variable MPT_URL or pass it as `base_url` "
47
+ "argument to MPTClient."
48
+ )
49
+ base_headers = {
50
+ "User-Agent": "swo-marketplace-client/1.0",
51
+ "Authorization": f"Bearer {api_token}",
52
+ }
53
+ self.httpx_client = Client(
54
+ base_url=base_url,
55
+ headers=base_headers,
56
+ timeout=timeout,
57
+ transport=HTTPTransport(retries=retries),
58
+ )
59
+
60
+ def request( # noqa: WPS211
61
+ self,
62
+ method: str,
63
+ url: str,
64
+ *,
65
+ files: RequestFiles | None = None,
66
+ json: Any | None = None,
67
+ query_params: QueryParam | None = None,
68
+ headers: HeaderTypes | None = None,
69
+ ) -> Response:
70
+ """Perform an HTTP request.
71
+
72
+ Args:
73
+ method: HTTP method.
74
+ url: URL to send the request to.
75
+ files: Request files.
76
+ json: Request JSON data.
77
+ query_params: Query parameters.
78
+ headers: Request headers.
79
+
80
+ Returns:
81
+ Response object.
82
+
83
+ Raises:
84
+ MPTError: If the request fails.
85
+ MPTApiError: If the response contains an error.
86
+ MPTHttpError: If the response contains an HTTP error.
87
+ """
88
+ try:
89
+ response = self.httpx_client.request(
90
+ method,
91
+ url,
92
+ files=files,
93
+ json=json,
94
+ params=query_params,
95
+ headers=headers,
96
+ )
97
+ except HTTPError as err:
98
+ raise MPTError(f"HTTP Error: {err}") from err
99
+
100
+ try:
101
+ response.raise_for_status()
102
+ except HTTPStatusError as http_status_exception:
103
+ raise transform_http_status_exception(http_status_exception) from http_status_exception
104
+ return Response(
105
+ headers=dict(response.headers),
106
+ status_code=response.status_code,
107
+ content=response.content,
108
+ )