s-netkit 0.0.1__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 (55) hide show
  1. s_netkit-0.0.1/.gitignore +50 -0
  2. s_netkit-0.0.1/.gitlab-ci.yml +32 -0
  3. s_netkit-0.0.1/CHANGELOG.md +54 -0
  4. s_netkit-0.0.1/LICENSE +21 -0
  5. s_netkit-0.0.1/PKG-INFO +120 -0
  6. s_netkit-0.0.1/README.md +98 -0
  7. s_netkit-0.0.1/netkit/__init__.py +240 -0
  8. s_netkit-0.0.1/netkit/contract.py +380 -0
  9. s_netkit-0.0.1/netkit/declare.py +329 -0
  10. s_netkit-0.0.1/netkit/errmap.py +357 -0
  11. s_netkit-0.0.1/netkit/forms.py +82 -0
  12. s_netkit-0.0.1/netkit/graphql.py +228 -0
  13. s_netkit-0.0.1/netkit/ladder/__init__.py +163 -0
  14. s_netkit-0.0.1/netkit/ladder/capabilities.py +172 -0
  15. s_netkit-0.0.1/netkit/ladder/core.py +410 -0
  16. s_netkit-0.0.1/netkit/ladder/events.py +145 -0
  17. s_netkit-0.0.1/netkit/ladder/gate.py +169 -0
  18. s_netkit-0.0.1/netkit/ladder/memory.py +342 -0
  19. s_netkit-0.0.1/netkit/ladder/minting.py +195 -0
  20. s_netkit-0.0.1/netkit/ladder/spec.py +225 -0
  21. s_netkit-0.0.1/netkit/ladder/steps.py +170 -0
  22. s_netkit-0.0.1/netkit/ladder/sync_core.py +146 -0
  23. s_netkit-0.0.1/netkit/limit.py +474 -0
  24. s_netkit-0.0.1/netkit/pagination.py +462 -0
  25. s_netkit-0.0.1/netkit/providers.py +406 -0
  26. s_netkit-0.0.1/netkit/retry.py +318 -0
  27. s_netkit-0.0.1/netkit/rpc.py +193 -0
  28. s_netkit-0.0.1/netkit/stream.py +186 -0
  29. s_netkit-0.0.1/netkit/transport/__init__.py +87 -0
  30. s_netkit-0.0.1/netkit/transport/_helpers.py +42 -0
  31. s_netkit-0.0.1/netkit/transport/http_client.py +446 -0
  32. s_netkit-0.0.1/netkit/transport/permissive.py +147 -0
  33. s_netkit-0.0.1/netkit/transport/rest_client.py +382 -0
  34. s_netkit-0.0.1/netkit/transport/transport_http.py +378 -0
  35. s_netkit-0.0.1/netkit/upload.py +206 -0
  36. s_netkit-0.0.1/pyproject.toml +76 -0
  37. s_netkit-0.0.1/tests/conftest.py +79 -0
  38. s_netkit-0.0.1/tests/test_declare.py +274 -0
  39. s_netkit-0.0.1/tests/test_errmap.py +284 -0
  40. s_netkit-0.0.1/tests/test_forms.py +89 -0
  41. s_netkit-0.0.1/tests/test_graphql.py +227 -0
  42. s_netkit-0.0.1/tests/test_ladder.py +932 -0
  43. s_netkit-0.0.1/tests/test_ladder_degradation_event.py +258 -0
  44. s_netkit-0.0.1/tests/test_lazy_facade.py +139 -0
  45. s_netkit-0.0.1/tests/test_limit.py +141 -0
  46. s_netkit-0.0.1/tests/test_pagination.py +479 -0
  47. s_netkit-0.0.1/tests/test_permissive_http.py +44 -0
  48. s_netkit-0.0.1/tests/test_providers.py +197 -0
  49. s_netkit-0.0.1/tests/test_retry.py +110 -0
  50. s_netkit-0.0.1/tests/test_rpc.py +221 -0
  51. s_netkit-0.0.1/tests/test_stream.py +170 -0
  52. s_netkit-0.0.1/tests/test_transport_client.py +413 -0
  53. s_netkit-0.0.1/tests/test_transport_rest.py +527 -0
  54. s_netkit-0.0.1/tests/test_transport_sync.py +261 -0
  55. s_netkit-0.0.1/tests/test_upload.py +227 -0
