gaard-api 0.1.1__tar.gz → 0.1.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.1.1/src/gaard_api.egg-info → gaard_api-0.1.2}/PKG-INFO +3 -2
- {gaard_api-0.1.1 → gaard_api-0.1.2}/pyproject.toml +3 -2
- {gaard_api-0.1.1 → gaard_api-0.1.2}/src/gaard_api/admin/services.py +15 -30
- {gaard_api-0.1.1 → gaard_api-0.1.2}/src/gaard_api/admin-web/assets/main.js +74 -19
- gaard_api-0.1.2/src/gaard_api/admin-web/package.json +10 -0
- {gaard_api-0.1.1 → gaard_api-0.1.2}/src/gaard_api/admin-web/src/main.ts +110 -15
- {gaard_api-0.1.1 → gaard_api-0.1.2}/src/gaard_api/api/v1/admin.py +38 -13
- {gaard_api-0.1.1 → gaard_api-0.1.2}/src/gaard_api/api/v1/prompts.py +4 -4
- {gaard_api-0.1.1 → gaard_api-0.1.2}/src/gaard_api/api/v1/query.py +15 -11
- {gaard_api-0.1.1 → gaard_api-0.1.2}/src/gaard_api/api/v1/schema.py +4 -4
- gaard_api-0.1.2/src/gaard_api/extensions.py +18 -0
- {gaard_api-0.1.1 → gaard_api-0.1.2/src/gaard_api.egg-info}/PKG-INFO +3 -2
- {gaard_api-0.1.1 → gaard_api-0.1.2}/src/gaard_api.egg-info/SOURCES.txt +2 -3
- {gaard_api-0.1.1 → gaard_api-0.1.2}/src/gaard_api.egg-info/requires.txt +2 -1
- {gaard_api-0.1.1 → gaard_api-0.1.2}/tests/test_admin_api.py +25 -0
- gaard_api-0.1.1/src/gaard_api/dependencies/__init__.py +0 -0
- gaard_api-0.1.1/src/gaard_api/schemas/__init__.py +0 -0
- gaard_api-0.1.1/src/gaard_api/services/__init__.py +0 -0
- {gaard_api-0.1.1 → gaard_api-0.1.2}/MANIFEST.in +0 -0
- {gaard_api-0.1.1 → gaard_api-0.1.2}/README.md +0 -0
- {gaard_api-0.1.1 → gaard_api-0.1.2}/setup.cfg +0 -0
- {gaard_api-0.1.1 → gaard_api-0.1.2}/src/gaard_api/__init__.py +0 -0
- {gaard_api-0.1.1 → gaard_api-0.1.2}/src/gaard_api/admin/__init__.py +0 -0
- {gaard_api-0.1.1 → gaard_api-0.1.2}/src/gaard_api/admin/database.py +0 -0
- {gaard_api-0.1.1 → gaard_api-0.1.2}/src/gaard_api/admin/defaults.py +0 -0
- {gaard_api-0.1.1 → gaard_api-0.1.2}/src/gaard_api/admin/models.py +0 -0
- {gaard_api-0.1.1 → gaard_api-0.1.2}/src/gaard_api/admin/prompt_runtime.py +0 -0
- {gaard_api-0.1.1 → gaard_api-0.1.2}/src/gaard_api/admin/security.py +0 -0
- {gaard_api-0.1.1 → gaard_api-0.1.2}/src/gaard_api/admin-web/assets/styles.css +0 -0
- {gaard_api-0.1.1 → gaard_api-0.1.2}/src/gaard_api/admin-web/index.html +0 -0
- {gaard_api-0.1.1 → gaard_api-0.1.2}/src/gaard_api/api/__init__.py +0 -0
- {gaard_api-0.1.1 → gaard_api-0.1.2}/src/gaard_api/api/v1/__init__.py +0 -0
- {gaard_api-0.1.1 → gaard_api-0.1.2}/src/gaard_api/cli.py +0 -0
- {gaard_api-0.1.1 → gaard_api-0.1.2}/src/gaard_api/cli_commands.py +0 -0
- {gaard_api-0.1.1 → gaard_api-0.1.2}/src/gaard_api/core/__init__.py +0 -0
- {gaard_api-0.1.1 → gaard_api-0.1.2}/src/gaard_api/core/error_handlers.py +0 -0
- {gaard_api-0.1.1 → gaard_api-0.1.2}/src/gaard_api/core/schema_cache.py +0 -0
- {gaard_api-0.1.1 → gaard_api-0.1.2}/src/gaard_api/core/settings.py +0 -0
- {gaard_api-0.1.1 → gaard_api-0.1.2}/src/gaard_api/example_data/__init__.py +0 -0
- {gaard_api-0.1.1 → gaard_api-0.1.2}/src/gaard_api/example_data/medical_poc/__init__.py +0 -0
- {gaard_api-0.1.1 → gaard_api-0.1.2}/src/gaard_api/example_data/medical_poc/schema.sql +0 -0
- {gaard_api-0.1.1 → gaard_api-0.1.2}/src/gaard_api/example_data/medical_poc/seed.sql +0 -0
- {gaard_api-0.1.1 → gaard_api-0.1.2}/src/gaard_api/example_database.py +0 -0
- {gaard_api-0.1.1 → gaard_api-0.1.2}/src/gaard_api/main.py +0 -0
- {gaard_api-0.1.1 → gaard_api-0.1.2}/src/gaard_api/server_cli.py +0 -0
- {gaard_api-0.1.1 → gaard_api-0.1.2}/src/gaard_api.egg-info/dependency_links.txt +0 -0
- {gaard_api-0.1.1 → gaard_api-0.1.2}/src/gaard_api.egg-info/entry_points.txt +0 -0
- {gaard_api-0.1.1 → gaard_api-0.1.2}/src/gaard_api.egg-info/top_level.txt +0 -0
- {gaard_api-0.1.1 → gaard_api-0.1.2}/tests/test_error_handlers.py +0 -0
- {gaard_api-0.1.1 → gaard_api-0.1.2}/tests/test_investigation_error_messages.py +0 -0
- {gaard_api-0.1.1 → gaard_api-0.1.2}/tests/test_prompt_runtime.py +0 -0
- {gaard_api-0.1.1 → gaard_api-0.1.2}/tests/test_server_cli.py +0 -0
- {gaard_api-0.1.1 → gaard_api-0.1.2}/tests/test_settings.py +0 -0
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gaard-api
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.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
7
|
Requires-Dist: gaard-core==0.1.0
|
|
8
|
-
Requires-Dist: gaard-connectors==0.1.
|
|
8
|
+
Requires-Dist: gaard-connectors==0.1.1
|
|
9
9
|
Requires-Dist: gaard-llm==0.1.0
|
|
10
|
+
Requires-Dist: gaard-plugin-api<0.2.0,>=0.1.0
|
|
10
11
|
Requires-Dist: fastapi>=0.111.0
|
|
11
12
|
Requires-Dist: uvicorn[standard]>=0.30.0
|
|
12
13
|
Requires-Dist: pydantic>=2.7.0
|
|
@@ -4,15 +4,16 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "gaard-api"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.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
13
|
"gaard-core==0.1.0",
|
|
14
|
-
"gaard-connectors==0.1.
|
|
14
|
+
"gaard-connectors==0.1.1",
|
|
15
15
|
"gaard-llm==0.1.0",
|
|
16
|
+
"gaard-plugin-api>=0.1.0,<0.2.0",
|
|
16
17
|
"fastapi>=0.111.0",
|
|
17
18
|
"uvicorn[standard]>=0.30.0",
|
|
18
19
|
"pydantic>=2.7.0",
|
|
@@ -5,12 +5,10 @@ from dataclasses import dataclass
|
|
|
5
5
|
from datetime import UTC, datetime, timedelta
|
|
6
6
|
from typing import Any
|
|
7
7
|
|
|
8
|
-
from sqlalchemy import create_engine, text
|
|
9
8
|
from sqlalchemy import delete, desc, select
|
|
10
9
|
from sqlalchemy.exc import SQLAlchemyError
|
|
11
10
|
from sqlalchemy.orm import Session
|
|
12
11
|
|
|
13
|
-
from gaard_connectors.sqlalchemy.introspector import SQLAlchemySchemaIntrospector
|
|
14
12
|
from gaard_core.errors import LlmProviderError
|
|
15
13
|
from gaard_core.llm_output import remove_thinking_blocks
|
|
16
14
|
from gaard_core.query_pipeline.models import OutputClassification, QueryRequest, QueryResponse
|
|
@@ -33,6 +31,7 @@ from gaard_api.admin.models import (
|
|
|
33
31
|
PromptTemplate,
|
|
34
32
|
)
|
|
35
33
|
from gaard_api.core.settings import settings
|
|
34
|
+
from gaard_api.extensions import get_connector_registry
|
|
36
35
|
|
|
37
36
|
|
|
38
37
|
def json_dumps(value: Any) -> str:
|
|
@@ -43,24 +42,16 @@ def json_loads(value: str) -> Any:
|
|
|
43
42
|
return json.loads(value or "{}")
|
|
44
43
|
|
|
45
44
|
|
|
46
|
-
SUPPORTED_DATABASE_TYPES = {
|
|
47
|
-
"sqlite": ("sqlite://", "sqlite"),
|
|
48
|
-
"postgresql": ("postgresql://", "postgresql+psycopg://"),
|
|
49
|
-
"mysql": ("mysql://", "mysql+pymysql://"),
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
|
|
53
45
|
def validate_datasource_url(database_type: str, database_url: str) -> None:
|
|
54
|
-
|
|
46
|
+
get_connector_registry().get(database_type).validate_database_url(database_url)
|
|
55
47
|
|
|
56
|
-
if prefixes is None:
|
|
57
|
-
raise ValueError("Unsupported datasource type.")
|
|
58
48
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
49
|
+
def validate_datasource_configuration(
|
|
50
|
+
database_type: str,
|
|
51
|
+
database_url: str,
|
|
52
|
+
sql_dialect: str,
|
|
53
|
+
) -> None:
|
|
54
|
+
get_connector_registry().validate(database_type, database_url, sql_dialect)
|
|
64
55
|
|
|
65
56
|
|
|
66
57
|
def mask_database_url(database_url: str) -> str:
|
|
@@ -1896,18 +1887,7 @@ def set_active_datasource_connector(
|
|
|
1896
1887
|
|
|
1897
1888
|
|
|
1898
1889
|
def test_datasource_connection(connector: DatasourceConnector) -> None:
|
|
1899
|
-
|
|
1900
|
-
{"check_same_thread": False}
|
|
1901
|
-
if connector.database_url.startswith("sqlite")
|
|
1902
|
-
else {}
|
|
1903
|
-
)
|
|
1904
|
-
engine = create_engine(connector.database_url, connect_args=connect_args)
|
|
1905
|
-
|
|
1906
|
-
try:
|
|
1907
|
-
with engine.connect() as connection:
|
|
1908
|
-
connection.execute(text("SELECT 1"))
|
|
1909
|
-
finally:
|
|
1910
|
-
engine.dispose()
|
|
1890
|
+
get_connector_registry().get(connector.database_type).connection_tester(connector.database_url)
|
|
1911
1891
|
|
|
1912
1892
|
|
|
1913
1893
|
def get_datasource_schema_cache(
|
|
@@ -1935,7 +1915,12 @@ def introspect_datasource_connector(
|
|
|
1935
1915
|
connector: DatasourceConnector,
|
|
1936
1916
|
actor: str,
|
|
1937
1917
|
) -> DatasourceSchemaCache:
|
|
1938
|
-
schema =
|
|
1918
|
+
schema = (
|
|
1919
|
+
get_connector_registry()
|
|
1920
|
+
.get(connector.database_type)
|
|
1921
|
+
.introspector_factory(connector.database_url)
|
|
1922
|
+
.introspect()
|
|
1923
|
+
)
|
|
1939
1924
|
existing = get_datasource_schema_cache(session, connector.id)
|
|
1940
1925
|
existing_settings = (
|
|
1941
1926
|
json_loads(existing.table_settings_json) if existing is not None else {}
|
|
@@ -81,6 +81,7 @@ const state = {
|
|
|
81
81
|
llmConfig: null,
|
|
82
82
|
governancePolicy: null,
|
|
83
83
|
datasources: [],
|
|
84
|
+
datasourceTypes: [],
|
|
84
85
|
selectedDatasourceId: null,
|
|
85
86
|
datasourceSchema: null,
|
|
86
87
|
datasourceSchemaLoading: false,
|
|
@@ -946,7 +947,14 @@ function getSelectedDatasource() {
|
|
|
946
947
|
}
|
|
947
948
|
function renderDatasourceForm(connector) {
|
|
948
949
|
const systemManaged = connector?.system_managed === true;
|
|
949
|
-
const
|
|
950
|
+
const selectedTypeKey = connector?.database_type || state.datasourceTypes[0]?.type_key || "";
|
|
951
|
+
const selectedType = getDatasourceType(selectedTypeKey);
|
|
952
|
+
const databaseUrlSchema = selectedType?.config_schema?.properties?.database_url;
|
|
953
|
+
const selectedSqlDialect = connector?.sql_dialect || selectedType?.default_sql_dialect || "";
|
|
954
|
+
const databaseUrl = connector?.database_url || String(databaseUrlSchema?.default || "");
|
|
955
|
+
const unavailableType = Boolean(selectedTypeKey && !selectedType);
|
|
956
|
+
const disabled = systemManaged || unavailableType || !selectedType ? "disabled" : "";
|
|
957
|
+
const connectorDescription = selectedType?.description || (unavailableType ? `Connector type '${selectedTypeKey}' is unavailable. Install or enable its plugin before editing this datasource.` : "No connector types are available. Install or enable a connector plugin.");
|
|
950
958
|
return `
|
|
951
959
|
<form id="datasource-form" class="form-grid">
|
|
952
960
|
<input type="hidden" name="id" value="${escapeHtml(connector?.id || "")}" />
|
|
@@ -954,32 +962,74 @@ function renderDatasourceForm(connector) {
|
|
|
954
962
|
<label>Connector key<input name="connector_key" ${connector || systemManaged ? "readonly" : ""} ${disabled} value="${escapeHtml(connector?.connector_key || "")}" /></label>
|
|
955
963
|
<label>Name<input name="name" ${disabled} value="${escapeHtml(connector?.name || "")}" /></label>
|
|
956
964
|
<div class="subgrid">
|
|
957
|
-
<label>
|
|
958
|
-
<label>SQL dialect<select name="sql_dialect" ${disabled}>${renderSqlDialectOptions(
|
|
965
|
+
<label>Connector type<select id="datasource-type" name="database_type" ${disabled}>${renderDatasourceTypeOptions(selectedTypeKey)}</select></label>
|
|
966
|
+
<label>SQL dialect<select id="datasource-sql-dialect" name="sql_dialect" ${disabled}>${renderSqlDialectOptions(selectedType, selectedSqlDialect)}</select></label>
|
|
959
967
|
</div>
|
|
960
|
-
<
|
|
968
|
+
<p id="datasource-type-description" class="muted">${escapeHtml(connectorDescription)}</p>
|
|
969
|
+
<label><span id="datasource-url-label">${escapeHtml(databaseUrlSchema?.title || "Database URL")}</span><input id="datasource-url" name="database_url" ${disabled} placeholder="${escapeHtml(databaseUrlSchema?.description || "")}" value="${escapeHtml(databaseUrl)}" /></label>
|
|
961
970
|
<label class="inline-check"><input name="active" type="checkbox" ${connector?.active ? "checked" : ""} ${disabled} /> Active datasource</label>
|
|
962
971
|
<div class="button-row">
|
|
963
|
-
<button type="button" id="test-datasource">Test</button>
|
|
972
|
+
<button type="button" id="test-datasource" ${disabled}>Test</button>
|
|
964
973
|
<button type="button" id="introspect-datasource" ${connector ? "" : "disabled"}>Schema introspection</button>
|
|
965
974
|
<button type="button" id="activate-datasource" ${connector && !connector.active && !systemManaged ? "" : "disabled"}>Activate</button>
|
|
966
975
|
<button class="primary" type="submit" ${systemManaged ? "disabled" : ""}>${connector ? "Save" : "Create"}</button>
|
|
967
976
|
</div>
|
|
968
977
|
</form>`;
|
|
969
978
|
}
|
|
970
|
-
function
|
|
971
|
-
return
|
|
972
|
-
"sqlite",
|
|
973
|
-
"postgresql",
|
|
974
|
-
"mysql"
|
|
975
|
-
].map((value)=>`<option value="${value}" ${selected === value ? "selected" : ""}>${value}</option>`).join("");
|
|
979
|
+
function getDatasourceType(typeKey) {
|
|
980
|
+
return state.datasourceTypes.find((item)=>item.type_key === typeKey) || null;
|
|
976
981
|
}
|
|
977
|
-
function
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
982
|
+
function renderDatasourceTypeOptions(selected) {
|
|
983
|
+
const datasourceTypes = [
|
|
984
|
+
...state.datasourceTypes
|
|
985
|
+
];
|
|
986
|
+
if (selected && !getDatasourceType(selected)) {
|
|
987
|
+
datasourceTypes.unshift({
|
|
988
|
+
type_key: selected,
|
|
989
|
+
label: `${selected} (plugin unavailable)`,
|
|
990
|
+
description: "",
|
|
991
|
+
sql_dialects: [],
|
|
992
|
+
default_sql_dialect: "",
|
|
993
|
+
config_schema: {}
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
if (!datasourceTypes.length) {
|
|
997
|
+
return `<option value="" selected>No connector types available</option>`;
|
|
998
|
+
}
|
|
999
|
+
return datasourceTypes.map((item)=>`<option value="${escapeHtml(item.type_key)}" ${item.type_key === selected ? "selected" : ""}>${escapeHtml(item.label)}</option>`).join("");
|
|
1000
|
+
}
|
|
1001
|
+
function renderSqlDialectOptions(datasourceType, selected) {
|
|
1002
|
+
const dialects = [
|
|
1003
|
+
...datasourceType?.sql_dialects || []
|
|
1004
|
+
];
|
|
1005
|
+
if (selected && !dialects.includes(selected)) {
|
|
1006
|
+
dialects.unshift(selected);
|
|
1007
|
+
}
|
|
1008
|
+
if (!dialects.length) {
|
|
1009
|
+
return `<option value="" selected>No SQL dialect available</option>`;
|
|
1010
|
+
}
|
|
1011
|
+
return dialects.map((value)=>`<option value="${escapeHtml(value)}" ${selected === value ? "selected" : ""}>${escapeHtml(value)}</option>`).join("");
|
|
1012
|
+
}
|
|
1013
|
+
function syncDatasourceTypeFields(event) {
|
|
1014
|
+
const typeKey = event.currentTarget.value;
|
|
1015
|
+
const datasourceType = getDatasourceType(typeKey);
|
|
1016
|
+
const sqlDialect = document.querySelector("#datasource-sql-dialect");
|
|
1017
|
+
const description = document.querySelector("#datasource-type-description");
|
|
1018
|
+
const urlLabel = document.querySelector("#datasource-url-label");
|
|
1019
|
+
const urlInput = document.querySelector("#datasource-url");
|
|
1020
|
+
const databaseUrlSchema = datasourceType?.config_schema?.properties?.database_url;
|
|
1021
|
+
if (sqlDialect) {
|
|
1022
|
+
sqlDialect.innerHTML = renderSqlDialectOptions(datasourceType, datasourceType?.default_sql_dialect || "");
|
|
1023
|
+
}
|
|
1024
|
+
if (description) {
|
|
1025
|
+
description.textContent = datasourceType?.description || "Connector type is unavailable.";
|
|
1026
|
+
}
|
|
1027
|
+
if (urlLabel) {
|
|
1028
|
+
urlLabel.textContent = databaseUrlSchema?.title || "Database URL";
|
|
1029
|
+
}
|
|
1030
|
+
if (urlInput) {
|
|
1031
|
+
urlInput.placeholder = databaseUrlSchema?.description || "";
|
|
1032
|
+
}
|
|
983
1033
|
}
|
|
984
1034
|
function renderModeOptions(selected, values) {
|
|
985
1035
|
return values.map((value)=>`<option value="${value}" ${selected === value ? "selected" : ""}>${value}</option>`).join("");
|
|
@@ -1315,6 +1365,7 @@ function attachSectionHandlers() {
|
|
|
1315
1365
|
document.querySelector("#llm-config-form")?.addEventListener("submit", saveLlmConfig);
|
|
1316
1366
|
document.querySelector("#governance-policy-form")?.addEventListener("submit", saveGovernancePolicy);
|
|
1317
1367
|
document.querySelector("#datasource-form")?.addEventListener("submit", saveDatasource);
|
|
1368
|
+
document.querySelector("#datasource-type")?.addEventListener("change", syncDatasourceTypeFields);
|
|
1318
1369
|
document.querySelector("#datasource-schema-form")?.addEventListener("submit", saveDatasourceSchema);
|
|
1319
1370
|
document.querySelector("#schema-show-enabled-only")?.addEventListener("change", (event)=>{
|
|
1320
1371
|
syncDatasourceSchemaDraftFromForm();
|
|
@@ -1974,8 +2025,12 @@ async function loadPrompts() {
|
|
|
1974
2025
|
render();
|
|
1975
2026
|
}
|
|
1976
2027
|
async function loadDatasources() {
|
|
1977
|
-
const
|
|
1978
|
-
|
|
2028
|
+
const [datasources, datasourceTypes] = await Promise.all([
|
|
2029
|
+
api("/api/v1/admin/datasources"),
|
|
2030
|
+
api("/api/v1/admin/datasource-types")
|
|
2031
|
+
]);
|
|
2032
|
+
state.datasources = datasources.items || [];
|
|
2033
|
+
state.datasourceTypes = datasourceTypes.items || [];
|
|
1979
2034
|
if (!state.selectedDatasourceId || state.selectedDatasourceId === "new") {
|
|
1980
2035
|
state.selectedDatasourceId = state.datasources[0]?.id || null;
|
|
1981
2036
|
}
|
|
@@ -26,14 +26,31 @@ type DatasourceConnector = {
|
|
|
26
26
|
id: number;
|
|
27
27
|
connector_key: string;
|
|
28
28
|
name: string;
|
|
29
|
-
database_type:
|
|
29
|
+
database_type: string;
|
|
30
30
|
database_url: string;
|
|
31
31
|
masked_database_url: string;
|
|
32
|
-
sql_dialect:
|
|
32
|
+
sql_dialect: string;
|
|
33
33
|
active: boolean;
|
|
34
34
|
system_managed: boolean;
|
|
35
35
|
};
|
|
36
36
|
|
|
37
|
+
type DatasourceTypeConfigSchema = {
|
|
38
|
+
properties?: Record<string, {
|
|
39
|
+
title?: string;
|
|
40
|
+
description?: string;
|
|
41
|
+
default?: unknown;
|
|
42
|
+
}>;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
type DatasourceType = {
|
|
46
|
+
type_key: string;
|
|
47
|
+
label: string;
|
|
48
|
+
description: string;
|
|
49
|
+
sql_dialects: string[];
|
|
50
|
+
default_sql_dialect: string;
|
|
51
|
+
config_schema: DatasourceTypeConfigSchema;
|
|
52
|
+
};
|
|
53
|
+
|
|
37
54
|
type OverviewDatasource = {
|
|
38
55
|
connector_key: string;
|
|
39
56
|
name: string;
|
|
@@ -108,6 +125,7 @@ type State = {
|
|
|
108
125
|
llmConfig: any | null;
|
|
109
126
|
governancePolicy: any | null;
|
|
110
127
|
datasources: DatasourceConnector[];
|
|
128
|
+
datasourceTypes: DatasourceType[];
|
|
111
129
|
selectedDatasourceId: number | "new" | null;
|
|
112
130
|
datasourceSchema: any | null;
|
|
113
131
|
datasourceSchemaLoading: boolean;
|
|
@@ -167,6 +185,7 @@ const state: State = {
|
|
|
167
185
|
llmConfig: null,
|
|
168
186
|
governancePolicy: null,
|
|
169
187
|
datasources: [],
|
|
188
|
+
datasourceTypes: [],
|
|
170
189
|
selectedDatasourceId: null,
|
|
171
190
|
datasourceSchema: null,
|
|
172
191
|
datasourceSchemaLoading: false,
|
|
@@ -1143,7 +1162,17 @@ function getSelectedDatasource(): DatasourceConnector | null {
|
|
|
1143
1162
|
|
|
1144
1163
|
function renderDatasourceForm(connector: DatasourceConnector | null): string {
|
|
1145
1164
|
const systemManaged = connector?.system_managed === true;
|
|
1146
|
-
const
|
|
1165
|
+
const selectedTypeKey = connector?.database_type || state.datasourceTypes[0]?.type_key || "";
|
|
1166
|
+
const selectedType = getDatasourceType(selectedTypeKey);
|
|
1167
|
+
const databaseUrlSchema = selectedType?.config_schema?.properties?.database_url;
|
|
1168
|
+
const selectedSqlDialect = connector?.sql_dialect || selectedType?.default_sql_dialect || "";
|
|
1169
|
+
const databaseUrl = connector?.database_url || String(databaseUrlSchema?.default || "");
|
|
1170
|
+
const unavailableType = Boolean(selectedTypeKey && !selectedType);
|
|
1171
|
+
const disabled = systemManaged || unavailableType || !selectedType ? "disabled" : "";
|
|
1172
|
+
const connectorDescription = selectedType?.description
|
|
1173
|
+
|| (unavailableType
|
|
1174
|
+
? `Connector type '${selectedTypeKey}' is unavailable. Install or enable its plugin before editing this datasource.`
|
|
1175
|
+
: "No connector types are available. Install or enable a connector plugin.");
|
|
1147
1176
|
return `
|
|
1148
1177
|
<form id="datasource-form" class="form-grid">
|
|
1149
1178
|
<input type="hidden" name="id" value="${escapeHtml(connector?.id || "")}" />
|
|
@@ -1151,13 +1180,14 @@ function renderDatasourceForm(connector: DatasourceConnector | null): string {
|
|
|
1151
1180
|
<label>Connector key<input name="connector_key" ${connector || systemManaged ? "readonly" : ""} ${disabled} value="${escapeHtml(connector?.connector_key || "")}" /></label>
|
|
1152
1181
|
<label>Name<input name="name" ${disabled} value="${escapeHtml(connector?.name || "")}" /></label>
|
|
1153
1182
|
<div class="subgrid">
|
|
1154
|
-
<label>
|
|
1155
|
-
<label>SQL dialect<select name="sql_dialect" ${disabled}>${renderSqlDialectOptions(
|
|
1183
|
+
<label>Connector type<select id="datasource-type" name="database_type" ${disabled}>${renderDatasourceTypeOptions(selectedTypeKey)}</select></label>
|
|
1184
|
+
<label>SQL dialect<select id="datasource-sql-dialect" name="sql_dialect" ${disabled}>${renderSqlDialectOptions(selectedType, selectedSqlDialect)}</select></label>
|
|
1156
1185
|
</div>
|
|
1157
|
-
<
|
|
1186
|
+
<p id="datasource-type-description" class="muted">${escapeHtml(connectorDescription)}</p>
|
|
1187
|
+
<label><span id="datasource-url-label">${escapeHtml(databaseUrlSchema?.title || "Database URL")}</span><input id="datasource-url" name="database_url" ${disabled} placeholder="${escapeHtml(databaseUrlSchema?.description || "")}" value="${escapeHtml(databaseUrl)}" /></label>
|
|
1158
1188
|
<label class="inline-check"><input name="active" type="checkbox" ${connector?.active ? "checked" : ""} ${disabled} /> Active datasource</label>
|
|
1159
1189
|
<div class="button-row">
|
|
1160
|
-
<button type="button" id="test-datasource">Test</button>
|
|
1190
|
+
<button type="button" id="test-datasource" ${disabled}>Test</button>
|
|
1161
1191
|
<button type="button" id="introspect-datasource" ${connector ? "" : "disabled"}>Schema introspection</button>
|
|
1162
1192
|
<button type="button" id="activate-datasource" ${connector && !connector.active && !systemManaged ? "" : "disabled"}>Activate</button>
|
|
1163
1193
|
<button class="primary" type="submit" ${systemManaged ? "disabled" : ""}>${connector ? "Save" : "Create"}</button>
|
|
@@ -1165,18 +1195,78 @@ function renderDatasourceForm(connector: DatasourceConnector | null): string {
|
|
|
1165
1195
|
</form>`;
|
|
1166
1196
|
}
|
|
1167
1197
|
|
|
1168
|
-
function
|
|
1169
|
-
return
|
|
1170
|
-
|
|
1198
|
+
function getDatasourceType(typeKey: string): DatasourceType | null {
|
|
1199
|
+
return state.datasourceTypes.find(item => item.type_key === typeKey) || null;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
function renderDatasourceTypeOptions(selected: string): string {
|
|
1203
|
+
const datasourceTypes = [...state.datasourceTypes];
|
|
1204
|
+
|
|
1205
|
+
if (selected && !getDatasourceType(selected)) {
|
|
1206
|
+
datasourceTypes.unshift({
|
|
1207
|
+
type_key: selected,
|
|
1208
|
+
label: `${selected} (plugin unavailable)`,
|
|
1209
|
+
description: "",
|
|
1210
|
+
sql_dialects: [],
|
|
1211
|
+
default_sql_dialect: "",
|
|
1212
|
+
config_schema: {},
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
if (!datasourceTypes.length) {
|
|
1217
|
+
return `<option value="" selected>No connector types available</option>`;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
return datasourceTypes
|
|
1221
|
+
.map(item => `<option value="${escapeHtml(item.type_key)}" ${item.type_key === selected ? "selected" : ""}>${escapeHtml(item.label)}</option>`)
|
|
1171
1222
|
.join("");
|
|
1172
1223
|
}
|
|
1173
1224
|
|
|
1174
|
-
function renderSqlDialectOptions(selected: string): string {
|
|
1175
|
-
|
|
1176
|
-
|
|
1225
|
+
function renderSqlDialectOptions(datasourceType: DatasourceType | null, selected: string): string {
|
|
1226
|
+
const dialects = [...(datasourceType?.sql_dialects || [])];
|
|
1227
|
+
|
|
1228
|
+
if (selected && !dialects.includes(selected)) {
|
|
1229
|
+
dialects.unshift(selected);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
if (!dialects.length) {
|
|
1233
|
+
return `<option value="" selected>No SQL dialect available</option>`;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
return dialects
|
|
1237
|
+
.map(value => `<option value="${escapeHtml(value)}" ${selected === value ? "selected" : ""}>${escapeHtml(value)}</option>`)
|
|
1177
1238
|
.join("");
|
|
1178
1239
|
}
|
|
1179
1240
|
|
|
1241
|
+
function syncDatasourceTypeFields(event: Event): void {
|
|
1242
|
+
const typeKey = (event.currentTarget as HTMLSelectElement).value;
|
|
1243
|
+
const datasourceType = getDatasourceType(typeKey);
|
|
1244
|
+
const sqlDialect = document.querySelector<HTMLSelectElement>("#datasource-sql-dialect");
|
|
1245
|
+
const description = document.querySelector<HTMLElement>("#datasource-type-description");
|
|
1246
|
+
const urlLabel = document.querySelector<HTMLElement>("#datasource-url-label");
|
|
1247
|
+
const urlInput = document.querySelector<HTMLInputElement>("#datasource-url");
|
|
1248
|
+
const databaseUrlSchema = datasourceType?.config_schema?.properties?.database_url;
|
|
1249
|
+
|
|
1250
|
+
if (sqlDialect) {
|
|
1251
|
+
sqlDialect.innerHTML = renderSqlDialectOptions(
|
|
1252
|
+
datasourceType,
|
|
1253
|
+
datasourceType?.default_sql_dialect || "",
|
|
1254
|
+
);
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
if (description) {
|
|
1258
|
+
description.textContent = datasourceType?.description || "Connector type is unavailable.";
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
if (urlLabel) {
|
|
1262
|
+
urlLabel.textContent = databaseUrlSchema?.title || "Database URL";
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
if (urlInput) {
|
|
1266
|
+
urlInput.placeholder = databaseUrlSchema?.description || "";
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1180
1270
|
function renderModeOptions(selected: string, values: string[]): string {
|
|
1181
1271
|
return values
|
|
1182
1272
|
.map(value => `<option value="${value}" ${selected === value ? "selected" : ""}>${value}</option>`)
|
|
@@ -1533,6 +1623,7 @@ function attachSectionHandlers(): void {
|
|
|
1533
1623
|
document.querySelector<HTMLFormElement>("#llm-config-form")?.addEventListener("submit", saveLlmConfig);
|
|
1534
1624
|
document.querySelector<HTMLFormElement>("#governance-policy-form")?.addEventListener("submit", saveGovernancePolicy);
|
|
1535
1625
|
document.querySelector<HTMLFormElement>("#datasource-form")?.addEventListener("submit", saveDatasource);
|
|
1626
|
+
document.querySelector<HTMLSelectElement>("#datasource-type")?.addEventListener("change", syncDatasourceTypeFields);
|
|
1536
1627
|
document.querySelector<HTMLFormElement>("#datasource-schema-form")?.addEventListener("submit", saveDatasourceSchema);
|
|
1537
1628
|
document.querySelector<HTMLInputElement>("#schema-show-enabled-only")?.addEventListener("change", event => {
|
|
1538
1629
|
syncDatasourceSchemaDraftFromForm();
|
|
@@ -2252,8 +2343,12 @@ async function loadPrompts(): Promise<void> {
|
|
|
2252
2343
|
}
|
|
2253
2344
|
|
|
2254
2345
|
async function loadDatasources(): Promise<void> {
|
|
2255
|
-
const
|
|
2256
|
-
|
|
2346
|
+
const [datasources, datasourceTypes] = await Promise.all([
|
|
2347
|
+
api<{ items: DatasourceConnector[] }>("/api/v1/admin/datasources"),
|
|
2348
|
+
api<{ items: DatasourceType[] }>("/api/v1/admin/datasource-types"),
|
|
2349
|
+
]);
|
|
2350
|
+
state.datasources = datasources.items || [];
|
|
2351
|
+
state.datasourceTypes = datasourceTypes.items || [];
|
|
2257
2352
|
if (!state.selectedDatasourceId || state.selectedDatasourceId === "new") {
|
|
2258
2353
|
state.selectedDatasourceId = state.datasources[0]?.id || null;
|
|
2259
2354
|
}
|
|
@@ -4,7 +4,6 @@ from datetime import datetime
|
|
|
4
4
|
from typing import Any, Annotated
|
|
5
5
|
|
|
6
6
|
from fastapi import APIRouter, Depends, Header, HTTPException, status
|
|
7
|
-
from gaard_connectors.sqlalchemy.executor import SQLAlchemyQueryExecutor
|
|
8
7
|
from gaard_core.errors import (
|
|
9
8
|
ConfigurationError,
|
|
10
9
|
LlmProviderError,
|
|
@@ -92,10 +91,12 @@ from gaard_api.admin.services import (
|
|
|
92
91
|
update_business_logic_suggestion_content,
|
|
93
92
|
update_schema_table_settings,
|
|
94
93
|
validate_datasource_url,
|
|
94
|
+
validate_datasource_configuration,
|
|
95
95
|
)
|
|
96
96
|
from gaard_api.api.v1.schema import get_schema_cache_key
|
|
97
97
|
from gaard_api.core.schema_cache import schema_context_cache
|
|
98
98
|
from gaard_api.core.settings import settings
|
|
99
|
+
from gaard_api.extensions import get_connector_registry
|
|
99
100
|
|
|
100
101
|
router = APIRouter()
|
|
101
102
|
|
|
@@ -220,22 +221,22 @@ class OverviewWidgetFromQueryRequest(BaseModel):
|
|
|
220
221
|
class DatasourceConnectorRequest(BaseModel):
|
|
221
222
|
connector_key: str = Field(min_length=1, pattern=r"^[a-zA-Z0-9_-]+$")
|
|
222
223
|
name: str = Field(min_length=1)
|
|
223
|
-
database_type: str = Field(pattern=r"^
|
|
224
|
+
database_type: str = Field(pattern=r"^[a-z][a-z0-9_-]{0,63}$")
|
|
224
225
|
database_url: str = Field(min_length=1)
|
|
225
|
-
sql_dialect: str = Field(pattern=r"^
|
|
226
|
+
sql_dialect: str = Field(pattern=r"^[a-z][a-z0-9_-]{0,63}$")
|
|
226
227
|
active: bool = False
|
|
227
228
|
|
|
228
229
|
|
|
229
230
|
class DatasourceConnectorUpdateRequest(BaseModel):
|
|
230
231
|
name: str = Field(min_length=1)
|
|
231
|
-
database_type: str = Field(pattern=r"^
|
|
232
|
+
database_type: str = Field(pattern=r"^[a-z][a-z0-9_-]{0,63}$")
|
|
232
233
|
database_url: str = Field(min_length=1)
|
|
233
|
-
sql_dialect: str = Field(pattern=r"^
|
|
234
|
+
sql_dialect: str = Field(pattern=r"^[a-z][a-z0-9_-]{0,63}$")
|
|
234
235
|
active: bool = False
|
|
235
236
|
|
|
236
237
|
|
|
237
238
|
class DatasourceConnectionTestRequest(BaseModel):
|
|
238
|
-
database_type: str = Field(pattern=r"^
|
|
239
|
+
database_type: str = Field(pattern=r"^[a-z][a-z0-9_-]{0,63}$")
|
|
239
240
|
database_url: str = Field(min_length=1)
|
|
240
241
|
|
|
241
242
|
|
|
@@ -472,10 +473,15 @@ def execute_overview_sql(
|
|
|
472
473
|
) -> QueryResult:
|
|
473
474
|
SelectOnlySqlValidator(dialect=connector.sql_dialect).validate(sql)
|
|
474
475
|
|
|
475
|
-
return
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
476
|
+
return (
|
|
477
|
+
get_connector_registry()
|
|
478
|
+
.get(connector.database_type)
|
|
479
|
+
.executor_factory(
|
|
480
|
+
connector.database_url,
|
|
481
|
+
get_query_runtime_config(session).query_max_rows,
|
|
482
|
+
)
|
|
483
|
+
.execute(sql)
|
|
484
|
+
)
|
|
479
485
|
|
|
480
486
|
|
|
481
487
|
def generate_overview_widget_sql(
|
|
@@ -1502,6 +1508,16 @@ def get_datasources(
|
|
|
1502
1508
|
}
|
|
1503
1509
|
|
|
1504
1510
|
|
|
1511
|
+
@router.get("/datasource-types")
|
|
1512
|
+
def get_datasource_types(
|
|
1513
|
+
user: AdminUser = Depends(get_current_admin),
|
|
1514
|
+
) -> dict[str, Any]:
|
|
1515
|
+
return {
|
|
1516
|
+
"items": [definition.serialize() for definition in get_connector_registry().list()],
|
|
1517
|
+
"viewer": user.username,
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
|
|
1505
1521
|
@router.post("/datasources")
|
|
1506
1522
|
def create_datasource(
|
|
1507
1523
|
request: DatasourceConnectorRequest,
|
|
@@ -1515,7 +1531,11 @@ def create_datasource(
|
|
|
1515
1531
|
)
|
|
1516
1532
|
|
|
1517
1533
|
try:
|
|
1518
|
-
|
|
1534
|
+
validate_datasource_configuration(
|
|
1535
|
+
request.database_type,
|
|
1536
|
+
request.database_url,
|
|
1537
|
+
request.sql_dialect,
|
|
1538
|
+
)
|
|
1519
1539
|
except ValueError as exc:
|
|
1520
1540
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
|
|
1521
1541
|
|
|
@@ -1584,7 +1604,11 @@ def update_datasource(
|
|
|
1584
1604
|
)
|
|
1585
1605
|
|
|
1586
1606
|
try:
|
|
1587
|
-
|
|
1607
|
+
validate_datasource_configuration(
|
|
1608
|
+
request.database_type,
|
|
1609
|
+
request.database_url,
|
|
1610
|
+
request.sql_dialect,
|
|
1611
|
+
)
|
|
1588
1612
|
except ValueError as exc:
|
|
1589
1613
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
|
|
1590
1614
|
|
|
@@ -1695,12 +1719,13 @@ def test_datasource_from_request(
|
|
|
1695
1719
|
request: DatasourceConnectionTestRequest,
|
|
1696
1720
|
) -> dict[str, Any]:
|
|
1697
1721
|
validate_datasource_url(request.database_type, request.database_url)
|
|
1722
|
+
definition = get_connector_registry().get(request.database_type)
|
|
1698
1723
|
connector = DatasourceConnector(
|
|
1699
1724
|
connector_key="__preview__",
|
|
1700
1725
|
name="__preview__",
|
|
1701
1726
|
database_type=request.database_type,
|
|
1702
1727
|
database_url=request.database_url,
|
|
1703
|
-
sql_dialect=
|
|
1728
|
+
sql_dialect=definition.default_sql_dialect,
|
|
1704
1729
|
active=False,
|
|
1705
1730
|
)
|
|
1706
1731
|
test_datasource_connection(connector)
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from fastapi import APIRouter
|
|
2
2
|
from pydantic import BaseModel, Field
|
|
3
3
|
|
|
4
|
-
from gaard_connectors.sqlalchemy.introspector import SQLAlchemySchemaIntrospector
|
|
5
4
|
from gaard_core.prompt_compiler.models import CompiledPrompt
|
|
6
5
|
from gaard_core.prompt_compiler.models import SqlGenerationPromptRequest
|
|
7
6
|
from gaard_core.prompt_compiler.sql_generation_prompt import SqlGenerationPromptCompiler
|
|
@@ -12,6 +11,7 @@ from gaard_api.admin.services import get_datasource_schema_context_safe, get_que
|
|
|
12
11
|
from gaard_api.api.v1.schema import get_schema_cache_key
|
|
13
12
|
from gaard_api.core.schema_cache import schema_context_cache
|
|
14
13
|
from gaard_api.core.settings import settings
|
|
14
|
+
from gaard_api.extensions import get_connector_registry
|
|
15
15
|
|
|
16
16
|
router = APIRouter()
|
|
17
17
|
|
|
@@ -32,9 +32,9 @@ def compile_sql_generation_prompt(
|
|
|
32
32
|
sql_dialect = connector.sql_dialect
|
|
33
33
|
else:
|
|
34
34
|
sql_dialect = settings.gaard_sql_dialect
|
|
35
|
-
introspector =
|
|
36
|
-
|
|
37
|
-
)
|
|
35
|
+
introspector = get_connector_registry().detect_from_database_url(
|
|
36
|
+
settings.gaard_datasource_url
|
|
37
|
+
).introspector_factory(settings.gaard_datasource_url)
|
|
38
38
|
service = SchemaContextService(
|
|
39
39
|
introspector=introspector,
|
|
40
40
|
cache=schema_context_cache,
|
|
@@ -8,8 +8,6 @@ from typing import Any, Callable
|
|
|
8
8
|
from fastapi import APIRouter
|
|
9
9
|
from fastapi.responses import StreamingResponse
|
|
10
10
|
|
|
11
|
-
from gaard_connectors.sqlalchemy.executor import SQLAlchemyQueryExecutor
|
|
12
|
-
from gaard_connectors.sqlalchemy.introspector import SQLAlchemySchemaIntrospector
|
|
13
11
|
from gaard_core.errors import (
|
|
14
12
|
ConfigurationError,
|
|
15
13
|
LlmProviderError,
|
|
@@ -75,6 +73,7 @@ from gaard_api.admin.services import (
|
|
|
75
73
|
from gaard_api.api.v1.schema import get_schema_cache_key
|
|
76
74
|
from gaard_api.core.schema_cache import schema_context_cache
|
|
77
75
|
from gaard_api.core.settings import settings
|
|
76
|
+
from gaard_api.extensions import get_connector_registry
|
|
78
77
|
|
|
79
78
|
router = APIRouter()
|
|
80
79
|
|
|
@@ -169,9 +168,9 @@ def create_sql_generator(
|
|
|
169
168
|
prompt_compiler=get_sql_generation_prompt_compiler(),
|
|
170
169
|
)
|
|
171
170
|
|
|
172
|
-
introspector =
|
|
173
|
-
|
|
174
|
-
)
|
|
171
|
+
introspector = get_connector_registry().detect_from_database_url(
|
|
172
|
+
settings.gaard_datasource_url
|
|
173
|
+
).introspector_factory(settings.gaard_datasource_url)
|
|
175
174
|
|
|
176
175
|
schema_context_service = SchemaContextService(
|
|
177
176
|
introspector=introspector,
|
|
@@ -315,9 +314,14 @@ def create_pipeline(datasource_context: DatasourceContext | None = None) -> Quer
|
|
|
315
314
|
else settings.gaard_sql_dialect
|
|
316
315
|
)
|
|
317
316
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
317
|
+
connector_definition = (
|
|
318
|
+
get_connector_registry().get(datasource_context[0].database_type)
|
|
319
|
+
if datasource_context is not None
|
|
320
|
+
else get_connector_registry().detect_from_database_url(database_url)
|
|
321
|
+
)
|
|
322
|
+
executor = connector_definition.executor_factory(
|
|
323
|
+
database_url,
|
|
324
|
+
runtime_config.query_max_rows,
|
|
321
325
|
)
|
|
322
326
|
llm_config = (
|
|
323
327
|
get_llm_runtime_config_safe()
|
|
@@ -376,9 +380,9 @@ def schema_and_business_logic_for_investigation(
|
|
|
376
380
|
get_active_business_logic_prompt_safe(connector.id),
|
|
377
381
|
)
|
|
378
382
|
|
|
379
|
-
introspector =
|
|
380
|
-
|
|
381
|
-
)
|
|
383
|
+
introspector = get_connector_registry().detect_from_database_url(
|
|
384
|
+
settings.gaard_datasource_url
|
|
385
|
+
).introspector_factory(settings.gaard_datasource_url)
|
|
382
386
|
schema_context_service = SchemaContextService(
|
|
383
387
|
introspector=introspector,
|
|
384
388
|
cache=schema_context_cache,
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from fastapi import APIRouter
|
|
2
2
|
|
|
3
|
-
from gaard_connectors.sqlalchemy.introspector import SQLAlchemySchemaIntrospector
|
|
4
3
|
from gaard_core.schema.context import SchemaContextService
|
|
5
4
|
from gaard_core.schema.models import DatabaseSchema
|
|
6
5
|
|
|
@@ -10,6 +9,7 @@ from gaard_api.admin.services import (
|
|
|
10
9
|
)
|
|
11
10
|
from gaard_api.core.schema_cache import schema_context_cache
|
|
12
11
|
from gaard_api.core.settings import settings
|
|
12
|
+
from gaard_api.extensions import get_connector_registry
|
|
13
13
|
|
|
14
14
|
router = APIRouter()
|
|
15
15
|
|
|
@@ -29,9 +29,9 @@ def get_schema() -> DatabaseSchema:
|
|
|
29
29
|
_connector, schema_cache = datasource_context
|
|
30
30
|
return selected_schema_from_cache(schema_cache)
|
|
31
31
|
|
|
32
|
-
introspector =
|
|
33
|
-
|
|
34
|
-
)
|
|
32
|
+
introspector = get_connector_registry().detect_from_database_url(
|
|
33
|
+
settings.gaard_datasource_url
|
|
34
|
+
).introspector_factory(settings.gaard_datasource_url)
|
|
35
35
|
|
|
36
36
|
service = SchemaContextService(
|
|
37
37
|
introspector=introspector,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from functools import lru_cache
|
|
2
|
+
|
|
3
|
+
from gaard_connectors import ConnectorRegistry, create_builtin_connector_registry
|
|
4
|
+
from gaard_plugin_api import ExtensionManager
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@lru_cache
|
|
8
|
+
def get_extension_manager() -> ExtensionManager:
|
|
9
|
+
manager = ExtensionManager()
|
|
10
|
+
manager.discover()
|
|
11
|
+
return manager
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@lru_cache
|
|
15
|
+
def get_connector_registry() -> ConnectorRegistry:
|
|
16
|
+
registry = create_builtin_connector_registry()
|
|
17
|
+
get_extension_manager().activate("connectors", registry)
|
|
18
|
+
return registry
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gaard-api
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.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
7
|
Requires-Dist: gaard-core==0.1.0
|
|
8
|
-
Requires-Dist: gaard-connectors==0.1.
|
|
8
|
+
Requires-Dist: gaard-connectors==0.1.1
|
|
9
9
|
Requires-Dist: gaard-llm==0.1.0
|
|
10
|
+
Requires-Dist: gaard-plugin-api<0.2.0,>=0.1.0
|
|
10
11
|
Requires-Dist: fastapi>=0.111.0
|
|
11
12
|
Requires-Dist: uvicorn[standard]>=0.30.0
|
|
12
13
|
Requires-Dist: pydantic>=2.7.0
|
|
@@ -5,6 +5,7 @@ src/gaard_api/__init__.py
|
|
|
5
5
|
src/gaard_api/cli.py
|
|
6
6
|
src/gaard_api/cli_commands.py
|
|
7
7
|
src/gaard_api/example_database.py
|
|
8
|
+
src/gaard_api/extensions.py
|
|
8
9
|
src/gaard_api/main.py
|
|
9
10
|
src/gaard_api/server_cli.py
|
|
10
11
|
src/gaard_api.egg-info/PKG-INFO
|
|
@@ -21,6 +22,7 @@ src/gaard_api/admin/prompt_runtime.py
|
|
|
21
22
|
src/gaard_api/admin/security.py
|
|
22
23
|
src/gaard_api/admin/services.py
|
|
23
24
|
src/gaard_api/admin-web/index.html
|
|
25
|
+
src/gaard_api/admin-web/package.json
|
|
24
26
|
src/gaard_api/admin-web/assets/main.js
|
|
25
27
|
src/gaard_api/admin-web/assets/styles.css
|
|
26
28
|
src/gaard_api/admin-web/src/main.ts
|
|
@@ -34,13 +36,10 @@ src/gaard_api/core/__init__.py
|
|
|
34
36
|
src/gaard_api/core/error_handlers.py
|
|
35
37
|
src/gaard_api/core/schema_cache.py
|
|
36
38
|
src/gaard_api/core/settings.py
|
|
37
|
-
src/gaard_api/dependencies/__init__.py
|
|
38
39
|
src/gaard_api/example_data/__init__.py
|
|
39
40
|
src/gaard_api/example_data/medical_poc/__init__.py
|
|
40
41
|
src/gaard_api/example_data/medical_poc/schema.sql
|
|
41
42
|
src/gaard_api/example_data/medical_poc/seed.sql
|
|
42
|
-
src/gaard_api/schemas/__init__.py
|
|
43
|
-
src/gaard_api/services/__init__.py
|
|
44
43
|
tests/test_admin_api.py
|
|
45
44
|
tests/test_error_handlers.py
|
|
46
45
|
tests/test_investigation_error_messages.py
|
|
@@ -91,6 +91,31 @@ def change_password(client: TestClient, token: str) -> None:
|
|
|
91
91
|
assert response.json()["must_change_password"] is False
|
|
92
92
|
|
|
93
93
|
|
|
94
|
+
def test_admin_lists_datasource_types_from_connector_registry(admin_client: TestClient) -> None:
|
|
95
|
+
token = login(admin_client)["token"]
|
|
96
|
+
change_password(admin_client, token)
|
|
97
|
+
|
|
98
|
+
response = admin_client.get(
|
|
99
|
+
"/api/v1/admin/datasource-types",
|
|
100
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
assert response.status_code == 200
|
|
104
|
+
definitions = {item["type_key"]: item for item in response.json()["items"]}
|
|
105
|
+
assert definitions["sqlite"]["default_sql_dialect"] == "sqlite"
|
|
106
|
+
assert definitions["postgresql"]["default_sql_dialect"] == "postgres"
|
|
107
|
+
assert definitions["mysql"]["config_schema"]["required"] == ["database_url"]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_admin_web_loads_connector_types_from_the_registry_api(admin_client: TestClient) -> None:
|
|
111
|
+
response = admin_client.get("/admin/assets/main.js")
|
|
112
|
+
|
|
113
|
+
assert response.status_code == 200
|
|
114
|
+
assert 'api("/api/v1/admin/datasource-types")' in response.text
|
|
115
|
+
assert "plugin unavailable" in response.text
|
|
116
|
+
assert "renderDatabaseTypeOptions" not in response.text
|
|
117
|
+
|
|
118
|
+
|
|
94
119
|
def test_system_seeded_mock_runtime_modes_are_migrated_to_current_defaults(
|
|
95
120
|
tmp_path: Path,
|
|
96
121
|
monkeypatch: pytest.MonkeyPatch,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|