woltapi 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.
- woltapi/__init__.py +90 -0
- woltapi/client.py +565 -0
- woltapi/credentials.py +67 -0
- woltapi/errors.py +101 -0
- woltapi/models.py +47 -0
- woltapi/purchase.py +1103 -0
- woltapi/selection.py +863 -0
- woltapi/services.py +24 -0
- woltapi/transport.py +198 -0
- woltapi-0.0.1.dist-info/METADATA +215 -0
- woltapi-0.0.1.dist-info/RECORD +14 -0
- woltapi-0.0.1.dist-info/WHEEL +5 -0
- woltapi-0.0.1.dist-info/licenses/LICENSE +661 -0
- woltapi-0.0.1.dist-info/top_level.txt +1 -0
woltapi/credentials.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Credential handling with explicit ordering-host scopes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from types import MappingProxyType
|
|
7
|
+
|
|
8
|
+
from .services import ServiceHost
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SessionCredentials:
|
|
12
|
+
"""Caller-provided request headers, scoped to one ordering host each.
|
|
13
|
+
|
|
14
|
+
No header name or value is inferred from the captured browser traffic. A
|
|
15
|
+
caller must provide credentials obtained through its own approved session
|
|
16
|
+
integration, and remains responsible for their validity and lifecycle.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
__slots__ = ("_headers",)
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
*,
|
|
24
|
+
restaurant_headers: Mapping[str, str] | None = None,
|
|
25
|
+
consumer_headers: Mapping[str, str] | None = None,
|
|
26
|
+
payment_headers: Mapping[str, str] | None = None,
|
|
27
|
+
) -> None:
|
|
28
|
+
self._headers = MappingProxyType(
|
|
29
|
+
{
|
|
30
|
+
ServiceHost.RESTAURANT: _freeze_headers(restaurant_headers),
|
|
31
|
+
ServiceHost.CONSUMER: _freeze_headers(consumer_headers),
|
|
32
|
+
ServiceHost.PAYMENT: _freeze_headers(payment_headers),
|
|
33
|
+
}
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
def headers_for(self, service: ServiceHost) -> Mapping[str, str]:
|
|
37
|
+
"""Return the immutable headers configured for one known service."""
|
|
38
|
+
|
|
39
|
+
return self._headers[service]
|
|
40
|
+
|
|
41
|
+
def __repr__(self) -> str:
|
|
42
|
+
configured = ", ".join(
|
|
43
|
+
service.value for service, headers in self._headers.items() if headers
|
|
44
|
+
)
|
|
45
|
+
return f"SessionCredentials(configured_services=({configured}))"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _freeze_headers(headers: Mapping[str, str] | None) -> Mapping[str, str]:
|
|
49
|
+
if headers is None:
|
|
50
|
+
return MappingProxyType({})
|
|
51
|
+
if not isinstance(headers, Mapping):
|
|
52
|
+
raise TypeError("Session credential headers must be a mapping.")
|
|
53
|
+
|
|
54
|
+
frozen: dict[str, str] = {}
|
|
55
|
+
for name, value in headers.items():
|
|
56
|
+
if (
|
|
57
|
+
not isinstance(name, str)
|
|
58
|
+
or not name.strip()
|
|
59
|
+
or "\r" in name
|
|
60
|
+
or "\n" in name
|
|
61
|
+
or not isinstance(value, str)
|
|
62
|
+
or "\r" in value
|
|
63
|
+
or "\n" in value
|
|
64
|
+
):
|
|
65
|
+
raise ValueError("Session credential headers must contain valid strings.")
|
|
66
|
+
frozen[name] = value
|
|
67
|
+
return MappingProxyType(frozen)
|
woltapi/errors.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Privacy-safe exceptions raised by the Wolt client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class WoltApiError(Exception):
|
|
7
|
+
"""Base exception for this package."""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SelectionError(WoltApiError):
|
|
11
|
+
"""Caller-provided selection data cannot be safely serialized."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class UnsupportedSelectionError(SelectionError):
|
|
15
|
+
"""A catalog or selection feature is outside the supported checkout scope."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class PurchasePreparationError(WoltApiError):
|
|
19
|
+
"""A selection, quote, consent state, or purchase input is unsafe to submit."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class PurchaseAuthorizationError(WoltApiError):
|
|
23
|
+
"""Caller authorization is absent or bound to a different prepared order."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class PurchaseAttemptStoreError(WoltApiError):
|
|
27
|
+
"""Durable local purchase-attempt state could not be safely updated."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class DuplicatePurchaseAttempt(WoltApiError):
|
|
31
|
+
"""The same authorized attempt was already recorded locally."""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self, attempt_id: str, state: str, purchase_id: str | None = None
|
|
35
|
+
) -> None:
|
|
36
|
+
self.attempt_id = attempt_id
|
|
37
|
+
self.state = state
|
|
38
|
+
self.purchase_id = purchase_id
|
|
39
|
+
super().__init__(
|
|
40
|
+
"This authorized purchase attempt was already recorded locally."
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class OrderOutcomeUnknown(WoltApiError):
|
|
45
|
+
"""A purchase may have reached the server but no usable success was established."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, attempt_id: str, purchase_id: str | None = None) -> None:
|
|
48
|
+
self.attempt_id = attempt_id
|
|
49
|
+
self.purchase_id = purchase_id
|
|
50
|
+
super().__init__("Purchase outcome is unknown; no automatic retry was sent.")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class WoltTransportError(WoltApiError):
|
|
54
|
+
"""A request could not safely produce a usable response."""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class HTTPStatusError(WoltTransportError):
|
|
58
|
+
"""A service returned a non-success HTTP status."""
|
|
59
|
+
|
|
60
|
+
def __init__(self, service: str, status_code: int) -> None:
|
|
61
|
+
self.service = service
|
|
62
|
+
self.status_code = status_code
|
|
63
|
+
super().__init__(
|
|
64
|
+
f"Wolt {service} request failed with HTTP status {status_code}."
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class RequestTimeoutError(WoltTransportError):
|
|
69
|
+
"""A request timed out before a usable response arrived."""
|
|
70
|
+
|
|
71
|
+
def __init__(self, service: str) -> None:
|
|
72
|
+
self.service = service
|
|
73
|
+
super().__init__(f"Wolt {service} request timed out.")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class RequestFailedError(WoltTransportError):
|
|
77
|
+
"""A request failed before a usable response arrived."""
|
|
78
|
+
|
|
79
|
+
def __init__(self, service: str) -> None:
|
|
80
|
+
self.service = service
|
|
81
|
+
super().__init__(f"Wolt {service} request failed before a response arrived.")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class ResponseError(WoltApiError):
|
|
85
|
+
"""A service response could not be safely interpreted."""
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class ResponseDecodeError(ResponseError):
|
|
89
|
+
"""A response was not valid JSON."""
|
|
90
|
+
|
|
91
|
+
def __init__(self, service: str) -> None:
|
|
92
|
+
self.service = service
|
|
93
|
+
super().__init__(f"Wolt {service} response was not valid JSON.")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class ResponseShapeError(ResponseError):
|
|
97
|
+
"""A response was missing the expected object or collection shape."""
|
|
98
|
+
|
|
99
|
+
def __init__(self, service: str) -> None:
|
|
100
|
+
self.service = service
|
|
101
|
+
super().__init__(f"Wolt {service} response had an unexpected shape.")
|
woltapi/models.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Small public summaries for discovery operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class Venue:
|
|
10
|
+
"""A venue found in a server-driven search result."""
|
|
11
|
+
|
|
12
|
+
id: str
|
|
13
|
+
slug: str
|
|
14
|
+
title: str | None
|
|
15
|
+
currency: str | None
|
|
16
|
+
delivers: bool | None
|
|
17
|
+
online: bool | None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class DeliveryTarget:
|
|
22
|
+
"""An opaque saved delivery reference without address details."""
|
|
23
|
+
|
|
24
|
+
id: str
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class PaymentMethod:
|
|
29
|
+
"""An enabled saved-card reference without card metadata."""
|
|
30
|
+
|
|
31
|
+
id: str
|
|
32
|
+
type: str
|
|
33
|
+
is_selected: bool
|
|
34
|
+
is_default: bool
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class OrderStatus:
|
|
39
|
+
"""A minimal operational order state with an unbounded status string."""
|
|
40
|
+
|
|
41
|
+
purchase_id: str
|
|
42
|
+
status: str
|
|
43
|
+
currency: str | None
|
|
44
|
+
payment_amount: int | float | None
|
|
45
|
+
total_price: int | float | None
|
|
46
|
+
delivery_price: int | float | None
|
|
47
|
+
delivery_method: str | None
|