paymentsgate 1.5.0__tar.gz → 1.5.1__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.

Potentially problematic release.


This version of paymentsgate might be problematic. Click here for more details.

@@ -1,22 +1,23 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: paymentsgate
3
- Version: 1.5.0
3
+ Version: 1.5.1
4
4
  Summary: PaymentsGate's Python SDK for REST API
5
5
  Home-page: https://github.com/paymentsgate/python-secure-api
6
6
  License: MIT
7
7
  Keywords: paymentsgate,payments,sdk,api
8
8
  Author: PaymentsGate
9
- Requires-Python: >=3.9,<4.0
9
+ Requires-Python: >=3.10,<4.0
10
10
  Classifier: Development Status :: 5 - Production/Stable
11
11
  Classifier: Intended Audience :: Developers
12
12
  Classifier: License :: OSI Approved :: MIT License
13
13
  Classifier: Programming Language :: Python :: 3
14
- Classifier: Programming Language :: Python :: 3.9
15
14
  Classifier: Programming Language :: Python :: 3.10
16
15
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
+ Requires-Dist: cryptography (>=44.0.2,<45.0.0)
17
+ Requires-Dist: httpx (>=0.28.1,<0.29.0)
17
18
  Requires-Dist: jwt (>=1.3.1,<2.0.0)
18
19
  Requires-Dist: pydantic (>=2.8.2,<3.0.0)
19
- Requires-Dist: requests (>=2.31.0,<3.0.0)
20
+ Requires-Dist: ruff (>=0.11.7,<0.12.0)
20
21
  Requires-Dist: tomli (>=2.0.1,<3.0.0)
21
22
  Project-URL: Documentation, https://github.com/paymentsgate/python-secure-api
22
23
  Project-URL: Repository, https://github.com/paymentsgate/python-secure-api
@@ -0,0 +1,17 @@
1
+ from paymentsgate.client import ApiClient
2
+ from paymentsgate.enums import (
3
+ AuthenticationRealms,
4
+ ApiPaths,
5
+ Currencies,
6
+ Languages,
7
+ Statuses,
8
+ CurrencyTypes,
9
+ InvoiceTypes,
10
+ CredentialsTypes,
11
+ RiskScoreLevels,
12
+ CancellationReason,
13
+ FeesStrategy,
14
+ InvoiceDirection,
15
+ TTLUnits,
16
+ )
17
+ from paymentsgate.models import Credentials
@@ -1,10 +1,8 @@
1
1
  from abc import ABC, abstractmethod
2
2
  from dataclasses import dataclass, field
3
3
 
4
- from paymentsgate.tokens import (
5
- AccessToken,
6
- RefreshToken
7
- )
4
+ from paymentsgate.tokens import AccessToken, RefreshToken
5
+
8
6
 
9
7
  class AbstractCache(ABC):
10
8
  """
@@ -19,15 +17,19 @@ class AbstractCache(ABC):
19
17
  ...
20
18
 
21
19
  @abstractmethod
22
- def set_token(self,token: AccessToken | RefreshToken) -> None:
20
+ def set_token(self, token: AccessToken | RefreshToken) -> None:
23
21
  """
24
22
  Save the token to the cache under the specified key
