keepup-http 0.5.0__py3-none-any.whl
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.
- keepup_http/__init__.py +38 -0
- keepup_http/api_docs.py +76 -0
- keepup_http/body_limit.py +156 -0
- keepup_http/events_api.py +237 -0
- keepup_http/factory.py +562 -0
- keepup_http/lock_routes.py +155 -0
- keepup_http/plugin.py +53 -0
- keepup_http/routes.py +549 -0
- keepup_http/static/css/aos.css +1 -0
- keepup_http/static/css/main_nebula.css +232 -0
- keepup_http/static/css/main_new.css +869 -0
- keepup_http/static/css/tailwind.css +1 -0
- keepup_http/static/index_nebula.html +282 -0
- keepup_http/static/index_new.html +275 -0
- keepup_http/static/js/aos.js +1 -0
- keepup_http/static/js/feather-icons.js +13 -0
- keepup_http/static/js/layout_bootstrap.js +29 -0
- keepup_http/static/js/main_new.js +2076 -0
- keepup_http/static/js/tailwind.js +83 -0
- keepup_http/static/modules/css/background_tasks.css +233 -0
- keepup_http/static/modules/css/cluster.css +16 -0
- keepup_http/static/modules/css/event_manager.css +386 -0
- keepup_http/static/modules/css/integration_logs.css +33 -0
- keepup_http/static/modules/css/metrics.css +115 -0
- keepup_http/static/modules/css/modules.css +189 -0
- keepup_http/static/modules/css/themes.css +563 -0
- keepup_http/static/modules/css/users.css +278 -0
- keepup_http/static/modules/js/background_tasks.js +695 -0
- keepup_http/static/modules/js/chart.js +14 -0
- keepup_http/static/modules/js/chartjs-adapter-date-fns.bundle.min.js +7 -0
- keepup_http/static/modules/js/cluster.js +374 -0
- keepup_http/static/modules/js/event_manager.js +1007 -0
- keepup_http/static/modules/js/integration_logs.js +795 -0
- keepup_http/static/modules/js/metrics.js +929 -0
- keepup_http/static/modules/js/modules.js +1128 -0
- keepup_http/static/modules/js/themes.js +684 -0
- keepup_http/static/modules/js/users.js +898 -0
- keepup_http/web.py +214 -0
- keepup_http-0.5.0.dist-info/METADATA +29 -0
- keepup_http-0.5.0.dist-info/RECORD +43 -0
- keepup_http-0.5.0.dist-info/WHEEL +5 -0
- keepup_http-0.5.0.dist-info/entry_points.txt +2 -0
- keepup_http-0.5.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""The panel's lock endpoints (keepup-121).
|
|
2
|
+
|
|
3
|
+
They were the tail of the kernel's `locks.py`, which is why the base imported
|
|
4
|
+
FastAPI for a panel it does not otherwise serve. The panel is the HTTP
|
|
5
|
+
transport's; what stays in the kernel is the machinery -- the advisory locks, the
|
|
6
|
+
counter of forced releases and the samples the metrics capability asks for.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
from fastapi import Depends, HTTPException, Query, status
|
|
12
|
+
|
|
13
|
+
from keepup import locks as lock_manager
|
|
14
|
+
from keepup_auth import user_roles
|
|
15
|
+
from keepup_auth.dependencies import get_current_admin
|
|
16
|
+
from keepup.roles import ROLE_ADMIN
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
__all__ = ["register_lock_routes"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def register_lock_routes(app):
|
|
24
|
+
"""Register the administrative lock endpoints on the application."""
|
|
25
|
+
|
|
26
|
+
@app.get("/api/admin/locks")
|
|
27
|
+
def get_active_locks(admin: dict = Depends(get_current_admin)):
|
|
28
|
+
"""Return the active locks (administrators only)."""
|
|
29
|
+
from keepup_db import DatabaseManagerV2
|
|
30
|
+
try:
|
|
31
|
+
if not user_roles.has_role(admin, ROLE_ADMIN):
|
|
32
|
+
logger.warning(f"Non-admin user {admin['username']} attempted to access locks endpoint")
|
|
33
|
+
raise HTTPException(
|
|
34
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
35
|
+
detail="Admin access required"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
locks = DatabaseManagerV2.execute(
|
|
39
|
+
"SELECT * FROM distributed_locks ORDER BY acquired_at DESC"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
logger.info(f"Admin {admin['username']} viewed active locks, count: {len(locks)}")
|
|
43
|
+
return {"locks": locks, "count": len(locks)}
|
|
44
|
+
|
|
45
|
+
except Exception as e:
|
|
46
|
+
logger.error(f"Error retrieving locks: {str(e)}")
|
|
47
|
+
raise HTTPException(
|
|
48
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
49
|
+
detail="Error retrieving lock information"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@app.delete("/api/admin/locks/{lock_name}")
|
|
54
|
+
def force_release_lock(
|
|
55
|
+
lock_name: str,
|
|
56
|
+
admin: dict = Depends(get_current_admin),
|
|
57
|
+
confirm: bool = Query(False, description="Confirm force release")
|
|
58
|
+
):
|
|
59
|
+
"""Force-release a lock (administrators only)."""
|
|
60
|
+
from keepup_db import DatabaseManagerV2
|
|
61
|
+
try:
|
|
62
|
+
if not user_roles.has_role(admin, ROLE_ADMIN):
|
|
63
|
+
logger.warning(f"Non-admin user {admin['username']} attempted to release lock {lock_name}")
|
|
64
|
+
raise HTTPException(
|
|
65
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
66
|
+
detail="Admin access required"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
existing_lock = DatabaseManagerV2.execute_one(
|
|
70
|
+
"SELECT * FROM distributed_locks WHERE lock_name = :lock_name",
|
|
71
|
+
{"lock_name": lock_name}
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if not existing_lock:
|
|
75
|
+
logger.info(f"Admin {admin['username']} attempted to release non-existent lock: {lock_name}")
|
|
76
|
+
return {
|
|
77
|
+
"success": False,
|
|
78
|
+
"message": f"Lock '{lock_name}' not found"
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if not confirm:
|
|
82
|
+
return {
|
|
83
|
+
"success": False,
|
|
84
|
+
"message": "Confirmation required. Use ?confirm=true to force release the lock.",
|
|
85
|
+
"lock_info": {
|
|
86
|
+
"lock_name": existing_lock["lock_name"],
|
|
87
|
+
"instance_id": existing_lock["instance_id"],
|
|
88
|
+
"acquired_at": existing_lock["acquired_at"]
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
result = DatabaseManagerV2.execute_commit(
|
|
93
|
+
"DELETE FROM distributed_locks WHERE lock_name = :lock_name",
|
|
94
|
+
{"lock_name": lock_name}
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
if result:
|
|
98
|
+
logger.warning(
|
|
99
|
+
f"Lock '{lock_name}' force-released by admin {admin['username']}. "
|
|
100
|
+
f"Was held by instance: {existing_lock['instance_id']}, "
|
|
101
|
+
f"acquired at: {existing_lock['acquired_at']}"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Counted and published as a sample by whoever collects it;
|
|
105
|
+
# never written into `system_metrics`, which belongs to the
|
|
106
|
+
# metrics capability -- a lock is the kernel's business
|
|
107
|
+
# (keepup-109).
|
|
108
|
+
global _force_released
|
|
109
|
+
|
|
110
|
+
_force_released += 1
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
"success": True,
|
|
114
|
+
"message": f"Lock '{lock_name}' force-released successfully",
|
|
115
|
+
"released_lock": {
|
|
116
|
+
"lock_name": existing_lock["lock_name"],
|
|
117
|
+
"instance_id": existing_lock["instance_id"],
|
|
118
|
+
"acquired_at": existing_lock["acquired_at"]
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
else:
|
|
122
|
+
return {
|
|
123
|
+
"success": False,
|
|
124
|
+
"message": "Failed to release lock"
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
except Exception as e:
|
|
128
|
+
logger.error(f"Error force-releasing lock '{lock_name}': {str(e)}")
|
|
129
|
+
raise HTTPException(
|
|
130
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
131
|
+
detail="Error releasing lock"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@app.get("/api/admin/locks/stats")
|
|
136
|
+
def get_locks_stats(admin: dict = Depends(get_current_admin)):
|
|
137
|
+
"""Return lock statistics (administrators only)."""
|
|
138
|
+
try:
|
|
139
|
+
if not user_roles.has_role(admin, ROLE_ADMIN):
|
|
140
|
+
raise HTTPException(
|
|
141
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
142
|
+
detail="Admin access required"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
stats = lock_manager.lock_stats()
|
|
146
|
+
|
|
147
|
+
logger.info(f"Admin {admin['username']} viewed locks statistics")
|
|
148
|
+
return stats
|
|
149
|
+
|
|
150
|
+
except Exception as e:
|
|
151
|
+
logger.error(f"Error retrieving locks stats: {str(e)}")
|
|
152
|
+
raise HTTPException(
|
|
153
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
154
|
+
detail="Error retrieving lock statistics"
|
|
155
|
+
)
|
keepup_http/plugin.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""The HTTP transport as a plugin: the application and how it is served (keepup-121).
|
|
2
|
+
|
|
3
|
+
The kernel resolves the catalogue, runs the lifecycle, collects what plugins
|
|
4
|
+
contribute and offers a seam for a transport. This capability is the transport
|
|
5
|
+
for HTTP: it builds the ASGI application, mounts the routes, sockets and
|
|
6
|
+
middleware the other plugins declare, serves the panel shell and answers
|
|
7
|
+
`/metrics`.
|
|
8
|
+
|
|
9
|
+
It is being filled in one slice at a time. Today it carries the documentation
|
|
10
|
+
pages and the event log's routes; the application assembly, the panel shell and
|
|
11
|
+
the route wrapper still live in the base and are reached through the
|
|
12
|
+
compatibility layer (keepup-122), which is what this plugin replaces.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
from typing import Any, Dict
|
|
17
|
+
|
|
18
|
+
from keepup.kernel.descriptor import KIND_TRANSPORT, PluginDescriptor
|
|
19
|
+
from keepup.kernel.transports import TransportPlugin
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
__all__ = ["HttpPlugin", "SERVICE_HTTP"]
|
|
24
|
+
|
|
25
|
+
#: What another plugin asks for to reach the application this transport built.
|
|
26
|
+
SERVICE_HTTP = "http"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class HttpPlugin(TransportPlugin):
|
|
30
|
+
"""The application, and the way in."""
|
|
31
|
+
|
|
32
|
+
descriptor = PluginDescriptor(
|
|
33
|
+
id="http",
|
|
34
|
+
name="HTTP transport",
|
|
35
|
+
version="0.5.0",
|
|
36
|
+
distribution="keepup-http",
|
|
37
|
+
kind=KIND_TRANSPORT,
|
|
38
|
+
priority=5,
|
|
39
|
+
provides=(f"{SERVICE_HTTP}>=1",),
|
|
40
|
+
contributions=("routes", "sockets", "middleware", "transport"),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def register(self, services):
|
|
44
|
+
"""Offer the application factory to whoever asks for it."""
|
|
45
|
+
services.provide(SERVICE_HTTP, self, version=1, plugin_id="http")
|
|
46
|
+
|
|
47
|
+
def get_handlers(self) -> Dict[str, Any]:
|
|
48
|
+
"""What another plugin may call: the documentation and event registrars."""
|
|
49
|
+
from keepup_http.api_docs import register_api_documentation
|
|
50
|
+
from keepup_http.events_api import register_event_api_routes
|
|
51
|
+
|
|
52
|
+
return {"register_api_documentation": register_api_documentation,
|
|
53
|
+
"register_event_api_routes": register_event_api_routes}
|