sysnet-pyutils 1.8.0__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 (65) hide show
  1. {sysnet_pyutils-1.8.0/sysnet_pyutils.egg-info → sysnet_pyutils-1.9.0}/PKG-INFO +50 -3
  2. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/README.md +49 -2
  3. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/pyproject.toml +1 -1
  4. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/sysnet_pyutils/__init__.py +7 -0
  5. sysnet_pyutils-1.9.0/sysnet_pyutils/request_context.py +90 -0
  6. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/sysnet_pyutils/utils.py +175 -31
  7. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0/sysnet_pyutils.egg-info}/PKG-INFO +50 -3
  8. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/sysnet_pyutils.egg-info/SOURCES.txt +4 -0
  9. sysnet_pyutils-1.9.0/tests/test_context_legacy_robustness.py +175 -0
  10. sysnet_pyutils-1.9.0/tests/test_context_secrets_masking.py +210 -0
  11. sysnet_pyutils-1.9.0/tests/test_request_context.py +301 -0
  12. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/LICENSE +0 -0
  13. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/setup.cfg +0 -0
  14. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/sysnet_pyutils/barcode.py +0 -0
  15. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/sysnet_pyutils/constants.py +0 -0
  16. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/sysnet_pyutils/cron_utils/__init__.py +0 -0
  17. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/sysnet_pyutils/cron_utils/validation.py +0 -0
  18. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/sysnet_pyutils/data_utils.py +0 -0
  19. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/sysnet_pyutils/domino.py +0 -0
  20. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/sysnet_pyutils/geo.py +0 -0
  21. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/sysnet_pyutils/globalmodel/__init__.py +0 -0
  22. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/sysnet_pyutils/globalmodel/global_model.py +0 -0
  23. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/sysnet_pyutils/globalmodel/models.py +0 -0
  24. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/sysnet_pyutils/globalmodel/timeutils.py +0 -0
  25. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/sysnet_pyutils/ident.py +0 -0
  26. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/sysnet_pyutils/log.py +0 -0
  27. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/sysnet_pyutils/models/__init__.py +0 -0
  28. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/sysnet_pyutils/models/general.py +0 -0
  29. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/sysnet_pyutils/ses.py +0 -0
  30. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/sysnet_pyutils/syslog/__init__.py +0 -0
  31. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/sysnet_pyutils/syslog/factory.py +0 -0
  32. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/sysnet_pyutils/tools.py +0 -0
  33. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/sysnet_pyutils.egg-info/dependency_links.txt +0 -0
  34. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/sysnet_pyutils.egg-info/not-zip-safe +0 -0
  35. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/sysnet_pyutils.egg-info/requires.txt +0 -0
  36. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/sysnet_pyutils.egg-info/top_level.txt +0 -0
  37. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/tests/test_config_secrets.py +0 -0
  38. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/tests/test_config_store.py +0 -0
  39. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/tests/test_cron_utils.py +0 -0
  40. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/tests/test_cron_validation_edge.py +0 -0
  41. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/tests/test_data_utils.py +0 -0
  42. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/tests/test_data_utils_datetime.py +0 -0
  43. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/tests/test_data_utils_edge.py +0 -0
  44. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/tests/test_data_utils_missing_branches.py +0 -0
  45. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/tests/test_domino.py +0 -0
  46. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/tests/test_domino_edge.py +0 -0
  47. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/tests/test_geo.py +0 -0
  48. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/tests/test_global_model_recursive.py +0 -0
  49. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/tests/test_global_model_timezone.py +0 -0
  50. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/tests/test_ident_barcode.py +0 -0
  51. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/tests/test_log_importerror_branch.py +0 -0
  52. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/tests/test_log_mongo.py +0 -0
  53. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/tests/test_models_contact.py +0 -0
  54. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/tests/test_models_general.py +0 -0
  55. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/tests/test_models_general_edge.py +0 -0
  56. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/tests/test_package_init_version.py +0 -0
  57. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/tests/test_recursive_update.py +0 -0
  58. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/tests/test_ses.py +0 -0
  59. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/tests/test_ses_importerror_branch.py +0 -0
  60. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/tests/test_syslog_factory.py +0 -0
  61. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/tests/test_tools.py +0 -0
  62. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/tests/test_utils.py +0 -0
  63. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/tests/test_utils_edge.py +0 -0
  64. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/tests/test_utils_log_base64.py +0 -0
  65. {sysnet_pyutils-1.8.0 → sysnet_pyutils-1.9.0}/tests/test_utils_missing_branches.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sysnet-pyutils
