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.
- firebird_admin-1.0.1/PKG-INFO +65 -0
- firebird_admin-1.0.1/README.md +20 -0
- firebird_admin-1.0.1/fbadmin/__init__.py +7 -0
- firebird_admin-1.0.1/fbadmin/__main__.py +54 -0
- firebird_admin-1.0.1/fbadmin/ai/__init__.py +41 -0
- firebird_admin-1.0.1/fbadmin/ai/providers.py +72 -0
- firebird_admin-1.0.1/fbadmin/api/__init__.py +43 -0
- firebird_admin-1.0.1/fbadmin/api/admin.py +84 -0
- firebird_admin-1.0.1/fbadmin/api/ai.py +117 -0
- firebird_admin-1.0.1/fbadmin/api/auth.py +215 -0
- firebird_admin-1.0.1/fbadmin/api/databases.py +305 -0
- firebird_admin-1.0.1/fbadmin/api/deps.py +40 -0
- firebird_admin-1.0.1/fbadmin/api/fbsecurity.py +261 -0
- firebird_admin-1.0.1/fbadmin/api/jobs.py +17 -0
- firebird_admin-1.0.1/fbadmin/api/maintenance.py +661 -0
- firebird_admin-1.0.1/fbadmin/api/metrics.py +56 -0
- firebird_admin-1.0.1/fbadmin/api/monitoring.py +276 -0
- firebird_admin-1.0.1/fbadmin/api/object_dialogs.py +132 -0
- firebird_admin-1.0.1/fbadmin/api/query.py +159 -0
- firebird_admin-1.0.1/fbadmin/api/replication.py +155 -0
- firebird_admin-1.0.1/fbadmin/api/schedules.py +103 -0
- firebird_admin-1.0.1/fbadmin/api/schema.py +422 -0
- firebird_admin-1.0.1/fbadmin/api/servers.py +317 -0
- firebird_admin-1.0.1/fbadmin/api/trace.py +121 -0
- firebird_admin-1.0.1/fbadmin/api/version.py +142 -0
- firebird_admin-1.0.1/fbadmin/app.py +126 -0
- firebird_admin-1.0.1/fbadmin/config.py +34 -0
- firebird_admin-1.0.1/fbadmin/connections.py +376 -0
- firebird_admin-1.0.1/fbadmin/desktop.py +122 -0
- firebird_admin-1.0.1/fbadmin/driver_patches.py +137 -0
- firebird_admin-1.0.1/fbadmin/jobs.py +78 -0
- firebird_admin-1.0.1/fbadmin/limits.py +147 -0
- firebird_admin-1.0.1/fbadmin/logsetup.py +122 -0
- firebird_admin-1.0.1/fbadmin/maintenance/__init__.py +1 -0
- firebird_admin-1.0.1/fbadmin/maintenance/archive.py +73 -0
- firebird_admin-1.0.1/fbadmin/maintenance/cron.py +119 -0
- firebird_admin-1.0.1/fbadmin/maintenance/gstat.py +233 -0
- firebird_admin-1.0.1/fbadmin/maintenance/ops.py +406 -0
- firebird_admin-1.0.1/fbadmin/maintenance/scheduler.py +239 -0
- firebird_admin-1.0.1/fbadmin/maintenance/svc.py +358 -0
- firebird_admin-1.0.1/fbadmin/maintenance/trace.py +298 -0
- firebird_admin-1.0.1/fbadmin/metadata/__init__.py +1 -0
- firebird_admin-1.0.1/fbadmin/metadata/catalog.py +1166 -0
- firebird_admin-1.0.1/fbadmin/metadata/catalog_extras.py +449 -0
- firebird_admin-1.0.1/fbadmin/metadata/ddlgen.py +1020 -0
- firebird_admin-1.0.1/fbadmin/metadata/designer.py +375 -0
- firebird_admin-1.0.1/fbadmin/metadata/types.py +92 -0
- firebird_admin-1.0.1/fbadmin/middleware.py +16 -0
- firebird_admin-1.0.1/fbadmin/monitoring/__init__.py +1 -0
- firebird_admin-1.0.1/fbadmin/monitoring/advisor.py +305 -0
- firebird_admin-1.0.1/fbadmin/monitoring/history.py +286 -0
- firebird_admin-1.0.1/fbadmin/monitoring/journal.py +107 -0
- firebird_admin-1.0.1/fbadmin/monitoring/metrics.py +66 -0
- firebird_admin-1.0.1/fbadmin/monitoring/probes.py +374 -0
- firebird_admin-1.0.1/fbadmin/monitoring/profiler.py +239 -0
- firebird_admin-1.0.1/fbadmin/monitoring/replication.py +186 -0
- firebird_admin-1.0.1/fbadmin/native.py +208 -0
- firebird_admin-1.0.1/fbadmin/plugins.py +73 -0
- firebird_admin-1.0.1/fbadmin/preconfig.py +118 -0
- firebird_admin-1.0.1/fbadmin/query.py +373 -0
- firebird_admin-1.0.1/fbadmin/schema/__init__.py +2 -0
- firebird_admin-1.0.1/fbadmin/schema/copydata.py +144 -0
- firebird_admin-1.0.1/fbadmin/schema/dataio.py +657 -0
- firebird_admin-1.0.1/fbadmin/schema/diff.py +534 -0
- firebird_admin-1.0.1/fbadmin/schema/extract.py +202 -0
- firebird_admin-1.0.1/fbadmin/schema/generator.py +273 -0
- firebird_admin-1.0.1/fbadmin/schema/model.py +503 -0
- firebird_admin-1.0.1/fbadmin/schema/report.py +72 -0
- firebird_admin-1.0.1/fbadmin/schema/search.py +113 -0
- firebird_admin-1.0.1/fbadmin/schema_ctx.py +88 -0
- firebird_admin-1.0.1/fbadmin/security/__init__.py +60 -0
- firebird_admin-1.0.1/fbadmin/security/audit.py +96 -0
- firebird_admin-1.0.1/fbadmin/security/core.py +374 -0
- firebird_admin-1.0.1/fbadmin/security/crypto.py +147 -0
- firebird_admin-1.0.1/fbadmin/security/db.py +205 -0
- firebird_admin-1.0.1/fbadmin/security/errors.py +30 -0
- firebird_admin-1.0.1/fbadmin/security/fbservices.py +147 -0
- firebird_admin-1.0.1/fbadmin/security/fbsql.py +281 -0
- firebird_admin-1.0.1/fbadmin/security/ldap.py +90 -0
- firebird_admin-1.0.1/fbadmin/security/middleware.py +396 -0
- firebird_admin-1.0.1/fbadmin/security/oidc.py +116 -0
- firebird_admin-1.0.1/fbadmin/security/proxy.py +20 -0
- firebird_admin-1.0.1/fbadmin/security/readonly.py +94 -0
- firebird_admin-1.0.1/fbadmin/security/servers.py +419 -0
- firebird_admin-1.0.1/fbadmin/security/sessions.py +154 -0
- firebird_admin-1.0.1/fbadmin/security/settings.py +235 -0
- firebird_admin-1.0.1/fbadmin/security/wirecompat.py +71 -0
- firebird_admin-1.0.1/fbadmin/serialize.py +53 -0
- firebird_admin-1.0.1/fbadmin/sqlsplit.py +277 -0
- firebird_admin-1.0.1/fbadmin/static/assets/DataView-C2gZQZsl.js +1 -0
- firebird_admin-1.0.1/fbadmin/static/assets/Jobs-BbRKWT35.js +2 -0
- firebird_admin-1.0.1/fbadmin/static/assets/Monitor-BjEKPBW_.js +1 -0
- firebird_admin-1.0.1/fbadmin/static/assets/QueryTool-DKWEk7JA.js +30 -0
- firebird_admin-1.0.1/fbadmin/static/assets/codemirror-CQtTaafu.js +12 -0
- firebird_admin-1.0.1/fbadmin/static/assets/index-DVXUQWWb.css +1 -0
- firebird_admin-1.0.1/fbadmin/static/assets/index-n-ToRVCh.js +122 -0
- firebird_admin-1.0.1/fbadmin/static/assets/react-D1VdbdIl.js +40 -0
- firebird_admin-1.0.1/fbadmin/static/assets/sql-formatter-BMeiBS2F.js +19 -0
- firebird_admin-1.0.1/fbadmin/static/index.html +17 -0
- firebird_admin-1.0.1/fbadmin/store.py +166 -0
- firebird_admin-1.0.1/fbadmin/updater.py +234 -0
- firebird_admin-1.0.1/firebird_admin.egg-info/PKG-INFO +65 -0
- firebird_admin-1.0.1/firebird_admin.egg-info/SOURCES.txt +134 -0
- firebird_admin-1.0.1/firebird_admin.egg-info/dependency_links.txt +1 -0
- firebird_admin-1.0.1/firebird_admin.egg-info/entry_points.txt +3 -0
- firebird_admin-1.0.1/firebird_admin.egg-info/requires.txt +30 -0
- firebird_admin-1.0.1/firebird_admin.egg-info/top_level.txt +1 -0
- firebird_admin-1.0.1/pyproject.toml +75 -0
- firebird_admin-1.0.1/setup.cfg +4 -0
- firebird_admin-1.0.1/tests/test_advisor_parse.py +47 -0
- firebird_admin-1.0.1/tests/test_ai.py +52 -0
- firebird_admin-1.0.1/tests/test_api.py +211 -0
- firebird_admin-1.0.1/tests/test_ddl_property.py +227 -0
- firebird_admin-1.0.1/tests/test_driver_patches.py +27 -0
- firebird_admin-1.0.1/tests/test_encryption_plugin.py +68 -0
- firebird_admin-1.0.1/tests/test_engineering.py +171 -0
- firebird_admin-1.0.1/tests/test_firebird_qa.py +143 -0
- firebird_admin-1.0.1/tests/test_ldap_real.py +56 -0
- firebird_admin-1.0.1/tests/test_maintenance.py +560 -0
- firebird_admin-1.0.1/tests/test_maintenance_units.py +202 -0
- firebird_admin-1.0.1/tests/test_monitoring.py +415 -0
- firebird_admin-1.0.1/tests/test_native_driver.py +75 -0
- firebird_admin-1.0.1/tests/test_objects_03.py +617 -0
- firebird_admin-1.0.1/tests/test_oidc_real.py +87 -0
- firebird_admin-1.0.1/tests/test_plugins.py +42 -0
- firebird_admin-1.0.1/tests/test_preconfig.py +75 -0
- firebird_admin-1.0.1/tests/test_query_tool.py +134 -0
- firebird_admin-1.0.1/tests/test_replication.py +176 -0
- firebird_admin-1.0.1/tests/test_schema_tools.py +336 -0
- firebird_admin-1.0.1/tests/test_schemas_fb6.py +130 -0
- firebird_admin-1.0.1/tests/test_security.py +477 -0
- firebird_admin-1.0.1/tests/test_security_fb.py +344 -0
- firebird_admin-1.0.1/tests/test_sqlsplit.py +111 -0
- firebird_admin-1.0.1/tests/test_sqlsplit_fuzz.py +153 -0
- firebird_admin-1.0.1/tests/test_types.py +43 -0
- 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,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)})
|