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 CHANGED
@@ -1,2 +1,2 @@
1
1
  __author__ = "lemuel@vokality.com"
2
- __version__ = "0.3.0"
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__(self, token: str) -> None:
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 = "https://api.mope.sr/api"
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__(self, token: str) -> None:
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 = "https://api.mope.sr/api"
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
- response.raise_for_status()
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
- response.raise_for_status()
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.0
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,,