25
23
  """
26
24
  ...
27
-
25
+
26
+
28
27
  @dataclass
29
28
  class DefaultCache(AbstractCache):
30
- tokens: dict[str, AccessToken | RefreshToken] = field(default_factory=dict, init=False)
29
+ tokens: dict[str, AccessToken | RefreshToken] = field(
30
+ default_factory=dict, init=False
31
+ )
32
+
31
33
  def get_token(self, key: str) -> AccessToken | RefreshToken | None:
32
34
  return self.tokens.get(key)
33
35
 
@@ -0,0 +1,464 @@
1
+ from __future__ import annotations
2
+ import logging
3
+
4
+ import json
5
+ from urllib.parse import urlencode
6
+ from pydantic import Field, BaseModel
7
+
8
+ from .types import TokenResponse
9
+ from .tokens import AccessToken, RefreshToken
10
+ from .exceptions import APIResponseError, APIAuthenticationError
11
+ from .models import (
12
+ Credentials,
13
+ GetQuoteModel,
14
+ GetQuoteResponseModel,
15
+ PayInModel,
16
+ PayInResponseModel,
17
+ PayOutModel,
18
+ PayOutResponseModel,
19
+ InvoiceModel,
20
+ GetQuoteTlv,
21
+ PayOutTlvRequest,
22
+ QuoteTlvResponse,
23
+ )
24
+ from .enums import ApiPaths
25
+ from .transport import Request, Response
26
+ from .logger import Logger
27
+ from .cache import AbstractCache, DefaultCache
28
+
29
+ import httpx
30
+
31
+
32
+ class BaseClient:
33
+
34
+ def __init__(self, config: Credentials, baseUrl: str, timeout: int = 20, debug: bool=False):
35
+ self.config = config
36
+ self.cache = DefaultCache()
37
+ self.baseUrl = baseUrl
38
+ self.timeout = timeout
39
+ if debug:
40
+ logging.basicConfig(level=logging.DEBUG)
41
+
42
+
43
+ class ApiAsyncClient(BaseClient):
44
+ async def PayIn(self, request: PayInModel) -> PayInResponseModel:
45
+ # Prepare request
46
+ request = Request(
47
+ method="post",
48
+ path=ApiPaths.invoices_payin,
49
+ content_type='application/json',
50
+ noAuth=False,
51
+ body=request.model_dump(exclude_none=True),
52
+ )
53
+
54
+ # Handle response
55
+ response = await self._send_request(request)
56
+ if (response.success):
57
+ return response.cast(PayInResponseModel, APIResponseError)
58
+ else:
59
+ raise APIResponseError(response)
60
+
61
+ async def PayOut(self, request: PayOutModel) -> PayOutResponseModel:
62
+ # Prepare request
63
+ request = Request(
64
+ method="post",
65
+ path=ApiPaths.invoices_payout,
66
+ content_type='application/json',
67
+ noAuth=False,
68
+ signature=True,
69
+ body=request.model_dump(exclude_none=True)
70
+ )
71
+
72
+ # Handle response
73
+ response = await self._send_request(request)
74
+ if (response.success):
75
+ return response.cast(PayOutResponseModel, APIResponseError)
76
+ else:
77
+ raise APIResponseError(response)
78
+
79
+ async def PayOutTlv(self, request: PayOutTlvRequest) -> PayOutResponseModel:
80
+ request = Request(
81
+ method="post",
82
+ path=ApiPaths.invoices_payout_tlv,
83
+ content_type="application/json",
84
+ noAuth=False,
85
+ signature=False,
86
+ body=request.model_dump(exclude_none=True),
87
+ )
88
+
89
+ # Handle response
90
+ response = await self._send_request(request)
91
+ if not response.success:
92
+ raise APIResponseError(response)
93
+
94
+ return response.cast(PayOutResponseModel, APIResponseError)
95
+
96
+ async def Quote(self, params: GetQuoteModel) -> GetQuoteResponseModel:
97
+ # Prepare request
98
+ request = Request(
99
+ method="get",
100
+ path=ApiPaths.fx_quote,
101
+ content_type='application/json',
102
+ noAuth=False,
103
+ signature=False,
104
+ body=params.model_dump(exclude_none=True)
105
+ )
106
+
107
+ # Handle response
108
+ response = await self._send_request(request)
109
+ if not response.success:
110
+ raise APIResponseError(response)
111
+
112
+ return response.cast(GetQuoteResponseModel, APIResponseError)
113
+
114
+ async def QuoteQr(self, params: GetQuoteTlv) -> QuoteTlvResponse:
115
+ request = Request(
116
+ method="post",
117
+ path=ApiPaths.fx_quote_tlv,
118
+ content_type="application/json",
119
+ noAuth=False,
120
+ signature=False,
121
+ body=params.model_dump(exclude_none=True),
122
+ )
123
+
124
+ # Handle response
125
+ response = await self._send_request(request)
126
+ if not response.success:
127
+ raise APIResponseError(response)
128
+
129
+ return response.cast(QuoteTlvResponse, APIResponseError)
130
+
131
+ async def Status(self, id: str) -> InvoiceModel:
132
+ # Prepare request
133
+ request = Request(
134
+ method="get",
135
+ path=ApiPaths.invoices_info.replace(':id', id),
136
+ content_type='application/json',
137
+ noAuth=False,
138
+ signature=False,
139
+ )
140
+
141
+ # Handle response
142
+ response = await self._send_request(request)
143
+ if not response.success:
144
+ raise APIResponseError(response)
145
+ return response.cast(InvoiceModel, APIResponseError)
146
+
147
+ async def get_token(self) -> AccessToken | None:
148
+ # First check if valid token is cached
149
+ token = self.cache.get_token("AccessToken")
150
+ refresh = self.cache.get_token("RefreshToken")
151
+
152
+ if token is not None and not token.is_expired:
153
+ return token
154
+ else:
155
+ # try to refresh token
156
+ if refresh is not None and not refresh.is_expired:
157
+ refreshed = await self._refresh_token(token, refresh)
158
+
159
+ if refreshed.success:
160
+ access = AccessToken(refreshed.json_body["access_token"])
161
+ refresh = RefreshToken(
162
+ refreshed.json_body["refresh_token"],
163
+ int(refreshed.json_body["expires_in"]),
164
+ )
165
+
166
+ self.cache.set_token(access)
167
+ self.cache.set_token(refresh)
168
+
169
+ return access
170
+
171
+ # try to issue token
172
+ response = await self._fetch_token()
173
+ if response.success:
174
+ access = AccessToken(response.json_body["access_token"])
175
+ refresh = RefreshToken(
176
+ response.json_body["refresh_token"],
177
+ int(response.json_body["expires_in"]),
178
+ )
179
+
180
+ self.cache.set_token(access)
181
+ self.cache.set_token(refresh)
182
+
183
+ return access
184
+ else:
185
+ raise APIAuthenticationError(response)
186
+
187
+ async def _send_request(self, request: Request) -> Response:
188
+ """
189
+ Send a specified Request to the GoPay REST API and process the response
190
+ """
191
+ dict_factory = lambda l: {k: v for k, v in l if v is not None}
192
+ body = request.body
193
+ # Add Bearer authentication to headers if needed
194
+ headers = request.headers or {}
195
+ if not request.noAuth:
196
+ auth = await self.get_token()
197
+ if auth is not None:
198
+ headers["Authorization"] = f"Bearer {auth.token}"
199
+
200
+ client = httpx.AsyncClient(timeout=self.timeout)
201
+ if request.method == 'get':
202
+ url = f'{self.baseUrl}{request.path}'
203
+ if body:
204
+ params = urlencode(body)
205
+ url = f'{url}?{params}'
206
+ r = await client.request(
207
+ method=request.method,
208
+ url=url,
209
+ headers=headers,
210
+ timeout=self.timeout
211
+ )
212
+ else:
213
+ r = await client.request(
214
+ method=request.method,
215
+ url=f"{self.baseUrl}{request.path}",
216
+ headers=headers,
217
+ json=body,
218
+ timeout=self.timeout
219
+ )
220
+
221
+ # Build Response instance, try to decode body as JSON
222
+ response = Response(raw_body=r.content, json={}, status_code=r.status_code)
223
+
224
+ try:
225
+ response.json_body = r.json()
226
+ except json.JSONDecodeError:
227
+ pass
228
+
229
+ return response
230
+
231
+ async def _fetch_token(self) -> Response:
232
+ # Prepare request
233
+ request = Request(
234
+ method="post",
235
+ path=ApiPaths.token_issue,
236
+ content_type='application/json',
237
+ noAuth=True,
238
+ body={"account_id": self.config.account_id, "public_key": self.config.public_key},
239
+ )
240
+ # Handle response
241
+ response = await self._send_request(request)
242
+ return response
243
+
244
+ async def _refresh_token(self) -> Response:
245
+ # Prepare request
246
+ request = Request(
247
+ method="post",
248
+ path=ApiPaths.token_refresh,
249
+ content_type='application/json',
250
+ body={"refresh_token": self.refreshToken},
251
+ )
252
+ # Handle response
253
+ response = await self._send_request(request)
254
+ return response
255
+
256
+
257
+ class ApiClient(BaseClient):
258
+
259
+ def PayIn(self, request: PayInModel) -> PayInResponseModel:
260
+ # Prepare request
261
+ request = Request(
262
+ method="post",
263
+ path=ApiPaths.invoices_payin,
264
+ content_type='application/json',
265
+ noAuth=False,
266
+ body=request.model_dump(exclude_none=True),
267
+ )
268
+
269
+ # Handle response
270
+ response = self._send_request(request)
271
+ if (response.success):
272
+ return response.cast(PayInResponseModel, APIResponseError)
273
+ else:
274
+ raise APIResponseError(response)
275
+
276
+ def PayOut(self, request: PayOutModel) -> PayOutResponseModel:
277
+ # Prepare request
278
+ request = Request(
279
+ method="post",
280
+ path=ApiPaths.invoices_payout,
281
+ content_type='application/json',
282
+ noAuth=False,
283
+ signature=True,
284
+ body=request.model_dump(exclude_none=True)
285
+ )
286
+
287
+ # Handle response
288
+ response = self._send_request(request)
289
+ if (response.success):
290
+ return response.cast(PayOutResponseModel, APIResponseError)
291
+ else:
292
+ raise APIResponseError(response)
293
+
294
+ def PayOutTlv(self, request: PayOutTlvRequest) -> PayOutResponseModel:
295
+ request = Request(
296
+ method="post",
297
+ path=ApiPaths.invoices_payout_tlv,
298
+ content_type="application/json",
299
+ noAuth=False,
300
+ signature=False,
301
+ body=request.model_dump(exclude_none=True),
302
+ )
303
+
304
+ # Handle response
305
+ response = self._send_request(request)
306
+ if not response.success:
307
+ raise APIResponseError(response)
308
+ return response.cast(PayOutResponseModel, APIResponseError)
309
+
310
+ def Quote(self, params: GetQuoteModel) -> GetQuoteResponseModel:
311
+ # Prepare request
312
+ request = Request(
313
+ method="get",
314
+ path=ApiPaths.fx_quote,
315
+ content_type='application/json',
316
+ noAuth=False,
317
+ signature=False,
318
+ body=params.model_dump(exclude_none=True)
319
+ )
320
+
321
+ # Handle response
322
+ response = self._send_request(request)
323
+ if not response.success:
324
+ raise APIResponseError(response)
325
+
326
+ return response.cast(GetQuoteResponseModel, APIResponseError)
327
+
328
+ def QuoteQr(self, params: GetQuoteTlv) -> QuoteTlvResponse:
329
+ request = Request(
330
+ method="post",
331
+ path=ApiPaths.fx_quote_tlv,
332
+ content_type="application/json",
333
+ noAuth=False,
334
+ signature=False,
335
+ body=params.model_dump(exclude_none=True),
336
+ )
337
+
338
+ # Handle response
339
+ response = self._send_request(request)
340
+ if not response.success:
341
+ raise APIResponseError(response)
342
+
343
+ return response.cast(QuoteTlvResponse, APIResponseError)
344
+
345
+ def Status(self, id: str) -> InvoiceModel:
346
+ # Prepare request
347
+ request = Request(
348
+ method="get",
349
+ path=ApiPaths.invoices_info.replace(':id', id),
350
+ content_type='application/json',
351
+ noAuth=False,
352
+ signature=False,
353
+ )
354
+
355
+ # Handle response
356
+ response = self._send_request(request)
357
+ if not response.success:
358
+ raise APIResponseError(response)
359
+
360
+ return response.cast(InvoiceModel, APIResponseError)
361
+
362
+ def get_token(self) -> AccessToken | None:
363
+ # First check if valid token is cached
364
+ token = self.cache.get_token('access')
365
+ refresh = self.cache.get_token('refresh')
366
+ if token is not None and not token.is_expired:
367
+ return token
368
+ else:
369
+ # try to refresh token
370
+ if refresh is not None and not refresh.is_expired:
371
+ refreshed = self._refresh_token()
372
+
373
+ if (refreshed.success):
374
+ access = AccessToken(
375
+ response.json_body["access_token"]
376
+ )
377
+ refresh = RefreshToken(
378
+ response.json_body["refresh_token"],
379
+ int(response.json_body["expires_in"]),
380
+ )
381
+ self.cache.set_token(access)
382
+ self.cache.set_token(refresh)
383
+
384
+ return access
385
+
386
+ # try to issue token
387
+ response = self._fetch_token()
388
+ if response.success:
389
+
390
+ access = AccessToken(
391
+ response.json_body["access_token"]
392
+ )
393
+ refresh = RefreshToken(
394
+ response.json_body["refresh_token"],
395
+ int(response.json_body["expires_in"]),
396
+ )
397
+ self.cache.set_token(access)
398
+ self.cache.set_token(refresh)
399
+
400
+ return access
401
+ else:
402
+ raise APIAuthenticationError(response)
403
+
404
+ def _send_request(self, request: Request) -> Response:
405
+ """
406
+ Send a specified Request to the GoPay REST API and process the response
407
+ """
408
+ # Add Bearer authentication to headers if needed
409
+ headers = request.headers or {}
410
+ if not request.noAuth:
411
+ auth = self.get_token()
412
+ if auth is not None:
413
+ headers["Authorization"] = f"Bearer {auth.token}"
414
+
415
+ if (request.method == 'get'):
416
+ params = urlencode(body)
417
+ r = httpx.request(
418
+ method=request.method,
419
+ url=f"{self.baseUrl}{request.path}?{params}",
420
+ headers=headers,
421
+ timeout=self.timeout
422
+ )
423
+ else:
424
+ r = httpx.request(
425
+ method=request.method,
426
+ url=f"{self.baseUrl}{request.path}",
427
+ headers=headers,
428
+ json=body,
429
+ timeout=self.timeout
430
+ )
431
+
432
+ # Build Response instance, try to decode body as JSON
433
+ response = Response(raw_body=r.content, json={}, status_code=r.status_code)
434
+
435
+ try:
436
+ response.json_body = r.json()
437
+ except json.JSONDecodeError:
438
+ pass
439
+ return response
440
+
441
+ def _fetch_token(self) -> Response:
442
+ # Prepare request
443
+ request = Request(
444
+ method="post",
445
+ path=ApiPaths.token_issue,
446
+ content_type='application/json',
447
+ noAuth=True,
448
+ body={"account_id": self.config.account_id, "public_key": self.config.public_key},
449
+ )
450
+ # Handle response
451
+ response = self._send_request(request)
452
+ return response
453
+
454
+ def _refresh_token(self) -> Response:
455
+ # Prepare request
456
+ request = Request(
457
+ method="post",
458
+ path=ApiPaths.token_refresh,
459
+ content_type='application/json',
460
+ body={"refresh_token": self.refreshToken},
461
+ )
462
+ # Handle response
463
+ response = self._send_request(request)
464
+ return response