mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-10-27 05:01:57 -06:00
Add assembly endpoints and approval flows
This commit is contained in:
@@ -19,6 +19,8 @@ from . import (
|
||||
sites,
|
||||
devices,
|
||||
credentials,
|
||||
assemblies,
|
||||
server_info,
|
||||
)
|
||||
|
||||
_REGISTRARS = (
|
||||
@@ -34,6 +36,8 @@ _REGISTRARS = (
|
||||
sites.register,
|
||||
devices.register,
|
||||
credentials.register,
|
||||
assemblies.register,
|
||||
server_info.register,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -113,4 +113,61 @@ def list_device_approvals() -> object:
|
||||
return jsonify({"approvals": [record.to_dict() for record in records]})
|
||||
|
||||
|
||||
@blueprint.route("/device-approvals/<approval_id>/approve", methods=["POST"])
|
||||
def approve_device_approval(approval_id: str) -> object:
|
||||
guard = _require_admin()
|
||||
if guard:
|
||||
return guard
|
||||
|
||||
payload = request.get_json(silent=True) or {}
|
||||
guid = payload.get("guid")
|
||||
resolution_raw = payload.get("conflict_resolution") or payload.get("resolution")
|
||||
resolution = resolution_raw.strip().lower() if isinstance(resolution_raw, str) else None
|
||||
|
||||
actor = session.get("username") if isinstance(session.get("username"), str) else None
|
||||
|
||||
try:
|
||||
result = _admin_service().approve_device_approval(
|
||||
approval_id,
|
||||
actor=actor,
|
||||
guid=guid,
|
||||
conflict_resolution=resolution,
|
||||
)
|
||||
except LookupError:
|
||||
return jsonify({"error": "not_found"}), 404
|
||||
except ValueError as exc:
|
||||
code = str(exc)
|
||||
if code == "approval_not_pending":
|
||||
return jsonify({"error": "approval_not_pending"}), 409
|
||||
if code == "conflict_resolution_required":
|
||||
return jsonify({"error": "conflict_resolution_required"}), 409
|
||||
if code == "invalid_guid":
|
||||
return jsonify({"error": "invalid_guid"}), 400
|
||||
raise
|
||||
|
||||
response = jsonify(result.to_dict())
|
||||
response.status_code = 200
|
||||
return response
|
||||
|
||||
|
||||
@blueprint.route("/device-approvals/<approval_id>/deny", methods=["POST"])
|
||||
def deny_device_approval(approval_id: str) -> object:
|
||||
guard = _require_admin()
|
||||
if guard:
|
||||
return guard
|
||||
|
||||
actor = session.get("username") if isinstance(session.get("username"), str) else None
|
||||
|
||||
try:
|
||||
result = _admin_service().deny_device_approval(approval_id, actor=actor)
|
||||
except LookupError:
|
||||
return jsonify({"error": "not_found"}), 404
|
||||
except ValueError as exc:
|
||||
if str(exc) == "approval_not_pending":
|
||||
return jsonify({"error": "approval_not_pending"}), 409
|
||||
raise
|
||||
|
||||
return jsonify(result.to_dict())
|
||||
|
||||
|
||||
__all__ = ["register", "blueprint"]
|
||||
|
||||
182
Data/Engine/interfaces/http/assemblies.py
Normal file
182
Data/Engine/interfaces/http/assemblies.py
Normal file
@@ -0,0 +1,182 @@
|
||||
"""HTTP endpoints for assembly management."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from flask import Blueprint, Flask, current_app, jsonify, request
|
||||
|
||||
from Data.Engine.services.container import EngineServiceContainer
|
||||
|
||||
blueprint = Blueprint("engine_assemblies", __name__)
|
||||
|
||||
|
||||
def register(app: Flask, _services: EngineServiceContainer) -> None:
|
||||
if "engine_assemblies" not in app.blueprints:
|
||||
app.register_blueprint(blueprint)
|
||||
|
||||
|
||||
def _services() -> EngineServiceContainer:
|
||||
services = current_app.extensions.get("engine_services")
|
||||
if services is None: # pragma: no cover - defensive
|
||||
raise RuntimeError("engine services not initialized")
|
||||
return services
|
||||
|
||||
|
||||
def _assembly_service():
|
||||
return _services().assembly_service
|
||||
|
||||
|
||||
def _value_error_response(exc: ValueError):
|
||||
code = str(exc)
|
||||
if code == "invalid_island":
|
||||
return jsonify({"error": "invalid island"}), 400
|
||||
if code == "path_required":
|
||||
return jsonify({"error": "path required"}), 400
|
||||
if code == "invalid_kind":
|
||||
return jsonify({"error": "invalid kind"}), 400
|
||||
if code == "invalid_destination":
|
||||
return jsonify({"error": "invalid destination"}), 400
|
||||
if code == "invalid_path":
|
||||
return jsonify({"error": "invalid path"}), 400
|
||||
if code == "cannot_delete_root":
|
||||
return jsonify({"error": "cannot delete root"}), 400
|
||||
return jsonify({"error": code or "invalid request"}), 400
|
||||
|
||||
|
||||
def _not_found_response(exc: FileNotFoundError):
|
||||
code = str(exc)
|
||||
if code == "file_not_found":
|
||||
return jsonify({"error": "file not found"}), 404
|
||||
if code == "folder_not_found":
|
||||
return jsonify({"error": "folder not found"}), 404
|
||||
return jsonify({"error": "not found"}), 404
|
||||
|
||||
|
||||
@blueprint.route("/api/assembly/list", methods=["GET"])
|
||||
def list_assemblies() -> object:
|
||||
island = (request.args.get("island") or "").strip()
|
||||
try:
|
||||
listing = _assembly_service().list_items(island)
|
||||
except ValueError as exc:
|
||||
return _value_error_response(exc)
|
||||
return jsonify(listing.to_dict())
|
||||
|
||||
|
||||
@blueprint.route("/api/assembly/load", methods=["GET"])
|
||||
def load_assembly() -> object:
|
||||
island = (request.args.get("island") or "").strip()
|
||||
rel_path = (request.args.get("path") or "").strip()
|
||||
try:
|
||||
result = _assembly_service().load_item(island, rel_path)
|
||||
except ValueError as exc:
|
||||
return _value_error_response(exc)
|
||||
except FileNotFoundError as exc:
|
||||
return _not_found_response(exc)
|
||||
return jsonify(result.to_dict())
|
||||
|
||||
|
||||
@blueprint.route("/api/assembly/create", methods=["POST"])
|
||||
def create_assembly() -> object:
|
||||
payload = request.get_json(silent=True) or {}
|
||||
island = (payload.get("island") or "").strip()
|
||||
kind = (payload.get("kind") or "").strip().lower()
|
||||
rel_path = (payload.get("path") or "").strip()
|
||||
content = payload.get("content")
|
||||
item_type = payload.get("type")
|
||||
try:
|
||||
result = _assembly_service().create_item(
|
||||
island,
|
||||
kind=kind,
|
||||
rel_path=rel_path,
|
||||
content=content,
|
||||
item_type=item_type if isinstance(item_type, str) else None,
|
||||
)
|
||||
except ValueError as exc:
|
||||
return _value_error_response(exc)
|
||||
return jsonify(result.to_dict())
|
||||
|
||||
|
||||
@blueprint.route("/api/assembly/edit", methods=["POST"])
|
||||
def edit_assembly() -> object:
|
||||
payload = request.get_json(silent=True) or {}
|
||||
island = (payload.get("island") or "").strip()
|
||||
rel_path = (payload.get("path") or "").strip()
|
||||
content = payload.get("content")
|
||||
item_type = payload.get("type")
|
||||
try:
|
||||
result = _assembly_service().edit_item(
|
||||
island,
|
||||
rel_path=rel_path,
|
||||
content=content,
|
||||
item_type=item_type if isinstance(item_type, str) else None,
|
||||
)
|
||||
except ValueError as exc:
|
||||
return _value_error_response(exc)
|
||||
except FileNotFoundError as exc:
|
||||
return _not_found_response(exc)
|
||||
return jsonify(result.to_dict())
|
||||
|
||||
|
||||
@blueprint.route("/api/assembly/rename", methods=["POST"])
|
||||
def rename_assembly() -> object:
|
||||
payload = request.get_json(silent=True) or {}
|
||||
island = (payload.get("island") or "").strip()
|
||||
kind = (payload.get("kind") or "").strip().lower()
|
||||
rel_path = (payload.get("path") or "").strip()
|
||||
new_name = (payload.get("new_name") or "").strip()
|
||||
item_type = payload.get("type")
|
||||
try:
|
||||
result = _assembly_service().rename_item(
|
||||
island,
|
||||
kind=kind,
|
||||
rel_path=rel_path,
|
||||
new_name=new_name,
|
||||
item_type=item_type if isinstance(item_type, str) else None,
|
||||
)
|
||||
except ValueError as exc:
|
||||
return _value_error_response(exc)
|
||||
except FileNotFoundError as exc:
|
||||
return _not_found_response(exc)
|
||||
return jsonify(result.to_dict())
|
||||
|
||||
|
||||
@blueprint.route("/api/assembly/move", methods=["POST"])
|
||||
def move_assembly() -> object:
|
||||
payload = request.get_json(silent=True) or {}
|
||||
island = (payload.get("island") or "").strip()
|
||||
rel_path = (payload.get("path") or "").strip()
|
||||
new_path = (payload.get("new_path") or "").strip()
|
||||
kind = (payload.get("kind") or "").strip().lower()
|
||||
try:
|
||||
result = _assembly_service().move_item(
|
||||
island,
|
||||
rel_path=rel_path,
|
||||
new_path=new_path,
|
||||
kind=kind,
|
||||
)
|
||||
except ValueError as exc:
|
||||
return _value_error_response(exc)
|
||||
except FileNotFoundError as exc:
|
||||
return _not_found_response(exc)
|
||||
return jsonify(result.to_dict())
|
||||
|
||||
|
||||
@blueprint.route("/api/assembly/delete", methods=["POST"])
|
||||
def delete_assembly() -> object:
|
||||
payload = request.get_json(silent=True) or {}
|
||||
island = (payload.get("island") or "").strip()
|
||||
rel_path = (payload.get("path") or "").strip()
|
||||
kind = (payload.get("kind") or "").strip().lower()
|
||||
try:
|
||||
result = _assembly_service().delete_item(
|
||||
island,
|
||||
rel_path=rel_path,
|
||||
kind=kind,
|
||||
)
|
||||
except ValueError as exc:
|
||||
return _value_error_response(exc)
|
||||
except FileNotFoundError as exc:
|
||||
return _not_found_response(exc)
|
||||
return jsonify(result.to_dict())
|
||||
|
||||
|
||||
__all__ = ["register", "blueprint"]
|
||||
53
Data/Engine/interfaces/http/server_info.py
Normal file
53
Data/Engine/interfaces/http/server_info.py
Normal file
@@ -0,0 +1,53 @@
|
||||
"""Server metadata endpoints."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from flask import Blueprint, Flask, jsonify
|
||||
|
||||
from Data.Engine.services.container import EngineServiceContainer
|
||||
|
||||
blueprint = Blueprint("engine_server_info", __name__)
|
||||
|
||||
|
||||
def register(app: Flask, _services: EngineServiceContainer) -> None:
|
||||
if "engine_server_info" not in app.blueprints:
|
||||
app.register_blueprint(blueprint)
|
||||
|
||||
|
||||
@blueprint.route("/api/server/time", methods=["GET"])
|
||||
def server_time() -> object:
|
||||
now_local = datetime.now().astimezone()
|
||||
now_utc = datetime.now(timezone.utc)
|
||||
tzinfo = now_local.tzinfo
|
||||
offset = tzinfo.utcoffset(now_local) if tzinfo else None
|
||||
|
||||
def _ordinal(n: int) -> str:
|
||||
if 11 <= (n % 100) <= 13:
|
||||
suffix = "th"
|
||||
else:
|
||||
suffix = {1: "st", 2: "nd", 3: "rd"}.get(n % 10, "th")
|
||||
return f"{n}{suffix}"
|
||||
|
||||
month = now_local.strftime("%B")
|
||||
day_disp = _ordinal(now_local.day)
|
||||
year = now_local.strftime("%Y")
|
||||
hour24 = now_local.hour
|
||||
hour12 = hour24 % 12 or 12
|
||||
minute = now_local.minute
|
||||
ampm = "AM" if hour24 < 12 else "PM"
|
||||
display = f"{month} {day_disp} {year} @ {hour12}:{minute:02d}{ampm}"
|
||||
|
||||
payload = {
|
||||
"epoch": int(now_local.timestamp()),
|
||||
"iso": now_local.isoformat(),
|
||||
"utc_iso": now_utc.isoformat().replace("+00:00", "Z"),
|
||||
"timezone": str(tzinfo) if tzinfo else "",
|
||||
"offset_seconds": int(offset.total_seconds()) if offset else 0,
|
||||
"display": display,
|
||||
}
|
||||
return jsonify(payload)
|
||||
|
||||
|
||||
__all__ = ["register", "blueprint"]
|
||||
Reference in New Issue
Block a user