@@ -0,0 +1,50 @@
1
+ # === universal gitignore ===
2
+
3
+ # OS / IDE
4
+ .DS_Store
5
+ Thumbs.db
6
+ .vscode/
7
+ .idea/
8
+ *.swp
9
+ *.swo
10
+
11
+ # Sensitive
12
+ .env
13
+ .env.local
14
+ *.key
15
+ *.pem
16
+ secrets/
17
+ private/
18
+
19
+ # Python
20
+ __pycache__/
21
+ *.py[cod]
22
+ .venv/
23
+ venv/
24
+ .pytest_cache/
25
+ .ruff_cache/
26
+ *.egg-info/
27
+
28
+ # Node / JS
29
+ node_modules/
30
+ .next/
31
+ dist/
32
+ build/
33
+
34
+ # Temporary / large
35
+ *.log
36
+ *.tmp
37
+ nul
38
+ NUL
39
+ *.zip
40
+ *.rar
41
+ *.7z
42
+
43
+ # Media (selectively unignore via !path/*.ext if needed for fixtures)
44
+ *.mp4
45
+ *.mov
46
+ *.avi
47
+ *.mkv
48
+
49
+ # git worktrees (агентские)
50
+ .worktrees/
@@ -0,0 +1,32 @@
1
+ # Автопубликация на PyPI по семвер-тегу vX.Y.Z через Trusted Publishing (OIDC).
2
+ # Токены НИГДЕ не хранятся: GitLab выдаёт короткоживущий OIDC-токен (PYPI_ID_TOKEN),
3
+ # он одноразово обменивается на PyPI API-токен. Издатель настраивается на pypi.org:
4
+ # Manage -> Publishing -> GitLab (namespace=cifropro1/products, project=netkit,
5
+ # top-level pipeline file=.gitlab-ci.yml, environment — пусто).
6
+ # Дистрибутив на PyPI: s-netkit.
7
+ # Запускается ТОЛЬКО на тег vX.Y.Z (обычный push на main ничего не публикует).
8
+ stages: [publish]
9
+
10
+ publish-pypi:
11
+ stage: publish
12
+ image: ghcr.io/astral-sh/uv:python3.12-bookworm
13
+ rules:
14
+ - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
15
+ id_tokens:
16
+ PYPI_ID_TOKEN:
17
+ aud: pypi
18
+ script:
19
+ - uv build
20
+ # обмен GitLab OIDC -> одноразовый PyPI API-токен (Trusted Publishing):
21
+ - |
22
+ export UV_PUBLISH_TOKEN=$(python3 - <<'PY'
23
+ import json, os, urllib.request
24
+ req = urllib.request.Request(
25
+ "https://pypi.org/_/oidc/mint-token",
26
+ data=json.dumps({"token": os.environ["PYPI_ID_TOKEN"]}).encode(),
27
+ headers={"Content-Type": "application/json"},
28
+ )
29
+ print(json.load(urllib.request.urlopen(req))["token"])
30
+ PY
31
+ )
32
+ - uv publish
@@ -0,0 +1,54 @@
1
+ # CHANGELOG — netkit (`s-netkit`)
2
+
3
+ Формат — [Keep a Changelog](https://keepachangelog.com/ru/1.1.0/);
4
+ версии — [SemVer](https://semver.org/lang/ru/).
5
+
6
+ ## 0.0.1 — выделение сетевого слоя
7
+
8
+ ### Добавлено
9
+
10
+ - **Кит целиком.** Сетевой слой китов выделен из `librarykit` в отдельный
11
+ дистрибутив `s-netkit`. Зависимости строго вниз по слоям: `corekit` + `httpx`
12
+ (+ `stamina` как движок повторов транспорта); `curl-cffi` и `websockets` —
13
+ опциональные extra.
14
+ - `netkit.transport` — исполнители запроса поверх httpx (async + sync близнец),
15
+ choke-point `HttpClient`/`SyncHttpClient`, `RestHttpClient`, permissive-рецепты.
16
+ - `netkit.stream` / `netkit.rpc` / `netkit.graphql` — persistent-каналы (WS),
17
+ codec-слой RPC, GraphQL-клиент.
18
+ - `netkit.limit` / `netkit.retry` — темп (token-bucket `RateLimiter`) и живучесть
19
+ (header-driven `RetryPolicy`, фиксированная `SimpleRetryPolicy`).
20
+ - `netkit.ladder` — лестница деградации (ступени исполнения и добычи, память
21
+ ступени, гейт диагностики, события спуска, sync-обёртка).
22
+ - `netkit.pagination` / `netkit.upload` / `netkit.forms` / `netkit.errmap` —
23
+ листание ресурса, resumable-догрузка, form-кодек, ответ→доменная ошибка.
24
+ - `netkit.contract` — сетевая часть «талии» (`Transport`/`SyncTransport`,
25
+ `ResponseLike`, `Auth`/`Refreshable`, `Codec`, `StreamTransport`, `ErrorMapper`,
26
+ `Paginator`) + реэкспорт значений `corekit`.
27
+ - **`netkit.declare` — суть кита:** транспорт ОБЪЯВЛЯЕТСЯ значением
28
+ (`TransportSpec`: вид `http`/`ws`/`rpc`/`graphql`, адрес, лимит, повторы), а
29
+ `declare()` отдаёт исполнителя с ЕДИНЫМ поведением лимитов и повторов поверх
30
+ любого вида. Свой вид добавляется `register_kind()` и получает то же поведение.
31
+ - **`netkit.providers` — реестр слотов.** Всё, что живёт ЭТАЖОМ ВЫШЕ (браузерный
32
+ минт, каталог профиля движка, склад состояния, межпроцессный лок, полная
33
+ диагностика, egress `ExecutionContext`, фабрика видов транспорта), приходит
34
+ сюда СЛОТАМИ, а не импортом — иначе `netkit → browserkit → netkit` замкнулось бы
35
+ в цикл. Слоты со своим дефолтом никогда не роняют вызов; браузерные слоты
36
+ дефолта не имеют и поднимают `ProviderMissing` с инструкцией.
37
+
38
+ ### Совместимость
39
+
40
+ - `librarykit` остаётся ФАСАДОМ: `librarykit.transport`, `librarykit.ladder`,
41
+ `librarykit.limit`, `librarykit.retry`, `librarykit.errmap`,
42
+ `librarykit.pagination`, `librarykit.upload`, `librarykit.forms`,
43
+ `librarykit.rpc`, `librarykit.stream`, `librarykit.graphql` реэкспортируют ТЕ
44
+ ЖЕ объекты (не копии). Подмодули (`librarykit.transport.http_client`,
45
+ `librarykit.ladder.minting`, …) сохранены РЕАЛЬНЫМИ модулями — `from pkg.sub
46
+ import X` и монки-патч по старому пути продолжают работать.
47
+ - `import librarykit` заполняет все слоты `netkit.providers` своими
48
+ реализациями, поэтому поведение существующих интеграций не меняется.
49
+
50
+ ### Стоимость импорта
51
+
52
+ - `import netkit` не исполняет ни одного подмодуля (PEP 562): ни httpx, ни
53
+ stamina, ни asyncio. Закреплено fitness-тестами `tests/test_lazy_facade.py`,
54
+ включая набор модулей, которые `librarykit` реэкспортирует ЖАДНО.
s_netkit-0.0.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dmitry
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,120 @@
1
+ Metadata-Version: 2.4
2
+ Name: s-netkit
3
+ Version: 0.0.1
4
+ Summary: СЕТЕВОЙ слой китов: транспорт (async+sync httpx), WS/RPC/GraphQL, лимиты и повторы, лестница деградации, пагинация, аплоад, form-кодек. Декларативное объявление транспорта с ЕДИНЫМ поведением лимитов и ретраев.
5
+ Author: Dmitry
6
+ License: MIT
7
+ License-File: LICENSE
8
+ Requires-Python: >=3.11
9
+ Requires-Dist: httpx>=0.27
10
+ Requires-Dist: s-corekit>=0.0.1
11
+ Requires-Dist: stamina>=24.3
12
+ Provides-Extra: curl
13
+ Requires-Dist: curl-cffi>=0.7; extra == 'curl'
14
+ Provides-Extra: dev
15
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
16
+ Requires-Dist: pytest>=8; extra == 'dev'
17
+ Requires-Dist: respx>=0.21; extra == 'dev'
18
+ Requires-Dist: ruff>=0.8; extra == 'dev'
19
+ Provides-Extra: ws
20
+ Requires-Dist: websockets>=12.0; extra == 'ws'
21
+ Description-Content-Type: text/markdown
22
+
23
+ # netkit (`s-netkit`)
24
+
25
+ **Сетевой слой китов.** Всё, чем интеграция разговаривает с чужим сервером:
26
+ транспорт, темп, живучесть, деградация. Ставится и работает без оркестратора —
27
+ зависимости идут строго вниз: `netkit → corekit`.
28
+
29
+ ```
30
+ clikit adapterkit <- ветки-оболочки
31
+ \ /
32
+ librarykit <- ОРКЕСТРАТОР (сессии, браузер, склад)
33
+ |
34
+ netkit <- СЕТЬ (этот пакет)
35
+ |
36
+ corekit <- ОСНОВАНИЕ (значения и правила)
37
+ ```
38
+
39
+ ## Что внутри
40
+
41
+ | модуль | ответственность |
42
+ |---|---|
43
+ | `netkit.transport` | исполнители запроса поверх httpx (async + sync близнец), choke-point `HttpClient`/`SyncHttpClient`, `RestHttpClient` к своему backend, permissive-рецепты |
44
+ | `netkit.stream` | persistent-каналы: `StreamTransport`, WS-реализация (extra `[ws]`) |
45
+ | `netkit.rpc` | codec-слой RPC: `JsonCodec` / `PrefixedJsonCodec` / `RpcClient` |
46
+ | `netkit.graphql` | GraphQL-клиент поверх транспорта кита |
47
+ | `netkit.limit` | `RateLimiter` + token-bucket: ПРОАКТИВНЫЙ темп, а не «поймал 429 — поспал» |
48
+ | `netkit.retry` | политики повторов: header-driven (`RetryPolicy`) и фиксированная (`SimpleRetryPolicy`) |
49
+ | `netkit.ladder` | лестница деградации: чем выполнять запросы и чем добывать состояние, память ступени, события спуска |
50
+ | `netkit.pagination` / `netkit.upload` / `netkit.forms` | листание ресурса, resumable-догрузка, form-urlencoded кодек |
51
+ | `netkit.errmap` | ответ сервера → доменная ошибка (декларативная таблица) |
52
+ | `netkit.declare` | **объявление транспорта** (`http`/`ws`/`rpc`/`graphql`) с ЕДИНЫМ поведением лимитов и повторов |
53
+ | `netkit.providers` | СЛОТЫ верхнего слоя: браузерный минт, склад состояния, диагностика, egress |
54
+
55
+ ## Объявить транспорт декларативно
56
+
57
+ Один и тот же лимит и одна и та же политика повторов — на любом виде транспорта.
58
+ Интеграция объявляет, а не пишет обвязку:
59
+
60
+ ```python
61
+ from netkit.declare import KIND_HTTP, KIND_WS, TransportSpec, declare
62
+ from netkit.limit import LimitPolicy, LimitScope
63
+ from netkit.retry import SimpleRetryPolicy
64
+
65
+ limit = LimitPolicy(rate=5, per=1.0) # 5 обменов в секунду
66
+ retry = SimpleRetryPolicy(backoff=(0.0, 0.0)) # 2 повтора без пауз
67
+ scope = LimitScope(service="acme")
68
+
69
+ api = declare(TransportSpec(kind=KIND_HTTP, url="https://api.acme.io",
70
+ limit=limit, scope=scope, retry=retry))
71
+ live = declare(TransportSpec(kind=KIND_WS, channel=my_ws_channel,
72
+ limit=limit, scope=scope, retry=retry))
73
+
74
+ await api.call("GET", "/v1/items") # ждёт квоту, повторяет 429/5xx и сбои
75
+ await live.call('{"op":"ping"}') # ТОТ ЖЕ темп и ТЕ ЖЕ повторы — без своего кода
76
+ ```
77
+
78
+ Свой вид (`grpc`, `sse`, …) добавляется `register_kind(kind, builder)` и сразу
79
+ получает то же поведение.
80
+
81
+ ## Слоты: как netkit зовёт то, что живёт выше
82
+
83
+ Последняя ступень лестницы поднимает браузер, а браузер — чужой кит, который сам
84
+ зависит от сети. Прямой импорт дал бы цикл, поэтому направление разворачивается:
85
+ netkit **объявляет слот**, верхний слой **заполняет** его на своём импорте.
86
+
87
+ ```python
88
+ from netkit.providers import SLOT_BROWSER_MINT, register_provider
89
+ register_provider(SLOT_BROWSER_MINT, my_mint_session)
90
+ ```
91
+
92
+ Слоты со своим дефолтом (`json_store`, `file_lock`, `state_root`, `path_slug`,
93
+ `transport_factory`, `diagnose`, `egress_proxy`) никогда не роняют вызов — netkit
94
+ умеет их сам, верхний слой лишь уточняет. Слоты без дефолта (браузерные) при
95
+ обращении поднимают `ProviderMissing` с инструкцией: молчаливой деградации
96
+ «ступень тихо ничего не сделала» здесь нет.
97
+
98
+ Если в окружении стоит `librarykit`, его импорт заполняет все слоты сам —
99
+ отдельная регистрация не нужна.
100
+
101
+ ## Совместимость
102
+
103
+ `librarykit` остаётся фасадом: `librarykit.transport`, `librarykit.ladder`,
104
+ `librarykit.limit`, `librarykit.retry`, `librarykit.pagination`,
105
+ `librarykit.upload`, `librarykit.forms`, `librarykit.rpc`, `librarykit.stream`,
106
+ `librarykit.graphql`, `librarykit.errmap` реэкспортируют ТЕ ЖЕ объекты (не
107
+ копии) — `isinstance` / `except` / `is` работают через любой из путей.
108
+
109
+ ## Стоимость импорта
110
+
111
+ `import netkit` не исполняет ни одного подмодуля: ни httpx, ни stamina, ни
112
+ asyncio. Имена резолвятся по PEP 562 при первом обращении — платит тот, кому
113
+ нужно.
114
+
115
+ ## Установка
116
+
117
+ ```bash
118
+ pip install s-netkit # ядро: corekit + httpx + stamina
119
+ pip install s-netkit[ws] # + websockets для WS-транспорта
120
+ ```
@@ -0,0 +1,98 @@
1
+ # netkit (`s-netkit`)
2
+
3
+ **Сетевой слой китов.** Всё, чем интеграция разговаривает с чужим сервером:
4
+ транспорт, темп, живучесть, деградация. Ставится и работает без оркестратора —
5
+ зависимости идут строго вниз: `netkit → corekit`.
6
+
7
+ ```
8
+ clikit adapterkit <- ветки-оболочки
9
+ \ /
10
+ librarykit <- ОРКЕСТРАТОР (сессии, браузер, склад)
11
+ |
12
+ netkit <- СЕТЬ (этот пакет)
13
+ |
14
+ corekit <- ОСНОВАНИЕ (значения и правила)
15
+ ```
16
+
17
+ ## Что внутри
18
+
19
+ | модуль | ответственность |
20
+ |---|---|
21
+ | `netkit.transport` | исполнители запроса поверх httpx (async + sync близнец), choke-point `HttpClient`/`SyncHttpClient`, `RestHttpClient` к своему backend, permissive-рецепты |
22
+ | `netkit.stream` | persistent-каналы: `StreamTransport`, WS-реализация (extra `[ws]`) |
23
+ | `netkit.rpc` | codec-слой RPC: `JsonCodec` / `PrefixedJsonCodec` / `RpcClient` |
24
+ | `netkit.graphql` | GraphQL-клиент поверх транспорта кита |
25
+ | `netkit.limit` | `RateLimiter` + token-bucket: ПРОАКТИВНЫЙ темп, а не «поймал 429 — поспал» |
26
+ | `netkit.retry` | политики повторов: header-driven (`RetryPolicy`) и фиксированная (`SimpleRetryPolicy`) |
27
+ | `netkit.ladder` | лестница деградации: чем выполнять запросы и чем добывать состояние, память ступени, события спуска |
28
+ | `netkit.pagination` / `netkit.upload` / `netkit.forms` | листание ресурса, resumable-догрузка, form-urlencoded кодек |
29
+ | `netkit.errmap` | ответ сервера → доменная ошибка (декларативная таблица) |
30
+ | `netkit.declare` | **объявление транспорта** (`http`/`ws`/`rpc`/`graphql`) с ЕДИНЫМ поведением лимитов и повторов |
31
+ | `netkit.providers` | СЛОТЫ верхнего слоя: браузерный минт, склад состояния, диагностика, egress |
32
+
33
+ ## Объявить транспорт декларативно
34
+
35
+ Один и тот же лимит и одна и та же политика повторов — на любом виде транспорта.
36
+ Интеграция объявляет, а не пишет обвязку:
37
+
38
+ ```python
39
+ from netkit.declare import KIND_HTTP, KIND_WS, TransportSpec, declare
40
+ from netkit.limit import LimitPolicy, LimitScope
41
+ from netkit.retry import SimpleRetryPolicy
42
+
43
+ limit = LimitPolicy(rate=5, per=1.0) # 5 обменов в секунду
44
+ retry = SimpleRetryPolicy(backoff=(0.0, 0.0)) # 2 повтора без пауз
45
+ scope = LimitScope(service="acme")
46
+
47
+ api = declare(TransportSpec(kind=KIND_HTTP, url="https://api.acme.io",
48
+ limit=limit, scope=scope, retry=retry))
49
+ live = declare(TransportSpec(kind=KIND_WS, channel=my_ws_channel,
50
+ limit=limit, scope=scope, retry=retry))
51
+
52
+ await api.call("GET", "/v1/items") # ждёт квоту, повторяет 429/5xx и сбои
53
+ await live.call('{"op":"ping"}') # ТОТ ЖЕ темп и ТЕ ЖЕ повторы — без своего кода
54
+ ```
55
+
56
+ Свой вид (`grpc`, `sse`, …) добавляется `register_kind(kind, builder)` и сразу
57
+ получает то же поведение.
58
+
59
+ ## Слоты: как netkit зовёт то, что живёт выше
60
+
61
+ Последняя ступень лестницы поднимает браузер, а браузер — чужой кит, который сам
62
+ зависит от сети. Прямой импорт дал бы цикл, поэтому направление разворачивается:
63
+ netkit **объявляет слот**, верхний слой **заполняет** его на своём импорте.
64
+
65
+ ```python
66
+ from netkit.providers import SLOT_BROWSER_MINT, register_provider
67
+ register_provider(SLOT_BROWSER_MINT, my_mint_session)
68
+ ```
69
+
70
+ Слоты со своим дефолтом (`json_store`, `file_lock`, `state_root`, `path_slug`,
71
+ `transport_factory`, `diagnose`, `egress_proxy`) никогда не роняют вызов — netkit
72
+ умеет их сам, верхний слой лишь уточняет. Слоты без дефолта (браузерные) при
73
+ обращении поднимают `ProviderMissing` с инструкцией: молчаливой деградации
74
+ «ступень тихо ничего не сделала» здесь нет.
75
+
76
+ Если в окружении стоит `librarykit`, его импорт заполняет все слоты сам —
77
+ отдельная регистрация не нужна.
78
+
79
+ ## Совместимость
80
+
81
+ `librarykit` остаётся фасадом: `librarykit.transport`, `librarykit.ladder`,
82
+ `librarykit.limit`, `librarykit.retry`, `librarykit.pagination`,
83
+ `librarykit.upload`, `librarykit.forms`, `librarykit.rpc`, `librarykit.stream`,
84
+ `librarykit.graphql`, `librarykit.errmap` реэкспортируют ТЕ ЖЕ объекты (не
85
+ копии) — `isinstance` / `except` / `is` работают через любой из путей.
86
+
87
+ ## Стоимость импорта
88
+
89
+ `import netkit` не исполняет ни одного подмодуля: ни httpx, ни stamina, ни
90
+ asyncio. Имена резолвятся по PEP 562 при первом обращении — платит тот, кому
91
+ нужно.
92
+
93
+ ## Установка
94
+
95
+ ```bash
96
+ pip install s-netkit # ядро: corekit + httpx + stamina
97
+ pip install s-netkit[ws] # + websockets для WS-транспорта
98
+ ```
@@ -0,0 +1,240 @@
1
+ """netkit — СЕТЕВОЙ слой китов. Всё, чем интеграция ходит наружу.
2
+
3
+ ФОРМА ГРАФА::
4
+
5
+ clikit adapterkit <- ветки-оболочки
6
+ \\ /
7
+ \\ /
8
+ librarykit <- ОРКЕСТРАТОР (сессии, браузер, склад)
9
+ |
10
+ netkit <- СЕТЬ (транспорт, лимиты, повторы)
11
+ |
12
+ corekit <- ОСНОВАНИЕ (значения и правила)
13
+
14
+ ЧТО СЮДА ПЕРЕЕХАЛО ИЗ librarykit И ПО КАКОМУ ПРИЗНАКУ. Ровно одно: **модуль
15
+ живёт разговором с чужим сервером**. Не «использует httpx», а именно отвечает за
16
+ доставку, темп и живучесть обмена:
17
+
18
+ * :mod:`netkit.transport` — исполнители запроса поверх httpx (async + sync
19
+ близнец), choke-point `HttpClient`/`SyncHttpClient`, REST-клиент к своему
20
+ backend, permissive-рецепты;
21
+ * :mod:`netkit.stream` — persistent-каналы (WebSocket) — `StreamTransport`;
22
+ * :mod:`netkit.rpc` — codec-слой RPC (`JsonCodec`/`PrefixedJsonCodec`);
23
+ * :mod:`netkit.graphql` — GraphQL-клиент поверх транспорта;
24
+ * :mod:`netkit.limit` — `RateLimiter` + token-bucket (проактивный темп);
25
+ * :mod:`netkit.retry` — политики повторов (header-driven и фиксированная);
26
+ * :mod:`netkit.ladder` — ЛЕСТНИЦА ДЕГРАДАЦИИ: чем выполнять запросы и чем
27
+ добывать состояние, с памятью ступени и событиями спуска;
28
+ * :mod:`netkit.pagination` / :mod:`netkit.upload` / :mod:`netkit.forms` —
29
+ листание ресурса, resumable-догрузка, form-urlencoded кодек;
30
+ * :mod:`netkit.errmap` — ответ сервера → доменная ошибка;
31
+ * :mod:`netkit.declare` — ОБЪЯВЛЕНИЕ транспорта (http/ws/rpc/graphql) с ЕДИНЫМ
32
+ поведением лимитов и повторов поверх любого вида.
33
+
34
+ ЧЕГО ЗДЕСЬ НЕТ И НЕ БУДЕТ: браузера, антибота, хранилища сессий, keyring,
35
+ шифрования, путей состояния. Всё, что живёт ВЫШЕ, приходит сюда СЛОТАМИ
36
+ (:mod:`netkit.providers`) — не импортом. Поэтому netkit ставится и работает без
37
+ оркестратора, а зависимости идут строго вниз: ``netkit → corekit``.
38
+
39
+ ЛЕНИВЫЙ ФАСАД. Корень пакета не исполняет НИ ОДНОГО подмодуля: `import netkit`
40
+ не тянет ни httpx, ни stamina, ни asyncio. Имена резолвятся по PEP 562 при
41
+ первом обращении (см. `_LAZY_NAMES` и `__getattr__` ниже) — платит тот, кому
42
+ нужно. Это не украшение: `librarykit` реэкспортирует отсюда десятки имён своими
43
+ жадными импортами, и любой жадный импорт здесь мгновенно вернул бы 264 мс httpx
44
+ в стоимость `import librarykit` (зафиксировано fitness-тестами обоих китов).
45
+
46
+ СОВМЕСТИМОСТЬ. Ни одно имя отсюда не «переехало» для потребителя: `librarykit`
47
+ и его подмодули (`librarykit.transport`, `librarykit.ladder`, `librarykit.limit`,
48
+ …) реэкспортируют ТЕ ЖЕ объекты (не копии), поэтому ``isinstance``/``except``/
49
+ ``is``-сравнения работают через любой из путей.
50
+ """
51
+ from __future__ import annotations
52
+
53
+ from importlib import import_module as _import_module
54
+ from importlib.util import find_spec as _find_spec
55
+ from typing import TYPE_CHECKING
56
+
57
+ if TYPE_CHECKING: # pragma: no cover — только статическая видимость для IDE/mypy
58
+ from typing import Any
59
+
60
+ __version__ = "0.0.1"
61
+
62
+ #: Ленивые ИМЕНА: имя в корне → модуль, из которого оно берётся. Все они входят в
63
+ #: `__all__`, поэтому `from netkit import *`, `getattr` и `dir()` видят полную
64
+ #: поверхность, ничего при этом не исполняя.
65
+ _LAZY_NAMES: dict[str, str] = {
66
+ # contract: формы «талии» сетевого слоя + реэкспорт значений основания
67
+ "PaginationMode": "netkit.contract",
68
+ "AuthMode": "netkit.contract",
69
+ "TransportKind": "netkit.contract",
70
+ "RequestExecutionMode": "netkit.contract",
71
+ "DEFAULT_TENANT": "netkit.contract",
72
+ "SessionRef": "netkit.contract",
73
+ "ExecutionContext": "netkit.contract",
74
+ "Creds": "netkit.contract",
75
+ "RequestBody": "netkit.contract",
76
+ "QuotaScope": "netkit.contract",
77
+ "QuotaInfo": "netkit.contract",
78
+ "LimitSpec": "netkit.contract",
79
+ "ResponseLike": "netkit.contract",
80
+ "Transport": "netkit.contract",
81
+ "SyncTransport": "netkit.contract",
82
+ "HttpRequestExecutor": "netkit.contract",
83
+ "SyncHttpRequestExecutor": "netkit.contract",
84
+ "StreamTransport": "netkit.contract",
85
+ "Codec": "netkit.contract",
86
+ "Auth": "netkit.contract",
87
+ "Refreshable": "netkit.contract",
88
+ "ProactiveRefreshable": "netkit.contract",
89
+ "ApplyRefreshable": "netkit.contract",
90
+ "SyncRefreshable": "netkit.contract",
91
+ "SyncProactiveRefreshable": "netkit.contract",
92
+ "ErrorMapper": "netkit.contract",
93
+ "Paginator": "netkit.contract",
94
+ # transport: исполнители запроса + choke-point'ы + REST-клиент
95
+ "HttpxTransport": "netkit.transport",
96
+ "HttpxRequestExecutor": "netkit.transport",
97
+ "HttpxSyncTransport": "netkit.transport",
98
+ "HttpxSyncRequestExecutor": "netkit.transport",
99
+ "HttpClient": "netkit.transport",
100
+ "SyncHttpClient": "netkit.transport",
101
+ "RestHttpClient": "netkit.transport",
102
+ "NoopAuth": "netkit.transport",
103
+ "PermissiveErrorMapper": "netkit.transport",
104
+ "build_permissive_http_client": "netkit.transport",
105
+ "build_permissive_sync_http_client": "netkit.transport",
106
+ "aclose_http_client": "netkit.transport",
107
+ "close_sync_http_client": "netkit.transport",
108
+ "RefreshCallback": "netkit.transport",
109
+ # limit / retry: темп и живучесть обмена
110
+ "LimitWindow": "netkit.limit",
111
+ "LimitPolicy": "netkit.limit",
112
+ "LimitScope": "netkit.limit",
113
+ "RateLimiter": "netkit.limit",
114
+ "RetryPolicy": "netkit.retry",
115
+ "DEFAULT_RETRY": "netkit.retry",
116
+ "SimpleRetryPolicy": "netkit.retry",
117
+ "DEFAULT_SIMPLE_RETRY": "netkit.retry",
118
+ # errmap: ответ сервера → доменная ошибка
119
+ "ErrorMap": "netkit.errmap",
120
+ "ErrorMapBuilder": "netkit.errmap",
121
+ "BodyRule": "netkit.errmap",
122
+ "ExcFactory": "netkit.errmap",
123
+ "AuthExpired": "netkit.errmap",
124
+ "build_error_map": "netkit.errmap",
125
+ "DEFAULT_ERROR_MAP": "netkit.errmap",
126
+ # rpc / stream / graphql: не-REST виды обмена
127
+ "JsonCodec": "netkit.rpc",
128
+ "PrefixedJsonCodec": "netkit.rpc",
129
+ "RpcClient": "netkit.rpc",
130
+ "StubStreamTransport": "netkit.stream",
131
+ "WebSocketsStreamTransport": "netkit.stream",
132
+ "StreamClosedError": "netkit.stream",
133
+ "GraphQLClient": "netkit.graphql",
134
+ # pagination / upload / forms: листание, догрузка, кодек тел
135
+ "CursorPaginator": "netkit.pagination",
136
+ "PageParams": "netkit.pagination",
137
+ "DEFAULT_PAGE_PARAMS": "netkit.pagination",
138
+ "ItemsExtractor": "netkit.pagination",
139
+ "CursorExtractor": "netkit.pagination",
140
+ "default_extract_items": "netkit.pagination",
141
+ "default_extract_cursor": "netkit.pagination",
142
+ "with_page_params": "netkit.pagination",
143
+ "FlatPage": "netkit.pagination",
144
+ "parse_flat_page": "netkit.pagination",
145
+ "next_flat_offset": "netkit.pagination",
146
+ "ChunkedUploader": "netkit.upload",
147
+ "UploadResult": "netkit.upload",
148
+ "FormCodec": "netkit.forms",
149
+ # declare: объявление транспорта + единое поведение лимитов и повторов
150
+ "TransportSpec": "netkit.declare",
151
+ "DeclaredTransport": "netkit.declare",
152
+ "declare": "netkit.declare",
153
+ "register_kind": "netkit.declare",
154
+ "kinds": "netkit.declare",
155
+ "UnknownTransportKind": "netkit.declare",
156
+ "KIND_HTTP": "netkit.declare",
157
+ "KIND_WS": "netkit.declare",
158
+ "KIND_RPC": "netkit.declare",
159
+ "KIND_GRAPHQL": "netkit.declare",
160
+ # providers: слоты верхнего слоя (браузер, склад, диагностика, egress)
161
+ "provider": "netkit.providers",
162
+ "register_provider": "netkit.providers",
163
+ "unregister_provider": "netkit.providers",
164
+ "registered_slots": "netkit.providers",
165
+ "ProviderMissing": "netkit.providers",
166
+ "SLOTS": "netkit.providers",
167
+ "SLOT_BROWSER_MINT": "netkit.providers",
168
+ "SLOT_BROWSER_PROVIDER": "netkit.providers",
169
+ "SLOT_ENGINE_PROFILE_DIR": "netkit.providers",
170
+ "SLOT_TRANSPORT_FACTORY": "netkit.providers",
171
+ "SLOT_DIAGNOSE": "netkit.providers",
172
+ "SLOT_EGRESS_PROXY": "netkit.providers",
173
+ "SLOT_JSON_STORE": "netkit.providers",
174
+ "SLOT_FILE_LOCK": "netkit.providers",
175
+ "SLOT_STATE_ROOT": "netkit.providers",
176
+ "SLOT_PATH_SLUG": "netkit.providers",
177
+ # ladder: лестница деградации (её поверхность полностью — в netkit.ladder)
178
+ "TransportLadder": "netkit.ladder",
179
+ "SyncTransportLadder": "netkit.ladder",
180
+ "ServiceSpec": "netkit.ladder",
181
+ "MinterLadder": "netkit.ladder",
182
+ "ExecutorStep": "netkit.ladder",
183
+ "MinterStep": "netkit.ladder",
184
+ "DegradationEvent": "netkit.ladder",
185
+ "subscribe_degradation": "netkit.ladder",
186
+ "unsubscribe_degradation": "netkit.ladder",
187
+ "emit_degradation": "netkit.ladder",
188
+ }
189
+
190
+ #: Подсказки по extras: модуль → что поставить, если внутри не хватило вендора.
191
+ _EXTRA_HINTS: dict[str, str] = {
192
+ "netkit.stream": "s-netkit[ws]",
193
+ }
194
+
195
+
196
+ def __getattr__(name: str) -> Any:
197
+ """Ленивый резолв имён корня (PEP 562).
198
+
199
+ Порядок: (1) карта ленивых ИМЁН `_LAZY_NAMES`; (2) РЕАЛЬНЫЙ подмодуль с таким
200
+ именем — чтобы `import netkit; netkit.transport.HttpClient` работало без
201
+ отдельного импорта подмодуля. Промах — обычная `AttributeError` (иначе
202
+ `hasattr()` у потребителя взрывался бы вместо возврата False).
203
+ """
204
+ target = _LAZY_NAMES.get(name)
205
+ if target is None:
206
+ if not name.startswith("_"):
207
+ try:
208
+ found = _find_spec(f"{__name__}.{name}") is not None
209
+ except (ImportError, ValueError):
210
+ found = False
211
+ if found:
212
+ module = _import_module(f"{__name__}.{name}")
213
+ globals()[name] = module
214
+ return module
215
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
216
+ try:
217
+ module = _import_module(target)
218
+ except ModuleNotFoundError as exc:
219
+ missing = exc.name or ""
220
+ if missing.startswith(__name__):
221
+ raise # сломан сам кит — не маскируем подсказкой про extras
222
+ hint = _EXTRA_HINTS.get(target)
223
+ raise ImportError(
224
+ f"{name!r} требует модуль {target!r}, но тому не хватает пакета {missing!r}."
225
+ + (f" Поставь extra: `pip install '{hint}'`." if hint else "")
226
+ ) from exc
227
+ value = getattr(module, name)
228
+ # Кэш в globals(): второй доступ идёт напрямую, мимо хука. Присваивание в dict
229
+ # атомарно, повторный импорт из другого потока вернёт ТОТ ЖЕ объект из
230
+ # sys.modules — гонка идемпотентна (важно под free-threading).
231
+ globals()[name] = value
232
+ return value
233
+
234
+
235
+ def __dir__() -> list[str]:
236
+ """Полная поверхность корня: жадные атрибуты + `__all__` + ленивые имена."""
237
+ return sorted(set(globals()) | set(__all__) | set(_LAZY_NAMES))
238
+
239
+
240
+ __all__ = ["__version__", *sorted(_LAZY_NAMES)]