elemctl 0.1.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.
elemctl-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 KeyFire
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.
elemctl-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,201 @@
1
+ Metadata-Version: 2.4
2
+ Name: elemctl
3
+ Version: 0.1.0
4
+ Summary: CLI, MCP-сервер и библиотека для Console API v2 платформы 1С:Предприятие.Элемент (1cmycloud)
5
+ Author: KeyFire
6
+ License-Expression: MIT
7
+ Keywords: 1c,element,1cmycloud,console-api,deploy,mcp,xbsl
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Natural Language :: Russian
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Software Development :: Build Tools
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Provides-Extra: mcp
24
+ Requires-Dist: mcp>=1.2; extra == "mcp"
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=7; extra == "dev"
27
+ Dynamic: license-file
28
+
29
+ # elemctl
30
+
31
+ Утилита командной строки, MCP-сервер и Python-библиотека для управления
32
+ приложениями облачной платформы **1С:Предприятие.Элемент** (1cmycloud.com)
33
+ через Console API v2.
34
+
35
+ elemctl закрывает жизненный цикл приложения на платформе без веб-консоли:
36
+ создать приложение, собрать архив сборки `.xasm`/`.xlib` из исходников
37
+ проекта, загрузить сборку, применить её к приложению и убедиться, что
38
+ применение действительно произошло (платформа умеет молча откатывать),
39
+ управлять ветками среды разработки, дампами и версией технологии. Один и
40
+ тот же движок доступен тремя способами: команда `elemctl` для терминала и
41
+ CI, MCP-сервер для AI-агентов (Claude Code и другие MCP-клиенты) и
42
+ python-модуль `elemctl` для собственных скриптов.
43
+
44
+ *elemctl is a CLI tool, MCP server and Python library for the
45
+ 1C:Enterprise.Element (1cmycloud) Console API: manage applications, upload
46
+ builds and deploy with honest apply verification. Docs are in Russian - the
47
+ platform's audience - but the CLI output is plain JSON.*
48
+
49
+ ## Возможности
50
+
51
+ - **Приложения**: список, карточка, создание, запуск, остановка, удаление,
52
+ версия технологии.
53
+ - **Проекты и сборки**: загрузка `.xasm`/`.xlib`, список сборок, удаление.
54
+ - **Сборка из исходников**: упаковка каталога проекта (`Проект.yaml` + модули)
55
+ в архив сборки с манифестом и git-метаданными, автоинкремент версии.
56
+ - **Деплой одной командой**: сборка -> загрузка -> применение -> перезапуск ->
57
+ **проверка фактического применения**.
58
+ - **Ветки среды разработки**: список, создание, привязка к приложению, merge,
59
+ загрузка изменений из git-ветки.
60
+ - **Дампы**: создание и контроль готовности.
61
+ - **MCP-сервер**: те же операции как инструменты для AI-агентов
62
+ (Claude Code и другие MCP-клиенты).
63
+
64
+ ### Честная проверка применения
65
+
66
+ Особенность платформы: если применение проекта падает, платформа **молча
67
+ откатывает** приложение на предыдущую сборку - статус `Running` ничего не
68
+ говорит об успехе деплоя. `elemctl deploy` поэтому не верит статусу, а
69
+ проверяет после деплоя:
70
+
71
+ 1. задачи приложения со статусом `Error`/`Failed`, начатые после старта
72
+ деплоя (старые ошибки из истории не учитываются);
73
+ 2. фактическую версию проекта приложения (`source.project-version`) - она
74
+ должна совпасть с только что загруженной сборкой;
75
+ 3. доступность uri приложения контрольным HTTP-запросом (информационно,
76
+ поле `uri-status` в отчёте: 401/403 нормальны для закрытых приложений).
77
+
78
+ Код возврата `deploy` равен нулю только если сборка действительно применилась.
79
+
80
+ ## Установка
81
+
82
+ ```bash
83
+ pipx install elemctl # или: pip install elemctl
84
+ pip install "elemctl[mcp]" # с MCP-сервером
85
+ ```
86
+
87
+ Требуется Python 3.10+. Ядро и CLI не имеют внешних зависимостей
88
+ (только стандартная библиотека).
89
+
90
+ ## Настройка
91
+
92
+ Реквизиты подключения берутся из переменных окружения либо из `.env`
93
+ в текущем каталоге (переменные окружения имеют приоритет):
94
+
95
+ | Переменная | Назначение |
96
+ |---|---|
97
+ | `ELEMENT_BASE_URL` | базовый URL платформы, например `https://1cmycloud.com` |
98
+ | `ELEMENT_CLIENT_ID` | Client-Id для получения токена |
99
+ | `ELEMENT_CLIENT_SECRET` | Client-Secret |
100
+ | `ELEMENT_APP_ID` | приложение по умолчанию (необязательно) |
101
+ | `ELEMENT_PROJECT_ID` | проект по умолчанию (необязательно) |
102
+ | `ELEMENT_SPACE_ID` | пространство по умолчанию (необязательно) |
103
+ | `ELEMENT_BRANCH_ID` | ветка платформы по умолчанию (необязательно) |
104
+
105
+ Client-Id/Client-Secret выпускаются в панели управления 1cmycloud
106
+ (раздел интеграций Console API). Шаблон файла - [.env.example](.env.example).
107
+
108
+ ## Быстрый старт
109
+
110
+ ```bash
111
+ # список приложений
112
+ elemctl apps list
113
+
114
+ # карточка приложения (статус, uri, фактическая версия проекта)
115
+ elemctl apps get <app-id>
116
+
117
+ # полный цикл деплоя из исходников с проверкой применения
118
+ elemctl deploy --app-id <app-id> --project-id <project-id> --project-dir acme/crm
119
+
120
+ # только собрать архив .xasm, никуда не загружая
121
+ elemctl build --project-dir acme/crm --output ./dist
122
+
123
+ # принять изменения ветки среды разработки
124
+ elemctl branches merge <branch-id>
125
+ ```
126
+
127
+ Вывод всех команд - JSON в stdout; прогресс длительных операций - в stderr.
128
+ Ошибки возвращаются JSON-объектом с полем `error` и кодом возврата 1.
129
+
130
+ Полный список команд: `elemctl --help`, по группам - `elemctl apps --help`,
131
+ `elemctl deploy --help` и т.д.
132
+
133
+ ## MCP-сервер
134
+
135
+ Сервер отдаёт операции платформы как MCP-инструменты (транспорт stdio):
136
+
137
+ ```bash
138
+ pip install "elemctl[mcp]"
139
+ claude mcp add elemctl -- elemctl mcp
140
+ ```
141
+
142
+ Реквизиты подключения сервер берёт из тех же переменных `ELEMENT_*` /
143
+ `.env`. Среди инструментов: `list_apps`, `get_app`, `deploy` (с полем `ok`
144
+ в ответе), `verify_deploy`, `list_builds`, `merge_branch` и другие.
145
+
146
+ ## Использование как библиотеки
147
+
148
+ ```python
149
+ from elemctl import Config, ElementClient
150
+ from elemctl.deploy import deploy_from_sources
151
+
152
+ client = ElementClient(Config.from_env())
153
+ apps = client.list_apps()
154
+
155
+ report = deploy_from_sources(
156
+ client,
157
+ app_id="...",
158
+ project_id="...",
159
+ project_dir="acme/crm",
160
+ log=print,
161
+ )
162
+ assert report.ok, report.problems
163
+ ```
164
+
165
+ ## Формат сборки
166
+
167
+ `.xasm` (приложение) и `.xlib` (библиотека) - это ZIP-архив:
168
+
169
+ ```
170
+ Assembly.yaml # манифест: ProjectKind, Vendor, Name, Version, ...
171
+ {vendor}/{name}/... # файлы проекта: .yaml, .xbsl, ресурсы
172
+ ```
173
+
174
+ Каталог проекта должен лежать по схеме `{repo}/{vendor}/{name}/Проект.yaml` -
175
+ пути в архиве строятся относительно корня репозитория. Вид проекта
176
+ (приложение/библиотека) определяется по полю `ВидПроекта` в `Проект.yaml`.
177
+
178
+ ## Ограничения и статус
179
+
180
+ - Инструмент **неофициальный** и не аффилирован с фирмой "1С"; Console API
181
+ может меняться без предупреждения.
182
+ - Операция `branches sync` (загрузка изменений из git-ветки в приложение)
183
+ использует внутренний UI API платформы, а не Console API v2 - контракт
184
+ наименее стабильный.
185
+ - Создание приложения только по `--project-id` на части конфигураций платформы
186
+ даёт пустой каркас без данных проекта. Надёжный путь - источник-сборка:
187
+ `elemctl apps create <имя> --project-id <id> --latest-build` (MCP-инструмент
188
+ `create_app` подставляет последнюю сборку автоматически), а после создания -
189
+ `elemctl deploy`.
190
+ - Приложение с неопубликованными правками в среде разработки платформа
191
+ удалить не даёт (HTTP 400 `FAILED_PRECONDITION`), принудительного удаления
192
+ в Console API нет - только через панель управления; elemctl подскажет это
193
+ в тексте ошибки.
194
+ - Пересоздание приложения (delete + create) меняет его URL - внешние
195
+ настройки, завязанные на адрес (OIDC redirect и т.п.), придётся обновлять.
196
+ "Мягкой" очистки данных приложения в Console API нет, она выполняется в
197
+ консоли управления.
198
+
199
+ ## Лицензия
200
+
201
+ [MIT](LICENSE)
@@ -0,0 +1,173 @@
1
+ # elemctl
2
+
3
+ Утилита командной строки, MCP-сервер и Python-библиотека для управления
4
+ приложениями облачной платформы **1С:Предприятие.Элемент** (1cmycloud.com)
5
+ через Console API v2.
6
+
7
+ elemctl закрывает жизненный цикл приложения на платформе без веб-консоли:
8
+ создать приложение, собрать архив сборки `.xasm`/`.xlib` из исходников
9
+ проекта, загрузить сборку, применить её к приложению и убедиться, что
10
+ применение действительно произошло (платформа умеет молча откатывать),
11
+ управлять ветками среды разработки, дампами и версией технологии. Один и
12
+ тот же движок доступен тремя способами: команда `elemctl` для терминала и
13
+ CI, MCP-сервер для AI-агентов (Claude Code и другие MCP-клиенты) и
14
+ python-модуль `elemctl` для собственных скриптов.
15
+
16
+ *elemctl is a CLI tool, MCP server and Python library for the
17
+ 1C:Enterprise.Element (1cmycloud) Console API: manage applications, upload
18
+ builds and deploy with honest apply verification. Docs are in Russian - the
19
+ platform's audience - but the CLI output is plain JSON.*
20
+
21
+ ## Возможности
22
+
23
+ - **Приложения**: список, карточка, создание, запуск, остановка, удаление,
24
+ версия технологии.
25
+ - **Проекты и сборки**: загрузка `.xasm`/`.xlib`, список сборок, удаление.
26
+ - **Сборка из исходников**: упаковка каталога проекта (`Проект.yaml` + модули)
27
+ в архив сборки с манифестом и git-метаданными, автоинкремент версии.
28
+ - **Деплой одной командой**: сборка -> загрузка -> применение -> перезапуск ->
29
+ **проверка фактического применения**.
30
+ - **Ветки среды разработки**: список, создание, привязка к приложению, merge,
31
+ загрузка изменений из git-ветки.
32
+ - **Дампы**: создание и контроль готовности.
33
+ - **MCP-сервер**: те же операции как инструменты для AI-агентов
34
+ (Claude Code и другие MCP-клиенты).
35
+
36
+ ### Честная проверка применения
37
+
38
+ Особенность платформы: если применение проекта падает, платформа **молча
39
+ откатывает** приложение на предыдущую сборку - статус `Running` ничего не
40
+ говорит об успехе деплоя. `elemctl deploy` поэтому не верит статусу, а
41
+ проверяет после деплоя:
42
+
43
+ 1. задачи приложения со статусом `Error`/`Failed`, начатые после старта
44
+ деплоя (старые ошибки из истории не учитываются);
45
+ 2. фактическую версию проекта приложения (`source.project-version`) - она
46
+ должна совпасть с только что загруженной сборкой;
47
+ 3. доступность uri приложения контрольным HTTP-запросом (информационно,
48
+ поле `uri-status` в отчёте: 401/403 нормальны для закрытых приложений).
49
+
50
+ Код возврата `deploy` равен нулю только если сборка действительно применилась.
51
+
52
+ ## Установка
53
+
54
+ ```bash
55
+ pipx install elemctl # или: pip install elemctl
56
+ pip install "elemctl[mcp]" # с MCP-сервером
57
+ ```
58
+
59
+ Требуется Python 3.10+. Ядро и CLI не имеют внешних зависимостей
60
+ (только стандартная библиотека).
61
+
62
+ ## Настройка
63
+
64
+ Реквизиты подключения берутся из переменных окружения либо из `.env`
65
+ в текущем каталоге (переменные окружения имеют приоритет):
66
+
67
+ | Переменная | Назначение |
68
+ |---|---|
69
+ | `ELEMENT_BASE_URL` | базовый URL платформы, например `https://1cmycloud.com` |
70
+ | `ELEMENT_CLIENT_ID` | Client-Id для получения токена |
71
+ | `ELEMENT_CLIENT_SECRET` | Client-Secret |
72
+ | `ELEMENT_APP_ID` | приложение по умолчанию (необязательно) |
73
+ | `ELEMENT_PROJECT_ID` | проект по умолчанию (необязательно) |
74
+ | `ELEMENT_SPACE_ID` | пространство по умолчанию (необязательно) |
75
+ | `ELEMENT_BRANCH_ID` | ветка платформы по умолчанию (необязательно) |
76
+
77
+ Client-Id/Client-Secret выпускаются в панели управления 1cmycloud
78
+ (раздел интеграций Console API). Шаблон файла - [.env.example](.env.example).
79
+
80
+ ## Быстрый старт
81
+
82
+ ```bash
83
+ # список приложений
84
+ elemctl apps list
85
+
86
+ # карточка приложения (статус, uri, фактическая версия проекта)
87
+ elemctl apps get <app-id>
88
+
89
+ # полный цикл деплоя из исходников с проверкой применения
90
+ elemctl deploy --app-id <app-id> --project-id <project-id> --project-dir acme/crm
91
+
92
+ # только собрать архив .xasm, никуда не загружая
93
+ elemctl build --project-dir acme/crm --output ./dist
94
+
95
+ # принять изменения ветки среды разработки
96
+ elemctl branches merge <branch-id>
97
+ ```
98
+
99
+ Вывод всех команд - JSON в stdout; прогресс длительных операций - в stderr.
100
+ Ошибки возвращаются JSON-объектом с полем `error` и кодом возврата 1.
101
+
102
+ Полный список команд: `elemctl --help`, по группам - `elemctl apps --help`,
103
+ `elemctl deploy --help` и т.д.
104
+
105
+ ## MCP-сервер
106
+
107
+ Сервер отдаёт операции платформы как MCP-инструменты (транспорт stdio):
108
+
109
+ ```bash
110
+ pip install "elemctl[mcp]"
111
+ claude mcp add elemctl -- elemctl mcp
112
+ ```
113
+
114
+ Реквизиты подключения сервер берёт из тех же переменных `ELEMENT_*` /
115
+ `.env`. Среди инструментов: `list_apps`, `get_app`, `deploy` (с полем `ok`
116
+ в ответе), `verify_deploy`, `list_builds`, `merge_branch` и другие.
117
+
118
+ ## Использование как библиотеки
119
+
120
+ ```python
121
+ from elemctl import Config, ElementClient
122
+ from elemctl.deploy import deploy_from_sources
123
+
124
+ client = ElementClient(Config.from_env())
125
+ apps = client.list_apps()
126
+
127
+ report = deploy_from_sources(
128
+ client,
129
+ app_id="...",
130
+ project_id="...",
131
+ project_dir="acme/crm",
132
+ log=print,
133
+ )
134
+ assert report.ok, report.problems
135
+ ```
136
+
137
+ ## Формат сборки
138
+
139
+ `.xasm` (приложение) и `.xlib` (библиотека) - это ZIP-архив:
140
+
141
+ ```
142
+ Assembly.yaml # манифест: ProjectKind, Vendor, Name, Version, ...
143
+ {vendor}/{name}/... # файлы проекта: .yaml, .xbsl, ресурсы
144
+ ```
145
+
146
+ Каталог проекта должен лежать по схеме `{repo}/{vendor}/{name}/Проект.yaml` -
147
+ пути в архиве строятся относительно корня репозитория. Вид проекта
148
+ (приложение/библиотека) определяется по полю `ВидПроекта` в `Проект.yaml`.
149
+
150
+ ## Ограничения и статус
151
+
152
+ - Инструмент **неофициальный** и не аффилирован с фирмой "1С"; Console API
153
+ может меняться без предупреждения.
154
+ - Операция `branches sync` (загрузка изменений из git-ветки в приложение)
155
+ использует внутренний UI API платформы, а не Console API v2 - контракт
156
+ наименее стабильный.
157
+ - Создание приложения только по `--project-id` на части конфигураций платформы
158
+ даёт пустой каркас без данных проекта. Надёжный путь - источник-сборка:
159
+ `elemctl apps create <имя> --project-id <id> --latest-build` (MCP-инструмент
160
+ `create_app` подставляет последнюю сборку автоматически), а после создания -
161
+ `elemctl deploy`.
162
+ - Приложение с неопубликованными правками в среде разработки платформа
163
+ удалить не даёт (HTTP 400 `FAILED_PRECONDITION`), принудительного удаления
164
+ в Console API нет - только через панель управления; elemctl подскажет это
165
+ в тексте ошибки.
166
+ - Пересоздание приложения (delete + create) меняет его URL - внешние
167
+ настройки, завязанные на адрес (OIDC redirect и т.п.), придётся обновлять.
168
+ "Мягкой" очистки данных приложения в Console API нет, она выполняется в
169
+ консоли управления.
170
+
171
+ ## Лицензия
172
+
173
+ [MIT](LICENSE)
@@ -0,0 +1,44 @@
1
+ # Метаданные пакета elemctl (раздел 1 спецификации).
2
+
3
+ [build-system]
4
+ requires = ["setuptools>=77"]
5
+ build-backend = "setuptools.build_meta"
6
+
7
+ [project]
8
+ name = "elemctl"
9
+ version = "0.1.0"
10
+ description = "CLI, MCP-сервер и библиотека для Console API v2 платформы 1С:Предприятие.Элемент (1cmycloud)"
11
+ readme = "README.md"
12
+ requires-python = ">=3.10"
13
+ license = "MIT"
14
+ license-files = ["LICENSE"]
15
+ authors = [{ name = "KeyFire" }]
16
+ keywords = ["1c", "element", "1cmycloud", "console-api", "deploy", "mcp", "xbsl"]
17
+ classifiers = [
18
+ "Development Status :: 4 - Beta",
19
+ "Environment :: Console",
20
+ "Intended Audience :: Developers",
21
+ "Natural Language :: Russian",
22
+ "Operating System :: OS Independent",
23
+ "Programming Language :: Python :: 3",
24
+ "Programming Language :: Python :: 3.10",
25
+ "Programming Language :: Python :: 3.11",
26
+ "Programming Language :: Python :: 3.12",
27
+ "Programming Language :: Python :: 3.13",
28
+ "Programming Language :: Python :: 3.14",
29
+ "Topic :: Software Development :: Build Tools",
30
+ ]
31
+ dependencies = []
32
+
33
+ [project.optional-dependencies]
34
+ mcp = ["mcp>=1.2"]
35
+ dev = ["pytest>=7"]
36
+
37
+ [project.scripts]
38
+ elemctl = "elemctl.cli:main"
39
+
40
+ [tool.setuptools.packages.find]
41
+ where = ["src"]
42
+
43
+ [tool.pytest.ini_options]
44
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,25 @@
1
+ """elemctl – CLI, MCP-сервер и библиотека для Console API v2 платформы
2
+ 1С:Предприятие.Элемент (1cmycloud).
3
+
4
+ Публичный API библиотеки:
5
+
6
+ from elemctl import Config, ElementClient
7
+ from elemctl.deploy import deploy_from_sources
8
+ """
9
+
10
+ from .client import ElementClient
11
+ from .config import Config
12
+ from .errors import ApiError, BuildError, ConfigError, ElemctlError, TransportError
13
+
14
+ __version__ = "0.1.0"
15
+
16
+ __all__ = [
17
+ "ApiError",
18
+ "BuildError",
19
+ "Config",
20
+ "ConfigError",
21
+ "ElemctlError",
22
+ "ElementClient",
23
+ "TransportError",
24
+ "__version__",
25
+ ]
@@ -0,0 +1,142 @@
1
+ """Получение и кеширование токена Console API.
2
+
3
+ Токен живёт около часа, поэтому кешируется в файле в системном каталоге
4
+ временных файлов с TTL один час; ключ кеша различает пары base_url +
5
+ client_id. При 401 клиент запрашивает токен принудительно (force=True).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import base64
11
+ import hashlib
12
+ import json
13
+ import tempfile
14
+ import time
15
+ from pathlib import Path
16
+
17
+ from .errors import ApiError
18
+
19
+ TOKEN_TTL = 3600.0
20
+ # Запас до истечения: кеш считается годным, если жить осталось больше запаса.
21
+ EXPIRY_MARGIN = 30.0
22
+
23
+
24
+ def extract_token(payload):
25
+ """Достать токен из ответа сервера.
26
+
27
+ Токен лежит в первом непустом из полей id_token, token, value,
28
+ access_token. Особый случай: значение "Not implemented" токеном
29
+ не является и пропускается.
30
+ """
31
+ if not isinstance(payload, dict):
32
+ return None
33
+ for key in ("id_token", "token", "value", "access_token"):
34
+ value = payload.get(key)
35
+ if not isinstance(value, str):
36
+ continue
37
+ value = value.strip()
38
+ if not value or value == "Not implemented":
39
+ continue
40
+ return value
41
+ return None
42
+
43
+
44
+ class TokenManager:
45
+ """Выдаёт действующий Bearer-токен, пряча кеширование и обновление."""
46
+
47
+ def __init__(self, config, transport, cache_dir=None):
48
+ self._config = config
49
+ self._transport = transport
50
+ self._cache_dir = Path(cache_dir) if cache_dir else Path(tempfile.gettempdir())
51
+ self._token = None
52
+ self._expires_at = 0.0
53
+
54
+ def get_token(self, force=False):
55
+ """Вернуть токен: из памяти, из файлового кеша или запросив новый."""
56
+ now = time.time()
57
+ if not force:
58
+ if self._token and now + EXPIRY_MARGIN < self._expires_at:
59
+ return self._token
60
+ cached = self._read_cache()
61
+ if cached and now + EXPIRY_MARGIN < cached.get("expires", 0):
62
+ self._token = cached["token"]
63
+ self._expires_at = cached["expires"]
64
+ return self._token
65
+ token = self._request_token()
66
+ self._token = token
67
+ self._expires_at = now + TOKEN_TTL
68
+ self._write_cache()
69
+ return token
70
+
71
+ # -- внутреннее -----------------------------------------------------
72
+
73
+ def _cache_path(self):
74
+ key = f"{self._config.base_url}|{self._config.client_id}"
75
+ digest = hashlib.sha256(key.encode("utf-8")).hexdigest()[:16]
76
+ return self._cache_dir / f"elemctl-token-{digest}.json"
77
+
78
+ def _read_cache(self):
79
+ try:
80
+ data = json.loads(self._cache_path().read_text(encoding="utf-8"))
81
+ except (OSError, ValueError):
82
+ return None
83
+ if isinstance(data, dict) and isinstance(data.get("token"), str):
84
+ return data
85
+ return None
86
+
87
+ def _write_cache(self):
88
+ try:
89
+ self._cache_dir.mkdir(parents=True, exist_ok=True)
90
+ payload = {"token": self._token, "expires": self._expires_at}
91
+ self._cache_path().write_text(
92
+ json.dumps(payload), encoding="utf-8"
93
+ )
94
+ except OSError:
95
+ # Кеш – только ускорение; его недоступность не должна ломать работу.
96
+ pass
97
+
98
+ def _request_token(self):
99
+ config = self._config.require()
100
+ credentials = f"{config.client_id}:{config.client_secret}".encode("utf-8")
101
+ basic = base64.b64encode(credentials).decode("ascii")
102
+ url = f"{config.base_url}/console/sys/token"
103
+ response = self._transport.request(
104
+ "POST",
105
+ url,
106
+ headers={
107
+ "Authorization": f"Basic {basic}",
108
+ "Content-Type": "application/x-www-form-urlencoded",
109
+ },
110
+ data=b"grant_type=client_credentials",
111
+ timeout=config.timeout,
112
+ )
113
+ if not 200 <= response.status < 300:
114
+ raise ApiError(
115
+ f"не удалось получить токен: HTTP {response.status}",
116
+ status=response.status,
117
+ method="POST",
118
+ url=url,
119
+ body=_safe_body(response),
120
+ )
121
+ try:
122
+ payload = response.json()
123
+ except ValueError:
124
+ payload = None
125
+ token = extract_token(payload)
126
+ if not token:
127
+ raise ApiError(
128
+ "токен не найден в ответе сервера (ожидались поля id_token, token, value или access_token)",
129
+ status=response.status,
130
+ method="POST",
131
+ url=url,
132
+ body=payload if payload is not None else response.text(),
133
+ )
134
+ return token
135
+
136
+
137
+ def _safe_body(response):
138
+ """Тело ответа для деталей ошибки: JSON, а если не разбирается – текст."""
139
+ try:
140
+ return response.json()
141
+ except ValueError:
142
+ return response.text()