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.
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any