s-clientkit 0.0.1__tar.gz → 0.2.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.
- {s_clientkit-0.0.1 → s_clientkit-0.2.0}/.gitignore +47 -47
- s_clientkit-0.2.0/.gitlab-ci.yml +101 -0
- s_clientkit-0.2.0/.importlinter +65 -0
- s_clientkit-0.2.0/.skillgateignore +12 -0
- {s_clientkit-0.0.1 → s_clientkit-0.2.0}/AGENTS.md +45 -45
- s_clientkit-0.2.0/CHANGELOG.md +71 -0
- {s_clientkit-0.0.1 → s_clientkit-0.2.0}/PKG-INFO +12 -8
- {s_clientkit-0.0.1 → s_clientkit-0.2.0}/README.md +8 -2
- s_clientkit-0.2.0/clientkit/__init__.py +126 -0
- s_clientkit-0.2.0/clientkit/_base.py +9 -0
- s_clientkit-0.2.0/clientkit/_candidates.py +9 -0
- s_clientkit-0.2.0/clientkit/_version.py +38 -0
- s_clientkit-0.2.0/clientkit/builder.py +9 -0
- s_clientkit-0.2.0/clientkit/declaration.py +31 -0
- s_clientkit-0.2.0/clientkit/errors.py +14 -0
- s_clientkit-0.2.0/clientkit/hidden.py +9 -0
- s_clientkit-0.2.0/clientkit/identity.py +31 -0
- s_clientkit-0.2.0/clientkit/ports.py +21 -0
- s_clientkit-0.2.0/clientkit/white.py +9 -0
- {s_clientkit-0.0.1 → s_clientkit-0.2.0}/docs/adr/0000-template.md +20 -20
- {s_clientkit-0.0.1 → s_clientkit-0.2.0}/docs/adr/README.md +30 -30
- {s_clientkit-0.0.1 → s_clientkit-0.2.0}/examples/hidden_service.toml +50 -50
- s_clientkit-0.2.0/pyproject.toml +79 -0
- s_clientkit-0.2.0/scripts/check_dist_leaks.py +180 -0
- s_clientkit-0.2.0/scripts/release_guard.py +138 -0
- {s_clientkit-0.0.1 → s_clientkit-0.2.0}/tests/test_hidden_holds_access.py +227 -227
- {s_clientkit-0.0.1 → s_clientkit-0.2.0}/tests/test_identity.py +155 -155
- s_clientkit-0.2.0/tests/test_limit_gate.py +101 -0
- s_clientkit-0.2.0/tests/test_ports_transport.py +60 -0
- {s_clientkit-0.0.1 → s_clientkit-0.2.0}/tests/test_same_call_both_kinds.py +305 -292
- {s_clientkit-0.0.1 → s_clientkit-0.2.0}/uv.lock +62 -28
- s_clientkit-0.0.1/CHANGELOG.md +0 -24
- s_clientkit-0.0.1/clientkit/__init__.py +0 -120
- s_clientkit-0.0.1/clientkit/_base.py +0 -160
- s_clientkit-0.0.1/clientkit/_candidates.py +0 -150
- s_clientkit-0.0.1/clientkit/_limit.py +0 -55
- s_clientkit-0.0.1/clientkit/builder.py +0 -65
- s_clientkit-0.0.1/clientkit/declaration.py +0 -338
- s_clientkit-0.0.1/clientkit/errors.py +0 -37
- s_clientkit-0.0.1/clientkit/hidden.py +0 -194
- s_clientkit-0.0.1/clientkit/identity.py +0 -249
- s_clientkit-0.0.1/clientkit/ports.py +0 -70
- s_clientkit-0.0.1/clientkit/white.py +0 -84
- s_clientkit-0.0.1/pyproject.toml +0 -61
- {s_clientkit-0.0.1 → s_clientkit-0.2.0}/LICENSE +0 -0
- {s_clientkit-0.0.1 → s_clientkit-0.2.0}/examples/white_service.toml +0 -0
- {s_clientkit-0.0.1 → s_clientkit-0.2.0}/tests/__init__.py +0 -0
- {s_clientkit-0.0.1 → s_clientkit-0.2.0}/tests/test_declaration.py +0 -0
|
@@ -1,47 +1,47 @@
|
|
|
1
|
-
# === atlas 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
|
|
1
|
+
# === atlas 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
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# СГЕНЕРИРОВАНО из devcontour/tools/kits/template/gitlab-ci.yml — не правь здесь.
|
|
2
|
+
# Правка идёт в шаблон, затем `python tools/kits/apply_template.py <кит>`.
|
|
3
|
+
# Сверка: `python tools/kits/apply_template.py <кит> --check` (код 1 при расхождении).
|
|
4
|
+
#
|
|
5
|
+
# Автопубликация на PyPI по семвер-тегу vX.Y.Z через Trusted Publishing (OIDC).
|
|
6
|
+
# Три стадии, и это не украшение: версии на PyPI неизменяемы, и шесть китов
|
|
7
|
+
# уезжали туда без единого теста (browserkit 0.0.18, netkit ≤0.0.12 — оба в бою).
|
|
8
|
+
# gates — тесты, линтер, слои, doctor: на каждый push и на тег
|
|
9
|
+
# release-guard — тег == версия во всех местах (pyproject/uv.lock/CHANGELOG)
|
|
10
|
+
# publish-pypi — только после двух первых; версия, уже лежащая на PyPI, не
|
|
11
|
+
# публикуется повторно (тег «догоняет» выпуск, а не роняет CI);
|
|
12
|
+
# после сборки и до обмена токена — check_dist_leaks.py
|
|
13
|
+
# (личные данные разработчика в собранном дистрибутиве)
|
|
14
|
+
# Токены НИГДЕ не хранятся: GitLab выдаёт короткоживущий OIDC-токен (PYPI_ID_TOKEN),
|
|
15
|
+
# он одноразово обменивается на PyPI API-токен. Издатель настраивается на pypi.org:
|
|
16
|
+
# Manage -> Publishing -> GitLab (namespace=cifropro1/products, project=clientkit,
|
|
17
|
+
# top-level pipeline file=.gitlab-ci.yml, environment — пусто).
|
|
18
|
+
# Дистрибутив на PyPI: s-clientkit.
|
|
19
|
+
stages: [test, guard, publish]
|
|
20
|
+
|
|
21
|
+
.uv:
|
|
22
|
+
image: ghcr.io/astral-sh/uv:python3.12-bookworm
|
|
23
|
+
|
|
24
|
+
gates:
|
|
25
|
+
extends: .uv
|
|
26
|
+
stage: test
|
|
27
|
+
rules:
|
|
28
|
+
- if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
|
|
29
|
+
- if: $CI_PIPELINE_SOURCE == "push"
|
|
30
|
+
script:
|
|
31
|
+
# dev-зависимости у китов объявлены двумя способами — extra `dev` и группа
|
|
32
|
+
# `[dependency-groups] dev`; --all-extras покрывает первое, группу добавляем
|
|
33
|
+
# только если она объявлена, иначе uv падает «Group dev is not defined».
|
|
34
|
+
- uv sync --all-extras $(python3 -c "import tomllib;print('--group dev' if 'dev' in tomllib.load(open('pyproject.toml','rb')).get('dependency-groups',{}) else '')")
|
|
35
|
+
- uv run pytest tests/ -q
|
|
36
|
+
# Линтер по ВСЕМУ пакету, а не по списку файлов: сужение до «файлов волны 0»
|
|
37
|
+
# держалось девять китов и ни разу не было расширено — сужение приживается,
|
|
38
|
+
# расширение нет.
|
|
39
|
+
- uv run ruff check .
|
|
40
|
+
# Границы слоёв: контракт в .importlinter (ставится тем же шаблоном).
|
|
41
|
+
# — доп. пакеты через --with (пусто у большинства
|
|
42
|
+
# китов; adapterkit — пример кита, которому нужен сосед на прогоне
|
|
43
|
+
# lint-imports, см. apply_template.py --extra-with).
|
|
44
|
+
- uv run --with import-linter lint-imports
|
|
45
|
+
# Гейт 1, часть «согласованность локальных venv» (kitsctl doctor). Скрипт
|
|
46
|
+
# раздаётся шаблоном не всем: в раннере видно ОДИН репозиторий, соседних
|
|
47
|
+
# venv'ов там нет, — поэтому шаг условный и молча пропускается, когда
|
|
48
|
+
# инструмента у кита нет. Настоящее место doctor — локальный прогон перед
|
|
49
|
+
# тегом; см. docs/kits/SAFETY-NET.md, «Известные ограничения».
|
|
50
|
+
- 'if [ -f scripts/kitsctl.py ]; then python3 scripts/kitsctl.py doctor; else echo "kitsctl doctor — инструмента нет в репозитории, пропуск (см. SAFETY-NET.md)"; fi'
|
|
51
|
+
|
|
52
|
+
release-guard:
|
|
53
|
+
extends: .uv
|
|
54
|
+
stage: guard
|
|
55
|
+
needs: [gates]
|
|
56
|
+
rules:
|
|
57
|
+
- if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
|
|
58
|
+
script:
|
|
59
|
+
- python3 scripts/release_guard.py "$CI_COMMIT_TAG"
|
|
60
|
+
|
|
61
|
+
publish-pypi:
|
|
62
|
+
extends: .uv
|
|
63
|
+
stage: publish
|
|
64
|
+
needs: [release-guard]
|
|
65
|
+
rules:
|
|
66
|
+
- if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
|
|
67
|
+
id_tokens:
|
|
68
|
+
PYPI_ID_TOKEN:
|
|
69
|
+
aud: pypi
|
|
70
|
+
script:
|
|
71
|
+
- |
|
|
72
|
+
DIST=$(python3 -c "import tomllib;print(tomllib.load(open('pyproject.toml','rb'))['project']['name'])")
|
|
73
|
+
VER="${CI_COMMIT_TAG#v}"
|
|
74
|
+
if python3 - "$DIST" "$VER" <<'PY'
|
|
75
|
+
import json, sys, urllib.request
|
|
76
|
+
dist, ver = sys.argv[1], sys.argv[2]
|
|
77
|
+
try:
|
|
78
|
+
data = json.load(urllib.request.urlopen(f"https://pypi.org/pypi/{dist}/json"))
|
|
79
|
+
except Exception:
|
|
80
|
+
sys.exit(1)
|
|
81
|
+
sys.exit(0 if ver in data.get("releases", {}) else 1)
|
|
82
|
+
PY
|
|
83
|
+
then echo "версия $VER уже на PyPI — публикацию пропускаем"; exit 0; fi
|
|
84
|
+
- uv build
|
|
85
|
+
# Последний рубеж перед необратимым шагом (версии на PyPI неизменяемы,
|
|
86
|
+
# yank — не удаление). Прецедент — leasekit: реальный email и домашний
|
|
87
|
+
# путь разработчика уехали в публичный индекс из тестовых фикстур. Ловит
|
|
88
|
+
# по классам, не по строкам; см. docs/kits/SAFETY-NET.md.
|
|
89
|
+
- python3 scripts/check_dist_leaks.py dist/
|
|
90
|
+
- |
|
|
91
|
+
export UV_PUBLISH_TOKEN=$(python3 - <<'PY'
|
|
92
|
+
import json, os, urllib.request
|
|
93
|
+
req = urllib.request.Request(
|
|
94
|
+
"https://pypi.org/_/oidc/mint-token",
|
|
95
|
+
data=json.dumps({"token": os.environ["PYPI_ID_TOKEN"]}).encode(),
|
|
96
|
+
headers={"Content-Type": "application/json"},
|
|
97
|
+
)
|
|
98
|
+
print(json.load(urllib.request.urlopen(req))["token"])
|
|
99
|
+
PY
|
|
100
|
+
)
|
|
101
|
+
- uv publish
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# СГЕНЕРИРОВАНО из devcontour/tools/kits/template/importlinter.ini — не правь
|
|
2
|
+
# здесь. Правка идёт в шаблон, затем `python tools/kits/apply_template.py <кит>`.
|
|
3
|
+
#
|
|
4
|
+
# Заготовка контракта слоёв. Запуск: `uv run --with import-linter lint-imports`.
|
|
5
|
+
#
|
|
6
|
+
# Контракт ниже — минимум, который годится ЛЮБОМУ киту и не требует ручной
|
|
7
|
+
# настройки: «clientkit не импортирует те киты экосистемы, которых нет в его
|
|
8
|
+
# объявленных зависимостях». Скрытая зависимость — это кит, который работает у
|
|
9
|
+
# автора (сосед стоит в venv) и разваливается у потребителя на первом импорте;
|
|
10
|
+
# именно так ломались pypi-publish и gemini-chat.
|
|
11
|
+
#
|
|
12
|
+
# Список forbidden_modules подставлен установщиком из реестра китов на момент
|
|
13
|
+
# установки: все известные пакеты китов МИНУС собственный МИНУС объявленные в
|
|
14
|
+
# pyproject зависимости. Появилась новая честная зависимость — объяви её в
|
|
15
|
+
# pyproject и переустанови шаблон, а не правь этот файл.
|
|
16
|
+
#
|
|
17
|
+
# Собственные контракты кита (layers/eager_forbidden/declared_dependencies,
|
|
18
|
+
# как у adapterkit) дописываются НИЖЕ отдельными секциями прямо в этом файле.
|
|
19
|
+
# Установщик сверяет файл целиком, поэтому `--check` после такой правки
|
|
20
|
+
# закономерно покажет расхождение с каноном — это ожидаемо и осознанно
|
|
21
|
+
# игнорируется в отчёте ревью, см. docs/kits/SAFETY-NET.md, «Что делать при
|
|
22
|
+
# расхождении» (п.2).
|
|
23
|
+
|
|
24
|
+
[importlinter]
|
|
25
|
+
# include_external_packages нужен: forbidden_modules перечисляет пакеты,
|
|
26
|
+
# внешние относительно root_packages.
|
|
27
|
+
include_external_packages = True
|
|
28
|
+
root_packages =
|
|
29
|
+
clientkit
|
|
30
|
+
exclude_type_checking_imports = True
|
|
31
|
+
|
|
32
|
+
[importlinter:contract:no-undeclared-kits]
|
|
33
|
+
name = clientkit не импортирует киты, которых нет в его зависимостях
|
|
34
|
+
type = forbidden
|
|
35
|
+
source_modules =
|
|
36
|
+
clientkit
|
|
37
|
+
forbidden_modules =
|
|
38
|
+
accountpoolkit
|
|
39
|
+
agentskit
|
|
40
|
+
aichatkit
|
|
41
|
+
authkit_contracts
|
|
42
|
+
authkit_server
|
|
43
|
+
b24kit
|
|
44
|
+
browserkit
|
|
45
|
+
chatkit
|
|
46
|
+
clikit
|
|
47
|
+
emojikit
|
|
48
|
+
gitkit
|
|
49
|
+
installerkit
|
|
50
|
+
iokit
|
|
51
|
+
issuekit
|
|
52
|
+
leasekit
|
|
53
|
+
librarykit
|
|
54
|
+
maxkit
|
|
55
|
+
modelkit
|
|
56
|
+
ormkit
|
|
57
|
+
persistkit
|
|
58
|
+
servicekit
|
|
59
|
+
sessionkit
|
|
60
|
+
skillkit
|
|
61
|
+
socialkit
|
|
62
|
+
storagekit
|
|
63
|
+
telemetrykit
|
|
64
|
+
totp_client
|
|
65
|
+
worker_service
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Исключения для секрет-скана s-skillgate перед push. Глобы — от корня репо.
|
|
2
|
+
#
|
|
3
|
+
# scripts/check_dist_leaks.py — сам гейт утечки личных данных (сетка
|
|
4
|
+
# безопасности китов, docs/kits/SAFETY-NET.md в devcontour), копия из
|
|
5
|
+
# tools/kits/template/check_dist_leaks.py. Ловит по классам, не по строкам:
|
|
6
|
+
# windows-путь с числовым именем пользователя, личный ящик на публичном
|
|
7
|
+
# провайдере, юникс-домашний путь. Regex-паттерны и пример подсказки
|
|
8
|
+
# (windows-путь с числовым account, «/home/user») по определению совпадают
|
|
9
|
+
# с собственным денилистом — иначе гейт не проверял бы то, ради чего написан (та же
|
|
10
|
+
# ситуация, что и с tests/golden/ у самого check_dist_leaks.py). Файл
|
|
11
|
+
# копируется буквально из канона — правки идут в шаблон, не сюда.
|
|
12
|
+
scripts/check_dist_leaks.py
|
|
@@ -1,45 +1,45 @@
|
|
|
1
|
-
# AGENTS.md — clientkit
|
|
2
|
-
|
|
3
|
-
> Контекст для AI-ассистентов (Claude Code, ChatGPT, Cursor и т.п.), работающих
|
|
4
|
-
> над этим проектом.
|
|
5
|
-
|
|
6
|
-
## Что это
|
|
7
|
-
|
|
8
|
-
(заполнить one-line)
|
|
9
|
-
|
|
10
|
-
## Atlas
|
|
11
|
-
|
|
12
|
-
Проект зарегистрирован в Atlas-БД (Atlas). Карточка:
|
|
13
|
-
|
|
14
|
-
```sh
|
|
15
|
-
atlas project get clientkit
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
Любые изменения метаданных (приоритет, статус, теги) — через atlas CLI:
|
|
19
|
-
|
|
20
|
-
- `atlas project update clientkit --priority P0` — поменять приоритет
|
|
21
|
-
- `atlas project tag add clientkit -t domain:<slug>` — добавить тег
|
|
22
|
-
- `atlas project move clientkit --to-type <type>` — конвертировать тип
|
|
23
|
-
|
|
24
|
-
## Тип / Статус (на момент создания)
|
|
25
|
-
|
|
26
|
-
- type=`kit`, status=`active`, priority=`P2`
|
|
27
|
-
|
|
28
|
-
## Правила работы
|
|
29
|
-
|
|
30
|
-
- Все исходные тексты, документы, код проекта — в этом репо.
|
|
31
|
-
- Чувствительные данные (`.env`, токены, ключи) — игнорируются `.gitignore`.
|
|
32
|
-
- AI-ассистенту разрешено: читать, генерировать, редактировать в этом репо.
|
|
33
|
-
|
|
34
|
-
## Канонические команды
|
|
35
|
-
|
|
36
|
-
- `atlas project get clientkit` — карточка проекта
|
|
37
|
-
- `atlas task list --project clientkit` — задачи проекта (когда W7
|
|
38
|
-
волна будет реализована)
|
|
39
|
-
|
|
40
|
-
<!-- atlas:usage:start -->
|
|
41
|
-
## Управление проектом — через Atlas
|
|
42
|
-
|
|
43
|
-
Этот проект ведётся в Atlas (личная PM-система портфеля). Для задач/проектов/эпиков/бэкапов
|
|
44
|
-
используй CLI `atlas` и вызывай навык `atlas` — вся логика и роутинг внутри навыка.
|
|
45
|
-
<!-- atlas:usage:end -->
|
|
1
|
+
# AGENTS.md — clientkit
|
|
2
|
+
|
|
3
|
+
> Контекст для AI-ассистентов (Claude Code, ChatGPT, Cursor и т.п.), работающих
|
|
4
|
+
> над этим проектом.
|
|
5
|
+
|
|
6
|
+
## Что это
|
|
7
|
+
|
|
8
|
+
(заполнить one-line)
|
|
9
|
+
|
|
10
|
+
## Atlas
|
|
11
|
+
|
|
12
|
+
Проект зарегистрирован в Atlas-БД (Atlas). Карточка:
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
atlas project get clientkit
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Любые изменения метаданных (приоритет, статус, теги) — через atlas CLI:
|
|
19
|
+
|
|
20
|
+
- `atlas project update clientkit --priority P0` — поменять приоритет
|
|
21
|
+
- `atlas project tag add clientkit -t domain:<slug>` — добавить тег
|
|
22
|
+
- `atlas project move clientkit --to-type <type>` — конвертировать тип
|
|
23
|
+
|
|
24
|
+
## Тип / Статус (на момент создания)
|
|
25
|
+
|
|
26
|
+
- type=`kit`, status=`active`, priority=`P2`
|
|
27
|
+
|
|
28
|
+
## Правила работы
|
|
29
|
+
|
|
30
|
+
- Все исходные тексты, документы, код проекта — в этом репо.
|
|
31
|
+
- Чувствительные данные (`.env`, токены, ключи) — игнорируются `.gitignore`.
|
|
32
|
+
- AI-ассистенту разрешено: читать, генерировать, редактировать в этом репо.
|
|
33
|
+
|
|
34
|
+
## Канонические команды
|
|
35
|
+
|
|
36
|
+
- `atlas project get clientkit` — карточка проекта
|
|
37
|
+
- `atlas task list --project clientkit` — задачи проекта (когда W7
|
|
38
|
+
волна будет реализована)
|
|
39
|
+
|
|
40
|
+
<!-- atlas:usage:start -->
|
|
41
|
+
## Управление проектом — через Atlas
|
|
42
|
+
|
|
43
|
+
Этот проект ведётся в Atlas (личная PM-система портфеля). Для задач/проектов/эпиков/бэкапов
|
|
44
|
+
используй CLI `atlas` и вызывай навык `atlas` — вся логика и роутинг внутри навыка.
|
|
45
|
+
<!-- atlas:usage:end -->
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Changelog — s-clientkit
|
|
2
|
+
|
|
3
|
+
## [0.2.0] — 2026-09-08
|
|
4
|
+
|
|
5
|
+
Слияние в adapterkit (волна 5, задача 2, Atlas #2710): у кита не набралось
|
|
6
|
+
живой массы под отдельный релизный цикл — единственный внешний потребитель
|
|
7
|
+
(reverse-factory) ходит только за `clientkit.identity`, а адреса-кандидаты уже
|
|
8
|
+
были тонкой обёрткой над `adapterkit.endpoints.EndpointRegistry`.
|
|
9
|
+
|
|
10
|
+
### Breaking
|
|
11
|
+
|
|
12
|
+
- Весь код (декларация сервиса, сборка белого/скрытого клиента, каталог имён
|
|
13
|
+
сервисов, адреса-кандидаты) переехал в `adapterkit.clientkit`. Пакет
|
|
14
|
+
`clientkit` — ТЕПЕРЬ ТОНКИЙ АЛИАС: каждый модуль (`clientkit.identity`,
|
|
15
|
+
`clientkit.declaration`, `clientkit.builder`, `clientkit.hidden`,
|
|
16
|
+
`clientkit.white`, `clientkit.ports`, `clientkit.errors`, `clientkit._base`,
|
|
17
|
+
`clientkit._candidates`) реэкспортирует одноимённый модуль
|
|
18
|
+
`adapterkit.clientkit.*`. Публичная поверхность (`from clientkit import
|
|
19
|
+
build_from_toml`, `from clientkit.identity import CATALOG`, …) сохранена 1:1.
|
|
20
|
+
- `s-adapterkit` — теперь ОБЯЗАТЕЛЬНАЯ базовая зависимость (`>=0.2.0,<0.3`,
|
|
21
|
+
extra `client` внутри неё тянет `s-netkit`): даже `clientkit.identity` физически
|
|
22
|
+
лежит в `adapterkit.clientkit.identity`.
|
|
23
|
+
- Extra `endpoints` — теперь no-op (`s-adapterkit` и так обязателен, ставить
|
|
24
|
+
отдельно больше нечего). Оставлен пустым, а не удалён, чтобы старый
|
|
25
|
+
`pip install s-clientkit[endpoints]` не падал `ExtraNotFound`.
|
|
26
|
+
- Extra `hidden` теперь форвардит `s-adapterkit[client-hidden]` вместо прямого
|
|
27
|
+
`s-authkit-client` (тот же пакет приезжает транзитивно).
|
|
28
|
+
|
|
29
|
+
## [0.1.0] — 2026-09-06
|
|
30
|
+
|
|
31
|
+
Волна китов-оснований: транспорт только через netkit, свой `TokenBucket` удалён.
|
|
32
|
+
|
|
33
|
+
### Breaking
|
|
34
|
+
- `s-netkit` теперь ОБЯЗАТЕЛЬНАЯ базовая зависимость (`>=0.1.1,<0.2`), extra
|
|
35
|
+
`net` убран — «поставил кит без netkit» больше не собирается вовсе.
|
|
36
|
+
- `clientkit._limit.TokenBucket` удалён. Гейт лимита в
|
|
37
|
+
`ServiceClientBase` — теперь `netkit.limit.RateLimiter` (тот же смысл:
|
|
38
|
+
«не чаще `rate` в секунду при ёмкости `burst`», объявленный в `[limits]`
|
|
39
|
+
декларации сервиса).
|
|
40
|
+
- `resolve_transport()` больше НЕ откатывается на голый `httpx.AsyncClient()`,
|
|
41
|
+
если под netkit что-то пошло не так, — молчаливая деградация с
|
|
42
|
+
`except Exception` заменена на `except ImportError`. Без netkit — отказ
|
|
43
|
+
`clientkit.errors.TransportUnavailable` с рецептом (`pip install s-netkit`
|
|
44
|
+
либо свой `transport=`), а не «тихо поехали на другом транспорте».
|
|
45
|
+
|
|
46
|
+
### Changed
|
|
47
|
+
- `clientkit.errors` — новый `TransportUnavailable` (та же доменная иерархия,
|
|
48
|
+
код `TRANSPORT_UNAVAILABLE`).
|
|
49
|
+
|
|
50
|
+
## 0.0.1 — кит-сборщик (задача #1423)
|
|
51
|
+
|
|
52
|
+
Первый выпуск.
|
|
53
|
+
|
|
54
|
+
- `clientkit.declaration` — декларация сервиса как ДАННЫЕ (`ServiceDecl` и
|
|
55
|
+
соседи, frozen dataclass'ы, round-trip в обычный dict, загрузка из TOML,
|
|
56
|
+
валидация при загрузке).
|
|
57
|
+
- `clientkit.builder` — `build_client` / `build_from_toml`: единственная развилка
|
|
58
|
+
кита, и она смотрит на `decl.kind`.
|
|
59
|
+
- `clientkit.white` — белый API: заголовок авторизации (`api_key`/`bearer`/
|
|
60
|
+
`oauth` с проактивным TTL) + прямой запрос. Ни сессий, ни чеканки, ни антибота.
|
|
61
|
+
- `clientkit.hidden` — скрытый API: лестница доступа из декларации, чеканка через
|
|
62
|
+
порт слоя доступа, ротация адресов-кандидатов, разбор отказов через
|
|
63
|
+
`corekit.diagnosis.access`.
|
|
64
|
+
- `clientkit._candidates` — книга адресов поверх `adapterkit.EndpointRegistry`
|
|
65
|
+
либо равнозначная встроенная (совпадение проверено тестом).
|
|
66
|
+
- `clientkit.ports` — `RequestPort` / `SecretLookup`; умолчание транспорта
|
|
67
|
+
(netkit → httpx) резолвится лениво.
|
|
68
|
+
|
|
69
|
+
Главное свойство: потребитель зовёт `await client.call("me")` одинаково в обоих
|
|
70
|
+
мирах, при этом белый путь не поднимает ни сессий, ни чеканки, ни антибота и
|
|
71
|
+
делает ровно один запрос.
|
|
@@ -1,27 +1,30 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: s-clientkit
|
|
3
|
-
Version: 0.0
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: КИТ-АЛИАС: код-сборщик клиента переехал в adapterkit.clientkit (волна 5, задача 2, Atlas #2710) — этот пакет тонко реэкспортирует его модули под прежними именами (clientkit.identity и остальные).
|
|
5
5
|
Author: Dmitry
|
|
6
6
|
License: MIT
|
|
7
7
|
License-File: LICENSE
|
|
8
8
|
Requires-Python: >=3.11
|
|
9
|
+
Requires-Dist: s-adapterkit[client]<0.3,>=0.2.0
|
|
9
10
|
Requires-Dist: s-corekit>=0.0.2
|
|
10
11
|
Provides-Extra: dev
|
|
11
12
|
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
12
13
|
Requires-Dist: pytest>=8; extra == 'dev'
|
|
13
14
|
Requires-Dist: ruff>=0.8; extra == 'dev'
|
|
14
15
|
Provides-Extra: endpoints
|
|
15
|
-
Requires-Dist: s-adapterkit>=0.1.8; extra == 'endpoints'
|
|
16
16
|
Provides-Extra: hidden
|
|
17
|
-
Requires-Dist: s-
|
|
17
|
+
Requires-Dist: s-adapterkit[client-hidden]<0.3,>=0.2.0; extra == 'hidden'
|
|
18
18
|
Provides-Extra: http
|
|
19
19
|
Requires-Dist: httpx>=0.27; extra == 'http'
|
|
20
|
-
Provides-Extra: net
|
|
21
|
-
Requires-Dist: s-netkit>=0.0.2; extra == 'net'
|
|
22
20
|
Description-Content-Type: text/markdown
|
|
23
21
|
|
|
24
|
-
# s-clientkit —
|
|
22
|
+
# s-clientkit — тонкий алиас поверх adapterkit.clientkit
|
|
23
|
+
|
|
24
|
+
> **Волна 5, задача 2 (Atlas #2710):** живой код переехал в
|
|
25
|
+
> `adapterkit.clientkit` — здесь остались тонкие модули-реэкспорты. Публичная
|
|
26
|
+
> поверхность (всё, что описано ниже) не изменилась; изменился только адрес
|
|
27
|
+
> кода под ней.
|
|
25
28
|
|
|
26
29
|
Готовый клиент сервиса **собирается по декларации**, а не пишется заново.
|
|
27
30
|
|
|
@@ -72,7 +75,8 @@ await hidden.call("me") # скрытый API с сессией и лестн
|
|
|
72
75
|
## Чего в ките нет
|
|
73
76
|
|
|
74
77
|
* **своего HTTP** — «чем ходить» приходит аргументом (`RequestPort`), умолчание
|
|
75
|
-
(netkit
|
|
78
|
+
(только netkit; без него — `TransportUnavailable` с рецептом установки) резолвится
|
|
79
|
+
лениво;
|
|
76
80
|
* **своего браузера** — чеканка приходит через порт слоя доступа;
|
|
77
81
|
* **своей таксономии отказов** — диагноз ставит `corekit.diagnosis.access`, та же
|
|
78
82
|
функция, что у живой пробы и у реестра эндпоинтов.
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
# s-clientkit —
|
|
1
|
+
# s-clientkit — тонкий алиас поверх adapterkit.clientkit
|
|
2
|
+
|
|
3
|
+
> **Волна 5, задача 2 (Atlas #2710):** живой код переехал в
|
|
4
|
+
> `adapterkit.clientkit` — здесь остались тонкие модули-реэкспорты. Публичная
|
|
5
|
+
> поверхность (всё, что описано ниже) не изменилась; изменился только адрес
|
|
6
|
+
> кода под ней.
|
|
2
7
|
|
|
3
8
|
Готовый клиент сервиса **собирается по декларации**, а не пишется заново.
|
|
4
9
|
|
|
@@ -49,7 +54,8 @@ await hidden.call("me") # скрытый API с сессией и лестн
|
|
|
49
54
|
## Чего в ките нет
|
|
50
55
|
|
|
51
56
|
* **своего HTTP** — «чем ходить» приходит аргументом (`RequestPort`), умолчание
|
|
52
|
-
(netkit
|
|
57
|
+
(только netkit; без него — `TransportUnavailable` с рецептом установки) резолвится
|
|
58
|
+
лениво;
|
|
53
59
|
* **своего браузера** — чеканка приходит через порт слоя доступа;
|
|
54
60
|
* **своей таксономии отказов** — диагноз ставит `corekit.diagnosis.access`, та же
|
|
55
61
|
функция, что у живой пробы и у реестра эндпоинтов.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""clientkit — ТОНКИЙ АЛИАС поверх ``adapterkit.clientkit`` (волна 5, задача 2,
|
|
2
|
+
Atlas #2710).
|
|
3
|
+
|
|
4
|
+
БЫЛО СВОИМ КИТОМ-СБОРЩИКОМ, СТАЛО АЛИАСОМ. Живой код (декларация сервиса,
|
|
5
|
+
сборка белого/скрытого клиента, каталог имён сервисов, адреса-кандидаты)
|
|
6
|
+
переехал в :mod:`adapterkit.clientkit` — рядом с реестром адресов
|
|
7
|
+
(:class:`adapterkit.endpoints.EndpointRegistry`), которым адреса-кандидаты и
|
|
8
|
+
раньше пользовались (см. историю ``clientkit._candidates``). Единственный
|
|
9
|
+
известный внешний потребитель на момент переноса — reverse-factory, и он ходит
|
|
10
|
+
ТОЛЬКО за :mod:`clientkit.identity` (``CATALOG``/``ServiceIdentity``/
|
|
11
|
+
``UnknownService``/``canonical``): этот путь работает как раньше, включая
|
|
12
|
+
установку без extra-флагов.
|
|
13
|
+
|
|
14
|
+
Каждый модуль здесь (``clientkit.identity``, ``clientkit.declaration``,
|
|
15
|
+
``clientkit.builder``, ``clientkit.hidden``, ``clientkit.white``,
|
|
16
|
+
``clientkit.ports``, ``clientkit.errors``, ``clientkit._base``,
|
|
17
|
+
``clientkit._candidates``) — тонкий файл-реэкспорт одноимённого модуля
|
|
18
|
+
``adapterkit.clientkit.*``: ``import clientkit.builder`` и т.п. продолжает
|
|
19
|
+
работать, даже если что-то в экосистеме импортирует их напрямую, а не через
|
|
20
|
+
корень пакета.
|
|
21
|
+
|
|
22
|
+
ОДИН ВЫЗОВ НА ОБА МИРА (без изменений)::
|
|
23
|
+
|
|
24
|
+
from clientkit import build_from_toml
|
|
25
|
+
|
|
26
|
+
white = build_from_toml("examples/white_service.toml", secrets=os.environ.get)
|
|
27
|
+
hidden = build_from_toml("examples/hidden_service.toml", session_store=store)
|
|
28
|
+
|
|
29
|
+
await white.call("me") # белый REST с api-ключом
|
|
30
|
+
await hidden.call("me") # скрытый API с сессией и лестницей деградации
|
|
31
|
+
"""
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import importlib
|
|
35
|
+
from typing import TYPE_CHECKING
|
|
36
|
+
|
|
37
|
+
if TYPE_CHECKING: # pragma: no cover — для тайпчекера/IDE
|
|
38
|
+
from adapterkit.clientkit._base import ServiceClient
|
|
39
|
+
from adapterkit.clientkit.builder import build_client, build_from_toml
|
|
40
|
+
from adapterkit.clientkit.declaration import (
|
|
41
|
+
AccessRecipe,
|
|
42
|
+
AuthDecl,
|
|
43
|
+
AuthScheme,
|
|
44
|
+
EndpointDecl,
|
|
45
|
+
LimitDecl,
|
|
46
|
+
ServiceDecl,
|
|
47
|
+
ServiceKind,
|
|
48
|
+
decl_from_data,
|
|
49
|
+
decl_to_data,
|
|
50
|
+
load_toml,
|
|
51
|
+
)
|
|
52
|
+
from adapterkit.clientkit.errors import AccessUnavailable, ClientBuildError, OperationFailed
|
|
53
|
+
from adapterkit.clientkit.hidden import HiddenApiClient
|
|
54
|
+
from adapterkit.clientkit.identity import (
|
|
55
|
+
CATALOG,
|
|
56
|
+
Instancing,
|
|
57
|
+
ServiceIdentity,
|
|
58
|
+
UnknownService,
|
|
59
|
+
canonical,
|
|
60
|
+
resolve_service,
|
|
61
|
+
suggest_service,
|
|
62
|
+
)
|
|
63
|
+
from adapterkit.clientkit.ports import RequestPort, SecretLookup
|
|
64
|
+
from adapterkit.clientkit.white import WhiteApiClient
|
|
65
|
+
|
|
66
|
+
#: имя -> модуль-владелец В adapterkit.clientkit (не в этом пакете — этот
|
|
67
|
+
#: пакет сам ничего не реализует). `hidden` в этой таблице стоит РЯДОМ с
|
|
68
|
+
#: `white`, но цена у них разная: обращение к `HiddenApiClient` подтягивает
|
|
69
|
+
#: слой доступа, а к `WhiteApiClient` — нет. Ленивость здесь и защищает эту
|
|
70
|
+
#: разницу.
|
|
71
|
+
_LAZY_NAMES: dict[str, str] = {
|
|
72
|
+
"build_client": "adapterkit.clientkit.builder",
|
|
73
|
+
"build_from_toml": "adapterkit.clientkit.builder",
|
|
74
|
+
"ServiceClient": "adapterkit.clientkit._base",
|
|
75
|
+
# ИМЯ сервиса — тоже данные, и притом нужные тем, кто базы не держит:
|
|
76
|
+
# навыку назвать свою сессию, записи трафика — положить снимок.
|
|
77
|
+
"ServiceIdentity": "adapterkit.clientkit.identity",
|
|
78
|
+
"Instancing": "adapterkit.clientkit.identity",
|
|
79
|
+
"UnknownService": "adapterkit.clientkit.identity",
|
|
80
|
+
"CATALOG": "adapterkit.clientkit.identity",
|
|
81
|
+
"canonical": "adapterkit.clientkit.identity",
|
|
82
|
+
"resolve_service": "adapterkit.clientkit.identity",
|
|
83
|
+
"suggest_service": "adapterkit.clientkit.identity",
|
|
84
|
+
# декларация — данные
|
|
85
|
+
"ServiceDecl": "adapterkit.clientkit.declaration",
|
|
86
|
+
"ServiceKind": "adapterkit.clientkit.declaration",
|
|
87
|
+
"AuthDecl": "adapterkit.clientkit.declaration",
|
|
88
|
+
"AuthScheme": "adapterkit.clientkit.declaration",
|
|
89
|
+
"LimitDecl": "adapterkit.clientkit.declaration",
|
|
90
|
+
"EndpointDecl": "adapterkit.clientkit.declaration",
|
|
91
|
+
"AccessRecipe": "adapterkit.clientkit.declaration",
|
|
92
|
+
"decl_from_data": "adapterkit.clientkit.declaration",
|
|
93
|
+
"decl_to_data": "adapterkit.clientkit.declaration",
|
|
94
|
+
"load_toml": "adapterkit.clientkit.declaration",
|
|
95
|
+
# порты
|
|
96
|
+
"RequestPort": "adapterkit.clientkit.ports",
|
|
97
|
+
"SecretLookup": "adapterkit.clientkit.ports",
|
|
98
|
+
# клиенты (обычно не нужны напрямую — собирает build_client)
|
|
99
|
+
"WhiteApiClient": "adapterkit.clientkit.white",
|
|
100
|
+
"HiddenApiClient": "adapterkit.clientkit.hidden",
|
|
101
|
+
# ошибки
|
|
102
|
+
"ClientBuildError": "adapterkit.clientkit.errors",
|
|
103
|
+
"AccessUnavailable": "adapterkit.clientkit.errors",
|
|
104
|
+
"OperationFailed": "adapterkit.clientkit.errors",
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
__all__ = ["__version__", *sorted(_LAZY_NAMES)]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def __getattr__(name: str):
|
|
111
|
+
if name == "__version__":
|
|
112
|
+
from ._version import resolve_version
|
|
113
|
+
|
|
114
|
+
value = resolve_version()
|
|
115
|
+
globals()["__version__"] = value
|
|
116
|
+
return value
|
|
117
|
+
module_name = _LAZY_NAMES.get(name)
|
|
118
|
+
if module_name is None:
|
|
119
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
120
|
+
value = getattr(importlib.import_module(module_name), name)
|
|
121
|
+
globals()[name] = value
|
|
122
|
+
return value
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def __dir__() -> list[str]:
|
|
126
|
+
return sorted({*globals(), *_LAZY_NAMES})
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Тонкий алиас — код переехал в ``adapterkit.clientkit._base`` (волна 5,
|
|
2
|
+
задача 2, Atlas #2710). Реэкспорт ради back-compat: ``from clientkit._base
|
|
3
|
+
import ServiceClient`` работает идентично.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from adapterkit.clientkit._base import ServiceClient, ServiceClientBase
|
|
8
|
+
|
|
9
|
+
__all__ = ["ServiceClient", "ServiceClientBase"]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Тонкий алиас — код переехал в ``adapterkit.clientkit._candidates`` (волна 5,
|
|
2
|
+
задача 2, Atlas #2710). Реэкспорт ради back-compat: ``from clientkit._candidates
|
|
3
|
+
import CandidateBook`` работает идентично.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from adapterkit.clientkit._candidates import CandidateBook
|
|
8
|
+
|
|
9
|
+
__all__ = ["CandidateBook"]
|