keepup-admin 0.3.0__tar.gz → 0.4.0__tar.gz

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 (137) hide show
  1. {keepup_admin-0.3.0/keepup_admin.egg-info → keepup_admin-0.4.0}/PKG-INFO +31 -9
  2. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/README.md +5 -4
  3. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/__init__.py +6 -0
  4. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/audit.py +4 -29
  5. keepup_admin-0.4.0/builtin/audit.py +107 -0
  6. keepup_admin-0.4.0/builtin/auth.py +100 -0
  7. keepup_admin-0.4.0/builtin/db.py +172 -0
  8. keepup_admin-0.4.0/builtin/integration_logs.py +297 -0
  9. keepup_admin-0.4.0/builtin/metrics.py +226 -0
  10. keepup_admin-0.4.0/builtin/postgres.py +70 -0
  11. keepup_admin-0.4.0/builtin/sqlite.py +68 -0
  12. keepup_admin-0.4.0/builtin/ui.py +146 -0
  13. keepup_admin-0.4.0/builtin/users.py +137 -0
  14. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/cluster.py +13 -1
  15. keepup_admin-0.4.0/compat.py +270 -0
  16. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/factory.py +59 -0
  17. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/integrations.py +4 -1
  18. {keepup_admin-0.3.0 → keepup_admin-0.4.0/keepup_admin.egg-info}/PKG-INFO +31 -9
  19. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/keepup_admin.egg-info/SOURCES.txt +46 -72
  20. keepup_admin-0.4.0/keepup_admin.egg-info/requires.txt +55 -0
  21. keepup_admin-0.4.0/kernel/__init__.py +120 -0
  22. keepup_admin-0.4.0/kernel/call.py +304 -0
  23. keepup_admin-0.4.0/kernel/catalogue.py +243 -0
  24. keepup_admin-0.4.0/kernel/contributions.py +317 -0
  25. keepup_admin-0.4.0/kernel/datasource.py +95 -0
  26. keepup_admin-0.4.0/kernel/descriptor.py +209 -0
  27. keepup_admin-0.4.0/kernel/lifecycle.py +814 -0
  28. keepup_admin-0.4.0/kernel/loader.py +240 -0
  29. keepup_admin-0.4.0/kernel/observability.py +127 -0
  30. keepup_admin-0.4.0/kernel/security.py +113 -0
  31. keepup_admin-0.4.0/kernel/services.py +267 -0
  32. keepup_admin-0.4.0/kernel/transports.py +119 -0
  33. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/locks.py +8 -1
  34. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/metrics.py +1 -1
  35. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/metrics_api.py +4 -1
  36. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/metrics_retention.py +3 -1
  37. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/modules.py +10 -1
  38. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/notification_bus.py +3 -1
  39. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/plugins/admin.py +3 -1
  40. keepup_admin-0.4.0/plugins/builtin.json +283 -0
  41. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/plugins/registry.py +81 -44
  42. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/plugins/routes.py +148 -117
  43. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/pyproject.toml +72 -21
  44. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/retention.py +4 -1
  45. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/schema.py +19 -124
  46. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/settings.py +11 -0
  47. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/themes.py +4 -16
  48. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/web.py +8 -1
  49. keepup_admin-0.3.0/auth/__init__.py +0 -7
  50. keepup_admin-0.3.0/auth/config.py +0 -128
  51. keepup_admin-0.3.0/auth/dependencies.py +0 -556
  52. keepup_admin-0.3.0/auth/dto/__init__.py +0 -0
  53. keepup_admin-0.3.0/auth/dto/token.py +0 -31
  54. keepup_admin-0.3.0/auth/external_accounts.py +0 -144
  55. keepup_admin-0.3.0/auth/factory.py +0 -28
  56. keepup_admin-0.3.0/auth/identity/__init__.py +0 -33
  57. keepup_admin-0.3.0/auth/identity/access.py +0 -119
  58. keepup_admin-0.3.0/auth/identity/accounts.py +0 -86
  59. keepup_admin-0.3.0/auth/identity/config.py +0 -220
  60. keepup_admin-0.3.0/auth/identity/contract.py +0 -174
  61. keepup_admin-0.3.0/auth/identity/loader.py +0 -119
  62. keepup_admin-0.3.0/auth/identity/runtime.py +0 -265
  63. keepup_admin-0.3.0/auth/login_throttle.py +0 -181
  64. keepup_admin-0.3.0/auth/oidc.py +0 -255
  65. keepup_admin-0.3.0/auth/oidc_policy.py +0 -122
  66. keepup_admin-0.3.0/auth/oidc_routes.py +0 -265
  67. keepup_admin-0.3.0/auth/panel_session.py +0 -386
  68. keepup_admin-0.3.0/auth/permissions.py +0 -59
  69. keepup_admin-0.3.0/auth/providers/__init__.py +0 -0
  70. keepup_admin-0.3.0/auth/providers/base.py +0 -216
  71. keepup_admin-0.3.0/auth/providers/local.py +0 -212
  72. keepup_admin-0.3.0/auth/routes.py +0 -514
  73. keepup_admin-0.3.0/auth/seed_accounts.py +0 -352
  74. keepup_admin-0.3.0/auth/session_lifetime.py +0 -104
  75. keepup_admin-0.3.0/auth/signing_key.py +0 -138
  76. keepup_admin-0.3.0/auth/socket_sessions.py +0 -176
  77. keepup_admin-0.3.0/auth/user_roles.py +0 -376
  78. keepup_admin-0.3.0/auth/user_routes.py +0 -313
  79. keepup_admin-0.3.0/auth/usernames.py +0 -36
  80. keepup_admin-0.3.0/auth/websocket.py +0 -88
  81. keepup_admin-0.3.0/db.py +0 -603
  82. keepup_admin-0.3.0/events.py +0 -506
  83. keepup_admin-0.3.0/keepup_admin.egg-info/requires.txt +0 -25
  84. keepup_admin-0.3.0/positional_sql.py +0 -120
  85. keepup_admin-0.3.0/tables.py +0 -304
  86. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/LICENSE +0 -0
  87. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/NOTICE +0 -0
  88. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/THIRD-PARTY.md +0 -0
  89. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/admin_trail.py +0 -0
  90. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/api_docs.py +0 -0
  91. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/api_versions.py +0 -0
  92. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/body_limit.py +0 -0
  93. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/cache.py +0 -0
  94. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/events_api.py +0 -0
  95. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/instance.py +0 -0
  96. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/keepup_admin.egg-info/dependency_links.txt +0 -0
  97. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/keepup_admin.egg-info/top_level.txt +0 -0
  98. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/log_shipping.py +0 -0
  99. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/logging_setup.py +0 -0
  100. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/plugins/__init__.py +0 -0
  101. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/plugins/base.py +0 -0
  102. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/plugins/enablement.py +0 -0
  103. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/plugins/route_mask.py +0 -0
  104. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/roles.py +0 -0
  105. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/scheduler.py +0 -0
  106. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/sections.json +0 -0
  107. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/security.py +0 -0
  108. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/setup.cfg +0 -0
  109. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/css/aos.css +0 -0
  110. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/css/main_nebula.css +0 -0
  111. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/css/main_new.css +0 -0
  112. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/css/tailwind.css +0 -0
  113. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/index_nebula.html +0 -0
  114. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/index_new.html +0 -0
  115. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/js/aos.js +0 -0
  116. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/js/feather-icons.js +0 -0
  117. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/js/layout_bootstrap.js +0 -0
  118. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/js/main_new.js +0 -0
  119. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/js/tailwind.js +0 -0
  120. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/modules/css/background_tasks.css +0 -0
  121. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/modules/css/cluster.css +0 -0
  122. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/modules/css/event_manager.css +0 -0
  123. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/modules/css/integration_logs.css +0 -0
  124. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/modules/css/metrics.css +0 -0
  125. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/modules/css/modules.css +0 -0
  126. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/modules/css/themes.css +0 -0
  127. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/modules/css/users.css +0 -0
  128. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/modules/js/background_tasks.js +0 -0
  129. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/modules/js/chart.js +0 -0
  130. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/modules/js/chartjs-adapter-date-fns.bundle.min.js +0 -0
  131. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/modules/js/cluster.js +0 -0
  132. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/modules/js/event_manager.js +0 -0
  133. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/modules/js/integration_logs.js +0 -0
  134. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/modules/js/metrics.js +0 -0
  135. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/modules/js/modules.js +0 -0
  136. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/modules/js/themes.js +0 -0
  137. {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/modules/js/users.js +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: keepup-admin
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: An admin framework over FastAPI: plugins, panel, users, replicas
5
5
  Author-email: Fedor Sergeev <fksergeev@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -22,10 +22,7 @@ Requires-Dist: fastapi<1,>=0.128
22
22
  Requires-Dist: starlette<2,>=1.3.1
23
23
  Requires-Dist: pydantic<3,>=2.11
24
24
  Requires-Dist: sqlalchemy<3,>=2.0
25
- Requires-Dist: psycopg2-binary>=2.9
26
25
  Requires-Dist: apscheduler<4,>=3.10
27
- Requires-Dist: bcrypt<5,>=4.0
28
- Requires-Dist: pyjwt[crypto]<3,>=2.15
29
26
  Requires-Dist: python-multipart>=0.0.31
30
27
  Requires-Dist: PyYAML<7,>=6.0
31
28
  Requires-Dist: psutil>=7.0
@@ -39,8 +36,32 @@ Requires-Dist: pytest>=8; extra == "test"
39
36
  Requires-Dist: pytest-asyncio>=0.24; extra == "test"
40
37
  Requires-Dist: pytest-cov>=5; extra == "test"
41
38
  Requires-Dist: uvicorn>=0.30; extra == "test"
39
+ Provides-Extra: db
40
+ Requires-Dist: keepup-db>=0.4.0; extra == "db"
41
+ Provides-Extra: auth
42
+ Requires-Dist: keepup-auth>=0.4.0; extra == "auth"
43
+ Provides-Extra: users
44
+ Requires-Dist: keepup-users>=0.4.0; extra == "users"
45
+ Provides-Extra: ui
46
+ Requires-Dist: keepup-ui>=0.4.0; extra == "ui"
47
+ Provides-Extra: audit
48
+ Requires-Dist: keepup-audit>=0.4.0; extra == "audit"
49
+ Provides-Extra: metrics
50
+ Requires-Dist: keepup-metrics>=0.4.0; extra == "metrics"
51
+ Provides-Extra: integration-log
52
+ Requires-Dist: keepup-integration-log>=0.4.0; extra == "integration-log"
53
+ Provides-Extra: panel
54
+ Requires-Dist: keepup-db>=0.4.0; extra == "panel"
55
+ Requires-Dist: keepup-auth>=0.4.0; extra == "panel"
56
+ Requires-Dist: keepup-users>=0.4.0; extra == "panel"
57
+ Requires-Dist: keepup-ui>=0.4.0; extra == "panel"
58
+ Requires-Dist: keepup-audit>=0.4.0; extra == "panel"
59
+ Requires-Dist: keepup-metrics>=0.4.0; extra == "panel"
60
+ Requires-Dist: keepup-integration-log>=0.4.0; extra == "panel"
42
61
  Provides-Extra: postgres
43
- Requires-Dist: asyncpg>=0.31; extra == "postgres"
62
+ Requires-Dist: keepup-postgres>=0.4.0; extra == "postgres"
63
+ Provides-Extra: sqlite
64
+ Requires-Dist: keepup-sqlite>=0.4.0; extra == "sqlite"
44
65
  Dynamic: license-file
45
66
 
46
67
  # keepup
@@ -125,16 +146,17 @@ between replicas needs.
125
146
  ## Tests
126
147
 
127
148
  ```bash
128
- pip install -e ".[test]"
149
+ pip install -e ".[test]" -e packages/keepup-*
129
150
  pytest tests/
130
151
  ```
131
152
 
132
153
  **Install first, then test.** Pointing pytest at the checkout does not work on
133
154
  its own: `import keepup` has to resolve, and the directory a clone lands in is
134
155
  called `keepup-admin`, so nothing is named `keepup` until the package is
135
- installed. The `test` extra carries what the suite needs beyond the package —
136
- notably `cryptography`, which the framework itself never imports and the OIDC
137
- tests sign tokens with.
156
+ installed. The capabilities are distributions of their own in `packages/` —
157
+ each carries the libraries it needs, and the suite imports them — so a clone
158
+ installs them from the tree; the extras name the same ones for an installation
159
+ from the index.
138
160
 
139
161
  The suite runs against SQLite with no network, needs no application beside the
140
162
  package, and holds a coverage floor declared in `pyproject.toml`. Checks that
@@ -80,16 +80,17 @@ between replicas needs.
80
80
  ## Tests
81
81
 
82
82
  ```bash
83
- pip install -e ".[test]"
83
+ pip install -e ".[test]" -e packages/keepup-*
84
84
  pytest tests/
85
85
  ```
86
86
 
87
87
  **Install first, then test.** Pointing pytest at the checkout does not work on
88
88
  its own: `import keepup` has to resolve, and the directory a clone lands in is
89
89
  called `keepup-admin`, so nothing is named `keepup` until the package is
90
- installed. The `test` extra carries what the suite needs beyond the package —
91
- notably `cryptography`, which the framework itself never imports and the OIDC
92
- tests sign tokens with.
90
+ installed. The capabilities are distributions of their own in `packages/` —
91
+ each carries the libraries it needs, and the suite imports them — so a clone
92
+ installs them from the tree; the extras name the same ones for an installation
93
+ from the index.
93
94
 
94
95
  The suite runs against SQLite with no network, needs no application beside the
95
96
  package, and holds a coverage floor declared in `pyproject.toml`. Checks that
@@ -26,6 +26,12 @@ except Exception:
26
26
  # itself, until the applications switch to installing by version.
27
27
  __version__ = "0.0.0+source"
28
28
 
29
+ # Names that moved into a distribution of their own keep working for one
30
+ # release, and say where they went (keepup-114, keepup-124).
31
+ from keepup import compat as _compat
32
+
33
+ _compat.install()
34
+
29
35
  __all__ = ["KeepupSettings", "StaticMount", "create_app", "__version__"]
30
36
 
31
37
 
@@ -22,8 +22,6 @@ from contextlib import asynccontextmanager
22
22
  from datetime import datetime
23
23
  from typing import Any, Dict, Optional
24
24
 
25
- from sqlalchemy import CheckConstraint, Column, DateTime, Index, Integer, Text, UniqueConstraint
26
- from sqlalchemy.dialects import postgresql
27
25
 
28
26
  from keepup import retention, tables
29
27
  from keepup.db import DatabaseManagerV2, db_config
@@ -48,6 +46,10 @@ __all__ = [
48
46
  "retention_days",
49
47
  ]
50
48
 
49
+ # The audit capability declares its own tables (keepup-124); re-exported here so
50
+ # the path that predates the catalogue still creates them.
51
+ from keepup_audit.tables import INCOMING_REQUESTS # noqa: E402
52
+
51
53
  logger = logging.getLogger(__name__)
52
54
 
53
55
  incoming_requests_buffer: Dict[str, Dict[str, Any]] = {}
@@ -389,33 +391,6 @@ async def audit_retention_background():
389
391
  await asyncio.sleep(AUDIT_ERROR_BACKOFF_SECONDS)
390
392
 
391
393
 
392
- INCOMING_REQUESTS = tables.table(
393
- "incoming_requests",
394
- tables.auto_id(),
395
- Column("instance_id", Text, nullable=False),
396
- Column("method", Text, nullable=False),
397
- Column("endpoint", Text, nullable=False),
398
- Column("host", Text, nullable=False),
399
- Column("request_data", postgresql.JSONB().with_variant(Text(), "sqlite")),
400
- Column("request_start_at", DateTime, nullable=False),
401
- Column("request_end_at", DateTime),
402
- Column("duration_ms", Integer),
403
- Column("http_status", Integer),
404
- Column("response_data", postgresql.JSONB().with_variant(Text(), "sqlite")),
405
- Column("error_message", Text),
406
- Column("created_at", DateTime, server_default=tables.NOW),
407
- UniqueConstraint("instance_id", "method", "endpoint", "request_start_at",
408
- name="incoming_requests_instance_method_endpoint_unique").ddl_if(dialect="postgresql"),
409
- CheckConstraint("duration_ms >= 0",
410
- name="incoming_requests_check_duration").ddl_if(dialect="postgresql"),
411
- CheckConstraint("(http_status IS NULL) OR (http_status >= 100 AND http_status <= 599)",
412
- name="incoming_requests_check_http_status").ddl_if(dialect="postgresql"),
413
- Index("incoming_requests_instance_method_endpoint_unique",
414
- "instance_id", "method", "endpoint", "request_start_at", unique=True).ddl_if(dialect="sqlite"),
415
- Index("idx_incoming_requests_instance_id", "instance_id"),
416
- Index("idx_incoming_requests_endpoint", "endpoint"),
417
- Index("idx_incoming_requests_created_at", "created_at"),
418
- )
419
394
 
420
395
 
421
396
  def init_incoming_requests_table():
@@ -0,0 +1,107 @@
1
+ """The audit: incoming calls, the application's events and the administrator's trail.
2
+
3
+ Task keepup-111. These live in three modules of the kernel -- ``audit.py``
4
+ (incoming requests, redaction, retention), ``events.py`` (the application's own
5
+ event log) and ``admin_trail.py`` (what an administrator decided) -- and all
6
+ three import SQLAlchemy, so the base bundle cannot be assembled without a
7
+ database library while they stay. The plugin here owns the two tables and
8
+ publishes the two services; the kernel's own trail and the route wrapper reach
9
+ them through the names (``keepup/kernel/observability.py``), not by importing
10
+ these modules.
11
+
12
+ The routes of the event log and the section that shows it travel with the panel
13
+ (keepup-104); what is here is the recording itself, its retention and the two
14
+ names.
15
+ """
16
+
17
+ import logging
18
+ from typing import Any, Dict, List
19
+
20
+ from keepup.kernel.datasource import SERVICE_DATASOURCE
21
+ from keepup.kernel.descriptor import KIND_OPTIONAL, PluginDescriptor
22
+ from keepup.kernel.observability import SERVICE_AUDIT, SERVICE_EVENTS, Recording
23
+ from keepup.audit import INCOMING_REQUESTS
24
+ from keepup.events import APP_EVENTS
25
+ from keepup.plugins.base import BasePlugin
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+ __all__ = ["AuditPlugin", "AuditRecording"]
30
+
31
+
32
+ class AuditRecording(Recording):
33
+ """The framework's own recording, behind the kernel's two names.
34
+
35
+ It delegates to the modules that already do the work (``keepup.audit``,
36
+ ``keepup.events``) so that nothing changes for a deployment today; those
37
+ modules travel into this capability's distribution in keepup-124.
38
+ """
39
+
40
+ def __init__(self, datasource: Any = None):
41
+ from keepup.audit import IncomingRequestLogger, log_api_request
42
+
43
+ super().__init__(start=log_api_request, end=IncomingRequestLogger.end_request,
44
+ name="keepup audit")
45
+
46
+ def watch(self, emitter: Any) -> None:
47
+ """Add another emitter, for a capability that keeps events of its own."""
48
+ self.emitters.append(emitter)
49
+
50
+
51
+ class AuditPlugin(BasePlugin):
52
+ """The capability: two tables, two services and the retention they need."""
53
+
54
+ descriptor = PluginDescriptor(
55
+ id="audit",
56
+ name="Event audit",
57
+ version="0.4.0",
58
+ distribution="keepup-audit",
59
+ kind=KIND_OPTIONAL,
60
+ priority=60,
61
+ provides=("audit>=1", "events>=1"),
62
+ requires=("datasource>=1",),
63
+ wants=("locks>=1", "scheduler>=1"),
64
+ contributions=("routes", "sections", "tables", "events"),
65
+ )
66
+
67
+ def __init__(self, config=None):
68
+ super().__init__("audit", "Event audit", config)
69
+
70
+ def register(self, services):
71
+ """Publish the recording where there is a data source to write it to.
72
+
73
+ Args:
74
+ services: the runtime's registry.
75
+ """
76
+ if services.has(SERVICE_DATASOURCE):
77
+ self.recording = AuditRecording(services.require(SERVICE_DATASOURCE))
78
+ services.provide(SERVICE_AUDIT, self.recording, version=1, plugin_id="audit")
79
+ services.provide(SERVICE_EVENTS, self.recording, version=1, plugin_id="audit")
80
+
81
+ async def initialize(self):
82
+ """Keep the recording published at registration; build one if there is none."""
83
+ if getattr(self, "recording", None) is None:
84
+ self.recording = AuditRecording(self.services.require(SERVICE_DATASOURCE))
85
+ return True
86
+
87
+ def get_declared_tables(self):
88
+ """The two tables this capability owns, declared once, until keepup-124."""
89
+ return [INCOMING_REQUESTS, APP_EVENTS]
90
+
91
+ def get_event_sinks(self) -> List[Any]:
92
+ """What writes an event down, for the kernel's own trail."""
93
+ return [self.emit]
94
+
95
+ async def emit(self, event_type: str, payload: Dict[str, Any] = None) -> None:
96
+ """Write one event through the module that still owns the table."""
97
+ from keepup.events import emit_event
98
+
99
+ await emit_event(event_type, payload or {})
100
+
101
+ def get_api_routes(self):
102
+ """No routes yet: the event log's own travel with the panel (keepup-104)."""
103
+ return []
104
+
105
+ def get_handlers(self):
106
+ """What another plugin may reach instead of importing the audit."""
107
+ return {"recording": self.recording, "emit": self.emit}
@@ -0,0 +1,100 @@
1
+ """Sign-in as a capability: who is calling, and what they may do.
2
+
3
+ Task keepup-116, first half. The kernel owns the shapes and the two names
4
+ (``keepup/kernel/security.py``, keepup-119); this plugin is a deployment putting
5
+ an answer behind them, and the tables that answer needs -- the panel's sessions
6
+ and the login attempts that throttle a guesser -- belong to it.
7
+
8
+ The sign-in machinery itself (``auth/routes.py``, ``panel_session.py``,
9
+ ``login_throttle.py``, the providers, OIDC and somebody else's identity system)
10
+ travels into this capability's distribution in keepup-124, together with the
11
+ routes, the sockets and the CSRF middleware it contributes. What is here is the
12
+ claim: two services, two tables, and an identity the runtime adopts.
13
+ """
14
+
15
+ import logging
16
+ from typing import Any, Dict, List
17
+
18
+ from keepup.kernel.datasource import SERVICE_DATASOURCE
19
+ from keepup.kernel.descriptor import KIND_OPTIONAL, PluginDescriptor
20
+ from keepup.kernel.security import SERVICE_AUTH, SERVICE_PERMISSIONS, Identity
21
+ from keepup.plugins.base import BasePlugin
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+ __all__ = ["AuthPlugin", "build_identity"]
26
+
27
+
28
+ def build_identity(name: str = "keepup-auth") -> Identity:
29
+ """The framework's own sign-in, behind the shape the kernel owns.
30
+
31
+ Args:
32
+ name: what to call it in a log line.
33
+
34
+ Returns:
35
+ The identity: how a caller is found, and what decides a right.
36
+ """
37
+ from keepup.auth.dependencies import get_panel_user
38
+ from keepup.auth.identity import access
39
+
40
+ return Identity(subject_dependency=get_panel_user, checker=access.check, name=name)
41
+
42
+
43
+ class AuthPlugin(BasePlugin):
44
+ """The capability: sessions, attempts, and the two services that answer."""
45
+
46
+ descriptor = PluginDescriptor(
47
+ id="auth",
48
+ name="Sign-in and sessions",
49
+ version="0.4.0",
50
+ distribution="keepup-auth",
51
+ kind=KIND_OPTIONAL,
52
+ priority=30,
53
+ provides=("auth>=1", "permissions>=1"),
54
+ requires=("datasource>=1",),
55
+ contributions=("routes", "sockets", "middleware", "tables", "permissions"),
56
+ )
57
+
58
+ def __init__(self, config=None):
59
+ super().__init__("auth", "Sign-in and sessions", config)
60
+
61
+ def register(self, services):
62
+ """Publish the identity under both names, once there is storage.
63
+
64
+ Args:
65
+ services: the runtime's registry.
66
+ """
67
+ if services.has(SERVICE_DATASOURCE):
68
+ self.identity = build_identity()
69
+ services.provide(SERVICE_AUTH, self.identity, version=1, plugin_id="auth")
70
+ services.provide(SERVICE_PERMISSIONS, self.identity, version=1, plugin_id="auth")
71
+
72
+ async def initialize(self):
73
+ """Keep the identity published at registration; build one if there is none."""
74
+ if getattr(self, "identity", None) is None:
75
+ self.identity = build_identity()
76
+ return True
77
+
78
+ def get_declared_tables(self) -> List[Any]:
79
+ """The two tables this capability owns, declared once, until keepup-124."""
80
+ from keepup.auth.login_throttle import LOGIN_ATTEMPTS
81
+ from keepup.auth.panel_session import AUTH_SESSION
82
+
83
+ return [AUTH_SESSION, LOGIN_ATTEMPTS]
84
+
85
+ def get_route_permissions(self) -> Dict[str, Any]:
86
+ """The rights this capability's routes ask for, by name.
87
+
88
+ Empty here, and declared all the same: the kind of contribution is what
89
+ another plugin reads to know that a right it asks for exists.
90
+ """
91
+ return {}
92
+
93
+ def get_api_routes(self) -> List[Any]:
94
+ """No routes yet: the sign-in's own (sign in, out, refresh, WebSocket, CSRF)
95
+ travel into this capability's distribution in keepup-124."""
96
+ return []
97
+
98
+ def get_handlers(self) -> Dict[str, Any]:
99
+ """What another plugin may reach instead of importing the sign-in."""
100
+ return {"identity": self.identity}
@@ -0,0 +1,172 @@
1
+ """The data source as a plugin: the abstraction that holds the database library.
2
+
3
+ Task keepup-124, first slice. `keepup/kernel/datasource.py` owns the shapes and
4
+ the two names (keepup-106), the drivers answer with `spec()` (keepup-107,
5
+ keepup-108) -- and nobody published `datasource`, so no capability could reach
6
+ storage through it. This is the plugin that does: it requires a driver, publishes
7
+ the service built on the framework's current manager (`keepup.db`), and creates
8
+ nothing itself except what capabilities declare.
9
+
10
+ It answers the abstraction with the manager, so the dialect, the pool and the
11
+ creation of declared tables stay where they are today and move into
12
+ `packages/keepup-db` in the rest of keepup-124. What is fixed here is the part
13
+ that cannot be deferred: `keepup-admin` plus `keepup-db` plus one driver is a
14
+ deployment that talks to a database, and every capability asks it by name.
15
+ """
16
+
17
+ import logging
18
+ from typing import Any, Dict, List, Mapping, Optional
19
+
20
+ from keepup.db import DatabaseManagerV2, db_config
21
+ from keepup.kernel.datasource import (
22
+ SERVICE_DATASOURCE,
23
+ SERVICE_DATASOURCE_DRIVER,
24
+ DriverSpec,
25
+ )
26
+ from keepup.kernel.descriptor import KIND_REQUIRED, PluginDescriptor
27
+ from keepup.plugins.base import BasePlugin
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+ __all__ = ["DbPlugin", "DatabaseSource", "SERVICE_DATASOURCE"]
32
+
33
+
34
+ class DatabaseSource:
35
+ """The framework's manager, wearing the abstraction's shape.
36
+
37
+ Everything a capability may ask for is here and nothing else: run a
38
+ statement, run one that changes something, create what was declared, close.
39
+ Which database this is comes from the driver, not from here.
40
+ """
41
+
42
+ def __init__(self, manager: Any, spec: Optional[DriverSpec] = None):
43
+ self.manager = manager
44
+ self.spec = spec
45
+
46
+ @property
47
+ def dialect(self) -> str:
48
+ """The dialect the driver named, or the configured one."""
49
+ if self.spec is not None and self.spec.dialect:
50
+ return self.spec.dialect
51
+ return "postgresql" if db_config.is_postgres() else "sqlite"
52
+
53
+ async def execute(self, statement: str, params: Optional[Mapping[str, Any]] = None) -> Any:
54
+ """Run a statement and answer its rows."""
55
+ return await self.manager.execute_async(statement, dict(params or {}))
56
+
57
+ async def execute_one(self, statement: str,
58
+ params: Optional[Mapping[str, Any]] = None) -> Optional[Dict[str, Any]]:
59
+ """Run a statement and answer its first row, or None."""
60
+ return await self.manager.execute_one_async(statement, dict(params or {}))
61
+
62
+ async def execute_commit(self, statement: str,
63
+ params: Optional[Mapping[str, Any]] = None) -> Any:
64
+ """Run a statement that changes something, and commit it."""
65
+ return await self.manager.execute_commit_async(statement, dict(params or {}))
66
+
67
+ async def execute_many(self, statement: str, params: List[Mapping[str, Any]]) -> Any:
68
+ """Run one statement for every set of parameters."""
69
+ return await self.manager.execute_many_async(statement, [dict(item) for item in params])
70
+
71
+ def ensure_tables(self, *tables: Any) -> None:
72
+ """Create what the declaration says, and add the columns it gained.
73
+
74
+ The declarations come from the capabilities that own them, which is why
75
+ this takes them as arguments rather than knowing any itself.
76
+ """
77
+ from keepup import tables as table_language
78
+
79
+ flat = []
80
+ for table in tables:
81
+ flat.extend(table if isinstance(table, (list, tuple)) else [table])
82
+ created = [table for table in flat if table is not None]
83
+ if not created:
84
+ return
85
+ table_language.ensure_tables(*created)
86
+
87
+ def ensure_columns(self, table: Any) -> None:
88
+ """Add the columns an existing table lacks."""
89
+ from keepup import tables as table_language
90
+
91
+ table_language.ensure_columns(table)
92
+
93
+ def get_session(self) -> Any:
94
+ """A session, for a caller that needs a transaction."""
95
+ return self.manager.get_session()
96
+
97
+ def raw_connection(self) -> Any:
98
+ """A driver cursor, for code that has to go below the abstraction."""
99
+ return self.manager.raw_connection()
100
+
101
+ def pool_status(self) -> Any:
102
+ """What the pool is doing, for the metrics capability."""
103
+ reader = getattr(self.manager, "get_pool_status", None)
104
+ return reader() if callable(reader) else {}
105
+
106
+ def dispose(self) -> None:
107
+ """Close what this opened."""
108
+ self.manager.dispose()
109
+
110
+
111
+ class DbPlugin(BasePlugin):
112
+ """The capability every other one needs: storage, by name."""
113
+
114
+ descriptor = PluginDescriptor(
115
+ id="db",
116
+ name="Database abstraction",
117
+ version="0.4.0",
118
+ distribution="keepup-db",
119
+ kind=KIND_REQUIRED,
120
+ priority=10,
121
+ provides=("datasource>=1",),
122
+ requires=("datasource_driver>=1",),
123
+ contributions=("tables",),
124
+ )
125
+
126
+ def __init__(self, config=None):
127
+ super().__init__("db", "Database abstraction", config)
128
+
129
+ def register(self, services):
130
+ """Publish the data source the driver describes.
131
+
132
+ Args:
133
+ services: the runtime's registry.
134
+ """
135
+ spec = None
136
+ if services.has(SERVICE_DATASOURCE_DRIVER):
137
+ driver = services.require(SERVICE_DATASOURCE_DRIVER)
138
+ spec = driver.spec() if callable(getattr(driver, "spec", None)) else None
139
+ self.source = DatabaseSource(DatabaseManagerV2, spec)
140
+ services.provide(SERVICE_DATASOURCE, self.source, version=1, plugin_id="db")
141
+ logger.info("The data source is %s", self.source.dialect)
142
+
143
+ async def initialize(self):
144
+ """Take the driver's description, and open nothing.
145
+
146
+ The driver is read here rather than at registration: a plugin's
147
+ ``register`` runs in the order the catalogue lists, and the abstraction
148
+ may well be listed before the driver it abstracts. By the time a phase
149
+ initialises, every plugin of it has registered.
150
+ """
151
+ if self.services.has(SERVICE_DATASOURCE_DRIVER):
152
+ driver = self.services.require(SERVICE_DATASOURCE_DRIVER)
153
+ reader = getattr(driver, "spec", None)
154
+ if callable(reader):
155
+ self.source.spec = reader()
156
+ return True
157
+
158
+ def get_declared_tables(self) -> List[Any]:
159
+ """Nothing: the abstraction owns no table of the framework's.
160
+
161
+ Every table belongs to the capability that keeps it there, which is what
162
+ makes `keepup-db` an abstraction rather than a schema (keepup-106).
163
+ """
164
+ return []
165
+
166
+ def get_api_routes(self) -> List[Any]:
167
+ """No routes: storage is reached by capabilities, not by people."""
168
+ return []
169
+
170
+ def get_handlers(self) -> Dict[str, Any]:
171
+ """What another plugin may reach instead of importing the manager."""
172
+ return {"service": self.source}