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.
Files changed (43) hide show
  1. keepup_http/__init__.py +38 -0
  2. keepup_http/api_docs.py +76 -0
  3. keepup_http/body_limit.py +156 -0
  4. keepup_http/events_api.py +237 -0
  5. keepup_http/factory.py +562 -0
  6. keepup_http/lock_routes.py +155 -0
  7. keepup_http/plugin.py +53 -0
  8. keepup_http/routes.py +549 -0
  9. keepup_http/static/css/aos.css +1 -0
  10. keepup_http/static/css/main_nebula.css +232 -0
  11. keepup_http/static/css/main_new.css +869 -0
  12. keepup_http/static/css/tailwind.css +1 -0
  13. keepup_http/static/index_nebula.html +282 -0
  14. keepup_http/static/index_new.html +275 -0
  15. keepup_http/static/js/aos.js +1 -0
  16. keepup_http/static/js/feather-icons.js +13 -0
  17. keepup_http/static/js/layout_bootstrap.js +29 -0
  18. keepup_http/static/js/main_new.js +2076 -0
  19. keepup_http/static/js/tailwind.js +83 -0
  20. keepup_http/static/modules/css/background_tasks.css +233 -0
  21. keepup_http/static/modules/css/cluster.css +16 -0
  22. keepup_http/static/modules/css/event_manager.css +386 -0
  23. keepup_http/static/modules/css/integration_logs.css +33 -0
  24. keepup_http/static/modules/css/metrics.css +115 -0
  25. keepup_http/static/modules/css/modules.css +189 -0
  26. keepup_http/static/modules/css/themes.css +563 -0
  27. keepup_http/static/modules/css/users.css +278 -0
  28. keepup_http/static/modules/js/background_tasks.js +695 -0
  29. keepup_http/static/modules/js/chart.js +14 -0
  30. keepup_http/static/modules/js/chartjs-adapter-date-fns.bundle.min.js +7 -0
  31. keepup_http/static/modules/js/cluster.js +374 -0
  32. keepup_http/static/modules/js/event_manager.js +1007 -0
  33. keepup_http/static/modules/js/integration_logs.js +795 -0
  34. keepup_http/static/modules/js/metrics.js +929 -0
  35. keepup_http/static/modules/js/modules.js +1128 -0
  36. keepup_http/static/modules/js/themes.js +684 -0
  37. keepup_http/static/modules/js/users.js +898 -0
  38. keepup_http/web.py +214 -0
  39. keepup_http-0.5.0.dist-info/METADATA +29 -0
  40. keepup_http-0.5.0.dist-info/RECORD +43 -0
  41. keepup_http-0.5.0.dist-info/WHEEL +5 -0
  42. keepup_http-0.5.0.dist-info/entry_points.txt +2 -0
  43. 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}