python3-commons 0.24.6__tar.gz → 0.25.0__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.
- python3_commons-0.25.0/PKG-INFO +434 -0
- python3_commons-0.25.0/README.md +385 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/pyproject.toml +6 -8
- {python3_commons-0.24.6 → python3_commons-0.25.0}/src/python3_commons/api_client.py +15 -3
- {python3_commons-0.24.6 → python3_commons-0.25.0}/src/python3_commons/audit.py +17 -5
- {python3_commons-0.24.6 → python3_commons-0.25.0}/src/python3_commons/conf.py +22 -8
- python3_commons-0.25.0/src/python3_commons/object_storage.py +247 -0
- python3_commons-0.25.0/src/python3_commons.egg-info/PKG-INFO +434 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/src/python3_commons.egg-info/SOURCES.txt +2 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/src/python3_commons.egg-info/requires.txt +4 -5
- {python3_commons-0.24.6 → python3_commons-0.25.0}/src/python3_commons.egg-info/scm_file_list.json +2 -0
- python3_commons-0.25.0/src/python3_commons.egg-info/scm_version.json +8 -0
- python3_commons-0.25.0/tests/integration/test_osc.py +49 -0
- python3_commons-0.25.0/tests/unit/test_api_client.py +442 -0
- python3_commons-0.25.0/tests/unit/test_audit.py +214 -0
- python3_commons-0.25.0/tests/unit/test_object_storage.py +247 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/uv.lock +301 -403
- python3_commons-0.24.6/PKG-INFO +0 -310
- python3_commons-0.24.6/README.md +0 -244
- python3_commons-0.24.6/src/python3_commons/object_storage.py +0 -213
- python3_commons-0.24.6/src/python3_commons.egg-info/PKG-INFO +0 -310
- python3_commons-0.24.6/src/python3_commons.egg-info/scm_version.json +0 -8
- python3_commons-0.24.6/tests/integration/test_osc.py +0 -45
- python3_commons-0.24.6/tests/unit/test_audit.py +0 -31
- {python3_commons-0.24.6 → python3_commons-0.25.0}/.coveragerc +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/.devcontainer/Dockerfile +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/.devcontainer/devcontainer.json +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/.devcontainer/docker-compose.yml +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/.env_template +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/.github/workflows/checks.yml +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/.github/workflows/python-publish.yaml +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/.github/workflows/release-on-tag-push.yml +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/.gitignore +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/.pre-commit-config.yaml +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/.python-version +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/AUTHORS.rst +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/CHANGELOG.rst +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/LICENSE +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/README.rst +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/docs/Makefile +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/docs/_static/.gitignore +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/docs/authors.rst +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/docs/changelog.rst +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/docs/conf.py +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/docs/index.rst +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/docs/license.rst +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/setup.cfg +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/src/python3_commons/__init__.py +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/src/python3_commons/async_functools.py +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/src/python3_commons/auth.py +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/src/python3_commons/cache.py +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/src/python3_commons/db/__init__.py +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/src/python3_commons/db/helpers.py +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/src/python3_commons/db/models/__init__.py +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/src/python3_commons/db/models/auth.py +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/src/python3_commons/db/models/common.py +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/src/python3_commons/db/models/rbac.py +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/src/python3_commons/db/models/users.py +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/src/python3_commons/exceptions.py +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/src/python3_commons/fs.py +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/src/python3_commons/generators.py +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/src/python3_commons/helpers.py +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/src/python3_commons/log/__init__.py +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/src/python3_commons/log/filters.py +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/src/python3_commons/log/formatters.py +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/src/python3_commons/permissions.py +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/src/python3_commons/serializers/__init__.py +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/src/python3_commons/serializers/common.py +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/src/python3_commons/serializers/json.py +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/src/python3_commons/serializers/msgpack.py +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/src/python3_commons/serializers/msgspec.py +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/src/python3_commons/soap_client.py +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/src/python3_commons.egg-info/dependency_links.txt +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/src/python3_commons.egg-info/top_level.txt +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/tests/__init__.py +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/tests/integration/__init__.py +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/tests/integration/conftest.py +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/tests/integration/test_auth.py +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/tests/integration/test_cache.py +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/tests/unit/__init__.py +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/tests/unit/conftest.py +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/tests/unit/db/__init__.py +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/tests/unit/db/test_async_session_manager.py +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/tests/unit/log/__init__.py +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/tests/unit/log/test_formatters.py +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/tests/unit/test_async_functools.py +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/tests/unit/test_auth.py +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/tests/unit/test_helpers.py +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/tests/unit/test_msgpack.py +0 -0
- {python3_commons-0.24.6 → python3_commons-0.25.0}/tests/unit/test_msgspec.py +0 -0
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python3-commons
|
|
3
|
+
Version: 0.25.0
|
|
4
|
+
Summary: Re-usable Python3 code
|
|
5
|
+
Author-email: Oleg Korsak <kamikaze.is.waiting.you@gmail.com>
|
|
6
|
+
License-Expression: GPL-3.0-only
|
|
7
|
+
Project-URL: Homepage, https://github.com/kamikaze/python3-commons
|
|
8
|
+
Project-URL: Documentation, https://github.com/kamikaze/python3-commons/wiki
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Programming Language :: Python
|
|
11
|
+
Requires-Python: <3.15.0,>=3.14.0
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
License-File: AUTHORS.rst
|
|
15
|
+
Requires-Dist: msgpack~=1.2.2
|
|
16
|
+
Requires-Dist: msgspec~=0.21.1
|
|
17
|
+
Requires-Dist: pydantic-settings~=2.15.0
|
|
18
|
+
Provides-Extra: all
|
|
19
|
+
Requires-Dist: python3_commons[api-client,audit,authn,authz,cache,database,object-storage,soap-client]; extra == "all"
|
|
20
|
+
Provides-Extra: api-client
|
|
21
|
+
Requires-Dist: aiohttp[speedups]<3.15.0,>=3.14.3; extra == "api-client"
|
|
22
|
+
Requires-Dist: certifi~=2026.7.22; extra == "api-client"
|
|
23
|
+
Requires-Dist: python3_commons[audit,object-storage]; extra == "api-client"
|
|
24
|
+
Provides-Extra: audit
|
|
25
|
+
Requires-Dist: lxml~=6.1.3; extra == "audit"
|
|
26
|
+
Requires-Dist: zeep[async]~=4.3.3; extra == "audit"
|
|
27
|
+
Requires-Dist: python3_commons[object-storage]; extra == "audit"
|
|
28
|
+
Provides-Extra: authn
|
|
29
|
+
Requires-Dist: aiohttp[speedups]<3.15.0,>=3.14.3; extra == "authn"
|
|
30
|
+
Requires-Dist: certifi~=2026.7.22; extra == "authn"
|
|
31
|
+
Requires-Dist: python3_commons[api-client]; extra == "authn"
|
|
32
|
+
Provides-Extra: authz
|
|
33
|
+
Requires-Dist: python3_commons[database]; extra == "authz"
|
|
34
|
+
Requires-Dist: python3_commons[api-client]; extra == "authz"
|
|
35
|
+
Provides-Extra: cache
|
|
36
|
+
Requires-Dist: valkey[libvalkey]~=6.1.1; extra == "cache"
|
|
37
|
+
Provides-Extra: database
|
|
38
|
+
Requires-Dist: asyncpg~=0.31.0; extra == "database"
|
|
39
|
+
Requires-Dist: SQLAlchemy[asyncio]~=2.1.1; extra == "database"
|
|
40
|
+
Provides-Extra: object-storage
|
|
41
|
+
Requires-Dist: object-storage-client~=0.1.12; extra == "object-storage"
|
|
42
|
+
Provides-Extra: soap-client
|
|
43
|
+
Requires-Dist: aiohttp[speedups]<3.15.0,>=3.14.3; extra == "soap-client"
|
|
44
|
+
Requires-Dist: certifi~=2026.7.22; extra == "soap-client"
|
|
45
|
+
Requires-Dist: lxml~=6.1.3; extra == "soap-client"
|
|
46
|
+
Requires-Dist: requests~=2.34.2; extra == "soap-client"
|
|
47
|
+
Requires-Dist: zeep[async]~=4.3.3; extra == "soap-client"
|
|
48
|
+
Dynamic: license-file
|
|
49
|
+
|
|
50
|
+
# python3-commons
|
|
51
|
+
|
|
52
|
+
Re-usable code for Python 3 projects.
|
|
53
|
+
|
|
54
|
+
## Installation
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
uv add python3-commons
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Optional Dependencies
|
|
61
|
+
|
|
62
|
+
Some features require extra dependencies. You can install them individually or all at once:
|
|
63
|
+
|
|
64
|
+
- `api-client`: For `python3_commons.api_client`
|
|
65
|
+
- `audit`: For `python3_commons.audit`
|
|
66
|
+
- `authn`: For `python3_commons.auth`
|
|
67
|
+
- `authz`: For `python3_commons.permissions`
|
|
68
|
+
- `cache`: For `python3_commons.cache`
|
|
69
|
+
- `database`: For `python3_commons.db`
|
|
70
|
+
- `object-storage`: For `python3_commons.object_storage`
|
|
71
|
+
- `soap-client`: For `python3_commons.soap_client`
|
|
72
|
+
- `all`: Install all optional dependencies
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
uv add "python3-commons[all]"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Features
|
|
79
|
+
|
|
80
|
+
### Async LRU Cache
|
|
81
|
+
|
|
82
|
+
An LRU cache decorator for asynchronous functions that handles `await` and prevents the dogpile effect (mitigating multiple concurrent calls for the same key by waiting for the first one to finish).
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from python3_commons.async_functools import async_lru_cache
|
|
86
|
+
|
|
87
|
+
@async_lru_cache(maxsize=128)
|
|
88
|
+
async def fetch_expensive_data(item_id: int):
|
|
89
|
+
# This function will only be called once for a given item_id
|
|
90
|
+
# even if multiple tasks await it simultaneously.
|
|
91
|
+
return await db.get(item_id)
|
|
92
|
+
|
|
93
|
+
# Usage
|
|
94
|
+
result = await fetch_expensive_data(42)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### API Client
|
|
98
|
+
|
|
99
|
+
A context manager for `aiohttp` requests with built-in audit logging to S3 and standardized error mapping to Python exceptions.
|
|
100
|
+
|
|
101
|
+
#### Standardized Exception Mapping
|
|
102
|
+
|
|
103
|
+
HTTP error status codes and network failures are mapped to built-in Python exceptions:
|
|
104
|
+
- `400 Bad Request` -> `ValueError`
|
|
105
|
+
- `401 Unauthorized`, `403 Forbidden` -> `PermissionError`
|
|
106
|
+
- `404 Not Found` -> `LookupError`
|
|
107
|
+
- `429 Too Many Requests` -> `InterruptedError`
|
|
108
|
+
- Other 4xx/5xx responses -> `aiohttp.ClientResponseError`
|
|
109
|
+
- Connection errors -> `ConnectionRefusedError` / `ConnectionResetError`
|
|
110
|
+
|
|
111
|
+
#### Enabling Audit Logging
|
|
112
|
+
|
|
113
|
+
Audit logging captures the outgoing request (formatted as an executable `curl` command) and the incoming response body, writing them asynchronously to Object Storage.
|
|
114
|
+
|
|
115
|
+
The destination path is resolved as follows:
|
|
116
|
+
- If `ApiClientSettings.audit_path` is configured (via `API_CLIENT_AUDIT_PATH`), it is used directly as the audit root for `api_client` (without prepending `"audit/"`):
|
|
117
|
+
`<audit_path>/<YYYY/MM/DD>/<audit_name>/<uri>/<method>_<timestamp>_<request_id>_{request,response}.txt`
|
|
118
|
+
- Otherwise, if `ObjectStorageSettings.work_path` is configured (via `OBJECT_STORAGE_WORK_PATH`), it is used as the root with `"audit/"` prepended:
|
|
119
|
+
`<work_path>/audit/<YYYY/MM/DD>/<audit_name>/<uri>/<method>_<timestamp>_<request_id>_{request,response}.txt`
|
|
120
|
+
- If neither is configured, the logs default to:
|
|
121
|
+
`audit/<YYYY/MM/DD>/<audit_name>/<uri>/<method>_<timestamp>_<request_id>_{request,response}.txt`
|
|
122
|
+
|
|
123
|
+
To enable audit logging:
|
|
124
|
+
1. **Configure Object Storage & S3 credentials**:
|
|
125
|
+
```bash
|
|
126
|
+
# API client audit path or Object storage root
|
|
127
|
+
export API_CLIENT_AUDIT_PATH="my-audit-bucket/audit-logs"
|
|
128
|
+
# or export OBJECT_STORAGE_WORK_PATH="my-audit-bucket"
|
|
129
|
+
|
|
130
|
+
# S3 credentials
|
|
131
|
+
export S3_ACCESS_KEY_ID="your-access-key-id"
|
|
132
|
+
export S3_SECRET_ACCESS_KEY="your-secret-access-key"
|
|
133
|
+
export S3_REGION="us-east-1"
|
|
134
|
+
# Optional configurations:
|
|
135
|
+
# export S3_ALLOW_HTTP="false"
|
|
136
|
+
# export S3_ADDRESSING_STYLE="virtual" # or "path"
|
|
137
|
+
# export S3_CERT_VERIFY="true"
|
|
138
|
+
```
|
|
139
|
+
Alternatively, you can configure them directly in Python:
|
|
140
|
+
```python
|
|
141
|
+
from python3_commons.conf import api_client_settings, object_storage_settings, s3_settings
|
|
142
|
+
from pydantic import SecretStr
|
|
143
|
+
|
|
144
|
+
api_client_settings.audit_path = "my-audit-bucket/audit-logs"
|
|
145
|
+
# or object_storage_settings.work_path = "my-audit-bucket"
|
|
146
|
+
|
|
147
|
+
s3_settings.access_key_id = SecretStr("your-access-key-id")
|
|
148
|
+
s3_settings.secret_access_key = SecretStr("your-secret-access-key")
|
|
149
|
+
s3_settings.region = "us-east-1"
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
2. **Pass `audit_name` to `request()`**: Provide a non-empty string identifier (such as the target service or operation name) in the `audit_name` argument. If `audit_name` is omitted (default `None`), audit logging is disabled for that call. If S3 credentials are not configured, audit logging is safely bypassed.
|
|
153
|
+
|
|
154
|
+
#### Usage Example
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
from aiohttp import ClientSession
|
|
158
|
+
from python3_commons.api_client import request
|
|
159
|
+
|
|
160
|
+
async with ClientSession() as session:
|
|
161
|
+
# 1. GET request with audit logging enabled
|
|
162
|
+
async with request(
|
|
163
|
+
session,
|
|
164
|
+
base_url="https://api.example.com",
|
|
165
|
+
uri="/users/123",
|
|
166
|
+
method="get",
|
|
167
|
+
headers={"Accept": "application/json"},
|
|
168
|
+
audit_name="user_service",
|
|
169
|
+
) as response:
|
|
170
|
+
user = await response.json()
|
|
171
|
+
|
|
172
|
+
# 2. POST request with JSON body and audit logging
|
|
173
|
+
async with request(
|
|
174
|
+
session,
|
|
175
|
+
base_url="https://api.example.com",
|
|
176
|
+
uri="/users",
|
|
177
|
+
method="post",
|
|
178
|
+
json={"name": "Alice", "role": "admin"},
|
|
179
|
+
audit_name="user_service",
|
|
180
|
+
) as response:
|
|
181
|
+
result = await response.json()
|
|
182
|
+
|
|
183
|
+
# 3. Request without audit logging (audit_name omitted)
|
|
184
|
+
async with request(
|
|
185
|
+
session,
|
|
186
|
+
base_url="https://api.example.com",
|
|
187
|
+
uri="/health",
|
|
188
|
+
method="get",
|
|
189
|
+
) as response:
|
|
190
|
+
status = await response.json()
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### SOAP Client
|
|
194
|
+
|
|
195
|
+
Async SOAP client support for `zeep` with S3 auditing capabilities.
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
from python3_commons.soap_client import soap_client
|
|
199
|
+
|
|
200
|
+
async with soap_client("https://example.com/service?wsdl") as client:
|
|
201
|
+
result = await client.service.GetData(id=42)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Database Management
|
|
205
|
+
|
|
206
|
+
SQLAlchemy async engine and session management with pool tuning, health checks, dynamic query builders, and declarative base models.
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
from python3_commons.db import AsyncSessionManager, is_healthy
|
|
210
|
+
from python3_commons.db.helpers import get_query
|
|
211
|
+
from python3_commons.conf import DBSettings
|
|
212
|
+
|
|
213
|
+
# Configuration
|
|
214
|
+
configs = {
|
|
215
|
+
"default": DBSettings(
|
|
216
|
+
dsn="postgresql+asyncpg://user:pass@localhost/db",
|
|
217
|
+
pool_size=20,
|
|
218
|
+
pool_timeout=30,
|
|
219
|
+
statement_timeout=30,
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
manager = AsyncSessionManager(configs)
|
|
223
|
+
|
|
224
|
+
# Usage
|
|
225
|
+
async with manager.get_session_context("default") as session:
|
|
226
|
+
result = await session.execute(...)
|
|
227
|
+
|
|
228
|
+
# Engine health check (executes SELECT 1 with timeout)
|
|
229
|
+
healthy = await is_healthy(manager.get_engine("default"))
|
|
230
|
+
|
|
231
|
+
# Dynamic query filtering and sorting
|
|
232
|
+
where_clause, order_clauses = get_query(
|
|
233
|
+
search={"name": "Alice"},
|
|
234
|
+
order_by="-created_at,name",
|
|
235
|
+
columns={
|
|
236
|
+
# name: (column, case_insensitive, cast_func, exact_match)
|
|
237
|
+
"name": (User.name, True, str, False),
|
|
238
|
+
"created_at": (User.created_at, False, str, True),
|
|
239
|
+
},
|
|
240
|
+
)
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Object Storage
|
|
244
|
+
|
|
245
|
+
Thin wrapper for `object-storage-client` supporting S3, GCS, Azure, and local file storage, configured via `object_storage_settings` (`OBJECT_STORAGE_WORK_PATH`) and `s3_settings` (`S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY`, `S3_REGION`). Storage functions accept a required `work_path` parameter to define the base storage location.
|
|
246
|
+
|
|
247
|
+
```python
|
|
248
|
+
from python3_commons import object_storage
|
|
249
|
+
import io
|
|
250
|
+
|
|
251
|
+
# Upload an object
|
|
252
|
+
await object_storage.put_object(
|
|
253
|
+
work_path="my-bucket",
|
|
254
|
+
path="uploads/file.txt",
|
|
255
|
+
data=io.BytesIO(b"Hello World"),
|
|
256
|
+
length=11
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
# Download an object as bytes
|
|
260
|
+
content = await object_storage.get_object("my-bucket", "uploads/file.txt")
|
|
261
|
+
|
|
262
|
+
# Stream an object
|
|
263
|
+
async with object_storage.get_object_stream("my-bucket", "uploads/file.txt") as stream:
|
|
264
|
+
data = await stream.read()
|
|
265
|
+
|
|
266
|
+
# List objects with a prefix
|
|
267
|
+
async for obj in object_storage.list_objects("my-bucket", "uploads/"):
|
|
268
|
+
print(obj['Key'])
|
|
269
|
+
|
|
270
|
+
# Delete single or multiple objects
|
|
271
|
+
await object_storage.remove_object("my-bucket", "uploads/file.txt")
|
|
272
|
+
await object_storage.remove_objects("my-bucket", object_names=["uploads/file1.txt", "uploads/file2.txt"])
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### OIDC Authentication
|
|
276
|
+
|
|
277
|
+
Client for OpenID Connect authentication, supporting configuration fetching, JWKS, and token acquisition.
|
|
278
|
+
|
|
279
|
+
```python
|
|
280
|
+
from python3_commons.auth import OIDCClient
|
|
281
|
+
from pydantic import HttpUrl
|
|
282
|
+
|
|
283
|
+
client = OIDCClient(
|
|
284
|
+
authority_url=HttpUrl("https://auth.example.com/realms/myrealm"),
|
|
285
|
+
client_id="my-app-client",
|
|
286
|
+
client_secret="secret"
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
async with client:
|
|
290
|
+
token_response = await client.fetch_token(username="user", password="password")
|
|
291
|
+
print(token_response.access_token)
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Valkey Cache
|
|
295
|
+
|
|
296
|
+
Async caching using Valkey (Redis-compatible) with automatic Msgpack serialization for complex types.
|
|
297
|
+
|
|
298
|
+
```python
|
|
299
|
+
from python3_commons import cache
|
|
300
|
+
|
|
301
|
+
# Store a dictionary
|
|
302
|
+
await cache.store("user:123", {"name": "Alice", "role": "admin"}, ttl=3600)
|
|
303
|
+
|
|
304
|
+
# Retrieve it
|
|
305
|
+
user_data = await cache.get("user:123")
|
|
306
|
+
|
|
307
|
+
# Set operations
|
|
308
|
+
await cache.add_set_item("active_users", "user:123")
|
|
309
|
+
is_active = await cache.has_set_item("active_users", "user:123")
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Structured Logging
|
|
313
|
+
|
|
314
|
+
A `JSONFormatter` for structured JSON logging and a `filter_maker` utility for level-based log filtering, compatible with standard Python `logging`.
|
|
315
|
+
|
|
316
|
+
```python
|
|
317
|
+
import logging
|
|
318
|
+
from python3_commons.log.formatters import JSONFormatter
|
|
319
|
+
from python3_commons.log.filters import filter_maker
|
|
320
|
+
|
|
321
|
+
handler = logging.StreamHandler()
|
|
322
|
+
handler.setFormatter(JSONFormatter())
|
|
323
|
+
# Filter to process logs up to INFO level only
|
|
324
|
+
handler.addFilter(filter_maker("INFO"))
|
|
325
|
+
logging.getLogger().addHandler(handler)
|
|
326
|
+
|
|
327
|
+
logger = logging.getLogger("app")
|
|
328
|
+
logger.info("User logged in", extra={"user_id": "abc-123", "ip": "1.2.3.4"})
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### Serialization
|
|
332
|
+
|
|
333
|
+
Enhanced JSON and Msgpack serialization for types not handled by default (Decimal, datetime, date, dataclasses, Pydantic models).
|
|
334
|
+
|
|
335
|
+
```python
|
|
336
|
+
from python3_commons.serializers.msgspec import serialize_msgpack, deserialize_msgpack
|
|
337
|
+
from decimal import Decimal
|
|
338
|
+
from datetime import datetime
|
|
339
|
+
|
|
340
|
+
data = {
|
|
341
|
+
"amount": Decimal("150.75"),
|
|
342
|
+
"timestamp": datetime.now(),
|
|
343
|
+
"tags": {"finance", "internal"}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
# Serialize to Msgpack
|
|
347
|
+
binary = serialize_msgpack(data)
|
|
348
|
+
|
|
349
|
+
# Deserialize back
|
|
350
|
+
restored = deserialize_msgpack(binary)
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### RBAC Permissions
|
|
354
|
+
|
|
355
|
+
Database-backed Role-Based Access Control (RBAC) permission checking for users and API keys.
|
|
356
|
+
|
|
357
|
+
```python
|
|
358
|
+
from python3_commons.permissions import has_user_permission, has_api_key_permission
|
|
359
|
+
from uuid import UUID
|
|
360
|
+
|
|
361
|
+
user_uuid = UUID("...")
|
|
362
|
+
allowed = await has_user_permission(session, user_uuid, "reports.view")
|
|
363
|
+
|
|
364
|
+
api_key_uuid = UUID("...")
|
|
365
|
+
key_allowed = await has_api_key_permission(session, api_key_uuid, "reports.view")
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
### Configuration Management
|
|
369
|
+
|
|
370
|
+
Typed application settings powered by Pydantic Settings:
|
|
371
|
+
|
|
372
|
+
- `CommonSettings` / `settings`: General logging level and format.
|
|
373
|
+
- `DBSettings` / `db_settings`: PostgreSQL database settings (`DB_` environment prefix, with `DB_PASS` alias for password).
|
|
374
|
+
- `S3Settings` / `s3_settings`: S3 credentials and connection configuration (`S3_` environment prefix: `ACCESS_KEY_ID`, `SECRET_ACCESS_KEY`, `REGION`).
|
|
375
|
+
- `ObjectStorageSettings` / `object_storage_settings`: Object storage root configuration (`OBJECT_STORAGE_` environment prefix: `WORK_PATH`).
|
|
376
|
+
- `ApiClientSettings` / `api_client_settings`: API client configuration (`API_CLIENT_` environment prefix: `AUDIT_PATH`).
|
|
377
|
+
- `OIDCSettings`: OpenID Connect configuration (`OIDC_` environment prefix).
|
|
378
|
+
- `ValkeySettings` / `valkey_settings`: Valkey/Redis cache configuration (`VALKEY_` environment prefix).
|
|
379
|
+
|
|
380
|
+
### File System Utilities
|
|
381
|
+
|
|
382
|
+
File system traversal helpers:
|
|
383
|
+
|
|
384
|
+
```python
|
|
385
|
+
from python3_commons.fs import iter_files
|
|
386
|
+
from pathlib import Path
|
|
387
|
+
|
|
388
|
+
# Recursively iterate files, ignoring hidden directories
|
|
389
|
+
for file_path in iter_files(Path("./data"), recursive=True):
|
|
390
|
+
print(file_path)
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
### General Helpers
|
|
394
|
+
|
|
395
|
+
A collection of useful utility functions:
|
|
396
|
+
|
|
397
|
+
- `to_snake_case(text)`: Converts strings to snake_case.
|
|
398
|
+
- `round_decimal(value, places)`: Rounds `Decimal` values.
|
|
399
|
+
- `tries(n)`: An async retry decorator.
|
|
400
|
+
- `log_execution_time`: An async decorator to log how long a function takes.
|
|
401
|
+
- `date_from_string` / `datetime_from_string`: Flexible date/time parsing.
|
|
402
|
+
- `date_range(start, end)`: Generator yielding dates in a range.
|
|
403
|
+
- `request_to_curl`: Converts request parameters to a `curl` command string.
|
|
404
|
+
- `SingletonMeta`: Thread-safe singleton metaclass.
|
|
405
|
+
- `replace_origin(url, host_url)`: Replaces scheme, host, and port of a URL.
|
|
406
|
+
- `parse_string_list`: Parses comma-separated string lists into tuples.
|
|
407
|
+
|
|
408
|
+
```python
|
|
409
|
+
from python3_commons.helpers import tries, log_execution_time, SingletonMeta
|
|
410
|
+
|
|
411
|
+
@tries(3)
|
|
412
|
+
@log_execution_time
|
|
413
|
+
async def flaky_network_call():
|
|
414
|
+
...
|
|
415
|
+
|
|
416
|
+
class AppService(metaclass=SingletonMeta):
|
|
417
|
+
pass
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
### Async CSV Stream
|
|
421
|
+
|
|
422
|
+
Efficiently generate CSV data as a byte stream from an async generator of tuples.
|
|
423
|
+
|
|
424
|
+
```python
|
|
425
|
+
from python3_commons.generators import tuple_csv_stream
|
|
426
|
+
|
|
427
|
+
async def generate_rows():
|
|
428
|
+
for i in range(1000):
|
|
429
|
+
yield (i, f"Name {i}", 10.5 * i)
|
|
430
|
+
|
|
431
|
+
async for chunk in tuple_csv_stream(generate_rows(), header=("ID", "Name", "Value")):
|
|
432
|
+
# Send chunk to HTTP response or write to file
|
|
433
|
+
pass
|
|
434
|
+
```
|