Compare commits
No commits in common. "a90dd9bc50d7e81c14d3747760d9cdb5cbce326a" and "bcef2d7b52601f41194762d6beb667efe8fb62b5" have entirely different histories.
a90dd9bc50
...
bcef2d7b52
5 changed files with 10 additions and 741 deletions
19
.gitignore
vendored
19
.gitignore
vendored
|
|
@ -1,21 +1,2 @@
|
|||
.bin/
|
||||
data-dev/
|
||||
|
||||
# Local deployment/config secrets
|
||||
.env
|
||||
docker-compose.override.yml
|
||||
example/autoupdate.json
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
Icon?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
.fseventsd
|
||||
|
|
|
|||
|
|
@ -248,7 +248,6 @@ environment variables.
|
|||
|
||||
## More Docs
|
||||
|
||||
- `docs/API.md` - current client API contract, examples, responses, and errors.
|
||||
- `docs/DEPLOYMENT.md` - deployment, reverse proxy, backup, and restore notes.
|
||||
- `deploy/update-server.env.example` - production environment example.
|
||||
- `deploy/Caddyfile.example` - Caddy reverse proxy example.
|
||||
|
|
|
|||
631
docs/API.md
631
docs/API.md
|
|
@ -1,631 +0,0 @@
|
|||
# Update Server API
|
||||
|
||||
Этот документ описывает текущий публичный HTTP API сервиса. Его можно передать
|
||||
клиентскому приложению или агенту, который встраивает автообновление.
|
||||
|
||||
Примеры ниже используют:
|
||||
|
||||
```text
|
||||
Base URL: https://updates.example.com
|
||||
API key: upsk_replace_me
|
||||
Project: desktop-app
|
||||
```
|
||||
|
||||
В реальной интеграции замени `Base URL`, API key и `project_slug` на свои
|
||||
значения.
|
||||
|
||||
## Общая модель
|
||||
|
||||
Update Server хранит проекты и релизы:
|
||||
|
||||
- `project` - приложение, продукт или канал обновлений;
|
||||
- `release` - загруженный файл обновления внутри проекта;
|
||||
- `api key` - клиентский ключ доступа к API.
|
||||
|
||||
Администратор работает через web UI:
|
||||
|
||||
```text
|
||||
/admin
|
||||
/admin/projects
|
||||
/admin/api-keys
|
||||
```
|
||||
|
||||
Клиентские приложения работают через JSON API:
|
||||
|
||||
```text
|
||||
/api/v1
|
||||
```
|
||||
|
||||
На текущий момент JSON API предназначен для чтения и скачивания обновлений.
|
||||
Создание проектов, загрузка релизов и создание API keys выполняются через admin
|
||||
web UI, а не через публичный JSON API.
|
||||
|
||||
## Авторизация
|
||||
|
||||
Защищенные endpoint'ы требуют bearer token:
|
||||
|
||||
```http
|
||||
Authorization: Bearer <api_key>
|
||||
```
|
||||
|
||||
Пример:
|
||||
|
||||
```bash
|
||||
curl -i \
|
||||
-H "Authorization: Bearer upsk_replace_me" \
|
||||
https://updates.example.com/api/v1/projects
|
||||
```
|
||||
|
||||
API key должен быть:
|
||||
|
||||
- активным;
|
||||
- не истекшим по `expires_at`, если срок задан;
|
||||
- с разрешением `can_download`;
|
||||
- со scope, который разрешает доступ к нужному проекту.
|
||||
|
||||
Поддерживаемые scope modes:
|
||||
|
||||
```text
|
||||
all_projects
|
||||
project_allow_list
|
||||
project_deny_list
|
||||
tag_allow_list
|
||||
tag_deny_list
|
||||
```
|
||||
|
||||
Полный API key показывается в admin UI только один раз при создании. Сервер
|
||||
хранит только hash ключа.
|
||||
|
||||
## Формат JSON
|
||||
|
||||
Все JSON-ответы возвращаются с:
|
||||
|
||||
```http
|
||||
Content-Type: application/json; charset=utf-8
|
||||
```
|
||||
|
||||
Поля времени возвращаются строкой в формате RFC3339/RFC3339Nano, например:
|
||||
|
||||
```json
|
||||
"created_at": "2026-06-11T10:15:30Z"
|
||||
```
|
||||
|
||||
Относительные URL в ответах, например `download_url`, нужно склеивать с
|
||||
`Base URL` клиента.
|
||||
|
||||
## Health Check
|
||||
|
||||
### GET /healthz
|
||||
|
||||
Публичная проверка готовности HTTP-сервера и SQLite.
|
||||
|
||||
```bash
|
||||
curl -i https://updates.example.com/healthz
|
||||
```
|
||||
|
||||
Успешный ответ:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json; charset=utf-8
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"database": "ok",
|
||||
"service": "Update Server",
|
||||
"status": "ok",
|
||||
"timestamp": "2026-06-11T10:15:30Z"
|
||||
}
|
||||
```
|
||||
|
||||
Если база недоступна:
|
||||
|
||||
```http
|
||||
HTTP/1.1 503 Service Unavailable
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"database": "unavailable",
|
||||
"error": "database ping failed",
|
||||
"service": "Update Server",
|
||||
"status": "degraded",
|
||||
"timestamp": "2026-06-11T10:15:30Z"
|
||||
}
|
||||
```
|
||||
|
||||
## API Index
|
||||
|
||||
### GET /api/v1
|
||||
|
||||
Публичный JSON-index текущей версии API. Авторизация не нужна.
|
||||
|
||||
```bash
|
||||
curl -i https://updates.example.com/api/v1
|
||||
```
|
||||
|
||||
Ответ:
|
||||
|
||||
```json
|
||||
{
|
||||
"auth": {
|
||||
"header": "Authorization: Bearer <api_key>",
|
||||
"type": "bearer"
|
||||
},
|
||||
"routes": [
|
||||
{
|
||||
"description": "List active projects accessible to the API key.",
|
||||
"method": "GET",
|
||||
"path": "/api/v1/projects"
|
||||
},
|
||||
{
|
||||
"description": "Get the latest active release metadata for an accessible project.",
|
||||
"method": "GET",
|
||||
"path": "/api/v1/projects/{projectSlug}/releases/latest"
|
||||
},
|
||||
{
|
||||
"description": "Get release metadata for an accessible release.",
|
||||
"method": "GET",
|
||||
"path": "/api/v1/releases/{releaseID}"
|
||||
},
|
||||
{
|
||||
"description": "Download the private artifact for an accessible release.",
|
||||
"method": "GET",
|
||||
"path": "/api/v1/releases/{releaseID}/download"
|
||||
}
|
||||
],
|
||||
"service": "Update Server",
|
||||
"status": "client-api-ready",
|
||||
"version": "v1"
|
||||
}
|
||||
```
|
||||
|
||||
## List Accessible Projects
|
||||
|
||||
### GET /api/v1/projects
|
||||
|
||||
Возвращает активные проекты, доступные текущему API key.
|
||||
|
||||
Требует:
|
||||
|
||||
```http
|
||||
Authorization: Bearer <api_key>
|
||||
```
|
||||
|
||||
Пример:
|
||||
|
||||
```bash
|
||||
curl -i \
|
||||
-H "Authorization: Bearer upsk_replace_me" \
|
||||
https://updates.example.com/api/v1/projects
|
||||
```
|
||||
|
||||
Успешный ответ:
|
||||
|
||||
```json
|
||||
{
|
||||
"projects": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Desktop App",
|
||||
"slug": "desktop-app",
|
||||
"description": "Primary desktop updater stream.",
|
||||
"latest_release_url": "/api/v1/projects/desktop-app/releases/latest"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Если доступных проектов нет:
|
||||
|
||||
```json
|
||||
{
|
||||
"projects": []
|
||||
}
|
||||
```
|
||||
|
||||
## Get Latest Release
|
||||
|
||||
### GET /api/v1/projects/{projectSlug}/releases/latest
|
||||
|
||||
Возвращает latest active release для активного и доступного проекта.
|
||||
|
||||
Latest release выбирается по:
|
||||
|
||||
```text
|
||||
created_at DESC, id DESC
|
||||
```
|
||||
|
||||
Пример:
|
||||
|
||||
```bash
|
||||
curl -i \
|
||||
-H "Authorization: Bearer upsk_replace_me" \
|
||||
https://updates.example.com/api/v1/projects/desktop-app/releases/latest
|
||||
```
|
||||
|
||||
Успешный ответ:
|
||||
|
||||
```json
|
||||
{
|
||||
"project": {
|
||||
"id": 1,
|
||||
"name": "Desktop App",
|
||||
"slug": "desktop-app",
|
||||
"description": "Primary desktop updater stream.",
|
||||
"latest_release_url": "/api/v1/projects/desktop-app/releases/latest"
|
||||
},
|
||||
"release": {
|
||||
"id": 2,
|
||||
"version": "1.1.0",
|
||||
"build": "build-2",
|
||||
"filename": "desktop-app-1.1.0.zip",
|
||||
"checksum_sha256": "4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7c6e64b2f7110b2",
|
||||
"size_bytes": 12345678,
|
||||
"content_type": "application/zip",
|
||||
"release_notes": "Improved desktop rollout.",
|
||||
"created_at": "2026-06-11T10:15:30Z",
|
||||
"metadata_url": "/api/v1/releases/2",
|
||||
"download_url": "/api/v1/releases/2/download"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Если проект не существует, архивирован или недоступен этому key:
|
||||
|
||||
```http
|
||||
HTTP/1.1 404 Not Found
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "resource not found"
|
||||
}
|
||||
```
|
||||
|
||||
Если проект доступен, но активных релизов нет:
|
||||
|
||||
```http
|
||||
HTTP/1.1 404 Not Found
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "release not found"
|
||||
}
|
||||
```
|
||||
|
||||
## Get Release Metadata
|
||||
|
||||
### GET /api/v1/releases/{releaseID}
|
||||
|
||||
Возвращает metadata релиза по ID. Релиз должен быть активным, его проект должен
|
||||
быть активным, и текущий API key должен иметь доступ к проекту релиза.
|
||||
|
||||
Пример:
|
||||
|
||||
```bash
|
||||
curl -i \
|
||||
-H "Authorization: Bearer upsk_replace_me" \
|
||||
https://updates.example.com/api/v1/releases/2
|
||||
```
|
||||
|
||||
Успешный ответ:
|
||||
|
||||
```json
|
||||
{
|
||||
"project": {
|
||||
"id": 1,
|
||||
"name": "Desktop App",
|
||||
"slug": "desktop-app",
|
||||
"description": "Primary desktop updater stream.",
|
||||
"latest_release_url": "/api/v1/projects/desktop-app/releases/latest"
|
||||
},
|
||||
"release": {
|
||||
"id": 2,
|
||||
"version": "1.1.0",
|
||||
"build": "build-2",
|
||||
"filename": "desktop-app-1.1.0.zip",
|
||||
"checksum_sha256": "4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7c6e64b2f7110b2",
|
||||
"size_bytes": 12345678,
|
||||
"content_type": "application/zip",
|
||||
"release_notes": "Improved desktop rollout.",
|
||||
"created_at": "2026-06-11T10:15:30Z",
|
||||
"metadata_url": "/api/v1/releases/2",
|
||||
"download_url": "/api/v1/releases/2/download"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Если `releaseID` не число:
|
||||
|
||||
```http
|
||||
HTTP/1.1 400 Bad Request
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "invalid release id"
|
||||
}
|
||||
```
|
||||
|
||||
Если релиз не существует, архивирован или недоступен:
|
||||
|
||||
```http
|
||||
HTTP/1.1 404 Not Found
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "resource not found"
|
||||
}
|
||||
```
|
||||
|
||||
## Download Release Artifact
|
||||
|
||||
### GET /api/v1/releases/{releaseID}/download
|
||||
|
||||
Скачивает приватный artifact релиза. Скачивание проходит через приложение, чтобы
|
||||
сервер мог проверить API key, permission и project scope.
|
||||
|
||||
Пример:
|
||||
|
||||
```bash
|
||||
curl -L \
|
||||
-H "Authorization: Bearer upsk_replace_me" \
|
||||
-o desktop-app-1.1.0.zip \
|
||||
https://updates.example.com/api/v1/releases/2/download
|
||||
```
|
||||
|
||||
Успешный ответ:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200 OK
|
||||
Content-Disposition: attachment; filename=desktop-app-1.1.0.zip
|
||||
Content-Type: application/zip
|
||||
X-Content-Type-Options: nosniff
|
||||
```
|
||||
|
||||
Body ответа - bytes загруженного файла.
|
||||
|
||||
Если artifact отсутствует на диске:
|
||||
|
||||
```http
|
||||
HTTP/1.1 404 Not Found
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "release not found"
|
||||
}
|
||||
```
|
||||
|
||||
Если релиз недоступен:
|
||||
|
||||
```http
|
||||
HTTP/1.1 404 Not Found
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "resource not found"
|
||||
}
|
||||
```
|
||||
|
||||
## Project Object
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Desktop App",
|
||||
"slug": "desktop-app",
|
||||
"description": "Primary desktop updater stream.",
|
||||
"latest_release_url": "/api/v1/projects/desktop-app/releases/latest"
|
||||
}
|
||||
```
|
||||
|
||||
Поля:
|
||||
|
||||
- `id` - numeric project ID.
|
||||
- `name` - display name.
|
||||
- `slug` - stable URL identifier.
|
||||
- `description` - optional description.
|
||||
- `latest_release_url` - relative URL для latest release lookup.
|
||||
|
||||
## Release Object
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 2,
|
||||
"version": "1.1.0",
|
||||
"build": "build-2",
|
||||
"filename": "desktop-app-1.1.0.zip",
|
||||
"checksum_sha256": "4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7c6e64b2f7110b2",
|
||||
"size_bytes": 12345678,
|
||||
"content_type": "application/zip",
|
||||
"release_notes": "Improved desktop rollout.",
|
||||
"created_at": "2026-06-11T10:15:30Z",
|
||||
"metadata_url": "/api/v1/releases/2",
|
||||
"download_url": "/api/v1/releases/2/download"
|
||||
}
|
||||
```
|
||||
|
||||
Поля:
|
||||
|
||||
- `id` - numeric release ID.
|
||||
- `version` - версия, введенная администратором при загрузке.
|
||||
- `build` - build label или build number, если задан.
|
||||
- `filename` - download filename.
|
||||
- `checksum_sha256` - SHA-256 загруженного artifact.
|
||||
- `size_bytes` - размер artifact в bytes.
|
||||
- `content_type` - MIME type artifact.
|
||||
- `release_notes` - release notes.
|
||||
- `created_at` - время загрузки релиза.
|
||||
- `metadata_url` - relative URL metadata endpoint.
|
||||
- `download_url` - relative URL download endpoint.
|
||||
|
||||
## Ошибки
|
||||
|
||||
JSON API возвращает ошибки в формате:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "message"
|
||||
}
|
||||
```
|
||||
|
||||
Основные статусы:
|
||||
|
||||
| Status | Когда |
|
||||
| --- | --- |
|
||||
| `400` | Некорректный `releaseID`, например не число. |
|
||||
| `401` | Нет bearer token, token неверный, key выключен или истек. |
|
||||
| `403` | API key валиден, но нет `can_download`. |
|
||||
| `404` | Ресурс не найден, архивирован или недоступен текущему key. |
|
||||
| `429` | Сработал rate limit client API. |
|
||||
| `500` | Внутренняя ошибка сервера. |
|
||||
| `503` | Health check: база недоступна; или API key service недоступен. |
|
||||
|
||||
### 401 Unauthorized
|
||||
|
||||
Missing header:
|
||||
|
||||
```http
|
||||
WWW-Authenticate: Bearer realm="update-server"
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "missing bearer api key"
|
||||
}
|
||||
```
|
||||
|
||||
Invalid, disabled or expired key:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "invalid api key"
|
||||
}
|
||||
```
|
||||
|
||||
### 403 Forbidden
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "api key permission denied"
|
||||
}
|
||||
```
|
||||
|
||||
### 429 Too Many Requests
|
||||
|
||||
```http
|
||||
Retry-After: 3
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "rate limit exceeded"
|
||||
}
|
||||
```
|
||||
|
||||
По умолчанию client API rate limit настраивается переменными:
|
||||
|
||||
```text
|
||||
APP_CLIENT_RATE_LIMIT_PER_MINUTE=120
|
||||
APP_CLIENT_RATE_LIMIT_BURST=60
|
||||
```
|
||||
|
||||
## Headers
|
||||
|
||||
Для protected API responses сервер добавляет:
|
||||
|
||||
```http
|
||||
Cache-Control: no-store, private, max-age=0
|
||||
Pragma: no-cache
|
||||
Expires: 0
|
||||
Vary: Authorization
|
||||
X-Robots-Tag: noindex, nofollow
|
||||
```
|
||||
|
||||
Также на все ответы добавляются security headers, включая:
|
||||
|
||||
```http
|
||||
X-Content-Type-Options: nosniff
|
||||
X-Frame-Options: DENY
|
||||
Referrer-Policy: no-referrer
|
||||
```
|
||||
|
||||
Если `APP_BASE_URL` использует `https://`, включается:
|
||||
|
||||
```http
|
||||
Strict-Transport-Security: max-age=31536000
|
||||
```
|
||||
|
||||
## Типовой Client Flow
|
||||
|
||||
1. Администратор создает проект в `/admin/projects`.
|
||||
2. Администратор загружает release artifact в проекте.
|
||||
3. Администратор создает API key в `/admin/api-keys`.
|
||||
4. Для ключа включает `can_download`.
|
||||
5. Для ключа выбирает scope, который разрешает доступ к проекту.
|
||||
6. Клиент запрашивает latest release:
|
||||
|
||||
```bash
|
||||
curl -sS \
|
||||
-H "Authorization: Bearer upsk_replace_me" \
|
||||
https://updates.example.com/api/v1/projects/desktop-app/releases/latest
|
||||
```
|
||||
|
||||
7. Клиент сравнивает `release.version` со своей текущей версией.
|
||||
8. Если версия новая, клиент скачивает `release.download_url`.
|
||||
9. Клиент проверяет:
|
||||
- `checksum_sha256`;
|
||||
- `size_bytes`;
|
||||
- при необходимости свою подпись artifact, если она есть в продукте.
|
||||
|
||||
## Python Example
|
||||
|
||||
В репозитории есть минимальный helper:
|
||||
|
||||
```text
|
||||
example/autoupdate.py
|
||||
example/update_client.py
|
||||
```
|
||||
|
||||
Приватный файл рядом со скриптом:
|
||||
|
||||
```json
|
||||
{
|
||||
"base_url": "https://updates.example.com",
|
||||
"api_key": "upsk_replace_me",
|
||||
"project_slug": "desktop-app"
|
||||
}
|
||||
```
|
||||
|
||||
Вызов:
|
||||
|
||||
```python
|
||||
from autoupdate import ensure_updated_from_config
|
||||
|
||||
ver = "1.0.0"
|
||||
ensure_updated_from_config(current_version=ver)
|
||||
```
|
||||
|
||||
Если `autoupdate.json` отсутствует, helper создаст файл с placeholder values,
|
||||
не пойдет в сеть и выведет короткое сообщение о настройке конфига.
|
||||
|
||||
## Чего Сейчас Нет В API
|
||||
|
||||
На текущий момент client JSON API не предоставляет:
|
||||
|
||||
- создание/редактирование проектов;
|
||||
- загрузку релизов;
|
||||
- создание/редактирование API keys;
|
||||
- список всех релизов проекта;
|
||||
- release channels вроде `stable` / `beta`;
|
||||
- query parameters вроде `platform`, `arch`, `current_version`;
|
||||
- публичные downloads без API key.
|
||||
|
||||
Эти действия либо выполняются через admin web UI, либо пока являются будущим
|
||||
расширением API.
|
||||
|
|
@ -21,12 +21,6 @@ class UpdateError(RuntimeError):
|
|||
|
||||
|
||||
ReleaseHandler = Callable[[Path, dict[str, Any]], None]
|
||||
DEFAULT_CONFIG_NAME = "autoupdate.json"
|
||||
DEFAULT_CONFIG = {
|
||||
"base_url": "https://updates.example.com",
|
||||
"api_key": "upsk_replace_me",
|
||||
"project_slug": "example-project",
|
||||
}
|
||||
|
||||
|
||||
def ensure_updated(
|
||||
|
|
@ -43,17 +37,7 @@ def ensure_updated(
|
|||
If `apply_update` is provided, it receives `(downloaded_artifact_path, release_dict)`
|
||||
and is responsible for applying the downloaded artifact before restart.
|
||||
"""
|
||||
if _is_placeholder_config(
|
||||
{
|
||||
"base_url": base_url,
|
||||
"api_key": api_key,
|
||||
"project_slug": project_slug,
|
||||
}
|
||||
):
|
||||
print("autoupdate: configure autoupdate.json to enable updates")
|
||||
return False
|
||||
|
||||
if not api_key:
|
||||
if not api_key or api_key == "upsk_replace_me":
|
||||
raise UpdateError("set a real API key before calling ensure_updated")
|
||||
|
||||
script_path = _current_script_path()
|
||||
|
|
@ -67,8 +51,6 @@ def ensure_updated(
|
|||
if not latest_version or latest_version == current_version:
|
||||
return False
|
||||
|
||||
print(f"autoupdate: updating {current_version} -> {latest_version}")
|
||||
|
||||
download_url = urljoin(base_url.rstrip("/") + "/", str(release.get("download_url") or ""))
|
||||
if not download_url:
|
||||
raise UpdateError("latest release does not contain a download URL")
|
||||
|
|
@ -93,27 +75,6 @@ def ensure_updated(
|
|||
return True
|
||||
|
||||
|
||||
def ensure_updated_from_config(
|
||||
current_version: str,
|
||||
config_path: str | Path | None = None,
|
||||
apply_update: ReleaseHandler | None = None,
|
||||
) -> bool:
|
||||
"""
|
||||
Load update settings from JSON and run the update check.
|
||||
|
||||
If `config_path` is omitted, `autoupdate.json` is loaded from the same
|
||||
directory as the currently running script.
|
||||
"""
|
||||
config = _load_config(config_path)
|
||||
return ensure_updated(
|
||||
base_url=config["base_url"],
|
||||
api_key=config["api_key"],
|
||||
project_slug=config["project_slug"],
|
||||
current_version=current_version,
|
||||
apply_update=apply_update,
|
||||
)
|
||||
|
||||
|
||||
def _current_script_path() -> Path:
|
||||
main_module = sys.modules.get("__main__")
|
||||
main_file = getattr(main_module, "__file__", None)
|
||||
|
|
@ -122,52 +83,6 @@ def _current_script_path() -> Path:
|
|||
return Path(main_file).resolve()
|
||||
|
||||
|
||||
def _default_config_path() -> Path:
|
||||
return _current_script_path().with_name(DEFAULT_CONFIG_NAME)
|
||||
|
||||
|
||||
def _load_config(config_path: str | Path | None) -> dict[str, str]:
|
||||
path = (
|
||||
Path(config_path).expanduser()
|
||||
if config_path is not None
|
||||
else _default_config_path()
|
||||
)
|
||||
try:
|
||||
with path.open("r", encoding="utf-8") as config_file:
|
||||
payload = json.load(config_file)
|
||||
except FileNotFoundError:
|
||||
_write_default_config(path)
|
||||
return DEFAULT_CONFIG.copy()
|
||||
except json.JSONDecodeError as exc:
|
||||
raise UpdateError(f"update config file is not valid JSON: {path}") from exc
|
||||
|
||||
if not isinstance(payload, dict):
|
||||
raise UpdateError(f"update config must be a JSON object: {path}")
|
||||
|
||||
config: dict[str, str] = {}
|
||||
for key in ("base_url", "api_key", "project_slug"):
|
||||
value = payload.get(key)
|
||||
if not isinstance(value, str) or not value.strip():
|
||||
raise UpdateError(f"update config field {key!r} is required: {path}")
|
||||
config[key] = value.strip()
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def _write_default_config(path: Path) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with path.open("w", encoding="utf-8") as config_file:
|
||||
json.dump(DEFAULT_CONFIG, config_file, indent=2)
|
||||
config_file.write("\n")
|
||||
|
||||
|
||||
def _is_placeholder_config(config: dict[str, str]) -> bool:
|
||||
return any(
|
||||
config.get(key, "").strip() == value
|
||||
for key, value in DEFAULT_CONFIG.items()
|
||||
)
|
||||
|
||||
|
||||
def _make_request(url: str, api_key: str, accept_json: bool = False) -> Request:
|
||||
headers = {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
from autoupdate import ensure_updated_from_config
|
||||
from autoupdate import ensure_updated
|
||||
|
||||
ver = "0.1.1"
|
||||
ensure_updated(
|
||||
base_url="http://127.0.0.1:8080",
|
||||
api_key="upsk_zdQu4gosIJjvIaQfn5A_ux9msFyDfEnc8c29F0ZCTmk",
|
||||
project_slug="test",
|
||||
current_version="0.1.3",
|
||||
)
|
||||
|
||||
ensure_updated_from_config(current_version=ver)
|
||||
|
||||
print(f"TEst {ver}")
|
||||
|
||||
print("TEst")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue