sysnet-pyutils 1.7.12__tar.gz → 1.9.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 (66) hide show
  1. {sysnet_pyutils-1.7.12/sysnet_pyutils.egg-info → sysnet_pyutils-1.9.0}/PKG-INFO +81 -5
  2. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/README.md +80 -4
  3. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/pyproject.toml +1 -1
  4. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/sysnet_pyutils/__init__.py +9 -0
  5. sysnet_pyutils-1.9.0/sysnet_pyutils/request_context.py +90 -0
  6. sysnet_pyutils-1.9.0/sysnet_pyutils/utils.py +1731 -0
  7. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0/sysnet_pyutils.egg-info}/PKG-INFO +81 -5
  8. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/sysnet_pyutils.egg-info/SOURCES.txt +6 -0
  9. sysnet_pyutils-1.9.0/tests/test_config_secrets.py +282 -0
  10. sysnet_pyutils-1.9.0/tests/test_config_store.py +834 -0
  11. sysnet_pyutils-1.9.0/tests/test_context_legacy_robustness.py +175 -0
  12. sysnet_pyutils-1.9.0/tests/test_context_secrets_masking.py +210 -0
  13. sysnet_pyutils-1.9.0/tests/test_request_context.py +301 -0
  14. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/tests/test_utils_missing_branches.py +3 -3
  15. sysnet_pyutils-1.7.12/sysnet_pyutils/utils.py +0 -1036
  16. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/LICENSE +0 -0
  17. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/setup.cfg +0 -0
  18. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/sysnet_pyutils/barcode.py +0 -0
  19. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/sysnet_pyutils/constants.py +0 -0
  20. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/sysnet_pyutils/cron_utils/__init__.py +0 -0
  21. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/sysnet_pyutils/cron_utils/validation.py +0 -0
  22. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/sysnet_pyutils/data_utils.py +0 -0
  23. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/sysnet_pyutils/domino.py +0 -0
  24. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/sysnet_pyutils/geo.py +0 -0
  25. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/sysnet_pyutils/globalmodel/__init__.py +0 -0
  26. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/sysnet_pyutils/globalmodel/global_model.py +0 -0
  27. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/sysnet_pyutils/globalmodel/models.py +0 -0
  28. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/sysnet_pyutils/globalmodel/timeutils.py +0 -0
  29. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/sysnet_pyutils/ident.py +0 -0
  30. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/sysnet_pyutils/log.py +0 -0
  31. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/sysnet_pyutils/models/__init__.py +0 -0
  32. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/sysnet_pyutils/models/general.py +0 -0
  33. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/sysnet_pyutils/ses.py +0 -0
  34. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/sysnet_pyutils/syslog/__init__.py +0 -0
  35. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/sysnet_pyutils/syslog/factory.py +0 -0
  36. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/sysnet_pyutils/tools.py +0 -0
  37. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/sysnet_pyutils.egg-info/dependency_links.txt +0 -0
  38. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/sysnet_pyutils.egg-info/not-zip-safe +0 -0
  39. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/sysnet_pyutils.egg-info/requires.txt +0 -0
  40. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/sysnet_pyutils.egg-info/top_level.txt +0 -0
  41. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/tests/test_cron_utils.py +0 -0
  42. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/tests/test_cron_validation_edge.py +0 -0
  43. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/tests/test_data_utils.py +0 -0
  44. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/tests/test_data_utils_datetime.py +0 -0
  45. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/tests/test_data_utils_edge.py +0 -0
  46. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/tests/test_data_utils_missing_branches.py +0 -0
  47. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/tests/test_domino.py +0 -0
  48. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/tests/test_domino_edge.py +0 -0
  49. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/tests/test_geo.py +0 -0
  50. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/tests/test_global_model_recursive.py +0 -0
  51. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/tests/test_global_model_timezone.py +0 -0
  52. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/tests/test_ident_barcode.py +0 -0
  53. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/tests/test_log_importerror_branch.py +0 -0
  54. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/tests/test_log_mongo.py +0 -0
  55. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/tests/test_models_contact.py +0 -0
  56. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/tests/test_models_general.py +0 -0
  57. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/tests/test_models_general_edge.py +0 -0
  58. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/tests/test_package_init_version.py +0 -0
  59. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/tests/test_recursive_update.py +0 -0
  60. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/tests/test_ses.py +0 -0
  61. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/tests/test_ses_importerror_branch.py +0 -0
  62. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/tests/test_syslog_factory.py +0 -0
  63. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/tests/test_tools.py +0 -0
  64. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/tests/test_utils.py +0 -0
  65. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/tests/test_utils_edge.py +0 -0
  66. {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/tests/test_utils_log_base64.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sysnet-pyutils
3
- Version: 1.7.12
3
+ Version: 1.9.0
4
4
  Summary: SYSNET Python Utilities - kompletní sada nástrojů pro Python aplikace
5
5
  Author-email: Data Developer <info@sysnet.cz>
6
6
  Maintainer-email: SYSNET Development Team <info@sysnet.cz>
@@ -729,7 +729,7 @@ Knihovna obsahuje základní, v aplikacích hojně používané, utility pro Pyt
729
729
 
730
730
  [![Python](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
731
731
  [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
732
- [![Verze](https://img.shields.io/badge/version-1.7.12-blue.svg)](https://pypi.org/project/sysnet-pyutils/1.7.12/)
732
+ [![Verze](https://img.shields.io/badge/version-1.9.0-blue.svg)](https://pypi.org/project/sysnet-pyutils/1.9.0/)
733
733
  [![Tests](https://github.com/SYSNET-CZ/pyutils/actions/workflows/ci.yml/badge.svg)](https://github.com/SYSNET-CZ/pyutils/actions/workflows/ci.yml)
734
734
  [![Coverage](https://img.shields.io/badge/coverage-90%25-brightgreen.svg)](tests/)
735
735
 
@@ -866,6 +866,26 @@ if config.loaded:
866
866
  db_url = config.config['database']
867
867
  ```
868
868
 
869
+ Chování `Config`:
870
+
871
+ - **Soubor:** ukládá se atomicky (dočasný soubor + přejmenování), čitelně (diakritika bez `\u` escape) a v pořadí klíčů z paměti. Nový soubor má práva 600 a chybějící adresáře
872
+ se vytvoří s právy 700; práva existujícího souboru se nemění a volnější než 600 se při načtení zalogují jako varování.
873
+ - **Výchozí cesta:** nezadáte-li `config_path`, použije se proměnná prostředí `PYUTILS_CONFIG_PATH`. Bez ní platí dosavadní
874
+ umístění `conf/config.yml` vedle modulu (s varováním); není-li zapisovatelné, vyhodí se `ConfigError` s radou.
875
+ - **Chyby:** poškozený YAML vyhodí `ConfigParseError` (podtřída `ConfigError` i `yaml.YAMLError`) s cestou, popisem a
876
+ řádkem, nikdy s obsahem souboru; soubor zůstane nepřepsaný.
877
+ - **Nové výchozí hodnoty:** s existujícím souborem se `config_dict` ignoruje. Chcete-li po upgradu aplikace doplnit chybějící
878
+ klíče, použijte `Config(config_path=..., config_dict=..., merge_defaults=True)`: doplní se jen to, co v souboru chybí
879
+ (vnořené slovníky do hloubky, seznamy ne) a hodnoty ze souboru se nikdy nepřepíšou.
880
+ - **Secrets:** hodnotu v YAML lze zadat odkazem `${env:NAZEV}` nebo `${file:/absolutni/cesta}` (i uvnitř textu, např.
881
+ `mongodb://u:${env:MONGO_PASSWORD}@h`); rozliší se při načtení na `Secret` (podtřída `str`, `repr` a log maskované,
882
+ `reveal()` vrací čistý `str`). Chybějící zdroj je `ConfigError`, nikdy tichý prázdný řetězec. `store()` zapisuje
883
+ zpět odkaz, nikdy hodnotu. `config.redacted()` vrací kopii pro health a debug, `config.audit()` cesty k heslům
884
+ v otevřené podobě. Doslovné `${` zapište jako `$${`. Viz `docs/adr/0001-config-secrets.md`.
885
+ - **Vstup:** `config_dict` se při inicializaci zkopíruje; pozdější změny `config` neovlivní slovník volajícího a naopak.
886
+ - **Opakované volání:** `Config(...)` vrací stále tutéž instanci; odlišná cesta nebo `config_dict` se ignoruje a zaloguje
887
+ se varování.
888
+
869
889
  #### Context a autentizace
870
890
 
871
891
  ```python
@@ -881,6 +901,44 @@ if ctx.check_api_key('some_key'):
881
901
  print(f"Přihlášen jako: {ctx.user_name}")
882
902
  ```
883
903
 
904
+ > **`Context` je singleton se sdíleným stavem.** `check_api_key()` ukládá identitu do jediné instance, kterou sdílejí
905
+ > všechny souběžné požadavky. Ve webové službě (FastAPI) může jeden požadavek po `await` vidět identitu jiného.
906
+ > Pokus se dvěma souběžnými požadavky to potvrdil: `Context` vrátil první osobě jméno druhé.
907
+ > **Ověřování API klíčů patří knihovně `sysnet-apikeys`** (standard CML `api-keys.md`); `check_api_key()` a
908
+ > **Ověřování API klíčů patří knihovně `sysnet-apikeys`** (standard CML `api-keys.md`); `check_api_key()`,
909
+ > `check_user()` a `api_keys_init()` jsou zastaralé (`DeprecationWarning`) a zůstávají jen kvůli stávajícím službám.
910
+ > Tajemství (`config`, `api_key`, `cert_pass`) se čtou jako dřív, ale nejsou ve `vars()`, v `repr` ani v `pickle`/`copy`;
911
+ > pro diagnostiku slouží `ctx.safe_dict()`.
912
+
913
+ ##### Per-request kontext (`RequestContext`)
914
+
915
+ `RequestContext` jen **nese** identitu ověřenou jinde, v `ContextVar`, takže ji každý požadavek vidí jen svou.
916
+ Ověřuje `sysnet-apikeys`; služba převede jeho `Principal` na kontext:
917
+
918
+ ```python
919
+ from fastapi import Depends
920
+ from sysnet_apikeys.fastapi import ApiKeyAuth
921
+ from sysnet_pyutils import RequestContext, current_request_context, set_request_context
922
+
923
+ auth = ApiKeyAuth(service) # sysnet-apikeys
924
+
925
+
926
+ async def authenticated(principal=Depends(auth)) -> RequestContext:
927
+ ctx = RequestContext(user_name=principal.client, agenda=principal.agenda)
928
+ set_request_context(ctx)
929
+ return ctx
930
+
931
+
932
+ # kdekoli později ve stejném požadavku:
933
+ agenda = current_request_context().agenda
934
+ ```
935
+
936
+ - `RequestContext` je neměnný (`frozen`); `api_key` a `cert_pass` jsou typu `Secret` (repr a log skrývají hodnotu).
937
+ - `authenticated` je `True`, nese-li kontext identitu (`user_name` nebo `api_key`); samotná `agenda` ji nedělá.
938
+ - Kontext nastavujte v **`async def`** závislosti. `ContextVar` nastavený v synchronní závislosti (`def`, běží ve vlákně)
939
+ se do endpointu nepřenese; ověřeno pokusem s FastAPI 0.150. Případně použijte hodnotu vrácenou z `Depends`.
940
+ - Pro dočasné nastavení v bloku: `with use_request_context(ctx): ...` (vždy vrátí předchozí hodnotu).
941
+
884
942
  #### Identifikátory a validace
885
943
 
886
944
  ```python
@@ -1355,12 +1413,12 @@ token nepatří do zdrojového kódu.
1355
1413
 
1356
1414
  #### Vydání nové verze
1357
1415
 
1358
- 1. Změňte `version` v `pyproject.toml` (například na `1.7.12`) a aktualizujte dokumentaci včetně changelogu a badge.
1416
+ 1. Změňte `version` v `pyproject.toml` (například na `1.8.0`) a aktualizujte dokumentaci včetně changelogu a badge.
1359
1417
  2. Commitněte změny a dostaňte je na `main`; před vytvořením tagu ověřte úspěšné CI a správný release commit.
1360
1418
  3. Na tomto commitu vytvořte a odešlete odpovídající tag:
1361
1419
 
1362
1420
  ```bash
1363
- git tag v1.7.12 && git push origin v1.7.12
1421
+ git tag v1.8.0 && git push origin v1.8.0
1364
1422
  ```
1365
1423
 
1366
1424
  4. GitHub Actions automaticky provedou kontroly, build a publikaci.
@@ -1389,6 +1447,24 @@ při běžném vydání již lokálně nic nenahrávejte na PyPI.
1389
1447
 
1390
1448
  Podrobnosti nových změn jsou v [CHANGELOG.md](CHANGELOG.md).
1391
1449
 
1450
+ ### Verze 1.9.0 (2026-10-28)
1451
+
1452
+ - 🧭 `RequestContext`: nosič identity jednoho požadavku v `ContextVar` (`set_request_context`, `current_request_context`, `use_request_context`); souběžné požadavky se nevidí. Ověřování API klíčů záměrně neobsahuje, patří knihovně `sysnet-apikeys`.
1453
+ - ⚠️ `Context.check_api_key()`, `Context.check_user()` a `api_keys_init()` jsou zastaralé (`DeprecationWarning` jednou za proces); chování pro platné konfigurace se nemění.
1454
+ - 🛡️ `Context`: vadná konfigurace nebo klíč jiného typu než neprázdný řetězec vrací `False` místo výjimky.
1455
+ - 🔐 `Context`: tajemství (`config`, `api_key`, `cert_pass`) nejsou ve `vars()`, v `repr` ani v `pickle`/`copy`; čtení atributů beze změny, nová metoda `safe_dict()`.
1456
+
1457
+ Podrobnosti a dopady na stávající aplikace jsou v [CHANGELOG.md](CHANGELOG.md).
1458
+
1459
+ ### Verze 1.8.0 (2026-10-27)
1460
+
1461
+ - 🔐 `Config`: ochrana secrets. Hodnotu v YAML lze zadat odkazem `${env:NAZEV}` nebo `${file:/absolutni/cesta}`; rozlišuje se při načtení na nový typ `Secret` (maskovaný `repr`, `str` a log), `store()` zapisuje zpět odkaz, nikdy hodnotu. Nově `Config.redacted()` a `Config.audit()`. Viz `docs/adr/0001-config-secrets.md`.
1462
+ - 💾 `Config.store()` a `replace()` zapisují atomicky (dočasný soubor + přejmenování); nový soubor má práva 600, chybějící adresáře se vytvoří s právy 700.
1463
+ - ⚠️ `Config` bez `config_dict` má `config == {}` místo `None` a nezapisuje `null`; `config_dict` se při inicializaci kopíruje; YAML se ukládá čitelně (diakritika) a v pořadí klíčů z paměti. První `store()` po upgradu přepíše soubor v novém formátu.
1464
+ - ✅ `ConfigParseError` pro poškozený YAML (i `yaml.YAMLError`), výchozí cesta z `PYUTILS_CONFIG_PATH`, opt-in `merge_defaults=True`, varování při ignorovaných parametrech opakovaného `Config(...)`.
1465
+
1466
+ Podrobnosti a dopady na stávající aplikace jsou v [CHANGELOG.md](CHANGELOG.md).
1467
+
1392
1468
  ### Verze 1.7.12 (2026-10-27)
1393
1469
 
1394
1470
  - ✅ GitHub Actions CI/CD: kontrola syntaxe, pytest a ruff při push/PR na `main`; ověření verze, testy, build a publikace na PyPI při tagu `v*`.
@@ -1526,6 +1602,6 @@ Tento projekt je licencován pod MIT licencí — viz [LICENSE](LICENSE) soubor.
1526
1602
 
1527
1603
  **Vytvořeno s ❤️ týmem SYSNET**
1528
1604
 
1529
- *Verze: 1.7.12*
1605
+ *Verze: 1.9.0*
1530
1606
 
1531
1607
  *Datum: 2026-10-27*
@@ -6,7 +6,7 @@ Knihovna obsahuje základní, v aplikacích hojně používané, utility pro Pyt
6
6
 
7
7
  [![Python](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
8
8
  [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
9
- [![Verze](https://img.shields.io/badge/version-1.7.12-blue.svg)](https://pypi.org/project/sysnet-pyutils/1.7.12/)
9
+ [![Verze](https://img.shields.io/badge/version-1.9.0-blue.svg)](https://pypi.org/project/sysnet-pyutils/1.9.0/)
10
10
  [![Tests](https://github.com/SYSNET-CZ/pyutils/actions/workflows/ci.yml/badge.svg)](https://github.com/SYSNET-CZ/pyutils/actions/workflows/ci.yml)
11
11
  [![Coverage](https://img.shields.io/badge/coverage-90%25-brightgreen.svg)](tests/)
12
12
 
@@ -143,6 +143,26 @@ if config.loaded:
143
143
  db_url = config.config['database']
144
144
  ```
145
145
 
146
+ Chování `Config`:
147
+
148
+ - **Soubor:** ukládá se atomicky (dočasný soubor + přejmenování), čitelně (diakritika bez `\u` escape) a v pořadí klíčů z paměti. Nový soubor má práva 600 a chybějící adresáře
149
+ se vytvoří s právy 700; práva existujícího souboru se nemění a volnější než 600 se při načtení zalogují jako varování.
150
+ - **Výchozí cesta:** nezadáte-li `config_path`, použije se proměnná prostředí `PYUTILS_CONFIG_PATH`. Bez ní platí dosavadní
151
+ umístění `conf/config.yml` vedle modulu (s varováním); není-li zapisovatelné, vyhodí se `ConfigError` s radou.
152
+ - **Chyby:** poškozený YAML vyhodí `ConfigParseError` (podtřída `ConfigError` i `yaml.YAMLError`) s cestou, popisem a
153
+ řádkem, nikdy s obsahem souboru; soubor zůstane nepřepsaný.
154
+ - **Nové výchozí hodnoty:** s existujícím souborem se `config_dict` ignoruje. Chcete-li po upgradu aplikace doplnit chybějící
155
+ klíče, použijte `Config(config_path=..., config_dict=..., merge_defaults=True)`: doplní se jen to, co v souboru chybí
156
+ (vnořené slovníky do hloubky, seznamy ne) a hodnoty ze souboru se nikdy nepřepíšou.
157
+ - **Secrets:** hodnotu v YAML lze zadat odkazem `${env:NAZEV}` nebo `${file:/absolutni/cesta}` (i uvnitř textu, např.
158
+ `mongodb://u:${env:MONGO_PASSWORD}@h`); rozliší se při načtení na `Secret` (podtřída `str`, `repr` a log maskované,
159
+ `reveal()` vrací čistý `str`). Chybějící zdroj je `ConfigError`, nikdy tichý prázdný řetězec. `store()` zapisuje
160
+ zpět odkaz, nikdy hodnotu. `config.redacted()` vrací kopii pro health a debug, `config.audit()` cesty k heslům
161
+ v otevřené podobě. Doslovné `${` zapište jako `$${`. Viz `docs/adr/0001-config-secrets.md`.
162
+ - **Vstup:** `config_dict` se při inicializaci zkopíruje; pozdější změny `config` neovlivní slovník volajícího a naopak.
163
+ - **Opakované volání:** `Config(...)` vrací stále tutéž instanci; odlišná cesta nebo `config_dict` se ignoruje a zaloguje
164
+ se varování.
165
+
146
166
  #### Context a autentizace
147
167
 
148
168
  ```python
@@ -158,6 +178,44 @@ if ctx.check_api_key('some_key'):
158
178
  print(f"Přihlášen jako: {ctx.user_name}")
159
179
  ```
160
180
 
181
+ > **`Context` je singleton se sdíleným stavem.** `check_api_key()` ukládá identitu do jediné instance, kterou sdílejí
182
+ > všechny souběžné požadavky. Ve webové službě (FastAPI) může jeden požadavek po `await` vidět identitu jiného.
183
+ > Pokus se dvěma souběžnými požadavky to potvrdil: `Context` vrátil první osobě jméno druhé.
184
+ > **Ověřování API klíčů patří knihovně `sysnet-apikeys`** (standard CML `api-keys.md`); `check_api_key()` a
185
+ > **Ověřování API klíčů patří knihovně `sysnet-apikeys`** (standard CML `api-keys.md`); `check_api_key()`,
186
+ > `check_user()` a `api_keys_init()` jsou zastaralé (`DeprecationWarning`) a zůstávají jen kvůli stávajícím službám.
187
+ > Tajemství (`config`, `api_key`, `cert_pass`) se čtou jako dřív, ale nejsou ve `vars()`, v `repr` ani v `pickle`/`copy`;
188
+ > pro diagnostiku slouží `ctx.safe_dict()`.
189
+
190
+ ##### Per-request kontext (`RequestContext`)
191
+
192
+ `RequestContext` jen **nese** identitu ověřenou jinde, v `ContextVar`, takže ji každý požadavek vidí jen svou.
193
+ Ověřuje `sysnet-apikeys`; služba převede jeho `Principal` na kontext:
194
+
195
+ ```python
196
+ from fastapi import Depends
197
+ from sysnet_apikeys.fastapi import ApiKeyAuth
198
+ from sysnet_pyutils import RequestContext, current_request_context, set_request_context
199
+
200
+ auth = ApiKeyAuth(service) # sysnet-apikeys
201
+
202
+
203
+ async def authenticated(principal=Depends(auth)) -> RequestContext:
204
+ ctx = RequestContext(user_name=principal.client, agenda=principal.agenda)
205
+ set_request_context(ctx)
206
+ return ctx
207
+
208
+
209
+ # kdekoli později ve stejném požadavku:
210
+ agenda = current_request_context().agenda
211
+ ```
212
+
213
+ - `RequestContext` je neměnný (`frozen`); `api_key` a `cert_pass` jsou typu `Secret` (repr a log skrývají hodnotu).
214
+ - `authenticated` je `True`, nese-li kontext identitu (`user_name` nebo `api_key`); samotná `agenda` ji nedělá.
215
+ - Kontext nastavujte v **`async def`** závislosti. `ContextVar` nastavený v synchronní závislosti (`def`, běží ve vlákně)
216
+ se do endpointu nepřenese; ověřeno pokusem s FastAPI 0.150. Případně použijte hodnotu vrácenou z `Depends`.
217
+ - Pro dočasné nastavení v bloku: `with use_request_context(ctx): ...` (vždy vrátí předchozí hodnotu).
218
+
161
219
  #### Identifikátory a validace
162
220
 
163
221
  ```python
@@ -632,12 +690,12 @@ token nepatří do zdrojového kódu.
632
690
 
633
691
  #### Vydání nové verze
634
692
 
635
- 1. Změňte `version` v `pyproject.toml` (například na `1.7.12`) a aktualizujte dokumentaci včetně changelogu a badge.
693
+ 1. Změňte `version` v `pyproject.toml` (například na `1.8.0`) a aktualizujte dokumentaci včetně changelogu a badge.
636
694
  2. Commitněte změny a dostaňte je na `main`; před vytvořením tagu ověřte úspěšné CI a správný release commit.
637
695
  3. Na tomto commitu vytvořte a odešlete odpovídající tag:
638
696
 
639
697
  ```bash
640
- git tag v1.7.12 && git push origin v1.7.12
698
+ git tag v1.8.0 && git push origin v1.8.0
641
699
  ```
642
700
 
643
701
  4. GitHub Actions automaticky provedou kontroly, build a publikaci.
@@ -666,6 +724,24 @@ při běžném vydání již lokálně nic nenahrávejte na PyPI.
666
724
 
667
725
  Podrobnosti nových změn jsou v [CHANGELOG.md](CHANGELOG.md).
668
726
 
727
+ ### Verze 1.9.0 (2026-10-28)
728
+
729
+ - 🧭 `RequestContext`: nosič identity jednoho požadavku v `ContextVar` (`set_request_context`, `current_request_context`, `use_request_context`); souběžné požadavky se nevidí. Ověřování API klíčů záměrně neobsahuje, patří knihovně `sysnet-apikeys`.
730
+ - ⚠️ `Context.check_api_key()`, `Context.check_user()` a `api_keys_init()` jsou zastaralé (`DeprecationWarning` jednou za proces); chování pro platné konfigurace se nemění.
731
+ - 🛡️ `Context`: vadná konfigurace nebo klíč jiného typu než neprázdný řetězec vrací `False` místo výjimky.
732
+ - 🔐 `Context`: tajemství (`config`, `api_key`, `cert_pass`) nejsou ve `vars()`, v `repr` ani v `pickle`/`copy`; čtení atributů beze změny, nová metoda `safe_dict()`.
733
+
734
+ Podrobnosti a dopady na stávající aplikace jsou v [CHANGELOG.md](CHANGELOG.md).
735
+
736
+ ### Verze 1.8.0 (2026-10-27)
737
+
738
+ - 🔐 `Config`: ochrana secrets. Hodnotu v YAML lze zadat odkazem `${env:NAZEV}` nebo `${file:/absolutni/cesta}`; rozlišuje se při načtení na nový typ `Secret` (maskovaný `repr`, `str` a log), `store()` zapisuje zpět odkaz, nikdy hodnotu. Nově `Config.redacted()` a `Config.audit()`. Viz `docs/adr/0001-config-secrets.md`.
739
+ - 💾 `Config.store()` a `replace()` zapisují atomicky (dočasný soubor + přejmenování); nový soubor má práva 600, chybějící adresáře se vytvoří s právy 700.
740
+ - ⚠️ `Config` bez `config_dict` má `config == {}` místo `None` a nezapisuje `null`; `config_dict` se při inicializaci kopíruje; YAML se ukládá čitelně (diakritika) a v pořadí klíčů z paměti. První `store()` po upgradu přepíše soubor v novém formátu.
741
+ - ✅ `ConfigParseError` pro poškozený YAML (i `yaml.YAMLError`), výchozí cesta z `PYUTILS_CONFIG_PATH`, opt-in `merge_defaults=True`, varování při ignorovaných parametrech opakovaného `Config(...)`.
742
+
743
+ Podrobnosti a dopady na stávající aplikace jsou v [CHANGELOG.md](CHANGELOG.md).
744
+
669
745
  ### Verze 1.7.12 (2026-10-27)
670
746
 
671
747
  - ✅ GitHub Actions CI/CD: kontrola syntaxe, pytest a ruff při push/PR na `main`; ověření verze, testy, build a publikace na PyPI při tagu `v*`.
@@ -803,6 +879,6 @@ Tento projekt je licencován pod MIT licencí — viz [LICENSE](LICENSE) soubor.
803
879
 
804
880
  **Vytvořeno s ❤️ týmem SYSNET**
805
881
 
806
- *Verze: 1.7.12*
882
+ *Verze: 1.9.0*
807
883
 
808
884
  *Datum: 2026-10-27*
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sysnet-pyutils"
7
- version = "1.7.12"
7
+ version = "1.9.0"
8
8
  description = "SYSNET Python Utilities - kompletní sada nástrojů pro Python aplikace"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -116,6 +116,13 @@ from sysnet_pyutils.models.general import (
116
116
  WorkflowType,
117
117
  calculate_status,
118
118
  )
119
+ from sysnet_pyutils.request_context import (
120
+ RequestContext,
121
+ current_request_context,
122
+ reset_request_context,
123
+ set_request_context,
124
+ use_request_context,
125
+ )
119
126
 
120
127
  # Syslog
121
128
  from sysnet_pyutils.syslog import SyslogFactory
@@ -123,9 +130,11 @@ from sysnet_pyutils.utils import ( # API klíče; Konverze; Hashe; Base64; Vali
123
130
  Config,
124
131
  ConfigError,
125
132
  ConfigFlag,
133
+ ConfigParseError,
126
134
  Context,
127
135
  Log,
128
136
  LoggedObject,
137
+ Secret,
129
138
  Singleton,
130
139
  api_key_generate,
131
140
  api_key_next,
@@ -0,0 +1,90 @@
1
+ """Nosič kontextu jednoho požadavku (ASGI-safe doplněk ke třídě ``Context``).
2
+
3
+ ``sysnet_pyutils.Context`` je singleton: drží stav posledního ověření a sdílí ho všechny souběžné požadavky.
4
+ Pokud si dva požadavky vymění stav, jeden z nich uvidí identitu druhého. Tento modul nabízí jen **nosič**
5
+ identity bez sdíleného stavu:
6
+
7
+ * neměnný :class:`RequestContext`,
8
+ * ``set_request_context`` / ``current_request_context`` / ``reset_request_context`` / ``use_request_context``
9
+ ukládají kontext do :class:`contextvars.ContextVar`, takže každá asynchronní úloha (požadavek) vidí jen svůj.
10
+
11
+ **Modul neověřuje API klíče.** Ověřování, hashování a správa klíčů patří výhradně knihovně ``sysnet-apikeys``
12
+ (standard CML ``constitution/standards/api-keys.md``, pravidlo 1). Identitu, kterou ``sysnet-apikeys`` vrátí
13
+ (``Principal`` s ``client``, ``agenda`` a ``scopes``), si služba převede na :class:`RequestContext` sama::
14
+
15
+ async def authenticated(principal: Principal = Depends(auth)) -> RequestContext:
16
+ ctx = RequestContext(user_name=principal.client, agenda=principal.agenda)
17
+ set_request_context(ctx)
18
+ return ctx
19
+
20
+ ``ContextVar`` nastavený v **synchronní** závislosti (``def``, běží ve vlákně) se do endpointu nepřenese. Nastavujte
21
+ ho v ``async def`` závislosti, nebo použijte vrácenou hodnotu (``Depends``) přímo.
22
+
23
+ Starý singleton ``Context`` zůstává beze změny kvůli stávajícím konzumentům.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import contextlib
29
+ from collections.abc import Iterator
30
+ from contextvars import ContextVar, Token
31
+ from dataclasses import dataclass
32
+
33
+ from sysnet_pyutils.utils import Secret
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class RequestContext:
38
+ """Neměnný kontext jednoho požadavku.
39
+
40
+ ``frozen`` brání tomu, aby se změnou sdílené prázdné instance změnil stav všech úloh, které kontext ještě
41
+ nenastavily. ``api_key`` a ``cert_pass`` jsou typu :class:`Secret`, takže se neobjeví v ``repr``, ``str`` ani
42
+ v logu; čistou hodnotu vrací ``.reveal()``.
43
+ """
44
+
45
+ api_key: Secret | None = None
46
+ agenda: str | None = None
47
+ user_name: str | None = None
48
+ cert_file: str | None = None
49
+ cert_pass: Secret | None = None
50
+
51
+ @property
52
+ def authenticated(self) -> bool:
53
+ """``True``, pokud kontext nese identitu volajícího (``user_name`` nebo ``api_key``).
54
+
55
+ ``sysnet-apikeys`` otevřený klíč nevrací (ukládá se jen hash), proto ``api_key`` bývá prázdný a identitu
56
+ nese ``user_name`` (klient z ``Principal``). Samotná ``agenda`` identitou není.
57
+ """
58
+ return self.api_key is not None or self.user_name is not None
59
+
60
+
61
+ _EMPTY: RequestContext = RequestContext()
62
+ _request_ctx: ContextVar[RequestContext] = ContextVar("sysnet_pyutils_request_context", default=_EMPTY)
63
+
64
+
65
+ def current_request_context() -> RequestContext:
66
+ """Kontext aktuální úlohy; pokud nebyl nastaven, vrací prázdný (neověřený)."""
67
+ return _request_ctx.get()
68
+
69
+
70
+ def set_request_context(ctx: RequestContext) -> Token[RequestContext]:
71
+ """Nastaví kontext pro aktuální úlohu. Vrací token pro :func:`reset_request_context`."""
72
+ return _request_ctx.set(ctx)
73
+
74
+
75
+ def reset_request_context(token: Token[RequestContext] | None = None) -> None:
76
+ """Vrátí předchozí hodnotu (podle ``token``), nebo bez tokenu nastaví prázdný kontext."""
77
+ if token is not None:
78
+ _request_ctx.reset(token)
79
+ else:
80
+ _request_ctx.set(_EMPTY)
81
+
82
+
83
+ @contextlib.contextmanager
84
+ def use_request_context(ctx: RequestContext) -> Iterator[RequestContext]:
85
+ """Nastaví kontext po dobu bloku a vždy ho vrátí zpět (i při výjimce)."""
86
+ token = set_request_context(ctx)
87
+ try:
88
+ yield ctx
89
+ finally:
90
+ reset_request_context(token)