firebird-admin 1.0.1__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 (136) hide show
  1. firebird_admin-1.0.1/PKG-INFO +65 -0
  2. firebird_admin-1.0.1/README.md +20 -0
  3. firebird_admin-1.0.1/fbadmin/__init__.py +7 -0
  4. firebird_admin-1.0.1/fbadmin/__main__.py +54 -0
  5. firebird_admin-1.0.1/fbadmin/ai/__init__.py +41 -0
  6. firebird_admin-1.0.1/fbadmin/ai/providers.py +72 -0
  7. firebird_admin-1.0.1/fbadmin/api/__init__.py +43 -0
  8. firebird_admin-1.0.1/fbadmin/api/admin.py +84 -0
  9. firebird_admin-1.0.1/fbadmin/api/ai.py +117 -0
  10. firebird_admin-1.0.1/fbadmin/api/auth.py +215 -0
  11. firebird_admin-1.0.1/fbadmin/api/databases.py +305 -0
  12. firebird_admin-1.0.1/fbadmin/api/deps.py +40 -0
  13. firebird_admin-1.0.1/fbadmin/api/fbsecurity.py +261 -0
  14. firebird_admin-1.0.1/fbadmin/api/jobs.py +17 -0
  15. firebird_admin-1.0.1/fbadmin/api/maintenance.py +661 -0
  16. firebird_admin-1.0.1/fbadmin/api/metrics.py +56 -0
  17. firebird_admin-1.0.1/fbadmin/api/monitoring.py +276 -0
  18. firebird_admin-1.0.1/fbadmin/api/object_dialogs.py +132 -0
  19. firebird_admin-1.0.1/fbadmin/api/query.py +159 -0
  20. firebird_admin-1.0.1/fbadmin/api/replication.py +155 -0
  21. firebird_admin-1.0.1/fbadmin/api/schedules.py +103 -0
  22. firebird_admin-1.0.1/fbadmin/api/schema.py +422 -0
  23. firebird_admin-1.0.1/fbadmin/api/servers.py +317 -0
  24. firebird_admin-1.0.1/fbadmin/api/trace.py +121 -0
  25. firebird_admin-1.0.1/fbadmin/api/version.py +142 -0
  26. firebird_admin-1.0.1/fbadmin/app.py +126 -0
  27. firebird_admin-1.0.1/fbadmin/config.py +34 -0
  28. firebird_admin-1.0.1/fbadmin/connections.py +376 -0
  29. firebird_admin-1.0.1/fbadmin/desktop.py +122 -0
  30. firebird_admin-1.0.1/fbadmin/driver_patches.py +137 -0
  31. firebird_admin-1.0.1/fbadmin/jobs.py +78 -0
  32. firebird_admin-1.0.1/fbadmin/limits.py +147 -0
  33. firebird_admin-1.0.1/fbadmin/logsetup.py +122 -0
  34. firebird_admin-1.0.1/fbadmin/maintenance/__init__.py +1 -0
  35. firebird_admin-1.0.1/fbadmin/maintenance/archive.py +73 -0
  36. firebird_admin-1.0.1/fbadmin/maintenance/cron.py +119 -0
  37. firebird_admin-1.0.1/fbadmin/maintenance/gstat.py +233 -0
  38. firebird_admin-1.0.1/fbadmin/maintenance/ops.py +406 -0
  39. firebird_admin-1.0.1/fbadmin/maintenance/scheduler.py +239 -0
  40. firebird_admin-1.0.1/fbadmin/maintenance/svc.py +358 -0
  41. firebird_admin-1.0.1/fbadmin/maintenance/trace.py +298 -0
  42. firebird_admin-1.0.1/fbadmin/metadata/__init__.py +1 -0
  43. firebird_admin-1.0.1/fbadmin/metadata/catalog.py +1166 -0
  44. firebird_admin-1.0.1/fbadmin/metadata/catalog_extras.py +449 -0
  45. firebird_admin-1.0.1/fbadmin/metadata/ddlgen.py +1020 -0
  46. firebird_admin-1.0.1/fbadmin/metadata/designer.py +375 -0
  47. firebird_admin-1.0.1/fbadmin/metadata/types.py +92 -0
  48. firebird_admin-1.0.1/fbadmin/middleware.py +16 -0
  49. firebird_admin-1.0.1/fbadmin/monitoring/__init__.py +1 -0
  50. firebird_admin-1.0.1/fbadmin/monitoring/advisor.py +305 -0
  51. firebird_admin-1.0.1/fbadmin/monitoring/history.py +286 -0
  52. firebird_admin-1.0.1/fbadmin/monitoring/journal.py +107 -0
  53. firebird_admin-1.0.1/fbadmin/monitoring/metrics.py +66 -0
  54. firebird_admin-1.0.1/fbadmin/monitoring/probes.py +374 -0
  55. firebird_admin-1.0.1/fbadmin/monitoring/profiler.py +239 -0
  56. firebird_admin-1.0.1/fbadmin/monitoring/replication.py +186 -0
  57. firebird_admin-1.0.1/fbadmin/native.py +208 -0
  58. firebird_admin-1.0.1/fbadmin/plugins.py +73 -0
  59. firebird_admin-1.0.1/fbadmin/preconfig.py +118 -0
  60. firebird_admin-1.0.1/fbadmin/query.py +373 -0
  61. firebird_admin-1.0.1/fbadmin/schema/__init__.py +2 -0
  62. firebird_admin-1.0.1/fbadmin/schema/copydata.py +144 -0
  63. firebird_admin-1.0.1/fbadmin/schema/dataio.py +657 -0
  64. firebird_admin-1.0.1/fbadmin/schema/diff.py +534 -0
  65. firebird_admin-1.0.1/fbadmin/schema/extract.py +202 -0
  66. firebird_admin-1.0.1/fbadmin/schema/generator.py +273 -0
  67. firebird_admin-1.0.1/fbadmin/schema/model.py +503 -0
  68. firebird_admin-1.0.1/fbadmin/schema/report.py +72 -0
  69. firebird_admin-1.0.1/fbadmin/schema/search.py +113 -0
  70. firebird_admin-1.0.1/fbadmin/schema_ctx.py +88 -0
  71. firebird_admin-1.0.1/fbadmin/security/__init__.py +60 -0
  72. firebird_admin-1.0.1/fbadmin/security/audit.py +96 -0
  73. firebird_admin-1.0.1/fbadmin/security/core.py +374 -0
  74. firebird_admin-1.0.1/fbadmin/security/crypto.py +147 -0
  75. firebird_admin-1.0.1/fbadmin/security/db.py +205 -0
  76. firebird_admin-1.0.1/fbadmin/security/errors.py +30 -0
  77. firebird_admin-1.0.1/fbadmin/security/fbservices.py +147 -0
  78. firebird_admin-1.0.1/fbadmin/security/fbsql.py +281 -0
  79. firebird_admin-1.0.1/fbadmin/security/ldap.py +90 -0
  80. firebird_admin-1.0.1/fbadmin/security/middleware.py +396 -0
  81. firebird_admin-1.0.1/fbadmin/security/oidc.py +116 -0
  82. firebird_admin-1.0.1/fbadmin/security/proxy.py +20 -0
  83. firebird_admin-1.0.1/fbadmin/security/readonly.py +94 -0
  84. firebird_admin-1.0.1/fbadmin/security/servers.py +419 -0
  85. firebird_admin-1.0.1/fbadmin/security/sessions.py +154 -0
  86. firebird_admin-1.0.1/fbadmin/security/settings.py +235 -0
  87. firebird_admin-1.0.1/fbadmin/security/wirecompat.py +71 -0
  88. firebird_admin-1.0.1/fbadmin/serialize.py +53 -0
  89. firebird_admin-1.0.1/fbadmin/sqlsplit.py +277 -0
  90. firebird_admin-1.0.1/fbadmin/static/assets/DataView-C2gZQZsl.js +1 -0
  91. firebird_admin-1.0.1/fbadmin/static/assets/Jobs-BbRKWT35.js +2 -0
  92. firebird_admin-1.0.1/fbadmin/static/assets/Monitor-BjEKPBW_.js +1 -0
  93. firebird_admin-1.0.1/fbadmin/static/assets/QueryTool-DKWEk7JA.js +30 -0
  94. firebird_admin-1.0.1/fbadmin/static/assets/codemirror-CQtTaafu.js +12 -0
  95. firebird_admin-1.0.1/fbadmin/static/assets/index-DVXUQWWb.css +1 -0
  96. firebird_admin-1.0.1/fbadmin/static/assets/index-n-ToRVCh.js +122 -0
  97. firebird_admin-1.0.1/fbadmin/static/assets/react-D1VdbdIl.js +40 -0
  98. firebird_admin-1.0.1/fbadmin/static/assets/sql-formatter-BMeiBS2F.js +19 -0
  99. firebird_admin-1.0.1/fbadmin/static/index.html +17 -0
  100. firebird_admin-1.0.1/fbadmin/store.py +166 -0
  101. firebird_admin-1.0.1/fbadmin/updater.py +234 -0
  102. firebird_admin-1.0.1/firebird_admin.egg-info/PKG-INFO +65 -0
  103. firebird_admin-1.0.1/firebird_admin.egg-info/SOURCES.txt +134 -0
  104. firebird_admin-1.0.1/firebird_admin.egg-info/dependency_links.txt +1 -0
  105. firebird_admin-1.0.1/firebird_admin.egg-info/entry_points.txt +3 -0
  106. firebird_admin-1.0.1/firebird_admin.egg-info/requires.txt +30 -0
  107. firebird_admin-1.0.1/firebird_admin.egg-info/top_level.txt +1 -0
  108. firebird_admin-1.0.1/pyproject.toml +75 -0
  109. firebird_admin-1.0.1/setup.cfg +4 -0
  110. firebird_admin-1.0.1/tests/test_advisor_parse.py +47 -0
  111. firebird_admin-1.0.1/tests/test_ai.py +52 -0
  112. firebird_admin-1.0.1/tests/test_api.py +211 -0
  113. firebird_admin-1.0.1/tests/test_ddl_property.py +227 -0
  114. firebird_admin-1.0.1/tests/test_driver_patches.py +27 -0
  115. firebird_admin-1.0.1/tests/test_encryption_plugin.py +68 -0
  116. firebird_admin-1.0.1/tests/test_engineering.py +171 -0
  117. firebird_admin-1.0.1/tests/test_firebird_qa.py +143 -0
  118. firebird_admin-1.0.1/tests/test_ldap_real.py +56 -0
  119. firebird_admin-1.0.1/tests/test_maintenance.py +560 -0
  120. firebird_admin-1.0.1/tests/test_maintenance_units.py +202 -0
  121. firebird_admin-1.0.1/tests/test_monitoring.py +415 -0
  122. firebird_admin-1.0.1/tests/test_native_driver.py +75 -0
  123. firebird_admin-1.0.1/tests/test_objects_03.py +617 -0
  124. firebird_admin-1.0.1/tests/test_oidc_real.py +87 -0
  125. firebird_admin-1.0.1/tests/test_plugins.py +42 -0
  126. firebird_admin-1.0.1/tests/test_preconfig.py +75 -0
  127. firebird_admin-1.0.1/tests/test_query_tool.py +134 -0
  128. firebird_admin-1.0.1/tests/test_replication.py +176 -0
  129. firebird_admin-1.0.1/tests/test_schema_tools.py +336 -0
  130. firebird_admin-1.0.1/tests/test_schemas_fb6.py +130 -0
  131. firebird_admin-1.0.1/tests/test_security.py +477 -0
  132. firebird_admin-1.0.1/tests/test_security_fb.py +344 -0
  133. firebird_admin-1.0.1/tests/test_sqlsplit.py +111 -0
  134. firebird_admin-1.0.1/tests/test_sqlsplit_fuzz.py +153 -0
  135. firebird_admin-1.0.1/tests/test_types.py +43 -0
  136. firebird_admin-1.0.1/tests/test_updater.py +130 -0
