spyreapi 0.0.2__tar.gz → 0.0.4__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 (30) hide show
  1. spyreapi-0.0.4/MANIFEST.in +4 -0
  2. {spyreapi-0.0.2/src/spyreapi.egg-info → spyreapi-0.0.4}/PKG-INFO +36 -1
  3. spyreapi-0.0.4/README.md +57 -0
  4. {spyreapi-0.0.2 → spyreapi-0.0.4}/pyproject.toml +1 -1
  5. {spyreapi-0.0.2 → spyreapi-0.0.4}/src/spyre/Models/inventory_models.py +7 -3
  6. {spyreapi-0.0.2 → spyreapi-0.0.4}/src/spyre/Models/sales_models.py +1 -1
  7. {spyreapi-0.0.2 → spyreapi-0.0.4}/src/spyre/Models/shared_models.py +30 -1
  8. {spyreapi-0.0.2 → spyreapi-0.0.4}/src/spyre/client.py +83 -13
  9. spyreapi-0.0.4/src/spyre/crm.py +26 -0
  10. {spyreapi-0.0.2 → spyreapi-0.0.4}/src/spyre/customers.py +1 -1
  11. {spyreapi-0.0.2 → spyreapi-0.0.4}/src/spyre/inventory.py +1 -1
  12. {spyreapi-0.0.2 → spyreapi-0.0.4}/src/spyre/sales.py +58 -8
  13. spyreapi-0.0.4/src/spyre/spire.py +23 -0
  14. {spyreapi-0.0.2 → spyreapi-0.0.4/src/spyreapi.egg-info}/PKG-INFO +36 -1
  15. {spyreapi-0.0.2 → spyreapi-0.0.4}/src/spyreapi.egg-info/SOURCES.txt +2 -3
  16. spyreapi-0.0.2/MANIFEST.in +0 -3
  17. spyreapi-0.0.2/README.md +0 -22
  18. spyreapi-0.0.2/src/spyre/config.py +0 -8
  19. spyreapi-0.0.2/src/spyre/spire.py +0 -14
  20. spyreapi-0.0.2/tests/testing.py +0 -378
  21. {spyreapi-0.0.2 → spyreapi-0.0.4}/LICENSE +0 -0
  22. {spyreapi-0.0.2 → spyreapi-0.0.4}/setup.cfg +0 -0
  23. {spyreapi-0.0.2 → spyreapi-0.0.4}/src/spyre/Exceptions.py +0 -0
  24. {spyreapi-0.0.2 → spyreapi-0.0.4}/src/spyre/Models/__init__.py +0 -0
  25. {spyreapi-0.0.2 → spyreapi-0.0.4}/src/spyre/Models/customers_models.py +0 -0
  26. {spyreapi-0.0.2 → spyreapi-0.0.4}/src/spyre/__init__.py +0 -0
  27. {spyreapi-0.0.2 → spyreapi-0.0.4}/src/spyre/utils.py +0 -0
  28. {spyreapi-0.0.2 → spyreapi-0.0.4}/src/spyreapi.egg-info/dependency_links.txt +0 -0
  29. {spyreapi-0.0.2 → spyreapi-0.0.4}/src/spyreapi.egg-info/requires.txt +0 -0
  30. {spyreapi-0.0.2 → spyreapi-0.0.4}/src/spyreapi.egg-info/top_level.txt +0 -0
