statuspro-openapi-client 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.
- statuspro_openapi_client-0.1.0.dist-info/METADATA +337 -0
- statuspro_openapi_client-0.1.0.dist-info/RECORD +74 -0
- statuspro_openapi_client-0.1.0.dist-info/WHEEL +4 -0
- statuspro_openapi_client-0.1.0.dist-info/entry_points.txt +3 -0
- statuspro_openapi_client-0.1.0.dist-info/licenses/LICENSE +21 -0
- statuspro_public_api_client/__init__.py +36 -0
- statuspro_public_api_client/_logging.py +33 -0
- statuspro_public_api_client/api/__init__.py +1 -0
- statuspro_public_api_client/api/orders/__init__.py +1 -0
- statuspro_public_api_client/api/orders/add_order_comment.py +215 -0
- statuspro_public_api_client/api/orders/bulk_update_order_status.py +194 -0
- statuspro_public_api_client/api/orders/get_order.py +188 -0
- statuspro_public_api_client/api/orders/get_viable_statuses.py +193 -0
- statuspro_public_api_client/api/orders/list_orders.py +366 -0
- statuspro_public_api_client/api/orders/lookup_order.py +208 -0
- statuspro_public_api_client/api/orders/set_order_due_date.py +215 -0
- statuspro_public_api_client/api/orders/update_order_status.py +215 -0
- statuspro_public_api_client/api/statuses/__init__.py +1 -0
- statuspro_public_api_client/api/statuses/get_statuses.py +161 -0
- statuspro_public_api_client/api_wrapper/__init__.py +15 -0
- statuspro_public_api_client/api_wrapper/_namespace.py +40 -0
- statuspro_public_api_client/api_wrapper/_registry.py +43 -0
- statuspro_public_api_client/api_wrapper/_resource.py +116 -0
- statuspro_public_api_client/client.py +267 -0
- statuspro_public_api_client/client_types.py +54 -0
- statuspro_public_api_client/domain/__init__.py +33 -0
- statuspro_public_api_client/domain/base.py +117 -0
- statuspro_public_api_client/domain/converters.py +71 -0
- statuspro_public_api_client/domain/order.py +87 -0
- statuspro_public_api_client/domain/status.py +30 -0
- statuspro_public_api_client/errors.py +16 -0
- statuspro_public_api_client/helpers/__init__.py +21 -0
- statuspro_public_api_client/helpers/base.py +26 -0
- statuspro_public_api_client/helpers/orders.py +78 -0
- statuspro_public_api_client/helpers/statuses.py +37 -0
- statuspro_public_api_client/log_setup.py +99 -0
- statuspro_public_api_client/models/__init__.py +53 -0
- statuspro_public_api_client/models/add_order_comment_request.py +68 -0
- statuspro_public_api_client/models/bulk_status_update_request.py +124 -0
- statuspro_public_api_client/models/bulk_status_update_response.py +72 -0
- statuspro_public_api_client/models/customer.py +74 -0
- statuspro_public_api_client/models/error_response.py +58 -0
- statuspro_public_api_client/models/history_item.py +171 -0
- statuspro_public_api_client/models/list_orders_financial_status_item.py +15 -0
- statuspro_public_api_client/models/list_orders_fulfillment_status_item.py +11 -0
- statuspro_public_api_client/models/locale_translation.py +66 -0
- statuspro_public_api_client/models/mail_log.py +82 -0
- statuspro_public_api_client/models/message_response.py +58 -0
- statuspro_public_api_client/models/order_list_item.py +180 -0
- statuspro_public_api_client/models/order_list_meta.py +120 -0
- statuspro_public_api_client/models/order_list_response.py +81 -0
- statuspro_public_api_client/models/order_response.py +220 -0
- statuspro_public_api_client/models/progress_timeline_item.py +93 -0
- statuspro_public_api_client/models/set_due_date_request.py +95 -0
- statuspro_public_api_client/models/status.py +190 -0
- statuspro_public_api_client/models/status_definition.py +82 -0
- statuspro_public_api_client/models/status_translations.py +62 -0
- statuspro_public_api_client/models/update_order_status_request.py +92 -0
- statuspro_public_api_client/models/validation_error_response.py +81 -0
- statuspro_public_api_client/models/validation_error_response_errors.py +54 -0
- statuspro_public_api_client/models/viable_status.py +82 -0
- statuspro_public_api_client/models_pydantic/__init__.py +122 -0
- statuspro_public_api_client/models_pydantic/_auto_registry.py +115 -0
- statuspro_public_api_client/models_pydantic/_base.py +349 -0
- statuspro_public_api_client/models_pydantic/_generated/__init__.py +53 -0
- statuspro_public_api_client/models_pydantic/_generated/errors.py +24 -0
- statuspro_public_api_client/models_pydantic/_generated/orders.py +136 -0
- statuspro_public_api_client/models_pydantic/_generated/statuses.py +25 -0
- statuspro_public_api_client/models_pydantic/_registry.py +171 -0
- statuspro_public_api_client/models_pydantic/converters.py +184 -0
- statuspro_public_api_client/py.typed +1 -0
- statuspro_public_api_client/statuspro-openapi.yaml +859 -0
- statuspro_public_api_client/statuspro_client.py +1156 -0
- statuspro_public_api_client/utils.py +290 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Contains some shared types for properties"""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping, MutableMapping
|
|
4
|
+
from http import HTTPStatus
|
|
5
|
+
from typing import IO, BinaryIO, Literal, TypeVar
|
|
6
|
+
|
|
7
|
+
from attrs import define
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Unset:
|
|
11
|
+
def __bool__(self) -> Literal[False]:
|
|
12
|
+
return False
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
UNSET: Unset = Unset()
|
|
16
|
+
|
|
17
|
+
# The types that `httpx.Client(files=)` can accept, copied from that library.
|
|
18
|
+
FileContent = IO[bytes] | bytes | str
|
|
19
|
+
FileTypes = (
|
|
20
|
+
# (filename, file (or bytes), content_type)
|
|
21
|
+
tuple[str | None, FileContent, str | None]
|
|
22
|
+
# (filename, file (or bytes), content_type, headers)
|
|
23
|
+
| tuple[str | None, FileContent, str | None, Mapping[str, str]]
|
|
24
|
+
)
|
|
25
|
+
RequestFiles = list[tuple[str, FileTypes]]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@define
|
|
29
|
+
class File:
|
|
30
|
+
"""Contains information for file uploads"""
|
|
31
|
+
|
|
32
|
+
payload: BinaryIO
|
|
33
|
+
file_name: str | None = None
|
|
34
|
+
mime_type: str | None = None
|
|
35
|
+
|
|
36
|
+
def to_tuple(self) -> FileTypes:
|
|
37
|
+
"""Return a tuple representation that httpx will accept for multipart/form-data"""
|
|
38
|
+
return self.file_name, self.payload, self.mime_type
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
T = TypeVar("T")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@define
|
|
45
|
+
class Response[T]:
|
|
46
|
+
"""A response from an endpoint"""
|
|
47
|
+
|
|
48
|
+
status_code: HTTPStatus
|
|
49
|
+
content: bytes
|
|
50
|
+
headers: MutableMapping[str, str]
|
|
51
|
+
parsed: T | None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
__all__ = ["UNSET", "File", "FileTypes", "RequestFiles", "Response", "Unset"]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Pydantic domain models for StatusPro entities.
|
|
2
|
+
|
|
3
|
+
Hand-written domain models representing business entities from the StatusPro API,
|
|
4
|
+
separate from the generated attrs API request/response models. Use these for
|
|
5
|
+
business logic, validation, and ergonomic access to API data.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
```python
|
|
9
|
+
from statuspro_public_api_client import StatusProClient
|
|
10
|
+
from statuspro_public_api_client.domain import Order
|
|
11
|
+
|
|
12
|
+
async with StatusProClient() as client:
|
|
13
|
+
orders = await client.orders.list(per_page=50)
|
|
14
|
+
for order in orders:
|
|
15
|
+
print(f"{order.name}: {order.status.name}")
|
|
16
|
+
```
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from .base import StatusProBaseModel
|
|
20
|
+
from .converters import to_unset, unwrap_unset
|
|
21
|
+
from .order import Customer, Order, OrderStatus, PageMeta
|
|
22
|
+
from .status import Status
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"Customer",
|
|
26
|
+
"Order",
|
|
27
|
+
"OrderStatus",
|
|
28
|
+
"PageMeta",
|
|
29
|
+
"Status",
|
|
30
|
+
"StatusProBaseModel",
|
|
31
|
+
"to_unset",
|
|
32
|
+
"unwrap_unset",
|
|
33
|
+
]
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Base domain model for StatusPro entities.
|
|
2
|
+
|
|
3
|
+
This module provides the foundation for Pydantic domain models that represent
|
|
4
|
+
business entities from the StatusPro Manufacturing ERP system.
|
|
5
|
+
|
|
6
|
+
Domain models are separate from the generated API request/response models and
|
|
7
|
+
are optimized for:
|
|
8
|
+
- ETL and data processing
|
|
9
|
+
- Business logic
|
|
10
|
+
- Data validation
|
|
11
|
+
- JSON schema generation
|
|
12
|
+
- Clean, ergonomic APIs
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import Any, ClassVar
|
|
18
|
+
|
|
19
|
+
from pydantic import AwareDatetime, BaseModel, ConfigDict, Field
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class StatusProBaseModel(BaseModel):
|
|
23
|
+
"""Base class for all Pydantic domain models.
|
|
24
|
+
|
|
25
|
+
Provides:
|
|
26
|
+
- Immutability by default (frozen=True)
|
|
27
|
+
- Automatic validation
|
|
28
|
+
- JSON schema generation
|
|
29
|
+
- Easy serialization for ETL
|
|
30
|
+
- Common timestamp fields
|
|
31
|
+
|
|
32
|
+
Example:
|
|
33
|
+
```python
|
|
34
|
+
class OrderDomain(StatusProBaseModel):
|
|
35
|
+
id: int
|
|
36
|
+
name: str
|
|
37
|
+
order_number: str | None = None
|
|
38
|
+
|
|
39
|
+
@computed_field
|
|
40
|
+
@property
|
|
41
|
+
def display_name(self) -> str:
|
|
42
|
+
return f"Order {self.name} (#{self.order_number})"
|
|
43
|
+
```
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
model_config = ConfigDict(
|
|
47
|
+
frozen=True, # Immutable by default
|
|
48
|
+
validate_assignment=True, # Validate on updates (if unfrozen)
|
|
49
|
+
arbitrary_types_allowed=True, # Allow datetime, etc.
|
|
50
|
+
str_strip_whitespace=True, # Clean string inputs
|
|
51
|
+
json_schema_extra={
|
|
52
|
+
"source": "StatusPro Manufacturing ERP",
|
|
53
|
+
"version": "v1",
|
|
54
|
+
},
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Metadata about the source model
|
|
58
|
+
_source_model: ClassVar[str] = "attrs"
|
|
59
|
+
_api_version: ClassVar[str] = "v1"
|
|
60
|
+
|
|
61
|
+
# Common timestamp fields (most StatusPro entities have these)
|
|
62
|
+
# Using AwareDatetime to match generated Pydantic models and ensure timezone awareness
|
|
63
|
+
created_at: AwareDatetime | None = Field(
|
|
64
|
+
None, description="Timestamp when entity was created"
|
|
65
|
+
)
|
|
66
|
+
updated_at: AwareDatetime | None = Field(
|
|
67
|
+
None, description="Timestamp when entity was last updated"
|
|
68
|
+
)
|
|
69
|
+
deleted_at: AwareDatetime | None = Field(
|
|
70
|
+
None, description="Timestamp when entity was soft-deleted (if applicable)"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def model_dump_for_etl(self) -> dict[str, Any]:
|
|
74
|
+
"""Export to ETL-friendly format.
|
|
75
|
+
|
|
76
|
+
Removes None values and uses field aliases for cleaner output.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Dictionary with all non-None fields
|
|
80
|
+
|
|
81
|
+
Example:
|
|
82
|
+
```python
|
|
83
|
+
order = Order(id=123, name="#1188", order_number="1188")
|
|
84
|
+
data = order.model_dump_for_etl()
|
|
85
|
+
# {"id": 123, "name": "#1188", "order_number": "1188"}
|
|
86
|
+
```
|
|
87
|
+
"""
|
|
88
|
+
return self.model_dump(exclude_none=True, by_alias=True)
|
|
89
|
+
|
|
90
|
+
def to_warehouse_json(self) -> str:
|
|
91
|
+
"""Export as JSON for data warehouse.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
JSON string with all non-None fields
|
|
95
|
+
|
|
96
|
+
Example:
|
|
97
|
+
```python
|
|
98
|
+
order = Order(id=123, name="#1188")
|
|
99
|
+
json_str = order.to_warehouse_json()
|
|
100
|
+
# '{"id":123,"name":"#1188"}'
|
|
101
|
+
```
|
|
102
|
+
"""
|
|
103
|
+
return self.model_dump_json(exclude_none=True, by_alias=True)
|
|
104
|
+
|
|
105
|
+
def to_dict_with_computed(self) -> dict[str, Any]:
|
|
106
|
+
"""Export including computed fields.
|
|
107
|
+
|
|
108
|
+
Unlike model_dump(), this includes @computed_field properties.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Dictionary with all fields including computed ones
|
|
112
|
+
"""
|
|
113
|
+
# Pydantic v2 automatically includes computed fields in model_dump
|
|
114
|
+
return self.model_dump(mode="python", exclude_none=True)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
__all__ = ["StatusProBaseModel"]
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Generic conversion utilities for attrs API models.
|
|
2
|
+
|
|
3
|
+
These helpers bridge the attrs-based generated models (which use ``UNSET`` for
|
|
4
|
+
unprovided fields) and Pydantic domain models (which use ``None``).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import overload
|
|
10
|
+
|
|
11
|
+
from ..client_types import UNSET, Unset
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def to_unset[T](value: T | None) -> T | Unset:
|
|
15
|
+
"""Convert None to UNSET sentinel value.
|
|
16
|
+
|
|
17
|
+
Useful when building attrs API request models from optional Pydantic fields,
|
|
18
|
+
where None means "not provided" and should be sent as UNSET to avoid
|
|
19
|
+
overwriting existing values.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
value: Value that might be None
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
The value unchanged if not None, or UNSET if None
|
|
26
|
+
|
|
27
|
+
Example:
|
|
28
|
+
```python
|
|
29
|
+
from statuspro_public_api_client.domain.converters import to_unset
|
|
30
|
+
|
|
31
|
+
to_unset(42) # 42
|
|
32
|
+
to_unset(None) # UNSET
|
|
33
|
+
to_unset("USD") # "USD"
|
|
34
|
+
```
|
|
35
|
+
"""
|
|
36
|
+
return UNSET if value is None else value
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@overload
|
|
40
|
+
def unwrap_unset[T](value: T | Unset | None) -> T | None: ...
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@overload
|
|
44
|
+
def unwrap_unset[T](value: T | Unset | None, default: T) -> T: ...
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def unwrap_unset[T](value: T | Unset | None, default: T | None = None) -> T | None:
|
|
48
|
+
"""Unwrap an Unset or None sentinel value.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
value: Value that might be Unset or None
|
|
52
|
+
default: Default value to return if Unset or None
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
The unwrapped value, or default if value is Unset or None. When a
|
|
56
|
+
non-None default is provided, the return type is narrowed to ``T``
|
|
57
|
+
(never None).
|
|
58
|
+
|
|
59
|
+
Example:
|
|
60
|
+
```python
|
|
61
|
+
from statuspro_public_api_client.client_types import UNSET
|
|
62
|
+
|
|
63
|
+
unwrap_unset(42) # 42
|
|
64
|
+
unwrap_unset(UNSET) # None
|
|
65
|
+
unwrap_unset(UNSET, 0) # 0
|
|
66
|
+
unwrap_unset(None, 0) # 0
|
|
67
|
+
```
|
|
68
|
+
"""
|
|
69
|
+
if value is None or isinstance(value, Unset):
|
|
70
|
+
return default
|
|
71
|
+
return value
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Domain models for StatusPro orders.
|
|
2
|
+
|
|
3
|
+
Hand-written Pydantic models for business logic, separate from the generated
|
|
4
|
+
attrs API models. Shapes mirror the OpenAPI ``Customer``, ``Status``,
|
|
5
|
+
``OrderListItem``, ``OrderResponse``, and ``OrderListMeta`` schemas.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pydantic import AwareDatetime, BaseModel, ConfigDict, Field
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class _Frozen(BaseModel):
|
|
14
|
+
"""Immutable Pydantic base used for nested types that don't carry timestamps."""
|
|
15
|
+
|
|
16
|
+
model_config = ConfigDict(
|
|
17
|
+
frozen=True,
|
|
18
|
+
str_strip_whitespace=True,
|
|
19
|
+
extra="ignore",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Customer(_Frozen):
|
|
24
|
+
"""Customer details attached to an order."""
|
|
25
|
+
|
|
26
|
+
name: str | None = None
|
|
27
|
+
email: str | None = None
|
|
28
|
+
locale: str | None = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class OrderStatus(_Frozen):
|
|
32
|
+
"""The status currently set on an order (nested ``Status`` schema).
|
|
33
|
+
|
|
34
|
+
Distinct from :class:`Status`, which is a top-level status definition
|
|
35
|
+
returned by ``/statuses`` and includes ``color`` instead of transition
|
|
36
|
+
metadata.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
is_set: bool | None = None
|
|
40
|
+
code: str | None = Field(None, description="8-char status code, e.g. 'st000002'")
|
|
41
|
+
name: str | None = None
|
|
42
|
+
public_name: str | None = None
|
|
43
|
+
description: str | None = None
|
|
44
|
+
public: bool | None = None
|
|
45
|
+
set_at: AwareDatetime | None = None
|
|
46
|
+
auto_change_at: AwareDatetime | None = None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Order(_Frozen):
|
|
50
|
+
"""An order as returned by ``/orders`` list pages or ``/orders/{id}``."""
|
|
51
|
+
|
|
52
|
+
id: int
|
|
53
|
+
name: str | None = Field(None, description="Display name, e.g. '#1188'")
|
|
54
|
+
order_number: str | None = None
|
|
55
|
+
customer: Customer | None = None
|
|
56
|
+
status: OrderStatus | None = None
|
|
57
|
+
due_date: AwareDatetime | None = None
|
|
58
|
+
due_date_to: AwareDatetime | None = None
|
|
59
|
+
history_count: int | None = Field(
|
|
60
|
+
None,
|
|
61
|
+
description="Number of history entries; only present on list responses",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class PageMeta(_Frozen):
|
|
66
|
+
"""Pagination envelope returned alongside list endpoints.
|
|
67
|
+
|
|
68
|
+
Matches the StatusPro ``OrderListMeta`` schema. ``current_page`` and
|
|
69
|
+
``last_page`` are used by the transport layer to auto-walk pages.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
current_page: int
|
|
73
|
+
last_page: int
|
|
74
|
+
per_page: int
|
|
75
|
+
total: int
|
|
76
|
+
from_: int | None = Field(None, alias="from")
|
|
77
|
+
to: int | None = None
|
|
78
|
+
|
|
79
|
+
model_config = ConfigDict(
|
|
80
|
+
frozen=True,
|
|
81
|
+
str_strip_whitespace=True,
|
|
82
|
+
extra="ignore",
|
|
83
|
+
populate_by_name=True,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
__all__ = ["Customer", "Order", "OrderStatus", "PageMeta"]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Domain model for StatusPro status definitions.
|
|
2
|
+
|
|
3
|
+
Matches the OpenAPI ``StatusDefinition`` schema (returned by ``/statuses``)
|
|
4
|
+
and is structurally identical to ``ViableStatus`` (returned by
|
|
5
|
+
``/orders/{id}/viable-statuses``).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Status(BaseModel):
|
|
14
|
+
"""A top-level status definition from the account's status catalog."""
|
|
15
|
+
|
|
16
|
+
code: str = Field(..., description="8-char code, e.g. 'st000002'")
|
|
17
|
+
name: str | None = None
|
|
18
|
+
description: str | None = None
|
|
19
|
+
color: str | None = Field(
|
|
20
|
+
None, description="Display color, either a name (e.g. 'pink') or hex"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
model_config = ConfigDict(
|
|
24
|
+
frozen=True,
|
|
25
|
+
str_strip_whitespace=True,
|
|
26
|
+
extra="ignore",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
__all__ = ["Status"]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Contains shared errors types that can be raised from API functions"""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class UnexpectedStatus(Exception):
|
|
5
|
+
"""Raised by api functions when the response status an undocumented status and Client.raise_on_unexpected_status is True"""
|
|
6
|
+
|
|
7
|
+
def __init__(self, status_code: int, content: bytes):
|
|
8
|
+
self.status_code = status_code
|
|
9
|
+
self.content = content
|
|
10
|
+
|
|
11
|
+
super().__init__(
|
|
12
|
+
f"Unexpected status code: {status_code}\n\nResponse content:\n{content.decode(errors='ignore')}"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
__all__ = ["UnexpectedStatus"]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Ergonomic helper facades for the StatusPro API client.
|
|
2
|
+
|
|
3
|
+
These classes wrap the generated API with domain-specific methods that reduce
|
|
4
|
+
boilerplate for common workflows. Each helper is accessed as an attribute on
|
|
5
|
+
``StatusProClient`` (e.g. ``client.orders.list(...)``).
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> async with StatusProClient() as client:
|
|
9
|
+
... orders = await client.orders.list(per_page=50)
|
|
10
|
+
... statuses = await client.statuses.list()
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from statuspro_public_api_client.helpers.base import Base
|
|
14
|
+
from statuspro_public_api_client.helpers.orders import Orders
|
|
15
|
+
from statuspro_public_api_client.helpers.statuses import Statuses
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"Base",
|
|
19
|
+
"Orders",
|
|
20
|
+
"Statuses",
|
|
21
|
+
]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Base class for domain classes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from statuspro_public_api_client.statuspro_client import StatusProClient
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Base:
|
|
12
|
+
"""Base class for all domain classes.
|
|
13
|
+
|
|
14
|
+
Provides common functionality and access to the StatusProClient instance.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
client: The StatusProClient instance to use for API calls.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, client: StatusProClient) -> None:
|
|
21
|
+
"""Initialize with a client instance.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
client: The StatusProClient instance to use for API calls.
|
|
25
|
+
"""
|
|
26
|
+
self._client = client
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Order helper facade — ergonomic wrappers around the generated order endpoints."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import builtins
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
from statuspro_public_api_client.helpers.base import Base
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from statuspro_public_api_client.domain import Order
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Orders(Base):
|
|
15
|
+
"""Ergonomic order operations that return domain ``Order`` models."""
|
|
16
|
+
|
|
17
|
+
async def list(
|
|
18
|
+
self,
|
|
19
|
+
*,
|
|
20
|
+
search: str | None = None,
|
|
21
|
+
status_code: str | None = None,
|
|
22
|
+
tags: builtins.list[str] | None = None,
|
|
23
|
+
tags_any: builtins.list[str] | None = None,
|
|
24
|
+
financial_status: builtins.list[str] | None = None,
|
|
25
|
+
fulfillment_status: builtins.list[str] | None = None,
|
|
26
|
+
exclude_cancelled: bool | None = None,
|
|
27
|
+
due_date_from: str | None = None,
|
|
28
|
+
due_date_to: str | None = None,
|
|
29
|
+
page: int | None = None,
|
|
30
|
+
per_page: int | None = None,
|
|
31
|
+
) -> builtins.list[Order]:
|
|
32
|
+
"""List orders. Auto-paginates when ``page`` is not set."""
|
|
33
|
+
from statuspro_public_api_client.api.orders import list_orders
|
|
34
|
+
from statuspro_public_api_client.domain import Order
|
|
35
|
+
from statuspro_public_api_client.utils import unwrap_data
|
|
36
|
+
|
|
37
|
+
kwargs: dict[str, Any] = {
|
|
38
|
+
"client": self._client,
|
|
39
|
+
"search": search,
|
|
40
|
+
"status_code": status_code,
|
|
41
|
+
"tags": tags,
|
|
42
|
+
"tags_any": tags_any,
|
|
43
|
+
"financial_status": financial_status,
|
|
44
|
+
"fulfillment_status": fulfillment_status,
|
|
45
|
+
"exclude_cancelled": exclude_cancelled,
|
|
46
|
+
"due_date_from": due_date_from,
|
|
47
|
+
"due_date_to": due_date_to,
|
|
48
|
+
"page": page,
|
|
49
|
+
"per_page": per_page,
|
|
50
|
+
}
|
|
51
|
+
kwargs = {k: v for k, v in kwargs.items() if v is not None}
|
|
52
|
+
response = await list_orders.asyncio_detailed(**kwargs)
|
|
53
|
+
raw_orders = unwrap_data(response, default=[])
|
|
54
|
+
return [Order.model_validate(o.to_dict()) for o in raw_orders]
|
|
55
|
+
|
|
56
|
+
async def get(self, order_id: int) -> Order:
|
|
57
|
+
"""Get a single order by id."""
|
|
58
|
+
from statuspro_public_api_client.api.orders import get_order
|
|
59
|
+
from statuspro_public_api_client.domain import Order
|
|
60
|
+
from statuspro_public_api_client.models.order_response import OrderResponse
|
|
61
|
+
from statuspro_public_api_client.utils import unwrap_as
|
|
62
|
+
|
|
63
|
+
response = await get_order.asyncio_detailed(client=self._client, order=order_id)
|
|
64
|
+
raw = unwrap_as(response, OrderResponse)
|
|
65
|
+
return Order.model_validate(raw.to_dict())
|
|
66
|
+
|
|
67
|
+
async def lookup(self, *, number: str, email: str) -> Order:
|
|
68
|
+
"""Look up an order by order number and customer email."""
|
|
69
|
+
from statuspro_public_api_client.api.orders import lookup_order
|
|
70
|
+
from statuspro_public_api_client.domain import Order
|
|
71
|
+
from statuspro_public_api_client.models.order_response import OrderResponse
|
|
72
|
+
from statuspro_public_api_client.utils import unwrap_as
|
|
73
|
+
|
|
74
|
+
response = await lookup_order.asyncio_detailed(
|
|
75
|
+
client=self._client, number=number, email=email
|
|
76
|
+
)
|
|
77
|
+
raw = unwrap_as(response, OrderResponse)
|
|
78
|
+
return Order.model_validate(raw.to_dict())
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Status helper facade — ergonomic wrappers around the generated status endpoints."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import builtins
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from statuspro_public_api_client.helpers.base import Base
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from statuspro_public_api_client.domain import Status
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Statuses(Base):
|
|
15
|
+
"""Ergonomic status catalog operations."""
|
|
16
|
+
|
|
17
|
+
async def list(self) -> builtins.list[Status]:
|
|
18
|
+
"""Return the full status catalog (``/statuses``)."""
|
|
19
|
+
from statuspro_public_api_client.api.statuses import get_statuses
|
|
20
|
+
from statuspro_public_api_client.domain import Status
|
|
21
|
+
from statuspro_public_api_client.utils import unwrap_data
|
|
22
|
+
|
|
23
|
+
response = await get_statuses.asyncio_detailed(client=self._client)
|
|
24
|
+
raw = unwrap_data(response, default=[])
|
|
25
|
+
return [Status.model_validate(s.to_dict()) for s in raw]
|
|
26
|
+
|
|
27
|
+
async def viable_for(self, order_id: int) -> builtins.list[Status]:
|
|
28
|
+
"""Return statuses that are valid transitions from the order's current state."""
|
|
29
|
+
from statuspro_public_api_client.api.orders import get_viable_statuses
|
|
30
|
+
from statuspro_public_api_client.domain import Status
|
|
31
|
+
from statuspro_public_api_client.utils import unwrap_data
|
|
32
|
+
|
|
33
|
+
response = await get_viable_statuses.asyncio_detailed(
|
|
34
|
+
client=self._client, order=order_id
|
|
35
|
+
)
|
|
36
|
+
raw = unwrap_data(response, default=[])
|
|
37
|
+
return [Status.model_validate(s.to_dict()) for s in raw]
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Logging setup utilities for the StatusPro API client.
|
|
2
|
+
|
|
3
|
+
This module provides logging configuration helpers for the enhanced StatusPro client.
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import logging.handlers
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class InfoToDebugFilter(logging.Filter):
|
|
13
|
+
"""Filter to convert INFO level logs to DEBUG level for specific loggers."""
|
|
14
|
+
|
|
15
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
|
16
|
+
if record.levelno == logging.INFO:
|
|
17
|
+
record.levelno = logging.DEBUG
|
|
18
|
+
record.levelname = "DEBUG"
|
|
19
|
+
return True
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def setup_logging(
|
|
23
|
+
log_dir: str = "logs",
|
|
24
|
+
log_file_prefix: str = "statuspro_client",
|
|
25
|
+
log_level: int = logging.INFO,
|
|
26
|
+
console_level: int = logging.INFO,
|
|
27
|
+
max_bytes: int = 10 * 1024 * 1024, # 10MB
|
|
28
|
+
backup_count: int = 5,
|
|
29
|
+
) -> logging.Logger:
|
|
30
|
+
"""Set up logging configuration for the StatusPro client.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
log_dir: Directory to store log files
|
|
34
|
+
log_file_prefix: Prefix for log file names
|
|
35
|
+
log_level: Logging level for file handler
|
|
36
|
+
console_level: Logging level for console handler
|
|
37
|
+
max_bytes: Maximum size of each log file before rotation
|
|
38
|
+
backup_count: Number of backup files to keep
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Configured logger instance
|
|
42
|
+
|
|
43
|
+
"""
|
|
44
|
+
# Create log directory if it doesn't exist
|
|
45
|
+
log_path = Path(log_dir)
|
|
46
|
+
log_path.mkdir(exist_ok=True)
|
|
47
|
+
|
|
48
|
+
# Create logger
|
|
49
|
+
logger = logging.getLogger("statuspro_client")
|
|
50
|
+
logger.setLevel(logging.DEBUG)
|
|
51
|
+
|
|
52
|
+
# Remove existing handlers to avoid duplicates
|
|
53
|
+
for handler in logger.handlers[:]:
|
|
54
|
+
logger.removeHandler(handler)
|
|
55
|
+
|
|
56
|
+
# Create formatters
|
|
57
|
+
detailed_formatter = logging.Formatter(
|
|
58
|
+
"%(asctime)s - %(name)s - %(levelname)s - %(funcName)s:%(lineno)d - %(message)s"
|
|
59
|
+
)
|
|
60
|
+
simple_formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
|
|
61
|
+
|
|
62
|
+
# File handler with rotation
|
|
63
|
+
log_file = log_path / f"{log_file_prefix}.log"
|
|
64
|
+
file_handler = logging.handlers.RotatingFileHandler(
|
|
65
|
+
log_file, maxBytes=max_bytes, backupCount=backup_count
|
|
66
|
+
)
|
|
67
|
+
file_handler.setLevel(log_level)
|
|
68
|
+
file_handler.setFormatter(detailed_formatter)
|
|
69
|
+
logger.addHandler(file_handler)
|
|
70
|
+
|
|
71
|
+
# Console handler
|
|
72
|
+
console_handler = logging.StreamHandler()
|
|
73
|
+
console_handler.setLevel(console_level)
|
|
74
|
+
console_handler.setFormatter(simple_formatter)
|
|
75
|
+
logger.addHandler(console_handler)
|
|
76
|
+
|
|
77
|
+
# Set up httpx logger with filter
|
|
78
|
+
httpx_logger = logging.getLogger("httpx")
|
|
79
|
+
httpx_logger.setLevel(logging.DEBUG)
|
|
80
|
+
httpx_logger.addFilter(InfoToDebugFilter())
|
|
81
|
+
|
|
82
|
+
logger.info(
|
|
83
|
+
f"Logging configured - File: {log_file}, Level: {logging.getLevelName(log_level)}"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
return logger
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def get_logger(name: str | None = None) -> logging.Logger:
|
|
90
|
+
"""Get a logger instance.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
name: Logger name (defaults to statuspro_client)
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Logger instance
|
|
97
|
+
|
|
98
|
+
"""
|
|
99
|
+
return logging.getLogger(name or "statuspro_client")
|