gaard-api 0.2.0__tar.gz → 0.2.2__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.
- {gaard_api-0.2.0/src/gaard_api.egg-info → gaard_api-0.2.2}/PKG-INFO +19 -6
- {gaard_api-0.2.0 → gaard_api-0.2.2}/pyproject.toml +16 -6
- {gaard_api-0.2.0 → gaard_api-0.2.2}/src/gaard_api/admin/database.py +41 -22
- {gaard_api-0.2.0 → gaard_api-0.2.2}/src/gaard_api/admin/models.py +1 -0
- {gaard_api-0.2.0 → gaard_api-0.2.2}/src/gaard_api/admin/services.py +274 -10
- gaard_api-0.2.2/src/gaard_api/admin-web/assets/getgaard.svg +87 -0
- {gaard_api-0.2.0 → gaard_api-0.2.2}/src/gaard_api/admin-web/assets/main.js +849 -98
- {gaard_api-0.2.0 → gaard_api-0.2.2}/src/gaard_api/admin-web/assets/styles.css +308 -12
- {gaard_api-0.2.0 → gaard_api-0.2.2}/src/gaard_api/api/v1/admin.py +342 -53
- {gaard_api-0.2.0 → gaard_api-0.2.2}/src/gaard_api/api/v1/analysis.py +15 -12
- {gaard_api-0.2.0 → gaard_api-0.2.2}/src/gaard_api/api/v1/query.py +273 -107
- {gaard_api-0.2.0 → gaard_api-0.2.2}/src/gaard_api/cli_commands.py +5 -2
- {gaard_api-0.2.0 → gaard_api-0.2.2}/src/gaard_api/core/settings.py +34 -0
- gaard_api-0.2.2/src/gaard_api/extensions.py +125 -0
- gaard_api-0.2.2/src/gaard_api/license.py +899 -0
- {gaard_api-0.2.0 → gaard_api-0.2.2}/src/gaard_api/main.py +27 -5
- gaard_api-0.2.2/src/gaard_api/package_updates.py +1007 -0
- gaard_api-0.2.2/src/gaard_api/query_hooks.py +322 -0
- {gaard_api-0.2.0 → gaard_api-0.2.2/src/gaard_api.egg-info}/PKG-INFO +19 -6
- {gaard_api-0.2.0 → gaard_api-0.2.2}/src/gaard_api.egg-info/SOURCES.txt +6 -1
- gaard_api-0.2.2/src/gaard_api.egg-info/requires.txt +34 -0
- {gaard_api-0.2.0 → gaard_api-0.2.2}/tests/test_admin_api.py +575 -1
- {gaard_api-0.2.0 → gaard_api-0.2.2}/tests/test_api_registry.py +74 -1
- {gaard_api-0.2.0 → gaard_api-0.2.2}/tests/test_error_handlers.py +25 -0
- gaard_api-0.2.2/tests/test_license_api.py +425 -0
- gaard_api-0.2.2/tests/test_license_service.py +324 -0
- gaard_api-0.2.0/src/gaard_api/admin-web/src/main.ts +0 -2755
- gaard_api-0.2.0/src/gaard_api/extensions.py +0 -50
- gaard_api-0.2.0/src/gaard_api.egg-info/requires.txt +0 -16
- {gaard_api-0.2.0 → gaard_api-0.2.2}/MANIFEST.in +0 -0
- {gaard_api-0.2.0 → gaard_api-0.2.2}/README.md +0 -0
- {gaard_api-0.2.0 → gaard_api-0.2.2}/setup.cfg +0 -0
- {gaard_api-0.2.0 → gaard_api-0.2.2}/src/gaard_api/__init__.py +0 -0
- {gaard_api-0.2.0 → gaard_api-0.2.2}/src/gaard_api/admin/__init__.py +0 -0
- {gaard_api-0.2.0 → gaard_api-0.2.2}/src/gaard_api/admin/defaults.py +0 -0
- {gaard_api-0.2.0 → gaard_api-0.2.2}/src/gaard_api/admin/prompt_runtime.py +0 -0
- {gaard_api-0.2.0 → gaard_api-0.2.2}/src/gaard_api/admin/security.py +0 -0
- {gaard_api-0.2.0 → gaard_api-0.2.2}/src/gaard_api/admin-web/index.html +0 -0
- {gaard_api-0.2.0 → gaard_api-0.2.2}/src/gaard_api/admin-web/package.json +0 -0
- {gaard_api-0.2.0 → gaard_api-0.2.2}/src/gaard_api/api/__init__.py +0 -0
- {gaard_api-0.2.0 → gaard_api-0.2.2}/src/gaard_api/api/v1/__init__.py +0 -0
- {gaard_api-0.2.0 → gaard_api-0.2.2}/src/gaard_api/api/v1/prompts.py +0 -0
- {gaard_api-0.2.0 → gaard_api-0.2.2}/src/gaard_api/api/v1/schema.py +0 -0
- {gaard_api-0.2.0 → gaard_api-0.2.2}/src/gaard_api/api_registry.py +0 -0
- {gaard_api-0.2.0 → gaard_api-0.2.2}/src/gaard_api/cli.py +0 -0
- {gaard_api-0.2.0 → gaard_api-0.2.2}/src/gaard_api/core/__init__.py +0 -0
- {gaard_api-0.2.0 → gaard_api-0.2.2}/src/gaard_api/core/error_handlers.py +0 -0
- {gaard_api-0.2.0 → gaard_api-0.2.2}/src/gaard_api/core/schema_cache.py +0 -0
- {gaard_api-0.2.0 → gaard_api-0.2.2}/src/gaard_api/example_data/__init__.py +0 -0
- {gaard_api-0.2.0 → gaard_api-0.2.2}/src/gaard_api/example_data/medical_poc/__init__.py +0 -0
- {gaard_api-0.2.0 → gaard_api-0.2.2}/src/gaard_api/example_data/medical_poc/schema.sql +0 -0
- {gaard_api-0.2.0 → gaard_api-0.2.2}/src/gaard_api/example_data/medical_poc/seed.sql +0 -0
- {gaard_api-0.2.0 → gaard_api-0.2.2}/src/gaard_api/example_database.py +0 -0
- {gaard_api-0.2.0 → gaard_api-0.2.2}/src/gaard_api/extension_services.py +0 -0
- {gaard_api-0.2.0 → gaard_api-0.2.2}/src/gaard_api/server_cli.py +0 -0
- {gaard_api-0.2.0 → gaard_api-0.2.2}/src/gaard_api.egg-info/dependency_links.txt +0 -0
- {gaard_api-0.2.0 → gaard_api-0.2.2}/src/gaard_api.egg-info/entry_points.txt +0 -0
- {gaard_api-0.2.0 → gaard_api-0.2.2}/src/gaard_api.egg-info/top_level.txt +0 -0
- {gaard_api-0.2.0 → gaard_api-0.2.2}/tests/test_analysis_api.py +0 -0
- {gaard_api-0.2.0 → gaard_api-0.2.2}/tests/test_prompt_runtime.py +0 -0
- {gaard_api-0.2.0 → gaard_api-0.2.2}/tests/test_query_error_messages.py +0 -0
- {gaard_api-0.2.0 → gaard_api-0.2.2}/tests/test_server_cli.py +0 -0
- {gaard_api-0.2.0 → gaard_api-0.2.2}/tests/test_settings.py +0 -0
|
@@ -1,18 +1,31 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gaard-api
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: GAARD backend web services providing admin interface
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
7
|
-
Requires-Dist: gaard-core==0.2.
|
|
8
|
-
Requires-Dist: gaard-connectors==0.2.
|
|
9
|
-
Requires-Dist: gaard-llm==0.2.
|
|
10
|
-
Requires-Dist: gaard-plugin-api<0.3.0,>=0.2.
|
|
7
|
+
Requires-Dist: gaard-core==0.2.2
|
|
8
|
+
Requires-Dist: gaard-connectors==0.2.2
|
|
9
|
+
Requires-Dist: gaard-llm==0.2.2
|
|
10
|
+
Requires-Dist: gaard-plugin-api<0.3.0,>=0.2.2
|
|
11
11
|
Requires-Dist: fastapi>=0.111.0
|
|
12
12
|
Requires-Dist: uvicorn[standard]>=0.30.0
|
|
13
13
|
Requires-Dist: pydantic>=2.7.0
|
|
14
14
|
Requires-Dist: sqlalchemy>=2.0.0
|
|
15
|
-
Requires-Dist:
|
|
15
|
+
Requires-Dist: httpx2>=2.5.0
|
|
16
|
+
Provides-Extra: oracle
|
|
17
|
+
Requires-Dist: oracledb>=2.0.0; extra == "oracle"
|
|
18
|
+
Provides-Extra: mssql
|
|
19
|
+
Requires-Dist: pyodbc>=5.0.0; extra == "mssql"
|
|
20
|
+
Provides-Extra: ibm-db2
|
|
21
|
+
Requires-Dist: ibm-db-sa>=0.4.4; extra == "ibm-db2"
|
|
22
|
+
Provides-Extra: teradata
|
|
23
|
+
Requires-Dist: teradatasqlalchemy>=20.0.0.9; extra == "teradata"
|
|
24
|
+
Provides-Extra: all-datasources
|
|
25
|
+
Requires-Dist: oracledb>=2.0.0; extra == "all-datasources"
|
|
26
|
+
Requires-Dist: pyodbc>=5.0.0; extra == "all-datasources"
|
|
27
|
+
Requires-Dist: ibm-db-sa>=0.4.4; extra == "all-datasources"
|
|
28
|
+
Requires-Dist: teradatasqlalchemy>=20.0.0.9; extra == "all-datasources"
|
|
16
29
|
Provides-Extra: dev
|
|
17
30
|
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
18
31
|
Requires-Dist: ruff>=0.5.0; extra == "dev"
|
|
@@ -4,24 +4,34 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "gaard-api"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.2"
|
|
8
8
|
description = "GAARD backend web services providing admin interface"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
11
11
|
|
|
12
12
|
dependencies = [
|
|
13
|
-
"gaard-core==0.2.
|
|
14
|
-
"gaard-connectors==0.2.
|
|
15
|
-
"gaard-llm==0.2.
|
|
16
|
-
"gaard-plugin-api>=0.2.
|
|
13
|
+
"gaard-core==0.2.2",
|
|
14
|
+
"gaard-connectors==0.2.2",
|
|
15
|
+
"gaard-llm==0.2.2",
|
|
16
|
+
"gaard-plugin-api>=0.2.2,<0.3.0",
|
|
17
17
|
"fastapi>=0.111.0",
|
|
18
18
|
"uvicorn[standard]>=0.30.0",
|
|
19
19
|
"pydantic>=2.7.0",
|
|
20
20
|
"sqlalchemy>=2.0.0",
|
|
21
|
-
"
|
|
21
|
+
"httpx2>=2.5.0"
|
|
22
22
|
]
|
|
23
23
|
|
|
24
24
|
[project.optional-dependencies]
|
|
25
|
+
oracle = ["oracledb>=2.0.0"]
|
|
26
|
+
mssql = ["pyodbc>=5.0.0"]
|
|
27
|
+
ibm_db2 = ["ibm-db-sa>=0.4.4"]
|
|
28
|
+
teradata = ["teradatasqlalchemy>=20.0.0.9"]
|
|
29
|
+
all-datasources = [
|
|
30
|
+
"oracledb>=2.0.0",
|
|
31
|
+
"pyodbc>=5.0.0",
|
|
32
|
+
"ibm-db-sa>=0.4.4",
|
|
33
|
+
"teradatasqlalchemy>=20.0.0.9",
|
|
34
|
+
]
|
|
25
35
|
dev = [
|
|
26
36
|
"pytest>=8.0.0",
|
|
27
37
|
"ruff>=0.5.0",
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
from collections.abc import Iterator
|
|
2
2
|
import json
|
|
3
|
+
import threading
|
|
3
4
|
|
|
4
5
|
from sqlalchemy import create_engine, delete, select
|
|
5
6
|
from sqlalchemy.engine import Engine
|
|
6
|
-
from sqlalchemy import inspect, text
|
|
7
|
+
from sqlalchemy import inspect, text, Table
|
|
7
8
|
from sqlalchemy.orm import Session, sessionmaker
|
|
8
9
|
|
|
9
10
|
from gaard_api.admin.defaults import DEFAULT_GOVERNANCE_POLICY_CONFIG, DEFAULT_PROMPTS
|
|
@@ -20,10 +21,13 @@ from gaard_api.admin.models import (
|
|
|
20
21
|
from gaard_api.admin.security import hash_password
|
|
21
22
|
from gaard_api.core.settings import settings
|
|
22
23
|
|
|
24
|
+
from typing import cast
|
|
25
|
+
|
|
23
26
|
|
|
24
27
|
_engine: Engine | None = None
|
|
25
28
|
_session_factory: sessionmaker[Session] | None = None
|
|
26
29
|
_engine_url: str | None = None
|
|
30
|
+
_init_lock = threading.RLock()
|
|
27
31
|
LEGACY_PROMPT_KEYS = {"investigation_readiness"}
|
|
28
32
|
|
|
29
33
|
|
|
@@ -66,23 +70,24 @@ def create_session() -> Session:
|
|
|
66
70
|
|
|
67
71
|
|
|
68
72
|
def init_metadata_store() -> None:
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
73
|
+
with _init_lock:
|
|
74
|
+
engine = get_engine()
|
|
75
|
+
Base.metadata.create_all(engine)
|
|
76
|
+
ensure_data_query_audit_schema(engine)
|
|
77
|
+
ensure_overview_widget_schema(engine)
|
|
78
|
+
|
|
79
|
+
if _session_factory is None:
|
|
80
|
+
raise RuntimeError("Admin metadata session factory is not initialized.")
|
|
81
|
+
|
|
82
|
+
with _session_factory() as session:
|
|
83
|
+
seed_admin_user(session)
|
|
84
|
+
seed_settings(session)
|
|
85
|
+
apply_runtime_settings(session)
|
|
86
|
+
seed_prompts(session)
|
|
87
|
+
seed_datasource_connectors(session)
|
|
88
|
+
seed_overview_widgets(session)
|
|
89
|
+
backfill_data_query_audit_types(session)
|
|
90
|
+
session.commit()
|
|
86
91
|
|
|
87
92
|
|
|
88
93
|
def seed_admin_user(session: Session) -> None:
|
|
@@ -129,7 +134,6 @@ def seed_settings(session: Session) -> None:
|
|
|
129
134
|
),
|
|
130
135
|
"data_query_audit_retention_days": str(settings.gaard_audit_retention_days),
|
|
131
136
|
"schema_cache_ttl_seconds": str(settings.gaard_schema_cache_ttl_seconds),
|
|
132
|
-
"license_edition": "community",
|
|
133
137
|
}
|
|
134
138
|
|
|
135
139
|
for key, value in defaults.items():
|
|
@@ -139,6 +143,9 @@ def seed_settings(session: Session) -> None:
|
|
|
139
143
|
elif setting.updated_by == "system" and setting.value != value:
|
|
140
144
|
setting.value = value
|
|
141
145
|
|
|
146
|
+
if session.get(AdminSetting, "license_edition") is None:
|
|
147
|
+
session.add(AdminSetting(key="license_edition", value="community"))
|
|
148
|
+
|
|
142
149
|
|
|
143
150
|
def apply_runtime_settings(session: Session) -> None:
|
|
144
151
|
schema_cache_ttl = session.get(AdminSetting, "schema_cache_ttl_seconds")
|
|
@@ -374,8 +381,8 @@ def seed_overview_widgets(session: Session) -> None:
|
|
|
374
381
|
existing.active = False
|
|
375
382
|
|
|
376
383
|
if existing is not None and existing.updated_by == "system":
|
|
377
|
-
existing.position = int(item["position"])
|
|
378
|
-
existing.grid_width = int(item["grid_width"])
|
|
384
|
+
existing.position = int(cast(int,item["position"]))
|
|
385
|
+
existing.grid_width = int(cast(int,item["grid_width"]))
|
|
379
386
|
existing.result_mode = str(item["result_mode"])
|
|
380
387
|
|
|
381
388
|
|
|
@@ -406,8 +413,20 @@ def ensure_data_query_audit_schema(engine: Engine) -> None:
|
|
|
406
413
|
)
|
|
407
414
|
)
|
|
408
415
|
|
|
416
|
+
if "llm_sql_language" not in columns:
|
|
417
|
+
with engine.begin() as connection:
|
|
418
|
+
connection.execute(
|
|
419
|
+
text(
|
|
420
|
+
"ALTER TABLE data_query_audit_logs "
|
|
421
|
+
"ADD COLUMN llm_sql_language VARCHAR(50) DEFAULT ''"
|
|
422
|
+
)
|
|
423
|
+
)
|
|
424
|
+
|
|
409
425
|
with engine.begin() as connection:
|
|
410
|
-
|
|
426
|
+
|
|
427
|
+
audit_log_table = cast(Table, DataQueryAuditLog.__table__)
|
|
428
|
+
|
|
429
|
+
for index in audit_log_table.indexes:
|
|
411
430
|
index.create(bind=connection, checkfirst=True)
|
|
412
431
|
|
|
413
432
|
|
|
@@ -89,6 +89,7 @@ class DataQueryAuditLog(Base):
|
|
|
89
89
|
question: Mapped[str] = mapped_column(Text)
|
|
90
90
|
answer: Mapped[str] = mapped_column(Text)
|
|
91
91
|
sql: Mapped[str] = mapped_column(Text)
|
|
92
|
+
llm_sql_language: Mapped[str] = mapped_column(String(50), default="")
|
|
92
93
|
output_classification: Mapped[OutputClassification] = mapped_column(
|
|
93
94
|
SAEnum(
|
|
94
95
|
OutputClassification,
|
|
@@ -2,8 +2,10 @@ import json
|
|
|
2
2
|
import re
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
from datetime import UTC, datetime, timedelta
|
|
5
|
-
from
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, cast
|
|
6
7
|
|
|
8
|
+
from sqlalchemy.engine import URL
|
|
7
9
|
from sqlalchemy import delete, desc, select
|
|
8
10
|
from sqlalchemy.exc import SQLAlchemyError
|
|
9
11
|
from sqlalchemy.orm import Session
|
|
@@ -53,6 +55,209 @@ def validate_datasource_configuration(
|
|
|
53
55
|
get_connector_registry().validate(database_type, database_url, sql_dialect)
|
|
54
56
|
|
|
55
57
|
|
|
58
|
+
@dataclass(frozen=True)
|
|
59
|
+
class NormalizedDatasourceConfiguration:
|
|
60
|
+
database_type: str
|
|
61
|
+
database_url: str
|
|
62
|
+
sql_dialect: str
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def normalize_datasource_configuration(
|
|
66
|
+
database_type: str,
|
|
67
|
+
connection_config: dict[str, Any] | None = None,
|
|
68
|
+
database_path: str | None = None,
|
|
69
|
+
database_url: str | None = None,
|
|
70
|
+
sql_dialect: str | None = None,
|
|
71
|
+
) -> NormalizedDatasourceConfiguration:
|
|
72
|
+
registry = get_connector_registry()
|
|
73
|
+
normalized_url = (database_url or "").strip()
|
|
74
|
+
normalized_path = (database_path or "").strip()
|
|
75
|
+
|
|
76
|
+
if not normalized_url and connection_config:
|
|
77
|
+
normalized_url = build_sqlalchemy_url_from_connection_config(
|
|
78
|
+
database_type,
|
|
79
|
+
connection_config,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
if not normalized_url and normalized_path:
|
|
83
|
+
normalized_url = build_sqlalchemy_url_from_path(database_type, normalized_path)
|
|
84
|
+
|
|
85
|
+
if not normalized_url:
|
|
86
|
+
raise ValueError("Datasource database path is required.")
|
|
87
|
+
|
|
88
|
+
if database_type:
|
|
89
|
+
definition = registry.get(database_type)
|
|
90
|
+
else:
|
|
91
|
+
definition = registry.detect_from_database_url(normalized_url)
|
|
92
|
+
|
|
93
|
+
resolved_dialect = (sql_dialect or "").strip() or definition.default_sql_dialect
|
|
94
|
+
registry.validate(definition.type_key, normalized_url, resolved_dialect)
|
|
95
|
+
|
|
96
|
+
return NormalizedDatasourceConfiguration(
|
|
97
|
+
database_type=definition.type_key,
|
|
98
|
+
database_url=normalized_url,
|
|
99
|
+
sql_dialect=resolved_dialect,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def build_sqlalchemy_url_from_path(database_type: str, database_path: str) -> str:
|
|
104
|
+
if "://" in database_path:
|
|
105
|
+
return database_path
|
|
106
|
+
|
|
107
|
+
if database_type == "sqlite":
|
|
108
|
+
return str(URL.create("sqlite", database=str(Path(database_path).expanduser())))
|
|
109
|
+
|
|
110
|
+
raise ValueError(
|
|
111
|
+
"Only SQLite datasources can be created from a filesystem path. "
|
|
112
|
+
"Use a SQLAlchemy URL for this datasource type."
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def build_sqlalchemy_url_from_connection_config(
|
|
117
|
+
database_type: str,
|
|
118
|
+
connection_config: dict[str, Any],
|
|
119
|
+
) -> str:
|
|
120
|
+
if database_type == "sqlite":
|
|
121
|
+
database_path = str(connection_config.get("database_path") or "").strip()
|
|
122
|
+
if not database_path:
|
|
123
|
+
raise ValueError("SQLite datasource requires a database path.")
|
|
124
|
+
return build_sqlalchemy_url_from_path(database_type, database_path)
|
|
125
|
+
|
|
126
|
+
if database_type == "postgresql":
|
|
127
|
+
return build_server_database_url(
|
|
128
|
+
drivername="postgresql+psycopg",
|
|
129
|
+
connection_config=connection_config,
|
|
130
|
+
default_port=5432,
|
|
131
|
+
query_keys=("sslmode",),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if database_type == "mysql":
|
|
135
|
+
return build_server_database_url(
|
|
136
|
+
drivername="mysql+pymysql",
|
|
137
|
+
connection_config=connection_config,
|
|
138
|
+
default_port=3306,
|
|
139
|
+
query_keys=("charset",),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
if database_type == "oracle":
|
|
143
|
+
return build_server_database_url(
|
|
144
|
+
drivername="oracle+oracledb",
|
|
145
|
+
connection_config=connection_config,
|
|
146
|
+
default_port=1521,
|
|
147
|
+
query_keys=("service_name",),
|
|
148
|
+
database_required=False,
|
|
149
|
+
required_query_keys=("service_name",),
|
|
150
|
+
required_query_key_labels={"service_name": "service name"},
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
if database_type == "mssql":
|
|
154
|
+
return build_server_database_url(
|
|
155
|
+
drivername="mssql+pyodbc",
|
|
156
|
+
connection_config=connection_config,
|
|
157
|
+
default_port=1433,
|
|
158
|
+
query_keys=("driver", "Encrypt", "TrustServerCertificate"),
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
if database_type == "ibm_db2":
|
|
162
|
+
return build_server_database_url(
|
|
163
|
+
drivername="db2+ibm_db",
|
|
164
|
+
connection_config=connection_config,
|
|
165
|
+
default_port=50000,
|
|
166
|
+
query_keys=(),
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
if database_type == "teradata":
|
|
170
|
+
return build_teradata_database_url(connection_config)
|
|
171
|
+
|
|
172
|
+
raise ValueError(f"Datasource type {database_type!r} does not support generated URLs.")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def build_server_database_url(
|
|
176
|
+
drivername: str,
|
|
177
|
+
connection_config: dict[str, Any],
|
|
178
|
+
default_port: int,
|
|
179
|
+
query_keys: tuple[str, ...],
|
|
180
|
+
database_required: bool = True,
|
|
181
|
+
required_query_keys: tuple[str, ...] = (),
|
|
182
|
+
required_query_key_labels: dict[str, str] | None = None,
|
|
183
|
+
) -> str:
|
|
184
|
+
host = str(connection_config.get("host") or "").strip()
|
|
185
|
+
database = str(connection_config.get("database") or "").strip()
|
|
186
|
+
username = str(connection_config.get("username") or "").strip()
|
|
187
|
+
password = str(connection_config.get("password") or "")
|
|
188
|
+
port_value = connection_config.get("port") or default_port
|
|
189
|
+
|
|
190
|
+
if not host:
|
|
191
|
+
raise ValueError("Datasource host is required.")
|
|
192
|
+
if database_required and not database:
|
|
193
|
+
raise ValueError("Datasource database name is required.")
|
|
194
|
+
if not username:
|
|
195
|
+
raise ValueError("Datasource username is required.")
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
port = int(port_value)
|
|
199
|
+
except (TypeError, ValueError) as exc:
|
|
200
|
+
raise ValueError("Datasource port must be a number.") from exc
|
|
201
|
+
|
|
202
|
+
labels = required_query_key_labels or {}
|
|
203
|
+
for key in required_query_keys:
|
|
204
|
+
if not str(connection_config.get(key) or "").strip():
|
|
205
|
+
label = labels.get(key, key.replace("_", " "))
|
|
206
|
+
raise ValueError(f"Datasource {label} is required.")
|
|
207
|
+
|
|
208
|
+
query = {
|
|
209
|
+
key: str(connection_config.get(key)).strip()
|
|
210
|
+
for key in query_keys
|
|
211
|
+
if connection_config.get(key) not in (None, "")
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return (
|
|
215
|
+
URL.create(
|
|
216
|
+
drivername=drivername,
|
|
217
|
+
username=username,
|
|
218
|
+
password=password or None,
|
|
219
|
+
host=host,
|
|
220
|
+
port=port,
|
|
221
|
+
database=database or None,
|
|
222
|
+
query=query,
|
|
223
|
+
)
|
|
224
|
+
.render_as_string(hide_password=False)
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def build_teradata_database_url(connection_config: dict[str, Any]) -> str:
|
|
229
|
+
host = str(connection_config.get("host") or "").strip()
|
|
230
|
+
username = str(connection_config.get("username") or "").strip()
|
|
231
|
+
password = str(connection_config.get("password") or "")
|
|
232
|
+
dbs_port_value = connection_config.get("dbs_port") or 1025
|
|
233
|
+
|
|
234
|
+
if not host:
|
|
235
|
+
raise ValueError("Datasource host is required.")
|
|
236
|
+
if not username:
|
|
237
|
+
raise ValueError("Datasource username is required.")
|
|
238
|
+
|
|
239
|
+
try:
|
|
240
|
+
dbs_port = int(dbs_port_value)
|
|
241
|
+
except (TypeError, ValueError) as exc:
|
|
242
|
+
raise ValueError("Datasource port must be a number.") from exc
|
|
243
|
+
|
|
244
|
+
query = {"dbs_port": str(dbs_port)}
|
|
245
|
+
for key in ("database", "tmode"):
|
|
246
|
+
if connection_config.get(key) not in (None, ""):
|
|
247
|
+
query[key] = str(connection_config.get(key)).strip()
|
|
248
|
+
|
|
249
|
+
return (
|
|
250
|
+
URL.create(
|
|
251
|
+
drivername="teradatasql",
|
|
252
|
+
username=username,
|
|
253
|
+
password=password or None,
|
|
254
|
+
host=host,
|
|
255
|
+
query=query,
|
|
256
|
+
)
|
|
257
|
+
.render_as_string(hide_password=False)
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
|
|
56
261
|
def mask_database_url(database_url: str) -> str:
|
|
57
262
|
if "://" not in database_url or "@" not in database_url:
|
|
58
263
|
return database_url
|
|
@@ -280,6 +485,7 @@ def _record_data_query_audit(
|
|
|
280
485
|
audit_metadata = dict(metadata)
|
|
281
486
|
audit_metadata.pop("audit_type", None)
|
|
282
487
|
audit_metadata.pop("output_classification", None)
|
|
488
|
+
llm_sql_language = str(audit_metadata.get("llm_sql_language") or "")
|
|
283
489
|
log = DataQueryAuditLog(
|
|
284
490
|
type=coerce_data_query_audit_type(audit_type),
|
|
285
491
|
output_classification=coerce_output_classification(output_classification),
|
|
@@ -288,6 +494,7 @@ def _record_data_query_audit(
|
|
|
288
494
|
question=request.question,
|
|
289
495
|
answer=answer,
|
|
290
496
|
sql=sql,
|
|
497
|
+
llm_sql_language=llm_sql_language,
|
|
291
498
|
metadata_json=json_dumps(audit_metadata),
|
|
292
499
|
)
|
|
293
500
|
session.add(log)
|
|
@@ -352,7 +559,7 @@ def set_setting(session: Session, key: str, value: str, actor: str) -> AdminSett
|
|
|
352
559
|
|
|
353
560
|
|
|
354
561
|
def default_governance_policy_config() -> dict[str, Any]:
|
|
355
|
-
return json.loads(json_dumps(DEFAULT_GOVERNANCE_POLICY_CONFIG))
|
|
562
|
+
return cast(dict[str, Any], json.loads(json_dumps(DEFAULT_GOVERNANCE_POLICY_CONFIG)))
|
|
356
563
|
|
|
357
564
|
|
|
358
565
|
def normalize_bool_setting(value: Any, field_name: str) -> bool:
|
|
@@ -1029,10 +1236,20 @@ def list_business_logic_suggestions(
|
|
|
1029
1236
|
session: Session,
|
|
1030
1237
|
connector_id: int,
|
|
1031
1238
|
) -> list[BusinessLogicSuggestion]:
|
|
1239
|
+
return list_business_logic_suggestions_for_connectors(session, [connector_id])
|
|
1240
|
+
|
|
1241
|
+
|
|
1242
|
+
def list_business_logic_suggestions_for_connectors(
|
|
1243
|
+
session: Session,
|
|
1244
|
+
connector_ids: list[int],
|
|
1245
|
+
) -> list[BusinessLogicSuggestion]:
|
|
1246
|
+
if not connector_ids:
|
|
1247
|
+
return []
|
|
1248
|
+
|
|
1032
1249
|
return list(
|
|
1033
1250
|
session.scalars(
|
|
1034
1251
|
select(BusinessLogicSuggestion)
|
|
1035
|
-
.where(BusinessLogicSuggestion.connector_id
|
|
1252
|
+
.where(BusinessLogicSuggestion.connector_id.in_(connector_ids))
|
|
1036
1253
|
.order_by(
|
|
1037
1254
|
BusinessLogicSuggestion.enabled.desc(),
|
|
1038
1255
|
desc(BusinessLogicSuggestion.updated_at),
|
|
@@ -1840,6 +2057,16 @@ def get_active_datasource_connector(session: Session) -> DatasourceConnector | N
|
|
|
1840
2057
|
return session.scalar(select(DatasourceConnector).where(DatasourceConnector.active.is_(True)))
|
|
1841
2058
|
|
|
1842
2059
|
|
|
2060
|
+
def get_active_datasource_connectors(session: Session) -> list[DatasourceConnector]:
|
|
2061
|
+
return list(
|
|
2062
|
+
session.scalars(
|
|
2063
|
+
select(DatasourceConnector)
|
|
2064
|
+
.where(DatasourceConnector.active.is_(True))
|
|
2065
|
+
.order_by(DatasourceConnector.name, DatasourceConnector.id)
|
|
2066
|
+
)
|
|
2067
|
+
)
|
|
2068
|
+
|
|
2069
|
+
|
|
1843
2070
|
def get_active_datasource_connector_safe() -> DatasourceConnector | None:
|
|
1844
2071
|
try:
|
|
1845
2072
|
session = create_session()
|
|
@@ -1859,13 +2086,9 @@ def set_active_datasource_connector(
|
|
|
1859
2086
|
connector: DatasourceConnector,
|
|
1860
2087
|
actor: str,
|
|
1861
2088
|
) -> None:
|
|
1862
|
-
|
|
1863
|
-
if is_system_datasource_connector(item):
|
|
1864
|
-
item.active = False
|
|
1865
|
-
continue
|
|
2089
|
+
from gaard_api.extensions import get_query_hook_registry
|
|
1866
2090
|
|
|
1867
|
-
|
|
1868
|
-
item.updated_by = actor if item.id == connector.id else item.updated_by
|
|
2091
|
+
get_query_hook_registry().set_active_datasource_connector(session, connector, actor)
|
|
1869
2092
|
|
|
1870
2093
|
|
|
1871
2094
|
def test_datasource_connection(connector: DatasourceConnector) -> None:
|
|
@@ -2085,7 +2308,6 @@ def get_datasource_schema_context_safe() -> (
|
|
|
2085
2308
|
session = create_session()
|
|
2086
2309
|
except SQLAlchemyError:
|
|
2087
2310
|
return None
|
|
2088
|
-
|
|
2089
2311
|
try:
|
|
2090
2312
|
connector = get_active_datasource_connector(session)
|
|
2091
2313
|
|
|
@@ -2104,3 +2326,45 @@ def get_datasource_schema_context_safe() -> (
|
|
|
2104
2326
|
return None
|
|
2105
2327
|
finally:
|
|
2106
2328
|
session.close()
|
|
2329
|
+
|
|
2330
|
+
|
|
2331
|
+
def get_datasource_schema_contexts_safe(
|
|
2332
|
+
datasource_ids: list[str] | None = None,
|
|
2333
|
+
) -> list[tuple[DatasourceConnector, DatasourceSchemaCache]]:
|
|
2334
|
+
try:
|
|
2335
|
+
session = create_session()
|
|
2336
|
+
except SQLAlchemyError:
|
|
2337
|
+
return []
|
|
2338
|
+
|
|
2339
|
+
try:
|
|
2340
|
+
if datasource_ids:
|
|
2341
|
+
connectors = [
|
|
2342
|
+
connector
|
|
2343
|
+
for datasource_id in datasource_ids
|
|
2344
|
+
if (connector := get_datasource_connector_by_key(session, datasource_id))
|
|
2345
|
+
is not None
|
|
2346
|
+
and not is_system_datasource_connector(connector)
|
|
2347
|
+
]
|
|
2348
|
+
else:
|
|
2349
|
+
connectors = [
|
|
2350
|
+
connector
|
|
2351
|
+
for connector in get_active_datasource_connectors(session)
|
|
2352
|
+
if not is_system_datasource_connector(connector)
|
|
2353
|
+
]
|
|
2354
|
+
|
|
2355
|
+
contexts: list[tuple[DatasourceConnector, DatasourceSchemaCache]] = []
|
|
2356
|
+
for connector in connectors:
|
|
2357
|
+
cache = get_datasource_schema_cache(session, connector.id)
|
|
2358
|
+
|
|
2359
|
+
if cache is None:
|
|
2360
|
+
cache = introspect_datasource_connector(session, connector, "system")
|
|
2361
|
+
session.commit()
|
|
2362
|
+
|
|
2363
|
+
contexts.append((connector, cache))
|
|
2364
|
+
|
|
2365
|
+
return contexts
|
|
2366
|
+
except SQLAlchemyError:
|
|
2367
|
+
session.rollback()
|
|
2368
|
+
return []
|
|
2369
|
+
finally:
|
|
2370
|
+
session.close()
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
2
|
+
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
|
3
|
+
|
|
4
|
+
<svg
|
|
5
|
+
version="1.1"
|
|
6
|
+
id="svg1"
|
|
7
|
+
width="1672"
|
|
8
|
+
height="1672"
|
|
9
|
+
viewBox="0 0 1672 1672"
|
|
10
|
+
sodipodi:docname="getgaard.svg"
|
|
11
|
+
inkscape:version="1.4 (e7c3feb1, 2024-10-09)"
|
|
12
|
+
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
13
|
+
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
14
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
15
|
+
xmlns:svg="http://www.w3.org/2000/svg">
|
|
16
|
+
<defs
|
|
17
|
+
id="defs1" />
|
|
18
|
+
<sodipodi:namedview
|
|
19
|
+
id="namedview1"
|
|
20
|
+
pagecolor="#ffffff"
|
|
21
|
+
bordercolor="#000000"
|
|
22
|
+
borderopacity="0.25"
|
|
23
|
+
inkscape:showpageshadow="2"
|
|
24
|
+
inkscape:pageopacity="0.0"
|
|
25
|
+
inkscape:pagecheckerboard="0"
|
|
26
|
+
inkscape:deskcolor="#d1d1d1"
|
|
27
|
+
inkscape:zoom="0.17088526"
|
|
28
|
+
inkscape:cx="2182.7512"
|
|
29
|
+
inkscape:cy="111.18572"
|
|
30
|
+
inkscape:window-width="1560"
|
|
31
|
+
inkscape:window-height="771"
|
|
32
|
+
inkscape:window-x="0"
|
|
33
|
+
inkscape:window-y="25"
|
|
34
|
+
inkscape:window-maximized="0"
|
|
35
|
+
inkscape:current-layer="g1" />
|
|
36
|
+
<g
|
|
37
|
+
inkscape:groupmode="layer"
|
|
38
|
+
inkscape:label="Image"
|
|
39
|
+
id="g1">
|
|
40
|
+
<g
|
|
41
|
+
id="g6"
|
|
42
|
+
transform="matrix(1.0610508,0,0,1.0610508,-2611.1446,1006.5308)">
|
|
43
|
+
<rect
|
|
44
|
+
style="fill:#111e2f;fill-opacity:1;stroke-width:1.09542"
|
|
45
|
+
id="rect2"
|
|
46
|
+
width="1437.7893"
|
|
47
|
+
height="1437.7893"
|
|
48
|
+
x="2529.908"
|
|
49
|
+
y="-879.6134"
|
|
50
|
+
ry="320.62204" />
|
|
51
|
+
<path
|
|
52
|
+
id="rect1"
|
|
53
|
+
style="fill:#ffffff;stroke-width:1.09542"
|
|
54
|
+
d="m 3234.3408,-626.0861 c -13.6106,0.3309 -25.9368,1.0895 -34.8867,2.3418 -84.1236,11.7709 -151.8696,39.6365 -216.4922,89.0508 -88.4727,67.6513 -151.6819,174.0139 -171.4746,288.5391 -8.8934,51.4587 -8.6698,114.0636 0.5859,163.333996 8.6422,46.004504 21.2085,84.4823042 40.9121,125.273404 23.7832,49.237 50.781,87.8784 88.418,126.5547 91.8576,94.394 218.5332,143.6113 347.0449,134.8359 127.7123,-8.7209 240.4621,-72.1949 316.1836,-177.998 45.9159,-64.1567 74.5299,-138.5546 84.8711,-220.666004 2.9258,-23.231196 4.2525,-75.228896 1.9883,-77.957096 -1.5107,-1.8203 -25.8188,-2.0429 -222.8203,-2.0429 -217.2534,0 -221.1459,0.044 -222.502,2.5781 -0.885,1.6537 -1.3808,21.59 -1.3808,55.5625 0,47.4299 0.2317,53.2391 2.207,55.4218 2.1138,2.3358 8.7633,2.4375 158,2.4376 129.1634,0 155.793,0.305 155.793,1.7832 0,3.2418 -7.4812,26.672 -12.6289,39.5507 -17.8663,44.6984 -41.4198,80.6843 -74.6856,114.1075 -26.441,26.566 -51.6323,44.8176 -82.6855,59.9082 -124.9554,60.7235 -275.5308,28.7986 -373.086,-79.1016 -92.4325,-102.2344 -119.9458,-248.8362 -70.1562,-373.8106 17.3657,-43.5887 43.6592,-83.5871 76.4023,-116.2246 38.482,-38.3578 81.0392,-64.5511 132.1739,-81.3515 48.2572,-15.855 102.913,-19.5453 159.332,-10.7598 6.9666,1.0848 13.6858,2.2858 14.9316,2.6699 1.272,0.392 3.4643,-0.5013 5,-2.0371 2.6697,-2.6697 2.7364,-4.0526 2.7364,-58.0371 v -55.3027 l -3,-1.9356 c -7.5481,-4.8687 -59.9495,-7.7173 -100.7813,-6.7246 z"
|
|
55
|
+
sodipodi:nodetypes="ssssssssssssssssssssssssssscsss" />
|
|
56
|
+
<g
|
|
57
|
+
id="g5"
|
|
58
|
+
transform="translate(2457.7895,-1000.6715)"
|
|
59
|
+
style="fill:#0cc6a6;fill-opacity:1">
|
|
60
|
+
<rect
|
|
61
|
+
style="opacity:1;fill:#0cc6a6;fill-opacity:1;stroke-width:0.938067"
|
|
62
|
+
id="rect3"
|
|
63
|
+
width="72.260895"
|
|
64
|
+
height="134.18539"
|
|
65
|
+
x="936.10278"
|
|
66
|
+
y="636.39392"
|
|
67
|
+
ry="15.974472" />
|
|
68
|
+
<rect
|
|
69
|
+
style="opacity:1;fill:#0cc6a6;fill-opacity:1;stroke-width:1.20121"
|
|
70
|
+
id="rect4"
|
|
71
|
+
width="72.260895"
|
|
72
|
+
height="220.02458"
|
|
73
|
+
x="1046.6084"
|
|
74
|
+
y="550.55475"
|
|
75
|
+
ry="19.286819" />
|
|
76
|
+
<rect
|
|
77
|
+
style="opacity:1;fill:#0cc6a6;fill-opacity:1;stroke-width:1.50056"
|
|
78
|
+
id="rect5"
|
|
79
|
+
width="72.260895"
|
|
80
|
+
height="343.35675"
|
|
81
|
+
x="1159.0873"
|
|
82
|
+
y="427.22256"
|
|
83
|
+
ry="10.289478" />
|
|
84
|
+
</g>
|
|
85
|
+
</g>
|
|
86
|
+
</g>
|
|
87
|
+
</svg>
|