python-mope 0.3.0__py3-none-any.whl → 0.3.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.
- mope/__version__.py +1 -1
- mope/client.py +35 -5
- mope/errors.py +52 -0
- mope/resources/shop.py +3 -2
- {python_mope-0.3.0.dist-info → python_mope-0.3.1.dist-info}/METADATA +23 -1
- python_mope-0.3.1.dist-info/RECORD +16 -0
- python_mope-0.3.0.dist-info/RECORD +0 -15
- {python_mope-0.3.0.dist-info → python_mope-0.3.1.dist-info}/WHEEL +0 -0
- {python_mope-0.3.0.dist-info → python_mope-0.3.1.dist-info}/licenses/LICENSE +0 -0
- {python_mope-0.3.0.dist-info → python_mope-0.3.1.dist-info}/top_level.txt +0 -0
mope/__version__.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
__author__ = "lemuel@vokality.com"
|
|
2
|
-
__version__ = "0.3.
|
|
2
|
+
__version__ = "0.3.1"
|
mope/client.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from collections.abc import Iterable, Mapping, Set
|
|
2
|
+
from math import isfinite
|
|
2
3
|
from typing import IO, Protocol, TypeAlias
|
|
3
4
|
from urllib.parse import urljoin
|
|
4
5
|
|
|
@@ -29,19 +30,45 @@ RequestData: TypeAlias = (
|
|
|
29
30
|
| str
|
|
30
31
|
| bytes
|
|
31
32
|
)
|
|
33
|
+
RequestTimeout: TypeAlias = float | tuple[float, float]
|
|
32
34
|
|
|
33
35
|
_user_agent = f"Python Mope {__version__}"
|
|
36
|
+
_BASE_URL = "https://api.mope.sr/api/"
|
|
37
|
+
_DEFAULT_TIMEOUT = (5.0, 30.0)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _timeout_seconds(value: object) -> float:
|
|
41
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
42
|
+
raise TypeError("timeout must contain numbers of seconds")
|
|
43
|
+
try:
|
|
44
|
+
seconds = float(value)
|
|
45
|
+
except OverflowError as error:
|
|
46
|
+
raise ValueError("timeout must be finite and greater than zero") from error
|
|
47
|
+
if not isfinite(seconds) or seconds <= 0:
|
|
48
|
+
raise ValueError("timeout must be finite and greater than zero")
|
|
49
|
+
return seconds
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _request_timeout(value: object) -> RequestTimeout:
|
|
53
|
+
if isinstance(value, tuple):
|
|
54
|
+
if len(value) != 2:
|
|
55
|
+
raise ValueError("timeout must be a (connect, read) pair")
|
|
56
|
+
return (_timeout_seconds(value[0]), _timeout_seconds(value[1]))
|
|
57
|
+
return _timeout_seconds(value)
|
|
34
58
|
|
|
35
59
|
|
|
36
60
|
class Client:
|
|
37
|
-
def __init__(
|
|
61
|
+
def __init__(
|
|
62
|
+
self, token: str, *, timeout: RequestTimeout = _DEFAULT_TIMEOUT
|
|
63
|
+
) -> None:
|
|
64
|
+
self._timeout = _request_timeout(timeout)
|
|
38
65
|
self._token = token
|
|
39
66
|
self._headers = {
|
|
40
67
|
"User-Agent": _user_agent,
|
|
41
68
|
"Content-Type": "application/json",
|
|
42
69
|
"Authorization": f"Bearer {token}",
|
|
43
70
|
}
|
|
44
|
-
self._base_url =
|
|
71
|
+
self._base_url = _BASE_URL
|
|
45
72
|
|
|
46
73
|
def call_api(
|
|
47
74
|
self,
|
|
@@ -59,20 +86,23 @@ class Client:
|
|
|
59
86
|
data=data,
|
|
60
87
|
json=json_value(json, path="request"),
|
|
61
88
|
headers=self._headers,
|
|
89
|
+
timeout=self._timeout,
|
|
62
90
|
)
|
|
63
91
|
|
|
64
92
|
|
|
65
93
|
class Mope:
|
|
66
|
-
def __init__(
|
|
94
|
+
def __init__(
|
|
95
|
+
self, token: str, *, timeout: RequestTimeout = _DEFAULT_TIMEOUT
|
|
96
|
+
) -> None:
|
|
67
97
|
self._token = token
|
|
68
98
|
self._headers = {
|
|
69
99
|
"User-Agent": _user_agent,
|
|
70
100
|
"Content-Type": "application/json",
|
|
71
101
|
"Authorization": f"Bearer {token}",
|
|
72
102
|
}
|
|
73
|
-
self.base_url =
|
|
103
|
+
self.base_url = _BASE_URL
|
|
74
104
|
|
|
75
|
-
self._client = Client(token)
|
|
105
|
+
self._client = Client(token, timeout=timeout)
|
|
76
106
|
|
|
77
107
|
self.shop = ShopResource(self._client)
|
|
78
108
|
|
mope/errors.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Typed details for HTTP errors returned by the Mopé API."""
|
|
2
|
+
|
|
3
|
+
from typing import Self
|
|
4
|
+
|
|
5
|
+
from requests import HTTPError, Response
|
|
6
|
+
|
|
7
|
+
from mope.json import JsonValue, json_integer, json_object, json_string
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MopeAPIError(HTTPError):
|
|
11
|
+
"""An HTTP rejection, with validated Mopé error details when available."""
|
|
12
|
+
|
|
13
|
+
response: Response | None
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
message: str,
|
|
18
|
+
*,
|
|
19
|
+
response: Response | None = None,
|
|
20
|
+
code: int | None = None,
|
|
21
|
+
data: JsonValue = None,
|
|
22
|
+
) -> None:
|
|
23
|
+
super().__init__(message, response=response)
|
|
24
|
+
self.message = message
|
|
25
|
+
self.code = code
|
|
26
|
+
self.data = data
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def status_code(self) -> int | None:
|
|
30
|
+
return self.response.status_code if self.response is not None else None
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def from_response(cls, response: Response, *, fallback_message: str) -> Self:
|
|
34
|
+
try:
|
|
35
|
+
payload = json_object(response.json())
|
|
36
|
+
# The schema wraps details in "error"; documented examples are unwrapped.
|
|
37
|
+
details = json_object(payload.get("error", payload), path="error")
|
|
38
|
+
code = json_integer(details["code"], path="error.code")
|
|
39
|
+
message = json_string(details["message"], path="error.message")
|
|
40
|
+
except (TypeError, ValueError, KeyError):
|
|
41
|
+
return cls(fallback_message, response=response)
|
|
42
|
+
return cls(message, response=response, code=code, data=details.get("data"))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def raise_for_status(response: Response) -> None:
|
|
46
|
+
"""Preserve HTTPError compatibility without letting a bad error body mask it."""
|
|
47
|
+
try:
|
|
48
|
+
response.raise_for_status()
|
|
49
|
+
except HTTPError as error:
|
|
50
|
+
raise MopeAPIError.from_response(
|
|
51
|
+
response, fallback_message=str(error)
|
|
52
|
+
) from error
|
mope/resources/shop.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from mope.errors import raise_for_status
|
|
1
2
|
from mope.models.payments import CreatePaymentResponse, CurrencyCode, PaymentRequest
|
|
2
3
|
from mope.transport import ApiTransport
|
|
3
4
|
|
|
@@ -30,7 +31,7 @@ class ShopResource:
|
|
|
30
31
|
"redirect_url": redirect_url,
|
|
31
32
|
},
|
|
32
33
|
)
|
|
33
|
-
|
|
34
|
+
raise_for_status(response)
|
|
34
35
|
return CreatePaymentResponse.from_json(response.json())
|
|
35
36
|
|
|
36
37
|
def get_payment_request(self, payment_id: str) -> PaymentRequest:
|
|
@@ -40,5 +41,5 @@ class ShopResource:
|
|
|
40
41
|
|
|
41
42
|
endpoint = f"shop/payment_request/{payment_id}"
|
|
42
43
|
response = self._client.call_api("GET", endpoint=endpoint)
|
|
43
|
-
|
|
44
|
+
raise_for_status(response)
|
|
44
45
|
return PaymentRequest.from_json(response.json())
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-mope
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: A client library for interacting with the Mopé Web Shop API
|
|
5
5
|
Author-email: Lemuel Boyce <lemuel@vokality.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -52,6 +52,28 @@ mope = Mope(token="test_123")
|
|
|
52
52
|
mope.shop.get_payment_request(payment_id="123")
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
+
### Timeouts and errors
|
|
56
|
+
|
|
57
|
+
Requests use a 5-second connect timeout and a 30-second read timeout by default.
|
|
58
|
+
Configure both with `Mope(token="test_123", timeout=(3.0, 15.0))`, or pass a
|
|
59
|
+
single number of seconds to apply to both. `mope.client.Client` accepts the same
|
|
60
|
+
keyword argument. Values must be finite and greater than zero. The read timeout
|
|
61
|
+
limits inactivity between received bytes, not the total duration of a request.
|
|
62
|
+
|
|
63
|
+
Shop methods raise `mope.errors.MopeAPIError` for HTTP errors. It is a subclass
|
|
64
|
+
of `requests.HTTPError`, so existing handlers keep working. Its `status_code`,
|
|
65
|
+
`code`, `message`, and `data` fields expose the HTTP status and validated Mopé
|
|
66
|
+
error details, such as code `1033` for account limits exceeded. If the error body
|
|
67
|
+
is missing or malformed, `code` and `data` are `None` and `message` retains the
|
|
68
|
+
HTTP error description. The original response and exception cause are preserved.
|
|
69
|
+
|
|
70
|
+
Connection failures and timeouts remain `requests.RequestException` subclasses.
|
|
71
|
+
Malformed successful responses still raise `TypeError`, `ValueError`, or
|
|
72
|
+
`KeyError`; invalid JSON raises `requests.exceptions.JSONDecodeError`, which is
|
|
73
|
+
also a `ValueError`. Applications should translate these errors at the API call
|
|
74
|
+
boundary and decide which failures are retryable. Requests are not automatically
|
|
75
|
+
retried, including payment creation after a timeout.
|
|
76
|
+
|
|
55
77
|
### Development
|
|
56
78
|
|
|
57
79
|
Install [uv](https://docs.astral.sh/uv/getting-started/installation/), then run:
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
mope/__init__.py,sha256=3nuq9PuAWTDZz6Ejg_737iQ5JRSC44uW_n5NPdHUscU,49
|
|
2
|
+
mope/__version__.py,sha256=nPR1XtJJZHIxfooRzFILzYZoEY8yB6SRqip56TzwbZQ,57
|
|
3
|
+
mope/client.py,sha256=gzVm5bwrXC1E8EzujzXys_yekAEF19HNhDAUY9nuYs4,3197
|
|
4
|
+
mope/errors.py,sha256=Tw3jJIZLbVchjyklLZrTCvizQhqqWiN9DRXufXiPTg8,1798
|
|
5
|
+
mope/json.py,sha256=kj03cwke1AXfd3LjFygapfYeopavF_e6YuasRMLamRE,1625
|
|
6
|
+
mope/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
mope/transport.py,sha256=AuN6SaKZG13d8ujVNOzMrSNMttH3P6Gl6x9hfznfoew,255
|
|
8
|
+
mope/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
mope/models/payments.py,sha256=oKovsudX6OvaTib4ya-GikuMDwvQVkDB-0pvSQyF-6I,3204
|
|
10
|
+
mope/resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
mope/resources/shop.py,sha256=yCkzIkE9d01W87-3RZQQ1OphVoWje79BLKPQ7GMNgTM,1379
|
|
12
|
+
python_mope-0.3.1.dist-info/licenses/LICENSE,sha256=H1HkxgxLo4v8voMaHpZVRh5Ao8jUezeLzSHpp182his,1069
|
|
13
|
+
python_mope-0.3.1.dist-info/METADATA,sha256=xpxImo4Eg_-rem1cADBj_YyFY3MhCT6OLV_iAZKv7HY,5401
|
|
14
|
+
python_mope-0.3.1.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
15
|
+
python_mope-0.3.1.dist-info/top_level.txt,sha256=KNJibHVOvmp47d34dVOoxczy3_oafEiKZ4szVTOW6AA,5
|
|
16
|
+
python_mope-0.3.1.dist-info/RECORD,,
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
mope/__init__.py,sha256=3nuq9PuAWTDZz6Ejg_737iQ5JRSC44uW_n5NPdHUscU,49
|
|
2
|
-
mope/__version__.py,sha256=m1lfA5cU5lKTRV_7jofTAXIaBSOm8FGd8VLnnfV4jug,57
|
|
3
|
-
mope/client.py,sha256=WK8IPinJu84GsxsDC3vDzGB3c8wcjFEisMtaCq8IID8,2074
|
|
4
|
-
mope/json.py,sha256=kj03cwke1AXfd3LjFygapfYeopavF_e6YuasRMLamRE,1625
|
|
5
|
-
mope/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
-
mope/transport.py,sha256=AuN6SaKZG13d8ujVNOzMrSNMttH3P6Gl6x9hfznfoew,255
|
|
7
|
-
mope/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
-
mope/models/payments.py,sha256=oKovsudX6OvaTib4ya-GikuMDwvQVkDB-0pvSQyF-6I,3204
|
|
9
|
-
mope/resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
-
mope/resources/shop.py,sha256=qQfDCbOOFJmIbii6bqpUOlg4npjUiJFqEd7nLeKu4Hc,1340
|
|
11
|
-
python_mope-0.3.0.dist-info/licenses/LICENSE,sha256=H1HkxgxLo4v8voMaHpZVRh5Ao8jUezeLzSHpp182his,1069
|
|
12
|
-
python_mope-0.3.0.dist-info/METADATA,sha256=JPw6Ao0bgzNLk3sPKznoSw3nK_G9VjKq8_Vuhf7TOLc,4046
|
|
13
|
-
python_mope-0.3.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
14
|
-
python_mope-0.3.0.dist-info/top_level.txt,sha256=KNJibHVOvmp47d34dVOoxczy3_oafEiKZ4szVTOW6AA,5
|
|
15
|
-
python_mope-0.3.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|