3
- Version: 1.8.0
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.8.0-blue.svg)](https://pypi.org/project/sysnet-pyutils/1.8.0/)
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
 
@@ -901,6 +901,44 @@ if ctx.check_api_key('some_key'):
901
901
  print(f"Přihlášen jako: {ctx.user_name}")
902
902
  ```
903
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
+
904
942
  #### Identifikátory a validace
905
943
 
906
944
  ```python
@@ -1409,6 +1447,15 @@ při běžném vydání již lokálně nic nenahrávejte na PyPI.
1409
1447
 
1410
1448
  Podrobnosti nových změn jsou v [CHANGELOG.md](CHANGELOG.md).
1411
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
+
1412
1459
  ### Verze 1.8.0 (2026-10-27)
1413
1460
 
1414
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`.
@@ -1555,6 +1602,6 @@ Tento projekt je licencován pod MIT licencí — viz [LICENSE](LICENSE) soubor.
1555
1602
 
1556
1603
  **Vytvořeno s ❤️ týmem SYSNET**
1557
1604
 
1558
- *Verze: 1.8.0*
1605
+ *Verze: 1.9.0*
1559
1606
 
1560
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.8.0-blue.svg)](https://pypi.org/project/sysnet-pyutils/1.8.0/)
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
 
@@ -178,6 +178,44 @@ if ctx.check_api_key('some_key'):
178
178
  print(f"Přihlášen jako: {ctx.user_name}")
179
179
  ```
180
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
+
181
219
  #### Identifikátory a validace
182
220
 
183
221
  ```python
@@ -686,6 +724,15 @@ při běžném vydání již lokálně nic nenahrávejte na PyPI.
686
724
 
687
725
  Podrobnosti nových změn jsou v [CHANGELOG.md](CHANGELOG.md).
688
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
+
689
736
  ### Verze 1.8.0 (2026-10-27)
690
737
 
691
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`.
@@ -832,6 +879,6 @@ Tento projekt je licencován pod MIT licencí — viz [LICENSE](LICENSE) soubor.
832
879
 
833
880
  **Vytvořeno s ❤️ týmem SYSNET**
834
881
 
835
- *Verze: 1.8.0*
882
+ *Verze: 1.9.0*
836
883
 
837
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.8.0"
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
@@ -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)
@@ -10,6 +10,7 @@ import secrets
10
10
  import stat
11
11
  import sys
12
12
  import traceback
13
+ import warnings
13
14
  from datetime import date, datetime, timedelta
14
15
  from typing import Any
15
16
  from urllib.parse import quote
@@ -43,6 +44,25 @@ class ConfigParseError(ConfigError, yaml.YAMLError):
43
44
  """
44
45
 
45
46
 
47
+ _DEPRECATION_WARNED: set[str] = set()
48
+
49
+
50
+ def _warn_deprecated_once(name: str, stacklevel: int = 3) -> None:
51
+ """Jednou za proces upozorní, že ověřování příchozích API klíčů patří knihovně ``sysnet-apikeys``.
52
+
53
+ Zpráva neobsahuje žádné hodnoty z konfigurace. ``stacklevel`` míří na volajícího zastaralé funkce.
54
+ """
55
+ if name in _DEPRECATION_WARNED:
56
+ return
57
+ _DEPRECATION_WARNED.add(name)
58
+ warnings.warn(
59
+ f"{name} je zastaralé: správu a ověřování API klíčů zajišťuje knihovna sysnet-apikeys "
60
+ "(standard CML constitution/standards/api-keys.md). Chování se nemění, odstranění je plánováno až v major vydání.",
61
+ DeprecationWarning,
62
+ stacklevel=stacklevel,
63
+ )
64
+
65
+
46
66
  class Singleton(type):
47
67
  """
48
68
  Třída Singleton
@@ -731,10 +751,15 @@ def api_keys_init(agenda: str = "main", amount: int = 4) -> list[dict]:
731
751
  """
732
752
  Vygeneruje klíče pro API
733
753
 
754
+ .. deprecated:: 1.9.0
755
+ Příchozí API klíče spravuje ``sysnet-apikeys`` (standard CML ``api-keys.md``); klíče v otevřené podobě
756
+ v YAML se nemají ukládat. Funkce zůstává kvůli stávajícím službám.
757
+
734
758
  :param agenda: Název agendy, pro kterou se klíče generují
735
759
  :param amount: Počet vygenerovaných klíčů
736
760
  :return: seznam vygenerovaných klíčů
737
761
  """
762
+ _warn_deprecated_once("api_keys_init")
738
763
  out = []
739
764
  for i in range(amount):
