everysk-lib 1.9.8__cp311-cp311-win_amd64.whl → 1.10.0__cp311-cp311-win_amd64.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,373 @@
1
+ ###############################################################################
2
+ #
3
+ # (C) Copyright 2026 EVERYSK TECHNOLOGIES
4
+ #
5
+ # This is an unpublished work containing confidential and proprietary
6
+ # information of EVERYSK TECHNOLOGIES. Disclosure, use, or reproduction
7
+ # without authorization of EVERYSK TECHNOLOGIES is prohibited.
8
+ #
9
+ ###############################################################################
10
+ import random
11
+ import re
12
+ import string
13
+
14
+ from everysk.core.fields import IntField, TupleField
15
+
16
+ ###############################################################################
17
+ # Globals
18
+ ###############################################################################
19
+
20
+ CNPJ_LENGTH: IntField = IntField(default=14, readonly=True)
21
+ CNPJ_BASE_LENGTH: IntField = IntField(default=12, readonly=True)
22
+
23
+ WEIGHTS_DV1: TupleField = TupleField(default=(5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2), readonly=True)
24
+ WEIGHTS_DV2: TupleField = TupleField(default=(6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2), readonly=True)
25
+
26
+ BASE_VALUES = string.ascii_uppercase + string.digits
27
+ BASE_VALUES_REGEX = r'[A-Z0-9]'
28
+
29
+
30
+ class CNPJError(ValueError):
31
+ """
32
+ Exception raised for errors encountered during the parsing or validation of a CNPJ (Cadastro Nacional da Pessoa Jurídica).
33
+
34
+ Attributes:
35
+ message (str): Explanation of the error.
36
+ """
37
+
38
+
39
+ class CNPJ:
40
+ """
41
+ Base class for handling Brazilian CNPJ (Cadastro Nacional da Pessoa Jurídica) documents.
42
+
43
+ This class provides methods for sanitizing, normalizing, validating, formatting, and generating CNPJ numbers,
44
+ including support for the new alphanumeric base format and the updated verification digit (DV) calculation rules.
45
+
46
+ cnpj : str | int | float | None
47
+ The CNPJ value to be processed. Can be a string, integer, float, or None.
48
+
49
+ Attributes
50
+ ----------
51
+ firm : str
52
+ The main firm identifier portion of the CNPJ (first 8 digits).
53
+ subsidiary : str
54
+ The branch/subsidiary identifier portion of the CNPJ (next 4 digits).
55
+ dv : str
56
+ The verification digits (last 2 digits) of the CNPJ.
57
+ """
58
+
59
+ def __init__(self, cnpj: str | int | float | None):
60
+ """
61
+ Initialize the instance with a CNPJ value.
62
+
63
+ Args:
64
+ cnpj (str | int | float | None): The CNPJ value, which can be a string, integer, float, or None.
65
+ """
66
+ self._input_cnpj = cnpj
67
+ self.cnpj = cnpj
68
+
69
+ def __str__(self) -> str:
70
+ """
71
+ Returns a string representation of the object by formatting it.
72
+ If formatting fails or returns None, an empty string is returned.
73
+
74
+ Returns:
75
+ str: The formatted string representation of the object, or an empty string if formatting fails.
76
+ """
77
+ return self.cnpj
78
+
79
+ def __repr__(self) -> str:
80
+ """
81
+ Return a string representation of the object, displaying the class name and the value of the 'cnpj' attribute.
82
+
83
+ Returns:
84
+ str: A string in the format "<ClassName>(cnpj='<cnpj_value>')".
85
+ """
86
+ return f"{self.__class__.__name__}('{self.cnpj}')"
87
+
88
+ @property
89
+ def firm(self) -> str:
90
+ """
91
+ Returns the base part of the firm's CNPJ (Cadastro Nacional da Pessoa Jurídica) as a string.
92
+ The CNPJ is sanitized and zero-filled if necessary, then truncated to exclude the last 4 digits.
93
+
94
+ Returns:
95
+ str: The base CNPJ string, excluding the branch identifier.
96
+ """
97
+ if self.is_valid():
98
+ return self.cnpj[: CNPJ_BASE_LENGTH.default - 4]
99
+ return None
100
+
101
+ @property
102
+ def subsidiary(self) -> str:
103
+ """
104
+ Returns the subsidiary portion of the CNPJ number as a string.
105
+
106
+ This property sanitizes the CNPJ value (optionally zero-filling it), then extracts and returns the last 4 digits, which represent the subsidiary code according to the CNPJ format.
107
+
108
+ Returns:
109
+ str: The 4-digit subsidiary code from the CNPJ.
110
+ """
111
+ if self.is_valid():
112
+ return self.cnpj[CNPJ_BASE_LENGTH.default - 4 : CNPJ_BASE_LENGTH.default]
113
+ return None
114
+
115
+ @property
116
+ def dv(self) -> str:
117
+ """
118
+ Returns the 'dv' (check digit) portion of the sanitized identifier string.
119
+
120
+ The method first sanitizes the identifier (optionally zero-filling it), then slices
121
+ the string from the position defined by `CNPJ_BASE_LENGTH.default` to extract the check digit(s).
122
+
123
+ Returns:
124
+ str: The check digit(s) of the sanitized identifier.
125
+ """
126
+ if self.is_valid():
127
+ return self.cnpj[CNPJ_BASE_LENGTH.default :]
128
+ return None
129
+
130
+ def sanitize(self, zfill: bool = True) -> str | None:
131
+ """
132
+ Sanitize a CNPJ string.
133
+
134
+ - Removes non-alphanumeric characters
135
+ - Uppercases letters
136
+ - If zfill=True, left-pads with zeros until length == 14
137
+
138
+ Parameters
139
+ ----------
140
+ zfill : bool, default=True
141
+ Whether to left-pad the sanitized value to length 14.
142
+
143
+ Returns
144
+ -------
145
+ str | None
146
+ Sanitized CNPJ or None if input is None.
147
+
148
+ Raises
149
+ ------
150
+ TypeError
151
+ If `cnpj` is not str|None.
152
+ """
153
+ sanitized = self._input_cnpj
154
+
155
+ if sanitized in {None, True, False, ''}:
156
+ self.cnpj = None
157
+
158
+ else:
159
+ if isinstance(sanitized, float):
160
+ sanitized = int(sanitized)
161
+
162
+ if isinstance(sanitized, int):
163
+ sanitized = str(sanitized)
164
+
165
+ if not isinstance(sanitized, str):
166
+ raise TypeError('CNPJ must be a string, integer, float, or None')
167
+
168
+ sanitized = ''.join(ch for ch in sanitized.strip() if ch.isalnum()).upper()
169
+
170
+ if zfill:
171
+ sanitized = sanitized.zfill(CNPJ_LENGTH.default)
172
+
173
+ self.cnpj = sanitized
174
+
175
+ return self.cnpj
176
+
177
+ def normalize(self, zfill: bool = False, errors: str = 'raise') -> str | None:
178
+ """
179
+ Normalize a CNPJ with structural validation only (no DV check).
180
+
181
+ Always:
182
+ - sanitizes input
183
+ - applies zfill to reach length 14
184
+
185
+ Structural rules:
186
+ - first 12 chars: alphanumeric
187
+ - last 2 chars: digits
188
+
189
+ Parameters
190
+ ----------
191
+ zfill : bool, default=False
192
+ Whether to left-pad the sanitized value to length 14.
193
+ errors : {'raise', 'coerce', 'ignore'}, default='raise'
194
+ Behavior when parsing fails while normalizing ``self.cnpj``.
195
+
196
+ Returns
197
+ -------
198
+ str | None
199
+ Sanitized 14-character CNPJ, None, or original input.
200
+ """
201
+ if errors not in {'raise', 'coerce', 'ignore'}:
202
+ raise ValueError("errors must be one of: 'raise', 'coerce', 'ignore'")
203
+
204
+ try:
205
+ self.sanitize(zfill=zfill)
206
+
207
+ if self.cnpj is None:
208
+ raise CNPJError('CNPJ is None.')
209
+
210
+ if not self.is_valid(check_dv=False):
211
+ raise CNPJError('CNPJ validation failed.')
212
+
213
+ except CNPJError as err:
214
+ if errors == 'coerce':
215
+ self.cnpj = None
216
+ if errors == 'raise':
217
+ raise err
218
+
219
+ return self.cnpj
220
+
221
+ def _ascii48_value(self, ch: str) -> int:
222
+ """
223
+ Converts a single alphanumeric character to its ASCII code minus 48.
224
+
225
+ This method is used internally for mapping characters according to the new DV rule.
226
+ It validates that the input is a single alphanumeric character and raises a CNPJError
227
+ if the input is invalid.
228
+
229
+ Args:
230
+ ch (str): A single character string to be converted.
231
+
232
+ Returns:
233
+ int: The ASCII value of the uppercase version of `ch`, minus 48.
234
+
235
+ Raises:
236
+ CNPJError: If `ch` is not a single character or is not alphanumeric.
237
+ """
238
+ if len(ch) != 1:
239
+ raise CNPJError(f'Invalid character {ch!r}: must be a single character')
240
+ if not ch.isalnum():
241
+ raise CNPJError(f'Invalid character {ch!r}: must be alphanumeric')
242
+ return ord(ch.upper()) - 48
243
+
244
+ def _calc_dv(self, payload: str, weights: tuple[int, ...]) -> str:
245
+ """
246
+ Calculates a single check digit (DV) using the modulo 11 algorithm.
247
+
248
+ Args:
249
+ payload (str): The input string for which the check digit is to be calculated.
250
+ weights (tuple[int, ...]): A tuple of integer weights to be applied to each character in the payload.
251
+
252
+ Returns:
253
+ str: The calculated check digit as a string. Returns '0' if the result is less than 2, otherwise returns (11 - result) as a string.
254
+
255
+ Note:
256
+ This is an internal method used for check digit calculation in Brazilian document validation.
257
+ """
258
+ total = sum(self._ascii48_value(ch) * w for ch, w in zip(payload, weights))
259
+ result = total % 11
260
+ return '0' if result < 2 else str(11 - result)
261
+
262
+ def _calc_dvs_from_base(self, base: str) -> str:
263
+ """
264
+ Calculates and returns the two check digits (DVs) for a given 12-character CNPJ base string.
265
+
266
+ Args:
267
+ base (str): A 12-character alphanumeric string representing the CNPJ base.
268
+
269
+ Returns:
270
+ str: The two calculated check digits concatenated as a string.
271
+
272
+ Raises:
273
+ CNPJError: If the base is not exactly 12 alphanumeric characters.
274
+ """
275
+ if len(base) != CNPJ_BASE_LENGTH.default or not all(ch.isalnum() for ch in base):
276
+ raise CNPJError('Base must be exactly 12 alphanumeric characters')
277
+ dv1 = self._calc_dv(base, WEIGHTS_DV1.default)
278
+ dv2 = self._calc_dv(f'{base}{dv1}', WEIGHTS_DV2.default)
279
+ return f'{dv1}{dv2}'
280
+
281
+ def is_valid(self, check_dv: bool = False) -> bool:
282
+ """
283
+ Validate a *sanitized* CNPJ (14 chars).
284
+ If instantiated with an unsanitized CNPJ, call self.sanitize() first.
285
+
286
+ Parameters
287
+ ----------
288
+ check_dv : bool, default=False
289
+ If False, only structural checks are performed.
290
+ If True, structural + DV check (módulo 11 with ASCII-48 mapping).
291
+
292
+ Returns
293
+ -------
294
+ bool
295
+ True if valid, else False.
296
+ """
297
+ if not isinstance(self.cnpj, str) or len(self.cnpj) != CNPJ_LENGTH.default:
298
+ return False
299
+
300
+ base, dvs = self.cnpj[: CNPJ_BASE_LENGTH.default], self.cnpj[CNPJ_BASE_LENGTH.default :]
301
+
302
+ if not dvs.isdigit():
303
+ return False
304
+
305
+ if not re.fullmatch(rf'{BASE_VALUES_REGEX}{{{CNPJ_BASE_LENGTH.default}}}', base):
306
+ return False
307
+
308
+ if not check_dv:
309
+ return True
310
+
311
+ try:
312
+ return dvs == self._calc_dvs_from_base(base)
313
+ except CNPJError:
314
+ return False
315
+
316
+ def format(self, errors: str = 'raise') -> str | None:
317
+ """
318
+ Format a CNPJ as 'AA.AAA.AAA/AAAA-DD'.
319
+
320
+ Delegates parsing to cls.normalize().
321
+
322
+ Parameters
323
+ ----------
324
+ errors : {'raise', 'coerce', 'ignore'}, default='raise'
325
+ Normalization error behavior.
326
+
327
+ Returns
328
+ -------
329
+ str | None
330
+ Formatted CNPJ, None, or original input.
331
+ """
332
+ self.normalize(zfill=True, errors=errors)
333
+ if self.cnpj is None or not isinstance(self.cnpj, str):
334
+ return self.cnpj
335
+ return f'{self.cnpj[0:2]}.{self.cnpj[2:5]}.{self.cnpj[5:8]}/{self.cnpj[8:12]}-{self.cnpj[12:]}'
336
+
337
+ @staticmethod
338
+ def generate_random(valid_dv: bool = True) -> str:
339
+ """
340
+ Generate a random CNPJ using the new alphanumeric base format.
341
+
342
+ Parameters
343
+ ----------
344
+ valid_dv : bool, default=True
345
+ If True, generate a DV-valid CNPJ.
346
+ If False, generate a structurally valid but DV-invalid CNPJ.
347
+
348
+ Returns
349
+ -------
350
+ str
351
+ Sanitized 14-character CNPJ.
352
+ """
353
+ base = ''.join(random.choice(BASE_VALUES) for _ in range(CNPJ_BASE_LENGTH.default))
354
+
355
+ # To avoid calling cls from a staticmethod, compute using local helpers:
356
+ # We'll re-use the module-level logic through a lightweight inline calc.
357
+ def ascii48_value(ch: str) -> int:
358
+ return ord(ch.upper()) - 48
359
+
360
+ def calc_dv(payload: str, weights: tuple[int, ...]) -> str:
361
+ total = sum(ascii48_value(ch) * w for ch, w in zip(payload, weights))
362
+ rest = total % 11
363
+ return '0' if rest < 2 else str(11 - rest)
364
+
365
+ dv1 = calc_dv(base, WEIGHTS_DV1.default)
366
+ dv2 = calc_dv(f'{base}{dv1}', WEIGHTS_DV2.default)
367
+ dvs = f'{dv1}{dv2}'
368
+
369
+ if valid_dv:
370
+ return CNPJ(f'{base}{dvs}')
371
+
372
+ invalid_second_dv = str((int(dvs[1]) + random.randint(1, 9)) % 10)
373
+ return CNPJ(f'{base}{dvs[0]}{invalid_second_dv}')
@@ -0,0 +1,42 @@
1
+ ###############################################################################
2
+ #
3
+ # (C) Copyright 2026 EVERYSK TECHNOLOGIES
4
+ #
5
+ # This is an unpublished work containing confidential and proprietary
6
+ # information of EVERYSK TECHNOLOGIES. Disclosure, use, or reproduction
7
+ # without authorization of EVERYSK TECHNOLOGIES is prohibited.
8
+ #
9
+ ################################################################################
10
+ import pandas as pd
11
+
12
+ from everysk.sdk.brutils.cnpj import CNPJ
13
+
14
+
15
+ @pd.api.extensions.register_series_accessor('cnpj')
16
+ class CNPJAccessor:
17
+ """
18
+ A pandas accessor class for handling CNPJ (Cadastro Nacional da Pessoa Jurídica) operations on pandas Series.
19
+
20
+ Parameters
21
+ ----------
22
+ pandas_obj : pandas.Series
23
+ The pandas Series object containing CNPJ values.
24
+ """
25
+
26
+ def __init__(self, pandas_obj):
27
+ self._obj = pandas_obj
28
+
29
+ def sanitize(self, zfill: bool = True):
30
+ return self._obj.apply(lambda x: CNPJ(x).sanitize(zfill=zfill))
31
+
32
+ def normalize(self, zfill: bool = False, errors: str = 'raise'):
33
+ return self._obj.apply(lambda x: CNPJ(x).normalize(zfill=zfill, errors=errors))
34
+
35
+ def is_valid(self, check_dv: bool = False):
36
+ return self._obj.apply(lambda x: CNPJ(x).is_valid(check_dv=check_dv))
37
+
38
+ def format(self, errors: str = 'raise'):
39
+ return self._obj.apply(lambda x: CNPJ(x).format(errors=errors))
40
+
41
+ def generate(self, valid_dv: bool = True):
42
+ return self._obj.apply(lambda x: str(CNPJ.generate_random(valid_dv=valid_dv)))
everysk/settings.py CHANGED
@@ -93,3 +93,6 @@ HTTP_USE_RANDOM_USER_AGENT = BoolField(default=False)
93
93
 
