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/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