740
765
  out.append(api_key_next(f"{agenda} {i + 1}"))
@@ -1505,9 +1530,41 @@ def parse_ldap_name(ldap_name: str) -> tuple[str | None, str | None]:
1505
1530
  return cn, ldap_name
1506
1531
 
1507
1532
 
1533
+ class _SecretVault:
1534
+ """Schránka na tajemství třídy ``Context`` (``config``, ``api_key``, ``cert_pass``).
1535
+
1536
+ Tajemství nejsou ve ``vars()`` ani v ``__dict__`` objektu; ve slovníku je jen tato schránka, která se
1537
+ vypisuje maskovaně a při ``pickle`` i ``copy`` se přenese prázdná.
1538
+ """
1539
+
1540
+ __slots__ = ("api_key", "cert_pass", "config")
1541
+
1542
+ def __init__(self) -> None:
1543
+ self.api_key = None
1544
+ self.cert_pass = None
1545
+ self.config = None
1546
+
1547
+ def __repr__(self) -> str:
1548
+ return "<tajemství skryta>"
1549
+
1550
+ def __reduce__(self):
1551
+ return (_SecretVault, ())
1552
+
1553
+
1508
1554
  class Context(LoggedObject, metaclass=Singleton):
1555
+ """Singleton se stavem posledního ověření (zastaralé pro ověřování API klíčů).
1556
+
1557
+ Instance je sdílená všemi souběžnými požadavky, takže po ``await`` může jeden požadavek vidět identitu jiného.
1558
+ Ověřování API klíčů patří knihovně ``sysnet-apikeys``; identitu požadavku nese ``RequestContext``
1559
+ (``contextvars``, bez sdíleného stavu). Třída zůstává beze změny chování kvůli stávajícím službám.
1560
+
1561
+ Tajemství (``config``, ``api_key``, ``cert_pass``) se čtou jako dřív, ale nejsou ve ``vars()``, v ``repr`` ani
1562
+ v ``pickle``/``copy``; pro diagnostiku slouží :meth:`safe_dict`.
1563
+ """
1564
+
1509
1565
  def __init__(self, config=None, api_key=None, user_name=None, agenda=None, cert_file=None, cert_pass=None):
1510
1566
  super().__init__(object_name="CONTEXT")
1567
+ self._vault = _SecretVault()
1511
1568
  self.config = config
1512
1569
  if self.config is None:
1513
1570
  self.log.error(f"{self.name} FAILED - Missing config")
@@ -1519,6 +1576,62 @@ class Context(LoggedObject, metaclass=Singleton):
1519
1576
  self.cert_pass = cert_pass
1520
1577
  self.log.info(f"{self.name} Created")
1521
1578
 
1579
+ @property
1580
+ def config(self):
1581
+ """Konfigurace (obsahuje tajemství, proto není ve ``vars()``)."""
1582
+ return self._vault.config
1583
+
1584
+ @config.setter
1585
+ def config(self, value):
1586
+ self._vault.config = value
1587
+
1588
+ @property
1589
+ def api_key(self):
1590
+ """Ověřený API klíč (tajemství, ve ``vars()`` není)."""
1591
+ return self._vault.api_key
1592
+
1593
+ @api_key.setter
1594
+ def api_key(self, value):
1595
+ self._vault.api_key = value
1596
+
1597
+ @property
1598
+ def cert_pass(self):
1599
+ """Heslo certifikátu (tajemství, ve ``vars()`` není). Čte se jako obyčejný řetězec."""
1600
+ return self._vault.cert_pass
1601
+
1602
+ @cert_pass.setter
1603
+ def cert_pass(self, value):
1604
+ self._vault.cert_pass = value
1605
+
1606
+ def safe_dict(self) -> dict:
1607
+ """Bezpečný diagnostický výpis: identita a certifikát, tajemství jako ``***`` (nebo ``None``, když chybí)."""
1608
+
1609
+ def mask(value):
1610
+ return None if value is None else "***"
1611
+
1612
+ return {
1613
+ "agenda": self.agenda,
1614
+ "user_name": self.user_name,
1615
+ "authenticated": self.authenticated,
1616
+ "cert_file": self.cert_file,
1617
+ "api_key": mask(self.api_key),
1618
+ "cert_pass": mask(self.cert_pass),
1619
+ "config": mask(self.config),
1620
+ }
1621
+
1622
+ def __repr__(self) -> str:
1623
+ info = self.safe_dict()
1624
+ parts = ", ".join(
1625
+ f"{k}={v!r}" if k not in ("api_key", "cert_pass", "config") else f"{k}={v}" for k, v in info.items()
1626
+ )
1627
+ return f"{type(self).__name__}({parts})"
1628
+
1629
+ def __getstate__(self):
1630
+ """Pro ``pickle`` a ``copy``: tajemství se nepřenášejí (kopie má prázdnou schránku)."""
1631
+ state = self.__dict__.copy()
1632
+ state["_vault"] = _SecretVault()
1633
+ return state
1634
+
1522
1635
  def _singleton_reconfigure(
1523
1636
  self, config=None, api_key=None, user_name=None, agenda=None, cert_file=None, cert_pass=None
1524
1637
  ):