94
94
  # Default directory to use the known_hosts file and other SFTP configurations
95
95
  EVERYSK_SFTP_DIR = StrField(default=f'{tempfile.gettempdir()}/sftp')
96
+
97
+ # Enable to show retry logs
98
+ RETRY_SHOW_LOGS = BoolField(default=False)
everysk/sql/connection.py CHANGED
@@ -7,18 +7,21 @@
7
7
  # without authorization of EVERYSK TECHNOLOGIES is prohibited.
8
8
  #
9
9
  ###############################################################################
10
+ from collections.abc import Callable
10
11
  from contextvars import ContextVar, Token
11
12
  from os import getpid
12
13
  from types import TracebackType
13
14
  from typing import Literal
14
15
 
15
- from psqlpy import Connection, ConnectionPool, QueryResult, SslMode
16
- from psqlpy import Transaction as _Transaction
16
+ from psycopg import Connection, OperationalError
17
+ from psycopg_pool import ConnectionPool as _ConnectionPool
17
18
 
18
19
  from everysk.config import settings
19
20
  from everysk.core.log import Logger
21
+ from everysk.core.retry import retry
22
+ from everysk.sql.row_factory import cls_row, dict_row
20
23
 
21
- _CONNECTIONS: dict[str, ConnectionPool] = {}
24
+ _CONNECTIONS: dict[str, 'ConnectionPool'] = {}
22
25
  log = Logger('everysk-lib-sql-query')
