spyreapi 0.0.6__tar.gz → 0.0.7__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.
- {spyreapi-0.0.6/src/spyreapi.egg-info → spyreapi-0.0.7}/PKG-INFO +6 -2
- {spyreapi-0.0.6 → spyreapi-0.0.7}/README.md +5 -1
- {spyreapi-0.0.6 → spyreapi-0.0.7}/pyproject.toml +1 -1
- {spyreapi-0.0.6 → spyreapi-0.0.7}/src/spyre/__init__.py +1 -1
- {spyreapi-0.0.6 → spyreapi-0.0.7}/src/spyre/client.py +10 -7
- {spyreapi-0.0.6 → spyreapi-0.0.7}/src/spyre/customers.py +3 -3
- {spyreapi-0.0.6 → spyreapi-0.0.7}/src/spyre/inventory.py +289 -40
- {spyreapi-0.0.6 → spyreapi-0.0.7}/src/spyre/sales.py +15 -12
- spyreapi-0.0.7/src/spyre/spire.py +36 -0
- {spyreapi-0.0.6 → spyreapi-0.0.7}/src/spyre/utils.py +18 -15
- {spyreapi-0.0.6 → spyreapi-0.0.7/src/spyreapi.egg-info}/PKG-INFO +6 -2
- spyreapi-0.0.6/src/spyre/spire.py +0 -23
- {spyreapi-0.0.6 → spyreapi-0.0.7}/LICENSE +0 -0
- {spyreapi-0.0.6 → spyreapi-0.0.7}/MANIFEST.in +0 -0
- {spyreapi-0.0.6 → spyreapi-0.0.7}/setup.cfg +0 -0
- {spyreapi-0.0.6 → spyreapi-0.0.7}/src/spyre/Exceptions.py +0 -0
- {spyreapi-0.0.6 → spyreapi-0.0.7}/src/spyre/Models/__init__.py +0 -0
- {spyreapi-0.0.6 → spyreapi-0.0.7}/src/spyre/Models/customers_models.py +0 -0
- {spyreapi-0.0.6 → spyreapi-0.0.7}/src/spyre/Models/inventory_models.py +0 -0
- {spyreapi-0.0.6 → spyreapi-0.0.7}/src/spyre/Models/sales_models.py +0 -0
- {spyreapi-0.0.6 → spyreapi-0.0.7}/src/spyre/Models/shared_models.py +0 -0
- {spyreapi-0.0.6 → spyreapi-0.0.7}/src/spyre/crm.py +0 -0
- {spyreapi-0.0.6 → spyreapi-0.0.7}/src/spyreapi.egg-info/SOURCES.txt +0 -0
- {spyreapi-0.0.6 → spyreapi-0.0.7}/src/spyreapi.egg-info/dependency_links.txt +0 -0
- {spyreapi-0.0.6 → spyreapi-0.0.7}/src/spyreapi.egg-info/requires.txt +0 -0
- {spyreapi-0.0.6 → spyreapi-0.0.7}/src/spyreapi.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: spyreapi
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.7
|
|
4
4
|
Summary: A robust and extensible Python client for interacting with the [Spire Business Software API](https://developer.spiresystems.com/reference). This client provides an object-oriented interface to get, create, update, delete, query, filter, sort, and manage various Spire modules such as Sales Orders, Invoices, Inventory Items, and more.
|
|
5
5
|
Author-email: Sanjid Sharaf <sanjidsharaf1@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -26,6 +26,10 @@ A robust and extensible Python client for interacting with the [Spire Business S
|
|
|
26
26
|
|
|
27
27
|
---
|
|
28
28
|
|
|
29
|
+
## Documentation
|
|
30
|
+
|
|
31
|
+
https://sanjid-sharaf.github.io/spyre/
|
|
32
|
+
|
|
29
33
|
## ✨ Features
|
|
30
34
|
|
|
31
35
|
- ✅ Object-oriented resource wrappers for each module (e.g., `salesOrder`, `invoice`, `item`)
|
|
@@ -41,7 +45,7 @@ A robust and extensible Python client for interacting with the [Spire Business S
|
|
|
41
45
|
## 📦 Installation
|
|
42
46
|
|
|
43
47
|
```bash
|
|
44
|
-
pip install
|
|
48
|
+
pip install spyreapi
|
|
45
49
|
```
|
|
46
50
|
|
|
47
51
|
---
|
|
@@ -4,6 +4,10 @@ A robust and extensible Python client for interacting with the [Spire Business S
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## Documentation
|
|
8
|
+
|
|
9
|
+
https://sanjid-sharaf.github.io/spyre/
|
|
10
|
+
|
|
7
11
|
## ✨ Features
|
|
8
12
|
|
|
9
13
|
- ✅ Object-oriented resource wrappers for each module (e.g., `salesOrder`, `invoice`, `item`)
|
|
@@ -19,7 +23,7 @@ A robust and extensible Python client for interacting with the [Spire Business S
|
|
|
19
23
|
## 📦 Installation
|
|
20
24
|
|
|
21
25
|
```bash
|
|
22
|
-
pip install
|
|
26
|
+
pip install spyreapi
|
|
23
27
|
```
|
|
24
28
|
|
|
25
29
|
---
|
|
@@ -7,14 +7,17 @@ from requests.exceptions import HTTPError, ConnectionError, Timeout, RequestExce
|
|
|
7
7
|
T = TypeVar('T', bound=BaseModel)
|
|
8
8
|
|
|
9
9
|
class SpireClient():
|
|
10
|
-
"""A lightweight
|
|
10
|
+
"""A lightweight to interact with the Spire API using requests sessions for connection reuse and authenticated calls."""
|
|
11
|
+
|
|
11
12
|
def __init__(self, host, company, username, password,):
|
|
12
13
|
"""
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
:
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
Initialize a SpireClient instance.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
host (str): Spire Server host.
|
|
18
|
+
company (str): Spire company.
|
|
19
|
+
username (str): Spire user username.
|
|
20
|
+
password (str): Spire user password.
|
|
18
21
|
"""
|
|
19
22
|
self.session = requests.Session()
|
|
20
23
|
self.session.auth = (username, password)
|
|
@@ -239,7 +242,7 @@ class APIResource(Generic[T]):
|
|
|
239
242
|
setattr(self, k, v)
|
|
240
243
|
|
|
241
244
|
def __getattr__(self, item):
|
|
242
|
-
"""
|
|
245
|
+
""" Delegate unknown attributes to the Pydantic model.
|
|
243
246
|
if you try to access an attribute on your wrapper class that doesn't exist in that class.
|
|
244
247
|
It will automatically forward the request to self._model """
|
|
245
248
|
|
|
@@ -98,13 +98,13 @@ class CustomerClient():
|
|
|
98
98
|
Query customer with optional full-text search, filtering, multi-field sorting, and pagination.
|
|
99
99
|
|
|
100
100
|
Args:
|
|
101
|
-
|
|
101
|
+
query (str, optional): Full-text search string.
|
|
102
102
|
sort (dict, optional): Dictionary of sorting rules (e.g., {"orderDate": "desc", "orderNo": "asc"}).
|
|
103
103
|
filter (dict, optional): Dictionary of filters to apply (will be JSON-encoded and URL-safe).
|
|
104
104
|
all (bool, optional): If True, retrieves all pages of results.
|
|
105
105
|
limit (int, optional): Number of results per page (max 1000).
|
|
106
106
|
start (int, optional): Starting offset for pagination.
|
|
107
|
-
**extra_params: Any additional parameters to include in the query.
|
|
107
|
+
**extra_params (Any): Any additional parameters to include in the query.
|
|
108
108
|
|
|
109
109
|
Returns:
|
|
110
110
|
List[customer]: List of wrapped customer resources.
|
|
@@ -127,7 +127,7 @@ class customer(APIResource[Customer]):
|
|
|
127
127
|
Model = Customer
|
|
128
128
|
|
|
129
129
|
|
|
130
|
-
def delete(self):
|
|
130
|
+
def delete(self) -> bool:
|
|
131
131
|
"""
|
|
132
132
|
Deletes the Customer from Spire.
|
|
133
133
|
|
|
@@ -28,23 +28,40 @@ class ItemsClient():
|
|
|
28
28
|
self.client = client
|
|
29
29
|
self.endpoint = 'inventory/items'
|
|
30
30
|
|
|
31
|
-
def get_item(self, id) -> "item":
|
|
31
|
+
def get_item(self, id: int = None, part_no: str = None, warehouse: str = None) -> "item":
|
|
32
32
|
"""
|
|
33
|
-
Retrieve
|
|
33
|
+
Retrieve an inventory item by ID or (part_no + warehouse).
|
|
34
34
|
|
|
35
|
-
Sends a GET request to the Spire API to fetch
|
|
36
|
-
specified ID
|
|
37
|
-
retains a reference to the
|
|
35
|
+
Sends a GET request to the Spire API to fetch inventory item data for the
|
|
36
|
+
specified ID, or uses a filtered query with part number and warehouse.
|
|
37
|
+
Wraps the result in an `item` instance, which retains a reference to the
|
|
38
|
+
client for further actions.
|
|
38
39
|
|
|
39
40
|
Args:
|
|
40
|
-
id (int): The ID of the
|
|
41
|
+
id (int, optional): The ID of the inventory item to retrieve.
|
|
42
|
+
part_no (str, optional): The part number of the item to retrieve.
|
|
43
|
+
warehouse (str, optional): The warehouse code where the item is located.
|
|
41
44
|
|
|
42
45
|
Returns:
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
item: An `item` wrapper instance containing the retrieved data.
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
ValueError: If neither ID nor (part_no and warehouse) are provided,
|
|
50
|
+
or if no matching item is found.
|
|
45
51
|
"""
|
|
46
|
-
|
|
47
|
-
|
|
52
|
+
if id is not None:
|
|
53
|
+
response = self.client._get(f"/{self.endpoint}/{str(id)}")
|
|
54
|
+
return item.from_json(response, self.client)
|
|
55
|
+
|
|
56
|
+
elif part_no and warehouse:
|
|
57
|
+
items = self.query_inventory_items(filter={"partNo": part_no, "whse": warehouse})
|
|
58
|
+
for itm in items:
|
|
59
|
+
if getattr(itm, "partNo", None) == part_no and getattr(itm, "whse", None) == warehouse:
|
|
60
|
+
return itm
|
|
61
|
+
raise ValueError(f"No item found with part_no='{part_no}' and warehouse='{warehouse}'.")
|
|
62
|
+
|
|
63
|
+
else:
|
|
64
|
+
raise ValueError("You must provide either 'id' or both 'part_no' and 'warehouse'.")
|
|
48
65
|
|
|
49
66
|
def create_item(self, item : 'InventoryItem') -> 'item':
|
|
50
67
|
"""
|
|
@@ -74,7 +91,7 @@ class ItemsClient():
|
|
|
74
91
|
raise CreateRequestError(self.endpoint, status_code=response.get('status_code'), error_message=error_message)
|
|
75
92
|
|
|
76
93
|
|
|
77
|
-
def update_item(self, id: int, inventory_item : 'InventoryItem') ->
|
|
94
|
+
def update_item(self, id: int, inventory_item : 'InventoryItem') -> "item":
|
|
78
95
|
"""
|
|
79
96
|
Update an existing Inventory Item by ID.
|
|
80
97
|
|
|
@@ -84,7 +101,7 @@ class ItemsClient():
|
|
|
84
101
|
|
|
85
102
|
Args:
|
|
86
103
|
id (int): The ID of the item to update.
|
|
87
|
-
|
|
104
|
+
inventory_item (InventoryItem): A InventoryItem instance with the sales order details.
|
|
88
105
|
|
|
89
106
|
Returns:
|
|
90
107
|
item: An instance of the item wrapper class initialized with
|
|
@@ -104,7 +121,7 @@ class ItemsClient():
|
|
|
104
121
|
id (int): The ID of the inventory item to delete.
|
|
105
122
|
|
|
106
123
|
Returns:
|
|
107
|
-
bool: True if the item was successfully deleted, False otherwise.
|
|
124
|
+
bool (bool): True if the item was successfully deleted, False otherwise.
|
|
108
125
|
"""
|
|
109
126
|
return self.client._delete(f"/{self.endpoint}/{str(id)}")
|
|
110
127
|
|
|
@@ -124,13 +141,13 @@ class ItemsClient():
|
|
|
124
141
|
Query inventory items with optional full-text search, filtering, multi-field sorting, and pagination.
|
|
125
142
|
|
|
126
143
|
Args:
|
|
127
|
-
|
|
144
|
+
query (str, optional): Full-text search string.
|
|
128
145
|
sort (dict, optional): Dictionary of sorting rules (e.g., {"orderDate": "desc", "orderNo": "asc"}).
|
|
129
146
|
filter (dict, optional): Dictionary of filters to apply (will be JSON-encoded and URL-safe).
|
|
130
147
|
all (bool, optional): If True, retrieves all pages of results.
|
|
131
148
|
limit (int, optional): Number of results per page (max 1000).
|
|
132
149
|
start (int, optional): Starting offset for pagination.
|
|
133
|
-
**extra_params: Any additional parameters to include in the query.
|
|
150
|
+
**extra_params (Any): Any additional parameters to include in the query.
|
|
134
151
|
|
|
135
152
|
Returns:
|
|
136
153
|
List[item]: List of wrapped sales order resources.
|
|
@@ -166,7 +183,6 @@ class ItemsClient():
|
|
|
166
183
|
items = response.get('records')
|
|
167
184
|
for item in items:
|
|
168
185
|
uoms.append(uom.from_json(json_data=item, client = self.client, item_id = id))
|
|
169
|
-
|
|
170
186
|
return uoms
|
|
171
187
|
|
|
172
188
|
def get_uom(self, item_id :int , uom_id : int) -> "uom":
|
|
@@ -186,10 +202,10 @@ class ItemsClient():
|
|
|
186
202
|
|
|
187
203
|
"""
|
|
188
204
|
response = self.client._get(f"/{self.endpoint}/{str(item_id)}/uoms/{str(uom_id)}")
|
|
189
|
-
return uom.from_json(response, self.client)
|
|
205
|
+
return uom.from_json(response, self.client , item_id = item_id)
|
|
190
206
|
|
|
191
207
|
|
|
192
|
-
def create_item_uom(self, id: int, uom : UnitOfMeasure) ->
|
|
208
|
+
def create_item_uom(self, id: int, uom : UnitOfMeasure) -> "uom":
|
|
193
209
|
"""
|
|
194
210
|
Create a new Unit of Measure (UOM) for a specific inventory item.
|
|
195
211
|
|
|
@@ -212,8 +228,8 @@ class ItemsClient():
|
|
|
212
228
|
location = response.get('headers').get('location')
|
|
213
229
|
parsed_url = urlparse(location)
|
|
214
230
|
path_segments = parsed_url.path.rstrip("/").split("/")
|
|
215
|
-
|
|
216
|
-
return self.get_uom(id)
|
|
231
|
+
new_uom_id = path_segments[-1]
|
|
232
|
+
return self.get_uom(item_id=id, uom_id=new_uom_id)
|
|
217
233
|
else:
|
|
218
234
|
error_message = response.get('content')
|
|
219
235
|
raise CreateRequestError(self.endpoint, status_code=response.get('status_code'), error_message=error_message)
|
|
@@ -234,7 +250,7 @@ class ItemsClient():
|
|
|
234
250
|
"""
|
|
235
251
|
return self.client._delete(f"/{self.endpoint}/{str(item_id)}/uoms/{str(uom_id)}")
|
|
236
252
|
|
|
237
|
-
def update_item_uom(self, item_id: int, uom_id : int,
|
|
253
|
+
def update_item_uom(self, item_id: int, uom_id : int, uom_record :'uom') -> 'uom':
|
|
238
254
|
"""
|
|
239
255
|
Update an existing Unit of Measure (UOM) for a given inventory item.
|
|
240
256
|
|
|
@@ -249,8 +265,8 @@ class ItemsClient():
|
|
|
249
265
|
Returns:
|
|
250
266
|
uom: The updated `uom` instance returned from the API.
|
|
251
267
|
"""
|
|
252
|
-
response = self.client._put(f"/{self.endpoint}/{str(item_id)}/{str(uom_id)}", json=
|
|
253
|
-
return uom.from_json(response, self.client)
|
|
268
|
+
response = self.client._put(f"/{self.endpoint}/{str(item_id)}/uoms/{str(uom_id)}", json=uom_record.model_dump(exclude_none=True, exclude_unset=True))
|
|
269
|
+
return uom.from_json(response, self.client, item_id = item_id)
|
|
254
270
|
|
|
255
271
|
|
|
256
272
|
def get_item_upcs(self, id : int) -> List["upc"]:
|
|
@@ -274,18 +290,95 @@ class ItemsClient():
|
|
|
274
290
|
|
|
275
291
|
return upcs
|
|
276
292
|
|
|
293
|
+
def get_upc(self, item_id :int , upc_id : int) -> "upc":
|
|
294
|
+
"""
|
|
295
|
+
Retrieve a specific UPC (upc) for a given inventory item.
|
|
296
|
+
|
|
297
|
+
This method sends a GET request to the Spire API to fetch a single upc record
|
|
298
|
+
associated with a specific inventory item and upc ID. The response is wrapped
|
|
299
|
+
into a `upc` object that maintains a reference to the API client.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
item_id (int): The unique ID of the inventory item.
|
|
303
|
+
upc_id (int): The unique ID of the upc to retrieve.
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
upc: A `upc` object representing the retrieved unit of measure.
|
|
307
|
+
|
|
308
|
+
"""
|
|
309
|
+
response = self.client._get(f"/{self.endpoint}/{str(item_id)}/upcs/{str(upc_id)}")
|
|
310
|
+
return upc.from_json(response, self.client , item_id = item_id)
|
|
311
|
+
|
|
312
|
+
def create_item_upc(self, id: int, upc : UPC) -> "upc":
|
|
313
|
+
"""
|
|
314
|
+
Create a new UPC (upc) for a specific inventory item.
|
|
315
|
+
|
|
316
|
+
Sends a POST request to the Spire API to create a new upc for the inventory item
|
|
317
|
+
with the specified ID. If successful, the method retrieves and returns the newly
|
|
318
|
+
created upc object. If the creation fails, an exception is raised.
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
id (int): The ID of the inventory item for which to create the upc.
|
|
322
|
+
upc (UPC): A Pydantic model instance representing the upc to create.
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
List[upc]: A list containing the created `upc` object.
|
|
326
|
+
|
|
327
|
+
Raises:
|
|
328
|
+
CreateRequestError: If the API response status is not 201 (Created).
|
|
329
|
+
"""
|
|
330
|
+
response = self.client._post(f"/{self.endpoint}/{str(id)}/upcs", json=upc.model_dump(exclude_unset=True, exclude_none=True))
|
|
331
|
+
if response.get('status_code') == 201:
|
|
332
|
+
location = response.get('headers').get('location')
|
|
333
|
+
parsed_url = urlparse(location)
|
|
334
|
+
path_segments = parsed_url.path.rstrip("/").split("/")
|
|
335
|
+
new_upc_id = path_segments[-1]
|
|
336
|
+
return self.get_upc(item_id=id, upc_id = new_upc_id)
|
|
337
|
+
else:
|
|
338
|
+
error_message = response.get('content')
|
|
339
|
+
raise CreateRequestError(self.endpoint, status_code=response.get('status_code'), error_message=error_message)
|
|
340
|
+
|
|
341
|
+
def delete_upc(self, item_id : int, upc_id :int) -> bool:
|
|
342
|
+
"""
|
|
343
|
+
Delete a UPC (upc) from a specific inventory item.
|
|
344
|
+
|
|
345
|
+
Sends a DELETE request to the Spire API to remove a upc associated with the
|
|
346
|
+
given item and upc ID.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
item_id (int): The ID of the inventory item.
|
|
350
|
+
upc_id (int): The ID of the Unit of Measure to delete.
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
bool: True if the deletion was successful, otherwise raises an error.
|
|
354
|
+
"""
|
|
355
|
+
return self.client._delete(f"/{self.endpoint}/{str(item_id)}/upcs/{str(upc_id)}")
|
|
356
|
+
|
|
357
|
+
def update_item_upc(self, item_id: int, upc_id : int, upc_record :'upc') -> 'upc':
|
|
358
|
+
"""
|
|
359
|
+
Update an existing UPC (upc) for a given inventory item.
|
|
360
|
+
|
|
361
|
+
Sends a PUT request to the Spire API to update the upc record with the specified
|
|
362
|
+
item and upc ID using the provided `upc` data.
|
|
363
|
+
|
|
364
|
+
Args:
|
|
365
|
+
item_id (int): The ID of the inventory item.
|
|
366
|
+
upc_id (int): The ID of the upc to update.
|
|
367
|
+
upc (upc): A `upc` instance with updated field values.
|
|
368
|
+
|
|
369
|
+
Returns:
|
|
370
|
+
upc: The updated `upc` instance returned from the API.
|
|
371
|
+
"""
|
|
372
|
+
response = self.client._put(f"/{self.endpoint}/{str(item_id)}/upcs/{str(upc_id)}", json=upc_record.model_dump(exclude_none=True, exclude_unset=True))
|
|
373
|
+
return upc.from_json(response, self.client, item_id = item_id)
|
|
374
|
+
|
|
277
375
|
|
|
278
376
|
class item(APIResource[InventoryItem]):
|
|
279
377
|
|
|
280
378
|
endpoint = 'inventory/items'
|
|
281
379
|
Model = InventoryItem
|
|
282
380
|
|
|
283
|
-
|
|
284
|
-
#TODO Check Upload, Add Uom & UPC, Add Price Matrix Record, Setting Sell & Buy UOM
|
|
285
|
-
|
|
286
|
-
"""
|
|
287
|
-
|
|
288
|
-
def delete(self):
|
|
381
|
+
def delete(self) -> bool:
|
|
289
382
|
"""
|
|
290
383
|
Cancels or deletes the item.
|
|
291
384
|
|
|
@@ -332,8 +425,25 @@ class item(APIResource[InventoryItem]):
|
|
|
332
425
|
|
|
333
426
|
return uoms
|
|
334
427
|
|
|
428
|
+
def get_upcs(self) -> List["upc"]:
|
|
429
|
+
"""
|
|
430
|
+
Retrieve all UPC records for the current inventory item.
|
|
431
|
+
|
|
432
|
+
This method sends a GET request to the Spire API and returns all UPCs
|
|
433
|
+
associated with this item's ID.
|
|
434
|
+
|
|
435
|
+
Returns:
|
|
436
|
+
List[upc]: A list of `upc` instances representing the available units of measure.
|
|
437
|
+
"""
|
|
438
|
+
upcs = []
|
|
439
|
+
response = self._client._get(f"{self.endpoint}/{self.id}/upcs")
|
|
440
|
+
items = response.get('records')
|
|
441
|
+
for item in items:
|
|
442
|
+
upcs.append(upc.from_json(json_data=item, client = self._client, item_id = id))
|
|
443
|
+
|
|
444
|
+
return upcs
|
|
335
445
|
|
|
336
|
-
def add_uom(self,
|
|
446
|
+
def add_uom(self, uom_record : UnitOfMeasure) -> "uom":
|
|
337
447
|
"""
|
|
338
448
|
Add a new Unit of Measure (UOM) to the current inventory item.
|
|
339
449
|
|
|
@@ -345,22 +455,49 @@ class item(APIResource[InventoryItem]):
|
|
|
345
455
|
uom (UnitOfMeasure): The UnitOfMeasure Pydantic model to be created.
|
|
346
456
|
|
|
347
457
|
Returns:
|
|
348
|
-
|
|
458
|
+
uom (uom): The newly created Unit of measure as a `uom` instance.
|
|
349
459
|
|
|
350
460
|
Raises:
|
|
351
461
|
CreateRequestError: If the API returns a non-201 status code during creation.
|
|
352
462
|
"""
|
|
353
|
-
response = self.
|
|
463
|
+
response = self._client._post(f"/{self.endpoint}/{str(self.id)}/uoms", json=uom_record.model_dump(exclude_unset=True, exclude_none=True))
|
|
354
464
|
if response.get('status_code') == 201:
|
|
355
465
|
location = response.get('headers').get('location')
|
|
356
466
|
parsed_url = urlparse(location)
|
|
357
467
|
path_segments = parsed_url.path.rstrip("/").split("/")
|
|
358
468
|
id = path_segments[-1]
|
|
359
|
-
return self.get_uom(id)
|
|
469
|
+
return InventoryClient(self._client).items.get_uom(id)
|
|
360
470
|
else:
|
|
361
471
|
error_message = response.get('content')
|
|
362
472
|
raise CreateRequestError(self.endpoint, status_code=response.get('status_code'), error_message=error_message)
|
|
473
|
+
|
|
474
|
+
def add_upc(self, upc_record : UPC) -> "upc":
|
|
475
|
+
"""
|
|
476
|
+
Add a new UPC to the current inventory item.
|
|
477
|
+
|
|
478
|
+
Sends a POST request to the Spire API to create a new UPC record using the provided
|
|
479
|
+
UPC model. If the creation is successful (HTTP 201), it retrieves
|
|
480
|
+
and returns the created UPC.
|
|
481
|
+
|
|
482
|
+
Args:
|
|
483
|
+
uom_record (UPC): The UPC Pydantic model to be created.
|
|
363
484
|
|
|
485
|
+
Returns:
|
|
486
|
+
upc (upc): The newly created UPC as a `upc` instance.
|
|
487
|
+
|
|
488
|
+
Raises:
|
|
489
|
+
CreateRequestError: If the API returns a non-201 status code during creation.
|
|
490
|
+
"""
|
|
491
|
+
response = self._client._post(f"/{self.endpoint}/{str(self.id)}/upcs", json=upc_record.model_dump(exclude_unset=True, exclude_none=True))
|
|
492
|
+
if response.get('status_code') == 201:
|
|
493
|
+
location = response.get('headers').get('location')
|
|
494
|
+
parsed_url = urlparse(location)
|
|
495
|
+
path_segments = parsed_url.path.rstrip("/").split("/")
|
|
496
|
+
id = path_segments[-1]
|
|
497
|
+
return InventoryClient(self._client).items.get_upc(id)
|
|
498
|
+
else:
|
|
499
|
+
error_message = response.get('content')
|
|
500
|
+
raise CreateRequestError(self.endpoint, status_code=response.get('status_code'), error_message=error_message)
|
|
364
501
|
|
|
365
502
|
class uom(APIResource[UnitOfMeasure]):
|
|
366
503
|
Model = UnitOfMeasure
|
|
@@ -375,7 +512,7 @@ class uom(APIResource[UnitOfMeasure]):
|
|
|
375
512
|
self._endpoint = f'inventory/items/{item_id}/uoms/{str(uom_id)}'
|
|
376
513
|
self._item_id = item_id
|
|
377
514
|
|
|
378
|
-
def delete(self):
|
|
515
|
+
def delete(self) -> bool:
|
|
379
516
|
"""
|
|
380
517
|
Cancels or deletes the uom.
|
|
381
518
|
|
|
@@ -395,7 +532,7 @@ class uom(APIResource[UnitOfMeasure]):
|
|
|
395
532
|
If an uom object is provided, updates the item using the given data.
|
|
396
533
|
|
|
397
534
|
Args:
|
|
398
|
-
|
|
535
|
+
_uom (uom, optional): An optional uom instance to use for the update.
|
|
399
536
|
|
|
400
537
|
Returns:
|
|
401
538
|
uom: The updated uom object reflecting the new status.
|
|
@@ -408,15 +545,127 @@ class UpcClient():
|
|
|
408
545
|
|
|
409
546
|
def __init__(self, client : SpireClient):
|
|
410
547
|
self.endpoint = 'inventory/upcs'
|
|
548
|
+
self.client = client
|
|
549
|
+
|
|
550
|
+
def get_upc(self, upc_id : int) -> "upc":
|
|
551
|
+
"""
|
|
552
|
+
Retrieve a specific UPC (upc) for given id.
|
|
553
|
+
|
|
554
|
+
This method sends a GET request to the Spire API to fetch a single upc record
|
|
555
|
+
associated with the upc ID. The response is wrapped
|
|
556
|
+
into a `upc` object that maintains a reference to the API client.
|
|
557
|
+
|
|
558
|
+
Args:
|
|
559
|
+
upc_id (int): The unique ID of the upc to retrieve.
|
|
560
|
+
|
|
561
|
+
Returns:
|
|
562
|
+
upc: A `upc` object representing the retrieved unit of measure.
|
|
563
|
+
|
|
564
|
+
"""
|
|
565
|
+
response = self.client._get(f"/{self.endpoint}/{str(upc_id)}")
|
|
566
|
+
return upc.from_json(response, self.client)
|
|
567
|
+
|
|
568
|
+
def create_item_upc(self, upc : UPC) -> "upc":
|
|
569
|
+
"""
|
|
570
|
+
Create a new UPC (upc) for a given id.
|
|
571
|
+
|
|
572
|
+
Sends a POST request to the Spire API to create a new upc.
|
|
573
|
+
If successful, the method retrieves and returns the newly
|
|
574
|
+
created upc object. If the creation fails, an exception is raised.
|
|
575
|
+
For a successful creation, either the whse and the partNo or the inventory
|
|
576
|
+
field has to be filled in
|
|
577
|
+
|
|
578
|
+
Args:
|
|
579
|
+
upc (UPC): A Pydantic model instance representing the upc to create.
|
|
580
|
+
|
|
581
|
+
Returns:
|
|
582
|
+
upc (upc): A list containing the created `upc` object.
|
|
583
|
+
|
|
584
|
+
Raises:
|
|
585
|
+
CreateRequestError: If the API response status is not 201 (Created).
|
|
586
|
+
"""
|
|
587
|
+
response = self.client._post(f"/{self.endpoint}/{str(id)}", json=upc.model_dump(exclude_unset=True, exclude_none=True))
|
|
588
|
+
if response.get('status_code') == 201:
|
|
589
|
+
location = response.get('headers').get('location')
|
|
590
|
+
parsed_url = urlparse(location)
|
|
591
|
+
path_segments = parsed_url.path.rstrip("/").split("/")
|
|
592
|
+
new_upc_id = path_segments[-1]
|
|
593
|
+
return self.get_upc(upc_id = new_upc_id)
|
|
594
|
+
else:
|
|
595
|
+
error_message = response.get('content')
|
|
596
|
+
raise CreateRequestError(self.endpoint, status_code=response.get('status_code'), error_message=error_message)
|
|
597
|
+
|
|
598
|
+
def delete_upc(self, upc_id :int) -> bool:
|
|
599
|
+
"""
|
|
600
|
+
Delete a UPC.
|
|
601
|
+
|
|
602
|
+
Sends a DELETE request to the Spire API to remove a upc associated with the
|
|
603
|
+
given upc ID.
|
|
604
|
+
|
|
605
|
+
Args:
|
|
606
|
+
upc_id (int): The ID of the Unit of Measure to delete.
|
|
607
|
+
|
|
608
|
+
Returns:
|
|
609
|
+
bool: True if the deletion was successful, otherwise raises an error.
|
|
610
|
+
"""
|
|
611
|
+
return self.client._delete(f"/{self.endpoint}/{str(upc_id)}")
|
|
612
|
+
|
|
613
|
+
def update_item_upc(self, upc_id : int, upc_record :'upc') -> 'upc':
|
|
614
|
+
"""
|
|
615
|
+
Update an existing UPC.
|
|
616
|
+
|
|
617
|
+
Sends a PUT request to the Spire API to update the upc record with the specified
|
|
618
|
+
upc ID using the provided `upc` data.
|
|
619
|
+
|
|
620
|
+
Args:
|
|
621
|
+
upc_id (int): The ID of the upc to update.
|
|
622
|
+
upc (upc): A `upc` instance with updated field values.
|
|
623
|
+
|
|
624
|
+
Returns:
|
|
625
|
+
upc: The updated `upc` instance returned from the API.
|
|
626
|
+
"""
|
|
627
|
+
response = self.client._put(f"/{self.endpoint}/{str(upc_id)}", json=upc_record.model_dump(exclude_none=True, exclude_unset=True))
|
|
628
|
+
return upc.from_json(response, self.client)
|
|
411
629
|
|
|
412
630
|
class upc(APIResource[UPC]):
|
|
413
631
|
_endpoint = ''
|
|
414
|
-
Model =
|
|
632
|
+
Model = UPC
|
|
415
633
|
|
|
416
634
|
def __init__(self, model, client, item_id=None):
|
|
417
|
-
|
|
418
|
-
raise ValueError("item_id must be provided")
|
|
635
|
+
|
|
419
636
|
super().__init__(model, client)
|
|
420
637
|
upc_id = model.id
|
|
421
|
-
|
|
422
|
-
|
|
638
|
+
if item_id is None:
|
|
639
|
+
self._endpoint = f'inventory/upcs/{str(upc_id)}'
|
|
640
|
+
else:
|
|
641
|
+
self._endpoint = f'inventory/items/{item_id}/upcs/{str(upc_id)}'
|
|
642
|
+
self._item_id = item_id
|
|
643
|
+
|
|
644
|
+
def delete(self) -> bool:
|
|
645
|
+
"""
|
|
646
|
+
Cancels or deletes the upc.
|
|
647
|
+
|
|
648
|
+
Sends a DELETE request to the API to remove the upc with the current ID.
|
|
649
|
+
|
|
650
|
+
Returns:
|
|
651
|
+
bool: True if the upc was successfully deleted (HTTP 204 or 200), False otherwise.
|
|
652
|
+
"""
|
|
653
|
+
|
|
654
|
+
return self._client._delete(f"/{self._endpoint}")
|
|
655
|
+
|
|
656
|
+
def update(self, _upc: "upc" = None) -> 'upc':
|
|
657
|
+
"""
|
|
658
|
+
Update the upc.
|
|
659
|
+
|
|
660
|
+
If no upc object is provided, updates the current instance on the server.
|
|
661
|
+
If an upc object is provided, updates the item using the given data.
|
|
662
|
+
|
|
663
|
+
Args:
|
|
664
|
+
_upc (upc, optional): An optional upc instance to use for the update.
|
|
665
|
+
|
|
666
|
+
Returns:
|
|
667
|
+
upc: The updated upc object reflecting the new status.
|
|
668
|
+
"""
|
|
669
|
+
data = _upc.model_dump(exclude_unset=True, exclude_none=True) if _upc else self.model_dump(exclude_unset=True, exclude_none=True)
|
|
670
|
+
response = self._client._put(f"/{self._endpoint}", json=data)
|
|
671
|
+
return upc.from_json(response, self._client, item_id = self._item_id)
|
|
@@ -36,7 +36,7 @@ class OrdersClient():
|
|
|
36
36
|
for order in orders:
|
|
37
37
|
if getattr(order, "orderNo", None) == order_number:
|
|
38
38
|
return order
|
|
39
|
-
|
|
39
|
+
raise ValueError(f"No order found for order number {order_number}")
|
|
40
40
|
else:
|
|
41
41
|
raise ValueError("Either 'id' or 'order_number' must be provided.")
|
|
42
42
|
|
|
@@ -116,13 +116,13 @@ class OrdersClient():
|
|
|
116
116
|
Query sales orders with optional full-text search, filtering, multi-field sorting, and pagination.
|
|
117
117
|
|
|
118
118
|
Args:
|
|
119
|
-
|
|
119
|
+
query (str, optional): Full-text search string.
|
|
120
120
|
sort (dict, optional): Dictionary of sorting rules (e.g., {"orderDate": "desc", "orderNo": "asc"}).
|
|
121
121
|
filter (dict, optional): Dictionary of filters to apply (will be JSON-encoded and URL-safe).
|
|
122
122
|
all (bool, optional): If True, retrieves all pages of results.
|
|
123
123
|
limit (int, optional): Number of results per page (max 1000).
|
|
124
124
|
start (int, optional): Starting offset for pagination.
|
|
125
|
-
**extra_params: Any additional parameters to include in the query.
|
|
125
|
+
**extra_params (Any): Any additional parameters to include in the query.
|
|
126
126
|
|
|
127
127
|
Returns:
|
|
128
128
|
List[salesOrder]: List of wrapped sales order resources.
|
|
@@ -145,7 +145,8 @@ class OrdersClient():
|
|
|
145
145
|
|
|
146
146
|
Args:
|
|
147
147
|
id (int): The id of the salesOrder to create a note on.
|
|
148
|
-
|
|
148
|
+
note_body (str): The body of the note.
|
|
149
|
+
note_subject (str): The subject of the note.
|
|
149
150
|
Returns:
|
|
150
151
|
note: The created note
|
|
151
152
|
|
|
@@ -217,13 +218,13 @@ class InvoiceClient():
|
|
|
217
218
|
Query invoices with optional full-text search, filtering, multi-field sorting, and pagination.
|
|
218
219
|
|
|
219
220
|
Args:
|
|
220
|
-
|
|
221
|
+
query (str, optional): Full-text search string.
|
|
221
222
|
sort (dict, optional): Dictionary of sorting rules (e.g., {"orderDate": "desc", "orderNo": "asc"}).
|
|
222
223
|
filter (dict, optional): Dictionary of filters to apply (will be JSON-encoded and URL-safe).
|
|
223
224
|
all (bool, optional): If True, retrieves all pages of results.
|
|
224
225
|
limit (int, optional): Number of results per page (max 1000).
|
|
225
226
|
start (int, optional): Starting offset for pagination.
|
|
226
|
-
**extra_params: Any additional parameters to include in the query.
|
|
227
|
+
**extra_params (Any): Any additional parameters to include in the query.
|
|
227
228
|
|
|
228
229
|
Returns:
|
|
229
230
|
List[invoice]: List of wrapped invoice resources.
|
|
@@ -245,7 +246,7 @@ class salesOrder(APIResource[SalesOrder]):
|
|
|
245
246
|
endpoint = "sales/orders/"
|
|
246
247
|
Model = SalesOrder
|
|
247
248
|
|
|
248
|
-
def invoice(self):
|
|
249
|
+
def invoice(self) -> "invoice":
|
|
249
250
|
"""
|
|
250
251
|
Invoice the current sales order.
|
|
251
252
|
|
|
@@ -253,8 +254,10 @@ class salesOrder(APIResource[SalesOrder]):
|
|
|
253
254
|
Note that quotes (salesOrder with type "Q") cannot be invoiced.
|
|
254
255
|
|
|
255
256
|
Returns:
|
|
256
|
-
invoice: The created invoice object if successful.
|
|
257
|
-
|
|
257
|
+
invoice (invoice): The created invoice object if successful.
|
|
258
|
+
|
|
259
|
+
Raises:
|
|
260
|
+
CreateRequestError: If invoice creation failed.
|
|
258
261
|
|
|
259
262
|
"""
|
|
260
263
|
response = self._client._post(f"/{self.endpoint}/{str(self.id)}/invoice")
|
|
@@ -280,7 +283,7 @@ class salesOrder(APIResource[SalesOrder]):
|
|
|
280
283
|
)
|
|
281
284
|
return salesOrder.from_json(response, self._client)
|
|
282
285
|
|
|
283
|
-
def delete(self):
|
|
286
|
+
def delete(self) -> bool:
|
|
284
287
|
"""
|
|
285
288
|
Cancels or deletes the sales order.
|
|
286
289
|
|
|
@@ -337,7 +340,7 @@ class invoice(APIResource[Invoice]):
|
|
|
337
340
|
endpoint = "sales/invoices/"
|
|
338
341
|
Model = Invoice
|
|
339
342
|
|
|
340
|
-
def reverse(self):
|
|
343
|
+
def reverse(self) -> salesOrder:
|
|
341
344
|
"""
|
|
342
345
|
Convert this invoice into a sales order.
|
|
343
346
|
|
|
@@ -359,7 +362,7 @@ class invoice(APIResource[Invoice]):
|
|
|
359
362
|
If no order object is provided, updates the current instance on the server.
|
|
360
363
|
|
|
361
364
|
Args:
|
|
362
|
-
|
|
365
|
+
invoice_ (Invoice): The Invoice model instance containing updated data.
|
|
363
366
|
|
|
364
367
|
Returns:
|
|
365
368
|
invoice: The updated invoice instance created from the response data.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from .client import SpireClient
|
|
2
|
+
from .sales import OrdersClient, InvoiceClient
|
|
3
|
+
from .customers import CustomerClient
|
|
4
|
+
from .inventory import InventoryClient
|
|
5
|
+
|
|
6
|
+
class Spire:
|
|
7
|
+
"""
|
|
8
|
+
High-level interface to interact with the Spire API.
|
|
9
|
+
|
|
10
|
+
This class wraps the lower-level API clients (Orders, Invoices, Customers, Inventory) into a unified interface,
|
|
11
|
+
initializing them using shared authentication via `SpireClient`.
|
|
12
|
+
|
|
13
|
+
Attributes:
|
|
14
|
+
client (SpireClient): Authenticated Spire API client.
|
|
15
|
+
orders (OrdersClient): Client for accessing sales orders.
|
|
16
|
+
invoices (InvoiceClient): Client for accessing invoices.
|
|
17
|
+
customers (CustomerClient): Client for accessing customer records.
|
|
18
|
+
inventory (InventoryClient): Client for accessing inventory items.
|
|
19
|
+
"""
|
|
20
|
+
def __init__(self, host : str, company : str, username : str, password : str):
|
|
21
|
+
"""
|
|
22
|
+
Creates a Spire session.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
host (str): Spire Server host (e.g., black-disk-5630.spirelan.com:10880).
|
|
26
|
+
company (str): Spire company.
|
|
27
|
+
username (str): Spire user username.
|
|
28
|
+
password (str): Spire user password.
|
|
29
|
+
"""
|
|
30
|
+
self.client = SpireClient(host, company, username, password)
|
|
31
|
+
self.orders = OrdersClient(self.client)
|
|
32
|
+
self.invoices = InvoiceClient(self.client)
|
|
33
|
+
self.customers = CustomerClient(self.client)
|
|
34
|
+
self.inventory = InventoryClient(self.client)
|
|
35
|
+
|
|
36
|
+
|
|
@@ -75,21 +75,24 @@ def create_duplicate_record(
|
|
|
75
75
|
return instance.__class__(**cleaned_data)
|
|
76
76
|
|
|
77
77
|
def create_sales_order_from_invoice(invoice: Invoice) -> SalesOrder:
|
|
78
|
-
"""
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
78
|
+
"""
|
|
79
|
+
Creates a Sales Order from an Invoice by copying relevant fields
|
|
80
|
+
and adjusting quantities and freight charges for order processing.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
invoice (Invoice): The invoice object to convert.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
SalesOrder: A new Sales Order instance populated with data from the invoice,
|
|
87
|
+
including negative freight and order quantities, and duplicated
|
|
88
|
+
address and item records.
|
|
89
|
+
|
|
90
|
+
Notes:
|
|
91
|
+
- Freight amount is negated.
|
|
92
|
+
- Order and committed quantities are negated unless zero.
|
|
93
|
+
- Shipping carrier and tracking number fields are reset to None.
|
|
94
|
+
- Duplicate records are created for address, shipping address, and items
|
|
95
|
+
to avoid mutating the original invoice data.
|
|
93
96
|
"""
|
|
94
97
|
|
|
95
98
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: spyreapi
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.7
|
|
4
4
|
Summary: A robust and extensible Python client for interacting with the [Spire Business Software API](https://developer.spiresystems.com/reference). This client provides an object-oriented interface to get, create, update, delete, query, filter, sort, and manage various Spire modules such as Sales Orders, Invoices, Inventory Items, and more.
|
|
5
5
|
Author-email: Sanjid Sharaf <sanjidsharaf1@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -26,6 +26,10 @@ A robust and extensible Python client for interacting with the [Spire Business S
|
|
|
26
26
|
|
|
27
27
|
---
|
|
28
28
|
|
|
29
|
+
## Documentation
|
|
30
|
+
|
|
31
|
+
https://sanjid-sharaf.github.io/spyre/
|
|
32
|
+
|
|
29
33
|
## ✨ Features
|
|
30
34
|
|
|
31
35
|
- ✅ Object-oriented resource wrappers for each module (e.g., `salesOrder`, `invoice`, `item`)
|
|
@@ -41,7 +45,7 @@ A robust and extensible Python client for interacting with the [Spire Business S
|
|
|
41
45
|
## 📦 Installation
|
|
42
46
|
|
|
43
47
|
```bash
|
|
44
|
-
pip install
|
|
48
|
+
pip install spyreapi
|
|
45
49
|
```
|
|
46
50
|
|
|
47
51
|
---
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
from .client import SpireClient
|
|
2
|
-
from .sales import OrdersClient, InvoiceClient
|
|
3
|
-
from .customers import CustomerClient
|
|
4
|
-
from .inventory import InventoryClient
|
|
5
|
-
|
|
6
|
-
class Spire:
|
|
7
|
-
def __init__(self, host : str, company : str, username : str, password : str):
|
|
8
|
-
"""
|
|
9
|
-
Creates a spire Session
|
|
10
|
-
|
|
11
|
-
:param host (str): Spire Server host. eg: black-disk-5630.spirelan.com:10880
|
|
12
|
-
:param company (str): Spire company
|
|
13
|
-
:param username (str): Spire user username.
|
|
14
|
-
:param password (str): Spire user password.
|
|
15
|
-
|
|
16
|
-
"""
|
|
17
|
-
self.client = SpireClient(host, company, username, password)
|
|
18
|
-
self.orders = OrdersClient(self.client)
|
|
19
|
-
self.invoices = InvoiceClient(self.client)
|
|
20
|
-
self.customers = CustomerClient(self.client)
|
|
21
|
-
self.inventory = InventoryClient(self.client)
|
|
22
|
-
|
|
23
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|