python3-commons 0.5.16__tar.gz → 0.5.18__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 python3-commons might be problematic. Click here for more details.

Files changed (48) hide show
  1. {python3_commons-0.5.16/src/python3_commons.egg-info → python3_commons-0.5.18}/PKG-INFO +1 -1
  2. {python3_commons-0.5.16 → python3_commons-0.5.18}/src/python3_commons/audit.py +23 -10
  3. {python3_commons-0.5.16 → python3_commons-0.5.18}/src/python3_commons/helpers.py +22 -0
  4. {python3_commons-0.5.16 → python3_commons-0.5.18}/src/python3_commons/object_storage.py +21 -23
  5. {python3_commons-0.5.16 → python3_commons-0.5.18}/src/python3_commons/serializers/msgspec.py +6 -2
  6. python3_commons-0.5.18/src/python3_commons/stream_tar.py +105 -0
  7. {python3_commons-0.5.16 → python3_commons-0.5.18/src/python3_commons.egg-info}/PKG-INFO +1 -1
  8. {python3_commons-0.5.16 → python3_commons-0.5.18}/src/python3_commons.egg-info/SOURCES.txt +1 -0
  9. python3_commons-0.5.18/tests/test_audit.py +29 -0
  10. python3_commons-0.5.16/tests/test_audit.py +0 -10
  11. {python3_commons-0.5.16 → python3_commons-0.5.18}/.coveragerc +0 -0
  12. {python3_commons-0.5.16 → python3_commons-0.5.18}/.github/workflows/python-publish.yaml +0 -0
  13. {python3_commons-0.5.16 → python3_commons-0.5.18}/.gitignore +0 -0
  14. {python3_commons-0.5.16 → python3_commons-0.5.18}/AUTHORS.rst +0 -0
  15. {python3_commons-0.5.16 → python3_commons-0.5.18}/CHANGELOG.rst +0 -0
  16. {python3_commons-0.5.16 → python3_commons-0.5.18}/LICENSE +0 -0
  17. {python3_commons-0.5.16 → python3_commons-0.5.18}/README.md +0 -0
  18. {python3_commons-0.5.16 → python3_commons-0.5.18}/README.rst +0 -0
  19. {python3_commons-0.5.16 → python3_commons-0.5.18}/docs/Makefile +0 -0
  20. {python3_commons-0.5.16 → python3_commons-0.5.18}/docs/_static/.gitignore +0 -0
  21. {python3_commons-0.5.16 → python3_commons-0.5.18}/docs/authors.rst +0 -0
  22. {python3_commons-0.5.16 → python3_commons-0.5.18}/docs/changelog.rst +0 -0
  23. {python3_commons-0.5.16 → python3_commons-0.5.18}/docs/conf.py +0 -0
  24. {python3_commons-0.5.16 → python3_commons-0.5.18}/docs/index.rst +0 -0
  25. {python3_commons-0.5.16 → python3_commons-0.5.18}/docs/license.rst +0 -0
  26. {python3_commons-0.5.16 → python3_commons-0.5.18}/pyproject.toml +0 -0
  27. {python3_commons-0.5.16 → python3_commons-0.5.18}/requirements.txt +0 -0
  28. {python3_commons-0.5.16 → python3_commons-0.5.18}/requirements_dev.txt +0 -0
  29. {python3_commons-0.5.16 → python3_commons-0.5.18}/requirements_test.txt +0 -0
  30. {python3_commons-0.5.16 → python3_commons-0.5.18}/setup.cfg +0 -0
  31. {python3_commons-0.5.16 → python3_commons-0.5.18}/setup.py +0 -0
  32. {python3_commons-0.5.16 → python3_commons-0.5.18}/src/python3_commons/__init__.py +0 -0
  33. {python3_commons-0.5.16 → python3_commons-0.5.18}/src/python3_commons/conf.py +0 -0
  34. {python3_commons-0.5.16 → python3_commons-0.5.18}/src/python3_commons/db.py +0 -0
  35. {python3_commons-0.5.16 → python3_commons-0.5.18}/src/python3_commons/fs.py +0 -0
  36. {python3_commons-0.5.16 → python3_commons-0.5.18}/src/python3_commons/logging/__init__.py +0 -0
  37. {python3_commons-0.5.16 → python3_commons-0.5.18}/src/python3_commons/logging/filters.py +0 -0
  38. {python3_commons-0.5.16 → python3_commons-0.5.18}/src/python3_commons/logging/formatter.py +0 -0
  39. {python3_commons-0.5.16 → python3_commons-0.5.18}/src/python3_commons/serializers/__init__.py +0 -0
  40. {python3_commons-0.5.16 → python3_commons-0.5.18}/src/python3_commons/serializers/json.py +0 -0
  41. {python3_commons-0.5.16 → python3_commons-0.5.18}/src/python3_commons/serializers/msgpack.py +0 -0
  42. {python3_commons-0.5.16 → python3_commons-0.5.18}/src/python3_commons.egg-info/dependency_links.txt +0 -0
  43. {python3_commons-0.5.16 → python3_commons-0.5.18}/src/python3_commons.egg-info/not-zip-safe +0 -0
  44. {python3_commons-0.5.16 → python3_commons-0.5.18}/src/python3_commons.egg-info/requires.txt +0 -0
  45. {python3_commons-0.5.16 → python3_commons-0.5.18}/src/python3_commons.egg-info/top_level.txt +0 -0
  46. {python3_commons-0.5.16 → python3_commons-0.5.18}/tests/conftest.py +0 -0
  47. {python3_commons-0.5.16 → python3_commons-0.5.18}/tests/test_msgpack.py +0 -0
  48. {python3_commons-0.5.16 → python3_commons-0.5.18}/tests/test_msgspec.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python3-commons
