shippingboapy 0.1.0__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.
- shippingboapy/__init__.py +0 -0
- shippingboapy/auth.py +117 -0
- shippingboapy/client.py +222 -0
- shippingboapy/config.py +15 -0
- shippingboapy/exceptions.py +54 -0
- shippingboapy/models/__init__.py +0 -0
- shippingboapy/models/address.py +101 -0
- shippingboapy/models/address_label.py +14 -0
- shippingboapy/models/address_label_config.py +113 -0
- shippingboapy/models/address_label_summaries.py +77 -0
- shippingboapy/models/carrier.py +14 -0
- shippingboapy/models/client_order_origin.py +24 -0
- shippingboapy/models/client_order_source.py +24 -0
- shippingboapy/models/dangerous_good_product_information.py +48 -0
- shippingboapy/models/filter.py +31 -0
- shippingboapy/models/kit_component.py +32 -0
- shippingboapy/models/logistician_service_config.py +15 -0
- shippingboapy/models/order.py +339 -0
- shippingboapy/models/order_dispatch.py +17 -0
- shippingboapy/models/order_document.py +28 -0
- shippingboapy/models/order_event.py +31 -0
- shippingboapy/models/order_item_product_mapping.py +26 -0
- shippingboapy/models/pack_component.py +22 -0
- shippingboapy/models/package.py +26 -0
- shippingboapy/models/product.py +283 -0
- shippingboapy/models/tag.py +23 -0
- shippingboapy/models/types.py +19 -0
- shippingboapy/py.typed +0 -0
- shippingboapy/resources/__init__.py +0 -0
- shippingboapy/resources/address.py +124 -0
- shippingboapy/resources/address_label.py +45 -0
- shippingboapy/resources/address_label_config.py +67 -0
- shippingboapy/resources/address_label_summaries.py +29 -0
- shippingboapy/resources/background_job.py +30 -0
- shippingboapy/resources/carrier.py +30 -0
- shippingboapy/resources/client_order_origin.py +30 -0
- shippingboapy/resources/client_order_source.py +30 -0
- shippingboapy/resources/dangerous_good_product_informations.py +118 -0
- shippingboapy/resources/kit_component.py +59 -0
- shippingboapy/resources/logistician_service_config.py +30 -0
- shippingboapy/resources/order.py +211 -0
- shippingboapy/resources/order_dispatch.py +27 -0
- shippingboapy/resources/order_document.py +62 -0
- shippingboapy/resources/order_event.py +45 -0
- shippingboapy/resources/order_item_product_mapping.py +90 -0
- shippingboapy/resources/order_tag.py +62 -0
- shippingboapy/resources/pack_component.py +79 -0
- shippingboapy/resources/package.py +27 -0
- shippingboapy/resources/product.py +173 -0
- shippingboapy-0.1.0.dist-info/METADATA +292 -0
- shippingboapy-0.1.0.dist-info/RECORD +54 -0
- shippingboapy-0.1.0.dist-info/WHEEL +5 -0
- shippingboapy-0.1.0.dist-info/licenses/LICENSE +21 -0
- shippingboapy-0.1.0.dist-info/top_level.txt +1 -0
|
File without changes
|
shippingboapy/auth.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from shippingboapy.config import ShippingBoConfig
|
|
3
|
+
import httpx
|
|
4
|
+
import time
|
|
5
|
+
from shippingboapy.exceptions import BadRequestError, UnauthorizedError, AuthenticationError, TokenRefreshError
|
|
6
|
+
from typing import Callable, Any, Awaitable
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class TokenData:
|
|
10
|
+
access_token: str
|
|
11
|
+
token_type: str
|
|
12
|
+
expires_in: int
|
|
13
|
+
refresh_token: str
|
|
14
|
+
scope: str
|
|
15
|
+
created_at: int | None
|
|
16
|
+
|
|
17
|
+
def is_token_expired(token_data: TokenData) -> bool:
|
|
18
|
+
"""Check if the token has expired."""
|
|
19
|
+
if token_data.created_at is None:
|
|
20
|
+
return True
|
|
21
|
+
current_time = int(time.time())
|
|
22
|
+
return current_time >= token_data.created_at + token_data.expires_in
|
|
23
|
+
|
|
24
|
+
async def get_token(auth_code: str, session: httpx.AsyncClient, config: ShippingBoConfig, headers: dict[str, Any] | None = None) -> TokenData | None:
|
|
25
|
+
"""
|
|
26
|
+
Get a valid access token, refreshing it if necessary.
|
|
27
|
+
Args:
|
|
28
|
+
auth_code (str): The authorization code to use for obtaining the access token.
|
|
29
|
+
session (httpx.AsyncClient): The HTTP client session to use for making the request.
|
|
30
|
+
config (ShippingBoConfig): The configuration object containing necessary parameters.
|
|
31
|
+
headers (dict[str, Any] | None): Optional headers to include in the request.
|
|
32
|
+
Returns:
|
|
33
|
+
TokenData | None: The token data if the request is successful, otherwise None.
|
|
34
|
+
Raises:
|
|
35
|
+
BadRequestError: If the request is malformed or contains invalid parameters.
|
|
36
|
+
UnauthorizedError: If the request is unauthorized.
|
|
37
|
+
AuthenticationError: If there is an error obtaining the token.
|
|
38
|
+
"""
|
|
39
|
+
payload = {
|
|
40
|
+
'grant_type': 'authorization_code',
|
|
41
|
+
'code': auth_code,
|
|
42
|
+
'client_id': config.client_id,
|
|
43
|
+
'client_secret' : config.client_secret,
|
|
44
|
+
'redirect_uri': config.redirect_uri
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
response = await session.post(config.auth_url, json=payload, headers=headers)
|
|
48
|
+
|
|
49
|
+
if response.status_code == 200:
|
|
50
|
+
token_data = response.json()
|
|
51
|
+
|
|
52
|
+
token = TokenData(
|
|
53
|
+
access_token=token_data['access_token'],
|
|
54
|
+
token_type=token_data['token_type'],
|
|
55
|
+
expires_in=token_data['expires_in'],
|
|
56
|
+
refresh_token=token_data['refresh_token'],
|
|
57
|
+
scope=token_data['scope'],
|
|
58
|
+
created_at= token_data.get('created_at', int(time.time()))
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
return token
|
|
62
|
+
else:
|
|
63
|
+
if response.status_code == 400:
|
|
64
|
+
raise BadRequestError(status_code=response.status_code, message=f"Bad request: {response.text}")
|
|
65
|
+
elif response.status_code == 401:
|
|
66
|
+
raise UnauthorizedError(status_code=response.status_code, message=f"Unauthorized access: {response.text}")
|
|
67
|
+
else:
|
|
68
|
+
raise AuthenticationError(f"Failed to get token: {response.status_code} - {response.text}")
|
|
69
|
+
|
|
70
|
+
async def refresh_token(refresh_token: str,
|
|
71
|
+
session: httpx.AsyncClient,
|
|
72
|
+
config: ShippingBoConfig,
|
|
73
|
+
headers: dict[str, Any] | None = None,
|
|
74
|
+
on_refresh: Callable[[TokenData], Awaitable[None]] | None = None) -> TokenData:
|
|
75
|
+
"""
|
|
76
|
+
Refresh the access token using the refresh token.
|
|
77
|
+
Args:
|
|
78
|
+
refresh_token (str): The refresh token to use for refreshing the access token.
|
|
79
|
+
session (httpx.AsyncClient): The HTTP client session to use for making the request.
|
|
80
|
+
config (ShippingBoConfig): The configuration object containing necessary parameters.
|
|
81
|
+
headers (dict[str, Any] | None): Optional headers to include in the request.
|
|
82
|
+
on_refresh (Callable[[TokenData], Awaitable[None]] | None): Optional callback function to be called after a successful token refresh. The function should accept a TokenData object as its argument.
|
|
83
|
+
Returns:
|
|
84
|
+
TokenData: The new token data if the refresh is successful.
|
|
85
|
+
Raises:
|
|
86
|
+
TokenRefreshError: If there is an error refreshing the token.
|
|
87
|
+
"""
|
|
88
|
+
payload = {
|
|
89
|
+
'grant_type': 'refresh_token',
|
|
90
|
+
'refresh_token': refresh_token,
|
|
91
|
+
'client_id': config.client_id,
|
|
92
|
+
'client_secret': config.client_secret,
|
|
93
|
+
'redirect_uri': config.redirect_uri
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
response = await session.post(config.auth_url, json=payload, headers=headers)
|
|
97
|
+
if response.status_code == 200:
|
|
98
|
+
token_data = response.json()
|
|
99
|
+
if on_refresh is not None and callable(on_refresh):
|
|
100
|
+
await on_refresh(TokenData(
|
|
101
|
+
access_token=token_data['access_token'],
|
|
102
|
+
token_type=token_data['token_type'],
|
|
103
|
+
expires_in=token_data['expires_in'],
|
|
104
|
+
refresh_token=token_data['refresh_token'],
|
|
105
|
+
scope=token_data['scope'],
|
|
106
|
+
created_at=int(time.time())
|
|
107
|
+
))
|
|
108
|
+
return TokenData(
|
|
109
|
+
access_token=token_data['access_token'],
|
|
110
|
+
token_type=token_data['token_type'],
|
|
111
|
+
expires_in=token_data['expires_in'],
|
|
112
|
+
refresh_token=token_data['refresh_token'],
|
|
113
|
+
scope=token_data['scope'],
|
|
114
|
+
created_at=int(time.time())
|
|
115
|
+
)
|
|
116
|
+
else:
|
|
117
|
+
raise TokenRefreshError(f"Failed to refresh token: {response.status_code} - {response.text}")
|
shippingboapy/client.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
from shippingboapy.config import ShippingBoConfig
|
|
3
|
+
from shippingboapy.auth import TokenData, get_token, refresh_token
|
|
4
|
+
import asyncio
|
|
5
|
+
from shippingboapy.exceptions import BadRequestError, AuthenticationError, TokenRefreshError, UnexpectedError, ForbiddenError, NotFoundError, ServerError
|
|
6
|
+
from shippingboapy.resources.product import ProductResource
|
|
7
|
+
from shippingboapy.resources.order import OrderResource
|
|
8
|
+
from shippingboapy.resources.order_tag import OrderTagResource
|
|
9
|
+
from shippingboapy.resources.pack_component import PackComponentResource
|
|
10
|
+
from shippingboapy.resources.package import PackageResource
|
|
11
|
+
from shippingboapy.resources.order_document import OrderDocumentResource
|
|
12
|
+
from shippingboapy.resources.address_label import AddressLabelResource
|
|
13
|
+
from shippingboapy.resources.order_dispatch import OrderDispatchResource
|
|
14
|
+
from shippingboapy.resources.order_event import OrderEventResource
|
|
15
|
+
from shippingboapy.resources.address_label_summaries import AddressLabelSummariesResource
|
|
16
|
+
from shippingboapy.resources.address_label_config import AddressLabelConfigResource
|
|
17
|
+
from shippingboapy.resources.address import AddressResource
|
|
18
|
+
from shippingboapy.resources.background_job import BackgroundJobResource
|
|
19
|
+
from shippingboapy.resources.carrier import CarrierResource
|
|
20
|
+
from shippingboapy.resources.client_order_origin import ClientOrderOriginResource
|
|
21
|
+
from shippingboapy.resources.client_order_source import ClientOrderSourceResource
|
|
22
|
+
from shippingboapy.resources.dangerous_good_product_informations import DangerousGoodProductInformationResource
|
|
23
|
+
from shippingboapy.resources.kit_component import KitComponentResource
|
|
24
|
+
from shippingboapy.resources.logistician_service_config import LogisticianServiceConfigResource
|
|
25
|
+
from shippingboapy.resources.order_item_product_mapping import OrderItemProductMappingResource
|
|
26
|
+
|
|
27
|
+
from typing import Callable, Awaitable, Any, Protocol
|
|
28
|
+
|
|
29
|
+
class OnTokenRefreshCallback(Protocol):
|
|
30
|
+
async def __call__(self, token_data: TokenData) -> None: ...
|
|
31
|
+
|
|
32
|
+
class RetryableRequest(Protocol):
|
|
33
|
+
async def __call__(self, _retry: int = 0, **kwargs: Any) -> httpx.Response: ...
|
|
34
|
+
|
|
35
|
+
class Client:
|
|
36
|
+
def __init__(self, access_token: str = "",
|
|
37
|
+
refresh_token: str = "",
|
|
38
|
+
app_id: str = "",
|
|
39
|
+
api_version: str = "",
|
|
40
|
+
client_id: str = "",
|
|
41
|
+
client_secret: str = "",
|
|
42
|
+
on_token_refresh: OnTokenRefreshCallback | None = None):
|
|
43
|
+
|
|
44
|
+
if not app_id:
|
|
45
|
+
raise AuthenticationError("app_id is required")
|
|
46
|
+
if not api_version:
|
|
47
|
+
raise AuthenticationError("api_version is required")
|
|
48
|
+
if not client_id:
|
|
49
|
+
raise AuthenticationError("client_id is required")
|
|
50
|
+
if not client_secret:
|
|
51
|
+
raise AuthenticationError("client_secret is required")
|
|
52
|
+
|
|
53
|
+
self.on_token_refresh: Callable[[TokenData], Awaitable[None]] | None = on_token_refresh
|
|
54
|
+
|
|
55
|
+
self.config = ShippingBoConfig(
|
|
56
|
+
app_id=app_id,
|
|
57
|
+
api_version=api_version,
|
|
58
|
+
client_id=client_id,
|
|
59
|
+
client_secret=client_secret
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
self.token: TokenData | None = TokenData(
|
|
63
|
+
access_token=str(access_token),
|
|
64
|
+
refresh_token=str(refresh_token),
|
|
65
|
+
token_type= "Bearer",
|
|
66
|
+
expires_in=3600,
|
|
67
|
+
scope="",
|
|
68
|
+
created_at = None
|
|
69
|
+
)
|
|
70
|
+
self.session = httpx.AsyncClient(timeout=self.config.timeout)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
self.products = ProductResource(self)
|
|
74
|
+
self.orders = OrderResource(self)
|
|
75
|
+
self.order_tags = OrderTagResource(self)
|
|
76
|
+
self.pack_components = PackComponentResource(self)
|
|
77
|
+
self.packages = PackageResource(self)
|
|
78
|
+
self.order_documents = OrderDocumentResource(self)
|
|
79
|
+
self.address_labels = AddressLabelResource(self)
|
|
80
|
+
self.order_dispatches = OrderDispatchResource(self)
|
|
81
|
+
self.order_events = OrderEventResource(self)
|
|
82
|
+
self.address_label_summaries = AddressLabelSummariesResource(self)
|
|
83
|
+
self.address_label_configs = AddressLabelConfigResource(self)
|
|
84
|
+
self.address = AddressResource(self)
|
|
85
|
+
self.background_jobs = BackgroundJobResource(self)
|
|
86
|
+
self.carrier = CarrierResource(self)
|
|
87
|
+
self.client_order_origin = ClientOrderOriginResource(self)
|
|
88
|
+
self.client_order_source = ClientOrderSourceResource(self)
|
|
89
|
+
self.dangerous_good_product_informations = DangerousGoodProductInformationResource(self)
|
|
90
|
+
self.kit_components = KitComponentResource(self)
|
|
91
|
+
self.logistician_service_configs = LogisticianServiceConfigResource(self)
|
|
92
|
+
self.order_item_product_mappings = OrderItemProductMappingResource(self)
|
|
93
|
+
|
|
94
|
+
def _set_token(self, token_data: TokenData):
|
|
95
|
+
self.token = token_data
|
|
96
|
+
|
|
97
|
+
def set_config(self, timeout: int | None = None, max_retries: int | None = None, retry_backoff_factor: float | None = None, redirect_uri: str | None = None, auth_url: str | None = None, api_url: str | None = None):
|
|
98
|
+
if timeout is not None:
|
|
99
|
+
self.config.timeout = timeout
|
|
100
|
+
self.session.timeout = timeout
|
|
101
|
+
if max_retries is not None:
|
|
102
|
+
self.config.max_retries = max_retries
|
|
103
|
+
if retry_backoff_factor is not None:
|
|
104
|
+
self.config.retry_backoff_factor = retry_backoff_factor
|
|
105
|
+
if redirect_uri is not None:
|
|
106
|
+
self.config.redirect_uri = redirect_uri
|
|
107
|
+
if auth_url is not None:
|
|
108
|
+
self.config.auth_url = auth_url
|
|
109
|
+
if api_url is not None:
|
|
110
|
+
self.config.api_url = api_url
|
|
111
|
+
|
|
112
|
+
async def _process_response(self, response: httpx.Response) -> httpx.Response:
|
|
113
|
+
match response.status_code:
|
|
114
|
+
case s if 200 <= s < 300:
|
|
115
|
+
return response
|
|
116
|
+
case 400:
|
|
117
|
+
raise BadRequestError(response.status_code, f"Bad request: {response.text}")
|
|
118
|
+
case 401:
|
|
119
|
+
raise AuthenticationError(response.status_code, f"Authentication failed: {response.text}")
|
|
120
|
+
case 403:
|
|
121
|
+
raise ForbiddenError(response.status_code, f"Forbidden access: {response.text}")
|
|
122
|
+
case 404:
|
|
123
|
+
raise NotFoundError(response.status_code, f"Resource not found: {response.text}")
|
|
124
|
+
case 422:
|
|
125
|
+
raise BadRequestError(response.status_code, f"Unprocessable entity: {response.text}")
|
|
126
|
+
case s if s >= 500:
|
|
127
|
+
raise ServerError(response.status_code, f"Server error after {self.config.max_retries} retries: {response.text}")
|
|
128
|
+
case _:
|
|
129
|
+
raise UnexpectedError(response.status_code, f"Unexpected error: {response.text}")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
async def _raw_request(self, method: str, endpoint: str, params: dict[str, Any] | None = None, **kwargs: Any) -> httpx.Response:
|
|
133
|
+
|
|
134
|
+
if self.token is None:
|
|
135
|
+
raise AuthenticationError("Access token is missing. Please authenticate first.")
|
|
136
|
+
|
|
137
|
+
url = f"{self.config.api_url.rstrip('/')}/{endpoint.lstrip('/')}"
|
|
138
|
+
extra_headers = kwargs.pop("headers", {})
|
|
139
|
+
|
|
140
|
+
auth_attempts : int = 0
|
|
141
|
+
server_attempts : int = 0
|
|
142
|
+
|
|
143
|
+
while True:
|
|
144
|
+
|
|
145
|
+
headers = {
|
|
146
|
+
"Authorization": f"Bearer {self.token.access_token}",
|
|
147
|
+
"X-API-VERSION": self.config.api_version,
|
|
148
|
+
"X-API-APP-ID": self.config.app_id,
|
|
149
|
+
"Accept": "application/json",
|
|
150
|
+
**extra_headers
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
response = await self.session.request(method, url, headers=headers, params=params, **kwargs)
|
|
154
|
+
|
|
155
|
+
if response.status_code == 401 and auth_attempts == 0:
|
|
156
|
+
auth_attempts += 1
|
|
157
|
+
try:
|
|
158
|
+
new_token : TokenData | None = await refresh_token(self.token.refresh_token, self.session, self.config, on_refresh=self.on_token_refresh)
|
|
159
|
+
self._set_token(new_token)
|
|
160
|
+
continue
|
|
161
|
+
except Exception as e:
|
|
162
|
+
raise TokenRefreshError(f"Failed to refresh token: {str(e)}") from e
|
|
163
|
+
|
|
164
|
+
if response.status_code >= 500 and server_attempts < self.config.max_retries:
|
|
165
|
+
server_attempts += 1
|
|
166
|
+
await asyncio.sleep(self.config.retry_backoff_factor * (2 ** (server_attempts - 1)))
|
|
167
|
+
continue
|
|
168
|
+
|
|
169
|
+
return await self._process_response(response)
|
|
170
|
+
|
|
171
|
+
async def _request(self, method: str, endpoint: str, params: dict[str, Any] | None = None, **kwargs: Any) -> Any:
|
|
172
|
+
response = await self._raw_request(method, endpoint, params, **kwargs)
|
|
173
|
+
if not response.content:
|
|
174
|
+
return {}
|
|
175
|
+
return response.json()
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
async def _download(self, method: str, endpoint: str, params: dict[str, Any] | None = None, **kwargs: Any) -> bytes:
|
|
179
|
+
kwargs.setdefault("headers", {})["Accept"] = "application/pdf, application/octet-stream, */*"
|
|
180
|
+
response = await self._raw_request(method, endpoint, params, **kwargs)
|
|
181
|
+
return response.content
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@classmethod
|
|
185
|
+
async def from_auth_code(cls, auth_code: str, app_id: str, api_version: str, client_id: str, client_secret: str, redirect_uri: str | None = None, headers: dict[str, Any] | None = None):
|
|
186
|
+
config_dict : dict[str, Any] = {
|
|
187
|
+
"app_id": app_id,
|
|
188
|
+
"api_version": api_version,
|
|
189
|
+
"client_id": client_id,
|
|
190
|
+
"client_secret": client_secret
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if redirect_uri is not None:
|
|
194
|
+
config_dict["redirect_uri"] = redirect_uri
|
|
195
|
+
|
|
196
|
+
config = ShippingBoConfig(**config_dict)
|
|
197
|
+
async with httpx.AsyncClient(timeout=config.timeout) as session:
|
|
198
|
+
token = await get_token(auth_code, session, config, headers)
|
|
199
|
+
|
|
200
|
+
if token is None:
|
|
201
|
+
raise AuthenticationError("Failed to obtain access token with the provided authorization code.")
|
|
202
|
+
|
|
203
|
+
cls = cls(
|
|
204
|
+
access_token=token.access_token,
|
|
205
|
+
refresh_token=token.refresh_token,
|
|
206
|
+
app_id=config.app_id,
|
|
207
|
+
api_version=config.api_version,
|
|
208
|
+
client_id=config.client_id,
|
|
209
|
+
client_secret=config.client_secret
|
|
210
|
+
)
|
|
211
|
+
cls._set_token(token)
|
|
212
|
+
return cls
|
|
213
|
+
|
|
214
|
+
async def close(self):
|
|
215
|
+
await self.session.aclose()
|
|
216
|
+
|
|
217
|
+
async def __aenter__(self):
|
|
218
|
+
return self
|
|
219
|
+
|
|
220
|
+
async def __aexit__(self):
|
|
221
|
+
await self.close()
|
|
222
|
+
|
shippingboapy/config.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
2
|
+
|
|
3
|
+
class ShippingBoConfig(BaseSettings):
|
|
4
|
+
app_id: str
|
|
5
|
+
api_version: str
|
|
6
|
+
client_secret: str
|
|
7
|
+
client_id: str
|
|
8
|
+
auth_url: str = "https://oauth.shippingbo.com/oauth/token"
|
|
9
|
+
api_url: str = "https://app.shippingbo.com"
|
|
10
|
+
redirect_uri: str = "urn:ietf:wg:oauth:2.0:oob"
|
|
11
|
+
max_retries: int = 3
|
|
12
|
+
retry_backoff_factor: float = 0.5
|
|
13
|
+
timeout : int = 30
|
|
14
|
+
|
|
15
|
+
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
class ShippingboAPYException(Exception):
|
|
2
|
+
"""Base exception for ShippingboAPY."""
|
|
3
|
+
pass
|
|
4
|
+
|
|
5
|
+
class ValueError(ValueError):
|
|
6
|
+
"""Raised when an invalid value is provided."""
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
class AuthenticationError(ShippingboAPYException):
|
|
10
|
+
"""Raised when authentication fails."""
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
class TokenExpiredError(AuthenticationError):
|
|
14
|
+
"""Raised when the authentication token has expired."""
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
class TokenRefreshError(AuthenticationError):
|
|
18
|
+
"""Raised when there is an error refreshing the authentication token."""
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
class APIRequestError(ShippingboAPYException):
|
|
22
|
+
"""Raised when an API request fails."""
|
|
23
|
+
def __init__(self, status_code, message):
|
|
24
|
+
self.status_code = status_code
|
|
25
|
+
self.message = message
|
|
26
|
+
super().__init__(f"API Request failed with status {status_code}: {message}")
|
|
27
|
+
|
|
28
|
+
class BadRequestError(APIRequestError):
|
|
29
|
+
"""Raised when the API request is malformed or contains invalid parameters."""
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
class UnauthorizedError(APIRequestError):
|
|
33
|
+
"""Raised when the API request is unauthorized."""
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
class ForbiddenError(APIRequestError):
|
|
37
|
+
"""Raised when the API request is forbidden."""
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
class NotFoundError(APIRequestError):
|
|
41
|
+
"""Raised when the requested resource is not found."""
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
class RateLimitError(APIRequestError):
|
|
45
|
+
"""Raised when the API rate limit is exceeded."""
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
class ServerError(APIRequestError):
|
|
49
|
+
"""Raised when the server encounters an error."""
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
class UnexpectedError(APIRequestError):
|
|
53
|
+
"""Raised when an unexpected error occurs."""
|
|
54
|
+
pass
|
|
File without changes
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from pydantic import BaseModel, Field
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from shippingboapy.models.types import ShippingboDateTime
|
|
4
|
+
|
|
5
|
+
class Address(BaseModel):
|
|
6
|
+
apartement_number: Optional[str] = Field(None, description="The number of the apartment, if applicable.")
|
|
7
|
+
building: Optional[str] = Field(None, alias="building", description="The building name or number, if applicable.")
|
|
8
|
+
city: str = Field(..., alias="city", description="The city of the address.")
|
|
9
|
+
civility: Optional[str] = Field(None, alias="civilitity", description="The civility of the recipient (e.g., Mr, Ms, Company).")
|
|
10
|
+
company_name: Optional[str] = Field(None, alias="company_name", description="The name of the company, if applicable.")
|
|
11
|
+
country: str = Field(..., alias="country", description="The country of the address.")
|
|
12
|
+
created_at: Optional[ShippingboDateTime] = Field(None, alias="created_at", description="The date and time when the address was created.")
|
|
13
|
+
email: Optional[str] = Field(None, alias="email", description="The email address associated with the address.")
|
|
14
|
+
eori_importer: Optional[str] = Field(None, alias="eori_importer", description="The EORI number of the importer, if applicable.")
|
|
15
|
+
firstname: Optional[str] = Field(None, alias="firstname", description="The first name of the recipient.")
|
|
16
|
+
fullname: Optional[str] = Field(None, alias="fullname", description="The full name of the recipient.")
|
|
17
|
+
id: Optional[int] = Field(..., alias="id", description="The unique identifier of the address.")
|
|
18
|
+
instructions: Optional[str] = Field(None, alias="instructions", description="Special instructions for the delivery, if applicable.")
|
|
19
|
+
lastname: Optional[str] = Field(None, alias="lastname", description="The last name of the recipient.")
|
|
20
|
+
phone1: Optional[str] = Field(None, alias="phone", description="The phone number associated with the address.")
|
|
21
|
+
phone2: Optional[str] = Field(None, alias="phone2", description="An additional phone number associated with the address, if applicable.")
|
|
22
|
+
place_name: Optional[str] = Field(None, alias="place_name", description="The name of the place, if applicable.")
|
|
23
|
+
state: Optional[str] = Field(None, alias="state", description="The state or province of the address.")
|
|
24
|
+
street1: Optional[str] = Field(None, alias="street1", description="The first line of the street address.")
|
|
25
|
+
street2: Optional[str] = Field(None, alias="street2", description="The second line of the street address, if applicable.")
|
|
26
|
+
street3: Optional[str] = Field(None, alias="street3", description="The third line of the street address, if applicable.")
|
|
27
|
+
street4: Optional[str] = Field(None, alias="street4", description="The fourth line of the street address, if applicable.")
|
|
28
|
+
updated_at: Optional[ShippingboDateTime] = Field(None, alias="updated_at", description="The date and time when the address was last updated.")
|
|
29
|
+
vat_importer: Optional[str] = Field(None, alias="vat_importer", description="The VAT number of the importer, if applicable.")
|
|
30
|
+
zip: Optional[str] = Field(None, alias="zip", description="The postal code of the address.")
|
|
31
|
+
|
|
32
|
+
model_config = {
|
|
33
|
+
"extra": "allow",
|
|
34
|
+
"populate_by_name": True,
|
|
35
|
+
"validate_assignment": True
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
class AddressCreate(BaseModel):
|
|
39
|
+
apartement_number: Optional[str] = Field(None, alias="apartement_number", description="The number of the apartment, if applicable.")
|
|
40
|
+
building: Optional[str] = Field(None, alias="building", description="The building name or number, if applicable.")
|
|
41
|
+
city: str = Field(..., alias="city", description="The city of the address.")
|
|
42
|
+
civility: Optional[str] = Field(None, alias="civilitity", description="The civility of the recipient (e.g., Mr, Ms, Company).")
|
|
43
|
+
company_name: Optional[str] = Field(None, alias="company_name", description="The name of the company, if applicable.")
|
|
44
|
+
country: str = Field(..., alias="country", description="The country of the address.")
|
|
45
|
+
created_at: Optional[ShippingboDateTime] = Field(None, alias="created_at", description="The date and time when the address was created.")
|
|
46
|
+
email: Optional[str] = Field(None, alias="email", description="The email address associated with the address.")
|
|
47
|
+
eori_importer: Optional[str] = Field(None, alias="eori_importer", description="The EORI number of the importer, if applicable.")
|
|
48
|
+
firstname: Optional[str] = Field(None, alias="firstname", description="The first name of the recipient.")
|
|
49
|
+
fullname: Optional[str] = Field(None, alias="fullname", description="The full name of the recipient.")
|
|
50
|
+
instructions: Optional[str] = Field(None, alias="instructions", description="Special instructions for the delivery, if applicable.")
|
|
51
|
+
lastname: Optional[str] = Field(None, alias="lastname", description="The last name of the recipient.")
|
|
52
|
+
phone1: Optional[str] = Field(None, alias="phone", description="The phone number associated with the address.")
|
|
53
|
+
phone2: Optional[str] = Field(None, alias="phone2", description="An additional phone number associated with the address, if applicable.")
|
|
54
|
+
place_name: Optional[str] = Field(None, alias="place_name", description="The name of the place, if applicable.")
|
|
55
|
+
state: Optional[str] = Field(None, alias="state", description="The state or province of the address.")
|
|
56
|
+
street1: Optional[str] = Field(None, alias="street1", description="The first line of the street address.")
|
|
57
|
+
street2: Optional[str] = Field(None, alias="street2", description="The second line of the street address, if applicable.")
|
|
58
|
+
street3: Optional[str] = Field(None, alias="street3", description="The third line of the street address, if applicable.")
|
|
59
|
+
street4: Optional[str] = Field(None, alias="street4", description="The fourth line of the street address, if applicable.")
|
|
60
|
+
updated_at: Optional[ShippingboDateTime] = Field(None, alias="updated_at", description="The date and time when the address was last updated.")
|
|
61
|
+
vat_importer: Optional[str] = Field(None, alias="vat_importer", description="The VAT number of the importer, if applicable.")
|
|
62
|
+
zip: Optional[str] = Field(None, alias="zip", description="The postal code of the address.")
|
|
63
|
+
|
|
64
|
+
model_config = {
|
|
65
|
+
"extra": "allow",
|
|
66
|
+
"populate_by_name": True,
|
|
67
|
+
"validate_assignment": True
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
class AddressUpdate(BaseModel):
|
|
71
|
+
address_kind: Optional[str] = Field("", alias="address_kind", description="The kind of the address (e.g., shipping, billing), if applicable.")
|
|
72
|
+
client_ref: Optional[str] = Field("", alias="client_ref", description="A client reference for the address, if applicable.")
|
|
73
|
+
prefill_origin: Optional[str] = Field("", alias="prefill_origin", description="The origin to prefill the address with, if applicable.")
|
|
74
|
+
apartement_number: Optional[str] = Field("", alias="apartement_number", description="The number of the apartment, if applicable.")
|
|
75
|
+
building: Optional[str] = Field("", alias="building", description="The building name or number, if applicable.")
|
|
76
|
+
city: str = Field(..., alias="city", description="The city of the address.")
|
|
77
|
+
civility: Optional[str] = Field("", alias="civility", description="The civility of the recipient (e.g., Mr, Ms, Company).")
|
|
78
|
+
company_name: Optional[str] = Field("", alias="company_name", description="The name of the company, if applicable.")
|
|
79
|
+
country: str = Field(..., alias="country", description="The country of the address.")
|
|
80
|
+
email: Optional[str] = Field("", alias="email", description="The email address associated with the address.")
|
|
81
|
+
eori_importer: Optional[str] = Field("", alias="eori_importer", description="The EORI number of the importer, if applicable.")
|
|
82
|
+
firstname: Optional[str] = Field("", alias="frstname", description="The first name of the recipient.") # Note: The alias "frstname" is intentional to match the expected field name in the API request for updating an address, which may differ from the field name used in the Address model.
|
|
83
|
+
fullname: Optional[str] = Field("", alias="fullname", description="The full name of the recipient.")
|
|
84
|
+
instructions: Optional[str] = Field("", alias="instructions", description="Special instructions for the delivery, if applicable.")
|
|
85
|
+
lastname: Optional[str] = Field("", alias="lastname", description="The last name of the recipient.")
|
|
86
|
+
phone1: Optional[str] = Field("", alias="phone", description="The phone number associated with the address.")
|
|
87
|
+
phone2: Optional[str] = Field("", alias="phone2", description="An additional phone number associated with the address, if applicable.")
|
|
88
|
+
place_name: Optional[str] = Field("", alias="place_name", description="The name of the place, if applicable.")
|
|
89
|
+
state: Optional[str] = Field("", alias="state", description="The state or province of the address.")
|
|
90
|
+
street1: Optional[str] = Field("", alias="street1", description="The first line of the street address.")
|
|
91
|
+
street2: Optional[str] = Field("", alias="street2", description="The second line of the street address, if applicable.")
|
|
92
|
+
street3: Optional[str] = Field("", alias="street3", description="The third line of the street address, if applicable.")
|
|
93
|
+
street4: Optional[str] = Field("", alias="street4", description="The fourth line of the street address, if applicable.")
|
|
94
|
+
vat_importer: Optional[str] = Field("", alias="vat_importer", description="The VAT number of the importer, if applicable.")
|
|
95
|
+
zip: Optional[str] = Field("", alias="zip", description="The postal code of the address.")
|
|
96
|
+
|
|
97
|
+
model_config = {
|
|
98
|
+
"extra": "allow",
|
|
99
|
+
"populate_by_name": True,
|
|
100
|
+
"validate_assignment": True
|
|
101
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from pydantic import BaseModel, Field
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
class AddressLabel(BaseModel):
|
|
5
|
+
address_label_config_id: int = Field(..., alias="address_label_config_id", description="The unique identifier of the address label configuration associated with the address label.")
|
|
6
|
+
id: Optional[int] = Field(None, alias="id", description="The unique identifier of the address label.")
|
|
7
|
+
label_url: Optional[str] = Field(None, alias="label_url", description="The URL of the address label, if applicable.")
|
|
8
|
+
package_id: Optional[int] = Field(None, alias="package_id", description="The unique identifier of the package associated with the address label, if applicable.")
|
|
9
|
+
|
|
10
|
+
model_config = {
|
|
11
|
+
"extra": "allow",
|
|
12
|
+
"populate_by_name": True,
|
|
13
|
+
"validate_assignment": True
|
|
14
|
+
}
|