paymentsgate 1.4.9__py3-none-any.whl → 1.5.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
paymentsgate/__init__.py CHANGED
@@ -1,17 +1,17 @@
1
1
  from paymentsgate.client import ApiClient
2
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
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
16
  )
17
- from paymentsgate.models import Credentials
17
+ from paymentsgate.models import Credentials
paymentsgate/cache.py CHANGED
@@ -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
 
paymentsgate/client.py CHANGED
@@ -1,58 +1,261 @@
1
1
  from __future__ import annotations
2
2
  import logging
3
- from dataclasses import dataclass, is_dataclass, field, asdict
3
+
4
4
  import json
5
5
  from urllib.parse import urlencode
6
+ from pydantic import Field, BaseModel
6
7
 
7
- from paymentsgate.tokens import (
8
- AccessToken,
9
- RefreshToken
10
- )
11
- from paymentsgate.exceptions import (
12
- APIResponseError,
13
- APIAuthenticationError
14
- )
15
- from paymentsgate.models import (
16
- Credentials,
17
- GetQuoteModel,
18
- GetQuoteResponseModel,
19
- PayInModel,
20
- PayInResponseModel,
21
- PayOutModel,
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,
22
18
  PayOutResponseModel,
23
- InvoiceModel
24
- )
25
- from paymentsgate.enums import ApiPaths
26
- from paymentsgate.transport import (
27
- Request,
28
- Response
29
- )
30
- from paymentsgate.logger import Logger
31
- from paymentsgate.cache import (
32
- AbstractCache,
33
- DefaultCache
19
+ InvoiceModel,
20
+ GetQuoteTlv,
21
+ PayOutTlvRequest,
22
+ QuoteTlvResponse,
34
23
  )
24
+ from .enums import ApiPaths
25
+ from .transport import Request, Response
26
+ from .logger import Logger
27
+ from .cache import AbstractCache, DefaultCache
35
28
 
36
- import requests
29
+ import httpx
37
30
 
38
- @dataclass
39
- class ApiClient:
40
- baseUrl: str = field(default="", init=False)
41
- timeout: int = field(default=180, init=True)
42
- logger: Logger = Logger
43
- cache: AbstractCache = field(default_factory=DefaultCache)
44
- config: Credentials = field(default_factory=dict, init=False)
45
31
 
46
- REQUEST_DEBUG: bool = False
47
- RESPONSE_DEBUG: bool = False
32
+ class BaseClient:
48
33
 
49
- def __init__(self, config: Credentials, baseUrl: str, debug: bool=False):
34
+ def __init__(self, config: Credentials, baseUrl: str, timeout: int = 20, debug: bool=False):
50
35
  self.config = config
51
36
  self.cache = DefaultCache()
52
37
  self.baseUrl = baseUrl
38
+ self.timeout = timeout
53
39
  if debug:
54
40
  logging.basicConfig(level=logging.DEBUG)
55
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
+
56
259
  def PayIn(self, request: PayInModel) -> PayInResponseModel:
57
260
  # Prepare request
58
261
  request = Request(
@@ -60,12 +263,11 @@ class ApiClient:
60
263
  path=ApiPaths.invoices_payin,
61
264
  content_type='application/json',
62
265
  noAuth=False,
63
- body=request,
266
+ body=request.model_dump(exclude_none=True),
64
267
  )
65
268
 
66
269
  # Handle response
67
270
  response = self._send_request(request)
68
- self.logger(request, response)
69
271
  if (response.success):
70
272
  return response.cast(PayInResponseModel, APIResponseError)
71
273
  else:
@@ -79,17 +281,32 @@ class ApiClient:
79
281
  content_type='application/json',
80
282
  noAuth=False,
81
283
  signature=True,
82
- body=request
284
+ body=request.model_dump(exclude_none=True)
83
285
  )
84
286
 
85
287
  # Handle response
86
288
  response = self._send_request(request)
87
- self.logger(request, response)
88
289
  if (response.success):
89
290
  return response.cast(PayOutResponseModel, APIResponseError)
90
291
  else:
91
292
  raise APIResponseError(response)
92
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
+
93
310
  def Quote(self, params: GetQuoteModel) -> GetQuoteResponseModel:
94
311
  # Prepare request
95
312
  request = Request(
@@ -98,17 +315,33 @@ class ApiClient:
98
315
  content_type='application/json',
99
316
  noAuth=False,
100
317
  signature=False,
101
- body=params
318
+ body=params.model_dump(exclude_none=True)
102
319
  )
103
320
 
104
321
  # Handle response
105
322
  response = self._send_request(request)