@@ -0,0 +1,65 @@
1
+ Metadata-Version: 2.4
2
+ Name: firebird-admin
3
+ Version: 1.0.1
4
+ Summary: A pgAdmin-style web administration and development tool for Firebird SQL
5
+ Author: fbadmin contributors
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/mariuz/fbadmin
8
+ Project-URL: Documentation, https://mariuz.github.io/fbadmin/
9
+ Project-URL: Issues, https://github.com/mariuz/fbadmin/issues
10
+ Keywords: firebird,database,admin,sql,pgadmin
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Web Environment
13
+ Classifier: Framework :: FastAPI
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: System Administrators
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Topic :: Database :: Front-Ends
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: fastapi>=0.110
22
+ Requires-Dist: uvicorn[standard]>=0.27
23
+ Requires-Dist: pydantic>=2.5
24
+ Requires-Dist: firebirdsql>=1.4.5
25
+ Requires-Dist: openpyxl>=3.1
26
+ Requires-Dist: cryptography>=42
27
+ Requires-Dist: argon2-cffi>=23.1
28
+ Requires-Dist: pyyaml>=6.0
29
+ Provides-Extra: dev
30
+ Requires-Dist: pytest>=8; extra == "dev"
31
+ Requires-Dist: httpx>=0.27; extra == "dev"
32
+ Requires-Dist: ruff>=0.5; extra == "dev"
33
+ Requires-Dist: hypothesis>=6.100; extra == "dev"
34
+ Requires-Dist: ldap3>=2.9; extra == "dev"
35
+ Provides-Extra: ai
36
+ Requires-Dist: anthropic>=1.0; extra == "ai"
37
+ Provides-Extra: ldap
38
+ Requires-Dist: ldap3>=2.9; extra == "ldap"
39
+ Provides-Extra: legacy-auth
40
+ Requires-Dist: passlib>=1.7; extra == "legacy-auth"
41
+ Provides-Extra: native
42
+ Requires-Dist: firebird-driver>=1.10; extra == "native"
43
+ Provides-Extra: desktop
44
+ Requires-Dist: pywebview>=5.0; extra == "desktop"
45
+
46
+ # fbadmin
47
+
48
+ A pgAdmin-style web administration and development tool for Firebird SQL (3.0, 4.0, 5.0):
49
+ object explorer, DDL extraction, a query tool with plans and transactions, an editable data
50
+ grid, live monitoring and backup/restore. No Firebird client library needed.
51
+
52
+ ```bash
53
+ pip install firebird-admin
54
+ fbadmin # starts on http://localhost:5050 and opens your browser
55
+ fbadmin-desktop # the same in a native window (pip install "firebird-admin[desktop]")
56
+ ```
57
+
58
+ Options: `--host`, `--port`, `--no-browser`, `--log-level`, `--log-format json`.
59
+
60
+ - Documentation: https://mariuz.github.io/fbadmin/
61
+ - Source, issues and roadmap: https://github.com/mariuz/fbadmin
62
+ - Docker image: `ghcr.io/mariuz/fbadmin`
63
+
64
+ fbadmin sends no telemetry. Without server mode it has no login screen and listens on
65
+ 127.0.0.1 only.
@@ -0,0 +1,20 @@
1
+ # fbadmin
2
+
3
+ A pgAdmin-style web administration and development tool for Firebird SQL (3.0, 4.0, 5.0):
4
+ object explorer, DDL extraction, a query tool with plans and transactions, an editable data
5
+ grid, live monitoring and backup/restore. No Firebird client library needed.
6
+
7
+ ```bash
8
+ pip install firebird-admin
9
+ fbadmin # starts on http://localhost:5050 and opens your browser
10
+ fbadmin-desktop # the same in a native window (pip install "firebird-admin[desktop]")
11
+ ```
12
+
13
+ Options: `--host`, `--port`, `--no-browser`, `--log-level`, `--log-format json`.
14
+
15
+ - Documentation: https://mariuz.github.io/fbadmin/
16
+ - Source, issues and roadmap: https://github.com/mariuz/fbadmin
17
+ - Docker image: `ghcr.io/mariuz/fbadmin`
18
+
19
+ fbadmin sends no telemetry. Without server mode it has no login screen and listens on
20
+ 127.0.0.1 only.
@@ -0,0 +1,7 @@
1
+ """fbadmin - a pgAdmin-style administration tool for Firebird SQL."""
2
+
3
+ __version__ = "1.0.1"
4
+
5
+ from . import driver_patches as _driver_patches # noqa: E402
6
+
7
+ _driver_patches.apply()
@@ -0,0 +1,54 @@
1
+ """Command line entry point: ``fbadmin`` or ``python -m fbadmin``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import os
7
+ import threading
8
+ import webbrowser
9
+
10
+ import uvicorn
11
+
12
+ from .config import settings
13
+ from .logsetup import LEVELS, configure_logging
14
+
15
+
16
+ def main() -> None:
17
+ p = argparse.ArgumentParser(prog="fbadmin", description="Web administration tool for Firebird SQL")
18
+ p.add_argument("--host", default=settings.host, help="bind address (default %(default)s)")
19
+ p.add_argument("--port", type=int, default=settings.port, help="port (default %(default)s)")
20
+ p.add_argument("--no-browser", action="store_true", help="do not open a browser window")
21
+ p.add_argument("--reload", action="store_true", help="auto-reload on code changes (development)")
22
+ p.add_argument("--log-level", choices=LEVELS, default=None,
23
+ help="log level (default $FBADMIN_LOG_LEVEL or info)")
24
+ p.add_argument("--log-format", choices=("text", "json"), default=None,
25
+ help="log output format; json writes one object per line (default $FBADMIN_LOG_FORMAT or text)")
26
+ # --- security / server mode (docs/SECURITY.md)
27
+ p.add_argument("--server-mode", action="store_true",
28
+ help="multi-user server mode with logins (same as FBADMIN_SERVER_MODE=1)")
29
+ p.add_argument("--ssl-certfile", help="serve HTTPS with this certificate (PEM)")
30
+ p.add_argument("--ssl-keyfile", help="private key for --ssl-certfile")
31
+ p.add_argument("--proxy-headers", action="store_true",
32
+ help="trust X-Forwarded-Proto/For from --forwarded-allow-ips (behind a reverse proxy)")
33
+ p.add_argument("--forwarded-allow-ips", default=os.environ.get("FORWARDED_ALLOW_IPS", "127.0.0.1"),
34
+ help="proxy addresses trusted for X-Forwarded-* headers (default %(default)s)")
35
+ args = p.parse_args()
36
+ log_config = configure_logging(args.log_level, args.log_format)
37
+ if args.server_mode:
38
+ os.environ["FBADMIN_SERVER_MODE"] = "1"
39
+ https = bool(args.ssl_certfile)
40
+ if not args.no_browser:
41
+ host = "localhost" if args.host in ("0.0.0.0", "127.0.0.1") else args.host
42
+ url = f"{'https' if https else 'http'}://{host}:{args.port}/"
43
+ threading.Timer(1.5, lambda: webbrowser.open(url)).start()
44
+ extra = {}
45
+ if https:
46
+ extra.update(ssl_certfile=args.ssl_certfile, ssl_keyfile=args.ssl_keyfile)
47
+ if args.proxy_headers:
48
+ extra.update(proxy_headers=True, forwarded_allow_ips=args.forwarded_allow_ips)
49
+ uvicorn.run("fbadmin.app:app", host=args.host, port=args.port, reload=args.reload,
50
+ log_config=log_config, access_log=False, **extra)
51
+
52
+
53
+ if __name__ == "__main__":
54
+ main()
@@ -0,0 +1,41 @@
1
+ """Opt-in AI assistant: explain plans, suggest indices, convert SQL from other dialects.
2
+
3
+ Disabled unless ``FBADMIN_AI_PROVIDER`` is set. Only SQL text, plans and table DDL are
4
+ sent to the provider — never table data. Providers are pluggable: register a class with
5
+ ``register_provider`` or via the ``fbadmin.ai_providers`` entry point group.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ from importlib.metadata import entry_points
12
+
13
+ from .providers import AIProvider, AnthropicProvider, AIUnavailable
14
+
15
+ _PROVIDERS: dict[str, type[AIProvider]] = {"anthropic": AnthropicProvider}
16
+
17
+
18
+ def register_provider(name: str, cls: type[AIProvider]) -> None:
19
+ _PROVIDERS[name] = cls
20
+
21
+
22
+ def _load_entry_points() -> None:
23
+ try:
24
+ for ep in entry_points(group="fbadmin.ai_providers"):
25
+ _PROVIDERS.setdefault(ep.name, ep.load())
26
+ except Exception: # noqa: BLE001 - a broken plugin must not break the app
27
+ pass
28
+
29
+
30
+ def get_provider() -> AIProvider | None:
31
+ name = os.environ.get("FBADMIN_AI_PROVIDER", "").strip().lower()
32
+ if not name or name == "none":
33
+ return None
34
+ _load_entry_points()
35
+ cls = _PROVIDERS.get(name)
36
+ if cls is None:
37
+ raise AIUnavailable(f"Unknown AI provider {name!r}; available: {', '.join(sorted(_PROVIDERS))}")
38
+ return cls()
39
+
40
+
41
+ __all__ = ["AIProvider", "AIUnavailable", "AnthropicProvider", "get_provider", "register_provider"]
@@ -0,0 +1,72 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+
5
+
6
+ class AIUnavailable(Exception):
7
+ """The assistant is not configured or the provider failed."""
8
+
9
+
10
+ class AIProvider:
11
+ """Interface for assistant backends."""
12
+
13
+ name = "base"
14
+
15
+ def complete(self, system: str, prompt: str) -> str: # pragma: no cover - interface
16
+ raise NotImplementedError
17
+
18
+ def describe(self) -> dict:
19
+ return {"provider": self.name}
20
+
21
+
22
+ class AnthropicProvider(AIProvider):
23
+ """Claude via the official Anthropic SDK (``pip install firebird-admin[ai]``).
24
+
25
+ Credentials come from the SDK's normal resolution (ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN,
26
+ an ``ant auth login`` profile, ...). Model: FBADMIN_AI_MODEL (default claude-opus-5-5).
27
+ """
28
+
29
+ name = "anthropic"
30
+
31
+ def __init__(self) -> None:
32
+ try:
33
+ import anthropic
34
+ except ImportError as exc:
35
+ raise AIUnavailable("The 'anthropic' package is not installed: pip install 'firebird-admin[ai]'") from exc
36
+ self._anthropic = anthropic
37
+ self.model = os.environ.get("FBADMIN_AI_MODEL", "claude-opus-5-5")
38
+ self.effort = os.environ.get("FBADMIN_AI_EFFORT", "high")
39
+ self.client = anthropic.Anthropic()
40
+
41
+ def describe(self) -> dict:
42
+ return {"provider": self.name, "model": self.model}
43
+
44
+ def complete(self, system: str, prompt: str) -> str:
45
+ a = self._anthropic
46
+ try:
47
+ # Server-side fallback: if a safety classifier declines, the API re-runs the
48
+ # request on a fallback model inside the same call.
49
+ with self.client.beta.messages.stream(
50
+ model=self.model,
51
+ max_tokens=16000,
52
+ system=system,
53
+ output_config={"effort": self.effort},
54
+ betas=["server-side-fallback-2026-07-01"],
55
+ fallbacks="default",
56
+ messages=[{"role": "user", "content": prompt}],
57
+ ) as stream:
58
+ msg = stream.get_final_message()
59
+ except a.AuthenticationError as exc:
60
+ raise AIUnavailable("Anthropic credentials are missing or invalid (set ANTHROPIC_API_KEY)") from exc
61
+ except a.RateLimitError as exc:
62
+ raise AIUnavailable("Rate limited by the Anthropic API; try again shortly") from exc
63
+ except a.APIStatusError as exc:
64
+ raise AIUnavailable(f"Anthropic API error {exc.status_code}: {exc.message}") from exc
65
+ except a.APIConnectionError as exc:
66
+ raise AIUnavailable("Cannot reach the Anthropic API (network)") from exc
67
+ if msg.stop_reason == "refusal":
68
+ raise AIUnavailable("The model declined this request")
69
+ text = "".join(b.text for b in msg.content if b.type == "text").strip()
70
+ if msg.stop_reason == "max_tokens":
71
+ text += "\n\n(answer truncated)"
72
+ return text
@@ -0,0 +1,43 @@
1
+ """API routers. Feature modules add themselves to ROUTERS (one line each, to merge cleanly)."""
2
+
3
+ from fastapi import APIRouter
4
+
5
+ from . import ai # noqa: I001 - one import per line keeps parallel branches mergeable
6
+ from . import databases
7
+ from . import jobs
8
+ from . import maintenance
9
+ from . import query
10
+ from . import schedules
11
+ from . import servers
12
+ from . import trace
13
+ from . import metrics
14
+ from . import monitoring
15
+ from . import replication
16
+ from . import version as version_api
17
+ from . import object_dialogs
18
+ from . import schema
19
+ from . import admin
20
+ from . import auth
21
+ from . import fbsecurity
22
+
23
+ ROUTERS: list[APIRouter] = [
24
+ servers.router,
25
+ databases.router,
26
+ query.router,
27
+ jobs.router,
28
+ ai.router,
29
+ maintenance.router,
30
+ trace.router,
31
+ schedules.router,
32
+ monitoring.router,
33
+ monitoring.settings_router,
34
+ metrics.router,
35
+ replication.router,
36
+ replication.conf_router,
37
+ version_api.router,
38
+ object_dialogs.router,
39
+ schema.router,
40
+ auth.router,
41
+ admin.router,
42
+ fbsecurity.router,
43
+ ]
@@ -0,0 +1,84 @@
1
+ """fbadmin user administration and the audit log viewer (administrators only)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from fastapi import APIRouter, Query
6
+ from pydantic import BaseModel
7
+
8
+ from ..security.core import require_admin
9
+ from ..security.errors import SecurityError
10
+ from .deps import get_state
11
+
12
+ router = APIRouter(prefix="/api/admin", tags=["admin"])
13
+
14
+
15
+ class UserIn(BaseModel):
16
+ username: str
17
+ email: str | None = None
18
+ password: str | None = None
19
+ role: str = "user"
20
+ active: bool = True
21
+ auth_source: str = "internal"
22
+
23
+
24
+ class UserUpdate(BaseModel):
25
+ email: str | None = None
26
+ password: str | None = None
27
+ role: str | None = None
28
+ active: bool | None = None
29
+
30
+
31
+ def _server_mode_admin():
32
+ p = require_admin()
33
+ if p is None:
34
+ raise SecurityError(400, "User accounts exist only in server mode", "desktop_mode")
35
+ return p
36
+
37
+
38
+ @router.get("/users")
39
+ def list_users():
40
+ _server_mode_admin()
41
+ return get_state().security.db.list_users()
42
+
43
+
44
+ @router.post("/users", status_code=201)
45
+ def create_user(body: UserIn):
46
+ _server_mode_admin()
47
+ sec = get_state().security
48
+ if body.auth_source not in ("internal", "ldap", "proxy") and not body.auth_source.startswith("oidc:"):
49
+ raise SecurityError(400, f"Unknown authentication source {body.auth_source!r}", "bad_source")
50
+ user = sec.create_user(body.username, email=body.email, password=body.password, role=body.role,
51
+ auth_source=body.auth_source, active=body.active)
52
+ return {k: v for k, v in user.items() if k not in ("password_hash", "vault")}
53
+
54
+
55
+ @router.put("/users/{user_id}")
56
+ def update_user(user_id: int, body: UserUpdate):
57
+ p = _server_mode_admin()
58
+ user = get_state().security.update_user(user_id, body.model_dump(exclude_unset=True), p)
59
+ return {k: v for k, v in user.items() if k not in ("password_hash", "vault")}
60
+
61
+
62
+ @router.delete("/users/{user_id}", status_code=204)
63
+ def delete_user(user_id: int):
64
+ p = _server_mode_admin()
65
+ get_state().security.delete_user(user_id, p)
66
+
67
+
68
+ @router.get("/audit")
69
+ def audit(
70
+ limit: int = Query(100, ge=1, le=1000),
71
+ offset: int = Query(0, ge=0),
72
+ username: str | None = None,
73
+ server: str | None = None,
74
+ q: str | None = None,
75
+ success: bool | None = None,
76
+ since: float | None = None,
77
+ until: float | None = None,
78
+ category: str | None = None,
79
+ ):
80
+ require_admin()
81
+ sec = get_state().security
82
+ out = sec.db.query_audit(limit=limit, offset=offset, username=username, server=server, text=q,
83
+ success=success, since=since, until=until, category=category) if sec.db.exists else {"total": 0, "rows": []}
84
+ return {**out, "enabled": sec.audit.enabled}
@@ -0,0 +1,117 @@
1
+ """AI assistant endpoints (opt-in, see fbadmin/ai)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+ from fastapi import APIRouter
8
+ from fastapi.responses import JSONResponse
9
+ from pydantic import BaseModel
10
+
11
+ from ..ai import AIUnavailable, get_provider
12
+ from ..metadata import Catalog
13
+ from ..query import explain
14
+ from ..sqlsplit import bind_params
15
+ from .deps import get_state
16
+
17
+ router = APIRouter(prefix="/api/ai", tags=["ai"])
18
+
19
+ SYSTEM = (
20
+ "You are a Firebird SQL expert embedded in fbadmin, a database administration tool. "
21
+ "Answer for Firebird {version} specifically (its SQL dialect, PSQL, plans and index behaviour). "
22
+ "Be concise and practical: short explanation, then concrete SQL in ```sql blocks when relevant. "
23
+ "Never invent tables or columns that are not in the provided DDL; say what is missing instead."
24
+ )
25
+
26
+ TASKS = {
27
+ "explain_plan": "Explain this query's execution plan in plain language. Point out full table scans "
28
+ "(NATURAL / Full Scan), sorts and joins that could be slow, and why the optimizer chose them.",
29
+ "suggest_indexes": "Suggest indices (CREATE INDEX statements) that would make this query faster, given the plan "
30
+ "and the table definitions. Mention selectivity concerns and existing indices that already cover it. "
31
+ "If no index would help, say so.",
32
+ "convert_sql": "Convert the following SQL from {dialect} to Firebird SQL. Output the converted SQL in one ```sql block, "
33
+ "then list the changes you made (e.g. LIMIT -> FETCH FIRST/ROWS, AUTO_INCREMENT -> IDENTITY, "
34
+ "string concatenation, date functions, data types).",
35
+ "explain_error": "Explain this Firebird error for the given SQL and show the corrected SQL.",
36
+ "ask": "Answer the user's question about this SQL / database.",
37
+ }
38
+
39
+
40
+ class AskIn(BaseModel):
41
+ task: str
42
+ sql: str = ""
43
+ question: str = ""
44
+ error: str = ""
45
+ dialect: str = "MySQL"
46
+ server_id: str | None = None
47
+ database_id: str | None = None
48
+
49
+
50
+ @router.get("/status")
51
+ def status():
52
+ try:
53
+ p = get_provider()
54
+ except AIUnavailable as exc:
55
+ return {"enabled": False, "reason": str(exc)}
56
+ if p is None:
57
+ return {"enabled": False, "reason": "Set FBADMIN_AI_PROVIDER=anthropic to enable the assistant"}
58
+ return {"enabled": True, **p.describe()}
59
+
60
+
61
+ def _context(server_id: str, db_id: str, sql: str, with_plan: bool) -> tuple[str, str]:
62
+ """Plan + DDL of the tables the statement touches; and the engine version."""
63
+ parts: list[str] = []
64
+ with get_state().manager.meta(server_id, db_id) as conn:
65
+ cat = Catalog(conn)
66
+ version = cat.engine_version
67
+ names: list[str] = []
68
+ if with_plan and sql.strip():
69
+ bound, _ = bind_params(sql.strip().rstrip(";"), [0])
70
+ try:
71
+ plan = explain(conn, bound)
72
+ detailed = explain(conn, bound, detailed=True)
73
+ parts.append(f"Plan:\n{plan}\n\nDetailed plan:\n{detailed}")
74
+ names += re.findall(r'Table (?:"[^"]+"\.)?"([^"]+)"', detailed or "") # FB6: "SCHEMA"."T"
75
+ except Exception as exc: # noqa: BLE001
76
+ parts.append(f"(could not prepare the statement: {exc})")
77
+ tables = {r["name"] for r in cat.list_objects("tables")} | {r["name"] for r in cat.list_objects("views")}
78
+ names += [w for w in re.findall(r'[A-Za-z_][A-Za-z0-9_$]*|"[^"]+"', sql) if w.strip('"').upper() in tables or w.strip('"') in tables]
79
+ seen = []
80
+ for n in names:
81
+ n = n.strip('"')
82
+ n = n if n in tables else n.upper()
83
+ if n in tables and n not in seen:
84
+ seen.append(n)
85
+ for n in seen[:12]:
86
+ otype = "tables" if n in {r["name"] for r in cat.list_objects("tables")} else "views"
87
+ try:
88
+ parts.append(cat.get_object(otype, n)["ddl"])
89
+ except Exception: # noqa: BLE001
90
+ pass
91
+ return "\n\n".join(parts), version
92
+
93
+
94
+ @router.post("/ask")
95
+ def ask(body: AskIn):
96
+ try:
97
+ provider = get_provider()
98
+ if provider is None:
99
+ return JSONResponse(status_code=400, content={"detail": "AI assistant is disabled (FBADMIN_AI_PROVIDER not set)"})
100
+ if body.task not in TASKS:
101
+ return JSONResponse(status_code=400, content={"detail": f"Unknown task {body.task!r}"})
102
+ ctx, version = "", "3.0+"
103
+ if body.server_id and body.database_id and body.task != "convert_sql":
104
+ ctx, version = _context(body.server_id, body.database_id, body.sql,
105
+ with_plan=body.task in ("explain_plan", "suggest_indexes"))
106
+ prompt = TASKS[body.task].format(dialect=body.dialect)
107
+ if body.question:
108
+ prompt += f"\n\nQuestion: {body.question}"
109
+ if body.error:
110
+ prompt += f"\n\nError:\n{body.error}"
111
+ prompt += f"\n\nSQL:\n```sql\n{body.sql}\n```"
112
+ if ctx:
113
+ prompt += f"\n\nContext from the database:\n{ctx}"
114
+ answer = provider.complete(SYSTEM.format(version=version or "3.0+"), prompt)
115
+ return {"answer": answer, **provider.describe()}
116
+ except AIUnavailable as exc:
117
+ return JSONResponse(status_code=503, content={"detail": str(exc)})