python-myanmar-payments 4.0.0a1__tar.gz → 4.0.0a2__tar.gz

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.
Files changed (44) hide show
  1. {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/CHANGELOG.md +6 -3
  2. {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/PKG-INFO +1 -1
  3. {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/skills/python-myanmar-payments/SKILL.md +6 -6
  4. {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/src/python_myanmar_payments/__init__.py +0 -2
  5. {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/src/python_myanmar_payments/_errors.py +12 -4
  6. {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/src/python_myanmar_payments/_facade.py +13 -30
  7. {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/src/python_myanmar_payments/_http.py +9 -11
  8. {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/src/python_myanmar_payments/_support.py +16 -23
  9. python_myanmar_payments-4.0.0a2/src/python_myanmar_payments/_version.py +1 -0
  10. {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/src/python_myanmar_payments/aya_pay.py +18 -25
  11. {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/src/python_myanmar_payments/cyber_source.py +22 -32
  12. {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/src/python_myanmar_payments/kbz_pay.py +17 -32
  13. {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/src/python_myanmar_payments/wave_money.py +24 -38
  14. {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/src/python_myanmar_payments/yoma_mmqr.py +21 -30
  15. {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/tests/fixtures/parity/vectors.json +454 -55
  16. {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/tests/test_aya_pay.py +7 -7
  17. {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/tests/test_core.py +29 -31
  18. {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/tests/test_cyber_source.py +22 -5
  19. {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/tests/test_kbz_pay.py +22 -13
  20. {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/tests/test_myanmar_payments.py +36 -11
  21. {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/tests/test_parity.py +67 -69
  22. {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/tests/test_wave_money.py +32 -26
  23. {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/tests/test_yoma_mmqr.py +16 -11
  24. python_myanmar_payments-4.0.0a1/src/python_myanmar_payments/_version.py +0 -1
  25. {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/.gitignore +0 -0
  26. {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/LICENSE.md +0 -0
  27. {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/README.md +0 -0
  28. {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/pyproject.toml +0 -0
  29. {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/src/python_myanmar_payments/_amount.py +0 -0
  30. {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/src/python_myanmar_payments/_cache.py +0 -0
  31. {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/src/python_myanmar_payments/_callback.py +0 -0
  32. {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/src/python_myanmar_payments/_json.py +0 -0
  33. {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/src/python_myanmar_payments/_results.py +0 -0
  34. {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/src/python_myanmar_payments/_status.py +0 -0
  35. {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/src/python_myanmar_payments/_validate.py +0 -0
  36. {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/src/python_myanmar_payments/_values.py +0 -0
  37. {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/src/python_myanmar_payments/py.typed +0 -0
  38. {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/tests/__init__.py +0 -0
  39. {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/tests/conftest.py +0 -0
  40. {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/tests/fixtures/aya_pay/callback_payload.json +0 -0
  41. {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/tests/fixtures/kbz_pay/sign_string.json +0 -0
  42. {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/tests/fixtures/wave_money/callback.json +0 -0
  43. {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/tests/helpers.py +0 -0
  44. {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/tests/test_amount.py +0 -0
@@ -9,9 +9,9 @@ Initial release. The version number matches the other Laranex Myanmar payments p
9
9
  ### Added
10
10
  - Gateways: KBZ Pay (`pwa`, `qr`, `app`, `status`), Wave Money (`initiate`), AYA Payment Gateway (`services`, `initiate`, `status`, `verify_redirect`), Yoma MMQR (`initiate`, `renew_qr`, `status`, `forget_token`) and CyberSource Secure Acceptance (`initiate`); every gateway validates its payment data (`KbzPay.validate()` and so on) and verifies callbacks with `handle_callback`.
11
11
  - A sync and an async client for every gateway that calls an API (`KbzPay` / `AsyncKbzPay`, `WaveMoney` / `AsyncWaveMoney`, `AyaPay` / `AsyncAyaPay`, `YomaMmqr` / `AsyncYomaMmqr`), sharing one implementation of signing, validation and response parsing. `CyberSource` makes no network calls and serves both.
12
- - `httpx` is the only runtime dependency: pass your own `httpx.Client` / `httpx.AsyncClient` as `http_client`, or let the gateway create one with a 30 second `timeout` and close it with `close()` / `aclose()` or a `with` / `async with` block.
12
+ - `httpx` is the only runtime dependency: pass your own `httpx.Client` / `httpx.AsyncClient` as `http_client`, or let the gateway create one with the config's `timeout_seconds` and close it with `close()` / `aclose()` or a `with` / `async with` block.
13
13
  - Exact `Amount` type (`Amount.kyat()`, `Amount.parse()`, `Amount.of()`) kept as decimal text, so amounts never pass through a float; payment data also takes a whole `int`, decimal text or a `Decimal`, and rejects floats. Decimals are accepted only where the gateway documents them (KBZ Pay up to 2 places, CyberSource any); every gateway except CyberSource is MMK only.
14
- - Typed payment data dataclasses (`KbzPayPaymentData`, `WaveMoneyPaymentData` with `WaveMoneyItem`, `AyaPayPaymentData`, `YomaMmqrPaymentData`, `CyberSourcePaymentData`) and a config class per gateway with `from_env()` (default `os.environ`), reading the same `*_SANDBOX`, credential and URL variables as the PHP, Laravel, Go and Node packages; a missing credential raises `ConfigurationError`. `sandbox` also takes text read like `*_SANDBOX` (`false`, `0`, `f`, `no` or `off` select production). Gateways, `MyanmarPayments` and `AsyncMyanmarPayments` take config objects or mappings of their keyword arguments; the facades build every gateway lazily, also from the environment.
14
+ - Typed payment data dataclasses (`KbzPayPaymentData`, `WaveMoneyPaymentData` with `WaveMoneyItem`, `AyaPayPaymentData`, `YomaMmqrPaymentData`, `CyberSourcePaymentData`) and a config class per gateway with `from_env()` (default `os.environ`), reading the same credential, setting and URL variables as the PHP, Laravel, Go and Node packages; a missing setting raises `ConfigurationError`. Gateways, `MyanmarPayments` and `AsyncMyanmarPayments` take config objects or mappings of their keyword arguments; the facades build every gateway lazily, also from the environment.
15
15
  - Result classes: `RedirectPayment`, `FormPayment` (with `to_html()`), `QrPayment` (with `qr_image_data_uri()`) and `AppPayment` (with `to_dict()`); `PaymentCallback` (with a gateway-independent `PaymentStatus` and the `Acknowledgement` each gateway expects, in its `acknowledgement` attribute) and `PaymentStatusResult`.
16
16
  - `CallbackRequest(body=..., headers=..., query=...)` takes the raw body bytes, headers and query string from Django, Flask, FastAPI or any other framework; `CallbackRequest.from_json()` replays a stored callback. JSON numbers in callbacks are read without a float conversion, so signatures and amounts keep the gateway's exact text, and `raw` (like `parsed_body()`, `input()` and error `raw`) holds every JSON number as its exact text, e.g. `"1000.50"`.
17
17
  - Errors: `PaymentError` and its subclasses `InvalidPaymentDataError` (per-field `errors`), `ApiError` (`gateway_code`, `gateway_message`, `http_status`, `raw`, network errors chained as `__cause__`), `SignatureVerificationError` and `ConfigurationError`.
@@ -22,7 +22,10 @@ Initial release. The version number matches the other Laranex Myanmar payments p
22
22
  - AYA Pay reads a `payload` whose `+` signs became spaces in an unencoded return query string, with or without base64 padding; partial padding, other alphabets and payloads that are not UTF-8 are rejected.
23
23
  - Yoma MMQR caches its token under `myanmar-payments.yoma-mmqr.token.<sha256 of base URL|client id>`, the same key as the PHP, Go and Node SDKs, so services in different languages can share one cache.
24
24
  - Callback, return and cancel URLs only need to be valid absolute http or https URLs; there is no HTTPS-only rule (gateways may still require HTTPS in production).
25
- - Wave Money's sandbox is `https://preprodpayments.wavemoney.io:8107`, with checkout at `https://preprodpayments.wavemoney.io/authenticate`.
26
25
  - Fully typed (`py.typed`, `mypy --strict`).
27
26
  - Agent skill in `skills/python-myanmar-payments` so coding agents use the package correctly; install it with `npx skills add laranex/python-myanmar-payments`.
28
27
  - Requires Python 3.10 or higher.
28
+
29
+ ### Changed since the pre-releases
30
+
31
+ - No sandbox switch: the sandbox setting (`sandbox`) and the `*_SANDBOX` variables are removed, and every gateway defaults to its production endpoints. Every other setting is required: the HTTP timeout (`timeout_seconds`, error key `timeout_in_seconds`, `MYANMAR_PAYMENTS_HTTP_TIMEOUT`) for every gateway that calls an API, Wave Money's `time_to_live_seconds` (error key `time_to_live_in_seconds`) and `merchant_name` (no `APP_NAME` fallback), Yoma MMQR's `api_version`, and CyberSource's `currency`, `locale` and `transaction_type` on each payment. A missing setting raises `ConfigurationError` naming it; a time setting that is not a whole number greater than 0 raises `The <gateway> configuration [<key>] must be a whole number greater than 0.` (`ConfigurationError(gateway, key, invalid=True)`). The `timeout` keyword argument of the gateways and facades, `DEFAULT_TIMEOUT` and the `SANDBOX_*` URL constants are removed. Upgrading: delete `*_SANDBOX`; to keep testing against UAT, set the URL overrides to the UAT URLs listed in the docs; set the newly required settings.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: python-myanmar-payments
3
- Version: 4.0.0a1
3
+ Version: 4.0.0a2
4
4
  Summary: Python SDK for Myanmar payment gateways: KBZ Pay, Wave Money, AYA Pay, Yoma MMQR and CyberSource. Typed requests and results, exact amounts, sync and async clients.
5
5
  Project-URL: Homepage, https://laranex.vercel.app/python-myanmar-payments
6
6
  Project-URL: Documentation, https://laranex.vercel.app/python-myanmar-payments
@@ -23,7 +23,7 @@ Requires Python 3.10+ and depends only on `httpx`. Import everything from `pytho
23
23
 
24
24
  ## Configure
25
25
 
26
- `MyanmarPayments.from_env()` reads `KBZ_PAY_*`, `WAVE_MONEY_*`, `AYA_PAY_*` (or `AYA_PGW_*`), `YOMA_MMQR_*` and `CYBER_SOURCE_*` from `os.environ` (the same variables as the PHP, Go and Node SDKs). `sandbox` defaults to `True`; set `*_SANDBOX=false` (or `sandbox=False`) in production.
26
+ `MyanmarPayments.from_env()` reads `KBZ_PAY_*`, `WAVE_MONEY_*`, `AYA_PAY_*` (or `AYA_PGW_*`), `YOMA_MMQR_*` and `CYBER_SOURCE_*` plus `MYANMAR_PAYMENTS_HTTP_TIMEOUT` from `os.environ` (the same variables as the PHP, Go and Node SDKs). Every setting except the URL overrides and Yoma's `YOMA_MMQR_WEBHOOK_SECRET` is required, including the HTTP timeout (`MYANMAR_PAYMENTS_HTTP_TIMEOUT`, seconds), `WAVE_MONEY_MERCHANT_NAME`, `WAVE_MONEY_TIME_TO_LIVE_IN_SECONDS` and `YOMA_MMQR_API_VERSION`. The URLs default to each gateway's production endpoints; to test against UAT, set the URL overrides (`KBZ_PAY_BASE_URL`, `KBZ_PAY_PWA_BASE_REDIRECT_URL`, `WAVE_MONEY_BASE_URL`, `WAVE_MONEY_AUTHENTICATE_URL`, `AYA_PAY_BASE_URL`, `YOMA_MMQR_BASE_URL`, `CYBER_SOURCE_BASE_URL`) to the UAT URLs, together with UAT credentials.
27
27
 
28
28
  ```python
29
29
  from python_myanmar_payments import MyanmarPayments
@@ -32,10 +32,10 @@ payments = MyanmarPayments.from_env() # create once, share across requests
32
32
  kbz = payments.kbz_pay() # also wave_money(), aya_pay(), yoma_mmqr(), cyber_source()
33
33
  ```
34
34
 
35
- - Or build one gateway: `KbzPay(KbzPayConfig(app_id=..., app_key=..., merchant_code=...))`, `KbzPay.from_env()` or `KbzPay(KbzPayConfig.from_env())`; in async code use `AsyncMyanmarPayments.from_env()` and `AsyncKbzPay`.
36
- - Or pass the settings directly: gateways and `MyanmarPayments(kbz_pay={"app_id": ..., "app_key": ..., "merchant_code": ...})` take config objects or mappings of their keyword arguments; a string `sandbox` such as `"false"` is read like the variable.
37
- - Options (keyword arguments): `http_client` (your own `httpx.Client` / `httpx.AsyncClient`) and `timeout` (seconds, default 30); close the clients the package created with `payments.close()` / `await payments.aclose()` or a `with` block. Yoma and the facades also take `token_cache` (any `TokenCache`, default `MemoryTokenCache`; back it with Redis when you run several processes).
38
- - A missing credential raises `ConfigurationError` (`gateway`, `key`).
35
+ - Or build one gateway: `KbzPay(KbzPayConfig(app_id=..., app_key=..., merchant_code=..., timeout_seconds=30))`, `KbzPay.from_env()` or `KbzPay(KbzPayConfig.from_env())`; in async code use `AsyncMyanmarPayments.from_env()` and `AsyncKbzPay`.
36
+ - Or pass the settings directly: gateways and `MyanmarPayments(kbz_pay={"app_id": ..., "app_key": ..., "merchant_code": ..., "timeout_seconds": 30})` take config objects or mappings of their keyword arguments; `timeout_seconds` and Wave's `time_to_live_seconds` take an `int` or integer text.
37
+ - Options (keyword arguments): `http_client` (your own `httpx.Client` / `httpx.AsyncClient`) (it keeps its own timeout; without one the gateway creates a client with the config's `timeout_seconds`); close the clients the package created with `payments.close()` / `await payments.aclose()` or a `with` block. Yoma and the facades also take `token_cache` (any `TokenCache`, default `MemoryTokenCache`; back it with Redis when you run several processes).
38
+ - A missing setting raises `ConfigurationError` (`gateway`, `key`), and a time setting that is not a whole number greater than 0 raises it too.
39
39
 
40
40
  ## Use
41
41
 
@@ -45,7 +45,7 @@ Amounts are `Amount.kyat(1000)`, `Amount.parse("1000.50")`, a whole `int`, decim
45
45
 
46
46
  ### Start a payment
47
47
 
48
- Each gateway takes a data object (`KbzPayPaymentData`, `WaveMoneyPaymentData` with `WaveMoneyItem`s, `AyaPayPaymentData` with an `AyaPayMethod`, `YomaMmqrPaymentData`, `CyberSourcePaymentData` with a `CyberSourceTransactionType`) and returns a typed result:
48
+ Each gateway takes a data object (`KbzPayPaymentData`, `WaveMoneyPaymentData` with `WaveMoneyItem`s, `AyaPayPaymentData` with an `AyaPayMethod`, `YomaMmqrPaymentData`, `CyberSourcePaymentData` with a `CyberSourceTransactionType`) and returns a typed result. `CyberSourcePaymentData` has no defaults: set `currency` (e.g. `"MMK"`), `locale` (e.g. `"en-us"`) and `transaction_type` (e.g. `CyberSourceTransactionType.SALE`).
49
49
 
50
50
  ```python
51
51
  from python_myanmar_payments import Amount, KbzPayPaymentData
@@ -15,7 +15,6 @@ from ._errors import (
15
15
  SignatureVerificationError,
16
16
  )
17
17
  from ._facade import AsyncMyanmarPayments, MyanmarPayments
18
- from ._http import DEFAULT_TIMEOUT
19
18
  from ._results import (
20
19
  Acknowledgement,
21
20
  AppPayment,
@@ -54,7 +53,6 @@ from .wave_money import (
54
53
  from .yoma_mmqr import AsyncYomaMmqr, YomaMmqr, YomaMmqrConfig, YomaMmqrPaymentData
55
54
 
56
55
  __all__ = [
57
- "DEFAULT_TIMEOUT",
58
56
  "Acknowledgement",
59
57
  "Amount",
60
58
  "AmountInput",
@@ -81,14 +81,22 @@ class SignatureVerificationError(PaymentError):
81
81
 
82
82
 
83
83
  class ConfigurationError(PaymentError):
84
- """Raised when a gateway is missing a credential or setting it needs."""
84
+ """Raised when a gateway is missing a credential or setting it needs.
85
+
86
+ ``invalid`` reports a time setting that is set but not a whole number greater
87
+ than 0.
88
+ """
85
89
 
86
90
  gateway: str
87
91
  """The gateway, e.g. ``kbz_pay``."""
88
92
  key: str
89
- """The missing setting, e.g. ``app_key``."""
93
+ """The missing or invalid setting, e.g. ``app_key``."""
90
94
 
91
- def __init__(self, gateway: str, key: str) -> None:
92
- super().__init__(f"The {gateway} configuration is missing [{key}].")
95
+ def __init__(self, gateway: str, key: str, invalid: bool = False) -> None:
96
+ if invalid:
97
+ message = f"The {gateway} configuration [{key}] must be a whole number greater than 0."
98
+ else:
99
+ message = f"The {gateway} configuration is missing [{key}]."
100
+ super().__init__(message)
93
101
  self.gateway = gateway
94
102
  self.key = key
@@ -10,7 +10,6 @@ import httpx
10
10
 
11
11
  from ._cache import AsyncTokenCache, MemoryTokenCache, TokenCache
12
12
  from ._errors import ConfigurationError
13
- from ._http import DEFAULT_TIMEOUT
14
13
  from ._support import EnvSource, config_of
15
14
  from .aya_pay import AsyncAyaPay, AyaPay, AyaPayConfig
16
15
  from .cyber_source import CyberSource, CyberSourceConfig
@@ -43,14 +42,12 @@ class _Payments:
43
42
  aya_pay: AyaPayConfig | Mapping[str, Any] | None,
44
43
  yoma_mmqr: YomaMmqrConfig | Mapping[str, Any] | None,
45
44
  cyber_source: CyberSourceConfig | Mapping[str, Any] | None,
46
- timeout: float | None,
47
45
  ) -> None:
48
46
  self._kbz_pay_config = _lazy(KbzPayConfig, kbz_pay)
49
47
  self._wave_money_config = _lazy(WaveMoneyConfig, wave_money)
50
48
  self._aya_pay_config = _lazy(AyaPayConfig, aya_pay)
51
49
  self._yoma_mmqr_config = _lazy(YomaMmqrConfig, yoma_mmqr)
52
50
  self._cyber_source_config = _lazy(CyberSourceConfig, cyber_source)
53
- self._timeout = timeout
54
51
  self._cyber_source: CyberSource | None = None
55
52
 
56
53
  def _read_env(self, env: EnvSource | None) -> None:
@@ -86,9 +83,8 @@ class MyanmarPayments(_Payments):
86
83
  cyber_source: CyberSourceConfig | Mapping[str, Any] | None = None,
87
84
  token_cache: TokenCache | None = None,
88
85
  http_client: httpx.Client | None = None,
89
- timeout: float | None = DEFAULT_TIMEOUT,
90
86
  ) -> None:
91
- super().__init__(kbz_pay, wave_money, aya_pay, yoma_mmqr, cyber_source, timeout)
87
+ super().__init__(kbz_pay, wave_money, aya_pay, yoma_mmqr, cyber_source)
92
88
  self._http_client = http_client
93
89
  self._token_cache: TokenCache = (
94
90
  token_cache if token_cache is not None else MemoryTokenCache()
@@ -105,15 +101,14 @@ class MyanmarPayments(_Payments):
105
101
  *,
106
102
  token_cache: TokenCache | None = None,
107
103
  http_client: httpx.Client | None = None,
108
- timeout: float | None = DEFAULT_TIMEOUT,
109
104
  ) -> MyanmarPayments:
110
105
  """Reads every gateway's configuration from environment variables.
111
106
 
112
- ``KBZ_PAY_*``, ``WAVE_MONEY_*``, ``AYA_PAY_*``, ``YOMA_MMQR_*`` and
113
- ``CYBER_SOURCE_*`` are read when the gateway is first used. Defaults to
114
- ``os.environ``.
107
+ ``KBZ_PAY_*``, ``WAVE_MONEY_*``, ``AYA_PAY_*``, ``YOMA_MMQR_*``,
108
+ ``CYBER_SOURCE_*`` and ``MYANMAR_PAYMENTS_HTTP_TIMEOUT`` are read when the
109
+ gateway is first used. Defaults to ``os.environ``.
115
110
  """
116
- payments = cls(token_cache=token_cache, http_client=http_client, timeout=timeout)
111
+ payments = cls(token_cache=token_cache, http_client=http_client)
117
112
  payments._read_env(env)
118
113
  return payments
119
114
 
@@ -121,23 +116,21 @@ class MyanmarPayments(_Payments):
121
116
  """The KBZ Pay gateway. Raises a ``ConfigurationError`` when not configured."""
122
117
  if self._kbz_pay is None:
123
118
  config = _resolve(self._kbz_pay_config, "kbz_pay", "app_id")
124
- self._kbz_pay = KbzPay(config, http_client=self._http_client, timeout=self._timeout)
119
+ self._kbz_pay = KbzPay(config, http_client=self._http_client)
125
120
  return self._kbz_pay
126
121
 
127
122
  def wave_money(self) -> WaveMoney:
128
123
  """The Wave Money gateway. Raises a ``ConfigurationError`` when not configured."""
129
124
  if self._wave_money is None:
130
125
  config = _resolve(self._wave_money_config, "wave_money", "merchant_id")
131
- self._wave_money = WaveMoney(
132
- config, http_client=self._http_client, timeout=self._timeout
133
- )
126
+ self._wave_money = WaveMoney(config, http_client=self._http_client)
134
127
  return self._wave_money
135
128
 
136
129
  def aya_pay(self) -> AyaPay:
137
130
  """The AYA Payment Gateway. Raises a ``ConfigurationError`` when not configured."""
138
131
  if self._aya_pay is None:
139
132
  config = _resolve(self._aya_pay_config, "aya_pay", "app_key")
140
- self._aya_pay = AyaPay(config, http_client=self._http_client, timeout=self._timeout)
133
+ self._aya_pay = AyaPay(config, http_client=self._http_client)
141
134
  return self._aya_pay
142
135
 
143
136
  def yoma_mmqr(self) -> YomaMmqr:
@@ -148,7 +141,6 @@ class MyanmarPayments(_Payments):
148
141
  config,
149
142
  token_cache=self._token_cache,
150
143
  http_client=self._http_client,
151
- timeout=self._timeout,
152
144
  )
153
145
  return self._yoma_mmqr
154
146
 
@@ -188,9 +180,8 @@ class AsyncMyanmarPayments(_Payments):
188
180
  cyber_source: CyberSourceConfig | Mapping[str, Any] | None = None,
189
181
  token_cache: TokenCache | AsyncTokenCache | None = None,
190
182
  http_client: httpx.AsyncClient | None = None,
191
- timeout: float | None = DEFAULT_TIMEOUT,
192
183
  ) -> None:
193
- super().__init__(kbz_pay, wave_money, aya_pay, yoma_mmqr, cyber_source, timeout)
184
+ super().__init__(kbz_pay, wave_money, aya_pay, yoma_mmqr, cyber_source)
194
185
  self._http_client = http_client
195
186
  self._token_cache: TokenCache | AsyncTokenCache = (
196
187
  token_cache if token_cache is not None else MemoryTokenCache()
@@ -207,10 +198,9 @@ class AsyncMyanmarPayments(_Payments):
207
198
  *,
208
199
  token_cache: TokenCache | AsyncTokenCache | None = None,
209
200
  http_client: httpx.AsyncClient | None = None,
210
- timeout: float | None = DEFAULT_TIMEOUT,
211
201
  ) -> AsyncMyanmarPayments:
212
202
  """Reads every gateway's configuration from environment variables on first use."""
213
- payments = cls(token_cache=token_cache, http_client=http_client, timeout=timeout)
203
+ payments = cls(token_cache=token_cache, http_client=http_client)
214
204
  payments._read_env(env)
215
205
  return payments
216
206
 
@@ -218,27 +208,21 @@ class AsyncMyanmarPayments(_Payments):
218
208
  """The KBZ Pay gateway. Raises a ``ConfigurationError`` when not configured."""
219
209
  if self._kbz_pay is None:
220
210
  config = _resolve(self._kbz_pay_config, "kbz_pay", "app_id")
221
- self._kbz_pay = AsyncKbzPay(
222
- config, http_client=self._http_client, timeout=self._timeout
223
- )
211
+ self._kbz_pay = AsyncKbzPay(config, http_client=self._http_client)
224
212
  return self._kbz_pay
225
213
 
226
214
  def wave_money(self) -> AsyncWaveMoney:
227
215
  """The Wave Money gateway. Raises a ``ConfigurationError`` when not configured."""
228
216
  if self._wave_money is None:
229
217
  config = _resolve(self._wave_money_config, "wave_money", "merchant_id")
230
- self._wave_money = AsyncWaveMoney(
231
- config, http_client=self._http_client, timeout=self._timeout
232
- )
218
+ self._wave_money = AsyncWaveMoney(config, http_client=self._http_client)
233
219
  return self._wave_money
234
220
 
235
221
  def aya_pay(self) -> AsyncAyaPay:
236
222
  """The AYA Payment Gateway. Raises a ``ConfigurationError`` when not configured."""
237
223
  if self._aya_pay is None:
238
224
  config = _resolve(self._aya_pay_config, "aya_pay", "app_key")
239
- self._aya_pay = AsyncAyaPay(
240
- config, http_client=self._http_client, timeout=self._timeout
241
- )
225
+ self._aya_pay = AsyncAyaPay(config, http_client=self._http_client)
242
226
  return self._aya_pay
243
227
 
244
228
  def yoma_mmqr(self) -> AsyncYomaMmqr:
@@ -249,7 +233,6 @@ class AsyncMyanmarPayments(_Payments):
249
233
  config,
250
234
  token_cache=self._token_cache,
251
235
  http_client=self._http_client,
252
- timeout=self._timeout,
253
236
  )
254
237
  return self._yoma_mmqr
255
238
 
@@ -14,9 +14,6 @@ import httpx
14
14
  from ._errors import ApiError
15
15
  from ._json import LosslessObject, dumps, parse_object
16
16
 
17
- DEFAULT_TIMEOUT = 30.0
18
- """The default request timeout, in seconds."""
19
-
20
17
 
21
18
  @dataclass(frozen=True)
22
19
  class HttpRequest:
@@ -81,7 +78,7 @@ def _response(response: httpx.Response) -> GatewayResponse:
81
78
  class SyncTransport:
82
79
  """Posts requests with an ``httpx.Client``, created on first use unless given."""
83
80
 
84
- def __init__(self, client: httpx.Client | None, timeout: float | None) -> None:
81
+ def __init__(self, client: httpx.Client | None, timeout: float) -> None:
85
82
  self._client = client
86
83
  self._owns_client = client is None
87
84
  self._timeout = timeout
@@ -113,7 +110,7 @@ class SyncTransport:
113
110
  class AsyncTransport:
114
111
  """Posts requests with an ``httpx.AsyncClient``, created on first use unless given."""
115
112
 
116
- def __init__(self, client: httpx.AsyncClient | None, timeout: float | None) -> None:
113
+ def __init__(self, client: httpx.AsyncClient | None, timeout: float) -> None:
117
114
  self._client = client
118
115
  self._owns_client = client is None
119
116
  self._timeout = timeout
@@ -147,13 +144,14 @@ class SyncGateway:
147
144
  """A gateway that calls its API with a synchronous ``httpx.Client``.
148
145
 
149
146
  Pass ``http_client`` to share a client (proxies, tracing, test transports);
150
- otherwise one is created on first use with ``timeout`` seconds (default 30)
151
- and closed by :meth:`close` or a ``with`` block.
147
+ otherwise one is created on first use with the config's ``timeout_seconds``
148
+ and closed by :meth:`close` or a ``with`` block. A client you pass keeps its
149
+ own timeout.
152
150
  """
153
151
 
154
152
  _transport: SyncTransport
155
153
 
156
- def _init_transport(self, http_client: httpx.Client | None, timeout: float | None) -> None:
154
+ def _init_transport(self, http_client: httpx.Client | None, timeout: float) -> None:
157
155
  self._transport = SyncTransport(http_client, timeout)
158
156
 
159
157
  def close(self) -> None:
@@ -176,13 +174,13 @@ class AsyncGateway:
176
174
  """A gateway that calls its API with an ``httpx.AsyncClient``.
177
175
 
178
176
  Pass ``http_client`` to share a client; otherwise one is created on first use
179
- with ``timeout`` seconds (default 30) and closed by :meth:`aclose` or an
180
- ``async with`` block.
177
+ with the config's ``timeout_seconds`` and closed by :meth:`aclose` or an
178
+ ``async with`` block. A client you pass keeps its own timeout.
181
179
  """
182
180
 
183
181
  _transport: AsyncTransport
184
182
 
185
- def _init_transport(self, http_client: httpx.AsyncClient | None, timeout: float | None) -> None:
183
+ def _init_transport(self, http_client: httpx.AsyncClient | None, timeout: float) -> None:
186
184
  self._transport = AsyncTransport(http_client, timeout)
187
185
 
188
186
  async def aclose(self) -> None:
@@ -117,25 +117,6 @@ def env_first(env: EnvSource, *keys: str) -> str:
117
117
  return ""
118
118
 
119
119
 
120
- _FALSE = frozenset({"false", "0", "f", "no", "off"})
121
-
122
-
123
- def env_sandbox(env: EnvSource, key: str) -> bool:
124
- """Reads a ``*_SANDBOX`` variable.
125
-
126
- Only ``false``, ``0``, ``f``, ``no`` and ``off`` (any case) select production;
127
- unset or unrecognized values mean sandbox.
128
- """
129
- return sandbox_flag(env.get(key) or "")
130
-
131
-
132
- def sandbox_flag(value: bool | str) -> bool:
133
- """A ``sandbox`` setting: a bool, or text read like a ``*_SANDBOX`` variable."""
134
- if isinstance(value, str):
135
- return value.strip().lower() not in _FALSE
136
- return bool(value)
137
-
138
-
139
120
  _C = TypeVar("_C")
140
121
 
141
122
 
@@ -149,10 +130,22 @@ def config_of(config_class: type[_C], config: _C | Mapping[str, Any]) -> _C:
149
130
  _INT = re.compile(r"[+-]?[0-9]+")
150
131
 
151
132
 
152
- def env_int(env: EnvSource, key: str) -> int | None:
153
- """An integer variable, or ``None`` when unset or not an integer."""
154
- value = (env.get(key) or "").strip()
155
- return int(value) if _INT.fullmatch(value) else None
133
+ def require_seconds(gateway: str, key: str, value: object) -> int:
134
+ """A whole number of seconds greater than 0, given as an int or integer text.
135
+
136
+ Unset or blank raises a :class:`ConfigurationError` naming ``key``; anything
137
+ else that is not a whole number greater than 0 raises it as invalid.
138
+ """
139
+ if value is None or (isinstance(value, str) and value.strip() == ""):
140
+ raise ConfigurationError(gateway, key)
141
+ seconds: int | None = None
142
+ if isinstance(value, int) and not isinstance(value, bool):
143
+ seconds = value
144
+ elif isinstance(value, str) and _INT.fullmatch(value.strip()):
145
+ seconds = int(value.strip())
146
+ if seconds is None or seconds <= 0:
147
+ raise ConfigurationError(gateway, key, invalid=True)
148
+ return seconds
156
149
 
157
150
 
158
151
  def require_setting(gateway: str, key: str, value: object) -> str:
@@ -0,0 +1 @@
1
+ __version__ = "4.0.0a2"
@@ -12,7 +12,6 @@ from ._amount import AmountInput, to_amount
12
12
  from ._callback import CallbackRequest, lossless_input, lossless_query_input
13
13
  from ._errors import ApiError, SignatureVerificationError
14
14
  from ._http import (
15
- DEFAULT_TIMEOUT,
16
15
  AsyncGateway,
17
16
  GatewayResponse,
18
17
  HttpRequest,
@@ -28,12 +27,11 @@ from ._support import (
28
27
  decode_base64,
29
28
  default_env,
30
29
  env_first,
31
- env_sandbox,
32
30
  hmac_sha256_hex,
33
31
  optional_setting,
32
+ require_seconds,
34
33
  require_setting,
35
34
  safe_equal,
36
- sandbox_flag,
37
35
  trim_url,
38
36
  unix_time,
39
37
  )
@@ -53,18 +51,19 @@ __all__ = [
53
51
  class AyaPayConfig:
54
52
  """AYA Payment Gateway (APG) credentials and endpoints.
55
53
 
56
- A missing credential raises a :class:`~python_myanmar_payments.ConfigurationError`.
54
+ Every setting except the URL override is required; a missing one raises a
55
+ :class:`~python_myanmar_payments.ConfigurationError`. The URL defaults to
56
+ production.
57
57
  """
58
58
 
59
- SANDBOX_URL: ClassVar[str] = "https://uat-pgw.ayainnovation.com"
60
59
  PRODUCTION_URL: ClassVar[str] = "https://pgw.ayainnovation.com"
61
60
 
62
61
  app_key: str
63
62
  """The public application key, sent with every request."""
64
63
  app_secret: str
65
64
  """The secret that signs requests and verifies callbacks."""
66
- sandbox: bool
67
- """Whether the UAT environment is used."""
65
+ timeout_seconds: int
66
+ """Seconds before the default HTTP client gives up on a request."""
68
67
  base_url: str
69
68
  """The base URL in use."""
70
69
 
@@ -73,34 +72,32 @@ class AyaPayConfig:
73
72
  *,
74
73
  app_key: str = "",
75
74
  app_secret: str = "",
76
- sandbox: bool | str = True,
75
+ timeout_seconds: int | str | None = None,
77
76
  base_url: str | None = None,
78
77
  ) -> None:
79
78
  self.app_key = require_setting("aya_pay", "app_key", app_key)
80
79
  self.app_secret = require_setting("aya_pay", "app_secret", app_secret)
81
- self.sandbox = sandbox = sandbox_flag(sandbox)
82
- self.base_url = trim_url(
83
- optional_setting(base_url) or (self.SANDBOX_URL if sandbox else self.PRODUCTION_URL)
84
- )
80
+ self.timeout_seconds = require_seconds("aya_pay", "timeout_in_seconds", timeout_seconds)
81
+ self.base_url = trim_url(optional_setting(base_url) or self.PRODUCTION_URL)
85
82
 
86
83
  @classmethod
87
84
  def from_env(cls, env: EnvSource | None = None) -> AyaPayConfig:
88
85
  """Reads the ``AYA_PAY_*`` environment variables.
89
86
 
90
- ``AYA_PAY_APP_KEY``, ``AYA_PAY_APP_SECRET``, ``AYA_PAY_SANDBOX`` and
91
- ``AYA_PAY_BASE_URL``, falling back to the ``AYA_PGW_*`` names. Defaults to
92
- ``os.environ``.
87
+ ``AYA_PAY_APP_KEY``, ``AYA_PAY_APP_SECRET``, ``MYANMAR_PAYMENTS_HTTP_TIMEOUT``
88
+ and ``AYA_PAY_BASE_URL``, falling back to the ``AYA_PGW_*`` names. Defaults
89
+ to ``os.environ``.
93
90
  """
94
91
  env = default_env() if env is None else env
95
92
  return cls(
96
93
  app_key=env_first(env, "AYA_PAY_APP_KEY", "AYA_PGW_APP_KEY"),
97
94
  app_secret=env_first(env, "AYA_PAY_APP_SECRET", "AYA_PGW_APP_SECRET"),
98
- sandbox=env_sandbox(env, "AYA_PAY_SANDBOX"),
95
+ timeout_seconds=env_first(env, "MYANMAR_PAYMENTS_HTTP_TIMEOUT"),
99
96
  base_url=env_first(env, "AYA_PAY_BASE_URL", "AYA_PGW_BASE_URL"),
100
97
  )
101
98
 
102
99
  def __repr__(self) -> str:
103
- return f"AyaPayConfig(app_key={self.app_key!r}, sandbox={self.sandbox!r})"
100
+ return f"AyaPayConfig(app_key={self.app_key!r})"
104
101
 
105
102
 
106
103
  class AyaPayMethod(_StrEnum):
@@ -444,10 +441,9 @@ class AyaPay(_AyaPayBase, SyncGateway):
444
441
  config: AyaPayConfig | Mapping[str, Any],
445
442
  *,
446
443
  http_client: httpx.Client | None = None,
447
- timeout: float | None = DEFAULT_TIMEOUT,
448
444
  ) -> None:
449
445
  super().__init__(config)
450
- self._init_transport(http_client, timeout)
446
+ self._init_transport(http_client, self.config.timeout_seconds)
451
447
 
452
448
  @classmethod
453
449
  def from_env(
@@ -455,10 +451,9 @@ class AyaPay(_AyaPayBase, SyncGateway):
455
451
  env: EnvSource | None = None,
456
452
  *,
457
453
  http_client: httpx.Client | None = None,
458
- timeout: float | None = DEFAULT_TIMEOUT,
459
454
  ) -> AyaPay:
460
455
  """A gateway configured from the ``AYA_PAY_*`` (or ``AYA_PGW_*``) variables."""
461
- return cls(AyaPayConfig.from_env(env), http_client=http_client, timeout=timeout)
456
+ return cls(AyaPayConfig.from_env(env), http_client=http_client)
462
457
 
463
458
  def services(self) -> list[AyaPayService]:
464
459
  """Lists the payment channels enabled for your merchant account."""
@@ -482,10 +477,9 @@ class AsyncAyaPay(_AyaPayBase, AsyncGateway):
482
477
  config: AyaPayConfig | Mapping[str, Any],
483
478
  *,
484
479
  http_client: httpx.AsyncClient | None = None,
485
- timeout: float | None = DEFAULT_TIMEOUT,
486
480
  ) -> None:
487
481
  super().__init__(config)
488
- self._init_transport(http_client, timeout)
482
+ self._init_transport(http_client, self.config.timeout_seconds)
489
483
 
490
484
  @classmethod
491
485
  def from_env(
@@ -493,10 +487,9 @@ class AsyncAyaPay(_AyaPayBase, AsyncGateway):
493
487
  env: EnvSource | None = None,
494
488
  *,
495
489
  http_client: httpx.AsyncClient | None = None,
496
- timeout: float | None = DEFAULT_TIMEOUT,
497
490
  ) -> AsyncAyaPay:
498
491
  """A gateway configured from the ``AYA_PAY_*`` (or ``AYA_PGW_*``) variables."""
499
- return cls(AyaPayConfig.from_env(env), http_client=http_client, timeout=timeout)
492
+ return cls(AyaPayConfig.from_env(env), http_client=http_client)
500
493
 
501
494
  async def services(self) -> list[AyaPayService]:
502
495
  """Lists the payment channels enabled for your merchant account."""