plentymarkets-api-client 0.1.2__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.
@@ -0,0 +1,19 @@
1
+ from .client import PlentyClient
2
+ from ._types import (
3
+ ContactCreatePayload,
4
+ ContactUpdatePayload,
5
+ ItemCreatePayload,
6
+ ItemUpdatePayload,
7
+ OrderCreatePayload,
8
+ StockCorrectionPayload,
9
+ )
10
+
11
+ __all__ = [
12
+ "PlentyClient",
13
+ "OrderCreatePayload",
14
+ "ItemCreatePayload",
15
+ "ItemUpdatePayload",
16
+ "StockCorrectionPayload",
17
+ "ContactCreatePayload",
18
+ "ContactUpdatePayload",
19
+ ]
@@ -0,0 +1,31 @@
1
+ class PlentyError(Exception):
2
+ """Base exception for all plentymarkets client errors."""
3
+
4
+
5
+ class AuthError(PlentyError):
6
+ """Raised on 401 responses or when login credentials are rejected."""
7
+
8
+
9
+ class NotFoundError(PlentyError):
10
+ """Raised on 404 responses."""
11
+
12
+
13
+ class RateLimitError(PlentyError):
14
+ """Raised on 429 responses.
15
+
16
+ Attributes:
17
+ retry_after: Seconds to wait before retrying, taken from the
18
+ ``Retry-After`` response header. ``0`` if the header is absent.
19
+ """
20
+
21
+ def __init__(self, message: str, retry_after: int = 0) -> None:
22
+ super().__init__(message)
23
+ self.retry_after = retry_after
24
+
25
+
26
+ class ServerError(PlentyError):
27
+ """Raised on 5xx responses."""
28
+
29
+
30
+ class ConnectionError(PlentyError):
31
+ """Raised when the network is unreachable, DNS fails, or a timeout occurs."""
@@ -0,0 +1,240 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime, timedelta, timezone
4
+ from typing import Any
5
+
6
+ _JsonDict = dict[str, Any]
7
+
8
+ import httpx
9
+
10
+ from ._exceptions import (
11
+ AuthError,
12
+ ConnectionError,
13
+ NotFoundError,
14
+ PlentyError,
15
+ RateLimitError,
16
+ ServerError,
17
+ )
18
+
19
+ _REFRESH_BUFFER_SECONDS = 60
20
+
21
+
22
+ class _Session:
23
+ """Manages the httpx client, login state, and token lifecycle.
24
+
25
+ Args:
26
+ base_url: PlentyMarkets shop base URL, e.g. ``https://your-shop.plentymarkets.com``.
27
+ username: PlentyMarkets back-end user e-mail.
28
+ password: PlentyMarkets back-end user password.
29
+ client: Shared ``httpx.AsyncClient`` instance.
30
+ """
31
+
32
+ def __init__(
33
+ self,
34
+ base_url: str,
35
+ username: str,
36
+ password: str,
37
+ client: httpx.AsyncClient,
38
+ ) -> None:
39
+ stripped = base_url.rstrip("/")
40
+ if "/rest" in stripped.split("//", 1)[-1]:
41
+ raise ValueError(
42
+ f"base_url must be the shop root without '/rest', got: {base_url!r}. "
43
+ "Example: 'https://your-shop.plentymarkets.com'"
44
+ )
45
+ self._base_url = stripped
46
+ self._username = username
47
+ self._password = password
48
+ self._client = client
49
+ self._access_token: str | None = None
50
+ self._token_expiry: datetime | None = None
51
+
52
+ @staticmethod
53
+ def _raise_for_status(response: httpx.Response, path: str = "/rest/login") -> None:
54
+ """Map HTTP error responses to PlentyError subclasses.
55
+
56
+ Args:
57
+ response: The httpx response to check.
58
+ path: Path string used in error messages.
59
+
60
+ Raises:
61
+ AuthError: On 401 responses.
62
+ NotFoundError: On 404 responses.
63
+ RateLimitError: On 429 responses.
64
+ ServerError: On 5xx responses.
65
+ """
66
+ if response.status_code == 401:
67
+ raise AuthError(f"Authentication failed: {path}")
68
+ if response.status_code == 404:
69
+ raise NotFoundError(f"{path} returned 404")
70
+ if response.status_code == 429:
71
+ retry_after = int(response.headers.get("Retry-After", 0))
72
+ raise RateLimitError("Rate limit exceeded", retry_after=retry_after)
73
+ if response.status_code >= 500:
74
+ raise ServerError(f"Server error {response.status_code}: {path}")
75
+
76
+ def _is_token_expiring(self) -> bool:
77
+ """Return True if the token is absent or within the refresh buffer window.
78
+
79
+ Returns:
80
+ True when re-authentication is required before the next request.
81
+ """
82
+ if self._access_token is None or self._token_expiry is None:
83
+ return True
84
+ return datetime.now(tz=timezone.utc) >= self._token_expiry - timedelta(
85
+ seconds=_REFRESH_BUFFER_SECONDS
86
+ )
87
+
88
+ async def login(self) -> None:
89
+ """Authenticate and store the access token.
90
+
91
+ Raises:
92
+ AuthError: If the server rejects the credentials (HTTP 401).
93
+ ServerError: On 5xx responses from the login endpoint.
94
+ RateLimitError: On 429 responses from the login endpoint.
95
+ ConnectionError: On network failures reaching the login endpoint.
96
+ """
97
+ try:
98
+ response = await self._client.post(
99
+ f"{self._base_url}/rest/login",
100
+ json={"username": self._username, "password": self._password},
101
+ )
102
+ except (httpx.ConnectError, httpx.TimeoutException) as exc:
103
+ raise ConnectionError(str(exc)) from exc
104
+ self._raise_for_status(response, "/rest/login")
105
+ if response.status_code >= 400:
106
+ raise PlentyError(f"Login error {response.status_code}")
107
+ data = response.json()
108
+ self._access_token = data["accessToken"]
109
+ self._token_expiry = datetime.now(tz=timezone.utc) + timedelta(
110
+ seconds=data["expiresIn"]
111
+ )
112
+
113
+ async def logout(self) -> None:
114
+ """Invalidate the current session token on the server and clear local state.
115
+
116
+ If there is no active token, this method is a no-op and makes no network
117
+ calls. Local state is always cleared, even if the DELETE request fails.
118
+ """
119
+ if self._access_token is None:
120
+ return
121
+ try:
122
+ await self._client.delete(
123
+ f"{self._base_url}/rest/login",
124
+ headers={"Authorization": f"Bearer {self._access_token}"},
125
+ )
126
+ finally:
127
+ self._access_token = None
128
+ self._token_expiry = None
129
+
130
+ async def _request(self, method: str, path: str, **kwargs: Any) -> _JsonDict:
131
+ """Send an authenticated HTTP request, refreshing the token if needed.
132
+
133
+ Args:
134
+ method: HTTP method (``GET``, ``POST``, ``PUT``, ``DELETE``).
135
+ path: URL path appended to ``base_url``, e.g. ``/rest/orders/1``.
136
+ **kwargs: Extra arguments forwarded to ``httpx.AsyncClient.request``.
137
+
138
+ Returns:
139
+ Parsed JSON response body, or ``{}`` for empty responses (HTTP 204).
140
+
141
+ Raises:
142
+ AuthError: On persistent 401 after one re-login attempt.
143
+ NotFoundError: On HTTP 404.
144
+ RateLimitError: On HTTP 429.
145
+ ServerError: On HTTP 5xx.
146
+ ConnectionError: On network-level failures.
147
+ """
148
+ if self._is_token_expiring():
149
+ await self.login()
150
+
151
+ headers = kwargs.pop("headers", {})
152
+ headers["Authorization"] = f"Bearer {self._access_token}"
153
+
154
+ try:
155
+ response = await self._client.request(
156
+ method,
157
+ f"{self._base_url}{path}",
158
+ headers=headers,
159
+ **kwargs,
160
+ )
161
+ except (httpx.ConnectError, httpx.TimeoutException) as exc:
162
+ raise ConnectionError(str(exc)) from exc
163
+
164
+ if response.status_code == 401:
165
+ await self.login()
166
+ headers["Authorization"] = f"Bearer {self._access_token}"
167
+ try:
168
+ response = await self._client.request(
169
+ method,
170
+ f"{self._base_url}{path}",
171
+ headers=headers,
172
+ **kwargs,
173
+ )
174
+ except (httpx.ConnectError, httpx.TimeoutException) as exc:
175
+ raise ConnectionError(str(exc)) from exc
176
+ if response.status_code == 401:
177
+ raise AuthError("Authentication failed after token refresh")
178
+
179
+ if response.status_code == 404:
180
+ raise NotFoundError(f"{method} {path} returned 404")
181
+ if response.status_code == 429:
182
+ retry_after = int(response.headers.get("Retry-After", 0))
183
+ raise RateLimitError("Rate limit exceeded", retry_after=retry_after)
184
+ if response.status_code >= 500:
185
+ raise ServerError(f"Server error {response.status_code}: {method} {path}")
186
+ if response.status_code >= 400:
187
+ raise PlentyError(f"Client error {response.status_code}: {method} {path}")
188
+
189
+ response.raise_for_status()
190
+ if not response.content:
191
+ return {}
192
+ return response.json()
193
+
194
+ async def get(self, path: str, **kwargs: Any) -> _JsonDict:
195
+ """Send an authenticated GET request.
196
+
197
+ Args:
198
+ path: URL path appended to ``base_url``.
199
+ **kwargs: Extra arguments forwarded to ``httpx.AsyncClient.request``.
200
+
201
+ Returns:
202
+ Parsed JSON response body.
203
+ """
204
+ return await self._request("GET", path, **kwargs)
205
+
206
+ async def post(self, path: str, **kwargs: Any) -> _JsonDict:
207
+ """Send an authenticated POST request.
208
+
209
+ Args:
210
+ path: URL path appended to ``base_url``.
211
+ **kwargs: Extra arguments forwarded to ``httpx.AsyncClient.request``.
212
+
213
+ Returns:
214
+ Parsed JSON response body.
215
+ """
216
+ return await self._request("POST", path, **kwargs)
217
+
218
+ async def put(self, path: str, **kwargs: Any) -> _JsonDict:
219
+ """Send an authenticated PUT request.
220
+
221
+ Args:
222
+ path: URL path appended to ``base_url``.
223
+ **kwargs: Extra arguments forwarded to ``httpx.AsyncClient.request``.
224
+
225
+ Returns:
226
+ Parsed JSON response body.
227
+ """
228
+ return await self._request("PUT", path, **kwargs)
229
+
230
+ async def delete(self, path: str, **kwargs: Any) -> _JsonDict:
231
+ """Send an authenticated DELETE request.
232
+
233
+ Args:
234
+ path: URL path appended to ``base_url``.
235
+ **kwargs: Extra arguments forwarded to ``httpx.AsyncClient.request``.
236
+
237
+ Returns:
238
+ Parsed JSON response body.
239
+ """
240
+ return await self._request("DELETE", path, **kwargs)
@@ -0,0 +1,277 @@
1
+ """TypedDict definitions for PlentyMarkets API request payloads.
2
+
3
+ Import these when constructing payloads to get IDE autocomplete and
4
+ static type checking. At runtime they are plain dicts — no overhead.
5
+
6
+ Example::
7
+
8
+ from plentymarkets._types import OrderCreatePayload
9
+
10
+ payload: OrderCreatePayload = {"typeId": 1, "plentyId": 12345}
11
+ order = await client.orders.create(payload)
12
+ """
13
+
14
+ from typing import NotRequired, Required, TypedDict
15
+
16
+
17
+ # ---------------------------------------------------------------------------
18
+ # Orders (/rest/orders)
19
+ # ---------------------------------------------------------------------------
20
+
21
+
22
+ class OrderCreatePayload(TypedDict, total=False):
23
+ """Payload for :meth:`OrdersModule.create` (``POST /rest/orders``).
24
+
25
+ Keys:
26
+ typeId: Order type ID. **Required.**
27
+ 1=sales order, 2=delivery order, 3=returns, 4=credit note,
28
+ 5=warranty, 6=repair, 7=offer, 8=advance order, 9=multi-order.
29
+ plentyId: Client (store) ID the order belongs to. **Required.**
30
+ statusId: Numeric order status ID (e.g. ``3.0`` = waiting for payment,
31
+ ``5.0`` = cleared for shipping). Decimal part is used for
32
+ sub-statuses.
33
+ ownerId: User ID of the order owner.
34
+ lockStatus: Lock state of the order. ``"unlocked"`` or ``"locked"``.
35
+ orderItems: List of order item dicts. Each item should include
36
+ ``typeId``, ``itemVariationId``, ``quantity``, and ``amounts``.
37
+ properties: List of order property dicts, each with ``typeId`` and
38
+ ``value``.
39
+ addressRelations: List of address relation dicts. Each entry requires
40
+ ``typeId`` (1=billing, 2=delivery) and ``addressId``.
41
+ relations: List of relation dicts, each with ``referenceType``,
42
+ ``referenceId``, and ``relation``.
43
+ """
44
+
45
+ typeId: Required[int]
46
+ plentyId: Required[int]
47
+ statusId: NotRequired[float]
48
+ ownerId: NotRequired[int]
49
+ lockStatus: NotRequired[str]
50
+ orderItems: NotRequired[list[dict]]
51
+ properties: NotRequired[list[dict]]
52
+ addressRelations: NotRequired[list[dict]]
53
+ relations: NotRequired[list[dict]]
54
+
55
+
56
+ # ---------------------------------------------------------------------------
57
+ # Items (/rest/items)
58
+ # ---------------------------------------------------------------------------
59
+
60
+
61
+ class ItemCreatePayload(TypedDict, total=False):
62
+ """Payload for :meth:`ItemsModule.create` (``POST /rest/items``).
63
+
64
+ Keys:
65
+ position: Display position in item lists.
66
+ stockType: Stock type. 0=stocked, 1=production, 2=colli,
67
+ 3=special order.
68
+ storeSpecial: Special shop display flag. 0=none, 1=special offer,
69
+ 2=new item, 3=top item.
70
+ ownerId: User ID of the item owner.
71
+ manufacturerId: Manufacturer ID.
72
+ producingCountryId: Country of origin ID (ISO country ID).
73
+ vatId: VAT configuration ID.
74
+ condition: Item condition. 0=new, 1=used, 2=refurbished,
75
+ 3=new with original packaging, 4=new with defects, 5=B-stock.
76
+ conditionApi: Condition value exposed via marketplace APIs
77
+ (0=new, 1=used, 2=not specified, 3=defective, 4=refurbished).
78
+ customsTariffNumber: Harmonized System (HS) customs tariff code.
79
+ revenueAccount: Revenue account number for accounting.
80
+ couponRestriction: 0=allow all, 1=forbid all, 2=allow coupons only.
81
+ isSubscribable: Whether the item supports subscription ordering.
82
+ isShippingPackage: Whether the item ships as a package unit.
83
+ flagOne: Item flag 1 (shop-specific integer tag).
84
+ flagTwo: Item flag 2 (shop-specific integer tag).
85
+ ageRestriction: Age restriction in years. Common values: 0, 6, 12,
86
+ 14, 16, 18, 50.
87
+ itemType: Item type string. ``"Default"``, ``"Bundle"``,
88
+ or ``"BundleItem"``.
89
+ amazonFbaPlatform: Amazon FBA platform. 0=none, 1=AMAZON_EU,
90
+ 2=AMAZON_FE, 3=AMAZON_NA.
91
+ isShippableByAmazon: Whether Amazon is allowed to fulfil this item.
92
+ amazonProductType: Amazon browse-node / product type number.
93
+ ebayPresetId: eBay listing preset ID.
94
+ ebayCategory: Primary eBay category ID.
95
+ ebayCategory2: Secondary eBay category ID.
96
+ rakutenCategoryId: Rakuten product category ID.
97
+ variations: List of variation dicts. Provide at least one variation
98
+ with ``isMain: true`` to create a usable item.
99
+ """
100
+
101
+ position: NotRequired[int]
102
+ stockType: NotRequired[int]
103
+ storeSpecial: NotRequired[int]
104
+ ownerId: NotRequired[int]
105
+ manufacturerId: NotRequired[int]
106
+ producingCountryId: NotRequired[int]
107
+ vatId: NotRequired[int]
108
+ condition: NotRequired[int]
109
+ conditionApi: NotRequired[int]
110
+ customsTariffNumber: NotRequired[str]
111
+ revenueAccount: NotRequired[int]
112
+ couponRestriction: NotRequired[int]
113
+ isSubscribable: NotRequired[bool]
114
+ isShippingPackage: NotRequired[bool]
115
+ flagOne: NotRequired[int]
116
+ flagTwo: NotRequired[int]
117
+ ageRestriction: NotRequired[int]
118
+ itemType: NotRequired[str]
119
+ amazonFbaPlatform: NotRequired[int]
120
+ isShippableByAmazon: NotRequired[bool]
121
+ amazonProductType: NotRequired[int]
122
+ ebayPresetId: NotRequired[int]
123
+ ebayCategory: NotRequired[int]
124
+ ebayCategory2: NotRequired[int]
125
+ rakutenCategoryId: NotRequired[int]
126
+ variations: NotRequired[list[dict]]
127
+
128
+
129
+ class ItemUpdatePayload(TypedDict, total=False):
130
+ """Payload for :meth:`ItemsModule.update` (``PUT /rest/items/{itemId}``).
131
+
132
+ All keys are optional — only the fields provided are updated.
133
+
134
+ Keys:
135
+ position: Display position in item lists.
136
+ stockType: Stock type. 0=stocked, 1=production, 2=colli,
137
+ 3=special order.
138
+ storeSpecial: Special shop display flag.
139
+ ownerId: User ID of the item owner.
140
+ manufacturerId: Manufacturer ID.
141
+ producingCountryId: Country of origin ID.
142
+ vatId: VAT configuration ID.
143
+ condition: Item condition (0–5).
144
+ conditionApi: Marketplace condition value (0–4).
145
+ customsTariffNumber: HS / customs tariff code.
146
+ revenueAccount: Revenue account number.
147
+ couponRestriction: 0=allow all, 1=forbid all, 2=allow coupons only.
148
+ isSubscribable: Whether the item supports subscription ordering.
149
+ isShippingPackage: Whether the item ships as a package unit.
150
+ flagOne: Item flag 1.
151
+ flagTwo: Item flag 2.
152
+ ageRestriction: Age restriction in years.
153
+ itemType: ``"Default"``, ``"Bundle"``, or ``"BundleItem"``.
154
+ amazonFbaPlatform: 0=none, 1=AMAZON_EU, 2=AMAZON_FE, 3=AMAZON_NA.
155
+ isShippableByAmazon: Whether Amazon can fulfil this item.
156
+ amazonProductType: Amazon product type number.
157
+ ebayPresetId: eBay listing preset ID.
158
+ ebayCategory: Primary eBay category ID.
159
+ ebayCategory2: Secondary eBay category ID.
160
+ rakutenCategoryId: Rakuten product category ID.
161
+ """
162
+
163
+ position: NotRequired[int]
164
+ stockType: NotRequired[int]
165
+ storeSpecial: NotRequired[int]
166
+ ownerId: NotRequired[int]
167
+ manufacturerId: NotRequired[int]
168
+ producingCountryId: NotRequired[int]
169
+ vatId: NotRequired[int]
170
+ condition: NotRequired[int]
171
+ conditionApi: NotRequired[int]
172
+ customsTariffNumber: NotRequired[str]
173
+ revenueAccount: NotRequired[int]
174
+ couponRestriction: NotRequired[int]
175
+ isSubscribable: NotRequired[bool]
176
+ isShippingPackage: NotRequired[bool]
177
+ flagOne: NotRequired[int]
178
+ flagTwo: NotRequired[int]
179
+ ageRestriction: NotRequired[int]
180
+ itemType: NotRequired[str]
181
+ amazonFbaPlatform: NotRequired[int]
182
+ isShippableByAmazon: NotRequired[bool]
183
+ amazonProductType: NotRequired[int]
184
+ ebayPresetId: NotRequired[int]
185
+ ebayCategory: NotRequired[int]
186
+ ebayCategory2: NotRequired[int]
187
+ rakutenCategoryId: NotRequired[int]
188
+
189
+
190
+ # ---------------------------------------------------------------------------
191
+ # Stock (/rest/stock/warehouses/{warehouseId}/stock/correction)
192
+ # ---------------------------------------------------------------------------
193
+
194
+
195
+ class StockCorrectionPayload(TypedDict):
196
+ """Payload for :meth:`StockModule.correct`
197
+ (``PUT /rest/stock/warehouses/{warehouseId}/stock/correction``).
198
+
199
+ Keys:
200
+ variationId: Variation ID whose stock should be corrected. **Required.**
201
+ quantity: New absolute stock quantity after correction. **Required.**
202
+ Positive values add stock; the API replaces the current level.
203
+ reasonId: Correction reason ID. **Required.** Common values:
204
+ 301=stock correction, 304=incoming items, 305=outgoing items,
205
+ 309=returns.
206
+ warehouseLocationId: Storage location ID within the warehouse.
207
+ **Required.** Use ``0`` for the warehouse default location.
208
+ """
209
+
210
+ variationId: int
211
+ quantity: float
212
+ reasonId: int
213
+ warehouseLocationId: int
214
+
215
+
216
+ # ---------------------------------------------------------------------------
217
+ # Contacts (/rest/accounts/contacts)
218
+ # ---------------------------------------------------------------------------
219
+
220
+
221
+ class ContactCreatePayload(TypedDict, total=False):
222
+ """Payload for :meth:`ContactsModule.create`
223
+ (``POST /rest/accounts/contacts``).
224
+
225
+ Keys:
226
+ typeId: Contact type. 1=customer, 2=interested party, 3=sales rep,
227
+ 4=supplier, 5=manufacturer, 6=partner.
228
+ referrerId: Sales referrer ID (float, links to the referrer table).
229
+ title: Title or form of address string (e.g. ``"Dr."``).
230
+ firstName: Contact first name.
231
+ lastName: Contact last name.
232
+ email: Primary e-mail address.
233
+ gender: Gender string. ``"male"``, ``"female"``, or ``"diverse"``.
234
+ lang: Two-letter ISO language code for the contact's preferred
235
+ language (e.g. ``"de"``, ``"en"``).
236
+ plentyId: Client (store) ID the contact belongs to.
237
+ options: List of contact option dicts. Each option requires
238
+ ``typeId`` (1=telephone, 2=email, 3=messenger, 4=marketplace),
239
+ ``subTypeId``, ``value``, and ``priority``.
240
+ """
241
+
242
+ typeId: NotRequired[int]
243
+ referrerId: NotRequired[float]
244
+ title: NotRequired[str]
245
+ firstName: NotRequired[str]
246
+ lastName: NotRequired[str]
247
+ email: NotRequired[str]
248
+ gender: NotRequired[str]
249
+ lang: NotRequired[str]
250
+ plentyId: NotRequired[int]
251
+ options: NotRequired[list[dict]]
252
+
253
+
254
+ class ContactUpdatePayload(TypedDict, total=False):
255
+ """Payload for :meth:`ContactsModule.update`
256
+ (``PUT /rest/accounts/contacts/{contactId}``).
257
+
258
+ All keys are optional — only the fields provided are updated.
259
+
260
+ Keys:
261
+ title: Title or form of address string.
262
+ firstName: Contact first name.
263
+ lastName: Contact last name.
264
+ email: Primary e-mail address.
265
+ gender: Gender string. ``"male"``, ``"female"``, or ``"diverse"``.
266
+ lang: Two-letter ISO language code.
267
+ options: List of contact option dicts (typeId, subTypeId, value,
268
+ priority). Replaces all existing options of the provided types.
269
+ """
270
+
271
+ title: NotRequired[str]
272
+ firstName: NotRequired[str]
273
+ lastName: NotRequired[str]
274
+ email: NotRequired[str]
275
+ gender: NotRequired[str]
276
+ lang: NotRequired[str]
277
+ options: NotRequired[list[dict]]
@@ -0,0 +1,96 @@
1
+ from __future__ import annotations
2
+
3
+ import httpx
4
+
5
+ from ._session import _Session
6
+ from .modules.contacts import ContactsModule
7
+ from .modules.items import ItemsModule
8
+ from .modules.orders import OrdersModule
9
+ from .modules.stock import StockModule
10
+
11
+
12
+ class PlentyClient:
13
+ """Async client for the PlentyMarkets REST API.
14
+
15
+ Use as an async context manager (recommended) or call ``login()``/``aclose()``
16
+ manually.
17
+
18
+ Args:
19
+ base_url: Shop base URL, e.g. ``https://your-shop.plentymarkets.com``.
20
+ username: PlentyMarkets back-end user e-mail.
21
+ password: PlentyMarkets back-end user password.
22
+
23
+ Example::
24
+
25
+ async with PlentyClient(
26
+ base_url="https://your-shop.plentymarkets.com",
27
+ username="user@example.com",
28
+ password="secret",
29
+ ) as client:
30
+ order = await client.orders.get(42)
31
+ """
32
+
33
+ def __init__(self, base_url: str, username: str, password: str) -> None:
34
+ self._httpx_client = httpx.AsyncClient()
35
+ self._session = _Session(base_url, username, password, self._httpx_client)
36
+ self._orders: OrdersModule | None = None
37
+ self._items: ItemsModule | None = None
38
+ self._stock: StockModule | None = None
39
+ self._contacts: ContactsModule | None = None
40
+
41
+ def __repr__(self) -> str:
42
+ authenticated = self._session._access_token is not None
43
+ return (
44
+ f"PlentyClient(base_url={self._session._base_url!r}, "
45
+ f"authenticated={authenticated})"
46
+ )
47
+
48
+ async def __aenter__(self) -> PlentyClient:
49
+ await self.login()
50
+ return self
51
+
52
+ async def __aexit__(self, *_: object) -> None:
53
+ try:
54
+ await self.logout()
55
+ finally:
56
+ await self.aclose()
57
+
58
+ async def login(self) -> None:
59
+ """Authenticate and acquire an access token."""
60
+ await self._session.login()
61
+
62
+ async def logout(self) -> None:
63
+ """Invalidate the current token on the server and clear local state."""
64
+ await self._session.logout()
65
+
66
+ async def aclose(self) -> None:
67
+ """Close the underlying HTTP connection pool."""
68
+ await self._httpx_client.aclose()
69
+
70
+ @property
71
+ def orders(self) -> OrdersModule:
72
+ """Access the Orders module (``/rest/orders``)."""
73
+ if self._orders is None:
74
+ self._orders = OrdersModule(self._session)
75
+ return self._orders
76
+
77
+ @property
78
+ def items(self) -> ItemsModule:
79
+ """Access the Items module (``/rest/items``)."""
80
+ if self._items is None:
81
+ self._items = ItemsModule(self._session)
82
+ return self._items
83
+
84
+ @property
85
+ def stock(self) -> StockModule:
86
+ """Access the Stock module (``/rest/stock``)."""
87
+ if self._stock is None:
88
+ self._stock = StockModule(self._session)
89
+ return self._stock
90
+
91
+ @property
92
+ def contacts(self) -> ContactsModule:
93
+ """Access the Contacts module (``/rest/accounts/contacts``)."""
94
+ if self._contacts is None:
95
+ self._contacts = ContactsModule(self._session)
96
+ return self._contacts
File without changes