python3-commons 0.24.7__tar.gz → 0.25.1__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 (90) hide show
  1. python3_commons-0.25.1/PKG-INFO +434 -0
  2. python3_commons-0.25.1/README.md +385 -0
  3. {python3_commons-0.24.7 → python3_commons-0.25.1}/pyproject.toml +4 -6
  4. {python3_commons-0.24.7 → python3_commons-0.25.1}/src/python3_commons/api_client.py +15 -3
  5. {python3_commons-0.24.7 → python3_commons-0.25.1}/src/python3_commons/audit.py +17 -5
  6. {python3_commons-0.24.7 → python3_commons-0.25.1}/src/python3_commons/conf.py +22 -8
  7. python3_commons-0.25.1/src/python3_commons/object_storage.py +247 -0
  8. python3_commons-0.25.1/src/python3_commons.egg-info/PKG-INFO +434 -0
  9. {python3_commons-0.24.7 → python3_commons-0.25.1}/src/python3_commons.egg-info/SOURCES.txt +2 -0
  10. {python3_commons-0.24.7 → python3_commons-0.25.1}/src/python3_commons.egg-info/requires.txt +3 -4
  11. {python3_commons-0.24.7 → python3_commons-0.25.1}/src/python3_commons.egg-info/scm_file_list.json +2 -0
  12. python3_commons-0.25.1/src/python3_commons.egg-info/scm_version.json +8 -0
  13. python3_commons-0.25.1/tests/integration/test_osc.py +49 -0
  14. python3_commons-0.25.1/tests/unit/test_api_client.py +491 -0
  15. python3_commons-0.25.1/tests/unit/test_audit.py +233 -0
  16. python3_commons-0.25.1/tests/unit/test_object_storage.py +249 -0
  17. {python3_commons-0.24.7 → python3_commons-0.25.1}/uv.lock +67 -181
  18. python3_commons-0.24.7/PKG-INFO +0 -310
  19. python3_commons-0.24.7/README.md +0 -244
  20. python3_commons-0.24.7/src/python3_commons/object_storage.py +0 -213
  21. python3_commons-0.24.7/src/python3_commons.egg-info/PKG-INFO +0 -310
  22. python3_commons-0.24.7/src/python3_commons.egg-info/scm_version.json +0 -8
  23. python3_commons-0.24.7/tests/integration/test_osc.py +0 -45
  24. python3_commons-0.24.7/tests/unit/test_audit.py +0 -31
  25. {python3_commons-0.24.7 → python3_commons-0.25.1}/.coveragerc +0 -0
  26. {python3_commons-0.24.7 → python3_commons-0.25.1}/.devcontainer/Dockerfile +0 -0
  27. {python3_commons-0.24.7 → python3_commons-0.25.1}/.devcontainer/devcontainer.json +0 -0
  28. {python3_commons-0.24.7 → python3_commons-0.25.1}/.devcontainer/docker-compose.yml +0 -0
  29. {python3_commons-0.24.7 → python3_commons-0.25.1}/.env_template +0 -0
  30. {python3_commons-0.24.7 → python3_commons-0.25.1}/.github/workflows/checks.yml +0 -0
  31. {python3_commons-0.24.7 → python3_commons-0.25.1}/.github/workflows/python-publish.yaml +0 -0
  32. {python3_commons-0.24.7 → python3_commons-0.25.1}/.github/workflows/release-on-tag-push.yml +0 -0
  33. {python3_commons-0.24.7 → python3_commons-0.25.1}/.gitignore +0 -0
  34. {python3_commons-0.24.7 → python3_commons-0.25.1}/.pre-commit-config.yaml +0 -0
  35. {python3_commons-0.24.7 → python3_commons-0.25.1}/.python-version +0 -0
  36. {python3_commons-0.24.7 → python3_commons-0.25.1}/AUTHORS.rst +0 -0
  37. {python3_commons-0.24.7 → python3_commons-0.25.1}/CHANGELOG.rst +0 -0
  38. {python3_commons-0.24.7 → python3_commons-0.25.1}/LICENSE +0 -0
  39. {python3_commons-0.24.7 → python3_commons-0.25.1}/README.rst +0 -0
  40. {python3_commons-0.24.7 → python3_commons-0.25.1}/docs/Makefile +0 -0
  41. {python3_commons-0.24.7 → python3_commons-0.25.1}/docs/_static/.gitignore +0 -0
  42. {python3_commons-0.24.7 → python3_commons-0.25.1}/docs/authors.rst +0 -0
  43. {python3_commons-0.24.7 → python3_commons-0.25.1}/docs/changelog.rst +0 -0
  44. {python3_commons-0.24.7 → python3_commons-0.25.1}/docs/conf.py +0 -0
  45. {python3_commons-0.24.7 → python3_commons-0.25.1}/docs/index.rst +0 -0
  46. {python3_commons-0.24.7 → python3_commons-0.25.1}/docs/license.rst +0 -0
  47. {python3_commons-0.24.7 → python3_commons-0.25.1}/setup.cfg +0 -0
  48. {python3_commons-0.24.7 → python3_commons-0.25.1}/src/python3_commons/__init__.py +0 -0
  49. {python3_commons-0.24.7 → python3_commons-0.25.1}/src/python3_commons/async_functools.py +0 -0
  50. {python3_commons-0.24.7 → python3_commons-0.25.1}/src/python3_commons/auth.py +0 -0
  51. {python3_commons-0.24.7 → python3_commons-0.25.1}/src/python3_commons/cache.py +0 -0
  52. {python3_commons-0.24.7 → python3_commons-0.25.1}/src/python3_commons/db/__init__.py +0 -0
  53. {python3_commons-0.24.7 → python3_commons-0.25.1}/src/python3_commons/db/helpers.py +0 -0
  54. {python3_commons-0.24.7 → python3_commons-0.25.1}/src/python3_commons/db/models/__init__.py +0 -0
  55. {python3_commons-0.24.7 → python3_commons-0.25.1}/src/python3_commons/db/models/auth.py +0 -0
  56. {python3_commons-0.24.7 → python3_commons-0.25.1}/src/python3_commons/db/models/common.py +0 -0
  57. {python3_commons-0.24.7 → python3_commons-0.25.1}/src/python3_commons/db/models/rbac.py +0 -0
  58. {python3_commons-0.24.7 → python3_commons-0.25.1}/src/python3_commons/db/models/users.py +0 -0
  59. {python3_commons-0.24.7 → python3_commons-0.25.1}/src/python3_commons/exceptions.py +0 -0
  60. {python3_commons-0.24.7 → python3_commons-0.25.1}/src/python3_commons/fs.py +0 -0
  61. {python3_commons-0.24.7 → python3_commons-0.25.1}/src/python3_commons/generators.py +0 -0
  62. {python3_commons-0.24.7 → python3_commons-0.25.1}/src/python3_commons/helpers.py +0 -0
  63. {python3_commons-0.24.7 → python3_commons-0.25.1}/src/python3_commons/log/__init__.py +0 -0
  64. {python3_commons-0.24.7 → python3_commons-0.25.1}/src/python3_commons/log/filters.py +0 -0
  65. {python3_commons-0.24.7 → python3_commons-0.25.1}/src/python3_commons/log/formatters.py +0 -0
  66. {python3_commons-0.24.7 → python3_commons-0.25.1}/src/python3_commons/permissions.py +0 -0
  67. {python3_commons-0.24.7 → python3_commons-0.25.1}/src/python3_commons/serializers/__init__.py +0 -0
  68. {python3_commons-0.24.7 → python3_commons-0.25.1}/src/python3_commons/serializers/common.py +0 -0
  69. {python3_commons-0.24.7 → python3_commons-0.25.1}/src/python3_commons/serializers/json.py +0 -0
  70. {python3_commons-0.24.7 → python3_commons-0.25.1}/src/python3_commons/serializers/msgpack.py +0 -0
  71. {python3_commons-0.24.7 → python3_commons-0.25.1}/src/python3_commons/serializers/msgspec.py +0 -0
  72. {python3_commons-0.24.7 → python3_commons-0.25.1}/src/python3_commons/soap_client.py +0 -0
  73. {python3_commons-0.24.7 → python3_commons-0.25.1}/src/python3_commons.egg-info/dependency_links.txt +0 -0
  74. {python3_commons-0.24.7 → python3_commons-0.25.1}/src/python3_commons.egg-info/top_level.txt +0 -0
  75. {python3_commons-0.24.7 → python3_commons-0.25.1}/tests/__init__.py +0 -0
  76. {python3_commons-0.24.7 → python3_commons-0.25.1}/tests/integration/__init__.py +0 -0
  77. {python3_commons-0.24.7 → python3_commons-0.25.1}/tests/integration/conftest.py +0 -0
  78. {python3_commons-0.24.7 → python3_commons-0.25.1}/tests/integration/test_auth.py +0 -0
  79. {python3_commons-0.24.7 → python3_commons-0.25.1}/tests/integration/test_cache.py +0 -0
  80. {python3_commons-0.24.7 → python3_commons-0.25.1}/tests/unit/__init__.py +0 -0
  81. {python3_commons-0.24.7 → python3_commons-0.25.1}/tests/unit/conftest.py +0 -0
  82. {python3_commons-0.24.7 → python3_commons-0.25.1}/tests/unit/db/__init__.py +0 -0
  83. {python3_commons-0.24.7 → python3_commons-0.25.1}/tests/unit/db/test_async_session_manager.py +0 -0
  84. {python3_commons-0.24.7 → python3_commons-0.25.1}/tests/unit/log/__init__.py +0 -0
  85. {python3_commons-0.24.7 → python3_commons-0.25.1}/tests/unit/log/test_formatters.py +0 -0
  86. {python3_commons-0.24.7 → python3_commons-0.25.1}/tests/unit/test_async_functools.py +0 -0
  87. {python3_commons-0.24.7 → python3_commons-0.25.1}/tests/unit/test_auth.py +0 -0
  88. {python3_commons-0.24.7 → python3_commons-0.25.1}/tests/unit/test_helpers.py +0 -0
  89. {python3_commons-0.24.7 → python3_commons-0.25.1}/tests/unit/test_msgpack.py +0 -0
  90. {python3_commons-0.24.7 → python3_commons-0.25.1}/tests/unit/test_msgspec.py +0 -0
@@ -0,0 +1,434 @@
1
+ Metadata-Version: 2.4
2
+ Name: python3-commons
3
+ Version: 0.25.1
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.3
16
+ Requires-Dist: msgspec~=0.22.0
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/"`). Note that `API_CLIENT_AUDIT_PATH` is a full object storage path like `s3://bucket/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 (full object storage path) or Object storage root
127
+ export API_CLIENT_AUDIT_PATH="s3://bucket/audit"
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 = "s3://bucket/audit"
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` full object storage path like `s3://bucket/audit`).
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
+ ```