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.
- getmeadow-0.1.0/PKG-INFO +67 -0
- getmeadow-0.1.0/README.md +56 -0
- getmeadow-0.1.0/pyproject.toml +21 -0
- getmeadow-0.1.0/pyproject.toml.orig +22 -0
- getmeadow-0.1.0/src/getmeadow/__init__.py +5 -0
- getmeadow-0.1.0/src/getmeadow/client.py +664 -0
- getmeadow-0.1.0/src/getmeadow/endpoints.py +44 -0
- getmeadow-0.1.0/src/getmeadow/exceptions.py +28 -0
- getmeadow-0.1.0/src/getmeadow/schemas.py +184 -0
getmeadow-0.1.0/PKG-INFO
ADDED
|
@@ -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,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")
|