chutils 2.7.3__tar.gz → 2.8.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.
- {chutils-2.7.3 → chutils-2.8.0}/PKG-INFO +41 -1
- {chutils-2.7.3 → chutils-2.8.0}/README.md +39 -0
- {chutils-2.7.3 → chutils-2.8.0}/pyproject.toml +29 -2
- {chutils-2.7.3 → chutils-2.8.0}/src/chutils/__init__.py +3 -0
- {chutils-2.7.3 → chutils-2.8.0}/src/chutils/__init__.pyi +93 -6
- chutils-2.8.0/src/chutils/__main__.py +4 -0
- {chutils-2.7.3 → chutils-2.8.0}/src/chutils/cache/base.py +17 -15
- {chutils-2.7.3 → chutils-2.8.0}/src/chutils/cache/decorator.py +8 -8
- {chutils-2.7.3 → chutils-2.8.0}/src/chutils/cache/in_memory.py +9 -7
- chutils-2.8.0/src/chutils/cli.py +112 -0
- {chutils-2.7.3 → chutils-2.8.0}/src/chutils/cli_booster.py +21 -18
- {chutils-2.7.3 → chutils-2.8.0}/src/chutils/cli_utils.py +48 -14
- {chutils-2.7.3 → chutils-2.8.0}/src/chutils/commands/base.py +6 -3
- {chutils-2.7.3 → chutils-2.8.0}/src/chutils/commands/config.py +69 -11
- chutils-2.8.0/src/chutils/commands/dev.py +307 -0
- {chutils-2.7.3 → chutils-2.8.0}/src/chutils/commands/init.py +13 -5
- {chutils-2.7.3 → chutils-2.8.0}/src/chutils/commands/paths.py +5 -3
- {chutils-2.7.3 → chutils-2.8.0}/src/chutils/commands/secrets.py +32 -21
- {chutils-2.7.3 → chutils-2.8.0}/src/chutils/commands/template.py +29 -12
- {chutils-2.7.3 → chutils-2.8.0}/src/chutils/commands/validate.py +30 -16
- {chutils-2.7.3 → chutils-2.8.0}/src/chutils/config/__init__.py +35 -15
- {chutils-2.7.3 → chutils-2.8.0}/src/chutils/config/core.py +82 -10
- chutils-2.8.0/src/chutils/config/dev.py +142 -0
- {chutils-2.7.3 → chutils-2.8.0}/src/chutils/config/diagnostics.py +13 -7
- {chutils-2.7.3 → chutils-2.8.0}/src/chutils/config/generator.py +21 -17
- {chutils-2.7.3 → chutils-2.8.0}/src/chutils/config/getters.py +82 -69
- {chutils-2.7.3 → chutils-2.8.0}/src/chutils/config/manager.py +72 -47
- {chutils-2.7.3 → chutils-2.8.0}/src/chutils/config/providers.py +75 -36
- {chutils-2.7.3 → chutils-2.8.0}/src/chutils/config/schema.py +28 -22
- {chutils-2.7.3 → chutils-2.8.0}/src/chutils/config/utils.py +111 -10
- {chutils-2.7.3 → chutils-2.8.0}/src/chutils/config/watcher.py +15 -10
- {chutils-2.7.3 → chutils-2.8.0}/src/chutils/context.py +15 -7
- {chutils-2.7.3 → chutils-2.8.0}/src/chutils/decorators.py +26 -20
- chutils-2.8.0/src/chutils/dev/__init__.py +12 -0
- chutils-2.8.0/src/chutils/dev/ai_lint.py +314 -0
- {chutils-2.7.3 → chutils-2.8.0}/src/chutils/dev/ast_indexer.py +91 -34
- {chutils-2.7.3 → chutils-2.8.0}/src/chutils/dev/models.py +22 -3
- chutils-2.8.0/src/chutils/dev/rules.py +524 -0
- {chutils-2.7.3 → chutils-2.8.0}/src/chutils/env.py +15 -0
- {chutils-2.7.3 → chutils-2.8.0}/src/chutils/exceptions.py +48 -6
- {chutils-2.7.3 → chutils-2.8.0}/src/chutils/features.py +22 -18
- {chutils-2.7.3 → chutils-2.8.0}/src/chutils/fs.py +52 -3
- {chutils-2.7.3 → chutils-2.8.0}/src/chutils/lifecycle.py +30 -20
- chutils-2.8.0/src/chutils/logger/__init__.py +57 -0
- chutils-2.8.0/src/chutils/logger/core.py +179 -0
- chutils-2.8.0/src/chutils/logger/formatters.py +78 -0
- {chutils-2.7.3 → chutils-2.8.0}/src/chutils/logger/handlers.py +5 -5
- chutils-2.8.0/src/chutils/logger/internal/builder.py +402 -0
- chutils-2.8.0/src/chutils/logger/internal/levels.py +58 -0
- chutils-2.8.0/src/chutils/logger/internal/utils.py +60 -0
- {chutils-2.7.3 → chutils-2.8.0}/src/chutils/logger/masking.py +25 -11
- {chutils-2.7.3 → chutils-2.8.0}/src/chutils/secret_manager/core.py +6 -2
- {chutils-2.7.3 → chutils-2.8.0}/src/chutils/secret_manager/providers.py +3 -2
- {chutils-2.7.3 → chutils-2.8.0}/src/chutils/testing/fixtures.py +5 -4
- {chutils-2.7.3 → chutils-2.8.0}/src/chutils/time.py +10 -8
- {chutils-2.7.3 → chutils-2.8.0}/src/chutils/tracing.py +23 -23
- chutils-2.8.0/src/chutils/typing.py +73 -0
- chutils-2.7.3/src/chutils/cli.py +0 -57
- chutils-2.7.3/src/chutils/commands/dev.py +0 -172
- chutils-2.7.3/src/chutils/dev/__init__.py +0 -3
- chutils-2.7.3/src/chutils/logger/__init__.py +0 -38
- chutils-2.7.3/src/chutils/logger/core.py +0 -526
- chutils-2.7.3/src/chutils/logger/formatters.py +0 -52
- {chutils-2.7.3 → chutils-2.8.0}/LICENSE +0 -0
- {chutils-2.7.3 → chutils-2.8.0}/src/chutils/cache/__init__.py +0 -0
- {chutils-2.7.3 → chutils-2.8.0}/src/chutils/cache/utils.py +0 -0
- {chutils-2.7.3 → chutils-2.8.0}/src/chutils/commands/__init__.py +0 -0
- {chutils-2.7.3 → chutils-2.8.0}/src/chutils/commands/utils.py +0 -0
- {chutils-2.7.3 → chutils-2.8.0}/src/chutils/config/GEMINI.md +0 -0
- {chutils-2.7.3 → chutils-2.8.0}/src/chutils/logger/GEMINI.md +0 -0
- {chutils-2.7.3 → chutils-2.8.0}/src/chutils/secret_manager/GEMINI.md +0 -0
- {chutils-2.7.3 → chutils-2.8.0}/src/chutils/secret_manager/__init__.py +0 -0
- {chutils-2.7.3 → chutils-2.8.0}/src/chutils/testing/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: chutils
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.8.0
|
|
4
4
|
Summary: Набор простых и удобных утилит для Python, который избавляет от рутины при работе с конфигурацией и логированием в новых проектах.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -46,6 +46,7 @@ Requires-Dist: python-json-logger (>=3.2.1) ; extra == "json"
|
|
|
46
46
|
Requires-Dist: pyyaml (>=6.0.3)
|
|
47
47
|
Requires-Dist: rich (>=15.0.0) ; extra == "full"
|
|
48
48
|
Requires-Dist: rich (>=15.0.0) ; extra == "rich"
|
|
49
|
+
Requires-Dist: typing-extensions (>=4.15.0,<5.0.0)
|
|
49
50
|
Requires-Dist: watchdog (>=6.0.0) ; extra == "full"
|
|
50
51
|
Requires-Dist: watchdog (>=6.0.0) ; extra == "watch"
|
|
51
52
|
Description-Content-Type: text/markdown
|
|
@@ -95,6 +96,10 @@ Every time you start a new project, you have to solve the same tasks:
|
|
|
95
96
|
- **🌐 Remote Configuration:** Load settings from HTTP/HTTPS endpoints with background polling and fallback support.
|
|
96
97
|
- **🔄 Hot-Reload:** Support for automatic configuration reloading on file changes without restart (requires
|
|
97
98
|
`pip install chutils[watch]`).
|
|
99
|
+
- **🛡️ Secure Paths:** Prevent Path Traversal attacks by safely resolving file paths against a base directory using
|
|
100
|
+
`resolve_safe_path()`.
|
|
101
|
+
- **🤖 AI Linter:** Run static analysis checks on your codebase to ensure AI readiness (strict type hints, structured
|
|
102
|
+
docstrings, API map sync) via `chutils dev ai-lint`.
|
|
98
103
|
- **⚡ Async Ready:** Most core functions have asynchronous versions (prefixed with `a`) for non-blocking execution.
|
|
99
104
|
- **🚀 Ready to Use:** Just install and use.
|
|
100
105
|
|
|
@@ -282,6 +287,22 @@ In environments like Docker or CI/CD where `keyring` is unavailable, you can sup
|
|
|
282
287
|
- Set `CH_DISABLE_KEYRING_WARNING=true` in environment.
|
|
283
288
|
- Or add `disable_keyring: true` under `secrets` section in `config.yml`.
|
|
284
289
|
|
|
290
|
+
### 4. Safe Path Resolution
|
|
291
|
+
|
|
292
|
+
Safely resolve relative and absolute paths against a base directory to prevent directory traversal vulnerabilities:
|
|
293
|
+
|
|
294
|
+
```python
|
|
295
|
+
from chutils.fs import resolve_safe_path
|
|
296
|
+
from chutils.exceptions import PathTraversalError
|
|
297
|
+
|
|
298
|
+
try:
|
|
299
|
+
# Resolves path relative to the base directory and checks if it's safe
|
|
300
|
+
safe_path = resolve_safe_path("user_file.txt", base_dir="./sandbox")
|
|
301
|
+
print(f"Safe absolute path: {safe_path}")
|
|
302
|
+
except PathTraversalError as e:
|
|
303
|
+
print(f"Path traversal detected! Attempted: {e.context.get('attempted_path')}")
|
|
304
|
+
```
|
|
305
|
+
|
|
285
306
|
## API Overview
|
|
286
307
|
|
|
287
308
|
### Configuration (`chutils.config`)
|
|
@@ -310,6 +331,11 @@ In environments like Docker or CI/CD where `keyring` is unavailable, you can sup
|
|
|
310
331
|
- `@retry`: Automatically retries a function if it fails. Supports sync/async, backoff, jitter, and exception filtering.
|
|
311
332
|
- `@cli_command`: Turns any function into a standalone CLI script with automatic argument parsing.
|
|
312
333
|
|
|
334
|
+
### File System (`chutils.fs`)
|
|
335
|
+
|
|
336
|
+
- `resolve_safe_path(path, base_dir)`: Safely resolves path and checks for Path Traversal attempts.
|
|
337
|
+
- `ensure_dir(path)`: Ensures directory exists.
|
|
338
|
+
|
|
313
339
|
### Time & Dates (`chutils.time`)
|
|
314
340
|
|
|
315
341
|
- `utc_now()`: Returns a timezone-aware UTC datetime.
|
|
@@ -388,6 +414,20 @@ Trace exactly where each configuration value comes from:
|
|
|
388
414
|
chutils config debug
|
|
389
415
|
```
|
|
390
416
|
|
|
417
|
+
### 6. AI-Readiness Linter
|
|
418
|
+
|
|
419
|
+
Perform a static audit of your codebase to ensure it is optimized for AI developers and agents:
|
|
420
|
+
|
|
421
|
+
```bash
|
|
422
|
+
# Run linter on the current project
|
|
423
|
+
chutils dev ai-lint
|
|
424
|
+
|
|
425
|
+
# Run linter in strict mode with ignored paths
|
|
426
|
+
chutils dev ai-lint --strict --ignore "temp/,build/"
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
See [docs/ai_lint.md](docs/ai_lint.md) for more details.
|
|
430
|
+
|
|
391
431
|
## License
|
|
392
432
|
|
|
393
433
|
The project is distributed under the MIT License.
|
|
@@ -43,6 +43,10 @@ Every time you start a new project, you have to solve the same tasks:
|
|
|
43
43
|
- **🌐 Remote Configuration:** Load settings from HTTP/HTTPS endpoints with background polling and fallback support.
|
|
44
44
|
- **🔄 Hot-Reload:** Support for automatic configuration reloading on file changes without restart (requires
|
|
45
45
|
`pip install chutils[watch]`).
|
|
46
|
+
- **🛡️ Secure Paths:** Prevent Path Traversal attacks by safely resolving file paths against a base directory using
|
|
47
|
+
`resolve_safe_path()`.
|
|
48
|
+
- **🤖 AI Linter:** Run static analysis checks on your codebase to ensure AI readiness (strict type hints, structured
|
|
49
|
+
docstrings, API map sync) via `chutils dev ai-lint`.
|
|
46
50
|
- **⚡ Async Ready:** Most core functions have asynchronous versions (prefixed with `a`) for non-blocking execution.
|
|
47
51
|
- **🚀 Ready to Use:** Just install and use.
|
|
48
52
|
|
|
@@ -230,6 +234,22 @@ In environments like Docker or CI/CD where `keyring` is unavailable, you can sup
|
|
|
230
234
|
- Set `CH_DISABLE_KEYRING_WARNING=true` in environment.
|
|
231
235
|
- Or add `disable_keyring: true` under `secrets` section in `config.yml`.
|
|
232
236
|
|
|
237
|
+
### 4. Safe Path Resolution
|
|
238
|
+
|
|
239
|
+
Safely resolve relative and absolute paths against a base directory to prevent directory traversal vulnerabilities:
|
|
240
|
+
|
|
241
|
+
```python
|
|
242
|
+
from chutils.fs import resolve_safe_path
|
|
243
|
+
from chutils.exceptions import PathTraversalError
|
|
244
|
+
|
|
245
|
+
try:
|
|
246
|
+
# Resolves path relative to the base directory and checks if it's safe
|
|
247
|
+
safe_path = resolve_safe_path("user_file.txt", base_dir="./sandbox")
|
|
248
|
+
print(f"Safe absolute path: {safe_path}")
|
|
249
|
+
except PathTraversalError as e:
|
|
250
|
+
print(f"Path traversal detected! Attempted: {e.context.get('attempted_path')}")
|
|
251
|
+
```
|
|
252
|
+
|
|
233
253
|
## API Overview
|
|
234
254
|
|
|
235
255
|
### Configuration (`chutils.config`)
|
|
@@ -258,6 +278,11 @@ In environments like Docker or CI/CD where `keyring` is unavailable, you can sup
|
|
|
258
278
|
- `@retry`: Automatically retries a function if it fails. Supports sync/async, backoff, jitter, and exception filtering.
|
|
259
279
|
- `@cli_command`: Turns any function into a standalone CLI script with automatic argument parsing.
|
|
260
280
|
|
|
281
|
+
### File System (`chutils.fs`)
|
|
282
|
+
|
|
283
|
+
- `resolve_safe_path(path, base_dir)`: Safely resolves path and checks for Path Traversal attempts.
|
|
284
|
+
- `ensure_dir(path)`: Ensures directory exists.
|
|
285
|
+
|
|
261
286
|
### Time & Dates (`chutils.time`)
|
|
262
287
|
|
|
263
288
|
- `utc_now()`: Returns a timezone-aware UTC datetime.
|
|
@@ -336,6 +361,20 @@ Trace exactly where each configuration value comes from:
|
|
|
336
361
|
chutils config debug
|
|
337
362
|
```
|
|
338
363
|
|
|
364
|
+
### 6. AI-Readiness Linter
|
|
365
|
+
|
|
366
|
+
Perform a static audit of your codebase to ensure it is optimized for AI developers and agents:
|
|
367
|
+
|
|
368
|
+
```bash
|
|
369
|
+
# Run linter on the current project
|
|
370
|
+
chutils dev ai-lint
|
|
371
|
+
|
|
372
|
+
# Run linter in strict mode with ignored paths
|
|
373
|
+
chutils dev ai-lint --strict --ignore "temp/,build/"
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
See [docs/ai_lint.md](docs/ai_lint.md) for more details.
|
|
377
|
+
|
|
339
378
|
## License
|
|
340
379
|
|
|
341
380
|
The project is distributed under the MIT License.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "chutils"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.8.0"
|
|
4
4
|
description = "Набор простых и удобных утилит для Python, который избавляет от рутины при работе с конфигурацией и логированием в новых проектах."
|
|
5
5
|
authors = [{name = "Chu4hel", email = "sergeiivanov636@gmail.com"}]
|
|
6
6
|
license = "MIT"
|
|
@@ -10,7 +10,8 @@ dependencies = [
|
|
|
10
10
|
"keyring (>=25.7.0)",
|
|
11
11
|
"pyyaml (>=6.0.3)",
|
|
12
12
|
"python-dotenv (>=1.2.1) ; python_full_version < '3.10'",
|
|
13
|
-
"python-dotenv (>=1.2.2) ; python_full_version >= '3.10'"
|
|
13
|
+
"python-dotenv (>=1.2.2) ; python_full_version >= '3.10'",
|
|
14
|
+
"typing-extensions (>=4.15.0,<5.0.0)"
|
|
14
15
|
]
|
|
15
16
|
|
|
16
17
|
[project.optional-dependencies]
|
|
@@ -80,6 +81,7 @@ pytest-cov = "^7.1.0"
|
|
|
80
81
|
pytest-asyncio = { version = "^1.3.0", python = ">=3.10" }
|
|
81
82
|
ruff = "^0.15.12"
|
|
82
83
|
rich = "^15.0.0"
|
|
84
|
+
mypy = { version = "^2.1.0", python = ">=3.10" }
|
|
83
85
|
|
|
84
86
|
[build-system]
|
|
85
87
|
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
@@ -88,4 +90,29 @@ build-backend = "poetry.core.masonry.api"
|
|
|
88
90
|
[tool.pytest.ini_options]
|
|
89
91
|
pythonpath = [
|
|
90
92
|
"src"
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
[tool.mypy]
|
|
96
|
+
python_version = "3.10"
|
|
97
|
+
strict = true
|
|
98
|
+
ignore_missing_imports = true
|
|
99
|
+
warn_unused_ignores = true
|
|
100
|
+
warn_redundant_casts = true
|
|
101
|
+
check_untyped_defs = true
|
|
102
|
+
disallow_untyped_defs = true
|
|
103
|
+
|
|
104
|
+
[tool.chutils.ai-lint]
|
|
105
|
+
strict = false
|
|
106
|
+
ignore = [
|
|
107
|
+
".git",
|
|
108
|
+
".venv",
|
|
109
|
+
"__pycache__",
|
|
110
|
+
"build",
|
|
111
|
+
"dist",
|
|
112
|
+
"tests",
|
|
113
|
+
"site",
|
|
114
|
+
"examples",
|
|
115
|
+
"docs",
|
|
116
|
+
"src/chutils/testing",
|
|
117
|
+
"src/chutils/typing.py"
|
|
91
118
|
]
|
|
@@ -51,6 +51,7 @@ _LAZY_MAPPING = {
|
|
|
51
51
|
'time': ('.time', None),
|
|
52
52
|
'tracing': ('.tracing', None),
|
|
53
53
|
'testing': ('.testing', None),
|
|
54
|
+
'dev': ('.dev', None),
|
|
54
55
|
|
|
55
56
|
# config
|
|
56
57
|
'get_config': ('.config', 'get_config'),
|
|
@@ -77,6 +78,8 @@ _LAZY_MAPPING = {
|
|
|
77
78
|
'get_config_paths': ('.config', 'get_config_paths'),
|
|
78
79
|
'get_all_config_paths': ('.config', 'get_all_config_paths'),
|
|
79
80
|
'export_schema': ('.config', 'export_schema'),
|
|
81
|
+
'load_ai_lint_config': ('.config', 'load_ai_lint_config'),
|
|
82
|
+
'parse_chutils_ignore': ('.config', 'parse_chutils_ignore'),
|
|
80
83
|
|
|
81
84
|
# features
|
|
82
85
|
'is_feature_enabled': ('.features', 'is_feature_enabled'),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import datetime
|
|
2
2
|
import logging
|
|
3
|
+
from enum import Enum
|
|
3
4
|
from typing import Any, Optional, List, Dict, Type, TypeVar, Union, Tuple, Callable
|
|
4
5
|
|
|
5
6
|
# Тип для Pydantic моделей
|
|
@@ -11,8 +12,12 @@ F = TypeVar("F", bound=Callable[..., Any])
|
|
|
11
12
|
def init(base_dir: str) -> None: ...
|
|
12
13
|
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
def get_config(
|
|
16
|
+
model: Optional[Type[T]] = None,
|
|
17
|
+
remote_url: Optional[str] = None,
|
|
18
|
+
remote_auth: Optional[Tuple[str, str]] = None,
|
|
19
|
+
polling_interval: Optional[int] = None,
|
|
20
|
+
) -> Union[Dict[str, Any], T]: ...
|
|
16
21
|
|
|
17
22
|
|
|
18
23
|
def get_config_value(section: str, key: str, fallback: Any = None, config: Optional[Dict[str, Any]] = None) -> Any: ...
|
|
@@ -71,6 +76,27 @@ def generate_env_template(model_class: Type[T], prefix: str = "CH") -> str: ...
|
|
|
71
76
|
def generate_json_schema(model_class: Type[T]) -> str: ...
|
|
72
77
|
|
|
73
78
|
|
|
79
|
+
def get_base_dir() -> Optional[str]: ...
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def get_config_file_path() -> Optional[str]: ...
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def is_config_loaded() -> bool: ...
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def are_paths_initialized() -> bool: ...
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def get_config_paths(cfg_file: Optional[str] = None) -> Tuple[Optional[str], Optional[str]]: ...
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def get_all_config_paths(cfg_file: Optional[str] = None) -> Tuple[Optional[str], Optional[str], Optional[str]]: ...
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def export_schema(model: Union[Type[T], str], output_path: Optional[Union[str, Any]] = None) -> str: ...
|
|
98
|
+
|
|
99
|
+
|
|
74
100
|
# --- features ---
|
|
75
101
|
def is_feature_enabled(feature_name: str, context: Optional[Dict[str, Any]] = None) -> bool: ...
|
|
76
102
|
|
|
@@ -87,14 +113,59 @@ class ChutilsLogger(logging.Logger):
|
|
|
87
113
|
def add_mask(self, secret: str) -> None: ...
|
|
88
114
|
|
|
89
115
|
|
|
90
|
-
def setup_logger(
|
|
91
|
-
|
|
92
|
-
|
|
116
|
+
def setup_logger(
|
|
117
|
+
name: str = 'app_logger',
|
|
118
|
+
config_section_name: Optional[str] = None,
|
|
119
|
+
log_level: Optional[LogLevel] = None,
|
|
120
|
+
log_file_name: Optional[str] = None,
|
|
121
|
+
force_reconfigure: bool = False,
|
|
122
|
+
rotation_type: Optional[str] = None,
|
|
123
|
+
max_bytes: Optional[int] = None,
|
|
124
|
+
compress: Optional[bool] = None,
|
|
125
|
+
backup_count: Optional[int] = None,
|
|
126
|
+
encoding: Optional[str] = None,
|
|
127
|
+
when: Optional[str] = None,
|
|
128
|
+
interval: Optional[int] = None,
|
|
129
|
+
utc: Optional[bool] = None,
|
|
130
|
+
at_time: Any = None,
|
|
131
|
+
json_format: Optional[bool] = None,
|
|
132
|
+
use_async: Optional[bool] = None,
|
|
133
|
+
max_queue_size: Optional[int] = None,
|
|
134
|
+
) -> ChutilsLogger: ...
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class LogLevel(str, Enum):
|
|
138
|
+
DEVDEBUG = "DEVDEBUG"
|
|
139
|
+
DEBUG = "DEBUG"
|
|
140
|
+
MEDIUMDEBUG = "MEDIUMDEBUG"
|
|
141
|
+
INFO = "INFO"
|
|
142
|
+
WARNING = "WARNING"
|
|
143
|
+
ERROR = "ERROR"
|
|
144
|
+
CRITICAL = "CRITICAL"
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def get_console(stderr: bool = False) -> Any: ...
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class SecretMaskingFilter(logging.Filter): ...
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class ChutilsJsonFormatter(logging.Formatter): ...
|
|
93
154
|
|
|
94
155
|
|
|
95
156
|
class SafeTimedRotatingFileHandler(logging.Handler): ...
|
|
96
157
|
|
|
97
158
|
|
|
159
|
+
class CompressingRotatingFileHandler(logging.Handler): ...
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class CompressingTimedRotatingFileHandler(logging.Handler): ...
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
DEVDEBUG_LEVEL_NUM: int
|
|
166
|
+
MEDIUMDEBUG_LEVEL_NUM: int
|
|
167
|
+
|
|
168
|
+
|
|
98
169
|
# --- context ---
|
|
99
170
|
def bind_context(**kwargs: Any) -> Any: ...
|
|
100
171
|
|
|
@@ -123,7 +194,22 @@ def utc_now() -> datetime.datetime: ...
|
|
|
123
194
|
def parse_datetime(value: Union[str, int, float]) -> datetime.datetime: ...
|
|
124
195
|
|
|
125
196
|
|
|
126
|
-
def humanize_timedelta(dt: datetime.datetime, locale: str = 'ru',
|
|
197
|
+
def humanize_timedelta(dt: datetime.datetime, locale: str = 'ru',
|
|
198
|
+
custom_locales: Optional[Dict[str, Any]] = None) -> str: ...
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# --- env (Discovery) ---
|
|
202
|
+
def is_rich_enabled() -> bool: ...
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def is_otel_enabled() -> bool: ...
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
RICH_AVAILABLE: bool
|
|
209
|
+
PYDANTIC_AVAILABLE: bool
|
|
210
|
+
WATCHDOG_AVAILABLE: bool
|
|
211
|
+
JSON_LOGGER_AVAILABLE: bool
|
|
212
|
+
OTEL_AVAILABLE: bool
|
|
127
213
|
|
|
128
214
|
|
|
129
215
|
# --- secret_manager ---
|
|
@@ -224,3 +310,4 @@ from . import lifecycle as lifecycle
|
|
|
224
310
|
from . import features as features
|
|
225
311
|
from . import time as time
|
|
226
312
|
from . import tracing as tracing
|
|
313
|
+
from . import dev as dev
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod
|
|
2
|
-
from typing import
|
|
2
|
+
from typing import Optional, TypeVar, Generic
|
|
3
3
|
|
|
4
|
+
T = TypeVar("T")
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
|
|
7
|
+
class BaseCacheBackend(ABC, Generic[T]):
|
|
6
8
|
"""
|
|
7
9
|
Базовый абстрактный класс для всех бэкендов кэширования.
|
|
8
10
|
|
|
@@ -10,7 +12,7 @@ class BaseCacheBackend(ABC):
|
|
|
10
12
|
"""
|
|
11
13
|
|
|
12
14
|
@abstractmethod
|
|
13
|
-
def get(self, key: str) ->
|
|
15
|
+
def get(self, key: str) -> Optional[T]:
|
|
14
16
|
"""
|
|
15
17
|
Получить значение из кэша.
|
|
16
18
|
|
|
@@ -18,18 +20,18 @@ class BaseCacheBackend(ABC):
|
|
|
18
20
|
key (str): Ключ кэша.
|
|
19
21
|
|
|
20
22
|
Returns:
|
|
21
|
-
|
|
23
|
+
Значение или None, если ключ не найден или просрочен.
|
|
22
24
|
"""
|
|
23
25
|
pass
|
|
24
26
|
|
|
25
27
|
@abstractmethod
|
|
26
|
-
def set(self, key: str, value:
|
|
28
|
+
def set(self, key: str, value: T, ttl: Optional[int] = None) -> None:
|
|
27
29
|
"""
|
|
28
30
|
Сохранить значение в кэше.
|
|
29
|
-
|
|
31
|
+
|
|
30
32
|
Args:
|
|
31
33
|
key (str): Ключ кэша.
|
|
32
|
-
value
|
|
34
|
+
value: Значение для сохранения.
|
|
33
35
|
ttl (Optional[int]): Время жизни в секундах. Если None, используется вечное хранение.
|
|
34
36
|
"""
|
|
35
37
|
pass
|
|
@@ -38,7 +40,7 @@ class BaseCacheBackend(ABC):
|
|
|
38
40
|
def delete(self, key: str) -> None:
|
|
39
41
|
"""
|
|
40
42
|
Удалить ключ из кэша.
|
|
41
|
-
|
|
43
|
+
|
|
42
44
|
Args:
|
|
43
45
|
key (str): Ключ кэша.
|
|
44
46
|
"""
|
|
@@ -48,10 +50,10 @@ class BaseCacheBackend(ABC):
|
|
|
48
50
|
def exists(self, key: str) -> bool:
|
|
49
51
|
"""
|
|
50
52
|
Проверить наличие ключа в кэше.
|
|
51
|
-
|
|
53
|
+
|
|
52
54
|
Args:
|
|
53
55
|
key (str): Ключ кэша.
|
|
54
|
-
|
|
56
|
+
|
|
55
57
|
Returns:
|
|
56
58
|
bool: True, если ключ существует и не просрочен.
|
|
57
59
|
"""
|
|
@@ -64,17 +66,17 @@ class BaseCacheBackend(ABC):
|
|
|
64
66
|
|
|
65
67
|
# --- Асинхронные методы (по умолчанию вызывают синхронные) ---
|
|
66
68
|
|
|
67
|
-
async def aget(self, key: str) ->
|
|
69
|
+
async def aget(self, key: str) -> Optional[T]:
|
|
68
70
|
"""Асинхронное получение значения."""
|
|
69
71
|
return self.get(key)
|
|
70
72
|
|
|
71
|
-
async def aset(self, key: str, value:
|
|
73
|
+
async def aset(self, key: str, value: T, ttl: Optional[int] = None) -> None:
|
|
72
74
|
"""Асинхронное сохранение значения."""
|
|
73
|
-
|
|
75
|
+
self.set(key, value, ttl)
|
|
74
76
|
|
|
75
77
|
async def adelete(self, key: str) -> None:
|
|
76
78
|
"""Асинхронное удаление значения."""
|
|
77
|
-
|
|
79
|
+
self.delete(key)
|
|
78
80
|
|
|
79
81
|
async def aexists(self, key: str) -> bool:
|
|
80
82
|
"""Асинхронная проверка наличия ключа."""
|
|
@@ -82,4 +84,4 @@ class BaseCacheBackend(ABC):
|
|
|
82
84
|
|
|
83
85
|
async def aclear(self) -> None:
|
|
84
86
|
"""Асинхронная очистка кэша."""
|
|
85
|
-
|
|
87
|
+
self.clear()
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import functools
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import Callable, Optional, Any
|
|
4
4
|
|
|
5
5
|
from .in_memory import InMemoryCacheBackend
|
|
6
6
|
from .utils import generate_cache_key, LockManager, AsyncLockManager
|
|
7
7
|
|
|
8
8
|
# Экземпляры по умолчанию
|
|
9
|
-
_default_backend = InMemoryCacheBackend()
|
|
9
|
+
_default_backend: InMemoryCacheBackend[Any] = InMemoryCacheBackend()
|
|
10
10
|
_sync_lock_manager = LockManager()
|
|
11
11
|
_async_lock_manager = AsyncLockManager()
|
|
12
12
|
|
|
@@ -15,23 +15,23 @@ def cache_with_ttl(
|
|
|
15
15
|
ttl: int = 60,
|
|
16
16
|
key_prefix: str = "",
|
|
17
17
|
sliding: bool = True,
|
|
18
|
-
backend: Optional[InMemoryCacheBackend] = None
|
|
19
|
-
) -> Callable:
|
|
18
|
+
backend: Optional[InMemoryCacheBackend[Any]] = None
|
|
19
|
+
) -> Callable[..., Any]:
|
|
20
20
|
"""
|
|
21
21
|
Декоратор для кэширования результатов выполнения функций с поддержкой TTL.
|
|
22
|
-
|
|
22
|
+
|
|
23
23
|
Args:
|
|
24
24
|
ttl (int): Время жизни закэшированного значения в секундах. По умолчанию 60.
|
|
25
25
|
key_prefix (str): Префикс для ключа кэша.
|
|
26
26
|
sliding (bool): Если True, TTL продлевается при каждом успешном чтении из кэша.
|
|
27
27
|
backend: Инстанс бэкенда для хранения (по умолчанию InMemoryCacheBackend).
|
|
28
|
-
|
|
28
|
+
|
|
29
29
|
Returns:
|
|
30
30
|
Callable: Обернутая функция.
|
|
31
31
|
"""
|
|
32
|
-
cache = backend or _default_backend
|
|
32
|
+
cache: InMemoryCacheBackend[Any] = backend or _default_backend
|
|
33
33
|
|
|
34
|
-
def decorator(func: Callable) -> Callable:
|
|
34
|
+
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
35
35
|
func_name = f"{func.__module__}.{func.__name__}"
|
|
36
36
|
is_async = asyncio.iscoroutinefunction(func)
|
|
37
37
|
|
|
@@ -1,28 +1,30 @@
|
|
|
1
1
|
import threading
|
|
2
2
|
import time
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import Dict, Optional, Tuple, TypeVar
|
|
4
4
|
|
|
5
5
|
from .base import BaseCacheBackend
|
|
6
6
|
|
|
7
|
+
T = TypeVar("T")
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
|
|
10
|
+
class InMemoryCacheBackend(BaseCacheBackend[T]):
|
|
9
11
|
"""
|
|
10
12
|
Реализация кэша в оперативной памяти на базе словаря.
|
|
11
|
-
|
|
13
|
+
|
|
12
14
|
Поддерживает TTL, потокобезопасность и ленивую очистку просроченных записей.
|
|
13
15
|
"""
|
|
14
16
|
|
|
15
17
|
def __init__(self) -> None:
|
|
16
18
|
# Структура: {key: (value, expires_at)}
|
|
17
|
-
self._cache: Dict[str, Tuple[
|
|
19
|
+
self._cache: Dict[str, Tuple[T, Optional[float]]] = {}
|
|
18
20
|
self._lock = threading.Lock()
|
|
19
21
|
|
|
20
|
-
def get(self, key: str) ->
|
|
22
|
+
def get(self, key: str) -> Optional[T]:
|
|
21
23
|
"""Получить значение. Если просрочено - удаляет его."""
|
|
22
24
|
with self._lock:
|
|
23
25
|
return self._get_without_lock(key)
|
|
24
26
|
|
|
25
|
-
def _get_without_lock(self, key: str) ->
|
|
27
|
+
def _get_without_lock(self, key: str) -> Optional[T]:
|
|
26
28
|
"""Внутренний метод получения без блокировки (для использования внутри других методов)."""
|
|
27
29
|
if key not in self._cache:
|
|
28
30
|
return None
|
|
@@ -34,7 +36,7 @@ class InMemoryCacheBackend(BaseCacheBackend):
|
|
|
34
36
|
|
|
35
37
|
return value
|
|
36
38
|
|
|
37
|
-
def set(self, key: str, value:
|
|
39
|
+
def set(self, key: str, value: T, ttl: Optional[int] = None) -> None:
|
|
38
40
|
"""Сохранить значение с TTL."""
|
|
39
41
|
expires_at = time.time() + ttl if ttl is not None else None
|
|
40
42
|
with self._lock:
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from chutils.commands import get_commands
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def main() -> None:
|
|
10
|
+
"""Точка входа в CLI."""
|
|
11
|
+
parser = argparse.ArgumentParser(
|
|
12
|
+
prog="chutils",
|
|
13
|
+
description="""
|
|
14
|
+
Набор утилит chutils для командной строки.
|
|
15
|
+
Помогает инициализировать проекты, управлять секретами и проверять конфигурацию.
|
|
16
|
+
""",
|
|
17
|
+
epilog="""
|
|
18
|
+
Примеры использования:
|
|
19
|
+
chutils init -y
|
|
20
|
+
chutils secrets set API_KEY "value"
|
|
21
|
+
chutils validate --model myapp.config:Settings
|
|
22
|
+
chutils show-paths --json
|
|
23
|
+
""",
|
|
24
|
+
formatter_class=argparse.RawDescriptionHelpFormatter
|
|
25
|
+
)
|
|
26
|
+
subparsers = parser.add_subparsers(
|
|
27
|
+
title="Доступные команды",
|
|
28
|
+
dest="command",
|
|
29
|
+
metavar="COMMAND",
|
|
30
|
+
help="Используйте 'chutils COMMAND --help' для получения справки по конкретной команде"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# Регистрируем все доступные команды
|
|
34
|
+
for cmd_class in get_commands():
|
|
35
|
+
cmd_instance = cmd_class()
|
|
36
|
+
cmd_instance.register(subparsers)
|
|
37
|
+
|
|
38
|
+
# Если аргументы не переданы, выводим help
|
|
39
|
+
if len(sys.argv) == 1:
|
|
40
|
+
parser.print_help()
|
|
41
|
+
sys.exit(0)
|
|
42
|
+
|
|
43
|
+
args = parser.parse_args()
|
|
44
|
+
|
|
45
|
+
# Диспетчеризация выполнения
|
|
46
|
+
if hasattr(args, 'handler'):
|
|
47
|
+
from chutils.exceptions import ChutilsException, PathTraversalError
|
|
48
|
+
from chutils.cli_utils import get_console
|
|
49
|
+
from chutils.env import RICH_AVAILABLE
|
|
50
|
+
import logging
|
|
51
|
+
|
|
52
|
+
console = get_console(stderr=True)
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
args.handler(args)
|
|
56
|
+
except PathTraversalError as e:
|
|
57
|
+
# Специфичное логирование для PathTraversal
|
|
58
|
+
logger = logging.getLogger("chutils.security")
|
|
59
|
+
logger.error(
|
|
60
|
+
"Попытка Path Traversal! Исходный путь: %s, Базовый путь: %s",
|
|
61
|
+
e.context.get('attempted_path'),
|
|
62
|
+
e.context.get('base_path')
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
if RICH_AVAILABLE:
|
|
66
|
+
from rich.text import Text
|
|
67
|
+
console.print()
|
|
68
|
+
console.print(Text("ОШИБКА БЕЗОПАСНОСТИ: ", style="bold red") + Text(e.message))
|
|
69
|
+
if e.hint:
|
|
70
|
+
console.print(Text("СОВЕТ: ", style="bold yellow") + Text(e.hint))
|
|
71
|
+
else:
|
|
72
|
+
console.print(f"\nОШИБКА БЕЗОПАСНОСТИ: {e.message}", markup=False)
|
|
73
|
+
if e.hint:
|
|
74
|
+
console.print(f"СОВЕТ: {e.hint}", markup=False)
|
|
75
|
+
sys.exit(1)
|
|
76
|
+
|
|
77
|
+
except ChutilsException as e:
|
|
78
|
+
if RICH_AVAILABLE:
|
|
79
|
+
from rich.text import Text
|
|
80
|
+
from rich.panel import Panel
|
|
81
|
+
# Выводим префикс стилизованно, а сообщение как чистый текст (защита от markup)
|
|
82
|
+
console.print()
|
|
83
|
+
console.print(Text("ОШИБКА: ", style="bold red") + Text(e.message))
|
|
84
|
+
|
|
85
|
+
if e.hint:
|
|
86
|
+
# Внутри панели используем Text для защиты от markup
|
|
87
|
+
console.print(
|
|
88
|
+
Panel(Text(e.hint), title="[bold yellow]Подсказка[/bold yellow]", border_style="yellow"))
|
|
89
|
+
else:
|
|
90
|
+
console.print(f"\nОШИБКА: {e.message}", markup=False)
|
|
91
|
+
if e.hint:
|
|
92
|
+
console.print(f"СОВЕТ: {e.hint}", markup=False)
|
|
93
|
+
sys.exit(1)
|
|
94
|
+
|
|
95
|
+
except Exception as e:
|
|
96
|
+
if RICH_AVAILABLE:
|
|
97
|
+
from rich.text import Text
|
|
98
|
+
console.print()
|
|
99
|
+
console.print(Text("НЕПРЕДВИДЕННАЯ ОШИБКА: ", style="bold red") + Text(str(e)))
|
|
100
|
+
else:
|
|
101
|
+
console.print(f"\nНЕПРЕДВИДЕННАЯ ОШИБКА: {e}", markup=False)
|
|
102
|
+
sys.exit(1)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
else:
|
|
106
|
+
parser.print_help()
|
|
107
|
+
|
|
108
|
+
sys.exit(0)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
if __name__ == "__main__":
|
|
112
|
+
main()
|