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.
@@ -0,0 +1,5 @@
1
+ """Qodalis CLI Data Explorer Elasticsearch plugin — Elasticsearch provider."""
2
+
3
+ from .elasticsearch_provider import ElasticsearchDataExplorerProvider
4
+
5
+ __all__ = ["ElasticsearchDataExplorerProvider"]
@@ -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,,
@@ -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