qodalis-cli-server-plugin-data-explorer-elasticsearch 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_elasticsearch/__init__.py +5 -0
- qodalis_cli_data_explorer_elasticsearch/elasticsearch_provider.py +227 -0
- qodalis_cli_server_plugin_data_explorer_elasticsearch-1.0.0b13.dist-info/METADATA +21 -0
- qodalis_cli_server_plugin_data_explorer_elasticsearch-1.0.0b13.dist-info/RECORD +5 -0
- qodalis_cli_server_plugin_data_explorer_elasticsearch-1.0.0b13.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""Elasticsearch-backed Data Explorer provider."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import time
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from elasticsearch import AsyncElasticsearch
|
|
10
|
+
|
|
11
|
+
from qodalis_cli_server_abstractions import (
|
|
12
|
+
DataExplorerExecutionContext,
|
|
13
|
+
DataExplorerProviderOptions,
|
|
14
|
+
DataExplorerResult,
|
|
15
|
+
DataExplorerSchemaColumn,
|
|
16
|
+
DataExplorerSchemaResult,
|
|
17
|
+
DataExplorerSchemaTable,
|
|
18
|
+
IDataExplorerProvider,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ElasticsearchDataExplorerProvider(IDataExplorerProvider):
|
|
23
|
+
"""Executes requests against an Elasticsearch cluster."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, node: str = "http://localhost:9200") -> None:
|
|
26
|
+
self._node = node
|
|
27
|
+
|
|
28
|
+
async def execute_async(
|
|
29
|
+
self, context: DataExplorerExecutionContext
|
|
30
|
+
) -> DataExplorerResult:
|
|
31
|
+
"""Execute an HTTP request against the Elasticsearch cluster."""
|
|
32
|
+
start = time.monotonic()
|
|
33
|
+
es: AsyncElasticsearch | None = None
|
|
34
|
+
try:
|
|
35
|
+
es = AsyncElasticsearch(self._node)
|
|
36
|
+
|
|
37
|
+
query = (context.query or "").strip()
|
|
38
|
+
lines = query.splitlines()
|
|
39
|
+
|
|
40
|
+
first_line = lines[0].strip() if lines else ""
|
|
41
|
+
parts = first_line.split(None, 1)
|
|
42
|
+
|
|
43
|
+
if len(parts) == 2 and parts[0].upper() in {
|
|
44
|
+
"GET", "POST", "PUT", "DELETE", "HEAD", "PATCH",
|
|
45
|
+
}:
|
|
46
|
+
method = parts[0].upper()
|
|
47
|
+
path = parts[1].strip()
|
|
48
|
+
else:
|
|
49
|
+
method = "GET"
|
|
50
|
+
path = first_line
|
|
51
|
+
|
|
52
|
+
body_lines = lines[1:]
|
|
53
|
+
body_text = "\n".join(body_lines).strip()
|
|
54
|
+
body: Any = None
|
|
55
|
+
if body_text:
|
|
56
|
+
body = json.loads(body_text)
|
|
57
|
+
|
|
58
|
+
if "/_cat/" in path or path.startswith("_cat/"):
|
|
59
|
+
if "format=json" not in path:
|
|
60
|
+
sep = "&" if "?" in path else "?"
|
|
61
|
+
path = f"{path}{sep}format=json"
|
|
62
|
+
|
|
63
|
+
response = await es.perform_request(
|
|
64
|
+
method=method,
|
|
65
|
+
path=path,
|
|
66
|
+
body=body,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
raw = response.body if hasattr(response, "body") else response
|
|
70
|
+
|
|
71
|
+
if isinstance(raw, dict) and "hits" in raw and isinstance(raw["hits"], dict):
|
|
72
|
+
hits = raw["hits"].get("hits", [])
|
|
73
|
+
if hits:
|
|
74
|
+
# Collect all field keys from _source (or the hit itself)
|
|
75
|
+
all_keys: list[str] = []
|
|
76
|
+
for hit in hits:
|
|
77
|
+
src = hit.get("_source", hit)
|
|
78
|
+
for k in src.keys():
|
|
79
|
+
if k not in all_keys:
|
|
80
|
+
all_keys.append(k)
|
|
81
|
+
columns = all_keys
|
|
82
|
+
rows = [
|
|
83
|
+
[hit.get("_source", hit).get(col) for col in columns]
|
|
84
|
+
for hit in hits
|
|
85
|
+
]
|
|
86
|
+
return DataExplorerResult(
|
|
87
|
+
success=True,
|
|
88
|
+
source=context.options.name,
|
|
89
|
+
language=context.options.language,
|
|
90
|
+
default_output_format=context.options.default_output_format,
|
|
91
|
+
execution_time=int((time.monotonic() - start) * 1000),
|
|
92
|
+
columns=columns,
|
|
93
|
+
rows=rows,
|
|
94
|
+
row_count=len(rows),
|
|
95
|
+
truncated=False,
|
|
96
|
+
error=None,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
if isinstance(raw, list):
|
|
100
|
+
if raw:
|
|
101
|
+
columns = list(raw[0].keys()) if isinstance(raw[0], dict) else ["value"]
|
|
102
|
+
rows = (
|
|
103
|
+
[[item.get(col) for col in columns] for item in raw]
|
|
104
|
+
if isinstance(raw[0], dict)
|
|
105
|
+
else [[item] for item in raw]
|
|
106
|
+
)
|
|
107
|
+
else:
|
|
108
|
+
columns = []
|
|
109
|
+
rows = []
|
|
110
|
+
return DataExplorerResult(
|
|
111
|
+
success=True,
|
|
112
|
+
source=context.options.name,
|
|
113
|
+
language=context.options.language,
|
|
114
|
+
default_output_format=context.options.default_output_format,
|
|
115
|
+
execution_time=int((time.monotonic() - start) * 1000),
|
|
116
|
+
columns=columns,
|
|
117
|
+
rows=rows,
|
|
118
|
+
row_count=len(rows),
|
|
119
|
+
truncated=False,
|
|
120
|
+
error=None,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
raw_json = json.dumps(raw, default=str)
|
|
124
|
+
return DataExplorerResult(
|
|
125
|
+
success=True,
|
|
126
|
+
source=context.options.name,
|
|
127
|
+
language=context.options.language,
|
|
128
|
+
default_output_format=context.options.default_output_format,
|
|
129
|
+
execution_time=int((time.monotonic() - start) * 1000),
|
|
130
|
+
columns=["result"],
|
|
131
|
+
rows=[[raw_json]],
|
|
132
|
+
row_count=1,
|
|
133
|
+
truncated=False,
|
|
134
|
+
error=None,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
except Exception as exc:
|
|
138
|
+
return DataExplorerResult(
|
|
139
|
+
success=False,
|
|
140
|
+
source=context.options.name,
|
|
141
|
+
language=context.options.language,
|
|
142
|
+
default_output_format=context.options.default_output_format,
|
|
143
|
+
execution_time=int((time.monotonic() - start) * 1000),
|
|
144
|
+
columns=None,
|
|
145
|
+
rows=[],
|
|
146
|
+
row_count=0,
|
|
147
|
+
truncated=False,
|
|
148
|
+
error=str(exc),
|
|
149
|
+
)
|
|
150
|
+
finally:
|
|
151
|
+
if es is not None:
|
|
152
|
+
await es.close()
|
|
153
|
+
|
|
154
|
+
async def get_schema_async(
|
|
155
|
+
self, options: DataExplorerProviderOptions
|
|
156
|
+
) -> DataExplorerSchemaResult | None:
|
|
157
|
+
"""Return index names and their field mappings."""
|
|
158
|
+
es: AsyncElasticsearch | None = None
|
|
159
|
+
try:
|
|
160
|
+
es = AsyncElasticsearch(self._node)
|
|
161
|
+
|
|
162
|
+
indices_response = await es.perform_request(
|
|
163
|
+
method="GET",
|
|
164
|
+
path="/_cat/indices?format=json",
|
|
165
|
+
)
|
|
166
|
+
indices_raw = (
|
|
167
|
+
indices_response.body
|
|
168
|
+
if hasattr(indices_response, "body")
|
|
169
|
+
else indices_response
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
tables: list[DataExplorerSchemaTable] = []
|
|
173
|
+
|
|
174
|
+
if isinstance(indices_raw, list):
|
|
175
|
+
for index_info in indices_raw:
|
|
176
|
+
index_name: str = index_info.get("index", "")
|
|
177
|
+
if not index_name or index_name.startswith("."):
|
|
178
|
+
# Skip system indices
|
|
179
|
+
continue
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
mapping_response = await es.perform_request(
|
|
183
|
+
method="GET",
|
|
184
|
+
path=f"/{index_name}/_mapping",
|
|
185
|
+
)
|
|
186
|
+
mapping_raw = (
|
|
187
|
+
mapping_response.body
|
|
188
|
+
if hasattr(mapping_response, "body")
|
|
189
|
+
else mapping_response
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
columns: list[DataExplorerSchemaColumn] = []
|
|
193
|
+
if isinstance(mapping_raw, dict):
|
|
194
|
+
index_mapping = mapping_raw.get(index_name, {})
|
|
195
|
+
mappings = index_mapping.get("mappings", {})
|
|
196
|
+
properties = mappings.get("properties", {})
|
|
197
|
+
for field_name, field_def in properties.items():
|
|
198
|
+
columns.append(
|
|
199
|
+
DataExplorerSchemaColumn(
|
|
200
|
+
name=field_name,
|
|
201
|
+
type=field_def.get("type", "object"),
|
|
202
|
+
nullable=True,
|
|
203
|
+
primary_key=False,
|
|
204
|
+
)
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
tables.append(
|
|
208
|
+
DataExplorerSchemaTable(
|
|
209
|
+
name=index_name,
|
|
210
|
+
type="index",
|
|
211
|
+
columns=columns,
|
|
212
|
+
)
|
|
213
|
+
)
|
|
214
|
+
except Exception:
|
|
215
|
+
# Skip indices we cannot fetch mappings for
|
|
216
|
+
tables.append(
|
|
217
|
+
DataExplorerSchemaTable(
|
|
218
|
+
name=index_name,
|
|
219
|
+
type="index",
|
|
220
|
+
columns=[],
|
|
221
|
+
)
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
return DataExplorerSchemaResult(source=options.name, tables=tables)
|
|
225
|
+
finally:
|
|
226
|
+
if es is not None:
|
|
227
|
+
await es.close()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: qodalis-cli-server-plugin-data-explorer-elasticsearch
|
|
3
|
+
Version: 1.0.0b13
|
|
4
|
+
Summary: Elasticsearch Data Explorer provider 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,elasticsearch,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: elasticsearch[async]>=8.16.0
|
|
21
|
+
Requires-Dist: qodalis-cli-server-abstractions
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
qodalis_cli_data_explorer_elasticsearch/__init__.py,sha256=9iY3XtgXtWWbTJ5rcIFsbstn0mcfHkepFfqBiHutqeY,201
|
|
2
|
+
qodalis_cli_data_explorer_elasticsearch/elasticsearch_provider.py,sha256=xKK7qsBnUnads_Ji7xNLZX8cDmQ_pIKmnLoBdh2wwUo,8707
|
|
3
|
+
qodalis_cli_server_plugin_data_explorer_elasticsearch-1.0.0b13.dist-info/METADATA,sha256=M5a3EMJHfced_YxYPFJzJct1HB4eA9AKqbHAy71irDw,1060
|
|
4
|
+
qodalis_cli_server_plugin_data_explorer_elasticsearch-1.0.0b13.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
5
|
+
qodalis_cli_server_plugin_data_explorer_elasticsearch-1.0.0b13.dist-info/RECORD,,
|