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.
- {sysnet_pyutils-1.7.12/sysnet_pyutils.egg-info → sysnet_pyutils-1.9.0}/PKG-INFO +81 -5
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/README.md +80 -4
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/pyproject.toml +1 -1
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/sysnet_pyutils/__init__.py +9 -0
- sysnet_pyutils-1.9.0/sysnet_pyutils/request_context.py +90 -0
- sysnet_pyutils-1.9.0/sysnet_pyutils/utils.py +1731 -0
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0/sysnet_pyutils.egg-info}/PKG-INFO +81 -5
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/sysnet_pyutils.egg-info/SOURCES.txt +6 -0
- sysnet_pyutils-1.9.0/tests/test_config_secrets.py +282 -0
- sysnet_pyutils-1.9.0/tests/test_config_store.py +834 -0
- sysnet_pyutils-1.9.0/tests/test_context_legacy_robustness.py +175 -0
- sysnet_pyutils-1.9.0/tests/test_context_secrets_masking.py +210 -0
- sysnet_pyutils-1.9.0/tests/test_request_context.py +301 -0
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/tests/test_utils_missing_branches.py +3 -3
- sysnet_pyutils-1.7.12/sysnet_pyutils/utils.py +0 -1036
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/LICENSE +0 -0
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/setup.cfg +0 -0
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/sysnet_pyutils/barcode.py +0 -0
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/sysnet_pyutils/constants.py +0 -0
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/sysnet_pyutils/cron_utils/__init__.py +0 -0
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/sysnet_pyutils/cron_utils/validation.py +0 -0
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/sysnet_pyutils/data_utils.py +0 -0
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/sysnet_pyutils/domino.py +0 -0
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/sysnet_pyutils/geo.py +0 -0
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/sysnet_pyutils/globalmodel/__init__.py +0 -0
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/sysnet_pyutils/globalmodel/global_model.py +0 -0
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/sysnet_pyutils/globalmodel/models.py +0 -0
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/sysnet_pyutils/globalmodel/timeutils.py +0 -0
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/sysnet_pyutils/ident.py +0 -0
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/sysnet_pyutils/log.py +0 -0
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/sysnet_pyutils/models/__init__.py +0 -0
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/sysnet_pyutils/models/general.py +0 -0
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/sysnet_pyutils/ses.py +0 -0
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/sysnet_pyutils/syslog/__init__.py +0 -0
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/sysnet_pyutils/syslog/factory.py +0 -0
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/sysnet_pyutils/tools.py +0 -0
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/sysnet_pyutils.egg-info/dependency_links.txt +0 -0
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/sysnet_pyutils.egg-info/not-zip-safe +0 -0
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/sysnet_pyutils.egg-info/requires.txt +0 -0
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/sysnet_pyutils.egg-info/top_level.txt +0 -0
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/tests/test_cron_utils.py +0 -0
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/tests/test_cron_validation_edge.py +0 -0
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/tests/test_data_utils.py +0 -0
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/tests/test_data_utils_datetime.py +0 -0
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/tests/test_data_utils_edge.py +0 -0
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/tests/test_data_utils_missing_branches.py +0 -0
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/tests/test_domino.py +0 -0
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/tests/test_domino_edge.py +0 -0
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/tests/test_geo.py +0 -0
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/tests/test_global_model_recursive.py +0 -0
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/tests/test_global_model_timezone.py +0 -0
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/tests/test_ident_barcode.py +0 -0
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/tests/test_log_importerror_branch.py +0 -0
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/tests/test_log_mongo.py +0 -0
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/tests/test_models_contact.py +0 -0
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/tests/test_models_general.py +0 -0
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/tests/test_models_general_edge.py +0 -0
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/tests/test_package_init_version.py +0 -0
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/tests/test_recursive_update.py +0 -0
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/tests/test_ses.py +0 -0
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/tests/test_ses_importerror_branch.py +0 -0
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/tests/test_syslog_factory.py +0 -0
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/tests/test_tools.py +0 -0
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/tests/test_utils.py +0 -0
- {sysnet_pyutils-1.7.12 → sysnet_pyutils-1.9.0}/tests/test_utils_edge.py +0 -0
- {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.
|
|
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
|
[](https://www.python.org/downloads/)
|
|
731
731
|
[](LICENSE)
|
|
732
|
-
[](https://pypi.org/project/sysnet-pyutils/1.9.0/)
|
|
733
733
|
[](https://github.com/SYSNET-CZ/pyutils/actions/workflows/ci.yml)
|
|
734
734
|
[](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.
|
|
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.
|
|
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.
|
|
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
|
[](https://www.python.org/downloads/)
|
|
8
8
|
[](LICENSE)
|
|
9
|
-
[](https://pypi.org/project/sysnet-pyutils/1.9.0/)
|
|
10
10
|
[](https://github.com/SYSNET-CZ/pyutils/actions/workflows/ci.yml)
|
|
11
11
|
[](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.
|
|
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.
|
|
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.
|
|
882
|
+
*Verze: 1.9.0*
|
|
807
883
|
|
|
808
884
|
*Datum: 2026-10-27*
|
|
@@ -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)
|