@@ -0,0 +1,4 @@
1
+ include README.md
2
+ include LICENSE
3
+ global-exclude *.pyc
4
+ global-exclude tests/*
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spyreapi
3
- Version: 0.0.2
3
+ Version: 0.0.4
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
@@ -42,3 +42,38 @@ A robust and extensible Python client for interacting with the [Spire Business S
42
42
 
43
43
  ```bash
44
44
  pip install -r requirements.txt
45
+ ```
46
+
47
+ ---
48
+
49
+ ## ⚙️ Configuration
50
+
51
+ - How to set up your spire client
52
+
53
+ ### Find your Spire URL
54
+
55
+ The base URL for the Spire API is the same url provided by Spire that you use to access Spire server and uses port 10880 as default:
56
+ Replace {spire-url} with the url provided by Spire.
57
+
58
+ - https://{spire-url}:10880/api/v2/
59
+
60
+ Spire Cloud
61
+ If you are using Spire cloud you do not need to specify a port. The base URL for API for Spire Cloud customers would be:
62
+
63
+ - https://{spire-cloud-url}/api/v2/
64
+
65
+ ### Set up your client with your credentials
66
+
67
+ ```python
68
+ from spyre import Spire
69
+ # host is your spire url and the port if applicable
70
+ client = Spire(host = 'your-spire-host', company = 'comapany-name' , username = 'username' , password = 'password' )
71
+
72
+ ```
73
+
74
+ ## Example : Updating the status of an inventory item
75
+ ```python
76
+ item = client.inventory.items.get_item(1101) # Gets item with id 1101
77
+ item.status = 1 # Use either item. or item.model. . item.model. will bring up all attributes
78
+ item.update()
79
+ ```
@@ -0,0 +1,57 @@
1
+ # Spire API Python Client
2
+
3
+ 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.
4
+
5
+ ---
6
+
7
+ ## ✨ Features
8
+
9
+ - ✅ Object-oriented resource wrappers for each module (e.g., `salesOrder`, `invoice`, `item`)
10
+ - 🔍 Full-text search via `q` parameter
11
+ - 🔁 Pagination with `start` and `limit` support
12
+ - 🧾 JSON-based advanced filtering (supports `$gt`, `$lt`, `$in`, `$or`, etc.)
13
+ - ↕️ Multi-field sorting with ascending/descending control
14
+ - 🔧 Clean abstraction layer for API endpoints
15
+ - 📦 Powered by `pydantic` models for validation
16
+
17
+ ---
18
+
19
+ ## 📦 Installation
20
+
21
+ ```bash
22
+ pip install -r requirements.txt
23
+ ```
24
+
25
+ ---
26
+
27
+ ## ⚙️ Configuration
28
+
29
+ - How to set up your spire client
30
+
31
+ ### Find your Spire URL
32
+
33
+ The base URL for the Spire API is the same url provided by Spire that you use to access Spire server and uses port 10880 as default:
34
+ Replace {spire-url} with the url provided by Spire.
35
+
36
+ - https://{spire-url}:10880/api/v2/
37
+
38
+ Spire Cloud
39
+ If you are using Spire cloud you do not need to specify a port. The base URL for API for Spire Cloud customers would be:
40
+
41
+ - https://{spire-cloud-url}/api/v2/
42
+
43
+ ### Set up your client with your credentials
44
+
45
+ ```python
46
+ from spyre import Spire
47
+ # host is your spire url and the port if applicable
48
+ client = Spire(host = 'your-spire-host', company = 'comapany-name' , username = 'username' , password = 'password' )
49
+
50
+ ```
51
+
52
+ ## Example : Updating the status of an inventory item
53
+ ```python
54
+ item = client.inventory.items.get_item(1101) # Gets item with id 1101
55
+ item.status = 1 # Use either item. or item.model. . item.model. will bring up all attributes
56
+ item.update()
57
+ ```
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "spyreapi"
7
- version = "0.0.2"
7
+ version = "0.0.4"
8
8
  authors = [
9
9
  { name="Sanjid Sharaf", email="sanjidsharaf1@gmail.com" },
10
10
  ]
@@ -37,7 +37,11 @@ class UnitOfMeasure(BaseModel):
37
37
 
38
38
  class Pricing(BaseModel):
39
39
  id: Optional[int] = None
40
- sellPrices: Optional[List[str]] = None
40
+ sellPrice: Optional[List[str]] = None
41
+ currMargin: Optional[str] = None
42
+ currMarginPct: Optional[str] = None
43
+ avgMargin: Optional[str] = None
44
+ avgMarginPct: Optional[str] = None
41
45
 
42
46
 
43
47
  class ItemUDF(BaseModel):
@@ -143,7 +147,7 @@ class InventoryItem(BaseModel):
143
147
  manufactureCountry: Optional[str] = None
144
148
  harmonizedCode: Optional[str] = None
145
149
  extendedDescription: Optional[str] = None
146
- pricing: Optional[Dict[str, Pricing]] = None
150
+ pricing: Optional[Union[ Pricing | Dict[str, Pricing]]] = None
147
151
  salesTaxFlags: Optional[Dict[str, Union[str, int, float, bool]]] = None
148
152
  images: Optional[List[str]] = None
149
153
  defaultExpiryDate: Optional[str] = None
@@ -151,7 +155,7 @@ class InventoryItem(BaseModel):
151
155
  upload: Optional[bool] = None
152
156
  showOptions: Optional[bool] = None
153
157
  lastModified: Optional[str] = None
154
- levy: Optional[str] = None
158
+ levy: Optional[Union[str | dict]] = None
155
159
  udf: Optional[ItemUDF] = None
156
160
  createdBy: Optional[str] = None
157
161
  modifiedBy: Optional[str] = None
@@ -15,7 +15,7 @@ class SalesOrderItem(BaseModel):
15
15
  sequence: Optional[int] = None
16
16
  parentSequence: Optional[int] = None
17
17
  inventory: Optional[Inventory] = None
18
- serials: Optional[str] = None
18
+ serials: Optional[List[str]] = None
19
19
  whse: Optional[str] = None
20
20
  partNo: Optional[str] = None
21
21
  description: Optional[str] = None
@@ -1,4 +1,4 @@
1
- from typing import List, Optional, Dict, Union
1
+ from typing import List, Optional, Dict, Union, Any
2
2
  from pydantic import BaseModel, model_validator
3
3
 
4
4
  class PhoneFax(BaseModel):
@@ -94,3 +94,32 @@ class Address(BaseModel):
94
94
  if isinstance(contacts, list) and len(contacts) > 3:
95
95
  data['contacts'] = contacts[:3]
96
96
  return data
97
+
98
+ class AssignedTo(BaseModel):
99
+ id: Optional[int] = None
100
+ uuid: Optional[str] = None
101
+ username: Optional[str] = None
102
+
103
+ class Note(BaseModel):
104
+ id: Optional[int] = None
105
+ linkTable: Optional[str] = None
106
+ linkNo: Optional[str] = None
107
+ subject: Optional[str] = None
108
+ body: Optional[str] = None
109
+ attachment: Optional[Any] = None
110
+ attachmentName: Optional[str] = None
111
+ dueDate: Optional[str] = None
112
+ completedDate: Optional[str] = None
113
+ attention: Optional[str] = None
114
+ type: Optional[str] = None
115
+ displayType: Optional[str] = None
116
+ assignedTo: Optional[AssignedTo] = None
117
+ groupType: Optional[str] = None
118
+ qty: Optional[int] = None
119
+ alert: Optional[Union[str, bool]] = None
120
+ print: Optional[Union[str, bool]] = None
121
+ created: Optional[str] = None
122
+ createdBy: Optional[str] = None
123
+ modified: Optional[str] = None
124
+ modifiedBy: Optional[str] = None
125
+ links: Optional[Dict[str, str]] = None
@@ -1,22 +1,46 @@
1
1
  import requests
2
- from .config import BASE_URL
3
2
  from typing import TypeVar, Optional, Type, Generic, List, Union, Tuple, Dict, Any
4
3
  from pydantic import BaseModel
5
4
  import json
6
5
  import urllib.parse
6
+ from requests.exceptions import HTTPError, ConnectionError, Timeout, RequestException
7
7
 
8
8
  T = TypeVar('T', bound=BaseModel)
9
9
 
10
- class SpireClient():
11
-
12
- def __init__(self, company, username, password,):
10
+ class SpireClient():
11
+ """A lightweight client to interact with the Spire API using requests sessions for connection reuse and authenticated calls."""
12
+ def __init__(self, host, company, username, password,):
13
+ """
14
+ :param host (str): Spire Server host
15
+ :param company (str): Spire company
16
+ :param username (str): Spire user username.
17
+ :param password (str): Spire user password.
18
+
19
+ """
13
20
  self.session = requests.Session()
14
21
  self.session.auth = (username, password)
15
22
  self.session.headers.update({
16
23
  "accept": "application/json",
17
24
  "content-type": "application/json"
18
25
  })
19
- self.base_url = f"{BASE_URL}/{company}"
26
+ self.base_url = f"https://{host}/api/v2/companies/{company}"
27
+
28
+ try:
29
+ response = self.session.get(self.base_url)
30
+ if response.text == 'No such company intertes':
31
+ raise ValueError(f"No company entries for {company}")
32
+ if response.text == 'Unauthorized':
33
+ raise ValueError(f"Invalid Authorization")
34
+
35
+ except ConnectionError as conn_err:
36
+ print(f"Connection error occurred for : {conn_err}")
37
+
38
+ except Timeout as timeout_err:
39
+ print(f"Request timed out: {timeout_err}")
40
+
41
+ except RequestException as req_err:
42
+ print(f"General error occurred: {req_err}")
43
+
20
44
 
21
45
  def _get(self, endpoint, params=None):
22
46
 
@@ -45,20 +69,50 @@ class SpireClient():
45
69
  return response.json()
46
70
 
47
71
  def _post(self, endpoint, data=None, json=None):
72
+ """
73
+ Send a POST request to the Spire API.
74
+
75
+ Args:
76
+ endpoint (str): The relative API endpoint (e.g., 'sales/orders').
77
+ data (dict, optional): Data to send in the body of the request.
78
+ json (dict, optional): JSON data to send in the body of the request.
79
+
80
+ Returns:
81
+ dict: A dictionary containing the response status code, URL, content, and headers.
82
+ """
48
83
  url = f"{self.base_url}/{endpoint.lstrip('/')}"
49
84
  response = self.session.post(url, data=data, json=json)
50
85
  return self._handle_response(response)
51
86
 
52
87
  def _put(self, endpoint, data=None, json=None):
88
+ """
89
+ Send a PUT request to the Spire API.
90
+
91
+ Args:
92
+ endpoint (str): The relative API endpoint (e.g., 'inventory/items/123').
93
+ data (dict, optional): Data to send in the body of the request.
94
+ json (dict, optional): JSON data to send in the body of the request.
95
+
96
+ Returns:
97
+ dict: A dictionary containing the response status code, URL, content, and headers.
98
+ """
53
99
  url = f"{self.base_url}/{endpoint.lstrip('/')}"
54
100
  response = self.session.put(url, data=data, json=json)
55
- response.raise_for_status()
56
- return response.json()
101
+ return self._handle_response(response)
57
102
 
58
103
  def _delete(self, endpoint):
104
+ """
105
+ Send a DELETE request to the Spire API.
106
+
107
+ Args:
108
+ endpoint (str): The relative API endpoint to delete (e.g., 'inventory/items/123').
109
+
110
+ Returns:
111
+ bool: True if the deletion was successful (status code 200, 202, or 204), False otherwise.
112
+ """
59
113
  url = f"{self.base_url}/{endpoint.lstrip('/')}"
60
114
  response = self.session.delete(url)
61
- return response.status_code == 200 or response.status_code == 204 or response.status_code == 202
115
+ return response.status_code in (200, 202, 204)
62
116
 
63
117
  def _handle_response(self, response):
64
118
 
@@ -106,18 +160,25 @@ class SpireClient():
106
160
  """
107
161
  collected = []
108
162
  current_start = start
109
-
163
+ remaining = limit
110
164
  while True:
165
+
166
+ current_limit = min(remaining, 1000)
111
167
  # Build the query params as a list of tuples to allow repeated keys like 'sort'
112
168
  params: List[Tuple[str, Any]] = [
113
169
  ("start", current_start),
114
- ("limit", min(limit, 1000))
170
+ ("limit", current_limit)
115
171
  ]
116
172
 
117
173
  if query:
118
174
  params.append(("q", query))
119
-
175
+
120
176
  if filter:
177
+ model_fields = resource_cls.Model.model_fields.keys()
178
+ invalid_fields = [key for key in filter.keys() if key not in model_fields]
179
+ if invalid_fields:
180
+ raise ValueError(f"Invalid filter field(s): {invalid_fields}. for {resource_cls.Model.__name__} ")
181
+
121
182
  encoded_filter = json.dumps(filter)
122
183
  params.append(("filter", encoded_filter))
123
184
 
@@ -137,10 +198,19 @@ class SpireClient():
137
198
  for item in items:
138
199
  collected.append(resource_cls.from_json(item, self))
139
200
 
140
- if not all or (current_start + limit >= count):
201
+ # Exit if:
202
+ # - 'all' is False and we reached the requested 'limit'
203
+ # - no more items are returned
204
+ if not all:
205
+ remaining -= len(items)
206
+ if remaining <= 0 or len(items) == 0:
207
+ break
208
+
209
+ # Exit if there are no more items available
210
+ if (current_start + current_limit) >= count or len(items) == 0:
141
211
  break
142
212
 
143
- current_start += limit
213
+ current_start += current_limit
144
214
 
145
215
  return collected
146
216
 
@@ -0,0 +1,26 @@
1
+ from .client import APIResource, SpireClient
2
+ from .Models.shared_models import Note
3
+
4
+ class CRMClient():
5
+
6
+ def __init__(self, client: SpireClient):
7
+ self.client = client
8
+ self.endpoint = "crm"
9
+
10
+ def get_note(self, id : int) -> "note":
11
+ """
12
+ Retrieve a note by its ID.
13
+
14
+ Args:
15
+ id (int): The ID of the note to retrieve.
16
+
17
+ Returns:
18
+ note: The retrieved note
19
+ """
20
+ response = self.client._get(f"/{self.endpoint}/notes/{str(id)}")
21
+ return note.from_json(response, self.client)
22
+
23
+
24
+ class note(APIResource[Note]):
25
+ endpoint = 'crm/notes'
26
+ Model = Note
@@ -83,7 +83,7 @@ class CustomerClient():
83
83
  return self.client._delete(f"/{self.endpoint}/{str(id)}")
84
84
 
85
85
 
86
- def query_invoices(
86
+ def query_customers (
87
87
  self,
88
88
  *,
89
89
  query: Optional[str] = None,
@@ -48,7 +48,7 @@ class ItemsClient():
48
48
 
49
49
  def create_item(self, item : 'InventoryItem') -> 'item':
50
50
  """
51
- Create a new Inventory Item in SPire.
51
+ Create a new Inventory Item in Spire.
52
52
 
53
53
  Sends a POST request to the Inventory/Items endpoint .
54
54
 
@@ -1,10 +1,12 @@
1
1
  from .client import APIResource
2
2
  from .Models.sales_models import SalesOrder, SalesOrderItem, Invoice
3
+ from .Models.shared_models import Note
3
4
  from .utils import *
4
5
  from .client import SpireClient
5
6
  from urllib.parse import urlparse
6
7
  from .Exceptions import CreateRequestError
7
8
  from typing import Any, Optional, List, Dict
9
+ from .crm import note, CRMClient
8
10
 
9
11
  class OrdersClient():
10
12
 
@@ -37,7 +39,7 @@ class OrdersClient():
37
39
  Sends a POST request to the sales order endpoint .
38
40
 
39
41
  Args:
40
- sales_order (dict): A SalesOrder instace containing the sales order details.
42
+ sales_order (dict): A SalesOrder instance containing the sales order details.
41
43
 
42
44
  Returns:
43
45
  salesOrder: The created SalesOrder instance.
@@ -129,6 +131,31 @@ class OrdersClient():
129
131
  **extra_params
130
132
  )
131
133
 
134
+ def create_sales_order_note(self, id: int , note_body : str, note_subject : str = "Note") -> note:
135
+ """
136
+ Create a new note on this sales order.
137
+
138
+ Args:
139
+ id (int): The id of the salesOrder to create a note on.
140
+ note (str): The body of the note
141
+ Returns:
142
+ note: The created note
143
+
144
+ Raises:
145
+ CreateRequestError: If the creation fails or response is invalid.
146
+ """
147
+
148
+ note_model = Note(body=note_body, subject=note_subject)
149
+ response = self.client._post(f"/{self.endpoint}/{id}/notes/", json=note_model.model_dump(exclude_unset=True, exclude_none=True))
150
+ if response.get('status_code') == 201:
151
+ location = response.get('headers').get('location')
152
+ parsed_url = urlparse(location)
153
+ path_segments = parsed_url.path.rstrip("/").split("/")
154
+ id = path_segments[-1]
155
+ return CRMClient(client=self.client).get_note(id)
156
+ else:
157
+ error_message = response.get('content')
158
+ raise CreateRequestError(self.endpoint, status_code=response.get('status_code'), error_message=error_message)
132
159
 
133
160
  class InvoiceClient():
134
161
 
@@ -223,12 +250,9 @@ class salesOrder(APIResource[SalesOrder]):
223
250
 
224
251
  """
225
252
  response = self._client._post(f"/{self.endpoint}/{str(self.id)}/invoice")
226
- if response.get('status_code') == 201:
227
- location = response.get('headers').get('location')
228
- parsed_url = urlparse(location)
229
- path_segments = parsed_url.path.rstrip("/").split("/")
230
- id = path_segments[-1]
231
- return InvoiceClient(client=self._client).get_invoice(id)
253
+ if response.get('status_code') == 200:
254
+ data = response.get('content').get('invoice')
255
+ return salesOrder.from_json(json_data=data, client= self._client)
232
256
  else:
233
257
  error_message = response.get('content')
234
258
  raise CreateRequestError(self.endpoint, status_code=response.get('status_code'), error_message=error_message)
@@ -277,6 +301,30 @@ class salesOrder(APIResource[SalesOrder]):
277
301
  response = self._client._put(f"/{self.endpoint}/{str(self.id)}", json=data)
278
302
  return salesOrder.from_json(response, self._client)
279
303
 
304
+ def add_note(self, note_body : "str" , note_subject : "str" = "Note") -> note:
305
+ """
306
+ Add a note to this sales order
307
+
308
+ Args:
309
+ note_body (str): the body of the note.
310
+ note_subject (str, "Note"): the subject of the note. the defualt value is just "Note"
311
+
312
+ Returns:
313
+ note: The note created.
314
+ """
315
+
316
+ note_model = Note(body=note_body, subject=note_subject)
317
+ response = self._client._post(f"/{self.endpoint}/{str(self.id)}/notes/", json=note_model.model_dump(exclude_unset=True, exclude_none=True))
318
+ if response.get('status_code') == 201:
319
+ location = response.get('headers').get('location')
320
+ parsed_url = urlparse(location)
321
+ path_segments = parsed_url.path.rstrip("/").split("/")
322
+ id = path_segments[-1]
323
+ return CRMClient(client=self._client).get_note(id)
324
+ else:
325
+ error_message = response.get('content')
326
+ raise CreateRequestError(f"/{self.endpoint}/{str(self.id)}/notes/", status_code=response.get('status_code'), error_message=error_message)
327
+
280
328
  class invoice(APIResource[Invoice]):
281
329
  endpoint = "sales/invoices/"
282
330
  Model = Invoice
@@ -310,4 +358,6 @@ class invoice(APIResource[Invoice]):
310
358
  """
311
359
  data = invoice_.model_dump(exclude_unset=True, exclude_none=True) if invoice_ else self.model_dump(exclude_unset=True, exclude_none=True)
312
360
  response = self._client._put(f"/{self.endpoint}/{str(self.id)}", json=data)
313
- return invoice.from_json(response, self._client)
361
+ return invoice.from_json(response, self._client)
362
+
363
+
@@ -0,0 +1,23 @@
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
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spyreapi
3
- Version: 0.0.2
3
+ Version: 0.0.4
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
@@ -42,3 +42,38 @@ A robust and extensible Python client for interacting with the [Spire Business S
42
42
 
43
43
  ```bash
44
44
  pip install -r requirements.txt
45
+ ```
46
+
47
+ ---
48
+
49
+ ## ⚙️ Configuration
50
+
51
+ - How to set up your spire client
52
+
53
+ ### Find your Spire URL
54
+
55
+ The base URL for the Spire API is the same url provided by Spire that you use to access Spire server and uses port 10880 as default:
56
+ Replace {spire-url} with the url provided by Spire.
57
+
58
+ - https://{spire-url}:10880/api/v2/
59
+
60
+ Spire Cloud
61
+ If you are using Spire cloud you do not need to specify a port. The base URL for API for Spire Cloud customers would be:
62
+
63
+ - https://{spire-cloud-url}/api/v2/
64
+
65
+ ### Set up your client with your credentials
66
+
67
+ ```python
68
+ from spyre import Spire
69
+ # host is your spire url and the port if applicable
70
+ client = Spire(host = 'your-spire-host', company = 'comapany-name' , username = 'username' , password = 'password' )
71
+
72
+ ```
73
+
74
+ ## Example : Updating the status of an inventory item
75
+ ```python
76
+ item = client.inventory.items.get_item(1101) # Gets item with id 1101
77
+ item.status = 1 # Use either item. or item.model. . item.model. will bring up all attributes
78
+ item.update()
79
+ ```
@@ -5,7 +5,7 @@ pyproject.toml
5
5
  src/spyre/Exceptions.py
6
6
  src/spyre/__init__.py
7
7
  src/spyre/client.py
8
- src/spyre/config.py
8
+ src/spyre/crm.py
9
9
  src/spyre/customers.py
10
10
  src/spyre/inventory.py
11
11
  src/spyre/sales.py
@@ -20,5 +20,4 @@ src/spyreapi.egg-info/PKG-INFO
20
20
  src/spyreapi.egg-info/SOURCES.txt
21
21
  src/spyreapi.egg-info/dependency_links.txt
22
22
  src/spyreapi.egg-info/requires.txt
23
- src/spyreapi.egg-info/top_level.txt
24
- tests/testing.py
23
+ src/spyreapi.egg-info/top_level.txt
@@ -1,3 +0,0 @@
1
- include README.md
2
- include LICENSE
3
- global-exclude *.pyc
spyreapi-0.0.2/README.md DELETED
@@ -1,22 +0,0 @@
1
- # Spire API Python Client
2
-
3
- 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.
4
-
5
- ---
6
-
7
- ## ✨ Features
8
-
9
- - ✅ Object-oriented resource wrappers for each module (e.g., `salesOrder`, `invoice`, `item`)
10
- - 🔍 Full-text search via `q` parameter
11
- - 🔁 Pagination with `start` and `limit` support
12
- - 🧾 JSON-based advanced filtering (supports `$gt`, `$lt`, `$in`, `$or`, etc.)
13
- - ↕️ Multi-field sorting with ascending/descending control
14
- - 🔧 Clean abstraction layer for API endpoints
15
- - 📦 Powered by `pydantic` models for validation
16
-
17
- ---
18
-
19
- ## 📦 Installation
20
-
21
- ```bash
22
- pip install -r requirements.txt
@@ -1,8 +0,0 @@
1
- import os
2
- from dotenv import load_dotenv
3
-
4
- load_dotenv()
5
-
6
- BASE_URL = os.getenv("BASE_URL")
7
- if not BASE_URL:
8
- raise ValueError("BASE_URL environment variable is not set. Please set it before running the application.")
@@ -1,14 +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, company : str, username : str, password : str):
8
- self.client = SpireClient(company, username, password)
9
- self.orders = OrdersClient(self.client)
10
- self.invoices = InvoiceClient(self.client)
11
- self.customers = CustomerClient(self.client)
12
- self.inventory = InventoryClient(self.client)
13
-
14
-
@@ -1,378 +0,0 @@
1
- import spyre
2
- import spyre.spire
3
-
4
- """
5
- Tested
6
- > Get Order
7
- > Pydantic Model
8
- > Creating Order
9
- > Deleting Order
10
- > Updating Order
11
- > Processing a order
12
- > Invoicing a order
13
- > Reversing a invoice(Check with eric)
14
- > Get Invoice
15
- > Update Invoice
16
- > Get Customer
17
- > Create Customer
18
- > Update Customer
19
- > Delete Customer
20
- > Querying
21
- > Filtering
22
- > Sorting
23
-
24
-
25
- Todo
26
-
27
-
28
- >Exceptions (Duplicate , Processing quotes, Invalid inPuts)
29
- >Documentation
30
- >Response handling/ object -> Replace Riase_for_status
31
- >Invoice Items/ Sales Order Item
32
- >Payments, Deposit on Sales Order
33
- >get by orderNo/invoiceNo etc
34
-
35
- # >Querying
36
- # >filtering and sorting
37
- # >Create returns the item created
38
- # >Customer
39
- # >Make Wrapper class and implemement Oob type classes & methods
40
- # >Create a model for Orders
41
- # >Test Create,Update,
42
- # >Processing Orders
43
- # >Cancel/Delete
44
- # >Return
45
- """
46
-
47
-
48
- spire = Spire('intertest' , 'david1' , 'david')
49
-
50
-
51
-
52
-
53
- order = spire.orders.get_sales_order(51247)
54
- order.model.location = "01"
55
- print(order.update())
56
-
57
- # order = spire.orders.get_sales_order(51245)
58
- # print(order.invoice())
59
-
60
- TAXES = {
61
- 0.05 : 1,
62
-
63
- 0.08 : 2,
64
- 0.13 : 3,
65
- 0.15 : 4,
66
- 0.14 : 5,
67
- 0.12 : 6,
68
- 0.11 : 7
69
- }
70
-
71
- order = {
72
- "amazon-order-id": "702-6212155-2707463",
73
- "merchant-order-id": "3045",
74
- "purchase-date": "5/31/2025 23:22",
75
- "last-updated-date": "6/2/2025 11:08",
76
- "order-status": "Shipped",
77
- "fulfillment-channel": "Merchant",
78
- "sales-channel": "Amazon.ca",
79
- "order-channel": "WebsiteOrderChannel",
80
- "url": "",
81
- "ship-service-level": "Standard",
82
- "product-name": "20V MAX 1/2IN Compact Impact WR- HR",
83
- "sku": "DEWDCF921B",
84
- "asin": "B09M3TL9BB",
85
- "number-of-items": 1,
86
- "item-status": "Shipped",
87
- "tax-collection-model": "",
88
- "tax-collection-responsible-party": "",
89
- "quantity": 1,
90
- "currency": "CAD",
91
- "item-price": 195,
92
- "item-tax": 21.45,
93
- "shipping-price": "",
94
- "shipping-tax": "",
95
- "gift-wrap-price": "",
96
- "gift-wrap-tax": "",
97
- "item-promotion-discount": "",
98
- "ship-promotion-discount": "",
99
- "address-type": "",
100
- "ship-city": "Maple Creek",
101
- "ship-state": "Saskatchewan",
102
- "ship-postal-code": "S0N 1N0",
103
- "ship-country": "CA",
104
- "promotion-ids": "",
105
- "is-business-order": False,
106
- "purchase-order-number": "",
107
- "price-designation": "",
108
- "fulfilled-by": "",
109
- "default-ship-from-address-name": "Interline Wholesale Hardware Distributors North",
110
- "default-ship-from-address-field-1": "399 Confederation Parkway",
111
- "default-ship-from-address-field-2": "",
112
- "default-ship-from-address-field-3": "",
113
- "default-ship-from-city": "Vaughan",
114
- "default-ship-from-state": "Ontario",
115
- "default-ship-from-country": "CA",
116
- "default-ship-from-postal-code": "L4K 4S1",
117
- "actual-ship-from-address-name": "Interline Wholesale Hardware Distributors North",
118
- "actual-ship-from-address-field-1": "399 Confederation Parkway",
119
- "actual-ship-from-address-field-2": "",
120
- "actual-ship-from-address-field-3": "",
121
- "actual-ship-from-city": "Vaughan",
122
- "actual-ship-from-state": "Ontario",
123
- "actual-ship-from-country": "CA",
124
- "actual-ship-from-postal-code": "L4K 4S1",
125
- "serial-numbers": ""
126
- }
127
-
128
- from datetime import datetime
129
-
130
- def convert_order_date(date_str: str) -> str:
131
- try:
132
- dt = datetime.strptime(date_str, "%m/%d/%Y %H:%M")
133
- return dt.strftime("%Y-%m-%d")
134
- except ValueError:
135
- return ""
136
-
137
-
138
- def get_tax_code(order: dict) -> int:
139
- try:
140
- item_price = float(order.get('item-price', 0))
141
- item_tax = float(order.get('item-tax', 0))
142
-
143
- if item_price == 0:
144
- raise ValueError("Item price cannot be zero.")
145
-
146
- tax_rate = item_tax / item_price
147
- print(tax_rate)
148
- # Find the closest tax rate key
149
- closest_rate = min(TAXES.keys(), key=lambda r: abs(r - tax_rate))
150
- print(closest_rate)
151
- return TAXES[closest_rate]
152
-
153
- except (TypeError, ValueError):
154
- return None # or raise an error if preferred
155
-
156
-
157
- def convert_order_to_sales_order(order: dict) -> SalesOrder:
158
-
159
- amazn_id = order.get('amazon-order-id')
160
- udf = {
161
- "shopid" : amazn_id,
162
- "shipped" : order.get('order-status') == 'shipped'
163
- }
164
-
165
- status = "O"
166
- # if order.get('order-status') == 'shipped':
167
- # status = "H"
168
-
169
- tax_code = get_tax_code(order)
170
-
171
- shipping_address = Address(city=order.get('ship-city'), provState= order.get('ship-state'), postalCode = order.get('ship-postal-code'), country=order.get('ship-country'),
172
- salesTaxes=[ { "code" : tax_code } ]
173
- )
174
-
175
- items = []
176
- item = SalesOrderItem(
177
- inventory=Inventory(partNo=order.get("sku"), whse='00'),
178
- partNo=order.get("sku"),
179
- orderQty=str(order.get("quantity")),
180
- unitPrice=str( float(order.get("item-price")) /float(order.get("quantity")) ),
181
- taxFlags= [True,True,True,True],
182
- committedQty=str(order.get("quantity")),
183
-
184
- )
185
-
186
- items.append(item)
187
- freight = str(
188
- float(order.get('shipping-tax', '0') or 0) -
189
- float(order.get('ship-promotion-discount', '0') or 0)
190
- )
191
-
192
- customer = Customer(customerNo="AMAZON")
193
-
194
- return SalesOrder(
195
- orderNo= "AMZN3" + amazn_id[-5:] ,
196
- orderDate=convert_order_date(order.get("purchase-date")),
197
- type = "O",
198
- referenceNo= amazn_id,
199
- status=status,
200
- currency=Currency(code=order.get("currency")) if order.get("currency") else None,
201
- shippingAddress=shipping_address,
202
- items=items,
203
- udf=udf,
204
- freight=freight,
205
- customer=customer,
206
-
207
- )
208
-
209
- sales_order = convert_order_to_sales_order(order)
210
- print(sales_order.model_dump_json(indent=2 , exclude_none= True))
211
-
212
-
213
-
214
- # order.model.payments = [ {"method" : "06" , "amount" : "216.45" , "layawayFlag" : False}]
215
- # order.update()
216
- # print(order.invoice())
217
-
218
-
219
-
220
- # uoms = spire.inventory.items.get_item_uoms(id = 811)
221
- # test_uom = uoms[2]
222
- # print(test_uom.description)
223
-
224
-
225
- # inv = spire.invoices.get_invoice(206363)
226
- # order_converted.
227
- # order_converted = create_sales_order_from_invoice(inv.model)
228
- # print(order_converted.model_dump_json(exclude_none=True, exclude_unset=True, indent=2))
229
-
230
-
231
- # cust = spire.customers.get_customer(1)
232
-
233
- # ord = spire.orders.get_sales_order(31319)
234
- # ord.customer = cust.model
235
-
236
- # print(ord.update())
237
-
238
-
239
- # order_test = json.loads("""
240
-
241
- # {
242
- # "customer": {
243
- # "id" : 3143
244
- # },
245
- # "currency": {
246
- # "code": "CAD"
247
-
248
- # },
249
- # "address": {
250
- # "country": "Can",
251
- # "defaultWarehouse": "00",
252
- # "line1": "629 Daintry Crescent",
253
- # "line2": "Cobourg ON K9A 4X9",
254
- # "email": "beatty731@yahoo.com",
255
- # "contacts": [
256
- # {
257
- # "phone": {
258
- # "number": "+1 905-207-1116"
259
- # }
260
- # }
261
- # ]
262
- # },
263
- # "contact": {
264
- # "phone": {
265
- # "number": "+1 905-207-1116"
266
- # },
267
- # "name": "safdasd"
268
- # },
269
- # "shippingCarrier": "1",
270
- # "referenceNo": "1",
271
- # "trackingNo": "1",
272
- # "shipDate": null,
273
- # "items": [
274
- # {
275
- # "inventory": {
276
- # "id": 12345,
277
- # "whse": "00"
278
- # }
279
- # }
280
- # ],
281
- # "freight": "1",
282
- # "discount": "1",
283
- # "surcharge": "1",
284
- # "status": "O",
285
- # "type": "Q",
286
- # "hold": false,
287
- # "customerPO": "1"
288
- # }
289
-
290
- # """)
291
-
292
-
293
-
294
- # order = SalesOrder(**order_test)
295
- # print(order.model_dump(exclude_unset=True, ))
296
-
297
-
298
-
299
- # spire.orders.get_sales_order
300
- # invoice_test = spire.invoices.get_invoice(206362)
301
- # response = invoice_test.reverse()
302
- # print(response)
303
-
304
- # order = spire.orders.get_sales_order(31312)
305
- # response = order.invoice()
306
-
307
- # response = order.update_sales_order
308
-
309
- # order = spire.orders.get_sales_order(31307)
310
- # response = order.delete()
311
- # print(response)
312
-
313
- # resp = spire.orders.delete_sales_order(31306)
314
- # print(resp)
315
-
316
-
317
-
318
- # response = spire.orders.create_sales_order(order_test)
319
- # print(response)
320
-
321
-
322
-
323
- # orders_client = OrdersClient(client)
324
- # order = orders_client.get_order(12334)
325
- # order.invoice()
326
-
327
-
328
- # invoice_client = InvoiceClient(client)
329
-
330
- # response = invoice_client.reverse_invoice(206354)
331
- # print(response)
332
-
333
- # invoice_em = invoice_client.get_invoice(206355)
334
- # invoice_model = Invoice(**invoice_em)
335
-
336
- # print(invoice_model.model_dump_json(indent=2))
337
-
338
- # order_converted = create_sales_order_from_invoice(invoice_model)
339
- # print(order_converted.model_dump_json(indent=2, exclude_unset=True, exclude_none=True))
340
-
341
- # response = orders_client.create_order(order_converted.model_dump(exclude_unset=True, exclude_none=True))
342
- # print(response)
343
-
344
-
345
- # Get an order by ID
346
- # order_1001 = orders_client.get_order(1001)
347
- # try:
348
- # orderModel = SalesOrder(**order_1001)
349
- # except Exception as e:
350
- # print(e.errors())
351
-
352
- # print(order)
353
- # print(orderModel.model_dump_json(indent=2))
354
-
355
- #Create an ORder
356
-
357
-
358
-
359
- # response = orders_client.create_order(order_test)
360
- # print(response)
361
-
362
- # deleting An order
363
- # response = orders_client.delete_order(31299)
364
- # print(response)
365
-
366
- # Updating an order
367
- # orderModel.location = "01"
368
- # print(orderModel.model_dump_json(indent=2))
369
-
370
- # response = orders_client.update_order(1001, orderModel.model_dump_json())
371
- # print(response)
372
-
373
-
374
- # order_test_model = SalesOrder(**order_test)
375
- # print(order_test_model.model_dump(exclude_none=True))
376
-
377
-
378
-
File without changes
File without changes
File without changes
File without changes