v1.0.89: detailed code comments — all Python files + index.html architecture overview
This commit is contained in:
@@ -1,7 +1,16 @@
|
||||
"""
|
||||
Операции с инстансами Nubes — получение списка и организации через API.
|
||||
"""
|
||||
|
||||
from api.http_client import HttpClient
|
||||
|
||||
|
||||
def get_instances(client):
|
||||
"""GET /instances с пагинацией → ВСЕ инстансы пользователя.
|
||||
|
||||
pageSize=200 — максимум за один запрос.
|
||||
Остановка по len(batch) < pageSize (НЕ доверяет total — бывали баги).
|
||||
Возвращает полный list[dict]."""
|
||||
results = []
|
||||
page = 1
|
||||
page_size = 200
|
||||
@@ -9,7 +18,8 @@ def get_instances(client):
|
||||
data = client.get("/instances", params={"pageSize": page_size, "page": page})
|
||||
batch = data.get("results", []) or []
|
||||
results.extend(batch)
|
||||
# Не доверяем total: если страница вернула меньше pageSize, значит данные закончились.
|
||||
# Если страница неполная — данные закончились.
|
||||
# Не проверяем total, потому что API иногда врёт про total.
|
||||
if len(batch) < page_size:
|
||||
break
|
||||
page += 1
|
||||
@@ -17,6 +27,9 @@ def get_instances(client):
|
||||
|
||||
|
||||
def get_organization(client):
|
||||
"""Первый инстанс с serviceId=19 (Организация).
|
||||
|
||||
Организация всегда одна на пользователя. Используется в UI: название, статус, ClientID."""
|
||||
for inst in get_instances(client):
|
||||
if inst.get("serviceId") == 19:
|
||||
return inst
|
||||
|
||||
@@ -64,8 +64,12 @@ def get_params_with_current_values(client, op_id, instance_uid):
|
||||
"name": code,
|
||||
"dataType": data_type,
|
||||
"isRequired": required,
|
||||
"defaultValue": merged_value,
|
||||
"defaultValue": merged_value, # ← фронт использует это для value="..." в инпутах
|
||||
"valueList": value_list,
|
||||
# dataDescriptor — только для map/map-fixed типов.
|
||||
# Содержит словарь {sub_code: {dataType, valueList, isRequired}} —
|
||||
# это нужно фронту чтобы отрисовать вложенную таблицу подполей.
|
||||
# Для не-map типов — None.
|
||||
"dataDescriptor": (
|
||||
{
|
||||
k: {
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
"""
|
||||
Операции с сервисами Nubes — получение списка и деталей через API.
|
||||
"""
|
||||
|
||||
from api.http_client import HttpClient
|
||||
|
||||
|
||||
def get_services(client):
|
||||
"""GET /services → список ВСЕХ сервисов пользователя.
|
||||
|
||||
Возвращает list[dict], каждый с ключами: svcId, svc, svcExtendedName, operations, ..."""
|
||||
data = client.get("/services")
|
||||
return data.get("results", [])
|
||||
|
||||
|
||||
def get_service_detail(client, svc_id):
|
||||
"""GET /services/{svcId} → детали одного сервиса.
|
||||
|
||||
Возвращает dict с ключами: svc, operations[], ...
|
||||
operations — список доступных операций (create, modify, delete, ...)."""
|
||||
data = client.get(f"/services/{svc_id}")
|
||||
return data.get("svc", {})
|
||||
|
||||
+48
-12
@@ -1,4 +1,21 @@
|
||||
"""Трекер созданных инстансов — по юзеру и стенду: /tmp/instances-{client_id}-{stand}.json."""
|
||||
"""
|
||||
Трекер созданных инстансов.
|
||||
|
||||
Хранит UID'ы инстансов, созданных через приложение, в JSON-файлах.
|
||||
Нужен ТОЛЬКО как краткосрочный fallback — когда инстанс создан, но ещё
|
||||
не появился в ответе GET /instances (облако может задержать на секунды).
|
||||
|
||||
Файлы изолированы по пользователю и стенду:
|
||||
/tmp/instances-{clientId}-{stand}.json
|
||||
|
||||
Блокировка fcntl.flock для безопасной работы с несколькими воркерами gunicorn.
|
||||
При редеплое /tmp/ теряется — это НЕ критично, cloud-first выдача всё покажет.
|
||||
|
||||
Функции:
|
||||
add(client_id, stand, uid, svc_id, name) — добавить инстанс
|
||||
remove(client_id, stand, uid) — удалить (после успешного delete)
|
||||
list_all(client_id, stand) — все записи списком
|
||||
"""
|
||||
import fcntl
|
||||
import json
|
||||
import os
|
||||
@@ -6,11 +23,15 @@ import time
|
||||
|
||||
|
||||
def _path(client_id, stand):
|
||||
"""Путь к файлу трекера для конкретного пользователя и стенда."""
|
||||
return f"/tmp/instances-{client_id}-{stand}.json"
|
||||
|
||||
|
||||
def _acquire_lock(fd):
|
||||
"""LOCK_EX с таймаутом 2 секунды (LOCK_NB + retry)."""
|
||||
"""Эксклюзивная блокировка файла с таймаутом 2 секунды.
|
||||
|
||||
Использует LOCK_NB (неблокирующий) + retry с шагом 50ms.
|
||||
Если за 2 секунды не взяли лок — возвращает False (не блокируемся навечно)."""
|
||||
deadline = time.time() + 2
|
||||
while True:
|
||||
try:
|
||||
@@ -23,44 +44,55 @@ def _acquire_lock(fd):
|
||||
|
||||
|
||||
def _locked_read(path):
|
||||
"""Читает файл под эксклюзивной блокировкой, пустой dict если нет."""
|
||||
"""Читает JSON-файл под эксклюзивной блокировкой.
|
||||
|
||||
Если файла нет — создаёт и возвращает пустой {}.
|
||||
Если JSON битый — возвращает {} (переживёт перезапись).
|
||||
Максимальный размер: 65536 байт."""
|
||||
try:
|
||||
# O_RDWR — чтение+запись, O_CREAT — создать если нет
|
||||
fd = os.open(path, os.O_RDWR | os.O_CREAT, 0o644)
|
||||
except OSError:
|
||||
return {}
|
||||
return {} # нет прав — молча возвращаем пустой
|
||||
try:
|
||||
if not _acquire_lock(fd):
|
||||
return {}
|
||||
return {} # не смогли заблокировать за 2с — не рискуем
|
||||
try:
|
||||
data = os.read(fd, 65536)
|
||||
if data:
|
||||
return json.loads(data.decode("utf-8"))
|
||||
except (json.JSONDecodeError, UnicodeDecodeError):
|
||||
pass
|
||||
pass # битый файл — переживём, при следующей записи перезапишется
|
||||
return {}
|
||||
finally:
|
||||
fcntl.flock(fd, fcntl.LOCK_UN)
|
||||
fcntl.flock(fd, fcntl.LOCK_UN) # ВСЕГДА снимаем блокировку
|
||||
os.close(fd)
|
||||
|
||||
|
||||
def _locked_write(path, data):
|
||||
"""Пишет файл под эксклюзивной блокировкой."""
|
||||
"""Пишет JSON-файл под эксклюзивной блокировкой.
|
||||
|
||||
Полностью перезаписывает файл: lseek(0) + ftruncate + write."""
|
||||
try:
|
||||
fd = os.open(path, os.O_RDWR | os.O_CREAT, 0o644)
|
||||
except OSError:
|
||||
return
|
||||
try:
|
||||
if not _acquire_lock(fd):
|
||||
return
|
||||
os.lseek(fd, 0, 0)
|
||||
os.ftruncate(fd, 0)
|
||||
return # не смогли заблокировать — не пишем (данные не потеряются, просто не сохранятся)
|
||||
os.lseek(fd, 0, 0) # в начало файла
|
||||
os.ftruncate(fd, 0) # обрезать старый контент
|
||||
os.write(fd, json.dumps(data, indent=2).encode("utf-8"))
|
||||
finally:
|
||||
fcntl.flock(fd, fcntl.LOCK_UN)
|
||||
fcntl.flock(fd, fcntl.LOCK_UN) # ВСЕГДА снимаем блокировку
|
||||
os.close(fd)
|
||||
|
||||
|
||||
def add(client_id, stand, instance_uid, svc_id, display_name):
|
||||
"""Добавить инстанс в трекер.
|
||||
|
||||
Читает текущий файл → добавляет запись → пишет обратно.
|
||||
Если инстанс уже есть — перезаписывает (идемпотентно)."""
|
||||
p = _path(client_id, stand)
|
||||
data = _locked_read(p)
|
||||
data[instance_uid] = {
|
||||
@@ -72,6 +104,9 @@ def add(client_id, stand, instance_uid, svc_id, display_name):
|
||||
|
||||
|
||||
def remove(client_id, stand, instance_uid):
|
||||
"""Удалить инстанс из трекера (после успешного delete).
|
||||
|
||||
pop с default=None — не падает если инстанса уже нет."""
|
||||
p = _path(client_id, stand)
|
||||
data = _locked_read(p)
|
||||
data.pop(instance_uid, None)
|
||||
@@ -79,4 +114,5 @@ def remove(client_id, stand, instance_uid):
|
||||
|
||||
|
||||
def list_all(client_id, stand):
|
||||
"""Все записи трекера — список dict'ов {svcId, displayName, instanceUid}."""
|
||||
return list(_locked_read(_path(client_id, stand)).values())
|
||||
|
||||
Reference in New Issue
Block a user