python-myanmar-payments 4.0.0a1__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.
@@ -0,0 +1,526 @@
1
+ """Yoma Bank MMQR: QR payments, status checks and callbacks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import base64
7
+ import inspect
8
+ import re
9
+ import threading
10
+ from collections.abc import Mapping, Sequence
11
+ from dataclasses import dataclass
12
+ from datetime import timedelta
13
+ from typing import Any, ClassVar, cast
14
+
15
+ import httpx
16
+
17
+ from ._amount import AmountInput, to_amount
18
+ from ._cache import AsyncTokenCache, MemoryTokenCache, TokenCache
19
+ from ._callback import CallbackRequest, lossless_body
20
+ from ._errors import ApiError, SignatureVerificationError
21
+ from ._http import (
22
+ DEFAULT_TIMEOUT,
23
+ AsyncGateway,
24
+ GatewayResponse,
25
+ HttpRequest,
26
+ SyncGateway,
27
+ form_request,
28
+ json_request,
29
+ )
30
+ from ._json import LosslessObject, to_plain_object
31
+ from ._results import PaymentCallback, PaymentStatusResult, QrPayment
32
+ from ._status import PaymentStatus, resolve_status
33
+ from ._support import (
34
+ EnvSource,
35
+ config_of,
36
+ default_env,
37
+ env_first,
38
+ env_sandbox,
39
+ hmac_sha256_hex,
40
+ optional_setting,
41
+ require_setting,
42
+ safe_equal,
43
+ sandbox_flag,
44
+ sha256_hex,
45
+ trim_url,
46
+ utc_now,
47
+ )
48
+ from ._validate import AmountRule, Validator
49
+ from ._values import get, is_nested, optional, trimmed
50
+
51
+ __all__ = [
52
+ "AsyncYomaMmqr",
53
+ "YomaMmqr",
54
+ "YomaMmqrConfig",
55
+ "YomaMmqrPaymentData",
56
+ ]
57
+
58
+
59
+ class YomaMmqrConfig:
60
+ """Yoma MMQR credentials and endpoints.
61
+
62
+ A missing credential raises a :class:`~python_myanmar_payments.ConfigurationError`.
63
+ """
64
+
65
+ SANDBOX_URL: ClassVar[str] = "https://devapi.yomabank.net"
66
+ PRODUCTION_URL: ClassVar[str] = "https://paymenthubapi.yomabank.com"
67
+ DEFAULT_API_VERSION: ClassVar[str] = "v1rc"
68
+
69
+ merchant_id: str
70
+ """The merchant ID Yoma issued."""
71
+ client_id: str
72
+ """The OAuth client ID."""
73
+ client_secret: str
74
+ """The OAuth client secret."""
75
+ webhook_hash_key: str
76
+ """The hash key Yoma issued for verifying callbacks."""
77
+ webhook_secret: str | None
78
+ """The secret you shared with Yoma; when set, callbacks must carry it in
79
+ ``X-Webhook-Secret``."""
80
+ sandbox: bool
81
+ """Whether the UAT payment hub is used."""
82
+ base_url: str
83
+ """The base URL in use."""
84
+ api_version: str
85
+ """The ``{version}`` segment of the API paths in use."""
86
+
87
+ def __init__(
88
+ self,
89
+ *,
90
+ merchant_id: str = "",
91
+ client_id: str = "",
92
+ client_secret: str = "",
93
+ webhook_hash_key: str = "",
94
+ webhook_secret: str | None = None,
95
+ sandbox: bool | str = True,
96
+ base_url: str | None = None,
97
+ api_version: str | None = None,
98
+ ) -> None:
99
+ self.merchant_id = require_setting("yoma_mmqr", "merchant_id", merchant_id)
100
+ self.client_id = require_setting("yoma_mmqr", "client_id", client_id)
101
+ self.client_secret = require_setting("yoma_mmqr", "client_secret", client_secret)
102
+ self.webhook_hash_key = require_setting("yoma_mmqr", "webhook_hashkey", webhook_hash_key)
103
+ self.webhook_secret = optional_setting(webhook_secret)
104
+ self.sandbox = sandbox = sandbox_flag(sandbox)
105
+ self.base_url = trim_url(
106
+ optional_setting(base_url) or (self.SANDBOX_URL if sandbox else self.PRODUCTION_URL)
107
+ )
108
+ self.api_version = optional_setting(api_version) or self.DEFAULT_API_VERSION
109
+
110
+ @classmethod
111
+ def from_env(cls, env: EnvSource | None = None) -> YomaMmqrConfig:
112
+ """Reads the ``YOMA_MMQR_*`` environment variables.
113
+
114
+ ``YOMA_MMQR_MERCHANT_ID``, ``YOMA_MMQR_CLIENT_ID``,
115
+ ``YOMA_MMQR_CLIENT_SECRET``, ``YOMA_MMQR_WEBHOOK_HASHKEY``,
116
+ ``YOMA_MMQR_WEBHOOK_SECRET``, ``YOMA_MMQR_SANDBOX``, ``YOMA_MMQR_BASE_URL``
117
+ and ``YOMA_MMQR_API_VERSION``. Defaults to ``os.environ``.
118
+ """
119
+ env = default_env() if env is None else env
120
+ return cls(
121
+ merchant_id=env_first(env, "YOMA_MMQR_MERCHANT_ID"),
122
+ client_id=env_first(env, "YOMA_MMQR_CLIENT_ID"),
123
+ client_secret=env_first(env, "YOMA_MMQR_CLIENT_SECRET"),
124
+ webhook_hash_key=env_first(env, "YOMA_MMQR_WEBHOOK_HASHKEY"),
125
+ webhook_secret=env_first(env, "YOMA_MMQR_WEBHOOK_SECRET"),
126
+ sandbox=env_sandbox(env, "YOMA_MMQR_SANDBOX"),
127
+ base_url=env_first(env, "YOMA_MMQR_BASE_URL"),
128
+ api_version=env_first(env, "YOMA_MMQR_API_VERSION"),
129
+ )
130
+
131
+ def __repr__(self) -> str:
132
+ return f"YomaMmqrConfig(merchant_id={self.merchant_id!r}, sandbox={self.sandbox!r})"
133
+
134
+
135
+ @dataclass(kw_only=True)
136
+ class YomaMmqrPaymentData:
137
+ """A Yoma MMQR order.
138
+
139
+ Yoma accepts each order number once; renew an expired QR with ``renew_qr()``.
140
+ Yoma documents no decimals, so amounts are whole kyat.
141
+ """
142
+
143
+ order_id: str
144
+ """Your unique order number, at most 20 characters."""
145
+ amount: AmountInput
146
+ """The amount in whole kyat, e.g. ``Amount.kyat(1000)`` or ``1000``."""
147
+ description: str
148
+ """Shown on the payment slip, at most 50 characters."""
149
+
150
+
151
+ _STATUSES: Mapping[str, PaymentStatus] = {
152
+ "SUCCESS": PaymentStatus.SUCCESSFUL,
153
+ "PENDING": PaymentStatus.PENDING,
154
+ "FAILED": PaymentStatus.FAILED,
155
+ "FAIL": PaymentStatus.FAILED,
156
+ }
157
+
158
+ _LEADING_INT = re.compile(r"\s*([+-]?[0-9]+)")
159
+
160
+
161
+ @dataclass(frozen=True)
162
+ class _Call:
163
+ path: str
164
+ url: str
165
+ data: Mapping[str, Any]
166
+ allowed_errors: Sequence[str] = ()
167
+
168
+ def request(self, token: str) -> HttpRequest:
169
+ return json_request(self.url, self.data, {"Authorization": f"Bearer {token}"})
170
+
171
+
172
+ class _YomaMmqrBase:
173
+ """What the sync and async Yoma clients share: requests, parsing and callbacks."""
174
+
175
+ QR_LIFETIME_SECONDS: ClassVar[int] = 120
176
+ """How long a generated QR stays payable, in seconds."""
177
+
178
+ config: YomaMmqrConfig
179
+ """The configuration in use."""
180
+
181
+ def __init__(self, config: YomaMmqrConfig | Mapping[str, Any]) -> None:
182
+ self.config = config_of(YomaMmqrConfig, config)
183
+
184
+ @staticmethod
185
+ def validate(data: YomaMmqrPaymentData) -> None:
186
+ """Checks the order against Yoma's documented limits.
187
+
188
+ Raises an :class:`~python_myanmar_payments.InvalidPaymentDataError`.
189
+ """
190
+ (
191
+ Validator()
192
+ .required("order_id", data.order_id)
193
+ .max("order_id", data.order_id, 20)
194
+ .amount("amount", data.amount, AmountRule("Yoma MMQR", 0))
195
+ .required("description", data.description)
196
+ .max("description", data.description, 50)
197
+ .validate()
198
+ )
199
+
200
+ def handle_callback(self, request: CallbackRequest) -> PaymentCallback:
201
+ """Verifies Yoma's payment callback.
202
+
203
+ The hash is HMAC-SHA256 of ``orderNumber=..&status=..`` keyed with the
204
+ order number followed by the webhook hash key.
205
+ """
206
+ payload = lossless_body(request)
207
+
208
+ if self.config.webhook_secret is not None and not safe_equal(
209
+ self.config.webhook_secret, request.header("X-Webhook-Secret") or ""
210
+ ):
211
+ raise SignatureVerificationError(
212
+ "Yoma MMQR callback has a missing or wrong X-Webhook-Secret header.",
213
+ to_plain_object(payload),
214
+ )
215
+
216
+ order_number = get(payload, "orderNumber")
217
+ status = trimmed(payload, "status")
218
+ expected = hmac_sha256_hex(
219
+ order_number + self.config.webhook_hash_key,
220
+ f"orderNumber={order_number}&status={status}",
221
+ )
222
+ if (
223
+ order_number == ""
224
+ or is_nested(payload.get("status"))
225
+ or not safe_equal(expected, get(payload, "hashValue").lower())
226
+ ):
227
+ raise SignatureVerificationError(
228
+ "Yoma MMQR callback hash verification failed.", to_plain_object(payload)
229
+ )
230
+
231
+ return PaymentCallback(
232
+ order_id=order_number,
233
+ status=resolve_status(_STATUSES, status.upper()),
234
+ gateway_status=status,
235
+ raw=to_plain_object(payload),
236
+ )
237
+
238
+ # --- Requests and results shared by both clients ------------------------------
239
+
240
+ def _api(self, path: str, data: Mapping[str, Any], allowed: Sequence[str] = ()) -> _Call:
241
+ url = f"{self.config.base_url}/payment-gateway/{self.config.api_version}/api/{path}"
242
+ return _Call(path, url, data, allowed)
243
+
244
+ def _checkout_call(self, data: YomaMmqrPaymentData) -> _Call:
245
+ self.validate(data)
246
+ return self._api(
247
+ "payment/checkout",
248
+ {
249
+ "merchantId": self.config.merchant_id,
250
+ "orderNumber": data.order_id,
251
+ "amount": str(to_amount(data.amount)),
252
+ "description": data.description,
253
+ },
254
+ )
255
+
256
+ @staticmethod
257
+ def _check_checkout(body: LosslessObject) -> None:
258
+ if body.get("checkOutStatus") is not True:
259
+ raise ApiError("Yoma MMQR did not confirm the checkout.", raw=to_plain_object(body))
260
+
261
+ def _renew_call(self, order_id: str) -> _Call:
262
+ return self._api(
263
+ "qr/generate", {"merchantId": self.config.merchant_id, "orderNumber": order_id}
264
+ )
265
+
266
+ def _qr_payment(self, order_id: str, body: LosslessObject) -> QrPayment:
267
+ qr_string = get(body, "qrString")
268
+ reference = get(body, "refLabel")
269
+ if qr_string == "" or reference == "":
270
+ raise ApiError("Yoma MMQR did not return a QR.", raw=to_plain_object(body))
271
+ return QrPayment(
272
+ order_id=order_id,
273
+ qr_image=qr_string,
274
+ expires_at=utc_now() + timedelta(seconds=self.QR_LIFETIME_SECONDS),
275
+ reference=reference,
276
+ raw=to_plain_object(body),
277
+ )
278
+
279
+ def _status_call(self, reference: str) -> _Call:
280
+ return self._api(
281
+ "payment/check-status",
282
+ {"merchantId": self.config.merchant_id, "refLabel": reference},
283
+ ["QR EXPIRED"],
284
+ )
285
+
286
+ @staticmethod
287
+ def _status_result(reference: str, body: LosslessObject) -> PaymentStatusResult:
288
+ if get(body, "errorCode") == "QR EXPIRED":
289
+ return PaymentStatusResult(
290
+ status=PaymentStatus.EXPIRED,
291
+ gateway_status="QR EXPIRED",
292
+ gateway_reference=reference,
293
+ raw=to_plain_object(body),
294
+ )
295
+ payment_status = trimmed(body, "paymentStatus")
296
+ return PaymentStatusResult(
297
+ status=resolve_status(_STATUSES, payment_status.upper()),
298
+ gateway_status=payment_status,
299
+ gateway_reference=get(body, "refLabel") or reference,
300
+ raw=to_plain_object(body),
301
+ )
302
+
303
+ @staticmethod
304
+ def _body(call: _Call, response: GatewayResponse) -> LosslessObject:
305
+ body = response.json()
306
+ error_code = get(body, "errorCode")
307
+ if not response.successful() or (
308
+ error_code != "" and error_code not in call.allowed_errors
309
+ ):
310
+ raise _api_error(call.path, response.status, body, error_code)
311
+ return body
312
+
313
+ def _token_request(self) -> HttpRequest:
314
+ credentials = f"{self.config.client_id}:{self.config.client_secret}"
315
+ encoded = base64.b64encode(credentials.encode()).decode()
316
+ return form_request(
317
+ f"{self.config.base_url}/token",
318
+ {"grant_type": "client_credentials"},
319
+ {"Authorization": f"Basic {encoded}"},
320
+ )
321
+
322
+ @staticmethod
323
+ def _token(response: GatewayResponse) -> tuple[str, int]:
324
+ """The access token and how many seconds to cache it."""
325
+ body = response.json()
326
+ token = get(body, "access_token")
327
+ if not response.successful() or token == "":
328
+ raise _api_error("token", response.status, body, get(body, "error"))
329
+ match = _LEADING_INT.match(get(body, "expires_in"))
330
+ expires_in = int(match.group(1)) if match else 0
331
+ return token, max(60, (expires_in if expires_in > 0 else 3600) - 60)
332
+
333
+ def _token_cache_key(self) -> str:
334
+ digest = sha256_hex(f"{self.config.base_url}|{self.config.client_id}")
335
+ return f"myanmar-payments.yoma-mmqr.token.{digest}"
336
+
337
+
338
+ def _api_error(endpoint: str, status: int, body: LosslessObject, error_code: str) -> ApiError:
339
+ message = optional(body, "errorDescription") or optional(body, "error_description")
340
+ return ApiError(
341
+ f"Yoma MMQR {endpoint} failed: [{error_code}] {message or ''}".rstrip()
342
+ if error_code
343
+ else f"Yoma MMQR {endpoint} failed with HTTP {status}.",
344
+ gateway_code=error_code or None,
345
+ gateway_message=message or None,
346
+ http_status=status,
347
+ raw=to_plain_object(body),
348
+ )
349
+
350
+
351
+ class YomaMmqr(_YomaMmqrBase, SyncGateway):
352
+ """Yoma Bank MMQR with a synchronous HTTP client.
353
+
354
+ QR payments, status checks and callbacks. Access tokens are cached in a
355
+ :class:`~python_myanmar_payments.TokenCache`; share the gateway (or pass a
356
+ shared cache) so they survive between calls. Use :class:`AsyncYomaMmqr` in
357
+ async code.
358
+ """
359
+
360
+ def __init__(
361
+ self,
362
+ config: YomaMmqrConfig | Mapping[str, Any],
363
+ *,
364
+ token_cache: TokenCache | None = None,
365
+ http_client: httpx.Client | None = None,
366
+ timeout: float | None = DEFAULT_TIMEOUT,
367
+ ) -> None:
368
+ super().__init__(config)
369
+ self._init_transport(http_client, timeout)
370
+ self._cache: TokenCache = token_cache if token_cache is not None else MemoryTokenCache()
371
+ self._token_lock = threading.Lock()
372
+
373
+ @classmethod
374
+ def from_env(
375
+ cls,
376
+ env: EnvSource | None = None,
377
+ *,
378
+ token_cache: TokenCache | None = None,
379
+ http_client: httpx.Client | None = None,
380
+ timeout: float | None = DEFAULT_TIMEOUT,
381
+ ) -> YomaMmqr:
382
+ """A gateway configured from the ``YOMA_MMQR_*`` environment variables."""
383
+ return cls(
384
+ YomaMmqrConfig.from_env(env),
385
+ token_cache=token_cache,
386
+ http_client=http_client,
387
+ timeout=timeout,
388
+ )
389
+
390
+ def initiate(self, data: YomaMmqrPaymentData) -> QrPayment:
391
+ """Checks the order out with Yoma and generates its first QR.
392
+
393
+ Call it once per order.
394
+ """
395
+ self._check_checkout(self._send(self._checkout_call(data)))
396
+ return self.renew_qr(data.order_id)
397
+
398
+ def renew_qr(self, order_id: str) -> QrPayment:
399
+ """Generates a new QR for an order that is already checked out.
400
+
401
+ Use it after the previous QR expired. The previous QR's reference stops
402
+ working.
403
+ """
404
+ return self._qr_payment(order_id, self._send(self._renew_call(order_id)))
405
+
406
+ def status(self, reference: str) -> PaymentStatusResult:
407
+ """Checks a QR's payment status by its reference (``QrPayment.reference``).
408
+
409
+ An expired QR reports ``expired``.
410
+ """
411
+ return self._status_result(reference, self._send(self._status_call(reference)))
412
+
413
+ def forget_token(self) -> None:
414
+ """Drops the cached access token, e.g. after rotating the client secret."""
415
+ self._cache.delete(self._token_cache_key())
416
+
417
+ def _send(self, call: _Call) -> LosslessObject:
418
+ response = self._transport.send(call.request(self._access_token()))
419
+ if response.status == 401:
420
+ self.forget_token()
421
+ response = self._transport.send(call.request(self._access_token()))
422
+ return self._body(call, response)
423
+
424
+ def _access_token(self) -> str:
425
+ key = self._token_cache_key()
426
+ cached = self._cache.get(key)
427
+ if isinstance(cached, str) and cached != "":
428
+ return cached
429
+ # Concurrent calls share one token request.
430
+ with self._token_lock:
431
+ cached = self._cache.get(key)
432
+ if isinstance(cached, str) and cached != "":
433
+ return cached
434
+ token, ttl = self._token(self._transport.send(self._token_request()))
435
+ self._cache.set(key, token, ttl)
436
+ return token
437
+
438
+
439
+ AnyTokenCache = TokenCache | AsyncTokenCache
440
+
441
+
442
+ class AsyncYomaMmqr(_YomaMmqrBase, AsyncGateway):
443
+ """Yoma Bank MMQR with an async HTTP client. The same API as :class:`YomaMmqr`, awaited.
444
+
445
+ ``token_cache`` takes a :class:`~python_myanmar_payments.TokenCache` or an
446
+ :class:`~python_myanmar_payments.AsyncTokenCache`.
447
+ """
448
+
449
+ def __init__(
450
+ self,
451
+ config: YomaMmqrConfig | Mapping[str, Any],
452
+ *,
453
+ token_cache: AnyTokenCache | None = None,
454
+ http_client: httpx.AsyncClient | None = None,
455
+ timeout: float | None = DEFAULT_TIMEOUT,
456
+ ) -> None:
457
+ super().__init__(config)
458
+ self._init_transport(http_client, timeout)
459
+ self._cache: AnyTokenCache = token_cache if token_cache is not None else MemoryTokenCache()
460
+ self._token_lock = asyncio.Lock()
461
+
462
+ @classmethod
463
+ def from_env(
464
+ cls,
465
+ env: EnvSource | None = None,
466
+ *,
467
+ token_cache: AnyTokenCache | None = None,
468
+ http_client: httpx.AsyncClient | None = None,
469
+ timeout: float | None = DEFAULT_TIMEOUT,
470
+ ) -> AsyncYomaMmqr:
471
+ """A gateway configured from the ``YOMA_MMQR_*`` environment variables."""
472
+ return cls(
473
+ YomaMmqrConfig.from_env(env),
474
+ token_cache=token_cache,
475
+ http_client=http_client,
476
+ timeout=timeout,
477
+ )
478
+
479
+ async def initiate(self, data: YomaMmqrPaymentData) -> QrPayment:
480
+ """Checks the order out with Yoma and generates its first QR. Call it once per order."""
481
+ self._check_checkout(await self._send(self._checkout_call(data)))
482
+ return await self.renew_qr(data.order_id)
483
+
484
+ async def renew_qr(self, order_id: str) -> QrPayment:
485
+ """Generates a new QR for an order that is already checked out."""
486
+ return self._qr_payment(order_id, await self._send(self._renew_call(order_id)))
487
+
488
+ async def status(self, reference: str) -> PaymentStatusResult:
489
+ """Checks a QR's payment status by its reference. An expired QR reports ``expired``."""
490
+ return self._status_result(reference, await self._send(self._status_call(reference)))
491
+
492
+ async def forget_token(self) -> None:
493
+ """Drops the cached access token, e.g. after rotating the client secret."""
494
+ await _resolve(self._cache.delete(self._token_cache_key()))
495
+
496
+ async def _send(self, call: _Call) -> LosslessObject:
497
+ response = await self._transport.send(call.request(await self._access_token()))
498
+ if response.status == 401:
499
+ await self.forget_token()
500
+ response = await self._transport.send(call.request(await self._access_token()))
501
+ return self._body(call, response)
502
+
503
+ async def _cached_token(self, key: str) -> str | None:
504
+ cached = await _resolve(self._cache.get(key))
505
+ return cached if isinstance(cached, str) and cached != "" else None
506
+
507
+ async def _access_token(self) -> str:
508
+ key = self._token_cache_key()
509
+ cached = await self._cached_token(key)
510
+ if cached is not None:
511
+ return cached
512
+ # Concurrent calls share one token request.
513
+ async with self._token_lock:
514
+ cached = await self._cached_token(key)
515
+ if cached is not None:
516
+ return cached
517
+ token, ttl = self._token(await self._transport.send(self._token_request()))
518
+ await _resolve(self._cache.set(key, token, ttl))
519
+ return token
520
+
521
+
522
+ async def _resolve(value: Any) -> Any:
523
+ """Awaits ``value`` when a cache method returned an awaitable."""
524
+ if inspect.isawaitable(value):
525
+ return await cast("Any", value)
526
+ return value
@@ -0,0 +1,43 @@
1
+ Metadata-Version: 2.5
2
+ Name: python-myanmar-payments
3
+ Version: 4.0.0a1
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
+ Project-URL: Homepage, https://laranex.vercel.app/python-myanmar-payments
6
+ Project-URL: Documentation, https://laranex.vercel.app/python-myanmar-payments
7
+ Project-URL: Repository, https://github.com/laranex/python-myanmar-payments
8
+ Project-URL: Issues, https://github.com/laranex/python-myanmar-payments/issues
9
+ Project-URL: Changelog, https://github.com/laranex/python-myanmar-payments/blob/dev/CHANGELOG.md
10
+ Author-email: Nay Thu Khant <naythukhant644@gmail.com>
11
+ License-Expression: MIT
12
+ License-File: LICENSE.md
13
+ Keywords: aya-pay,cybersource,kbzpay,laranex,mmqr,myanmar,payment-gateway,payments,python-myanmar-payments,wave-money,wavepay,yoma
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Framework :: AsyncIO
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Programming Language :: Python :: 3.14
25
+ Classifier: Topic :: Office/Business :: Financial
26
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
27
+ Classifier: Typing :: Typed
28
+ Requires-Python: >=3.10
29
+ Requires-Dist: httpx>=0.25
30
+ Description-Content-Type: text/markdown
31
+
32
+ # Python Myanmar Payments
33
+
34
+ [![PyPI](https://img.shields.io/pypi/v/python-myanmar-payments.svg?style=flat-square)](https://pypi.org/project/python-myanmar-payments/)
35
+ [![Tests](https://github.com/laranex/python-myanmar-payments/actions/workflows/tests.yml/badge.svg)](https://github.com/laranex/python-myanmar-payments/actions/workflows/tests.yml)
36
+ [![Python](https://img.shields.io/pypi/pyversions/python-myanmar-payments.svg?style=flat-square)](https://pypi.org/project/python-myanmar-payments/)
37
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](LICENSE.md)
38
+
39
+ 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. Built for humans and AI agents.
40
+
41
+ ## Documentation
42
+
43
+ Full documentation, including installation, usage and the AI agent skill, lives at **[laranex.vercel.app/python-myanmar-payments](https://laranex.vercel.app/python-myanmar-payments)**.
@@ -0,0 +1,24 @@
1
+ python_myanmar_payments/__init__.py,sha256=FI4lGJ1-7fFQ6B_AiLWmp1uHHHq18qfUHrcwQLxG20c,2633
2
+ python_myanmar_payments/_amount.py,sha256=YoacvJ6_ZB9JM5exgkGPmzZM4IkZFR5e4cpvPv1iz5s,6720
3
+ python_myanmar_payments/_cache.py,sha256=qxAGw7RjNWUWJQFbhcf-ROAv7oAXLtagUxKFn1EeLHM,2482
4
+ python_myanmar_payments/_callback.py,sha256=xwrmWfFF5zXdtCyyfc--6ie3lCsq8Zu6Vurog2_lrd4,6678
5
+ python_myanmar_payments/_errors.py,sha256=4QPyVdMSTig_-CZHb24j3JN93NNA6XQ59kigFrXHd6M,2841
6
+ python_myanmar_payments/_facade.py,sha256=OieDF_OdpmQOmhvLWDNJ3-zgi9_4E9febEg3RcBkiGg,11194
7
+ python_myanmar_payments/_http.py,sha256=vtjLdcO0o_ogfAeWisUAjhusO3sdCARwTclURHszceI,6272
8
+ python_myanmar_payments/_json.py,sha256=FyN7J7fpHqfqa8GGlroqGTIOSR1K0JiYBouz20FGp0A,2608
9
+ python_myanmar_payments/_results.py,sha256=Klf9U9pgG7ej5FnSJ-tJX5HSa0tSx3DCkscenZD4Ubk,9234
10
+ python_myanmar_payments/_status.py,sha256=DRGkOrP1aE9hvt-vaH8f1rQcIo70IYZdqhhdaeGesrI,2326
11
+ python_myanmar_payments/_support.py,sha256=iL5TO-IPN3sx3noWkvwbOZGvh2keSUIwyXVvzHogpDg,4971
12
+ python_myanmar_payments/_validate.py,sha256=OYZokICLELcAEo42ODLYE4kNIn5FCVplTB0anirr0F8,6209
13
+ python_myanmar_payments/_values.py,sha256=19LviQmhRbOatqjfGcZ7bi6GEparzUmUg0fz-V30Ok0,1953
14
+ python_myanmar_payments/_version.py,sha256=0RKQvZCh46QOqtdJaDVgeyftE3UvgFVB8C9x4fITksU,24
15
+ python_myanmar_payments/aya_pay.py,sha256=pUSC20v_Ay0t2cvmKsLe8XEjO5UneBRYE3_DFvj7xOg,18076
16
+ python_myanmar_payments/cyber_source.py,sha256=WCzcv9vZ4SFm6OeI2A9G1DceBNjOdblvFDTt-pFwars,11457
17
+ python_myanmar_payments/kbz_pay.py,sha256=TOzEZLbqgbSUXek75EXF_k6ZyALZiAZ5itogi_XgRYY,19873
18
+ python_myanmar_payments/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
+ python_myanmar_payments/wave_money.py,sha256=cXyWOc6ix14-_GSAVMLzMvJvzvkgJX39RBbJSkTtbTw,16234
20
+ python_myanmar_payments/yoma_mmqr.py,sha256=x1XDMWfjww83Kiq5OCrUgFc_Yy3mWF5_mj0mY5K9lS0,19486
21
+ python_myanmar_payments-4.0.0a1.dist-info/METADATA,sha256=kb_a_m8p7THe2QxNUz4MhjvBu_gfb3C90ubKpWJOCSg,2643
22
+ python_myanmar_payments-4.0.0a1.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
23
+ python_myanmar_payments-4.0.0a1.dist-info/licenses/LICENSE.md,sha256=so-zBzmHxeh5dKArUhl-gYkpoljJXaw5TxWD9yOUVXU,1065
24
+ python_myanmar_payments-4.0.0a1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Nay Thu Khant
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.