3
- Version: 0.5.16
3
+ Version: 0.5.18
4
4
  Summary: Re-usable Python3 code
5
5
  Home-page: https://github.com/kamikaze/python3-commons
6
6
  Author: Oleg Korsak
@@ -13,7 +13,7 @@ from zeep.wsdl.definitions import AbstractOperation
13
13
 
14
14
  from python3_commons import object_storage
15
15
  from python3_commons.conf import S3Settings, s3_settings
16
- from python3_commons.object_storage import get_s3_client
16
+ from python3_commons.object_storage import ObjectStorage
17
17
 
18
18
  logger = logging.getLogger(__name__)
19
19
 
@@ -24,16 +24,27 @@ class GeneratedStream(io.BytesIO):
24
24
  self.generator = generator
25
25
 
26
26
  def read(self, size: int = -1):
27
- if size == -1:
28
- size = 4096
27
+ if size < 0:
28
+ while True:
29
+ try:
30
+ chunk = next(self.generator)
31
+ except StopIteration:
32
+ break
33
+ else:
34
+ self.write(chunk)
35
+ else:
36
+ total_written_size = 0
29
37
 
30
- while self.tell() < size:
31
- try:
32
- chunk = next(self.generator)
33
- except StopIteration:
34
- break
38
+ while total_written_size < size:
39
+ try:
40
+ chunk = next(self.generator)
41
+ except StopIteration:
42
+ break
43
+ else:
44
+ self.write(chunk)
45
+ total_written_size += len(chunk)
35
46
 
36
- self.write(chunk)
47
+ self.seek(0)
37
48
 
38
49
  if chunk := super().read(size):
39
50
  pos = self.tell()
@@ -44,6 +55,8 @@ class GeneratedStream(io.BytesIO):
44
55
  if unread_data_size > 0:
45
56
  buf[:unread_data_size] = buf[pos:pos+unread_data_size]
46
57
 
58
+ del buf
59
+
47
60
  self.seek(0)
48
61
  self.truncate(unread_data_size)
49
62
 
