qodalis-cli-server-plugin-data-explorer 1.0.0b13__py3-none-any.whl
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.
- qodalis_cli_data_explorer/__init__.py +11 -0
- qodalis_cli_data_explorer/data_explorer_controller.py +77 -0
- qodalis_cli_data_explorer/data_explorer_executor.py +89 -0
- qodalis_cli_data_explorer/data_explorer_registry.py +53 -0
- qodalis_cli_server_plugin_data_explorer-1.0.0b13.dist-info/METADATA +20 -0
- qodalis_cli_server_plugin_data_explorer-1.0.0b13.dist-info/RECORD +7 -0
- qodalis_cli_server_plugin_data_explorer-1.0.0b13.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Qodalis CLI Data Explorer plugin — registry, executor, and controller."""
|
|
2
|
+
|
|
3
|
+
from .data_explorer_registry import DataExplorerRegistry
|
|
4
|
+
from .data_explorer_executor import DataExplorerExecutor
|
|
5
|
+
from .data_explorer_controller import create_data_explorer_router
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"DataExplorerRegistry",
|
|
9
|
+
"DataExplorerExecutor",
|
|
10
|
+
"create_data_explorer_router",
|
|
11
|
+
]
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""FastAPI router for the Data Explorer API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import asdict
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from fastapi import APIRouter
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
|
|
11
|
+
from .data_explorer_executor import DataExplorerExecutor
|
|
12
|
+
from .data_explorer_registry import DataExplorerRegistry
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _to_camel(snake: str) -> str:
|
|
16
|
+
"""Convert a snake_case string to camelCase."""
|
|
17
|
+
parts = snake.split("_")
|
|
18
|
+
return parts[0] + "".join(w.capitalize() for w in parts[1:])
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _camel_keys(obj: Any, *, _depth: int = 0) -> Any:
|
|
22
|
+
"""Convert dict keys from snake_case to camelCase, preserving row data as-is."""
|
|
23
|
+
if isinstance(obj, dict):
|
|
24
|
+
result: dict[str, Any] = {}
|
|
25
|
+
for k, v in obj.items():
|
|
26
|
+
new_key = _to_camel(k) if _depth < 2 else k
|
|
27
|
+
# Don't recurse into "rows" — that's user data
|
|
28
|
+
if new_key == "rows":
|
|
29
|
+
result[new_key] = v
|
|
30
|
+
else:
|
|
31
|
+
result[new_key] = _camel_keys(v, _depth=_depth + 1)
|
|
32
|
+
return result
|
|
33
|
+
if isinstance(obj, list):
|
|
34
|
+
return [_camel_keys(item, _depth=_depth) for item in obj]
|
|
35
|
+
return obj
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class DataExplorerExecuteRequest(BaseModel):
|
|
39
|
+
"""Request body for executing a data explorer query."""
|
|
40
|
+
|
|
41
|
+
source: str
|
|
42
|
+
query: str
|
|
43
|
+
parameters: dict[str, Any] | None = None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def create_data_explorer_router(
|
|
47
|
+
registry: DataExplorerRegistry,
|
|
48
|
+
executor: DataExplorerExecutor,
|
|
49
|
+
) -> APIRouter:
|
|
50
|
+
"""Create and return a FastAPI router with data-explorer endpoints."""
|
|
51
|
+
router = APIRouter()
|
|
52
|
+
|
|
53
|
+
@router.get("/sources")
|
|
54
|
+
async def get_sources() -> list[dict[str, Any]]:
|
|
55
|
+
return _camel_keys(registry.get_sources())
|
|
56
|
+
|
|
57
|
+
@router.post("/execute")
|
|
58
|
+
async def execute(request: DataExplorerExecuteRequest) -> dict[str, Any]:
|
|
59
|
+
result = await executor.execute_async(request.model_dump())
|
|
60
|
+
return _camel_keys(asdict(result))
|
|
61
|
+
|
|
62
|
+
@router.get("/schema")
|
|
63
|
+
async def get_schema(source: str) -> dict[str, Any]:
|
|
64
|
+
entry = registry.get(source)
|
|
65
|
+
if entry is None:
|
|
66
|
+
from fastapi import HTTPException
|
|
67
|
+
raise HTTPException(status_code=404, detail=f"Unknown data source: '{source}'")
|
|
68
|
+
|
|
69
|
+
provider, options = entry
|
|
70
|
+
schema = await provider.get_schema_async(options)
|
|
71
|
+
if schema is None:
|
|
72
|
+
from fastapi import HTTPException
|
|
73
|
+
raise HTTPException(status_code=404, detail="Schema introspection is not supported by this data source.")
|
|
74
|
+
|
|
75
|
+
return _camel_keys(asdict(schema))
|
|
76
|
+
|
|
77
|
+
return router
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Executor that dispatches queries to the correct Data Explorer provider."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from qodalis_cli_server_abstractions import (
|
|
9
|
+
DataExplorerExecutionContext,
|
|
10
|
+
DataExplorerResult,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from .data_explorer_registry import DataExplorerRegistry
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DataExplorerExecutor:
|
|
17
|
+
"""Wraps provider execution with timeout enforcement and row truncation."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, registry: DataExplorerRegistry) -> None:
|
|
20
|
+
self._registry = registry
|
|
21
|
+
|
|
22
|
+
async def execute_async(self, request: dict[str, Any]) -> DataExplorerResult:
|
|
23
|
+
"""Execute a query against the appropriate provider with timeout enforcement."""
|
|
24
|
+
source = request.get("source", "")
|
|
25
|
+
query = request.get("query", "")
|
|
26
|
+
parameters = request.get("parameters") or {}
|
|
27
|
+
|
|
28
|
+
entry = self._registry.get(source)
|
|
29
|
+
if entry is None:
|
|
30
|
+
return DataExplorerResult(
|
|
31
|
+
success=False,
|
|
32
|
+
source=source,
|
|
33
|
+
language="sql", # type: ignore[arg-type]
|
|
34
|
+
default_output_format="table", # type: ignore[arg-type]
|
|
35
|
+
execution_time=0,
|
|
36
|
+
columns=None,
|
|
37
|
+
rows=[],
|
|
38
|
+
row_count=0,
|
|
39
|
+
truncated=False,
|
|
40
|
+
error=f"Unknown data source: {source}",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
provider, options = entry
|
|
44
|
+
context = DataExplorerExecutionContext(
|
|
45
|
+
query=query,
|
|
46
|
+
parameters=parameters,
|
|
47
|
+
options=options,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
timeout_s = options.timeout / 1000.0
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
result = await asyncio.wait_for(
|
|
54
|
+
provider.execute_async(context),
|
|
55
|
+
timeout=timeout_s,
|
|
56
|
+
)
|
|
57
|
+
except asyncio.TimeoutError:
|
|
58
|
+
return DataExplorerResult(
|
|
59
|
+
success=False,
|
|
60
|
+
source=source,
|
|
61
|
+
language=options.language,
|
|
62
|
+
default_output_format=options.default_output_format,
|
|
63
|
+
execution_time=options.timeout,
|
|
64
|
+
columns=None,
|
|
65
|
+
rows=[],
|
|
66
|
+
row_count=0,
|
|
67
|
+
truncated=False,
|
|
68
|
+
error=f"Query timed out after {options.timeout}ms",
|
|
69
|
+
)
|
|
70
|
+
except Exception as exc:
|
|
71
|
+
return DataExplorerResult(
|
|
72
|
+
success=False,
|
|
73
|
+
source=source,
|
|
74
|
+
language=options.language,
|
|
75
|
+
default_output_format=options.default_output_format,
|
|
76
|
+
execution_time=0,
|
|
77
|
+
columns=None,
|
|
78
|
+
rows=[],
|
|
79
|
+
row_count=0,
|
|
80
|
+
truncated=False,
|
|
81
|
+
error=str(exc),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
if result.row_count > options.max_rows:
|
|
85
|
+
result.rows = result.rows[: options.max_rows]
|
|
86
|
+
result.row_count = options.max_rows
|
|
87
|
+
result.truncated = True
|
|
88
|
+
|
|
89
|
+
return result
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Registry for Data Explorer providers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import asdict
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from qodalis_cli_server_abstractions import (
|
|
9
|
+
DataExplorerProviderOptions,
|
|
10
|
+
IDataExplorerProvider,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class _RegisteredProvider:
|
|
15
|
+
"""Internal wrapper that pairs a provider with its options."""
|
|
16
|
+
|
|
17
|
+
__slots__ = ("provider", "options")
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
provider: IDataExplorerProvider,
|
|
22
|
+
options: DataExplorerProviderOptions,
|
|
23
|
+
) -> None:
|
|
24
|
+
self.provider = provider
|
|
25
|
+
self.options = options
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class DataExplorerRegistry:
|
|
29
|
+
"""Stores :class:`IDataExplorerProvider` instances keyed by source name."""
|
|
30
|
+
|
|
31
|
+
def __init__(self) -> None:
|
|
32
|
+
self._providers: dict[str, _RegisteredProvider] = {}
|
|
33
|
+
|
|
34
|
+
def register(
|
|
35
|
+
self,
|
|
36
|
+
provider: IDataExplorerProvider,
|
|
37
|
+
options: DataExplorerProviderOptions,
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Register a provider under the source name defined in *options*."""
|
|
40
|
+
self._providers[options.name] = _RegisteredProvider(provider, options)
|
|
41
|
+
|
|
42
|
+
def get(self, source: str) -> tuple[IDataExplorerProvider, DataExplorerProviderOptions] | None:
|
|
43
|
+
"""Return the provider and its options for *source*, or ``None``."""
|
|
44
|
+
entry = self._providers.get(source)
|
|
45
|
+
if entry is None:
|
|
46
|
+
return None
|
|
47
|
+
return entry.provider, entry.options
|
|
48
|
+
|
|
49
|
+
def get_sources(self) -> list[dict[str, Any]]:
|
|
50
|
+
"""Return a JSON-serialisable list of all registered sources."""
|
|
51
|
+
return [
|
|
52
|
+
asdict(entry.options) for entry in self._providers.values()
|
|
53
|
+
]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: qodalis-cli-server-plugin-data-explorer
|
|
3
|
+
Version: 1.0.0b13
|
|
4
|
+
Summary: Data Explorer plugin for Qodalis CLI servers
|
|
5
|
+
Project-URL: Homepage, https://github.com/qodalis-solutions/cli-server-python
|
|
6
|
+
Project-URL: Repository, https://github.com/qodalis-solutions/cli-server-python
|
|
7
|
+
Project-URL: Issues, https://github.com/qodalis-solutions/cli-server-python/issues
|
|
8
|
+
Author-email: Nicolae Lupei <nicolae.lupei@qodalis.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Keywords: cli,data-explorer,plugin,qodalis
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: qodalis-cli-server-abstractions
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
qodalis_cli_data_explorer/__init__.py,sha256=QTGZKvJIxZtmbGWXO5v6FmW_v4l46kV9qVLDEI5xUYg,366
|
|
2
|
+
qodalis_cli_data_explorer/data_explorer_controller.py,sha256=-2cncoZWRFivODr2iGhdu11AgPMPp_QyUkqpqxR1VUQ,2536
|
|
3
|
+
qodalis_cli_data_explorer/data_explorer_executor.py,sha256=onzLBttxz1XXOFJxEu8X7FfpKB5AZg1SRgA6lD2mx7I,2898
|
|
4
|
+
qodalis_cli_data_explorer/data_explorer_registry.py,sha256=a8tvUtMv7q9ndYuG9xYUM6ZNHnFTfXCOtSlfucFuzVg,1617
|
|
5
|
+
qodalis_cli_server_plugin_data_explorer-1.0.0b13.dist-info/METADATA,sha256=qJshDto_0rtecR8hvs4us6IscBDY-qdWtBzBkdnz-58,965
|
|
6
|
+
qodalis_cli_server_plugin_data_explorer-1.0.0b13.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
7
|
+
qodalis_cli_server_plugin_data_explorer-1.0.0b13.dist-info/RECORD,,
|