chutils 2.7.4__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.
Files changed (73) hide show
  1. {chutils-2.7.4 → chutils-2.8.0}/PKG-INFO +41 -1
  2. {chutils-2.7.4 → chutils-2.8.0}/README.md +39 -0
  3. {chutils-2.7.4 → chutils-2.8.0}/pyproject.toml +29 -2
  4. {chutils-2.7.4 → chutils-2.8.0}/src/chutils/__init__.py +3 -0
  5. {chutils-2.7.4 → chutils-2.8.0}/src/chutils/__init__.pyi +93 -6
  6. chutils-2.8.0/src/chutils/__main__.py +4 -0
  7. {chutils-2.7.4 → chutils-2.8.0}/src/chutils/cache/base.py +17 -15
  8. {chutils-2.7.4 → chutils-2.8.0}/src/chutils/cache/decorator.py +8 -8
  9. {chutils-2.7.4 → chutils-2.8.0}/src/chutils/cache/in_memory.py +9 -7
  10. chutils-2.8.0/src/chutils/cli.py +112 -0
  11. {chutils-2.7.4 → chutils-2.8.0}/src/chutils/cli_booster.py +21 -18
  12. {chutils-2.7.4 → chutils-2.8.0}/src/chutils/cli_utils.py +48 -14
  13. {chutils-2.7.4 → chutils-2.8.0}/src/chutils/commands/base.py +6 -3
  14. {chutils-2.7.4 → chutils-2.8.0}/src/chutils/commands/config.py +69 -11
  15. chutils-2.8.0/src/chutils/commands/dev.py +307 -0
  16. {chutils-2.7.4 → chutils-2.8.0}/src/chutils/commands/init.py +13 -5
  17. {chutils-2.7.4 → chutils-2.8.0}/src/chutils/commands/paths.py +5 -3
  18. {chutils-2.7.4 → chutils-2.8.0}/src/chutils/commands/secrets.py +32 -21
  19. {chutils-2.7.4 → chutils-2.8.0}/src/chutils/commands/template.py +29 -12
  20. {chutils-2.7.4 → chutils-2.8.0}/src/chutils/commands/validate.py +30 -16
  21. {chutils-2.7.4 → chutils-2.8.0}/src/chutils/config/__init__.py +35 -15
  22. {chutils-2.7.4 → chutils-2.8.0}/src/chutils/config/core.py +60 -31
  23. chutils-2.8.0/src/chutils/config/dev.py +142 -0
  24. {chutils-2.7.4 → chutils-2.8.0}/src/chutils/config/diagnostics.py +13 -7
  25. {chutils-2.7.4 → chutils-2.8.0}/src/chutils/config/generator.py +21 -17
  26. {chutils-2.7.4 → chutils-2.8.0}/src/chutils/config/getters.py +56 -45
  27. {chutils-2.7.4 → chutils-2.8.0}/src/chutils/config/manager.py +72 -47
  28. {chutils-2.7.4 → chutils-2.8.0}/src/chutils/config/providers.py +75 -37
  29. {chutils-2.7.4 → chutils-2.8.0}/src/chutils/config/schema.py +28 -22
  30. {chutils-2.7.4 → chutils-2.8.0}/src/chutils/config/utils.py +111 -10
  31. {chutils-2.7.4 → chutils-2.8.0}/src/chutils/config/watcher.py +15 -10
  32. {chutils-2.7.4 → chutils-2.8.0}/src/chutils/context.py +15 -7
  33. {chutils-2.7.4 → chutils-2.8.0}/src/chutils/decorators.py +26 -20
  34. chutils-2.8.0/src/chutils/dev/__init__.py +12 -0
  35. chutils-2.8.0/src/chutils/dev/ai_lint.py +314 -0
  36. {chutils-2.7.4 → chutils-2.8.0}/src/chutils/dev/ast_indexer.py +91 -34
  37. {chutils-2.7.4 → chutils-2.8.0}/src/chutils/dev/models.py +22 -3
  38. chutils-2.8.0/src/chutils/dev/rules.py +524 -0
  39. {chutils-2.7.4 → chutils-2.8.0}/src/chutils/env.py +15 -0
  40. {chutils-2.7.4 → chutils-2.8.0}/src/chutils/exceptions.py +48 -6
  41. {chutils-2.7.4 → chutils-2.8.0}/src/chutils/features.py +22 -18
  42. {chutils-2.7.4 → chutils-2.8.0}/src/chutils/fs.py +52 -3
  43. {chutils-2.7.4 → chutils-2.8.0}/src/chutils/lifecycle.py +30 -20
  44. chutils-2.8.0/src/chutils/logger/__init__.py +57 -0
  45. chutils-2.8.0/src/chutils/logger/core.py +179 -0
  46. chutils-2.8.0/src/chutils/logger/formatters.py +78 -0
  47. {chutils-2.7.4 → chutils-2.8.0}/src/chutils/logger/handlers.py +5 -5
  48. chutils-2.8.0/src/chutils/logger/internal/builder.py +402 -0
  49. chutils-2.8.0/src/chutils/logger/internal/levels.py +58 -0
  50. chutils-2.8.0/src/chutils/logger/internal/utils.py +60 -0
  51. {chutils-2.7.4 → chutils-2.8.0}/src/chutils/logger/masking.py +25 -11
  52. {chutils-2.7.4 → chutils-2.8.0}/src/chutils/secret_manager/core.py +6 -2
  53. {chutils-2.7.4 → chutils-2.8.0}/src/chutils/secret_manager/providers.py +3 -2
  54. {chutils-2.7.4 → chutils-2.8.0}/src/chutils/testing/fixtures.py +5 -4
  55. {chutils-2.7.4 → chutils-2.8.0}/src/chutils/time.py +10 -8
  56. {chutils-2.7.4 → chutils-2.8.0}/src/chutils/tracing.py +23 -23
  57. chutils-2.8.0/src/chutils/typing.py +73 -0
  58. chutils-2.7.4/src/chutils/cli.py +0 -57
  59. chutils-2.7.4/src/chutils/commands/dev.py +0 -172
  60. chutils-2.7.4/src/chutils/dev/__init__.py +0 -3
  61. chutils-2.7.4/src/chutils/logger/__init__.py +0 -38
  62. chutils-2.7.4/src/chutils/logger/core.py +0 -526
  63. chutils-2.7.4/src/chutils/logger/formatters.py +0 -52
  64. {chutils-2.7.4 → chutils-2.8.0}/LICENSE +0 -0
  65. {chutils-2.7.4 → chutils-2.8.0}/src/chutils/cache/__init__.py +0 -0
  66. {chutils-2.7.4 → chutils-2.8.0}/src/chutils/cache/utils.py +0 -0
  67. {chutils-2.7.4 → chutils-2.8.0}/src/chutils/commands/__init__.py +0 -0
  68. {chutils-2.7.4 → chutils-2.8.0}/src/chutils/commands/utils.py +0 -0
  69. {chutils-2.7.4 → chutils-2.8.0}/src/chutils/config/GEMINI.md +0 -0
  70. {chutils-2.7.4 → chutils-2.8.0}/src/chutils/logger/GEMINI.md +0 -0
  71. {chutils-2.7.4 → chutils-2.8.0}/src/chutils/secret_manager/GEMINI.md +0 -0
  72. {chutils-2.7.4 → chutils-2.8.0}/src/chutils/secret_manager/__init__.py +0 -0
  73. {chutils-2.7.4 → 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.7.4
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.7.4"
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
- # --- config ---
15
- def get_config(model: Optional[Type[T]] = None) -> Union[Dict[str, Any], T]: ...
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(name: Optional[str] = None, level: Optional[Union[str, int]] = None,
91
- log_file: Optional[str] = None, config_section_name: Optional[str] = None,
92
- json_format: Optional[bool] = None, use_async: Optional[bool] = None) -> ChutilsLogger: ...
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', custom_locales: Optional[dict] = None) -> str: ...
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
@@ -0,0 +1,4 @@
1
+ from chutils.cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -1,8 +1,10 @@
1
1
  from abc import ABC, abstractmethod
2
- from typing import Any, Optional
2
+ from typing import Optional, TypeVar, Generic
3
3
 
4
+ T = TypeVar("T")
4
5
 
5
- class BaseCacheBackend(ABC):
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) -> Any:
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
- Any: Значение или None, если ключ не найден или просрочен.
23
+ Значение или None, если ключ не найден или просрочен.
22
24
  """
23
25
  pass
24
26
 
25
27
  @abstractmethod
26
- def set(self, key: str, value: Any, ttl: Optional[int] = None) -> None:
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 (Any): Значение для сохранения.
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) -> Any:
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: Any, ttl: Optional[int] = None) -> None:
73
+ async def aset(self, key: str, value: T, ttl: Optional[int] = None) -> None:
72
74
  """Асинхронное сохранение значения."""
73
- return self.set(key, value, ttl)
75
+ self.set(key, value, ttl)
74
76
 
75
77
  async def adelete(self, key: str) -> None:
76
78
  """Асинхронное удаление значения."""
77
- return self.delete(key)
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
- return self.clear()
87
+ self.clear()
@@ -1,12 +1,12 @@
1
1
  import asyncio
2
2
  import functools
3
- from typing import Any, Callable, Optional
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 Any, Dict, Optional, Tuple
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
- class InMemoryCacheBackend(BaseCacheBackend):
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[Any, Optional[float]]] = {}
19
+ self._cache: Dict[str, Tuple[T, Optional[float]]] = {}
18
20
  self._lock = threading.Lock()
19
21
 
20
- def get(self, key: str) -> Any:
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) -> Any:
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: Any, ttl: Optional[int] = None) -> None:
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()