@@ -1539,49 +1652,80 @@ class Context(LoggedObject, metaclass=Singleton):
1539
1652
  self.cert_pass = None
1540
1653
  self.log.info(f"{self.name} Cleared")
1541
1654
 
1655
+ def _agendas(self):
1656
+ """Dvojice (agenda, sekce) jen pro sekce typu slovník; vadná konfigurace nevyhodí výjimku."""
1657
+ if not isinstance(self.config, dict):
1658
+ self.log.error(f"{self.name} - config is not a dict, authentication is not possible")
1659
+ return []
1660
+ return [(agenda, section) for agenda, section in self.config.items() if isinstance(section, dict)]
1661
+
1662
+ def _load_cert(self, section):
1663
+ """Načte cestu a heslo certifikátu agendy; neplatná sekce ``cert`` se přeskočí."""
1664
+ cert = section.get(CONFIG_CERT)
1665
+ if not isinstance(cert, dict):
1666
+ return
1667
+ if CONFIG_PATH in cert:
1668
+ self.cert_file = cert[CONFIG_PATH]
1669
+ if CONFIG_PASSWORD in cert:
1670
+ self.cert_pass = cert[CONFIG_PASSWORD]
1671
+
1542
1672
  def check_api_key(self, api_key):
1673
+ """Ověří API klíč podle ``config[agenda]['api_keys']``.
1674
+
1675
+ .. deprecated:: 1.9.0
1676
+ Ověřování API klíčů patří knihovně ``sysnet-apikeys`` (standard CML ``api-keys.md``). Chování se nemění.
1677
+
1678
+ Vstup musí být neprázdný řetězec, jinak vrací ``False``. Vadná konfigurace (sekce ``api_keys`` jiného typu
1679
+ než seznam, položky jiného typu než slovník) se přeskočí s varováním bez hodnot a nevyhodí výjimku.
1680
+ """
1681
+ _warn_deprecated_once("Context.check_api_key")
1543
1682
  self.clear()
1544
- if api_key in [None, ""]:
1683
+ if not isinstance(api_key, str) or api_key == "":
1545
1684
  return False
1546
- for agenda in self.config.keys():
1547
- if not isinstance(self.config[agenda], dict):
1685
+ for agenda, section in self._agendas():
1686
+ if CONFIG_API_KEYS not in section:
1548
1687
  continue
1549
- if CONFIG_API_KEYS not in self.config[agenda]:
1688
+ entries = section[CONFIG_API_KEYS]
1689
+ if not isinstance(entries, list):
1690
+ self.log.warning(f"{self.name} - '{CONFIG_API_KEYS}' of agenda '{agenda}' is not a list, skipped")
1550
1691
  continue
1551
- for ak in self.config[agenda][CONFIG_API_KEYS]:
1552
- if api_key in ak.keys():
1692
+ for ak in entries:
1693
+ if not isinstance(ak, dict):
1694
+ continue
1695
+ for candidate, user in ak.items():
1696
+ if not isinstance(candidate, str):
1697
+ self.log.warning(
1698
+ f"{self.name} - agenda '{agenda}' has a non-string API key ({type(candidate).__name__}), "
1699
+ "compared as text"
1700
+ )
1701
+ if str(candidate) != api_key:
1702
+ continue
1553
1703
  self.api_key = api_key
1554
1704
  self.agenda = agenda
1555
- self.user_name = ak[api_key]
1705
+ self.user_name = user
1556
1706
  self.authenticated = True
1557
- if CONFIG_CERT in self.config[agenda]:
1558
- if CONFIG_PATH in self.config[agenda][CONFIG_CERT]:
1559
- self.cert_file = self.config[agenda][CONFIG_CERT][CONFIG_PATH]
1560
- if CONFIG_PASSWORD in self.config[agenda][CONFIG_CERT]:
1561
- self.cert_pass = self.config[agenda][CONFIG_CERT][CONFIG_PASSWORD]
1707
+ self._load_cert(section)
1562
1708
  self.log.info(f"{self.name} - User '{self.user_name}' logged in")
