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.
Files changed (26) hide show
  1. {spyreapi-0.0.6/src/spyreapi.egg-info → spyreapi-0.0.7}/PKG-INFO +6 -2
  2. {spyreapi-0.0.6 → spyreapi-0.0.7}/README.md +5 -1
  3. {spyreapi-0.0.6 → spyreapi-0.0.7}/pyproject.toml +1 -1
  4. {spyreapi-0.0.6 → spyreapi-0.0.7}/src/spyre/__init__.py +1 -1
  5. {spyreapi-0.0.6 → spyreapi-0.0.7}/src/spyre/client.py +10 -7
  6. {spyreapi-0.0.6 → spyreapi-0.0.7}/src/spyre/customers.py +3 -3
  7. {spyreapi-0.0.6 → spyreapi-0.0.7}/src/spyre/inventory.py +289 -40
  8. {spyreapi-0.0.6 → spyreapi-0.0.7}/src/spyre/sales.py +15 -12
  9. spyreapi-0.0.7/src/spyre/spire.py +36 -0
  10. {spyreapi-0.0.6 → spyreapi-0.0.7}/src/spyre/utils.py +18 -15
  11. {spyreapi-0.0.6 → spyreapi-0.0.7/src/spyreapi.egg-info}/PKG-INFO +6 -2
  12. spyreapi-0.0.6/src/spyre/spire.py +0 -23
  13. {spyreapi-0.0.6 → spyreapi-0.0.7}/LICENSE +0 -0
  14. {spyreapi-0.0.6 → spyreapi-0.0.7}/MANIFEST.in +0 -0
  15. {spyreapi-0.0.6 → spyreapi-0.0.7}/setup.cfg +0 -0
  16. {spyreapi-0.0.6 → spyreapi-0.0.7}/src/spyre/Exceptions.py +0 -0
  17. {spyreapi-0.0.6 → spyreapi-0.0.7}/src/spyre/Models/__init__.py +0 -0
  18. {spyreapi-0.0.6 → spyreapi-0.0.7}/src/spyre/Models/customers_models.py +0 -0
  19. {spyreapi-0.0.6 → spyreapi-0.0.7}/src/spyre/Models/inventory_models.py +0 -0
  20. {spyreapi-0.0.6 → spyreapi-0.0.7}/src/spyre/Models/sales_models.py +0 -0
  21. {spyreapi-0.0.6 → spyreapi-0.0.7}/src/spyre/Models/shared_models.py +0 -0
  22. {spyreapi-0.0.6 → spyreapi-0.0.7}/src/spyre/crm.py +0 -0
  23. {spyreapi-0.0.6 → spyreapi-0.0.7}/src/spyreapi.egg-info/SOURCES.txt +0 -0
  24. {spyreapi-0.0.6 → spyreapi-0.0.7}/src/spyreapi.egg-info/dependency_links.txt +0 -0
  25. {spyreapi-0.0.6 → spyreapi-0.0.7}/src/spyreapi.egg-info/requires.txt +0 -0
  26. {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.6
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 -r requirements.txt
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 -r requirements.txt
26
+ pip install spyreapi
23
27
  ```
24
28
 
25
29
  ---
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "spyreapi"
7
- version = "0.0.6"
7
+ version = "0.0.7"
8
8
  authors = [
9
9
  { name="Sanjid Sharaf", email="sanjidsharaf1@gmail.com" },
10
10
  ]
@@ -12,7 +12,7 @@ __all__ = [
12
12
  "Spire",
13
13
  "ItemsClient",
14
14
  "InventoryClient",
15
- "SalesOrderClient",
15
+ "OrdersClient",
16
16
  "SalesOrder",
17
17
  "SalesOrderItem",
18
18
  "InventoryItem",
@@ -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 client to interact with the Spire API using requests sessions for connection reuse and authenticated calls."""
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
- :param host (str): Spire Server host
14
- :param company (str): Spire company
15
- :param username (str): Spire user username.
16
- :param password (str): Spire user password.
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
- """ delegate unknown attributes to the Pydantic model.
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
- q (str, optional): Full-text search string.
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 a inventory item by its ID.
33
+ Retrieve an inventory item by ID or (part_no + warehouse).
34
34
 
35
- Sends a GET request to the Spire API to fetch a inentory item data for the
36
- specified ID. Wraps the result in a `item` instance, which
37
- retains a reference to the client for further actions.
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 sales order to retrieve.
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
- salesOrder: A `item` wrapper instance containing the retrieved
44
- data and a reference to the client session.
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
- response = self.client._get(f"/{self.endpoint}/{str(id)}")
47
- return item.from_json(response, self.client)
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') -> 'item':
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
- item (InventoryItem): A InventoryItem instance with the sales order details.
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
- q (str, optional): Full-text search string.
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) -> List["uom"]:
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
- id = path_segments[-1]
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, uom :'uom') -> 'uom':
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=uom.model_dump(exclude_none=True, exclude_unset=True))
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, uom : UnitOfMeasure) -> List["uom"]:
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
- List[uom]: A list containing the newly created `uom` instance.
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.client._post(f"/{self.endpoint}/{str(self.id)}/uoms", json=uom.model_dump(exclude_unset=True, exclude_none=True))
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
- uom (uom, optional): An optional uom instance to use for the update.
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
- if item_id is None:
418
- raise ValueError("item_id must be provided")
635
+
419
636
  super().__init__(model, client)
420
637
  upc_id = model.id
421
- self._endpoint = f'inventory/items/{item_id}/upcs/{str(upc_id)}'
422
- self._item_id = item_id
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
- return None
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
- q (str, optional): Full-text search string.
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
- note (str): The body of the note
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
- q (str, optional): Full-text search string.
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
- dict: The full response if invoicing failed.
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
- invoice (Invoice): The Invoice model instance containing updated data.
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
- """Creates a Sales Order from an Invoice
79
-
80
-
81
- Changes when converting
82
- freight negative
83
- backorders
84
- extendedpriceorderd
85
- orderqty
86
- shippingCarrier
87
- subtotalordered
88
- taxes : {total}
89
- total
90
- totalOrdered
91
- trackingNo
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.6
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 -r requirements.txt
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