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.
- s_netkit-0.0.1/.gitignore +50 -0
- s_netkit-0.0.1/.gitlab-ci.yml +32 -0
- s_netkit-0.0.1/CHANGELOG.md +54 -0
- s_netkit-0.0.1/LICENSE +21 -0
- s_netkit-0.0.1/PKG-INFO +120 -0
- s_netkit-0.0.1/README.md +98 -0
- s_netkit-0.0.1/netkit/__init__.py +240 -0
- s_netkit-0.0.1/netkit/contract.py +380 -0
- s_netkit-0.0.1/netkit/declare.py +329 -0
- s_netkit-0.0.1/netkit/errmap.py +357 -0
- s_netkit-0.0.1/netkit/forms.py +82 -0
- s_netkit-0.0.1/netkit/graphql.py +228 -0
- s_netkit-0.0.1/netkit/ladder/__init__.py +163 -0
- s_netkit-0.0.1/netkit/ladder/capabilities.py +172 -0
- s_netkit-0.0.1/netkit/ladder/core.py +410 -0
- s_netkit-0.0.1/netkit/ladder/events.py +145 -0
- s_netkit-0.0.1/netkit/ladder/gate.py +169 -0
- s_netkit-0.0.1/netkit/ladder/memory.py +342 -0
- s_netkit-0.0.1/netkit/ladder/minting.py +195 -0
- s_netkit-0.0.1/netkit/ladder/spec.py +225 -0
- s_netkit-0.0.1/netkit/ladder/steps.py +170 -0
- s_netkit-0.0.1/netkit/ladder/sync_core.py +146 -0
- s_netkit-0.0.1/netkit/limit.py +474 -0
- s_netkit-0.0.1/netkit/pagination.py +462 -0
- s_netkit-0.0.1/netkit/providers.py +406 -0
- s_netkit-0.0.1/netkit/retry.py +318 -0
- s_netkit-0.0.1/netkit/rpc.py +193 -0
- s_netkit-0.0.1/netkit/stream.py +186 -0
- s_netkit-0.0.1/netkit/transport/__init__.py +87 -0
- s_netkit-0.0.1/netkit/transport/_helpers.py +42 -0
- s_netkit-0.0.1/netkit/transport/http_client.py +446 -0
- s_netkit-0.0.1/netkit/transport/permissive.py +147 -0
- s_netkit-0.0.1/netkit/transport/rest_client.py +382 -0
- s_netkit-0.0.1/netkit/transport/transport_http.py +378 -0
- s_netkit-0.0.1/netkit/upload.py +206 -0
- s_netkit-0.0.1/pyproject.toml +76 -0
- s_netkit-0.0.1/tests/conftest.py +79 -0
- s_netkit-0.0.1/tests/test_declare.py +274 -0
- s_netkit-0.0.1/tests/test_errmap.py +284 -0
- s_netkit-0.0.1/tests/test_forms.py +89 -0
- s_netkit-0.0.1/tests/test_graphql.py +227 -0
- s_netkit-0.0.1/tests/test_ladder.py +932 -0
- s_netkit-0.0.1/tests/test_ladder_degradation_event.py +258 -0
- s_netkit-0.0.1/tests/test_lazy_facade.py +139 -0
- s_netkit-0.0.1/tests/test_limit.py +141 -0
- s_netkit-0.0.1/tests/test_pagination.py +479 -0
- s_netkit-0.0.1/tests/test_permissive_http.py +44 -0
- s_netkit-0.0.1/tests/test_providers.py +197 -0
- s_netkit-0.0.1/tests/test_retry.py +110 -0
- s_netkit-0.0.1/tests/test_rpc.py +221 -0
- s_netkit-0.0.1/tests/test_stream.py +170 -0
- s_netkit-0.0.1/tests/test_transport_client.py +413 -0
- s_netkit-0.0.1/tests/test_transport_rest.py +527 -0
- s_netkit-0.0.1/tests/test_transport_sync.py +261 -0
- 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.
|
s_netkit-0.0.1/PKG-INFO
ADDED
|
@@ -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
|
+
```
|
s_netkit-0.0.1/README.md
ADDED
|
@@ -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)]
|