106
- self.logger(request, response)
107
323
  if not response.success:
108
324
  raise APIResponseError(response)
109
325
 
110
326
  return response.cast(GetQuoteResponseModel, APIResponseError)
111
-
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
+
112
345
  def Status(self, id: str) -> InvoiceModel:
113
346
  # Prepare request
114
347
  request = Request(
@@ -121,14 +354,12 @@ class ApiClient:
121
354
 
122
355
  # Handle response
123
356
  response = self._send_request(request)
124
- self.logger(request, response)
125
357
  if not response.success:
126
358
  raise APIResponseError(response)
127
359
 
128
360
  return response.cast(InvoiceModel, APIResponseError)
129
361
 
130
- @property
131
- def token(self) -> AccessToken | None:
362
+ def get_token(self) -> AccessToken | None:
132
363
  # First check if valid token is cached
133
364
  token = self.cache.get_token('access')
134
365
  refresh = self.cache.get_token('refresh')
@@ -141,27 +372,27 @@ class ApiClient:
141
372
 
142
373
  if (refreshed.success):
143
374
  access = AccessToken(
144
- response.json["access_token"]
375
+ response.json_body["access_token"]
145
376
  )
146
377
  refresh = RefreshToken(
147
- response.json["refresh_token"],
148
- int(response.json["expires_in"]),
378
+ response.json_body["refresh_token"],
379
+ int(response.json_body["expires_in"]),
149
380
  )
150
381
  self.cache.set_token(access)
151
382
  self.cache.set_token(refresh)
152
383
 
153
384
  return access
154
385
 
155
- # try to issue token
156
- response = self._get_token()
386
+ # try to issue token
387
+ response = self._fetch_token()
157
388
  if response.success:
158
389
 
159
390
  access = AccessToken(
160
- response.json["access_token"]
391
+ response.json_body["access_token"]
161
392
  )
162
393
  refresh = RefreshToken(
163
- response.json["refresh_token"],
164
- int(response.json["expires_in"]),
394
+ response.json_body["refresh_token"],
395
+ int(response.json_body["expires_in"]),
165
396
  )
166
397
  self.cache.set_token(access)
167
398
  self.cache.set_token(refresh)
@@ -174,24 +405,23 @@ class ApiClient:
174
405
  """
175
406
  Send a specified Request to the GoPay REST API and process the response
176
407
  """
177
- body = asdict(request.body) if is_dataclass(request.body) else request.body
178
408
  # Add Bearer authentication to headers if needed
179
409
  headers = request.headers or {}
180
410
  if not request.noAuth:
181
- auth = self.token
411
+ auth = self.get_token()
182
412
  if auth is not None:
183
413
  headers["Authorization"] = f"Bearer {auth.token}"
184
414
 
185
415
  if (request.method == 'get'):
186
416
  params = urlencode(body)
187
- r = requests.request(
417
+ r = httpx.request(
188
418
  method=request.method,
189
419
  url=f"{self.baseUrl}{request.path}?{params}",
190
420
  headers=headers,
191
421
  timeout=self.timeout
192
422
  )
193
423
  else:
194
- r = requests.request(
424
+ r = httpx.request(
195
425
  method=request.method,
196
426
  url=f"{self.baseUrl}{request.path}",
197
427
  headers=headers,
@@ -202,18 +432,13 @@ class ApiClient:
202
432
  # Build Response instance, try to decode body as JSON
203
433
  response = Response(raw_body=r.content, json={}, status_code=r.status_code)
204
434
 
205
- if (self.REQUEST_DEBUG):
206
- print(f"{request.method} => {self.baseUrl}{request.path} => {response.status_code}")
207
-
208
435
  try:
209
- response.json = r.json()
436
+ response.json_body = r.json()
210
437
  except json.JSONDecodeError:
211
438
  pass
212
-
213
- self.logger(request, response)
214
439
  return response
215
440
 
216
- def _get_token(self) -> Response:
441
+ def _fetch_token(self) -> Response:
217
442
  # Prepare request
218
443
  request = Request(
219
444
  method="post",
@@ -224,9 +449,8 @@ class ApiClient:
224
449
  )
225
450
  # Handle response
226
451
  response = self._send_request(request)
227
- self.logger(request, response)
228
452
  return response
229
-
453
+
230
454
  def _refresh_token(self) -> Response:
231
455
  # Prepare request
232
456
  request = Request(
@@ -237,5 +461,4 @@ class ApiClient:
237
461
  )
238
462
  # Handle response
239
463
  response = self._send_request(request)
240
- self.logger(request, response)
241
464
  return response