python3-commons 0.6.7__tar.gz → 0.6.8__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.
Files changed (48) hide show
  1. {python3_commons-0.6.7/src/python3_commons.egg-info → python3_commons-0.6.8}/PKG-INFO +1 -1
  2. {python3_commons-0.6.7 → python3_commons-0.6.8}/pyproject.toml +1 -1
  3. python3_commons-0.6.8/src/python3_commons/api_client.py +56 -0
  4. {python3_commons-0.6.7 → python3_commons-0.6.8}/src/python3_commons/audit.py +12 -8
  5. {python3_commons-0.6.7 → python3_commons-0.6.8}/src/python3_commons/helpers.py +38 -18
  6. {python3_commons-0.6.7 → python3_commons-0.6.8}/src/python3_commons/object_storage.py +1 -1
  7. {python3_commons-0.6.7 → python3_commons-0.6.8/src/python3_commons.egg-info}/PKG-INFO +1 -1
  8. {python3_commons-0.6.7 → python3_commons-0.6.8}/tests/test_msgspec.py +22 -1
  9. python3_commons-0.6.7/src/python3_commons/api_client.py +0 -44
  10. {python3_commons-0.6.7 → python3_commons-0.6.8}/.coveragerc +0 -0
  11. {python3_commons-0.6.7 → python3_commons-0.6.8}/.github/workflows/python-publish.yaml +0 -0
  12. {python3_commons-0.6.7 → python3_commons-0.6.8}/.gitignore +0 -0
  13. {python3_commons-0.6.7 → python3_commons-0.6.8}/AUTHORS.rst +0 -0
  14. {python3_commons-0.6.7 → python3_commons-0.6.8}/CHANGELOG.rst +0 -0
  15. {python3_commons-0.6.7 → python3_commons-0.6.8}/LICENSE +0 -0
  16. {python3_commons-0.6.7 → python3_commons-0.6.8}/README.md +0 -0
  17. {python3_commons-0.6.7 → python3_commons-0.6.8}/README.rst +0 -0
  18. {python3_commons-0.6.7 → python3_commons-0.6.8}/docs/Makefile +0 -0
  19. {python3_commons-0.6.7 → python3_commons-0.6.8}/docs/_static/.gitignore +0 -0
  20. {python3_commons-0.6.7 → python3_commons-0.6.8}/docs/authors.rst +0 -0
  21. {python3_commons-0.6.7 → python3_commons-0.6.8}/docs/changelog.rst +0 -0
  22. {python3_commons-0.6.7 → python3_commons-0.6.8}/docs/conf.py +0 -0
  23. {python3_commons-0.6.7 → python3_commons-0.6.8}/docs/index.rst +0 -0
  24. {python3_commons-0.6.7 → python3_commons-0.6.8}/docs/license.rst +0 -0
  25. {python3_commons-0.6.7 → python3_commons-0.6.8}/requirements.txt +0 -0
  26. {python3_commons-0.6.7 → python3_commons-0.6.8}/requirements_dev.txt +0 -0
  27. {python3_commons-0.6.7 → python3_commons-0.6.8}/requirements_test.txt +0 -0
  28. {python3_commons-0.6.7 → python3_commons-0.6.8}/setup.cfg +0 -0
  29. {python3_commons-0.6.7 → python3_commons-0.6.8}/setup.py +0 -0
  30. {python3_commons-0.6.7 → python3_commons-0.6.8}/src/python3_commons/__init__.py +0 -0
  31. {python3_commons-0.6.7 → python3_commons-0.6.8}/src/python3_commons/conf.py +0 -0
  32. {python3_commons-0.6.7 → python3_commons-0.6.8}/src/python3_commons/db.py +0 -0
  33. {python3_commons-0.6.7 → python3_commons-0.6.8}/src/python3_commons/fs.py +0 -0
  34. {python3_commons-0.6.7 → python3_commons-0.6.8}/src/python3_commons/logging/__init__.py +0 -0
  35. {python3_commons-0.6.7 → python3_commons-0.6.8}/src/python3_commons/logging/filters.py +0 -0
  36. {python3_commons-0.6.7 → python3_commons-0.6.8}/src/python3_commons/logging/formatters.py +0 -0
  37. {python3_commons-0.6.7 → python3_commons-0.6.8}/src/python3_commons/serializers/__init__.py +0 -0
  38. {python3_commons-0.6.7 → python3_commons-0.6.8}/src/python3_commons/serializers/json.py +0 -0
  39. {python3_commons-0.6.7 → python3_commons-0.6.8}/src/python3_commons/serializers/msgpack.py +0 -0
  40. {python3_commons-0.6.7 → python3_commons-0.6.8}/src/python3_commons/serializers/msgspec.py +0 -0
  41. {python3_commons-0.6.7 → python3_commons-0.6.8}/src/python3_commons.egg-info/SOURCES.txt +0 -0
  42. {python3_commons-0.6.7 → python3_commons-0.6.8}/src/python3_commons.egg-info/dependency_links.txt +0 -0
  43. {python3_commons-0.6.7 → python3_commons-0.6.8}/src/python3_commons.egg-info/requires.txt +0 -0
  44. {python3_commons-0.6.7 → python3_commons-0.6.8}/src/python3_commons.egg-info/top_level.txt +0 -0
  45. {python3_commons-0.6.7 → python3_commons-0.6.8}/tests/conftest.py +0 -0
  46. {python3_commons-0.6.7 → python3_commons-0.6.8}/tests/test_audit.py +0 -0
  47. {python3_commons-0.6.7 → python3_commons-0.6.8}/tests/test_helpers.py +0 -0
  48. {python3_commons-0.6.7 → python3_commons-0.6.8}/tests/test_msgpack.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: python3-commons
