getmeadow 0.1.0__tar.gz

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,67 @@
1
+ Metadata-Version: 2.3
2
+ Name: getmeadow
3
+ Version: 0.1.0
4
+ Summary: Python client for the getmeadow api
5
+ Author: Sebash
6
+ Author-email: Sebash <sebash@mail.friendlyautomations.com>
7
+ Requires-Dist: httpx>=0.28.1
8
+ Requires-Dist: pydantic>=2.13.4
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+
12
+ # GetMeadow
13
+
14
+ [API Documentation](https://api-docs.getmeadow.com/introduction)
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ pip install getmeadow
20
+ ```
21
+
22
+ A Python client for the Meadow API built on `httpx` and `pydantic`
23
+
24
+ GetMeadow uses Pydantic for request and response validation and currently supports username/password authentication.
25
+ Many client methods return a two-item tuple containing the HTTP status code and the response data:
26
+
27
+ ```python
28
+ status_code, data = client.get_orders()
29
+ ```
30
+
31
+ ## Organizations
32
+ Organization handling works similarly to the Meadow web application.
33
+ API URLs are generated using the client instance's current organization ID. Calling `client.change_org_by_name` changes the active organization to the organization matching the provided name.
34
+ Organization data is available through the roles attribute:
35
+
36
+ Organization data can be found in the `roles` attribute
37
+
38
+ `client.roles['organizations']`
39
+
40
+ Most API methods, such as get_orders() and create_user(), operate on the currently selected organization.
41
+ ```python
42
+ client = MeadowClient(
43
+ username=os.getenv("MEADOW_USERNAME"),
44
+ password=os.getenv("MEADOW_PASSWORD")
45
+ )
46
+
47
+ product_id = 12345
48
+ product_option_id = 678910
49
+ payment_type_id = 54321
50
+ patient_hash = '9mLifw'
51
+
52
+ order = {
53
+ "type": "in-store",
54
+ "status": "draft",
55
+ "lineItems": [{
56
+ "productId": product_id,
57
+ "productOptionId": product_option_id,
58
+ "quantity": 1
59
+ }],
60
+ "payments": [{
61
+ 'paymentTypeId': payment_type_id,
62
+ 'remaining': True,
63
+ }],
64
+ "patientHash": patient_hash
65
+ }
66
+ status_code, result = client.create_order(order)
67
+ ```
@@ -0,0 +1,56 @@
1
+ # GetMeadow
2
+
3
+ [API Documentation](https://api-docs.getmeadow.com/introduction)
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install getmeadow
9
+ ```
10
+
11
+ A Python client for the Meadow API built on `httpx` and `pydantic`
12
+
13
+ GetMeadow uses Pydantic for request and response validation and currently supports username/password authentication.
14
+ Many client methods return a two-item tuple containing the HTTP status code and the response data:
15
+
16
+ ```python
17
+ status_code, data = client.get_orders()
18
+ ```
19
+
20
+ ## Organizations
21
+ Organization handling works similarly to the Meadow web application.
22
+ API URLs are generated using the client instance's current organization ID. Calling `client.change_org_by_name` changes the active organization to the organization matching the provided name.
23
+ Organization data is available through the roles attribute:
24
+
25
+ Organization data can be found in the `roles` attribute
26
+
27
+ `client.roles['organizations']`
28
+
29
+ Most API methods, such as get_orders() and create_user(), operate on the currently selected organization.
30
+ ```python
31
+ client = MeadowClient(
32
+ username=os.getenv("MEADOW_USERNAME"),
33
+ password=os.getenv("MEADOW_PASSWORD")
34
+ )
35
+
36
+ product_id = 12345
37
+ product_option_id = 678910
38
+ payment_type_id = 54321
39
+ patient_hash = '9mLifw'
40
+
41
+ order = {
42
+ "type": "in-store",
43
+ "status": "draft",
44
+ "lineItems": [{
45
+ "productId": product_id,
46
+ "productOptionId": product_option_id,
47
+ "quantity": 1
48
+ }],
49
+ "payments": [{
50
+ 'paymentTypeId': payment_type_id,
51
+ 'remaining': True,
52
+ }],
53
+ "patientHash": patient_hash
54
+ }
55
+ status_code, result = client.create_order(order)
56
+ ```
@@ -0,0 +1,21 @@
1
+ [project]
2
+ name = "getmeadow"
3
+ version = "0.1.0"
4
+ description = "Python client for the getmeadow api"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ dependencies = [
8
+ "httpx>=0.28.1",
9
+ "pydantic>=2.13.4",
10
+ ]
11
+
12
+ [[project.authors]]
13
+ name = "Sebash"
14
+ email = "sebash@mail.friendlyautomations.com"
15
+
16
+ [build-system]
17
+ requires = ["uv_build>=0.12.5,<0.13.0"]
18
+ build-backend = "uv_build"
19
+
20
+ [dependency-groups]
21
+ dev = ["pytest>=9.1.1"]
@@ -0,0 +1,22 @@
1
+ [project]
2
+ name = "getmeadow"
3
+ version = "0.1.0"
4
+ description = "Python client for the getmeadow api"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Sebash", email = "sebash@mail.friendlyautomations.com" }
8
+ ]
9
+ requires-python = ">=3.10"
10
+ dependencies = [
11
+ "httpx>=0.28.1",
12
+ "pydantic>=2.13.4",
13
+ ]
14
+
15
+ [build-system]
16
+ requires = ["uv_build>=0.12.5,<0.13.0"]
17
+ build-backend = "uv_build"
18
+
19
+ [dependency-groups]
20
+ dev = [
21
+ "pytest>=9.1.1",
22
+ ]
@@ -0,0 +1,5 @@
1
+ from .client import MeadowClient
2
+ from .endpoints import MeadowEndpoints, IamIntercomEndpoints
3
+ from . import schemas
4
+
5
+ __all__ = ['MeadowClient', 'MeadowEndpoints', 'IamIntercomEndpoints', 'schemas']
@@ -0,0 +1,664 @@
1
+ import json
2
+ from datetime import datetime
3
+ import time
4
+ from functools import wraps
5
+ from http.client import RemoteDisconnected
6
+ from typing import List, Optional, Literal
7
+ from urllib.parse import quote
8
+ import httpx
9
+
10
+
11
+ from .endpoints import (
12
+ MeadowEndpoints, IamIntercomEndpoints
13
+ )
14
+ from .schemas import (
15
+ Order, NewUser, AWSDocumentRequest, MeadowNewDocument, Product,
16
+ Option, Reconciliation, UpdatePurchaseOrderLineItem,
17
+ ReceiveLineItem, CreatePurchaseOrderLineItem
18
+ )
19
+ from .exceptions import (
20
+ CreateUserException, CreateIDException, AuthenticationException, InvalidRequestException, ConnectionException,
21
+ ResponseParseException
22
+ )
23
+
24
+
25
+
26
+ def handle_disconnect(func):
27
+ @wraps(func)
28
+ def wrapper(*args, **kwargs):
29
+ for attempt in range(5):
30
+ try:
31
+ return func(*args, **kwargs)
32
+
33
+ except httpx.TransportError as e:
34
+ print(f"HTTP error ({attempt + 1}/5): {e}")
35
+
36
+ if attempt < 4:
37
+ time.sleep(0.25)
38
+
39
+ raise ConnectionException("Unable to maintain connection to API")
40
+
41
+ return wrapper
42
+
43
+
44
+
45
+ class MeadowClient(httpx.Client):
46
+
47
+ def __init__(self, username=None, password=None, organization=None):
48
+ super().__init__()
49
+ if username and password:
50
+ self.username = username
51
+ self.password = password
52
+ status_code, login_res = self.login()
53
+ if status_code != 200:
54
+ raise AuthenticationException(str(login_res))
55
+ self.user_id = login_res["userId"]
56
+ self.application_id = login_res["applicationId"]
57
+ self.token = login_res["token"]
58
+ self.updated_at = login_res["updatedAt"]
59
+ self.created_at = login_res["createdAt"]
60
+ self.id = login_res["id"]
61
+ self.headers.update({"Authorization": f"token {self.token}"})
62
+ self.headers.update({"Accept": "application/vnd.meadow+json; version=1"})
63
+ # after login headers and tokens set
64
+ self.roles = self.get_roles()[1]
65
+ self.org_id = self.roles['organizations'][0]['id']
66
+ # elif api_key and client_key:
67
+ # self.headers.update({"X-Consumer-Key": api_key, "X-Client-Key": client_key})
68
+ else:
69
+ raise AuthenticationException("Username and password")
70
+
71
+ if organization:
72
+ self.change_org_by_name(organization)
73
+
74
+
75
+ @handle_disconnect
76
+ def post(self, *args, **kwargs) -> tuple[int, dict]:
77
+ s, r = MeadowClient._get_result_from_response(super().post(*args, **kwargs))
78
+ if not isinstance(r, dict):
79
+ raise ResponseParseException("Expected `dict` instance in response")
80
+ return s, r
81
+
82
+ @handle_disconnect
83
+ def get(self, *args, **kwargs) -> tuple[int, dict|list|bytes]:
84
+ return MeadowClient._get_result_from_response(super().get(*args, **kwargs))
85
+
86
+ @handle_disconnect
87
+ def put(self, *args, **kwargs) -> tuple[int, dict]:
88
+ s, r = MeadowClient._get_result_from_response(super().put(*args, **kwargs))
89
+ if not isinstance(r, dict):
90
+ raise ResponseParseException("Expected `dict` instance in response")
91
+ return s, r
92
+
93
+
94
+ @handle_disconnect
95
+ def delete(self, *args, **kwargs) -> tuple[int, dict]:
96
+ s, r = MeadowClient._get_result_from_response(super().put(*args, **kwargs))
97
+ if not isinstance(r, dict):
98
+ raise ResponseParseException("Expected `dict` instance in response")
99
+ return s, r
100
+
101
+ @staticmethod
102
+ def _get_result_from_response(response: httpx.Response) -> tuple[int, dict|bytes]:
103
+ try:
104
+ result_data = response.json()
105
+ if "data" in result_data:
106
+ result = result_data['data']
107
+ elif "error" in result_data:
108
+ result = result_data['error']
109
+ else:
110
+ result = result_data
111
+ return response.status_code, result
112
+ except:
113
+ return response.status_code, response.content
114
+
115
+
116
+
117
+ def login(self) -> tuple[int, dict]:
118
+ """
119
+ Logs the user in
120
+ :return: status code and response data
121
+ """
122
+ return self.put(
123
+ MeadowEndpoints.token,
124
+ json={"emailOrPhone": self.username, "password": self.password},
125
+ )
126
+
127
+ def create_brand(self, name: str) -> tuple[int, dict]:
128
+ """
129
+ creates a brand
130
+ :param name: name of the brand
131
+ :return: status code and response data
132
+ """
133
+ return self.post(MeadowEndpoints.brands.format(org_id=self.org_id), json={"name": name})
134
+
135
+ def get_users(self, starting_after_id=None, user_type="adult-use"):
136
+ params = {"type": user_type}
137
+ if starting_after_id is not None:
138
+ params['startingAfterId'] = starting_after_id
139
+ return self.get(MeadowEndpoints.users.format(org_id=self.org_id), params=params)
140
+
141
+
142
+ def get_brands(self) -> tuple[int, list]:
143
+ s, r = self.get(MeadowEndpoints.brands.format(org_id=self.org_id))
144
+ if not isinstance(r, list):
145
+ raise ResponseParseException("Expected `list` instance in response")
146
+ return s, r
147
+
148
+ def get_purchase_orders(self, starting_after: int | None = None)-> tuple[int, list]:
149
+ params = {}
150
+ if starting_after:
151
+ params['startingAfter'] = starting_after
152
+ s, r = self.get(MeadowEndpoints.purchase_orders.format(org_id=self.org_id), params=params)
153
+ if not isinstance(r, list):
154
+ raise ResponseParseException("Expected `list` instance in response")
155
+ return s, r
156
+
157
+ def get_purchase_order(self, po_id: int) -> tuple[int, dict]:
158
+ s, r = self.get(MeadowEndpoints.purchase_order.format(org_id=self.org_id, po_id=po_id))
159
+ if not isinstance(r, dict):
160
+ raise ResponseParseException("Expected `dict` instance in response")
161
+ return s, r
162
+
163
+ def get_product_categories(self) -> tuple[int, list]:
164
+ s, r = self.get(MeadowEndpoints.all_product_categories.format(org_id=self.org_id))
165
+ if not isinstance(r, list):
166
+ raise ResponseParseException("Expected `list` instance in response")
167
+ return s, r
168
+
169
+ def get_vendors(self) -> tuple[int, list]:
170
+ s, r = self.get(MeadowEndpoints.inventory_vendors.format(org_id=self.org_id))
171
+ if not isinstance(r, list):
172
+ raise ResponseParseException("Expected `list` instance in response")
173
+ return s, r
174
+
175
+ def get_vendor(self, vendor_id) -> tuple[int, dict]:
176
+ s, r = self.get(MeadowEndpoints.inventory_vendor.format(org_id=self.org_id, vendor_id=vendor_id))
177
+ if not isinstance(r, dict):
178
+ raise ResponseParseException("Expected `dict` instance in response")
179
+ return s, r
180
+
181
+ def get_compliance_transfer(self, compliance_transfer_id) -> tuple[int, dict]:
182
+ s, r = self.get(MeadowEndpoints.compliance_transfer.format(org_id=self.org_id, compliance_transfer_id=compliance_transfer_id))
183
+ if not isinstance(r, dict):
184
+ raise ResponseParseException("Expected `dict` instance in response")
185
+ return s, r
186
+
187
+ def get_compliance_transfers(self, package_status: str = "ready") -> tuple[int, list]:
188
+ params = {"packageStatus": package_status}
189
+ s, r = self.get(MeadowEndpoints.compliance_transfers.format(org_id=self.org_id), params=params)
190
+ if not isinstance(r, list):
191
+ raise ResponseParseException("Expected `list` instance in response")
192
+ return s, r
193
+
194
+ def get_packages(self, package_status="ready", include_product_data: bool = True) -> tuple[int, list]:
195
+ params = {"status": package_status, "includeProductData": include_product_data}
196
+ s, r = self.get(MeadowEndpoints.packages.format(org_id=self.org_id), params=params)
197
+ if not isinstance(r, list):
198
+ raise ResponseParseException("Expected `list` instance in response")
199
+ return s, r
200
+
201
+ def get_full(self) -> tuple[int, dict]:
202
+ s, r = self.get(MeadowEndpoints.full.format(org_id=self.org_id))
203
+ if not isinstance(r, dict):
204
+ raise ResponseParseException("Expected `dict` instance in response")
205
+ return s, r
206
+
207
+ def get_inventory_locations(self) -> tuple[int, list]:
208
+ s, r = self.get(MeadowEndpoints.inventory_locations.format(org_id=self.org_id))
209
+ if not isinstance(r, list):
210
+ raise ResponseParseException("Expected `list` instance in response")
211
+ return s, r
212
+
213
+ def get_my_user_data(self) -> tuple[int, dict]:
214
+ s, r = self.get(MeadowEndpoints.me)
215
+ if not isinstance(r, dict):
216
+ raise ResponseParseException("Expected `dict` instance in response")
217
+ return s, r
218
+
219
+ def get_roles(self) -> tuple[int, dict]:
220
+ s, r = self.get(MeadowEndpoints.roles)
221
+ if not isinstance(r, dict):
222
+ raise ResponseParseException("Expected `dict` instance in response")
223
+ return s, r
224
+
225
+ def get_pusher_auth(self, socket_id, channel_name="private-marketing-1493"):
226
+ return self.post(
227
+ MeadowEndpoints.pusher_auth,
228
+ data={
229
+ "channel_name": channel_name,
230
+ "socket_id": socket_id
231
+ }
232
+ )[1]['auth']
233
+
234
+ def get_iam_data(self) -> tuple[int, dict]:
235
+ _, user_data = self.get_my_user_data()
236
+ _, roles = self.get_roles()
237
+ return self.post(IamIntercomEndpoints.web_ping,
238
+ data={
239
+ "app_id": "",
240
+ "platform": "web",
241
+ "installation_type": "js-snippet",
242
+ "internal": "{}",
243
+ "is_intersection_booted": "false",
244
+ "page_title": "Meadow",
245
+ "user_active_company_id": "undefined",
246
+ "user_data": json.dumps({
247
+ "email": user_data["email"],
248
+ "user_id": user_data["hashId"],
249
+ "user_hash": roles['intercomUserHash'],
250
+ "name": "Sebastian Campos"
251
+ }),
252
+ "source": "apiBoot",
253
+ "sampling": "false",
254
+ "referer": "https://admin.getmeadow.com/",
255
+ })
256
+
257
+ def get_orders(
258
+ self,
259
+ status: Literal["draft", "new", "packed", "fulfilled", "canceled", "all"] = "new"
260
+ ) -> tuple[int, list]:
261
+
262
+ s, r = self.get(MeadowEndpoints.orders.format(org_id=self.org_id), params={"status": status})
263
+ if not isinstance(r, list):
264
+ raise ResponseParseException("Expected `list` instance in response")
265
+ return s, r
266
+
267
+ def get_order(self, order_id: int) -> tuple[int, dict]:
268
+ s, r = self.get(MeadowEndpoints.orders.format(org_id=self.org_id) + "/" + str(order_id))
269
+ if not isinstance(r, dict):
270
+ raise ResponseParseException("Expected `dict` instance in response")
271
+ return s, r
272
+
273
+
274
+ def get_purchase_order_payments(self, po_id: int) -> tuple[int, list]:
275
+ s, r = self.get(MeadowEndpoints.purchase_order_payment.format(org_id=self.org_id, po_id=po_id))
276
+ if not isinstance(r, list):
277
+ raise ResponseParseException("Expected `list` instance in response")
278
+ return s, r
279
+
280
+
281
+ def update_order_status(self, order_id, status: Literal["new", "packed", "fulfilled"], is_admin=True) -> tuple[int, dict]:
282
+ payload = {}
283
+ if is_admin:
284
+ payload['isAdmin'] = is_admin
285
+ if status == "canceled":
286
+ raise InvalidRequestException("Invalid status Use client.cancel_order instead")
287
+ payload['status'] = status
288
+ return self.put(MeadowEndpoints.order.format(org_id=self.org_id, order_id=order_id), json=payload)
289
+
290
+
291
+ def create_product_category(self, name: str, cannabis_type: str) -> tuple[int, dict]:
292
+ return self.post(
293
+ MeadowEndpoints.product_categories.format(org_id=self.org_id),
294
+ json={"name": name, "cannabisType": cannabis_type},
295
+ )
296
+
297
+ def create_vendor(
298
+ self,
299
+ name,
300
+ seller_permit_number,
301
+ notes,
302
+ street1,
303
+ postal_code,
304
+ city,
305
+ state,
306
+ phone = "",
307
+ street2 = "",
308
+
309
+ )-> tuple[int, dict]:
310
+ payload = {
311
+ "name": name,
312
+ "sellerPermitNumber": seller_permit_number,
313
+ "notes": notes,
314
+ "phone": phone,
315
+ "street1": street1,
316
+ "postalCode": postal_code,
317
+ "city": city,
318
+ "state": state,
319
+ }
320
+ if street2:
321
+ payload['street2'] = street2
322
+ return self.post(MeadowEndpoints.vendors.format(org_id=self.org_id), json=payload)
323
+
324
+
325
+ def create_product(self, product: dict | Product) -> tuple[int, dict]:
326
+ if isinstance(product, dict):
327
+ product = Product(**product)
328
+ return self.post(MeadowEndpoints.products.format(org_id=self.org_id), json=product.model_dump(by_alias=True))
329
+
330
+ def create_purchase_order(
331
+ self,
332
+ inventory_vendor_id: int,
333
+ expected_at: str,
334
+ line_items: List[dict | CreatePurchaseOrderLineItem],
335
+ external_invoice_number: Optional[str] = None,
336
+ payment_terms_due_date: Optional[str] = None,
337
+ shipping_handling_fee: Optional[int] = 0,
338
+ shipping_handling_fee_excise: Optional[int] = 0
339
+ ) -> tuple[int, dict]:
340
+ line_items = [
341
+ CreatePurchaseOrderLineItem(**i).model_dump(by_alias=True) if isinstance(i, dict) else i.model_dump(by_alias=True)
342
+ for i in line_items
343
+ ]
344
+ payload = {
345
+ "inventoryVendorId": inventory_vendor_id,
346
+ "expectedAt": expected_at,
347
+ "paymentTermsDueDate": payment_terms_due_date,
348
+ "externalInvoiceNumber": external_invoice_number,
349
+ "shippingHandlingFee": shipping_handling_fee,
350
+ "shippingHandlingFeeExcise": shipping_handling_fee_excise,
351
+ "lineItems": line_items
352
+ }
353
+ return self.post(
354
+ MeadowEndpoints.purchase_orders.format(org_id=self.org_id), json=payload
355
+ )
356
+
357
+ def update_purchase_order(
358
+ self,
359
+ po_number: int,
360
+ inventory_vendor_id: int,
361
+ expected_at: str,
362
+ line_items: List[dict | UpdatePurchaseOrderLineItem],
363
+ payment_status: str = "unpaid",
364
+ payment_terms: Optional[str] = None,
365
+ external_invoice_number: Optional[str] = None,
366
+ payment_terms_due_date: Optional[str] = None,
367
+ shipping_handling_fee: Optional[int] = 0,
368
+ shipping_handling_fee_excise: Optional[int] = 0,
369
+ notes: Optional[str] = None,
370
+ status: str = "open"
371
+ ) -> tuple[int, dict]:
372
+ line_items = [
373
+ UpdatePurchaseOrderLineItem(**i).model_dump(by_alias=True) if isinstance(i, dict) else i.model_dump(by_alias=True)
374
+ for i in line_items
375
+ ]
376
+
377
+ payload = {
378
+ "inventoryVendorId": inventory_vendor_id,
379
+ "expectedAt": expected_at,
380
+ "paymentStatus": payment_status,
381
+ "paymentTerms": payment_terms,
382
+ "paymentTermsDueDate": payment_terms_due_date,
383
+ "notes": notes,
384
+ "externalInvoiceNumber": external_invoice_number,
385
+ "status": status,
386
+ "shippingHandlingFee": shipping_handling_fee,
387
+ "shippingHandlingFeeExcise": shipping_handling_fee_excise,
388
+ "lineItems": line_items
389
+ }
390
+ return self.put(
391
+ MeadowEndpoints.purchase_order.format(org_id=self.org_id, po_id=po_number), json=payload
392
+ )
393
+
394
+ def receive_line_items(self, inventory_location_id: int, po_id: int, line_items: List[ReceiveLineItem | dict]) -> tuple[int, dict]:
395
+ line_items = [
396
+ ReceiveLineItem(**i).model_dump(by_alias=True, exclude_none=True) if isinstance(i, dict) else i.model_dump(by_alias=True, exclude_none=True)
397
+ for i in line_items
398
+ ]
399
+ payload = {
400
+ "inventoryLocationId": inventory_location_id,
401
+ "lineItems": line_items
402
+ }
403
+
404
+ return self.post(
405
+ MeadowEndpoints.purchase_order_receive.format(org_id=self.org_id, po_id=po_id), json=payload
406
+ )
407
+
408
+ def update_purchase_order_status(self, po_id: int, status: str):
409
+ payload = {"status": status}
410
+ return self.put(MeadowEndpoints.purchase_order.format(org_id=self.org_id, po_id=po_id), json=payload)
411
+
412
+ def post_purchase_order_payment(self, po_id: int, amount: int, payment_type: str, payment_date: Optional[str] = None):
413
+ payload = {"paymentDate":payment_date,"amount":amount, "paymentType": payment_type}
414
+ return self.post(MeadowEndpoints.purchase_order_payment.format(org_id=self.org_id, po_id=po_id), json=payload)
415
+
416
+ def metrc_refresh(self) -> tuple[int, dict]:
417
+ return self.post(MeadowEndpoints.metrc_compliance_transfer_sync.format(org_id=self.org_id))
418
+
419
+
420
+ def change_org_by_name(self, name: str):
421
+ organizations = self.roles['organizations']
422
+ if new_org_context := next(filter(lambda o: o['name'] == name, organizations), None):
423
+ self.org_id = new_org_context['id']
424
+ else:
425
+ raise InvalidRequestException(f"Name: {name}, not found in {', '.join([i['name'] for i in organizations])}")
426
+
427
+ def get_reports(self, status="new") -> tuple[int, list]:
428
+ s, r = self.get(MeadowEndpoints.reports.format(org_id=self.org_id), params={"status": status})
429
+ if not isinstance(r, list):
430
+ raise ResponseParseException("Expected a `list` from this endpoint")
431
+ return s, r
432
+
433
+ def download_report(self, report_id: int) -> bytes:
434
+ _, url = self.get(MeadowEndpoints.reports.format(org_id=self.org_id) + f"/{report_id}/url")
435
+ if not isinstance(url, bytes):
436
+ raise ResponseParseException("Expected a `str` from this endpoint")
437
+ r = httpx.get(url.decode())
438
+ return r.content
439
+
440
+ def get_user_data(self, user_id):
441
+ return self.get(MeadowEndpoints.users.format(org_id=self.org_id) + "/" + str(user_id))
442
+
443
+ def get_recent_user_addresses(self, user_id):
444
+ return self.get(MeadowEndpoints.recent_address.format(org_id=self.org_id, user_id=user_id))
445
+
446
+ def cancel_order(self, order_id, cancel_msg: str, adjust_shift: bool = False):
447
+ payload = {"cancelationReason": cancel_msg, "adjustShift": adjust_shift, "status": "canceled"}
448
+ return self.put(MeadowEndpoints.orders.format(org_id=self.org_id) + "/" + order_id, json=payload)
449
+
450
+ def create_order(self, meadow_order: Order | dict):
451
+ if isinstance(meadow_order, Order):
452
+ data = meadow_order.model_dump(by_alias=True, exclude_none=True)
453
+ else:
454
+ data = Order(**meadow_order).model_dump(by_alias=True, exclude_none=True)
455
+ return self.post(MeadowEndpoints.orders.format(org_id=self.org_id), json=data)
456
+
457
+ def create_user(self, meadow_user: dict | NewUser):
458
+ if isinstance(meadow_user, dict):
459
+ meadow_user = NewUser(**meadow_user)
460
+ status_code, result = self.post(MeadowEndpoints.users.format(org_id=self.org_id), json=meadow_user.model_dump(by_alias=True))
461
+ if status_code != 201:
462
+ print("Error creating order", result)
463
+ raise CreateUserException(f"{result}")
464
+ return result
465
+
466
+ def delete_document(self, document_id: int, user_id: int):
467
+ return self.delete(MeadowEndpoints.user_documents.format(org_id=self.org_id, user_id=user_id) + "/" + str(document_id))
468
+
469
+ def get_document(self, user_id, document_id) -> tuple[str, bytes]:
470
+ _, document = self.get(
471
+ MeadowEndpoints.user_documents.format(org_id=self.org_id, user_id=user_id) + f'/{document_id}',
472
+ )
473
+ if not isinstance(document, dict):
474
+ raise ResponseParseException("Expected a `dict` from this endpoint")
475
+ s3_url = document['signedUrl']
476
+ mime_type = document['mime']
477
+ doc_bytes = httpx.get(s3_url).content
478
+ return mime_type, doc_bytes
479
+
480
+ def upload_document(
481
+ self,
482
+ document: dict | AWSDocumentRequest,
483
+ document_type_id: int,
484
+ image: bytes,
485
+ user_id: int,
486
+ mime_type: str
487
+ ) -> tuple[int, dict]:
488
+ if isinstance(document, dict):
489
+ document = AWSDocumentRequest(**document).model_dump(by_alias=True, exclude_none=True)
490
+ else:
491
+ document = document.model_dump(by_alias=True, exclude_none=True)
492
+
493
+ _, signed_res = self.post(MeadowEndpoints.sign_s3.format(org_id=self.org_id), json=document)
494
+ url = signed_res['signedRequest']['url']
495
+ payload = signed_res['signedRequest']['fields']
496
+ files = {'file': ('test.jpg', image, mime_type)}
497
+ image_post_res = httpx.post(url, data=payload, files=files)
498
+ if image_post_res.status_code != 204:
499
+ raise CreateIDException(str(image_post_res.content.decode()))
500
+ meadow_doc = MeadowNewDocument(
501
+ documentTypeId=document_type_id,
502
+ path=signed_res['path'],
503
+ mime=mime_type
504
+ )
505
+ try_count = 0
506
+ while True:
507
+ try:
508
+ status_code, meadow_post = self.post(
509
+ MeadowEndpoints.user_documents.format(org_id=self.org_id, user_id=user_id), json=meadow_doc.model_dump(by_alias=True),
510
+ )
511
+ if status_code != 201:
512
+ raise CreateIDException(str(meadow_post))
513
+ return status_code, meadow_post
514
+ except (ConnectionAbortedError, RemoteDisconnected) as e:
515
+ time.sleep(3)
516
+ try_count += 1
517
+ if try_count >= 4:
518
+ raise CreateIDException(str(e))
519
+
520
+
521
+ def delete_user(self, user_id):
522
+ current_timestamp = datetime.utcnow().strftime(
523
+ '%Y-%m-%dT%H:%M:%S.') + f"{int(datetime.utcnow().microsecond / 1000)}Z"
524
+ payload = {
525
+ "deletedAt": current_timestamp
526
+ }
527
+ return self.put(MeadowEndpoints.users.format(org_id=self.org_id) + f"/{user_id}", json=payload)
528
+
529
+ def search_users(self, query):
530
+ encoded_query = quote(query, safe="")
531
+ return self.get(MeadowEndpoints.user_search.format(org_id=self.org_id) + f"?query={encoded_query}")
532
+
533
+ def check_delivery_zone_address(self, address=None, lat_and_lng=None):
534
+ payload = {"address": address}
535
+ if lat_and_lng:
536
+ payload = {"latLng": lat_and_lng}
537
+ s, r = self.post(MeadowEndpoints.delivery_zone_addresses.format(org_id=self.org_id), json=payload)
538
+ return s, r
539
+
540
+ def post_pricing(
541
+ self,
542
+ patient_hash,
543
+ line_items: list[dict],
544
+ tax_exempt: bool,
545
+ delivery_zone_id: int | None,
546
+ order_type: str
547
+ ):
548
+ pricing = {
549
+ "adjustments": [],
550
+ "discounts": [],
551
+ "lineItems": line_items,
552
+ "patientHash": patient_hash,
553
+ "payments": [],
554
+ "allowEmpty": True,
555
+ "isAdmin": False,
556
+ "taxExempt": tax_exempt,
557
+ "deliveryZoneId": delivery_zone_id,
558
+ "type": order_type
559
+ }
560
+ return self.post(MeadowEndpoints.pricing.format(org_id=self.org_id), json=pricing)
561
+
562
+ @staticmethod
563
+ def check_inventory_options(inventory_id, line_item):
564
+ options = line_item['options']
565
+ for o in options:
566
+ for lc in o['locationInventory']:
567
+ if lc['inventoryLocationId'] == inventory_id and lc['maxQuantity'] > 0:
568
+ return True
569
+ return False
570
+
571
+ @staticmethod
572
+ def set_inventory_stock_value(inventory_id, line_item):
573
+ options = line_item['options']
574
+ line_item['in_stock'] = False
575
+ for o in options:
576
+ for lc in o['locationInventory']:
577
+ if lc['inventoryLocationId'] == inventory_id and lc['maxQuantity'] > 0:
578
+ line_item['in_stock'] = True
579
+ return line_item
580
+
581
+ def get_inventory(
582
+ self,
583
+ inventory_id = None,
584
+ include_archived: bool = True,
585
+ include_moving_average_cost_per_unit: bool = True,
586
+ include_threshold_status: bool = True,
587
+ include_compliance_item_names: bool = True,
588
+ filter_for_active: bool = True,
589
+ filter_for_in_stock: bool = False,
590
+ source="web-admin"
591
+ ) -> list[dict]:
592
+ payload = {
593
+ "includeArchived": "true" if include_archived else "false",
594
+ "includeMovingAverageCostPerUnit": "true" if include_moving_average_cost_per_unit else "false",
595
+ "includeThresholdStatus": "true" if include_threshold_status else "false",
596
+ "includeComplianceItemNames": "true" if include_compliance_item_names else "false",
597
+ "source": source
598
+ }
599
+ _, line_items = self.get(MeadowEndpoints.inventory.format(org_id=self.org_id), params=payload)
600
+ if not isinstance(line_items, list):
601
+ raise ResponseParseException("Expected `list` instance in response")
602
+ if filter_for_active:
603
+ line_items = list(filter(lambda l: l['isActive'], line_items))
604
+ if filter_for_in_stock and inventory_id:
605
+ line_items = list(filter(lambda l: MeadowClient.check_inventory_options(inventory_id, l), line_items))
606
+ if inventory_id:
607
+ line_items = [MeadowClient.set_inventory_stock_value(inventory_id, l) for l in line_items]
608
+ return line_items
609
+
610
+ def get_discounts(self):
611
+ return self.get(MeadowEndpoints.discounts.format(org_id=self.org_id))
612
+
613
+ def get_product(self, product_id):
614
+ return self.get(MeadowEndpoints.product.format(org_id=self.org_id, product_id=product_id))
615
+
616
+
617
+ def get_inventory_transactions(
618
+ self,
619
+ product_id: Optional[int] = None,
620
+ starting_after: Optional[str] = None,
621
+ ) -> tuple[int, dict]:
622
+ params = {}
623
+ if product_id:
624
+ params['productId'] = product_id
625
+ if starting_after:
626
+ params['startAfter'] = starting_after
627
+ s, r = self.get(MeadowEndpoints.inventory_transactions.format(org_id=self.org_id), params=params)
628
+ if not isinstance(r, dict):
629
+ raise ResponseParseException("Expected `list` instance in response")
630
+ return s, r
631
+
632
+
633
+ def delete_product(self, product_id):
634
+ current_timestamp = datetime.utcnow().strftime(
635
+ '%Y-%m-%dT%H:%M:%S.') + f"{int(datetime.utcnow().microsecond / 1000)}Z"
636
+ payload = {"deletedAt": current_timestamp}
637
+ return self.put(MeadowEndpoints.product.format(org_id=self.org_id, product_id=product_id), json=payload)
638
+
639
+ def update_product(self, product_id, product: dict | Product):
640
+ if isinstance(product, dict):
641
+ product = Product(**product)
642
+ return self.put(
643
+ MeadowEndpoints.product.format(org_id=self.org_id, product_id=product_id),
644
+ json=product.model_dump(by_alias=True, exclude_none=True)
645
+ )
646
+
647
+ def update_product_options(self, product_id, options: List[dict | Option], sales_price_unit = None):
648
+ options = [
649
+ Option(**o).model_dump(by_alias=True, exclude_none=True) if isinstance(o,dict) else o.model_dump(by_alias=True, exclude_none=True)
650
+ for o in options
651
+ ]
652
+ payload = {"options": options, "salesPriceUnit": sales_price_unit}
653
+ return self.put(
654
+ MeadowEndpoints.product_options.format(org_id=self.org_id, product_id=product_id),
655
+ json=payload
656
+ )
657
+
658
+ def create_reconciliation(self, reconciliation: dict | Reconciliation):
659
+ if isinstance(reconciliation, dict):
660
+ reconciliation = Reconciliation(**reconciliation)
661
+ return self.post(
662
+ MeadowEndpoints.reconciliations.format(org_id=self.org_id),
663
+ json=reconciliation.model_dump(by_alias=True)
664
+ )
@@ -0,0 +1,44 @@
1
+ class MeadowEndpoints:
2
+ token = "https://admin.getmeadow.com/api/users/tokens"
3
+ orders = "https://api.getmeadow.com/organizations/{org_id}/orders"
4
+ order = "https://api.getmeadow.com/organizations/{org_id}/orders/{order_id}"
5
+ pusher_auth = "https://api.getmeadow.com/pusher/auth"
6
+ me = "https://api.getmeadow.com/users/me"
7
+ roles = "https://api.getmeadow.com/users/me/roles"
8
+ reports = "https://api.getmeadow.com/organizations/{org_id}/reports"
9
+ inventory_locations = "https://api.getmeadow.com/organizations/{org_id}/inventory-locations"
10
+ inventory_vendors = "https://api.getmeadow.com/organizations/{org_id}/inventory-vendors"
11
+ inventory_vendor = "https://api.getmeadow.com/organizations/{org_id}/inventory-vendors/{vendor_id}"
12
+ inventory = "https://daffodil.getmeadow.com/organizations/{org_id}/products/all"
13
+ discounts = "https://api.getmeadow.com/organizations/{org_id}/discounts"
14
+ delivery_zone_addresses = "https://api.getmeadow.com/organizations/{org_id}/delivery-zones/addresses"
15
+ users = "https://api.getmeadow.com/organizations/{org_id}/users"
16
+ inventory_transactions = " https://api.getmeadow.com/organizations/{org_id}/inventory-transactions"
17
+ user_search = users + "/search"
18
+ full = "https://api.getmeadow.com/organizations/{org_id}/full"
19
+ user_documents = "https://api.getmeadow.com/organizations/{org_id}/users/{user_id}/documents"
20
+ recent_address = "https://api.getmeadow.com/organizations/{org_id}/users/{user_id}/addresses/recent"
21
+ pricing = "https://api.getmeadow.com/organizations/{org_id}/pricing"
22
+ create_customer = "https://admin.getmeadow.com/customers/create"
23
+ sign_s3 = "https://api.getmeadow.com/organizations/{org_id}/general/sign-s3"
24
+ prod_document = "https://s3.us-west-2.amazonaws.com/meadow-documents-production"
25
+ brands = "https://api.getmeadow.com/organizations/{org_id}/brands"
26
+ products = "https://api.getmeadow.com/organizations/{org_id}/products"
27
+ product = "https://api.getmeadow.com/organizations/{org_id}/products/{product_id}"
28
+ product_options = "https://api.getmeadow.com/organizations/{org_id}/products/{product_id}/options"
29
+ product_categories = "https://api.getmeadow.com/organizations/{org_id}/product-categories"
30
+ all_product_categories = product_categories + "/all"
31
+ purchase_orders = "https://api.getmeadow.com/organizations/{org_id}/purchase-orders"
32
+ purchase_order = "https://api.getmeadow.com/organizations/{org_id}/purchase-orders/{po_id}"
33
+ purchase_order_payment = "https://api.getmeadow.com/organizations/{org_id}/purchase-orders/{po_id}/payments"
34
+ purchase_order_receive = "https://api.getmeadow.com/organizations/{org_id}/purchase-orders/{po_id}/receive"
35
+ reconciliations = "https://api.getmeadow.com/organizations/{org_id}/reconciliations"
36
+ vendors = "https://api.getmeadow.com/organizations/{org_id}/inventory-vendors"
37
+ metrc_compliance_transfer_sync = "https://api.getmeadow.com/organizations/{org_id}/compliance-transfers/sync"
38
+ compliance_transfers = "https://api.getmeadow.com/organizations/{org_id}/compliance-transfers"
39
+ compliance_transfer = "https://api.getmeadow.com/organizations/{org_id}/compliance-transfers/{compliance_transfer_id}"
40
+ packages = "https://api.getmeadow.com/organizations/{org_id}/packages"
41
+
42
+
43
+ class IamIntercomEndpoints:
44
+ web_ping = "https://api-iam.intercom.io/messenger/web/ping"
@@ -0,0 +1,28 @@
1
+ class CustomerNotFoundException(Exception):
2
+ pass
3
+
4
+
5
+ class CreateOrderException(Exception):
6
+ pass
7
+
8
+
9
+ class CreateUserException(Exception):
10
+ pass
11
+
12
+
13
+ class CreateIDException(Exception):
14
+ pass
15
+
16
+
17
+ class AuthenticationException(Exception):
18
+ pass
19
+
20
+ class InvalidRequestException(Exception):
21
+ pass
22
+
23
+ class ConnectionException(Exception):
24
+ pass
25
+
26
+
27
+ class ResponseParseException(Exception):
28
+ pass
@@ -0,0 +1,184 @@
1
+ from typing import List, Optional, Any, Dict, Literal
2
+ from pydantic import BaseModel, Field
3
+
4
+
5
+ class AWSDocumentRequest(BaseModel):
6
+ type: str
7
+ file_name: str = Field(..., alias="fileName")
8
+ file_type: str = Field(..., alias="fileType")
9
+
10
+
11
+ class MeadowNewDocument(BaseModel):
12
+ document_type_id: int = Field(..., alias="documentTypeId")
13
+ path: str
14
+ mime: str
15
+
16
+
17
+ class NewUser(BaseModel):
18
+ first_name: str = Field(..., alias="firstName")
19
+ last_name: str = Field(..., alias="lastName")
20
+ email: Optional[str]
21
+ phone: str
22
+
23
+
24
+ class Payment(BaseModel):
25
+ payment_type_id: int = Field(..., alias="paymentTypeId")
26
+ remaining: bool = True
27
+
28
+
29
+ class LineItem(BaseModel):
30
+ product_id: int = Field(..., alias="productId")
31
+ product_option_id: int = Field(..., alias="productOptionId")
32
+ quantity: Optional[int] = None
33
+ remove_discount_ids: List[Any] = Field(default_factory=list, alias="removeDiscountIds")
34
+ add_discount_ids: List[int] = Field(default_factory=list, alias="addDiscountIds")
35
+ custom_discounts: List[Any] = Field(default_factory=list, alias="customDiscounts")
36
+
37
+
38
+ class Address(BaseModel):
39
+ street1: str
40
+ street2: Optional[str] = None
41
+ city: str
42
+ state: str
43
+ postal_code: str = Field(..., alias="postalCode")
44
+ county: Optional[str]
45
+
46
+
47
+ class Order(BaseModel):
48
+ type: Literal["delivery", "in-store"]
49
+ payments: List[Payment]
50
+ adjustments: List[Any] = []
51
+ add_discount_ids: List[Any] = Field(default_factory=list, alias="addDiscountIds")
52
+ discount_codes: List[Any] = Field(default_factory=list, alias="discountCodes")
53
+ discount_limits: Dict[Any, Any] = Field(default_factory=dict, alias="discountLimits")
54
+ remove_discount_ids: List[Any] = Field(default_factory=list, alias="removeDiscountIds")
55
+ remove_discount_instances: Dict[Any, Any] = Field(default_factory=dict, alias="removeDiscountInstances")
56
+ admin_notes: Optional[str] = Field(None, alias="adminNotes")
57
+
58
+ #discounts: List[Any] = []
59
+ delivery_zone_id: Optional[int] = Field(None, alias="deliveryZoneId")
60
+ line_items: List[LineItem] = Field(..., alias="lineItems")
61
+ patient_hash: str = Field(..., alias="patientHash")
62
+ status: Optional[str]
63
+ source: str = 'web-admin'
64
+ is_admin: bool = Field(True, alias="isAdmin")
65
+ tax_exempt: bool = Field(False, alias="taxExempt")
66
+ inventory_location_id: Any = Field(None, alias="inventoryLocationId")
67
+ address: Optional[Address] = None
68
+
69
+ class Option(BaseModel):
70
+ amount: int | float = 1
71
+ content: Optional[str] = None
72
+ name: str = ''
73
+ weedmaps_v2_verified_variant: Optional[Any] = Field(None, alias="weedmapsV2VerifiedVariant")
74
+ price: int | float
75
+ sales_price: Optional[Any] = Field(..., alias="salesPrice")
76
+ id: Optional[Any] = None
77
+
78
+ class Product(BaseModel):
79
+ name: str
80
+ strain_type: str = Field(..., alias="strainType")
81
+ category: str
82
+ weedmaps_category_id: Optional[int] = Field(default=None, alias='weedmapsCategoryId')
83
+ sub_categories: List[Any] = Field(default_factory=list, alias="subCategories")
84
+ tags: List[Any] = Field(default_factory=list, alias="tags")
85
+ compounds: Optional[Dict[Any, Any]] = Field(default_factory=dict, alias="compounds")
86
+ unit: str
87
+ brand_id: Optional[int] = Field(None, alias="brandId")
88
+ sales_price_unit: Optional[Any] = Field(None, alias='salesPriceUnit')
89
+ description: str = ''
90
+ is_active: bool = Field(..., alias="isActive")
91
+ is_featured: bool = Field(..., alias="isFeatured")
92
+ options: Optional[List[Option]] = Field(default_factory=list, alias="options")
93
+ pricing_tier_id: Optional[Any] = Field(None, alias='pricingTierId')
94
+ pricing_type: Optional[str] = Field(None, alias="pricingType")
95
+ photos: Optional[List[Any]] = Field(default_factory=list, alias="photos")
96
+ auto_import_compounds: Optional[Any] = Field(None, alias="autoImportCompounds")
97
+
98
+
99
+ class ReconciliationLineItem(BaseModel):
100
+ amount: int
101
+ operation: str
102
+ package_id: Optional[Any] = Field(None, alias="packageId")
103
+ product_id: int = Field(..., alias="productId")
104
+ product_option_id: int = Field(..., alias="productOptionId")
105
+
106
+ class Reconciliation(BaseModel):
107
+ inventory_location_id: int = Field(..., alias="inventoryLocationId")
108
+ line_items: List[ReconciliationLineItem] = Field(default_factory=list, alias="lineItems")
109
+ notes: str
110
+ compliance_notes: str = Field(..., alias="complianceNotes")
111
+ compliance_reason: str = Field(..., alias="complianceReason")
112
+
113
+ class PurchaseOrder(BaseModel):
114
+ id: Optional[int]
115
+ organization_id: int
116
+ user_id: int = Field(..., alias="userId")
117
+ total_amount: str = Field(..., alias="totalAmount")
118
+ created_at: str = Field(..., alias="createdAt")
119
+ updated_at: str = Field(..., alias="updatedAt")
120
+ inventory_vendor_id: int = Field(..., alias="inventoryVendorId")
121
+ notes: Optional[Any] = None
122
+ subtotal: int
123
+ expected_at: str = Field(..., alias="expectedAt")
124
+ status: str
125
+ payment_status: str = Field(..., alias="paymentStatus")
126
+ payment_terms: Optional[Any] = Field(None, alias="paymentTerms")
127
+ payment_terms_due_date: Optional[Any] = Field(None, alias="paymentTermsDueDate")
128
+ total_amount_received: str = Field(..., alias="totalAmountReceived")
129
+ final_total: int = Field(..., alias="finalTotal")
130
+ ca_excise_total: int = Field(..., alias="caExciseTotal")
131
+ amount_paid: int = Field(..., alias="amountPaid")
132
+ amount_outstanding: int = Field(..., alias="amountOutstanding")
133
+ external_invoice_number: str = Field(..., alias="externalInvoiceNumber")
134
+ external_invoice_file_path: Optional[str] = Field(None, alias="externalInvoiceFilePath")
135
+ shipping_handling_fee: int = Field(..., alias="shippingHandlingFee")
136
+ shipping_handling_fee_excise: int = Field(..., alias="shippingHandlingFeeExcise")
137
+ api_consumer_id: Optional[Any] = Field(None, alias="apiConsumerId")
138
+ is_overdue: bool = Field(..., alias="isOverdue")
139
+ inventory_vendor: Optional[Any] = Field(None, alias="inventoryVendor")
140
+ line_items: List[LineItem] = Field(default_factory=list, alias="lineItems")
141
+
142
+
143
+ class ReceiveLineItem(BaseModel):
144
+ amount: int
145
+ purchase_order_line_item_id: int = Field(..., alias="purchaseOrderLineItemId")
146
+ package_id: Optional[int] = Field(..., alias="packageId")
147
+ expirationDate: Optional[str] = Field(..., alias="expirationDate")
148
+ multiplier: Optional[str] = Field(..., alias="multiplier")
149
+ thcPercent: Optional[str] = Field(..., alias="thcPercent")
150
+ thcMg: Optional[str] = Field(None, alias="thcMg")
151
+ cbdPercent: Optional[str] = Field(None, alias="cbdPercent")
152
+ cbdMg: Optional[str] = Field(None, alias="cbdMg")
153
+ producerName: str = Field(..., alias="producerName")
154
+ producerLicense: str = Field(..., alias="producerLicense")
155
+ harvestDate: str = Field(..., alias="harvestDate")
156
+ harvestFacilityName: str = Field(..., alias="harvestFacilityName")
157
+ itemStrain: Optional[str] = Field(..., alias="itemStrain")
158
+ labName: Optional[str] = Field(None, alias="labName")
159
+ labDate: Optional[str] = Field(None, alias="labDate")
160
+
161
+
162
+
163
+
164
+ class CreatePurchaseOrderLineItem(BaseModel):
165
+ product_id: int = Field(..., alias="productId")
166
+ product_option_id: int = Field(..., alias="productOptionId")
167
+ amount: int
168
+ cost_per_unit: str = Field(..., alias="costPerUnit")
169
+ ca_excise_per_unit: int = Field(0, alias="caExcisePerUnit")
170
+ ca_excise_override: Optional[bool] = Field(False, alias="caExciseOverride")
171
+ tmp_id: Optional[str] = Field(None, alias="tmpId")
172
+ compliance_item_name: Optional[str] = Field(None, alias="complianceItemName")
173
+
174
+
175
+ class UpdatePurchaseOrderLineItem(BaseModel):
176
+ id: Optional[int] = Field(None, alias="id")
177
+ product_id: int = Field(..., alias="productId")
178
+ product_option_id: int = Field(..., alias="productOptionId")
179
+ amount: int
180
+ cost_per_unit: str = Field(..., alias="costPerUnit")
181
+ ca_excise_per_unit: int = Field(0, alias="caExcisePerUnit")
182
+ ca_excise_override: Optional[bool] = Field(False, alias="caExciseOverride")
183
+ tmp_id: Optional[str] = Field(None, alias="tmpId")
184
+ compliance_item_name: Optional[str] = Field(None, alias="complianceItemName")