1563
- break
1564
- if self.authenticated:
1565
- break
1709
+ return self.authenticated
1566
1710
  return self.authenticated
1567
1711
 
1568
1712
  def check_user(self, username, password):
1713
+ """Ověří jméno a heslo podle ``config[agenda]['paswd']``.
1714
+
1715
+ .. deprecated:: 1.9.0
1716
+ Viz :meth:`check_api_key`. Chování se nemění; vadná sekce ``paswd`` se přeskočí bez výjimky.
1717
+ """
1718
+ _warn_deprecated_once("Context.check_user")
1569
1719
  self.clear()
1570
- for agenda in self.config.keys():
1571
- if not isinstance(self.config[agenda], dict):
1572
- continue
1573
- if CONFIG_PASSWD not in self.config[agenda]:
1720
+ for agenda, section in self._agendas():
1721
+ users = section.get(CONFIG_PASSWD)
1722
+ if not isinstance(users, dict):
1574
1723
  continue
1575
- if username in self.config[agenda][CONFIG_PASSWD]:
1576
- if self.config[agenda][CONFIG_PASSWD][username] == password:
1577
- self.api_key = None
1578
- self.agenda = agenda
1579
- self.user_name = username
1580
- self.authenticated = True
1581
- if CONFIG_CERT in self.config[agenda]:
1582
- if CONFIG_PATH in self.config[agenda][CONFIG_CERT]:
1583
- self.cert_file = self.config[agenda][CONFIG_CERT][CONFIG_PATH]
1584
- if CONFIG_PASSWORD in self.config[agenda][CONFIG_CERT]:
1585
- self.cert_pass = self.config[agenda][CONFIG_CERT][CONFIG_PASSWORD]
1586
- break
1724
+ if username in users and users[username] == password:
1725
+ self.api_key = None
1726
+ self.agenda = agenda
1727
+ self.user_name = username
1728
+ self.authenticated = True
1729
+ self._load_cert(section)
1730
+ break
1587
1731
  return self.authenticated
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sysnet-pyutils
3
- Version: 1.8.0
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.8.0-blue.svg)](https://pypi.org/project/sysnet-pyutils/1.8.0/)
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
 
@@ -901,6 +901,44 @@ if ctx.check_api_key('some_key'):
901
901
  print(f"Přihlášen jako: {ctx.user_name}")
902
902
  ```
903
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
+
904
942
  #### Identifikátory a validace
905
943
 
906
944
  ```python
@@ -1409,6 +1447,15 @@ při běžném vydání již lokálně nic nenahrávejte na PyPI.
1409
1447
 
1410
1448
  Podrobnosti nových změn jsou v [CHANGELOG.md](CHANGELOG.md).
1411
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
+
1412
1459
  ### Verze 1.8.0 (2026-10-27)
1413
1460
 
1414
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`.
@@ -1555,6 +1602,6 @@ Tento projekt je licencován pod MIT licencí — viz [LICENSE](LICENSE) soubor.
1555
1602
 
1556
1603
  **Vytvořeno s ❤️ týmem SYSNET**
1557
1604
 
1558
- *Verze: 1.8.0*
1605
+ *Verze: 1.9.0*
1559
1606
 
1560
1607
  *Datum: 2026-10-27*
@@ -9,6 +9,7 @@ sysnet_pyutils/domino.py
9
9
  sysnet_pyutils/geo.py
10
10
  sysnet_pyutils/ident.py
11
11
  sysnet_pyutils/log.py
12
+ sysnet_pyutils/request_context.py
12
13
  sysnet_pyutils/ses.py
13
14
  sysnet_pyutils/tools.py
14
15
  sysnet_pyutils/utils.py
@@ -30,6 +31,8 @@ sysnet_pyutils/syslog/__init__.py
30
31
  sysnet_pyutils/syslog/factory.py
31
32
  tests/test_config_secrets.py
32
33
  tests/test_config_store.py
34
+ tests/test_context_legacy_robustness.py
35
+ tests/test_context_secrets_masking.py
33
36
  tests/test_cron_utils.py
34
37
  tests/test_cron_validation_edge.py
35
38
  tests/test_data_utils.py
@@ -49,6 +52,7 @@ tests/test_models_general.py
49
52
  tests/test_models_general_edge.py
50
53
  tests/test_package_init_version.py
51
54
  tests/test_recursive_update.py
55
+ tests/test_request_context.py
52
56
  tests/test_ses.py
53
57
  tests/test_ses_importerror_branch.py
54
58
  tests/test_syslog_factory.py