3
- Version: 0.6.7
3
+ Version: 0.6.8
4
4
  Summary: Re-usable Python3 code
5
5
  Author-email: Oleg Korsak <kamikaze.is.waiting.you@gmail.com>
6
6
  License: gpl-3
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python3-commons"
7
- version = "0.6.7"
7
+ version = "0.6.8"
8
8
  description = "Re-usable Python3 code"
9
9
  authors = [
10
10
  {name = "Oleg Korsak", email = "kamikaze.is.waiting.you@gmail.com"}
@@ -0,0 +1,56 @@
1
+ from contextlib import asynccontextmanager
2
+ from datetime import datetime, UTC
3
+ from typing import AsyncGenerator, Literal, Mapping, Sequence
4
+
5
+ from aiohttp import ClientSession
6
+ from aiohttp.web_response import Response
7
+ from pydantic import HttpUrl
8
+
9
+ from python3_commons import audit
10
+ from python3_commons.conf import s3_settings
11
+ from python3_commons.helpers import request_to_curl
12
+
13
+
14
+ @asynccontextmanager
15
+ async def request(
16
+ client: ClientSession,
17
+ base_url: HttpUrl,
18
+ uri: str,
19
+ query: Mapping | None = None,
20
+ method: Literal['get', 'post', 'put', 'patch', 'options', 'head', 'delete'] = 'get',
21
+ headers: Mapping | None = None,
22
+ json: Mapping | Sequence | str | None = None,
23
+ data: bytes | None = None,
24
+ audit_name: str | None = None
25
+ ) -> AsyncGenerator[Response]:
26
+ now = datetime.now(tz=UTC)
27
+ date_path = now.strftime('%Y/%m/%d')
28
+ timestamp = now.strftime('%H%M%S_%f')
29
+ uri_path = uri[:-1] if uri.endswith('/') else uri
30
+ uri_path = uri_path[1:] if uri_path.startswith('/') else uri_path
31
+ url = f'{base_url}{uri}'
32
+
33
+ if audit_name:
34
+ curl_request = None
35
+
36
+ if method == 'get':
37
+ if query:
38
+ curl_request = request_to_curl(url, query, method, headers)
39
+ else:
40
+ curl_request = request_to_curl(url, query, method, headers, json, data)
41
+
42
+ if curl_request:
43
+ await audit.write_audit_data(
44
+ s3_settings,
45
+ f'{date_path}/{audit_name}/{uri_path}/{method}_{timestamp}_request.txt',
46
+ curl_request.encode('utf-8')
47
+ )
48
+
49
+ client_method = getattr(client, method)
50
+
51
+ if method == 'get':
52
+ async with client_method(url, params=query) as response:
53
+ yield response
54
+ else:
55
+ async with client_method(url, params=query, json=json, data=data) as response:
56
+ yield response
@@ -3,6 +3,7 @@ import io
3
3
  import logging
4
4
  import tarfile
5
5
  from bz2 import BZ2Compressor
6
+ from collections import deque
6
7
  from datetime import datetime, timedelta, UTC
7
8
  from typing import Generator, Iterable
8
9
  from uuid import uuid4
@@ -42,8 +43,7 @@ class GeneratedStream(io.BytesIO):
42
43
  except StopIteration:
43
44
  break
44
45
  else:
45
- self.write(chunk)
46
- total_written_size += len(chunk)
46
+ total_written_size += self.write(chunk)
47
47
 
48
48
  self.seek(0)
49
49
 
@@ -69,7 +69,7 @@ class GeneratedStream(io.BytesIO):
69
69
 
70
70
  def generate_archive(objects: Iterable[tuple[str, datetime, bytes]],
71
71
  chunk_size: int = 4096) -> Generator[bytes, None, None]:
72
- buffer = io.BytesIO()
72
+ buffer = deque()
73
73
 
74
74
  with tarfile.open(fileobj=buffer, mode='w') as archive:
75
75
  for name, last_modified, content in objects:
@@ -79,18 +79,22 @@ def generate_archive(objects: Iterable[tuple[str, datetime, bytes]],
79
79
  info.mtime = last_modified.timestamp()
80
80
  archive.addfile(info, io.BytesIO(content))
81
81
 
82
- buffer.seek(0)
82
+ buffer_length = buffer.tell()
83
83
 
84
- while True:
84
+ while buffer_length >= chunk_size:
85
+ buffer.seek(0)
85
86
  chunk = buffer.read(chunk_size)
87
+ chunk_len = len(chunk)
86
88
 
87
89
  if not chunk:
88
90
  break
89
91
 
90
92
  yield chunk
91
93
 
92
- buffer.seek(0)
93
- buffer.truncate(0)
94
+ buffer.seek(0)
95
+ buffer.truncate(chunk_len)
96
+ buffer.seek(0, io.SEEK_END)
97
+ buffer_length = buffer.tell()
94
98
 
95
99
  while True:
96
100
  chunk = buffer.read(chunk_size)
@@ -145,7 +149,7 @@ async def archive_audit_data(root_path: str = 'audit'):
145
149
  if objects := object_storage.get_objects(bucket_name, date_path, recursive=True):
146
150
  logger.info(f'Compacting files in: {date_path}')
147
151
 
148
- generator = generate_archive(objects, chunk_size=5*1024*1024)
152
+ generator = generate_archive(objects, chunk_size=900_000)
149
153
  bzip2_generator = generate_bzip2(generator)
150
154
  archive_stream = GeneratedStream(bzip2_generator)
151
155
 
@@ -1,10 +1,13 @@
1
- import datetime
2
1
  import logging
3
2
  import shlex
4
3
  import threading
5
-
4
+ from datetime import date, datetime, timedelta
6
5
  from decimal import Decimal, ROUND_HALF_UP
7
- from typing import Mapping
6
+ from json import dumps
7
+ from typing import Literal, Mapping, Sequence
8
+ from urllib.parse import urlencode
9
+
10
+ from python3_commons.serializers.json import CustomJSONEncoder
8
11
 
9
12
  logger = logging.getLogger(__name__)
10
13
 
@@ -30,23 +33,23 @@ class SingletonMeta(type):
30
33
  return instance
31
34
 
32
35
 
33
- def date_from_string(string: str, fmt: str = '%d.%m.%Y') -> datetime.date:
36
+ def date_from_string(string: str, fmt: str = '%d.%m.%Y') -> date:
34
37
  try:
35
- return datetime.datetime.strptime(string, fmt).date()
38
+ return datetime.strptime(string, fmt).date()
36
39
  except ValueError:
37
- return datetime.date.fromisoformat(string)
40
+ return date.fromisoformat(string)
38
41
 
39
42
 
40
- def datetime_from_string(string: str) -> datetime.datetime:
43
+ def datetime_from_string(string: str) -> datetime:
41
44
  try:
42
- return datetime.datetime.strptime(string, '%d.%m.%Y %H:%M:%S')
45
+ return datetime.strptime(string, '%d.%m.%Y %H:%M:%S')
43
46
  except ValueError:
44
- return datetime.datetime.fromisoformat(string)
47
+ return datetime.fromisoformat(string)
45
48
 
46
49
 
47
50
  def date_range(start_date, end_date):
48
51
  for n in range(int((end_date - start_date).days + 1)):
49
- yield start_date + datetime.timedelta(days=n)
52
+ yield start_date + timedelta(days=n)
50
53
 
51
54
 
52
55
  def tries(times):
@@ -72,16 +75,33 @@ def round_decimal(value: Decimal, decimal_places=2, rounding_mode=ROUND_HALF_UP)
72
75
  return value
73
76
 
74
77
 
75
- def request_to_curl(url: str, method: str, headers: Mapping, body: bytes | None = None) -> str:
76
- curl_cmd = ['curl', '-i', '-X', method, shlex.quote(url)]
78
+ def request_to_curl(
79
+ url: str,
80
+ query: Mapping | None = None,
81
+ method: Literal['get', 'post', 'put', 'patch', 'options', 'head', 'delete'] = 'get',
82
+ headers: Mapping | None = None,
83
+ json: Mapping | Sequence | str | None = None,
84
+ data: bytes | None = None
85
+ ) -> str:
86
+ if query:
87
+ url = f'{url}?{urlencode(query)}'
88
+
89
+ curl_cmd = ['curl', '-i', '-X', method.upper(), shlex.quote(url)]
90
+
91
+ if headers:
92
+ for key, value in headers.items():
93
+ header_line = f'{key}: {value}'
94
+ curl_cmd.append('-H')
95
+ curl_cmd.append(shlex.quote(header_line))
77
96
 
78
- for key, value in headers.items():
79
- header_line = f'{key}: {value}'
97
+ if json:
80
98
  curl_cmd.append('-H')
81
- curl_cmd.append(shlex.quote(header_line))
99
+ curl_cmd.append(shlex.quote('Content-Type: application/json'))
82
100
 
83
- if body is not None:
84
- curl_cmd.append('--data')
85
- curl_cmd.append(shlex.quote(body.decode('utf-8')))
101
+ curl_cmd.append('-d')
102
+ curl_cmd.append(shlex.quote(dumps(json, cls=CustomJSONEncoder)))
103
+ elif data:
104
+ curl_cmd.append('-d')
105
+ curl_cmd.append(shlex.quote(data.decode('utf-8')))
86
106
 
87
107
  return ' '.join(curl_cmd)
@@ -42,7 +42,7 @@ def get_absolute_path(path: str) -> str:
42
42
  return path
43
43
 
44
44
 
45
- def put_object(bucket_name: str, path: str, data: io.BytesIO, length: int, part_size: int = 0) -> str:
45
+ def put_object(bucket_name: str, path: str, data: io.BytesIO, length: int, part_size: int = 0) -> str | None:
46
46
  if s3_client := ObjectStorage(s3_settings).get_client():
47
47
  result = s3_client.put_object(bucket_name, path, data, length, part_size=part_size)
48
48
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: python3-commons
3
- Version: 0.6.7
3
+ Version: 0.6.8
4
4
  Summary: Re-usable Python3 code
5
5
  Author-email: Oleg Korsak <kamikaze.is.waiting.you@gmail.com>
6
6
  License: gpl-3
@@ -1,7 +1,12 @@
1
+ from decimal import Decimal
2
+
1
3
  from python3_commons.serializers import msgspec
2
4
 
3
5
 
4
6
  def test_encode_decode_dict_to_msgpack(data_dict):
7
+ """
8
+ enc_hook is not being called on complex types like dict
9
+ """
5
10
  expected_result = {
6
11
  'A': 1,
7
12
  'B': 'B',
@@ -12,7 +17,7 @@ def test_encode_decode_dict_to_msgpack(data_dict):
12
17
  }
13
18
  binary_data = msgspec.serialize_msgpack(data_dict)
14
19
 
15
- assert msgspec.deserialize_msgpack(binary_data) == expected_result
20
+ assert msgspec.deserialize_msgpack(binary_data) == data_dict
16
21
 
17
22
 
18
23
  def test_encode_decode_dataclass_to_msgpack(data_dataclass):
@@ -33,3 +38,19 @@ def test_encode_decode_struct_to_msgpack_native(data_struct):
33
38
  decoded_struct = msgspec.deserialize_msgpack_native(binary_data, data_struct.__class__)
34
39
 
35
40
  assert decoded_struct == data_struct
41
+
42
+
43
+ def test_encode_decode_decimal_to_msgpack():
44
+ value = Decimal('1.2345')
45
+ binary_data = msgspec.serialize_msgpack(value)
46
+ decoded_value = msgspec.deserialize_msgpack(binary_data, Decimal)
47
+
48
+ assert decoded_value == value
49
+
50
+
51
+ def test_encode_decode_str_to_msgpack():
52
+ value = '1.2345'
53
+ binary_data = msgspec.serialize_msgpack(value)
54
+ decoded_value = msgspec.deserialize_msgpack(binary_data)
55
+
56
+ assert decoded_value == value
@@ -1,44 +0,0 @@
1
- from contextlib import asynccontextmanager
2
- from datetime import datetime, UTC
3
- from typing import Literal, Mapping
4
- from urllib.parse import urlencode
5
-
6
- import aiohttp
7
- from pydantic import HttpUrl
8
-
9
- from python3_commons import audit
10
- from python3_commons.conf import s3_settings
11
- from python3_commons.helpers import request_to_curl
12
-
13
-
14
- @asynccontextmanager
15
- async def request(
16
- base_url: HttpUrl,
17
- uri: str,
18
- params: Mapping | None = None,
19
- method: Literal['get', 'post', 'patch', 'put', 'delete'] = 'get',
20
- audit_name: str | None = None,
21
- ):
22
- now = datetime.now(tz=UTC)
23
- date_path = now.strftime('%Y/%m/%d')
24
- timestamp = now.strftime('%H%M%S_%f')
25
- uri_path = uri[:-1] if uri.endswith('/') else uri
26
- uri_path = uri_path[1:] if uri_path.startswith('/') else uri_path
27
- url = f'{base_url}{uri}'
28
-
29
- if params:
30
- url_with_params = f'{url}?{urlencode(params)}'
31
-
32
- if audit_name:
33
- curl_request = request_to_curl(url_with_params, method, {}, None)
34
- await audit.write_audit_data(
35
- s3_settings,
36
- f'{date_path}/{audit_name}/{uri_path}/{method}_{timestamp}_request.txt',
37
- curl_request.encode('utf-8')
38
- )
39
-
40
- async with aiohttp.ClientSession() as client:
41
- client_method = getattr(client, method)
42
-
43
- async with client_method(url, params=params) as response:
44
- yield response
File without changes