23
26
 
24
27
 
@@ -27,37 +30,49 @@ def _log(message: str, extra: dict | None = None) -> None:
27
30
  log.debug(message, extra=extra)
28
31
 
29
32
 
33
+ class ConnectionPool(_ConnectionPool):
34
+ def __del__(self) -> None:
35
+ # To close the connections when the pool is deleted
36
+ # https://everysk.atlassian.net/browse/COD-8885
37
+ try:
38
+ return super().__del__()
39
+ except RuntimeError:
40
+ # The connection is already closed or discarded because we cannot join the current thread
41
+ # RuntimeError: cannot join current thread
42
+ pass
43
+
44
+ return None
45
+
46
+
30
47
  class Transaction:
31
48
  ## Private attributes
32
49
  _connection: Connection
33
50
  _pool: ConnectionPool
34
51
  _token: Token
35
- _transaction: _Transaction
36
52
 
37
53
  ## Public attributes
38
- connection: ContextVar[_Transaction] = ContextVar('postgresql-psqlpy-transaction', default=None)
54
+ connection: ContextVar[Connection] = ContextVar('postgresql-psqlpy-transaction', default=None)
39
55
 
40
56
  def __init__(self, dsn: str | None = None) -> None:
41
57
  self._pool: ConnectionPool = get_pool(dsn=dsn)
