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.
- {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/CHANGELOG.md +6 -3
- {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/PKG-INFO +1 -1
- {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/skills/python-myanmar-payments/SKILL.md +6 -6
- {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/src/python_myanmar_payments/__init__.py +0 -2
- {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/src/python_myanmar_payments/_errors.py +12 -4
- {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/src/python_myanmar_payments/_facade.py +13 -30
- {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/src/python_myanmar_payments/_http.py +9 -11
- {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/src/python_myanmar_payments/_support.py +16 -23
- python_myanmar_payments-4.0.0a2/src/python_myanmar_payments/_version.py +1 -0
- {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/src/python_myanmar_payments/aya_pay.py +18 -25
- {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/src/python_myanmar_payments/cyber_source.py +22 -32
- {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/src/python_myanmar_payments/kbz_pay.py +17 -32
- {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/src/python_myanmar_payments/wave_money.py +24 -38
- {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/src/python_myanmar_payments/yoma_mmqr.py +21 -30
- {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/tests/fixtures/parity/vectors.json +454 -55
- {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/tests/test_aya_pay.py +7 -7
- {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/tests/test_core.py +29 -31
- {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/tests/test_cyber_source.py +22 -5
- {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/tests/test_kbz_pay.py +22 -13
- {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/tests/test_myanmar_payments.py +36 -11
- {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/tests/test_parity.py +67 -69
- {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/tests/test_wave_money.py +32 -26
- {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/tests/test_yoma_mmqr.py +16 -11
- python_myanmar_payments-4.0.0a1/src/python_myanmar_payments/_version.py +0 -1
- {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/.gitignore +0 -0
- {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/LICENSE.md +0 -0
- {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/README.md +0 -0
- {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/pyproject.toml +0 -0
- {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/src/python_myanmar_payments/_amount.py +0 -0
- {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/src/python_myanmar_payments/_cache.py +0 -0
- {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/src/python_myanmar_payments/_callback.py +0 -0
- {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/src/python_myanmar_payments/_json.py +0 -0
- {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/src/python_myanmar_payments/_results.py +0 -0
- {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/src/python_myanmar_payments/_status.py +0 -0
- {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/src/python_myanmar_payments/_validate.py +0 -0
- {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/src/python_myanmar_payments/_values.py +0 -0
- {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/src/python_myanmar_payments/py.typed +0 -0
- {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/tests/__init__.py +0 -0
- {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/tests/conftest.py +0 -0
- {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/tests/fixtures/aya_pay/callback_payload.json +0 -0
- {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/tests/fixtures/kbz_pay/sign_string.json +0 -0
- {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/tests/fixtures/wave_money/callback.json +0 -0
- {python_myanmar_payments-4.0.0a1 → python_myanmar_payments-4.0.0a2}/tests/helpers.py +0 -0
- {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
|
|
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
|
|
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.
|
|
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). `
|
|
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
|
|
36
|
-
- Or pass the settings directly: gateways and `MyanmarPayments(kbz_pay={"app_id": ..., "app_key": ..., "merchant_code":
|
|
37
|
-
- Options (keyword arguments): `http_client` (your own `httpx.Client` / `httpx.AsyncClient`)
|
|
38
|
-
- A missing
|
|
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
|
-
|
|
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
|
|
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_
|
|
113
|
-
``CYBER_SOURCE_*`` are read when the
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 ``
|
|
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
|
|
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 ``
|
|
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
|
|
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
|
|
153
|
-
"""
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
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
|
-
|
|
67
|
-
"""
|
|
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
|
-
|
|
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.
|
|
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``, ``
|
|
91
|
-
``AYA_PAY_BASE_URL``, falling back to the ``AYA_PGW_*`` names. Defaults
|
|
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
|
-
|
|
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}
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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."""
|