python3-commons 0.9.18__py3-none-any.whl → 0.9.20__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.
Potentially problematic release.
This version of python3-commons might be problematic. Click here for more details.
- python3_commons/api_client.py +34 -14
- python3_commons/audit.py +2 -2
- python3_commons/auth.py +27 -28
- python3_commons/cache.py +28 -27
- python3_commons/db/__init__.py +6 -6
- python3_commons/db/helpers.py +5 -7
- python3_commons/fs.py +2 -2
- python3_commons/helpers.py +5 -4
- python3_commons/object_storage.py +27 -28
- python3_commons/serializers/common.py +8 -0
- python3_commons/serializers/json.py +2 -4
- python3_commons/serializers/msgpack.py +14 -12
- python3_commons/serializers/msgspec.py +26 -17
- {python3_commons-0.9.18.dist-info → python3_commons-0.9.20.dist-info}/METADATA +4 -4
- python3_commons-0.9.20.dist-info/RECORD +30 -0
- python3_commons-0.9.18.dist-info/RECORD +0 -29
- {python3_commons-0.9.18.dist-info → python3_commons-0.9.20.dist-info}/WHEEL +0 -0
- {python3_commons-0.9.18.dist-info → python3_commons-0.9.20.dist-info}/licenses/AUTHORS.rst +0 -0
- {python3_commons-0.9.18.dist-info → python3_commons-0.9.20.dist-info}/licenses/LICENSE +0 -0
- {python3_commons-0.9.18.dist-info → python3_commons-0.9.20.dist-info}/top_level.txt +0 -0
python3_commons/api_client.py
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import errno
|
|
1
2
|
import logging
|
|
3
|
+
from collections.abc import AsyncGenerator, Mapping, Sequence
|
|
2
4
|
from contextlib import asynccontextmanager
|
|
3
5
|
from datetime import UTC, datetime
|
|
4
6
|
from enum import Enum
|
|
5
7
|
from http import HTTPStatus
|
|
6
8
|
from json import dumps
|
|
7
|
-
from typing import
|
|
9
|
+
from typing import Literal
|
|
8
10
|
from uuid import uuid4
|
|
9
11
|
|
|
10
12
|
from aiohttp import ClientResponse, ClientSession, ClientTimeout, client_exceptions
|
|
@@ -52,8 +54,8 @@ async def request(
|
|
|
52
54
|
date_path = now.strftime('%Y/%m/%d')
|
|
53
55
|
timestamp = now.strftime('%H%M%S_%f')
|
|
54
56
|
request_id = str(uuid4())[-12:]
|
|
55
|
-
uri_path = uri
|
|
56
|
-
uri_path = uri_path
|
|
57
|
+
uri_path = uri.removesuffix('/')
|
|
58
|
+
uri_path = uri_path.removeprefix('/')
|
|
57
59
|
url = f'{u[:-1] if (u := str(base_url)).endswith("/") else u}{uri}'
|
|
58
60
|
|
|
59
61
|
if audit_name:
|
|
@@ -90,15 +92,25 @@ async def request(
|
|
|
90
92
|
else:
|
|
91
93
|
match response.status:
|
|
92
94
|
case HTTPStatus.UNAUTHORIZED:
|
|
93
|
-
|
|
95
|
+
msg = 'Unauthorized'
|
|
96
|
+
|
|
97
|
+
raise PermissionError(msg)
|
|
94
98
|
case HTTPStatus.FORBIDDEN:
|
|
95
|
-
|
|
99
|
+
msg = 'Forbidden'
|
|
100
|
+
|
|
101
|
+
raise PermissionError(msg)
|
|
96
102
|
case HTTPStatus.NOT_FOUND:
|
|
97
|
-
|
|
103
|
+
msg = 'Not found'
|
|
104
|
+
|
|
105
|
+
raise LookupError(msg)
|
|
98
106
|
case HTTPStatus.BAD_REQUEST:
|
|
99
|
-
|
|
107
|
+
msg = 'Bad request'
|
|
108
|
+
|
|
109
|
+
raise ValueError(msg)
|
|
100
110
|
case HTTPStatus.TOO_MANY_REQUESTS:
|
|
101
|
-
|
|
111
|
+
msg = 'Too many requests'
|
|
112
|
+
|
|
113
|
+
raise InterruptedError(msg)
|
|
102
114
|
case _:
|
|
103
115
|
response.raise_for_status()
|
|
104
116
|
else:
|
|
@@ -116,13 +128,21 @@ async def request(
|
|
|
116
128
|
|
|
117
129
|
yield response
|
|
118
130
|
except client_exceptions.ClientConnectorError as e:
|
|
119
|
-
|
|
131
|
+
msg = 'Cient connection error'
|
|
132
|
+
|
|
133
|
+
raise ConnectionRefusedError(msg) from e
|
|
120
134
|
except client_exceptions.ClientOSError as e:
|
|
121
|
-
if e.errno ==
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
raise ConnectionResetError(
|
|
135
|
+
if e.errno == errno.EPIPE:
|
|
136
|
+
msg = 'Broken pipe'
|
|
137
|
+
|
|
138
|
+
raise ConnectionResetError(msg) from e
|
|
139
|
+
elif e.errno == errno.ECONNRESET:
|
|
140
|
+
msg = 'Connection reset by peer'
|
|
141
|
+
|
|
142
|
+
raise ConnectionResetError(msg) from e
|
|
125
143
|
|
|
126
144
|
raise
|
|
127
145
|
except client_exceptions.ServerDisconnectedError as e:
|
|
128
|
-
|
|
146
|
+
msg = 'Server disconnected'
|
|
147
|
+
|
|
148
|
+
raise ConnectionResetError(msg) from e
|
python3_commons/audit.py
CHANGED
|
@@ -143,8 +143,8 @@ async def write_audit_data(settings: S3Settings, key: str, data: bytes):
|
|
|
143
143
|
absolute_path = object_storage.get_absolute_path(f'audit/{key}')
|
|
144
144
|
|
|
145
145
|
await object_storage.put_object(settings.s3_bucket, absolute_path, io.BytesIO(data), len(data))
|
|
146
|
-
except Exception
|
|
147
|
-
logger.
|
|
146
|
+
except Exception:
|
|
147
|
+
logger.exception('Failed storing object in storage.')
|
|
148
148
|
else:
|
|
149
149
|
logger.debug(f'Stored object in storage: {key}')
|
|
150
150
|
else:
|
python3_commons/auth.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
from collections.abc import Callable, Coroutine, MutableMapping, Sequence
|
|
2
3
|
from http import HTTPStatus
|
|
3
|
-
from typing import Annotated, Any,
|
|
4
|
+
from typing import Annotated, Any, TypeVar
|
|
4
5
|
|
|
5
6
|
import aiohttp
|
|
6
7
|
import msgspec
|
|
@@ -21,10 +22,7 @@ class TokenData(msgspec.Struct):
|
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
T = TypeVar('T', bound=TokenData)
|
|
24
|
-
|
|
25
25
|
OIDC_CONFIG_URL = f'{oidc_settings.authority_url}/.well-known/openid-configuration'
|
|
26
|
-
_JWKS: dict | None = None
|
|
27
|
-
|
|
28
26
|
bearer_security = HTTPBearer(auto_error=oidc_settings.enabled)
|
|
29
27
|
|
|
30
28
|
|
|
@@ -32,58 +30,59 @@ async def fetch_openid_config() -> dict:
|
|
|
32
30
|
"""
|
|
33
31
|
Fetch the OpenID configuration (including JWKS URI) from OIDC authority.
|
|
34
32
|
"""
|
|
35
|
-
async with aiohttp.ClientSession() as session:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
)
|
|
33
|
+
async with aiohttp.ClientSession() as session, session.get(OIDC_CONFIG_URL) as response:
|
|
34
|
+
if response.status != HTTPStatus.OK:
|
|
35
|
+
raise HTTPException(
|
|
36
|
+
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail='Failed to fetch OpenID configuration'
|
|
37
|
+
)
|
|
41
38
|
|
|
42
|
-
|
|
39
|
+
return await response.json()
|
|
43
40
|
|
|
44
41
|
|
|
45
42
|
async def fetch_jwks(jwks_uri: str) -> dict:
|
|
46
43
|
"""
|
|
47
44
|
Fetch the JSON Web Key Set (JWKS) for validating the token's signature.
|
|
48
45
|
"""
|
|
49
|
-
async with aiohttp.ClientSession() as session:
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail='Failed to fetch JWKS')
|
|
46
|
+
async with aiohttp.ClientSession() as session, session.get(jwks_uri) as response:
|
|
47
|
+
if response.status != HTTPStatus.OK:
|
|
48
|
+
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail='Failed to fetch JWKS')
|
|
53
49
|
|
|
54
|
-
|
|
50
|
+
return await response.json()
|
|
55
51
|
|
|
56
52
|
|
|
57
|
-
def get_token_verifier
|
|
53
|
+
def get_token_verifier[T](
|
|
54
|
+
token_cls: type[T],
|
|
55
|
+
jwks: MutableMapping,
|
|
56
|
+
) -> Callable[[HTTPAuthorizationCredentials], Coroutine[Any, Any, T | None]]:
|
|
58
57
|
async def get_verified_token(
|
|
59
58
|
authorization: Annotated[HTTPAuthorizationCredentials, Depends(bearer_security)],
|
|
60
59
|
) -> T | None:
|
|
61
60
|
"""
|
|
62
61
|
Verify the JWT access token using OIDC authority JWKS.
|
|
63
62
|
"""
|
|
64
|
-
global _JWKS
|
|
65
|
-
|
|
66
63
|
if not oidc_settings.enabled:
|
|
67
64
|
return None
|
|
68
65
|
|
|
69
66
|
token = authorization.credentials
|
|
70
67
|
|
|
71
68
|
try:
|
|
72
|
-
if not
|
|
69
|
+
if not jwks:
|
|
73
70
|
openid_config = await fetch_openid_config()
|
|
74
|
-
|
|
71
|
+
_jwks = await fetch_jwks(openid_config['jwks_uri'])
|
|
72
|
+
jwks.clear()
|
|
73
|
+
jwks.update(_jwks)
|
|
75
74
|
|
|
76
75
|
if oidc_settings.client_id:
|
|
77
|
-
payload = jwt.decode(token,
|
|
76
|
+
payload = jwt.decode(token, jwks, algorithms=['RS256'], audience=oidc_settings.client_id)
|
|
78
77
|
else:
|
|
79
|
-
payload = jwt.decode(token,
|
|
78
|
+
payload = jwt.decode(token, jwks, algorithms=['RS256'])
|
|
80
79
|
|
|
81
80
|
token_data = token_cls(**payload)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
except jwt.ExpiredSignatureError:
|
|
85
|
-
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail='Token has expired')
|
|
81
|
+
except jwt.ExpiredSignatureError as e:
|
|
82
|
+
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail='Token has expired') from e
|
|
86
83
|
except JWTError as e:
|
|
87
|
-
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail=f'Token is invalid: {str(e)}')
|
|
84
|
+
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail=f'Token is invalid: {str(e)}') from e
|
|
85
|
+
|
|
86
|
+
return token_data
|
|
88
87
|
|
|
89
88
|
return get_verified_token
|
python3_commons/cache.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import socket
|
|
3
|
+
from collections.abc import Mapping, Sequence
|
|
3
4
|
from platform import platform
|
|
4
|
-
from typing import Any
|
|
5
|
+
from typing import Any
|
|
5
6
|
|
|
6
7
|
import valkey
|
|
7
8
|
from pydantic import RedisDsn
|
|
@@ -34,7 +35,7 @@ class AsyncValkeyClient(metaclass=SingletonMeta):
|
|
|
34
35
|
|
|
35
36
|
@staticmethod
|
|
36
37
|
def _get_keepalive_options():
|
|
37
|
-
if platform
|
|
38
|
+
if platform in {'linux', 'darwin'}:
|
|
38
39
|
return {socket.TCP_KEEPIDLE: 10, socket.TCP_KEEPINTVL: 5, socket.TCP_KEEPCNT: 5}
|
|
39
40
|
else:
|
|
40
41
|
return {}
|
|
@@ -88,7 +89,7 @@ async def delete(*names: str | bytes | memoryview):
|
|
|
88
89
|
await get_valkey_client().delete(*names)
|
|
89
90
|
|
|
90
91
|
|
|
91
|
-
async def store_bytes(name: str, data: bytes, ttl: int = None, if_not_set: bool = False):
|
|
92
|
+
async def store_bytes(name: str, data: bytes, ttl: int = None, *, if_not_set: bool = False):
|
|
92
93
|
r = get_valkey_client()
|
|
93
94
|
|
|
94
95
|
return await r.set(name, data, ex=ttl, nx=if_not_set)
|
|
@@ -100,8 +101,8 @@ async def get_bytes(name: str) -> bytes | None:
|
|
|
100
101
|
return await r.get(name)
|
|
101
102
|
|
|
102
103
|
|
|
103
|
-
async def store(name: str, obj: Any, ttl: int = None, if_not_set: bool = False):
|
|
104
|
-
return await store_bytes(name, serialize_msgpack_native(obj), ttl, if_not_set)
|
|
104
|
+
async def store(name: str, obj: Any, ttl: int = None, *, if_not_set: bool = False):
|
|
105
|
+
return await store_bytes(name, serialize_msgpack_native(obj), ttl, if_not_set=if_not_set)
|
|
105
106
|
|
|
106
107
|
|
|
107
108
|
async def get(name: str, default=None, data_type: Any = None) -> Any:
|
|
@@ -130,8 +131,8 @@ async def store_sequence(name: str, data: Sequence, ttl: int = None):
|
|
|
130
131
|
|
|
131
132
|
if ttl:
|
|
132
133
|
await r.expire(name, ttl)
|
|
133
|
-
except valkey.exceptions.ConnectionError
|
|
134
|
-
logger.
|
|
134
|
+
except valkey.exceptions.ConnectionError:
|
|
135
|
+
logger.exception('Failed to store sequence in cache.')
|
|
135
136
|
|
|
136
137
|
|
|
137
138
|
async def get_sequence(name: str, _type: type = list) -> Sequence:
|
|
@@ -150,8 +151,8 @@ async def store_dict(name: str, data: Mapping, ttl: int = None):
|
|
|
150
151
|
|
|
151
152
|
if ttl:
|
|
152
153
|
await r.expire(name, ttl)
|
|
153
|
-
except valkey.exceptions.ConnectionError
|
|
154
|
-
logger.
|
|
154
|
+
except valkey.exceptions.ConnectionError:
|
|
155
|
+
logger.exception('Failed to store dict in cache.')
|
|
155
156
|
|
|
156
157
|
|
|
157
158
|
async def get_dict(name: str, value_data_type=None) -> dict | None:
|
|
@@ -174,8 +175,8 @@ async def set_dict(name: str, mapping: dict, ttl: int = None):
|
|
|
174
175
|
|
|
175
176
|
if ttl:
|
|
176
177
|
await r.expire(name, ttl)
|
|
177
|
-
except valkey.exceptions.ConnectionError
|
|
178
|
-
logger.
|
|
178
|
+
except valkey.exceptions.ConnectionError:
|
|
179
|
+
logger.exception('Failed to set dict in cache.')
|
|
179
180
|
|
|
180
181
|
|
|
181
182
|
async def get_dict_item(name: str, key: str, data_type=None, default=None):
|
|
@@ -184,28 +185,28 @@ async def get_dict_item(name: str, key: str, data_type=None, default=None):
|
|
|
184
185
|
|
|
185
186
|
if data := await r.hget(name, key):
|
|
186
187
|
return deserialize_msgpack_native(data, data_type)
|
|
188
|
+
except valkey.exceptions.ConnectionError:
|
|
189
|
+
logger.exception('Failed to get dict item from cache.')
|
|
187
190
|
|
|
188
|
-
return
|
|
189
|
-
except valkey.exceptions.ConnectionError as e:
|
|
190
|
-
logger.error(f'Failed to get dict item from cache: {e}')
|
|
191
|
+
return None
|
|
191
192
|
|
|
192
|
-
return
|
|
193
|
+
return default
|
|
193
194
|
|
|
194
195
|
|
|
195
196
|
async def set_dict_item(name: str, key: str, obj: Any):
|
|
196
197
|
try:
|
|
197
198
|
r = get_valkey_client()
|
|
198
199
|
await r.hset(name, key, serialize_msgpack_native(obj))
|
|
199
|
-
except valkey.exceptions.ConnectionError
|
|
200
|
-
logger.
|
|
200
|
+
except valkey.exceptions.ConnectionError:
|
|
201
|
+
logger.exception('Failed to set dict item in cache.')
|
|
201
202
|
|
|
202
203
|
|
|
203
204
|
async def delete_dict_item(name: str, *keys):
|
|
204
205
|
try:
|
|
205
206
|
r = get_valkey_client()
|
|
206
207
|
await r.hdel(name, *keys)
|
|
207
|
-
except valkey.exceptions.ConnectionError
|
|
208
|
-
logger.
|
|
208
|
+
except valkey.exceptions.ConnectionError:
|
|
209
|
+
logger.exception('Failed to delete dict item from cache.')
|
|
209
210
|
|
|
210
211
|
|
|
211
212
|
async def store_set(name: str, value: set, ttl: int = None):
|
|
@@ -215,8 +216,8 @@ async def store_set(name: str, value: set, ttl: int = None):
|
|
|
215
216
|
|
|
216
217
|
if ttl:
|
|
217
218
|
await r.expire(name, ttl)
|
|
218
|
-
except valkey.exceptions.ConnectionError
|
|
219
|
-
logger.
|
|
219
|
+
except valkey.exceptions.ConnectionError:
|
|
220
|
+
logger.exception('Failed to store set in cache.')
|
|
220
221
|
|
|
221
222
|
|
|
222
223
|
async def has_set_item(name: str, value: str) -> bool:
|
|
@@ -224,8 +225,8 @@ async def has_set_item(name: str, value: str) -> bool:
|
|
|
224
225
|
r = get_valkey_client()
|
|
225
226
|
|
|
226
227
|
return await r.sismember(name, serialize_msgpack_native(value)) == 1
|
|
227
|
-
except valkey.exceptions.ConnectionError
|
|
228
|
-
logger.
|
|
228
|
+
except valkey.exceptions.ConnectionError:
|
|
229
|
+
logger.exception('Failed to check if set has item in cache.')
|
|
229
230
|
|
|
230
231
|
return False
|
|
231
232
|
|
|
@@ -234,8 +235,8 @@ async def add_set_item(name: str, *values: str):
|
|
|
234
235
|
try:
|
|
235
236
|
r = get_valkey_client()
|
|
236
237
|
await r.sadd(name, *map(serialize_msgpack_native, values))
|
|
237
|
-
except valkey.exceptions.ConnectionError
|
|
238
|
-
logger.
|
|
238
|
+
except valkey.exceptions.ConnectionError:
|
|
239
|
+
logger.exception('Failed to add set item into cache.')
|
|
239
240
|
|
|
240
241
|
|
|
241
242
|
async def delete_set_item(name: str, value: str):
|
|
@@ -249,8 +250,8 @@ async def get_set_members(name: str) -> set[str] | None:
|
|
|
249
250
|
smembers = await r.smembers(name)
|
|
250
251
|
|
|
251
252
|
return set(map(deserialize_msgpack_native, smembers))
|
|
252
|
-
except valkey.exceptions.ConnectionError
|
|
253
|
-
logger.
|
|
253
|
+
except valkey.exceptions.ConnectionError:
|
|
254
|
+
logger.exception('Failed to get set members from cache.')
|
|
254
255
|
|
|
255
256
|
return None
|
|
256
257
|
|
python3_commons/db/__init__.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import contextlib
|
|
2
2
|
import logging
|
|
3
|
-
from
|
|
3
|
+
from collections.abc import AsyncGenerator, Callable, Mapping
|
|
4
4
|
|
|
5
5
|
from sqlalchemy import MetaData
|
|
6
6
|
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_engine_from_config
|
|
@@ -24,7 +24,7 @@ class AsyncSessionManager:
|
|
|
24
24
|
try:
|
|
25
25
|
return self.db_settings[name]
|
|
26
26
|
except KeyError:
|
|
27
|
-
logger.
|
|
27
|
+
logger.exception(f'Missing database settings: {name}')
|
|
28
28
|
|
|
29
29
|
raise
|
|
30
30
|
|
|
@@ -63,8 +63,8 @@ class AsyncSessionManager:
|
|
|
63
63
|
|
|
64
64
|
return session_maker
|
|
65
65
|
|
|
66
|
-
def get_async_session(self, name: str) -> Callable[[], AsyncGenerator[AsyncSession
|
|
67
|
-
async def get_session() -> AsyncGenerator[AsyncSession
|
|
66
|
+
def get_async_session(self, name: str) -> Callable[[], AsyncGenerator[AsyncSession]]:
|
|
67
|
+
async def get_session() -> AsyncGenerator[AsyncSession]:
|
|
68
68
|
session_maker = self.get_session_maker(name)
|
|
69
69
|
|
|
70
70
|
async with session_maker() as session:
|
|
@@ -83,7 +83,7 @@ async def is_healthy(engine: AsyncEngine) -> bool:
|
|
|
83
83
|
result = await conn.execute('SELECT 1;')
|
|
84
84
|
|
|
85
85
|
return result.scalar() == 1
|
|
86
|
-
except Exception
|
|
87
|
-
logger.
|
|
86
|
+
except Exception:
|
|
87
|
+
logger.exception('Database connection is not healthy.')
|
|
88
88
|
|
|
89
89
|
return False
|
python3_commons/db/helpers.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from
|
|
2
|
+
from collections.abc import Mapping
|
|
3
3
|
|
|
4
4
|
import sqlalchemy as sa
|
|
5
5
|
from sqlalchemy import asc, desc, func
|
|
@@ -26,11 +26,12 @@ def get_query(
|
|
|
26
26
|
for order_by_col in order_by.split(','):
|
|
27
27
|
if order_by_col.startswith('-'):
|
|
28
28
|
direction = desc
|
|
29
|
-
|
|
29
|
+
order_by_col_clean = order_by_col[1:]
|
|
30
30
|
else:
|
|
31
31
|
direction = asc
|
|
32
|
+
order_by_col_clean = order_by_col
|
|
32
33
|
|
|
33
|
-
order_by_cols[
|
|
34
|
+
order_by_cols[order_by_col_clean] = direction
|
|
34
35
|
|
|
35
36
|
order_by_clauses = tuple(
|
|
36
37
|
direction(columns[order_by_col][0]) for order_by_col, direction in order_by_cols.items()
|
|
@@ -54,9 +55,6 @@ def get_query(
|
|
|
54
55
|
else:
|
|
55
56
|
where_parts = None
|
|
56
57
|
|
|
57
|
-
if where_parts
|
|
58
|
-
where_clause = sa.and_(*where_parts)
|
|
59
|
-
else:
|
|
60
|
-
where_clause = None
|
|
58
|
+
where_clause = sa.and_(*where_parts) if where_parts else None
|
|
61
59
|
|
|
62
60
|
return where_clause, order_by_clauses
|
python3_commons/fs.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
from collections.abc import Generator
|
|
1
2
|
from pathlib import Path
|
|
2
|
-
from typing import Generator
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
def iter_files(root: Path, recursive: bool = True) -> Generator[Path
|
|
5
|
+
def iter_files(root: Path, *, recursive: bool = True) -> Generator[Path]:
|
|
6
6
|
for item in root.iterdir():
|
|
7
7
|
if item.is_file():
|
|
8
8
|
yield item
|
python3_commons/helpers.py
CHANGED
|
@@ -6,11 +6,12 @@ import threading
|
|
|
6
6
|
import time
|
|
7
7
|
from abc import ABCMeta
|
|
8
8
|
from collections import defaultdict
|
|
9
|
+
from collections.abc import Mapping, Sequence
|
|
9
10
|
from datetime import date, datetime, timedelta
|
|
10
11
|
from decimal import ROUND_HALF_UP, Decimal
|
|
11
12
|
from http.cookies import BaseCookie
|
|
12
13
|
from json import dumps
|
|
13
|
-
from typing import Literal
|
|
14
|
+
from typing import Literal
|
|
14
15
|
from urllib.parse import urlencode
|
|
15
16
|
|
|
16
17
|
from python3_commons.serializers.json import CustomJSONEncoder
|
|
@@ -34,7 +35,7 @@ class SingletonMeta(ABCMeta):
|
|
|
34
35
|
try:
|
|
35
36
|
return cls.__instances[cls]
|
|
36
37
|
except KeyError:
|
|
37
|
-
instance = super(
|
|
38
|
+
instance = super().__call__(*args, **kwargs)
|
|
38
39
|
cls.__instances[cls] = instance
|
|
39
40
|
|
|
40
41
|
return instance
|
|
@@ -66,9 +67,9 @@ def tries(times):
|
|
|
66
67
|
# noinspection PyBroadException
|
|
67
68
|
try:
|
|
68
69
|
return await f(*args, **kwargs)
|
|
69
|
-
except Exception
|
|
70
|
+
except Exception:
|
|
70
71
|
if _time >= times:
|
|
71
|
-
raise
|
|
72
|
+
raise
|
|
72
73
|
|
|
73
74
|
return wrapper
|
|
74
75
|
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import io
|
|
4
3
|
import logging
|
|
5
4
|
from contextlib import asynccontextmanager
|
|
6
|
-
from
|
|
7
|
-
from typing import TYPE_CHECKING, AsyncGenerator, Iterable, Mapping, Sequence
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
8
6
|
|
|
9
7
|
import aiobotocore.session
|
|
10
|
-
from aiobotocore.response import StreamingBody
|
|
11
8
|
from botocore.config import Config
|
|
12
9
|
|
|
13
10
|
if TYPE_CHECKING:
|
|
11
|
+
import io
|
|
12
|
+
from collections.abc import AsyncGenerator, Iterable, Mapping, Sequence
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
|
|
15
|
+
from aiobotocore.response import StreamingBody
|
|
14
16
|
from types_aiobotocore_s3.client import S3Client
|
|
15
17
|
|
|
16
18
|
from python3_commons.conf import S3Settings, s3_settings
|
|
@@ -41,14 +43,13 @@ class ObjectStorage(metaclass=SingletonMeta):
|
|
|
41
43
|
self._config = config
|
|
42
44
|
|
|
43
45
|
@asynccontextmanager
|
|
44
|
-
async def get_client(self) -> AsyncGenerator[S3Client
|
|
46
|
+
async def get_client(self) -> AsyncGenerator[S3Client]:
|
|
45
47
|
async with self._session.create_client('s3', **self._config) as client:
|
|
46
48
|
yield client
|
|
47
49
|
|
|
48
50
|
|
|
49
51
|
def get_absolute_path(path: str) -> str:
|
|
50
|
-
|
|
51
|
-
path = path[1:]
|
|
52
|
+
path = path.removeprefix('/')
|
|
52
53
|
|
|
53
54
|
if bucket_root := s3_settings.s3_bucket_root:
|
|
54
55
|
path = f'{bucket_root[:1] if bucket_root.startswith("/") else bucket_root}/{path}'
|
|
@@ -66,14 +67,13 @@ async def put_object(bucket_name: str, path: str, data: io.BytesIO, length: int,
|
|
|
66
67
|
await s3_client.put_object(Bucket=bucket_name, Key=path, Body=data, ContentLength=length)
|
|
67
68
|
|
|
68
69
|
logger.debug(f'Stored object into object storage: {bucket_name}:{path}')
|
|
69
|
-
|
|
70
|
-
return f's3://{bucket_name}/{path}'
|
|
71
|
-
|
|
72
70
|
except Exception as e:
|
|
73
|
-
logger.
|
|
71
|
+
logger.exception(f'Failed to put object to object storage: {bucket_name}:{path}', exc_info=e)
|
|
74
72
|
|
|
75
73
|
raise
|
|
76
74
|
|
|
75
|
+
return f's3://{bucket_name}/{path}'
|
|
76
|
+
|
|
77
77
|
|
|
78
78
|
@asynccontextmanager
|
|
79
79
|
async def get_object_stream(bucket_name: str, path: str) -> AsyncGenerator[StreamingBody]:
|
|
@@ -88,7 +88,7 @@ async def get_object_stream(bucket_name: str, path: str) -> AsyncGenerator[Strea
|
|
|
88
88
|
async with response['Body'] as stream:
|
|
89
89
|
yield stream
|
|
90
90
|
except Exception as e:
|
|
91
|
-
logger.
|
|
91
|
+
logger.exception(f'Failed getting object from object storage: {bucket_name}:{path}', exc_info=e)
|
|
92
92
|
|
|
93
93
|
raise
|
|
94
94
|
|
|
@@ -102,7 +102,7 @@ async def get_object(bucket_name: str, path: str) -> bytes:
|
|
|
102
102
|
return body
|
|
103
103
|
|
|
104
104
|
|
|
105
|
-
async def list_objects(bucket_name: str, prefix: str, recursive: bool = True) -> AsyncGenerator[Mapping
|
|
105
|
+
async def list_objects(bucket_name: str, prefix: str, *, recursive: bool = True) -> AsyncGenerator[Mapping]:
|
|
106
106
|
storage = ObjectStorage(s3_settings)
|
|
107
107
|
|
|
108
108
|
async with storage.get_client() as s3_client:
|
|
@@ -117,9 +117,9 @@ async def list_objects(bucket_name: str, prefix: str, recursive: bool = True) ->
|
|
|
117
117
|
|
|
118
118
|
|
|
119
119
|
async def get_object_streams(
|
|
120
|
-
bucket_name: str, path: str, recursive: bool = True
|
|
121
|
-
) -> AsyncGenerator[tuple[str, datetime, StreamingBody]
|
|
122
|
-
async for obj in list_objects(bucket_name, path, recursive):
|
|
120
|
+
bucket_name: str, path: str, *, recursive: bool = True
|
|
121
|
+
) -> AsyncGenerator[tuple[str, datetime, StreamingBody]]:
|
|
122
|
+
async for obj in list_objects(bucket_name, path, recursive=recursive):
|
|
123
123
|
object_name = obj['Key']
|
|
124
124
|
last_modified = obj['LastModified']
|
|
125
125
|
|
|
@@ -128,9 +128,9 @@ async def get_object_streams(
|
|
|
128
128
|
|
|
129
129
|
|
|
130
130
|
async def get_objects(
|
|
131
|
-
bucket_name: str, path: str, recursive: bool = True
|
|
132
|
-
) -> AsyncGenerator[tuple[str, datetime, bytes]
|
|
133
|
-
async for object_name, last_modified, stream in get_object_streams(bucket_name, path, recursive):
|
|
131
|
+
bucket_name: str, path: str, *, recursive: bool = True
|
|
132
|
+
) -> AsyncGenerator[tuple[str, datetime, bytes]]:
|
|
133
|
+
async for object_name, last_modified, stream in get_object_streams(bucket_name, path, recursive=recursive):
|
|
134
134
|
data = await stream.read()
|
|
135
135
|
|
|
136
136
|
yield object_name, last_modified, data
|
|
@@ -144,7 +144,7 @@ async def remove_object(bucket_name: str, object_name: str):
|
|
|
144
144
|
await s3_client.delete_object(Bucket=bucket_name, Key=object_name)
|
|
145
145
|
logger.debug(f'Removed object from object storage: {bucket_name}:{object_name}')
|
|
146
146
|
except Exception as e:
|
|
147
|
-
logger.
|
|
147
|
+
logger.exception(f'Failed to remove object from object storage: {bucket_name}:{object_name}', exc_info=e)
|
|
148
148
|
|
|
149
149
|
raise
|
|
150
150
|
|
|
@@ -155,13 +155,12 @@ async def remove_objects(
|
|
|
155
155
|
storage = ObjectStorage(s3_settings)
|
|
156
156
|
|
|
157
157
|
async with storage.get_client() as s3_client:
|
|
158
|
-
objects_to_delete = []
|
|
159
|
-
|
|
160
158
|
if prefix:
|
|
161
|
-
|
|
162
|
-
|
|
159
|
+
objects_to_delete = tuple(
|
|
160
|
+
{'Key': obj['Key']} async for obj in list_objects(bucket_name, prefix, recursive=True)
|
|
161
|
+
)
|
|
163
162
|
elif object_names:
|
|
164
|
-
objects_to_delete =
|
|
163
|
+
objects_to_delete = tuple({'Key': name} for name in object_names)
|
|
165
164
|
else:
|
|
166
165
|
return None
|
|
167
166
|
|
|
@@ -182,9 +181,9 @@ async def remove_objects(
|
|
|
182
181
|
errors.extend(response['Errors'])
|
|
183
182
|
|
|
184
183
|
logger.debug(f'Removed {len(objects_to_delete)} objects from object storage: {bucket_name}')
|
|
185
|
-
|
|
186
|
-
return errors if errors else None
|
|
187
184
|
except Exception as e:
|
|
188
|
-
logger.
|
|
185
|
+
logger.exception(f'Failed to remove objects from object storage: {bucket_name}', exc_info=e)
|
|
189
186
|
|
|
190
187
|
raise
|
|
188
|
+
|
|
189
|
+
return errors if errors else None
|
|
@@ -10,11 +10,9 @@ from typing import Any
|
|
|
10
10
|
class CustomJSONEncoder(json.JSONEncoder):
|
|
11
11
|
def default(self, o) -> Any:
|
|
12
12
|
try:
|
|
13
|
-
return super(
|
|
13
|
+
return super().default(o)
|
|
14
14
|
except TypeError:
|
|
15
|
-
if isinstance(o, datetime):
|
|
16
|
-
return o.isoformat()
|
|
17
|
-
elif isinstance(o, date):
|
|
15
|
+
if isinstance(o, (datetime, date)):
|
|
18
16
|
return o.isoformat()
|
|
19
17
|
elif isinstance(o, bytes):
|
|
20
18
|
return base64.b64encode(o).decode('ascii')
|
|
@@ -7,6 +7,7 @@ from decimal import Decimal
|
|
|
7
7
|
import msgpack
|
|
8
8
|
from msgpack import ExtType
|
|
9
9
|
|
|
10
|
+
from python3_commons.serializers.common import ExtendedType
|
|
10
11
|
from python3_commons.serializers.json import CustomJSONEncoder
|
|
11
12
|
|
|
12
13
|
logger = logging.getLogger(__name__)
|
|
@@ -14,26 +15,27 @@ logger = logging.getLogger(__name__)
|
|
|
14
15
|
|
|
15
16
|
def msgpack_encoder(obj):
|
|
16
17
|
if isinstance(obj, Decimal):
|
|
17
|
-
return ExtType(
|
|
18
|
+
return ExtType(ExtendedType.DECIMAL, str(obj).encode())
|
|
18
19
|
elif isinstance(obj, datetime):
|
|
19
|
-
return ExtType(
|
|
20
|
+
return ExtType(ExtendedType.DATETIME, obj.isoformat().encode())
|
|
20
21
|
elif isinstance(obj, date):
|
|
21
|
-
return ExtType(
|
|
22
|
+
return ExtType(ExtendedType.DATE, obj.isoformat().encode())
|
|
22
23
|
elif dataclasses.is_dataclass(obj):
|
|
23
|
-
return ExtType(
|
|
24
|
+
return ExtType(ExtendedType.DATACLASS, json.dumps(dataclasses.asdict(obj), cls=CustomJSONEncoder).encode())
|
|
24
25
|
|
|
25
26
|
return f'no encoder for {obj}'
|
|
26
27
|
|
|
27
28
|
|
|
28
29
|
def msgpack_decoder(code, data):
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
30
|
+
match code:
|
|
31
|
+
case ExtendedType.DECIMAL:
|
|
32
|
+
return Decimal(data.decode())
|
|
33
|
+
case ExtendedType.DATETIME:
|
|
34
|
+
return datetime.fromisoformat(data.decode())
|
|
35
|
+
case ExtendedType.DATE:
|
|
36
|
+
return date.fromisoformat(data.decode())
|
|
37
|
+
case ExtendedType.DATACLASS:
|
|
38
|
+
return json.loads(data)
|
|
37
39
|
|
|
38
40
|
return f'no decoder for type {code}'
|
|
39
41
|
|
|
@@ -4,12 +4,13 @@ import logging
|
|
|
4
4
|
import struct
|
|
5
5
|
from datetime import date, datetime
|
|
6
6
|
from decimal import Decimal
|
|
7
|
-
from typing import Any,
|
|
7
|
+
from typing import Any, TypeVar
|
|
8
8
|
|
|
9
9
|
from msgspec import msgpack
|
|
10
10
|
from msgspec.msgpack import Ext, encode
|
|
11
11
|
from pydantic import BaseModel
|
|
12
12
|
|
|
13
|
+
from python3_commons.serializers.common import ExtendedType
|
|
13
14
|
from python3_commons.serializers.json import CustomJSONEncoder
|
|
14
15
|
|
|
15
16
|
logger = logging.getLogger(__name__)
|
|
@@ -18,28 +19,36 @@ T = TypeVar('T')
|
|
|
18
19
|
|
|
19
20
|
def enc_hook(obj: Any) -> Any:
|
|
20
21
|
if isinstance(obj, Decimal):
|
|
21
|
-
return Ext(
|
|
22
|
+
return Ext(ExtendedType.DECIMAL, struct.pack('b', str(obj).encode()))
|
|
22
23
|
elif isinstance(obj, datetime):
|
|
23
|
-
return Ext(
|
|
24
|
+
return Ext(ExtendedType.DATETIME, struct.pack('b', obj.isoformat().encode()))
|
|
24
25
|
elif isinstance(obj, date):
|
|
25
|
-
return Ext(
|
|
26
|
+
return Ext(ExtendedType.DATE, struct.pack('b', obj.isoformat().encode()))
|
|
26
27
|
elif dataclasses.is_dataclass(obj):
|
|
27
|
-
return Ext(
|
|
28
|
+
return Ext(
|
|
29
|
+
ExtendedType.DATACLASS,
|
|
30
|
+
struct.pack('b', json.dumps(dataclasses.asdict(obj), cls=CustomJSONEncoder).encode()),
|
|
31
|
+
)
|
|
28
32
|
|
|
29
|
-
|
|
33
|
+
msg = f'Objects of type {type(obj)} are not supported'
|
|
34
|
+
|
|
35
|
+
raise NotImplementedError(msg)
|
|
30
36
|
|
|
31
37
|
|
|
32
38
|
def ext_hook(code: int, data: memoryview) -> Any:
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
match code:
|
|
40
|
+
case ExtendedType.DECIMAL:
|
|
41
|
+
return Decimal(data.tobytes().decode())
|
|
42
|
+
case ExtendedType.DATETIME:
|
|
43
|
+
return datetime.fromisoformat(data.tobytes().decode())
|
|
44
|
+
case ExtendedType.DATE:
|
|
45
|
+
return date.fromisoformat(data.tobytes().decode())
|
|
46
|
+
case ExtendedType.DATACLASS:
|
|
47
|
+
return json.loads(data.tobytes())
|
|
48
|
+
case _:
|
|
49
|
+
msg = f'Extension type code {code} is not supported'
|
|
41
50
|
|
|
42
|
-
|
|
51
|
+
raise NotImplementedError(msg)
|
|
43
52
|
|
|
44
53
|
|
|
45
54
|
MSGPACK_ENCODER = msgpack.Encoder(enc_hook=enc_hook)
|
|
@@ -56,7 +65,7 @@ def serialize_msgpack_native(data: Any) -> bytes:
|
|
|
56
65
|
return result
|
|
57
66
|
|
|
58
67
|
|
|
59
|
-
def deserialize_msgpack_native(data: bytes, data_type:
|
|
68
|
+
def deserialize_msgpack_native[T](data: bytes, data_type: type[T] | None = None) -> T | Any:
|
|
60
69
|
if data_type:
|
|
61
70
|
if issubclass(data_type, BaseModel):
|
|
62
71
|
decoded = MSGPACK_DECODER_NATIVE.decode(data)
|
|
@@ -78,7 +87,7 @@ def serialize_msgpack(data: Any) -> bytes:
|
|
|
78
87
|
return result
|
|
79
88
|
|
|
80
89
|
|
|
81
|
-
def deserialize_msgpack(data: bytes, data_type:
|
|
90
|
+
def deserialize_msgpack[T](data: bytes, data_type: type[T] | None = None) -> T | Any:
|
|
82
91
|
if data_type:
|
|
83
92
|
if issubclass(data_type, BaseModel):
|
|
84
93
|
decoded = MSGPACK_DECODER.decode(data)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python3-commons
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.20
|
|
4
4
|
Summary: Re-usable Python3 code
|
|
5
5
|
Author-email: Oleg Korsak <kamikaze.is.waiting.you@gmail.com>
|
|
6
6
|
License-Expression: GPL-3.0
|
|
@@ -17,15 +17,15 @@ Requires-Dist: aiohttp[speedups]~=3.12.15
|
|
|
17
17
|
Requires-Dist: asyncpg~=0.30.0
|
|
18
18
|
Requires-Dist: fastapi-users-db-sqlalchemy~=7.0.0
|
|
19
19
|
Requires-Dist: fastapi-users[sqlalchemy]~=14.0.1
|
|
20
|
-
Requires-Dist: lxml~=6.0.
|
|
20
|
+
Requires-Dist: lxml~=6.0.2
|
|
21
21
|
Requires-Dist: msgpack~=1.1.1
|
|
22
22
|
Requires-Dist: msgspec~=0.19.0
|
|
23
|
-
Requires-Dist: pydantic[email]~=2.11.
|
|
23
|
+
Requires-Dist: pydantic[email]~=2.11.9
|
|
24
24
|
Requires-Dist: pydantic-settings~=2.10.1
|
|
25
25
|
Requires-Dist: python-jose==3.5.0
|
|
26
26
|
Requires-Dist: SQLAlchemy[asyncio]~=2.0.43
|
|
27
27
|
Requires-Dist: valkey[libvalkey]~=6.1.1
|
|
28
|
-
Requires-Dist: zeep~=4.3.
|
|
28
|
+
Requires-Dist: zeep~=4.3.2
|
|
29
29
|
Dynamic: license-file
|
|
30
30
|
|
|
31
31
|
Re-usable Python3 code
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
python3_commons/__init__.py,sha256=0KgaYU46H_IMKn-BuasoRN3C4Hi45KlkHHoPbU9cwiA,189
|
|
2
|
+
python3_commons/api_client.py,sha256=0PE8iYW5zq7n89veC6afSkwj_fzD_nldBc5wdXyg5jo,5266
|
|
3
|
+
python3_commons/audit.py,sha256=kfUaSiNAg9NJuUhnBXqplNiyuPQuDFM9NcD-HZV2qd4,6077
|
|
4
|
+
python3_commons/auth.py,sha256=fWfoh5665F-YnGTgG9wDAosYIM9vFoTsQXEcCyzD72M,2951
|
|
5
|
+
python3_commons/cache.py,sha256=Hib86F1k1SviF-a2RyoTxRrFi0XV2KaWOfBi5TlOu-g,7697
|
|
6
|
+
python3_commons/conf.py,sha256=GOvR1oYss6AKnNlyj9JbT0mSrfQFrnzmgm0IiGVahho,2428
|
|
7
|
+
python3_commons/fs.py,sha256=dn8ZcwsQf9xcAEg6neoxLN6IzJbWpprfm8wV8S55BL0,337
|
|
8
|
+
python3_commons/helpers.py,sha256=utcorGvXvZLsC4H_H8WGwzeIMgCOCHac2jbKpVCeixA,3928
|
|
9
|
+
python3_commons/object_storage.py,sha256=pvA-gJehnbIJmqu8ebT6oSqW-dR-uZl3E1AV86ffUpY,6568
|
|
10
|
+
python3_commons/permissions.py,sha256=bhjTp-tq-oaTGFMHNnSBlcVX5XQCTL0nWcu6SdPEAB4,1555
|
|
11
|
+
python3_commons/db/__init__.py,sha256=PZTIC0RPzYzZ2Fh4HeyKtYIHaUPB7A92yeOK--PGxnw,2942
|
|
12
|
+
python3_commons/db/helpers.py,sha256=n56yYCE0fvzvU7nL1936NfZhbaQmvfumzRsGimBlNV4,1776
|
|
13
|
+
python3_commons/db/models/__init__.py,sha256=zjZCf0DNDkqmPZ49quJ6KZohtKH87viI_ijDG3E0PVE,554
|
|
14
|
+
python3_commons/db/models/auth.py,sha256=NMHirujigpaRR0Bhhe2gzy8Q8PPABuaA-D8ZY7aaqeE,1177
|
|
15
|
+
python3_commons/db/models/common.py,sha256=nRLQVi7Y0SsXo3qMIwQX6GuDO9kHnlma4O_mYXQVtHQ,1512
|
|
16
|
+
python3_commons/db/models/rbac.py,sha256=BIB7nJXQkCes0XA-fg-oCHP6YU0_rXIm29O73j4pNUg,3160
|
|
17
|
+
python3_commons/log/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
+
python3_commons/log/filters.py,sha256=fuyjXZAUm-i2MNrxvFYag8F8Rr27x8W8MdV3ke6miSs,175
|
|
19
|
+
python3_commons/log/formatters.py,sha256=p2AtZD4Axp3Em0e9gWzW8U_yOR5entD7xn7Edvc-IuM,719
|
|
20
|
+
python3_commons/serializers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
|
+
python3_commons/serializers/common.py,sha256=VkA7C6wODvHk0QBXVX_x2JieDstihx3U__UFbTYf654,120
|
|
22
|
+
python3_commons/serializers/json.py,sha256=Dae0gouk9jiUOkmh8z1AcTIHD52OgBn2aXK9JLKMMms,718
|
|
23
|
+
python3_commons/serializers/msgpack.py,sha256=AwzBSUdbdq8yYdGzmEsiWw0bnL9XRQFa1Vh-nt2s56k,1499
|
|
24
|
+
python3_commons/serializers/msgspec.py,sha256=yp3LvsxDUTIv__3AaLed5ddZ0rHm7BYqmrrehVAJb14,3000
|
|
25
|
+
python3_commons-0.9.20.dist-info/licenses/AUTHORS.rst,sha256=3R9JnfjfjH5RoPWOeqKFJgxVShSSfzQPIrEr1nxIo9Q,90
|
|
26
|
+
python3_commons-0.9.20.dist-info/licenses/LICENSE,sha256=xxILuojHm4fKQOrMHPSslbyy6WuKAN2RiG74HbrYfzM,34575
|
|
27
|
+
python3_commons-0.9.20.dist-info/METADATA,sha256=BfW_2kWlG3QkN3fmzHlmXrkNjHZGLM_WiXujw6BTTjc,1134
|
|
28
|
+
python3_commons-0.9.20.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
29
|
+
python3_commons-0.9.20.dist-info/top_level.txt,sha256=lJI6sCBf68eUHzupCnn2dzG10lH3jJKTWM_hrN1cQ7M,16
|
|
30
|
+
python3_commons-0.9.20.dist-info/RECORD,,
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
python3_commons/__init__.py,sha256=0KgaYU46H_IMKn-BuasoRN3C4Hi45KlkHHoPbU9cwiA,189
|
|
2
|
-
python3_commons/api_client.py,sha256=vIWt6QIbHw_x-1yKhXyJEN3Aj22KMsZwJXMsdWR7oaQ,4970
|
|
3
|
-
python3_commons/audit.py,sha256=p4KRKt0ogkHhJSulg6j5GU-JKBBE903H2c0nuW16GtM,6083
|
|
4
|
-
python3_commons/auth.py,sha256=fINE7zeq-oaEk2lwkdP1KOhfCpcIBaC8P9UzXQI37J0,2922
|
|
5
|
-
python3_commons/cache.py,sha256=lf27LTD4Z9Iqi5GaK8jH8UC0cL9sHH8wicZ88YDp6Mg,7725
|
|
6
|
-
python3_commons/conf.py,sha256=GOvR1oYss6AKnNlyj9JbT0mSrfQFrnzmgm0IiGVahho,2428
|
|
7
|
-
python3_commons/fs.py,sha256=wfLjybXndwLqNlOxTpm_HRJnuTcC4wbrHEOaEeCo9Wc,337
|
|
8
|
-
python3_commons/helpers.py,sha256=9m9Q_ImXzOsKmt7ObLUhd11vnGfwUH_KO4IwLaTMrFA,3930
|
|
9
|
-
python3_commons/object_storage.py,sha256=6jESNzI__r9wJdTWJQRXhpbdmC2QrZVUoZyvetfQNwY,6544
|
|
10
|
-
python3_commons/permissions.py,sha256=bhjTp-tq-oaTGFMHNnSBlcVX5XQCTL0nWcu6SdPEAB4,1555
|
|
11
|
-
python3_commons/db/__init__.py,sha256=5nArsGm17e-pelpOwAeBKy2n_Py20XqklZsNgkcJ-DQ,2947
|
|
12
|
-
python3_commons/db/helpers.py,sha256=PY0h08aLiGx-J54wmP3GHPCgGCcLd60rayAUnR3aWdI,1742
|
|
13
|
-
python3_commons/db/models/__init__.py,sha256=zjZCf0DNDkqmPZ49quJ6KZohtKH87viI_ijDG3E0PVE,554
|
|
14
|
-
python3_commons/db/models/auth.py,sha256=NMHirujigpaRR0Bhhe2gzy8Q8PPABuaA-D8ZY7aaqeE,1177
|
|
15
|
-
python3_commons/db/models/common.py,sha256=nRLQVi7Y0SsXo3qMIwQX6GuDO9kHnlma4O_mYXQVtHQ,1512
|
|
16
|
-
python3_commons/db/models/rbac.py,sha256=BIB7nJXQkCes0XA-fg-oCHP6YU0_rXIm29O73j4pNUg,3160
|
|
17
|
-
python3_commons/log/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
-
python3_commons/log/filters.py,sha256=fuyjXZAUm-i2MNrxvFYag8F8Rr27x8W8MdV3ke6miSs,175
|
|
19
|
-
python3_commons/log/formatters.py,sha256=p2AtZD4Axp3Em0e9gWzW8U_yOR5entD7xn7Edvc-IuM,719
|
|
20
|
-
python3_commons/serializers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
|
-
python3_commons/serializers/json.py,sha256=91UaXLGKGj0yPyrnuMeNrkG2GuPUgcgAsmIokUgEwpU,808
|
|
22
|
-
python3_commons/serializers/msgpack.py,sha256=WrvaPE187shSK8zkH4UHHMimEZNMv9RaDSwsBE2HlCw,1269
|
|
23
|
-
python3_commons/serializers/msgspec.py,sha256=0AliXlEl5sewi0UENjI8St5ZScXE5DNRERKzqWKy2Ps,2674
|
|
24
|
-
python3_commons-0.9.18.dist-info/licenses/AUTHORS.rst,sha256=3R9JnfjfjH5RoPWOeqKFJgxVShSSfzQPIrEr1nxIo9Q,90
|
|
25
|
-
python3_commons-0.9.18.dist-info/licenses/LICENSE,sha256=xxILuojHm4fKQOrMHPSslbyy6WuKAN2RiG74HbrYfzM,34575
|
|
26
|
-
python3_commons-0.9.18.dist-info/METADATA,sha256=H1oSSmkAcQc8WxKeBi7A8JNIs12sFpxHa-exEMlyoWo,1134
|
|
27
|
-
python3_commons-0.9.18.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
28
|
-
python3_commons-0.9.18.dist-info/top_level.txt,sha256=lJI6sCBf68eUHzupCnn2dzG10lH3jJKTWM_hrN1cQ7M,16
|
|
29
|
-
python3_commons-0.9.18.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|