42
58
 
43
- async def __aenter__(self) -> None:
44
- self._connection = await self._pool.connection()
45
- self._transaction = self._connection.transaction()
46
- await self._transaction.begin()
47
- self._token = self.connection.set(self._transaction)
59
+ def __enter__(self) -> None:
60
+ self._connection = self._pool.getconn()
61
+ self._token = self.connection.set(self._connection)
48
62
 
49
63
  return self
50
64
 
51
- async def __aexit__(
65
+ def __exit__(
52
66
  self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None
53
67
  ) -> None:
54
68
  if exc_type is None:
55
- await self._transaction.commit()
69
+ self._connection.commit()
56
70
  else:
57
- await self._transaction.rollback()
71
+ self._connection.rollback()
58
72
 
59
73
  self.connection.reset(self._token)
60
- self._connection.close()
74
+ # Return the connection to the pool
75
+ self._pool.putconn(self._connection)
61
76
 
62
77
  return False
63
78
 
@@ -89,7 +104,7 @@ def make_connection_dsn(
89
104
  return 'postgresql://{user}:{password}@{host}:{port}/{database}'.format(**options)
90
105
 
91
106
 
92
- def get_pool(dsn: str | None = None) -> ConnectionPool:
107
+ def get_pool(dsn: str | None = None, **kwargs) -> ConnectionPool:
93
108
  """
94
109
  Retrieve a database connection pool for the given DSN.
95
110
 
@@ -102,28 +117,38 @@ def get_pool(dsn: str | None = None) -> ConnectionPool:
102
117
 
103
118
  Args:
104
119
  dsn (str | None): The Data Source Name for the database connection. If None, a default DSN is used.
120
+ **kwargs: Additional keyword arguments to configure the connection pool.
105
121
 
106
122
  Returns:
107
123
  ConnectionPool: The connection pool associated with the given DSN.
108
124
  """
109
125
  dsn = dsn or make_connection_dsn()
126
+ # https://www.psycopg.org/psycopg3/docs/api/pool.html
127
+ kwargs['check'] = ConnectionPool.check_connection
128
+ kwargs['min_size'] = kwargs.get('min_size', settings.POSTGRESQL_POOL_MIN_SIZE)
129
+ kwargs['max_size'] = kwargs.get('max_size', settings.POSTGRESQL_POOL_MAX_SIZE)
130
+ kwargs['max_idle'] = kwargs.get('max_idle', settings.POSTGRESQL_POOL_MAX_IDLE)
131
+ kwargs['max_lifetime'] = kwargs.get('max_lifetime', settings.POSTGRESQL_POOL_MAX_LIFETIME)
132
+ kwargs['max_waiting'] = kwargs.get('max_waiting', settings.POSTGRESQL_POOL_MAX_WAITING)
133
+ kwargs['reconnect_timeout'] = kwargs.get('reconnect_timeout', settings.POSTGRESQL_POOL_RECONNECT_TIMEOUT)
134
+ kwargs['timeout'] = kwargs.get('timeout', settings.POSTGRESQL_POOL_TIMEOUT)
135
+ kwargs['open'] = kwargs.get('open', settings.POSTGRESQL_POOL_OPEN)
136
+
110
137
  key = f'{getpid()}:{hash(dsn)}'
111
138
  if key not in _CONNECTIONS:
112
- _CONNECTIONS[key] = ConnectionPool(
113
- dsn=dsn,
114
- max_db_pool_size=settings.POSTGRESQL_POOL_MAX_SIZE,
115
- ssl_mode=getattr(SslMode, settings.POSTGRESQL_CONNECTION_ENCRYPTION, None),
116
- )
139
+ _CONNECTIONS[key] = ConnectionPool(conninfo=dsn, **kwargs)
140
+
117
141
  return _CONNECTIONS[key]
118
142
 
119
143
 
120
- async def execute(
144
+ def execute(
121
145
  query: str,
122
146
  params: dict | None = None,
123
147
  return_type: Literal['dict', 'list'] = 'list',
124
148
  dsn: str | None = None,
125
149
  cls: type | None = None,
126
- ) -> list[dict] | list[object] | dict:
150
+ loads: Callable | None = None,
151
+ ) -> list[dict] | list[object] | dict | None:
127
152
  """
128
153
  Execute a query and return the results.
129
154
  If return_type is a class, return a list of instances of that class.
@@ -136,25 +161,48 @@ async def execute(
136
161
  return_type (Literal['dict', 'list'], optional): The type of return value. Defaults to 'list'.
137
162
  dsn (str | None, optional): The DSN to use for the connection. Defaults to None.
138
163
  cls (type | None, optional): The class to map the results to. Defaults to None.
164
+ loads (Callable | None, optional): Optional function to process each value. Defaults to None.
165
+ retry (int, optional): The current retry count. Defaults to 0.
139
166
  """
140
- conn = Transaction.connection.get()
167
+ conn: Connection = Transaction.connection.get()
141
168
  if not conn:
142
169
  pool: ConnectionPool = get_pool(dsn=dsn)
143
- conn = await pool.connection()
170
+ conn: Connection = pool.getconn()
171
+ is_transactional = False
144
172
  log_message = 'PostgreSQL query executed.'
145
173
  else:
174
+ is_transactional = True
146
175
  log_message = 'PostgreSQL query executed within transaction.'
147
176
 
148
177
  _log(log_message, extra={'labels': {'query': query, 'params': params}})
149
- result: QueryResult = await conn.execute(query, params)
150
- if not Transaction.connection.get():
151
- conn.close()
152
178
 
153
- if cls:
154
- result = result.as_class(cls)
155
- if return_type == 'dict':
156
- return {row[cls._primary_key]: row for row in result}
179
+ row_factory = cls_row(cls, loads) if cls else dict_row(loads)
180
+ # For transactions we let it be controlled externally by the context manager
181
+ try:
182
+ with conn.cursor(row_factory=row_factory) as cur:
183
+ result = retry(cur.execute, {'query': query, 'params': params}, retries=3, exceptions=OperationalError)
184
+
185
+ if result.description:
186
+ result = cur.fetchall()
187
+ else:
188
+ result = None
189
+ except Exception:
190
+ # On error we need to rollback
191
+ if not is_transactional:
192
+ conn.rollback()
193
+ raise
194
+
195
+ else:
196
+ # Block that only executes if no exception was raised in the try block
197
+ if not is_transactional:
198
+ conn.commit()
199
+
200
+ finally:
201
+ # We only return the connection to the pool if we are not in a transaction
202
+ if not is_transactional:
203
+ pool.putconn(conn)
157
204
 
158
- return result
205
+ if result and cls and return_type == 'dict':
206
+ return {row[cls._primary_key]: row for row in result}
159
207
 
160
- return result.result()
208
+ return result
everysk/sql/model.py CHANGED
@@ -7,7 +7,6 @@
7
7
  # without authorization of EVERYSK TECHNOLOGIES is prohibited.
8
8
  #
9
9
  ###############################################################################
10
- import asyncio
11
10
  import inspect
12
11
  from copy import deepcopy
13
12
  from types import GenericAlias, UnionType
@@ -260,12 +259,8 @@ class BaseModel(dict, metaclass=BaseModelMetaClass):
260
259
  return_type: Literal['dict', 'list'] = 'list',
261
260
  klass: type | None = None,
262
261
  ) -> Any:
263
- loop = asyncio.get_event_loop()
264
262
  kwargs = {'query': query, 'params': params, 'return_type': return_type, 'dsn': cls._dsn, 'cls': klass}
265
- if loop.is_running():
266
- return asyncio.run_coroutine_threadsafe(execute(**kwargs), loop).result()
267
-
268
- return loop.run_until_complete(execute(**kwargs))
263
+ return execute(**kwargs)
269
264
 
270
265
  @classmethod
271
266
  def _generate_attributes(cls) -> None: