spyreapi 0.0.1__py3-none-any.whl
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.
- spyre/Exceptions.py +45 -0
- spyre/Models/__init__.py +0 -0
- spyre/Models/customers_models.py +40 -0
- spyre/Models/inventory_models.py +160 -0
- spyre/Models/sales_models.py +170 -0
- spyre/Models/shared_models.py +96 -0
- spyre/__init__.py +22 -0
- spyre/client.py +208 -0
- spyre/config.py +6 -0
- spyre/customers.py +157 -0
- spyre/inventory.py +344 -0
- spyre/sales.py +313 -0
- spyre/spire.py +14 -0
- spyre/utils.py +130 -0
- spyreapi-0.0.1.dist-info/METADATA +37 -0
- spyreapi-0.0.1.dist-info/RECORD +19 -0
- spyreapi-0.0.1.dist-info/WHEEL +5 -0
- spyreapi-0.0.1.dist-info/licenses/LICENSE +21 -0
- spyreapi-0.0.1.dist-info/top_level.txt +1 -0
spyre/client.py
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from config import BASE_URL
|
|
3
|
+
from typing import TypeVar, Optional, Type, Generic, List, Union, Tuple, Dict, Any
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
import json
|
|
6
|
+
import urllib.parse
|
|
7
|
+
|
|
8
|
+
T = TypeVar('T', bound=BaseModel)
|
|
9
|
+
|
|
10
|
+
class SpireClient():
|
|
11
|
+
|
|
12
|
+
def __init__(self, company, username, password,):
|
|
13
|
+
self.session = requests.Session()
|
|
14
|
+
self.session.auth = (username, password)
|
|
15
|
+
self.session.headers.update({
|
|
16
|
+
"accept": "application/json",
|
|
17
|
+
"content-type": "application/json"
|
|
18
|
+
})
|
|
19
|
+
self.base_url = f"{BASE_URL}/{company}"
|
|
20
|
+
|
|
21
|
+
def _get(self, endpoint, params=None):
|
|
22
|
+
|
|
23
|
+
"""
|
|
24
|
+
Send a GET request to the Spire API.
|
|
25
|
+
|
|
26
|
+
This method constructs the full URL using the provided endpoint,
|
|
27
|
+
sends an HTTP GET request with optional query parameters, and returns
|
|
28
|
+
the parsed JSON response. Raises an HTTPError if the response contains
|
|
29
|
+
an unsuccessful status code.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
endpoint (str): The relative API endpoint (e.g., 'inventory/items/123').
|
|
33
|
+
params (dict, optional): A dictionary of query parameters to include
|
|
34
|
+
in the request (e.g., {'status': 'active'}). Defaults to None.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
dict: The JSON-decoded response from the API.
|
|
38
|
+
|
|
39
|
+
Raises:
|
|
40
|
+
requests.exceptions.HTTPError: If the response contains an HTTP error status.
|
|
41
|
+
"""
|
|
42
|
+
url = f"{self.base_url}/{endpoint.lstrip('/')}"
|
|
43
|
+
response = self.session.get(url , params=params)
|
|
44
|
+
response.raise_for_status()
|
|
45
|
+
return response.json()
|
|
46
|
+
|
|
47
|
+
def _post(self, endpoint, data=None, json=None):
|
|
48
|
+
url = f"{self.base_url}/{endpoint.lstrip('/')}"
|
|
49
|
+
response = self.session.post(url, data=data, json=json)
|
|
50
|
+
return self._handle_response(response)
|
|
51
|
+
|
|
52
|
+
def _put(self, endpoint, data=None, json=None):
|
|
53
|
+
url = f"{self.base_url}/{endpoint.lstrip('/')}"
|
|
54
|
+
response = self.session.put(url, data=data, json=json)
|
|
55
|
+
response.raise_for_status()
|
|
56
|
+
return response.json()
|
|
57
|
+
|
|
58
|
+
def _delete(self, endpoint):
|
|
59
|
+
url = f"{self.base_url}/{endpoint.lstrip('/')}"
|
|
60
|
+
response = self.session.delete(url)
|
|
61
|
+
return response.status_code == 200 or response.status_code == 204 or response.status_code == 202
|
|
62
|
+
|
|
63
|
+
def _handle_response(self, response):
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
content = response.json()
|
|
67
|
+
except ValueError:
|
|
68
|
+
content = response.text
|
|
69
|
+
return{
|
|
70
|
+
"status_code": response.status_code,
|
|
71
|
+
"url": response.url,
|
|
72
|
+
"content": content,
|
|
73
|
+
"headers" : response.headers
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
def _query(
|
|
77
|
+
self,
|
|
78
|
+
endpoint: str,
|
|
79
|
+
resource_cls: Type["APIResource[T]"],
|
|
80
|
+
*,
|
|
81
|
+
all: bool = False,
|
|
82
|
+
limit: int = 1000,
|
|
83
|
+
start: int = 0,
|
|
84
|
+
query: Optional[str] = None,
|
|
85
|
+
filter: Optional[Dict[str, Any]] = None,
|
|
86
|
+
sort: Optional[Dict[str, str]] = None,
|
|
87
|
+
**extra_params
|
|
88
|
+
) -> List["APIResource[T]"]:
|
|
89
|
+
"""
|
|
90
|
+
Query a list of resources from a Spire API endpoint with support for
|
|
91
|
+
pagination, searching, filtering, and multi-level sorting.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
endpoint (str): The API endpoint (e.g., 'sales/orders').
|
|
95
|
+
resource_cls (Type[APIResource[T]]): The resource wrapper class (e.g., SalesOrderResource).
|
|
96
|
+
all (bool, optional): If True, fetches all available pages of results.
|
|
97
|
+
limit (int, optional): Number of results per page (max 1000). Default is 1000.
|
|
98
|
+
start (int, optional): Starting offset for pagination. Default is 0.
|
|
99
|
+
q (str, optional): Free-text search query.
|
|
100
|
+
filter (dict, optional): Dictionary of filter criteria, which will be JSON-encoded.
|
|
101
|
+
sort (dict, optional): Dictionary of sorting rules (e.g., {"orderDate": "desc", "orderNo": "asc"}).
|
|
102
|
+
**extra_params: Any additional query parameters to pass to the API.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
List[APIResource[T]]: A list of wrapped resource instances.
|
|
106
|
+
"""
|
|
107
|
+
collected = []
|
|
108
|
+
current_start = start
|
|
109
|
+
|
|
110
|
+
while True:
|
|
111
|
+
# Build the query params as a list of tuples to allow repeated keys like 'sort'
|
|
112
|
+
params: List[Tuple[str, Any]] = [
|
|
113
|
+
("start", current_start),
|
|
114
|
+
("limit", min(limit, 1000))
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
if query:
|
|
118
|
+
params.append(("q", query))
|
|
119
|
+
|
|
120
|
+
if filter:
|
|
121
|
+
encoded_filter = json.dumps(filter)
|
|
122
|
+
params.append(("filter", encoded_filter))
|
|
123
|
+
|
|
124
|
+
if sort:
|
|
125
|
+
for field, direction in sort.items():
|
|
126
|
+
prefix = "-" if direction.lower() == "desc" else ""
|
|
127
|
+
params.append(("sort", f"{prefix}{field}"))
|
|
128
|
+
|
|
129
|
+
# Add any additional custom parameters
|
|
130
|
+
for k, v in extra_params.items():
|
|
131
|
+
params.append((k, v))
|
|
132
|
+
|
|
133
|
+
response = self._get(endpoint.rstrip("/"), params=params)
|
|
134
|
+
items = response.get("records", [])
|
|
135
|
+
count = response.get("count", 0)
|
|
136
|
+
|
|
137
|
+
for item in items:
|
|
138
|
+
collected.append(resource_cls.from_json(item, self))
|
|
139
|
+
|
|
140
|
+
if not all or (current_start + limit >= count):
|
|
141
|
+
break
|
|
142
|
+
|
|
143
|
+
current_start += limit
|
|
144
|
+
|
|
145
|
+
return collected
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class APIResource(Generic[T]):
|
|
149
|
+
"""Active-record-like wrapper that knows a *session* and a *model*."""
|
|
150
|
+
_model: T
|
|
151
|
+
_client: SpireClient
|
|
152
|
+
Model: type
|
|
153
|
+
endpoint: str
|
|
154
|
+
|
|
155
|
+
def __init__(self, model: T, client: SpireClient, **kwargs):
|
|
156
|
+
object.__setattr__(self, "_model", model)
|
|
157
|
+
object.__setattr__(self, "_client", client)
|
|
158
|
+
|
|
159
|
+
# Let child classes handle extra kwargs
|
|
160
|
+
for k, v in kwargs.items():
|
|
161
|
+
setattr(self, k, v)
|
|
162
|
+
|
|
163
|
+
def __getattr__(self, item):
|
|
164
|
+
""" delegate unknown attributes to the Pydantic model.
|
|
165
|
+
if you try to access an attribute on your wrapper class that doesn't exist in that class.
|
|
166
|
+
It will automatically forward the request to self._model """
|
|
167
|
+
|
|
168
|
+
return getattr(self._model, item)
|
|
169
|
+
|
|
170
|
+
def __setattr__(self, key, value):
|
|
171
|
+
"""
|
|
172
|
+
Delegate attribute setting to the Pydantic model unless the attribute
|
|
173
|
+
starts with an underscore (which indicates internal fields like _model or _client).
|
|
174
|
+
"""
|
|
175
|
+
if key.startswith("_"):
|
|
176
|
+
object.__setattr__(self, key, value)
|
|
177
|
+
else:
|
|
178
|
+
setattr(self._model, key, value)
|
|
179
|
+
|
|
180
|
+
def __str__(self):
|
|
181
|
+
return self._model.model_dump_json(indent=2)
|
|
182
|
+
|
|
183
|
+
@property
|
|
184
|
+
def model(self) -> T:
|
|
185
|
+
"""
|
|
186
|
+
Accessor for the underlying Pydantic model.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
T: The internal Pydantic model instance.
|
|
190
|
+
"""
|
|
191
|
+
return self._model
|
|
192
|
+
|
|
193
|
+
@classmethod
|
|
194
|
+
def from_json(cls, json_data: dict, client: SpireClient, **kwargs) -> "APIResource":
|
|
195
|
+
model_instance = cls.Model(**json_data)
|
|
196
|
+
return cls(model_instance, client, **kwargs)
|
|
197
|
+
|
|
198
|
+
def refresh(self):
|
|
199
|
+
updated = self._client._get(f"{self.endpoint}/{self.id}")
|
|
200
|
+
self._model = self.Model(**updated)
|
|
201
|
+
return self
|
|
202
|
+
|
|
203
|
+
def to_dict(self):
|
|
204
|
+
return self._model.model_dump(exclude_unset=True, exclude_none=True)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
|
spyre/config.py
ADDED
spyre/customers.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
from client import SpireClient
|
|
2
|
+
from Models.customers_models import Customer
|
|
3
|
+
from client import APIResource
|
|
4
|
+
from Exceptions import *
|
|
5
|
+
from urllib.parse import urlparse
|
|
6
|
+
from typing import Optional, Dict, Any, List
|
|
7
|
+
|
|
8
|
+
class CustomerClient():
|
|
9
|
+
|
|
10
|
+
def __init__(self, client : SpireClient):
|
|
11
|
+
self.client = client
|
|
12
|
+
self.endpoint = "customers"
|
|
13
|
+
|
|
14
|
+
def get_customer(self, id: int) -> "customer":
|
|
15
|
+
"""
|
|
16
|
+
Retrieve a customer by ID.
|
|
17
|
+
|
|
18
|
+
Sends a GET request to the customer endpoint to fetch details of a specific customer.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
id (int): The ID of the customer to retrieve.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
Customer: A Customer object populated with the retrieved data.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
response = self.client._get(f"{self.endpoint}/{str(id)}")
|
|
28
|
+
return customer.from_json(json_data=response, client=self.client)
|
|
29
|
+
|
|
30
|
+
def create_customer(self, customer : 'Customer') -> "customer":
|
|
31
|
+
"""
|
|
32
|
+
Create a new customer.
|
|
33
|
+
|
|
34
|
+
Sends a POST request to the customer endpoint with the customer data
|
|
35
|
+
to create a new customer record.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
customer (Customer): The Customer object containing the data to be created.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Customer: The newly created Customer object returned by the API.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
response = self.client._post(f"/{self.endpoint}", json=customer.model_dump(exclude_unset=True, exclude_none=True))
|
|
45
|
+
if response.get('status_code') == 201:
|
|
46
|
+
location = response.get('headers').get('location')
|
|
47
|
+
parsed_url = urlparse(location)
|
|
48
|
+
path_segments = parsed_url.path.rstrip("/").split("/")
|
|
49
|
+
id = path_segments[-1]
|
|
50
|
+
return self.get_customer(id)
|
|
51
|
+
else:
|
|
52
|
+
error_message = response.get('content')
|
|
53
|
+
raise CreateRequestError(self.endpoint, status_code=response.get('status_code'), error_message=error_message)
|
|
54
|
+
|
|
55
|
+
def update_customer(self, id : int, customer : 'Customer') -> "customer":
|
|
56
|
+
"""
|
|
57
|
+
Update an existing customer by ID.
|
|
58
|
+
|
|
59
|
+
Sends a PUT request to update a customer record using the provided customer data.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
id (int): The ID of the customer to update.
|
|
63
|
+
customer (Customer): A Pydantic model representing the updated customer data.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
customer: A new Customer instance built from the updated response data.
|
|
67
|
+
"""
|
|
68
|
+
response = self.client._put(f"/{self.endpoint}/{str(id)}", json=customer.model_dump(exclude_none=True, exclude_unset=True))
|
|
69
|
+
return customer.from_json(response, self.client)
|
|
70
|
+
|
|
71
|
+
def delete_customer(self, id : int) -> bool:
|
|
72
|
+
"""
|
|
73
|
+
Delete a customer by ID.
|
|
74
|
+
|
|
75
|
+
Sends a DELETE request to the customer endpoint. Returns True if deletion was successful (HTTP 200/204),
|
|
76
|
+
otherwise returns False.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
id (int): The ID of the customer to delete.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
bool: True if the customer was successfully deleted, False otherwise.
|
|
83
|
+
"""
|
|
84
|
+
return self.client._delete(f"/{self.endpoint}/{str(id)}")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def query_invoices(
|
|
88
|
+
self,
|
|
89
|
+
*,
|
|
90
|
+
query: Optional[str] = None,
|
|
91
|
+
sort: Optional[Dict[str, str]] = None,
|
|
92
|
+
filter: Optional[Dict[str, Any]] = None,
|
|
93
|
+
all: bool = False,
|
|
94
|
+
limit: int = 1000,
|
|
95
|
+
start: int = 0,
|
|
96
|
+
**extra_params
|
|
97
|
+
) -> List["customer"]:
|
|
98
|
+
"""
|
|
99
|
+
Query customer with optional full-text search, filtering, multi-field sorting, and pagination.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
q (str, optional): Full-text search string.
|
|
103
|
+
sort (dict, optional): Dictionary of sorting rules (e.g., {"orderDate": "desc", "orderNo": "asc"}).
|
|
104
|
+
filter (dict, optional): Dictionary of filters to apply (will be JSON-encoded and URL-safe).
|
|
105
|
+
all (bool, optional): If True, retrieves all pages of results.
|
|
106
|
+
limit (int, optional): Number of results per page (max 1000).
|
|
107
|
+
start (int, optional): Starting offset for pagination.
|
|
108
|
+
**extra_params: Any additional parameters to include in the query.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
List[customer]: List of wrapped customer resources.
|
|
112
|
+
"""
|
|
113
|
+
return self.client._query(
|
|
114
|
+
endpoint=self.endpoint,
|
|
115
|
+
resource_cls=customer,
|
|
116
|
+
query=query,
|
|
117
|
+
sort=sort,
|
|
118
|
+
filter=filter,
|
|
119
|
+
all=all,
|
|
120
|
+
limit=limit,
|
|
121
|
+
start=start,
|
|
122
|
+
**extra_params
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class customer(APIResource[Customer]):
|
|
127
|
+
endpoint = "customers"
|
|
128
|
+
Model = Customer
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def delete(self):
|
|
132
|
+
"""
|
|
133
|
+
Deletes the Customer from Spire.
|
|
134
|
+
|
|
135
|
+
Sends a DELETE request to the API to remove the customer with the current ID.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
bool: True if the order was successfully deleted (HTTP 204 or 200), False otherwise.
|
|
139
|
+
"""
|
|
140
|
+
return self._client._delete(f"/{self.endpoint}/{str(self.id)}")
|
|
141
|
+
|
|
142
|
+
def update(self, customer: "customer" = None) -> 'customer':
|
|
143
|
+
"""
|
|
144
|
+
Update the customer.
|
|
145
|
+
|
|
146
|
+
If no order object is provided, updates the current instance on the server.
|
|
147
|
+
If an order object is provided, updates the customer using the given data.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
customer (customer, optional): An optional customer instance to use for the update.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
customer: The updated customer object reflecting the new status.
|
|
154
|
+
"""
|
|
155
|
+
data = customer.model_dump(exclude_unset=True, exclude_none=True) if customer else self.model_dump(exclude_unset=True, exclude_none=True)
|
|
156
|
+
response = self._client._put(f"/{self.endpoint}/{str(self.id)}", json=data)
|
|
157
|
+
return customer.from_json(response, self._client)
|