@@ -82,7 +95,7 @@ def generate_archive(objects: Iterable[tuple[str, datetime, bytes]],
82
95
  def write_audit_data_sync(settings: S3Settings, key: str, data: bytes):
83
96
  if settings.s3_secret_access_key:
84
97
  try:
85
- client = get_s3_client(settings)
98
+ client = ObjectStorage(settings).get_client()
86
99
  absolute_path = object_storage.get_absolute_path(f'audit/{key}')
87
100
 
88
101
  client.put_object(settings.s3_bucket, absolute_path, io.BytesIO(data), len(data))
@@ -1,6 +1,7 @@
1
1
  import datetime
2
2
  import logging
3
3
  import shlex
4
+ import threading
4
5
 
5
6
  from decimal import Decimal, ROUND_HALF_UP
6
7
  from typing import Mapping
@@ -8,6 +9,27 @@ from typing import Mapping
8
9
  logger = logging.getLogger(__name__)
9
10
 
10
11
 
12
+ class SingletonMeta(type):
13
+ """
14
+ A metaclass that creates a Singleton base class when called.
15
+ """
16
+ _instances = {}
17
+ _lock = threading.Lock()
18
+
19
+ def __call__(cls, *args, **kwargs):
20
+ try:
21
+ return cls._instances[cls]
22
+ except KeyError:
23
+ with cls._lock:
24
+ try:
25
+ return cls._instances[cls]
26
+ except KeyError:
27
+ instance = super(SingletonMeta, cls).__call__(*args, **kwargs)
28
+ cls._instances[cls] = instance
29
+
30
+ return instance
31
+
32
+
11
33
  def date_from_string(string: str, fmt: str = '%d.%m.%Y') -> datetime.date:
12
34
  try:
13
35
  return datetime.datetime.strptime(string, fmt).date()
@@ -1,5 +1,6 @@
1
1
  import io
2
2
  import logging
3
+ from contextlib import contextmanager
3
4
  from datetime import datetime
4
5
  from typing import Generator, Iterable
5
6
 
@@ -8,16 +9,17 @@ from minio.datatypes import Object
8
9
  from minio.deleteobjects import DeleteObject, DeleteError
9
10
 
10
11
  from python3_commons.conf import s3_settings, S3Settings
12
+ from python3_commons.helpers import SingletonMeta
11
13
 
12
14
  logger = logging.getLogger(__name__)
13
- __CLIENT = None
14
15
 
15
16
 
16
- def get_s3_client(settings: S3Settings) -> Minio:
17
- global __CLIENT
17
+ class ObjectStorage(metaclass=SingletonMeta):
18
+ def __init__(self, settings: S3Settings):
19
+ if not s3_settings.s3_endpoint_url:
20
+ raise ValueError('s3_settings.s3_endpoint_url must be set')
18
21
 
19
- if not __CLIENT and s3_settings.s3_endpoint_url:
20
- __CLIENT = Minio(
22
+ self._client = Minio(
21
23
  settings.s3_endpoint_url,
22
24
  region=settings.s3_region_name,
23
25
  access_key=settings.s3_access_key_id.get_secret_value(),
@@ -26,7 +28,8 @@ def get_s3_client(settings: S3Settings) -> Minio:
26
28
  cert_check=settings.s3_cert_verify
27
29
  )
28
30
 
29
- return __CLIENT
31
+ def get_client(self) -> Minio:
32
+ return self._client
30
33
 
31
34
 
32
35
  def get_absolute_path(path: str) -> str:
@@ -40,9 +43,7 @@ def get_absolute_path(path: str) -> str:
40
43
 
41
44
 
42
45
  def put_object(bucket_name: str, path: str, data: io.BytesIO, length: int, part_size: int = 0) -> str:
43
- s3_client = get_s3_client(s3_settings)
44
-
45
- if s3_client:
46
+ if s3_client := ObjectStorage(s3_settings).get_client():
46
47
  result = s3_client.put_object(bucket_name, path, data, length, part_size=part_size)
47
48
 
48
49
  logger.debug(f'Stored object into object storage: {bucket_name}:{path}')
@@ -52,10 +53,9 @@ def put_object(bucket_name: str, path: str, data: io.BytesIO, length: int, part_
52
53
  logger.warning(f'No S3 client available, skipping object put')
53
54
 
54
55
 
56
+ @contextmanager
55
57
  def get_object_stream(bucket_name: str, path: str):
56
- s3_client = get_s3_client(s3_settings)
57
-
58
- if s3_client:
58
+ if s3_client := ObjectStorage(s3_settings).get_client():
59
59
  logger.debug(f'Getting object from object storage: {bucket_name}:{path}')
60
60
 
61
61
  try:
@@ -65,19 +65,17 @@ def get_object_stream(bucket_name: str, path: str):
65
65
 
66
66
  raise
67
67
 
68
- return response
68
+ yield response
69
+
70
+ response.close()
71
+ response.release_conn()
69
72
  else:
70
73
  logger.warning(f'No S3 client available, skipping object put')
71
74
 
72
75
 
73
76
  def get_object(bucket_name: str, path: str) -> bytes:
74
- response = get_object_stream(bucket_name, path)
75
-
76
- try:
77
- body = response.read()
78
- finally:
79
- response.close()
80
- response.release_conn()
77
+ with get_object_stream(bucket_name, path) as stream:
78
+ body = stream.read()
81
79
 
82
80
  logger.debug(f'Loaded object from object storage: {bucket_name}:{path}')
83
81
 
@@ -85,7 +83,7 @@ def get_object(bucket_name: str, path: str) -> bytes:
85
83
 
86
84
 
87
85
  def list_objects(bucket_name: str, prefix: str, recursive: bool = True) -> Generator[Object, None, None]:
88
- s3_client = get_s3_client(s3_settings)
86
+ s3_client = ObjectStorage(s3_settings).get_client()
89
87
 
90
88
  yield from s3_client.list_objects(bucket_name, prefix=prefix, recursive=recursive)
91
89
 
@@ -104,13 +102,13 @@ def get_objects(bucket_name: str, path: str,
104
102
 
105
103
 
106
104
  def remove_object(bucket_name: str, object_name: str):
107
- s3_client = get_s3_client(s3_settings)
105
+ s3_client = ObjectStorage(s3_settings).get_client()
108
106
  s3_client.remove_object(bucket_name, object_name)
109
107
 
110
108
 
111
109
  def remove_objects(bucket_name: str, prefix: str = None,
112
110
  object_names: Iterable[str] = None) -> Iterable[DeleteError] | None:
113
- s3_client = get_s3_client(s3_settings)
111
+ s3_client = ObjectStorage(s3_settings).get_client()
114
112
 
115
113
  if prefix:
116
114
  delete_object_list = map(
@@ -40,8 +40,12 @@ def ext_hook(code: int, data: memoryview) -> Any:
40
40
  raise NotImplementedError(f'Extension type code {code} is not supported')
41
41
 
42
42
 
43
+ MSGPACK_ENCODER = msgpack.Encoder(enc_hook=enc_hook)
44
+ MSGPACK_DECODER = msgpack.Decoder(ext_hook=ext_hook)
45
+
46
+
43
47
  def serialize_msgpack(data) -> bytes:
44
- result = msgpack.encode(data, enc_hook=enc_hook)
48
+ result = MSGPACK_ENCODER.encode(data)
45
49
 
46
50
  return result
47
51
 
@@ -50,6 +54,6 @@ def deserialize_msgpack(data: bytes, data_type=None):
50
54
  if data_type:
51
55
  result = msgpack.decode(data, type=data_type)
52
56
  else:
53
- result = msgpack.decode(data, ext_hook=ext_hook)
57
+ result = MSGPACK_DECODER.decode(data)
54
58
 
55
59
  return result
@@ -0,0 +1,105 @@
1
+ import logging
2
+ import os
3
+ import sys
4
+ import tarfile
5
+ from io import BytesIO
6
+
7
+
8
+ logger = logging.getLogger(__name__)
9
+ BLOCK_SIZE = 4096
10
+
11
+
12
+ class FileStream(object):
13
+ def __init__(self):
14
+ self.buffer = BytesIO()
15
+ self.offset = 0
16
+
17
+ def write(self, chunk):
18
+ self.buffer.write(chunk)
19
+ self.offset += len(chunk)
20
+
21
+ def tell(self):
22
+ return self.offset
23
+
24
+ def close(self):
25
+ self.buffer.close()
26
+
27
+ def pop(self):
28
+ value = self.buffer.getvalue()
29
+ self.buffer.seek(0)
30
+ self.buffer.truncate()
31
+
32
+ return value
33
+
34
+
35
+ def stream_build_tar(in_filename, streaming_fp):
36
+ tar = tarfile.TarFile.open('test.tar.gz', 'w|gz', streaming_fp)
37
+
38
+ stat = os.stat(in_filename)
39
+
40
+ tar_info = tarfile.TarInfo(in_filename)
41
+
42
+ # Note that you can get this information from the storage backend,
43
+ # but it's valid for either to raise a NotImplementedError, so it's
44
+ # important to check.
45
+ #
46
+ # Things like the mode or ownership won't be available.
47
+ tar_info.mtime = stat.st_mtime
48
+ tar_info.size = stat.st_size
49
+
50
+ # Note that we don't pass a fileobj, so we don't write any data
51
+ # through addfile. We'll do this ourselves.
52
+ tar.addfile(tar_info)
53
+
54
+ yield
55
+
56
+ with open(in_filename, 'rb') as in_fp:
57
+ total_size = 0
58
+
59
+ while True:
60
+ s = in_fp.read(BLOCK_SIZE)
61
+
62
+ if len(s) > 0:
63
+ tar.fileobj.write(s)
64
+
65
+ yield
66
+
67
+ if len(s) < BLOCK_SIZE:
68
+ blocks, remainder = divmod(tar_info.size, tarfile.BLOCKSIZE)
69
+
70
+ if remainder > 0:
71
+ tar.fileobj.write(tarfile.NUL *
72
+ (tarfile.BLOCKSIZE - remainder))
73
+
74
+ yield
75
+
76
+ blocks += 1
77
+
78
+ tar.offset += blocks * tarfile.BLOCKSIZE
79
+ break
80
+
81
+ tar.close()
82
+
83
+ yield
84
+
85
+
86
+ def main():
87
+ if len(sys.argv) != 3:
88
+ print('Usage: %s in_filename out_filename' % sys.argv[0])
89
+ sys.exit(1)
90
+
91
+ in_filename = sys.argv[1]
92
+ out_filename = sys.argv[2]
93
+
94
+ streaming_fp = FileStream()
95
+
96
+ with open(out_filename, 'wb') as out_fp:
97
+ for i in stream_build_tar(in_filename, streaming_fp):
98
+ s = streaming_fp.pop()
99
+
100
+ if len(s) > 0:
101
+ print('Writing %d bytes...' % len(s))
102
+ out_fp.write(s)
103
+ out_fp.flush()
104
+
105
+ print('Wrote tar file to %s' % out_filename)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python3-commons
3
- Version: 0.5.16
3
+ Version: 0.5.18
4
4
  Summary: Re-usable Python3 code
5
5
  Home-page: https://github.com/kamikaze/python3-commons
6
6
  Author: Oleg Korsak
@@ -26,6 +26,7 @@ src/python3_commons/db.py
26
26
  src/python3_commons/fs.py
27
27
  src/python3_commons/helpers.py
28
28
  src/python3_commons/object_storage.py
29
+ src/python3_commons/stream_tar.py
29
30
  src/python3_commons.egg-info/PKG-INFO
30
31
  src/python3_commons.egg-info/SOURCES.txt
31
32
  src/python3_commons.egg-info/dependency_links.txt
@@ -0,0 +1,29 @@
1
+ from io import BytesIO
2
+
3
+ from python3_commons.audit import GeneratedStream, generate_archive
4
+
5
+
6
+ def test_generated_stream(s3_file_objects):
7
+ expected_data = b''
8
+ generator = generate_archive(s3_file_objects, chunk_size=5 * 1024 * 1024)
9
+ archive_stream = GeneratedStream(generator)
10
+ archived_data = archive_stream.read()
11
+
12
+ with open('/tmp/test.tar', 'wb') as f:
13
+ f.write(archived_data)
14
+
15
+ assert archived_data == expected_data
16
+
17
+
18
+ def test_generated_stream_by_chunks(s3_file_objects):
19
+ expected_data = b''
20
+ generator = generate_archive(s3_file_objects, chunk_size=2)
21
+ archive_stream = GeneratedStream(generator)
22
+ archived_data = BytesIO()
23
+
24
+ with open('/tmp/test_chunked.tar', 'wb') as f:
25
+ while chunk := archive_stream.read(2):
26
+ f.write(chunk)
27
+ archived_data.write(chunk)
28
+
29
+ assert archived_data.getvalue() == expected_data
@@ -1,10 +0,0 @@
1
- from python3_commons.audit import GeneratedStream, generate_archive
2
-
3
-
4
- def test_generated_stream(s3_file_objects):
5
- expected_data = b''
6
- generator = generate_archive(s3_file_objects, chunk_size=5 * 1024 * 1024)
7
- archive_stream = GeneratedStream(generator)
8
- archived_data = archive_stream.read()
9
-
10
- assert archived_data == expected_data