spice-mcp 0.1.0__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.
Potentially problematic release.
This version of spice-mcp might be problematic. Click here for more details.
- spice_mcp/__init__.py +2 -0
- spice_mcp/adapters/__init__.py +0 -0
- spice_mcp/adapters/dune/__init__.py +10 -0
- spice_mcp/adapters/dune/admin.py +94 -0
- spice_mcp/adapters/dune/cache.py +185 -0
- spice_mcp/adapters/dune/client.py +255 -0
- spice_mcp/adapters/dune/extract.py +1461 -0
- spice_mcp/adapters/dune/helpers.py +11 -0
- spice_mcp/adapters/dune/transport.py +70 -0
- spice_mcp/adapters/dune/types.py +52 -0
- spice_mcp/adapters/dune/typing_utils.py +10 -0
- spice_mcp/adapters/dune/urls.py +126 -0
- spice_mcp/adapters/http_client.py +156 -0
- spice_mcp/config.py +81 -0
- spice_mcp/core/__init__.py +0 -0
- spice_mcp/core/errors.py +101 -0
- spice_mcp/core/models.py +88 -0
- spice_mcp/core/ports.py +69 -0
- spice_mcp/logging/query_history.py +131 -0
- spice_mcp/mcp/__init__.py +1 -0
- spice_mcp/mcp/server.py +546 -0
- spice_mcp/mcp/tools/__init__.py +1 -0
- spice_mcp/mcp/tools/base.py +41 -0
- spice_mcp/mcp/tools/execute_query.py +425 -0
- spice_mcp/mcp/tools/sui_package_overview.py +56 -0
- spice_mcp/observability/__init__.py +0 -0
- spice_mcp/observability/logging.py +18 -0
- spice_mcp/polars_utils.py +15 -0
- spice_mcp/py.typed +1 -0
- spice_mcp/service_layer/__init__.py +0 -0
- spice_mcp/service_layer/discovery_service.py +20 -0
- spice_mcp/service_layer/query_admin_service.py +26 -0
- spice_mcp/service_layer/query_service.py +118 -0
- spice_mcp/service_layer/sui_service.py +131 -0
- spice_mcp-0.1.0.dist-info/METADATA +133 -0
- spice_mcp-0.1.0.dist-info/RECORD +39 -0
- spice_mcp-0.1.0.dist-info/WHEEL +4 -0
- spice_mcp-0.1.0.dist-info/entry_points.txt +2 -0
- spice_mcp-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,1461 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
import time
|
|
5
|
+
from typing import TYPE_CHECKING, overload
|
|
6
|
+
|
|
7
|
+
from ..http_client import HttpClient
|
|
8
|
+
from . import cache as _cache
|
|
9
|
+
from . import urls as _urls
|
|
10
|
+
from .transport import (
|
|
11
|
+
current_http_client,
|
|
12
|
+
use_http_client,
|
|
13
|
+
)
|
|
14
|
+
from .transport import (
|
|
15
|
+
get as _transport_get,
|
|
16
|
+
)
|
|
17
|
+
from .transport import (
|
|
18
|
+
post as _transport_post,
|
|
19
|
+
)
|
|
20
|
+
from .types import (
|
|
21
|
+
ExecuteKwargs,
|
|
22
|
+
Execution,
|
|
23
|
+
OutputKwargs,
|
|
24
|
+
Performance,
|
|
25
|
+
PollKwargs,
|
|
26
|
+
Query,
|
|
27
|
+
RetrievalKwargs,
|
|
28
|
+
)
|
|
29
|
+
from .typing_utils import resolve_raw_sql_template_id
|
|
30
|
+
|
|
31
|
+
# Keep local helper implementations for compatibility with tests
|
|
32
|
+
|
|
33
|
+
ADAPTER_VERSION = "spice-mcp-adapter"
|
|
34
|
+
|
|
35
|
+
if TYPE_CHECKING:
|
|
36
|
+
from collections.abc import Mapping, Sequence
|
|
37
|
+
from typing import Any, Literal
|
|
38
|
+
|
|
39
|
+
import polars as pl
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
# Back-compat helpers expected by tests and adapter code
|
|
44
|
+
|
|
45
|
+
def _is_sql(query: int | str) -> bool:
|
|
46
|
+
if isinstance(query, int):
|
|
47
|
+
return False
|
|
48
|
+
if isinstance(query, str):
|
|
49
|
+
if query.startswith('https://') or query.startswith('api.dune.com') or query.startswith('dune.com/queries'):
|
|
50
|
+
return False
|
|
51
|
+
try:
|
|
52
|
+
int(query)
|
|
53
|
+
return False
|
|
54
|
+
except ValueError:
|
|
55
|
+
return True
|
|
56
|
+
raise Exception('invalid format for query: ' + str(type(query)))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _determine_input_type(
|
|
60
|
+
query_or_execution: Query | Execution,
|
|
61
|
+
parameters: Mapping[str, Any] | None = None,
|
|
62
|
+
) -> tuple[int | None, Execution | None, Mapping[str, Any] | None]:
|
|
63
|
+
if isinstance(query_or_execution, str) and query_or_execution == '':
|
|
64
|
+
raise Exception('empty query')
|
|
65
|
+
if isinstance(query_or_execution, int | str):
|
|
66
|
+
if _is_sql(query_or_execution):
|
|
67
|
+
query_id = resolve_raw_sql_template_id()
|
|
68
|
+
execution = None
|
|
69
|
+
new_params = dict(parameters or {})
|
|
70
|
+
new_params.update({'query': query_or_execution})
|
|
71
|
+
parameters = new_params
|
|
72
|
+
else:
|
|
73
|
+
query_id = _urls.get_query_id(query_or_execution)
|
|
74
|
+
execution = None
|
|
75
|
+
elif isinstance(query_or_execution, dict) and 'execution_id' in query_or_execution:
|
|
76
|
+
query_id = None
|
|
77
|
+
execution = query_or_execution # type: ignore[assignment]
|
|
78
|
+
else:
|
|
79
|
+
raise Exception('input must be a query id, query url, or execution id')
|
|
80
|
+
return query_id, execution, parameters
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _http_get(url: str, *, headers: Mapping[str, str], timeout: float):
|
|
84
|
+
import requests
|
|
85
|
+
|
|
86
|
+
return requests.get(url, headers=headers, timeout=timeout)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@overload
|
|
90
|
+
def query(
|
|
91
|
+
query_or_execution: Query | Execution,
|
|
92
|
+
*,
|
|
93
|
+
verbose: bool = True,
|
|
94
|
+
refresh: bool = False,
|
|
95
|
+
max_age: float | None = None,
|
|
96
|
+
parameters: Mapping[str, Any] | None = None,
|
|
97
|
+
api_key: str | None = None,
|
|
98
|
+
performance: Performance = 'medium',
|
|
99
|
+
poll: Literal[False],
|
|
100
|
+
poll_interval: float = 1.0,
|
|
101
|
+
timeout_seconds: float | None = None,
|
|
102
|
+
limit: int | None = None,
|
|
103
|
+
offset: int | None = None,
|
|
104
|
+
sample_count: int | None = None,
|
|
105
|
+
sort_by: str | None = None,
|
|
106
|
+
columns: Sequence[str] | None = None,
|
|
107
|
+
extras: Mapping[str, Any] | None = None,
|
|
108
|
+
types: Sequence[type[pl.DataType]]
|
|
109
|
+
| Mapping[str, type[pl.DataType]]
|
|
110
|
+
| None = None,
|
|
111
|
+
all_types: Sequence[type[pl.DataType]]
|
|
112
|
+
| Mapping[str, type[pl.DataType]]
|
|
113
|
+
| None = None,
|
|
114
|
+
cache: bool = True,
|
|
115
|
+
cache_dir: str | None = None,
|
|
116
|
+
save_to_cache: bool = True,
|
|
117
|
+
load_from_cache: bool = True,
|
|
118
|
+
include_execution: bool = False,
|
|
119
|
+
) -> Execution: ...
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@overload
|
|
123
|
+
def query(
|
|
124
|
+
query_or_execution: Query | Execution,
|
|
125
|
+
*,
|
|
126
|
+
verbose: bool = True,
|
|
127
|
+
refresh: bool = False,
|
|
128
|
+
max_age: float | None = None,
|
|
129
|
+
parameters: Mapping[str, Any] | None = None,
|
|
130
|
+
api_key: str | None = None,
|
|
131
|
+
performance: Performance = 'medium',
|
|
132
|
+
poll: Literal[True] = True,
|
|
133
|
+
poll_interval: float = 1.0,
|
|
134
|
+
limit: int | None = None,
|
|
135
|
+
offset: int | None = None,
|
|
136
|
+
sample_count: int | None = None,
|
|
137
|
+
sort_by: str | None = None,
|
|
138
|
+
columns: Sequence[str] | None = None,
|
|
139
|
+
extras: Mapping[str, Any] | None = None,
|
|
140
|
+
types: Sequence[type[pl.DataType]]
|
|
141
|
+
| Mapping[str, type[pl.DataType]]
|
|
142
|
+
| None = None,
|
|
143
|
+
all_types: Sequence[type[pl.DataType]]
|
|
144
|
+
| Mapping[str, type[pl.DataType]]
|
|
145
|
+
| None = None,
|
|
146
|
+
cache: bool = True,
|
|
147
|
+
cache_dir: str | None = None,
|
|
148
|
+
save_to_cache: bool = True,
|
|
149
|
+
load_from_cache: bool = True,
|
|
150
|
+
include_execution: Literal[False] = False,
|
|
151
|
+
) -> pl.DataFrame: ...
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@overload
|
|
155
|
+
def query(
|
|
156
|
+
query_or_execution: Query | Execution,
|
|
157
|
+
*,
|
|
158
|
+
verbose: bool = True,
|
|
159
|
+
refresh: bool = False,
|
|
160
|
+
max_age: float | None = None,
|
|
161
|
+
parameters: Mapping[str, Any] | None = None,
|
|
162
|
+
api_key: str | None = None,
|
|
163
|
+
performance: Performance = 'medium',
|
|
164
|
+
poll: Literal[True] = True,
|
|
165
|
+
poll_interval: float = 1.0,
|
|
166
|
+
limit: int | None = None,
|
|
167
|
+
offset: int | None = None,
|
|
168
|
+
sample_count: int | None = None,
|
|
169
|
+
sort_by: str | None = None,
|
|
170
|
+
columns: Sequence[str] | None = None,
|
|
171
|
+
extras: Mapping[str, Any] | None = None,
|
|
172
|
+
types: Sequence[type[pl.DataType]]
|
|
173
|
+
| Mapping[str, type[pl.DataType]]
|
|
174
|
+
| None = None,
|
|
175
|
+
all_types: Sequence[type[pl.DataType]]
|
|
176
|
+
| Mapping[str, type[pl.DataType]]
|
|
177
|
+
| None = None,
|
|
178
|
+
cache: bool = True,
|
|
179
|
+
cache_dir: str | None = None,
|
|
180
|
+
save_to_cache: bool = True,
|
|
181
|
+
load_from_cache: bool = True,
|
|
182
|
+
include_execution: Literal[True],
|
|
183
|
+
) -> tuple[pl.DataFrame, Execution]: ...
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def query(
|
|
187
|
+
query_or_execution: Query | Execution,
|
|
188
|
+
*,
|
|
189
|
+
verbose: bool = True,
|
|
190
|
+
refresh: bool = False,
|
|
191
|
+
max_age: float | None = None,
|
|
192
|
+
parameters: Mapping[str, Any] | None = None,
|
|
193
|
+
api_key: str | None = None,
|
|
194
|
+
performance: Performance = 'medium',
|
|
195
|
+
poll: bool = True,
|
|
196
|
+
poll_interval: float = 1.0,
|
|
197
|
+
timeout_seconds: float | None = None,
|
|
198
|
+
limit: int | None = None,
|
|
199
|
+
offset: int | None = None,
|
|
200
|
+
sample_count: int | None = None,
|
|
201
|
+
sort_by: str | None = None,
|
|
202
|
+
columns: Sequence[str] | None = None,
|
|
203
|
+
extras: Mapping[str, Any] | None = None,
|
|
204
|
+
types: Sequence[type[pl.DataType]]
|
|
205
|
+
| Mapping[str, type[pl.DataType]]
|
|
206
|
+
| None = None,
|
|
207
|
+
all_types: Sequence[type[pl.DataType]]
|
|
208
|
+
| Mapping[str, type[pl.DataType]]
|
|
209
|
+
| None = None,
|
|
210
|
+
cache: bool = True,
|
|
211
|
+
cache_dir: str | None = None,
|
|
212
|
+
save_to_cache: bool = True,
|
|
213
|
+
load_from_cache: bool = True,
|
|
214
|
+
include_execution: bool = False,
|
|
215
|
+
http_client: HttpClient | None = None,
|
|
216
|
+
) -> pl.DataFrame | Execution | tuple[pl.DataFrame, Execution]:
|
|
217
|
+
"""get results of query as dataframe
|
|
218
|
+
|
|
219
|
+
# Parameters
|
|
220
|
+
- query: query or execution to retrieve results of
|
|
221
|
+
- verbose: whether to print verbose info
|
|
222
|
+
- refresh: trigger a new execution instead of using most recent execution
|
|
223
|
+
- max_age: max age of last execution in seconds, or trigger a new execution
|
|
224
|
+
- parameters: dict of query parameters
|
|
225
|
+
- api_key: dune api key, otherwise use DUNE_API_KEY env var
|
|
226
|
+
- performance: performance level
|
|
227
|
+
- poll: wait for result as DataFrame, or just return Execution handle
|
|
228
|
+
- poll_interval: polling interval in seconds
|
|
229
|
+
- limit: number of rows to query in result
|
|
230
|
+
- offset: row number to start returning results from
|
|
231
|
+
- sample_count: number of random samples from query to return
|
|
232
|
+
- sort_by: an ORDER BY clause to sort data by
|
|
233
|
+
- columns: columns to retrieve, by default retrieve all columns
|
|
234
|
+
- extras: extra parameters used for fetching execution result
|
|
235
|
+
- examples: ignore_max_datapoints_per_request, allow_partial_results
|
|
236
|
+
- types: column types to use in output polars dataframe
|
|
237
|
+
- all_types: like types, but must strictly include all columns in data
|
|
238
|
+
- cache: whether to use cache for saving or loading
|
|
239
|
+
- cache_dir: directory to use for cached data (create tmp_dir if None)
|
|
240
|
+
- save_to_cache: whether to save to cache, set false to load only
|
|
241
|
+
- load_from_cache: whether to load from cache, set false to save only
|
|
242
|
+
- include_execution: return Execution metadata alongside query result
|
|
243
|
+
"""
|
|
244
|
+
|
|
245
|
+
with use_http_client(http_client):
|
|
246
|
+
# determine whether target is a query or an execution
|
|
247
|
+
query_id, execution, parameters = _determine_input_type(
|
|
248
|
+
query_or_execution,
|
|
249
|
+
parameters,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
# gather arguments
|
|
253
|
+
execute_kwargs: ExecuteKwargs = {
|
|
254
|
+
'query_id': query_id,
|
|
255
|
+
'api_key': api_key,
|
|
256
|
+
'parameters': parameters,
|
|
257
|
+
'performance': performance,
|
|
258
|
+
}
|
|
259
|
+
poll_kwargs: PollKwargs = {
|
|
260
|
+
'poll_interval': poll_interval,
|
|
261
|
+
'api_key': api_key,
|
|
262
|
+
'verbose': verbose,
|
|
263
|
+
'timeout_seconds': timeout_seconds,
|
|
264
|
+
}
|
|
265
|
+
result_kwargs: RetrievalKwargs = {
|
|
266
|
+
'limit': limit,
|
|
267
|
+
'offset': offset,
|
|
268
|
+
'sample_count': sample_count,
|
|
269
|
+
'sort_by': sort_by,
|
|
270
|
+
'columns': columns,
|
|
271
|
+
'extras': extras,
|
|
272
|
+
'types': types,
|
|
273
|
+
'all_types': all_types,
|
|
274
|
+
'verbose': verbose,
|
|
275
|
+
}
|
|
276
|
+
output_kwargs: OutputKwargs = {
|
|
277
|
+
'execute_kwargs': execute_kwargs,
|
|
278
|
+
'result_kwargs': result_kwargs,
|
|
279
|
+
'cache': cache,
|
|
280
|
+
'save_to_cache': save_to_cache,
|
|
281
|
+
'cache_dir': cache_dir,
|
|
282
|
+
'include_execution': include_execution,
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
# execute or retrieve query
|
|
286
|
+
if query_id:
|
|
287
|
+
if cache and load_from_cache and not refresh:
|
|
288
|
+
cache_result, cache_execution = _cache.load_from_cache(
|
|
289
|
+
execute_kwargs, result_kwargs, output_kwargs
|
|
290
|
+
)
|
|
291
|
+
if cache_result is not None:
|
|
292
|
+
return cache_result
|
|
293
|
+
if execution is None and cache_execution is not None:
|
|
294
|
+
execution = cache_execution
|
|
295
|
+
if max_age is not None and not refresh:
|
|
296
|
+
age = get_query_latest_age(**execute_kwargs, verbose=verbose) # type: ignore
|
|
297
|
+
if age is None or age > max_age:
|
|
298
|
+
refresh = True
|
|
299
|
+
if not refresh:
|
|
300
|
+
df = get_results(**execute_kwargs, **result_kwargs)
|
|
301
|
+
if df is not None:
|
|
302
|
+
return process_result(df, execution, **output_kwargs)
|
|
303
|
+
execution = execute_query(**execute_kwargs, verbose=verbose)
|
|
304
|
+
|
|
305
|
+
# await execution completion
|
|
306
|
+
if execution is None:
|
|
307
|
+
raise Exception('could not determine execution')
|
|
308
|
+
if poll:
|
|
309
|
+
poll_execution(execution, **poll_kwargs)
|
|
310
|
+
df = get_results(execution, api_key, **result_kwargs)
|
|
311
|
+
if df is not None:
|
|
312
|
+
return process_result(df, execution, **output_kwargs)
|
|
313
|
+
else:
|
|
314
|
+
raise Exception('no successful execution for query')
|
|
315
|
+
else:
|
|
316
|
+
return execution
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
@overload
|
|
320
|
+
async def async_query(
|
|
321
|
+
query_or_execution: Query | Execution,
|
|
322
|
+
*,
|
|
323
|
+
verbose: bool = True,
|
|
324
|
+
refresh: bool = False,
|
|
325
|
+
max_age: float | None = None,
|
|
326
|
+
parameters: Mapping[str, Any] | None = None,
|
|
327
|
+
api_key: str | None = None,
|
|
328
|
+
performance: Performance = 'medium',
|
|
329
|
+
poll: Literal[False],
|
|
330
|
+
poll_interval: float = 1.0,
|
|
331
|
+
timeout_seconds: float | None = None,
|
|
332
|
+
limit: int | None = None,
|
|
333
|
+
offset: int | None = None,
|
|
334
|
+
sample_count: int | None = None,
|
|
335
|
+
sort_by: str | None = None,
|
|
336
|
+
columns: Sequence[str] | None = None,
|
|
337
|
+
extras: Mapping[str, Any] | None = None,
|
|
338
|
+
types: Sequence[type[pl.DataType]]
|
|
339
|
+
| Mapping[str, type[pl.DataType]]
|
|
340
|
+
| None = None,
|
|
341
|
+
all_types: Sequence[type[pl.DataType]]
|
|
342
|
+
| Mapping[str, type[pl.DataType]]
|
|
343
|
+
| None = None,
|
|
344
|
+
cache: bool = True,
|
|
345
|
+
cache_dir: str | None = None,
|
|
346
|
+
save_to_cache: bool = True,
|
|
347
|
+
load_from_cache: bool = True,
|
|
348
|
+
include_execution: bool = False,
|
|
349
|
+
) -> Execution: ...
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
@overload
|
|
353
|
+
async def async_query(
|
|
354
|
+
query_or_execution: Query | Execution,
|
|
355
|
+
*,
|
|
356
|
+
verbose: bool = True,
|
|
357
|
+
refresh: bool = False,
|
|
358
|
+
max_age: float | None = None,
|
|
359
|
+
parameters: Mapping[str, Any] | None = None,
|
|
360
|
+
api_key: str | None = None,
|
|
361
|
+
performance: Performance = 'medium',
|
|
362
|
+
poll: Literal[True] = True,
|
|
363
|
+
poll_interval: float = 1.0,
|
|
364
|
+
limit: int | None = None,
|
|
365
|
+
offset: int | None = None,
|
|
366
|
+
sample_count: int | None = None,
|
|
367
|
+
sort_by: str | None = None,
|
|
368
|
+
columns: Sequence[str] | None = None,
|
|
369
|
+
extras: Mapping[str, Any] | None = None,
|
|
370
|
+
types: Sequence[type[pl.DataType]]
|
|
371
|
+
| Mapping[str, type[pl.DataType]]
|
|
372
|
+
| None = None,
|
|
373
|
+
all_types: Sequence[type[pl.DataType]]
|
|
374
|
+
| Mapping[str, type[pl.DataType]]
|
|
375
|
+
| None = None,
|
|
376
|
+
cache: bool = True,
|
|
377
|
+
cache_dir: str | None = None,
|
|
378
|
+
save_to_cache: bool = True,
|
|
379
|
+
load_from_cache: bool = True,
|
|
380
|
+
include_execution: Literal[False] = False,
|
|
381
|
+
) -> pl.DataFrame: ...
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
@overload
|
|
385
|
+
async def async_query(
|
|
386
|
+
query_or_execution: Query | Execution,
|
|
387
|
+
*,
|
|
388
|
+
verbose: bool = True,
|
|
389
|
+
refresh: bool = False,
|
|
390
|
+
max_age: float | None = None,
|
|
391
|
+
parameters: Mapping[str, Any] | None = None,
|
|
392
|
+
api_key: str | None = None,
|
|
393
|
+
performance: Performance = 'medium',
|
|
394
|
+
poll: Literal[True] = True,
|
|
395
|
+
poll_interval: float = 1.0,
|
|
396
|
+
limit: int | None = None,
|
|
397
|
+
offset: int | None = None,
|
|
398
|
+
sample_count: int | None = None,
|
|
399
|
+
sort_by: str | None = None,
|
|
400
|
+
columns: Sequence[str] | None = None,
|
|
401
|
+
extras: Mapping[str, Any] | None = None,
|
|
402
|
+
types: Sequence[type[pl.DataType]]
|
|
403
|
+
| Mapping[str, type[pl.DataType]]
|
|
404
|
+
| None = None,
|
|
405
|
+
all_types: Sequence[type[pl.DataType]]
|
|
406
|
+
| Mapping[str, type[pl.DataType]]
|
|
407
|
+
| None = None,
|
|
408
|
+
cache: bool = True,
|
|
409
|
+
cache_dir: str | None = None,
|
|
410
|
+
save_to_cache: bool = True,
|
|
411
|
+
load_from_cache: bool = True,
|
|
412
|
+
include_execution: Literal[True],
|
|
413
|
+
) -> tuple[pl.DataFrame, Execution]: ...
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
async def async_query(
|
|
417
|
+
query_or_execution: Query | Execution,
|
|
418
|
+
*,
|
|
419
|
+
verbose: bool = True,
|
|
420
|
+
refresh: bool = False,
|
|
421
|
+
max_age: float | None = None,
|
|
422
|
+
parameters: Mapping[str, Any] | None = None,
|
|
423
|
+
api_key: str | None = None,
|
|
424
|
+
performance: Performance = 'medium',
|
|
425
|
+
poll: bool = True,
|
|
426
|
+
poll_interval: float = 1.0,
|
|
427
|
+
timeout_seconds: float | None = None,
|
|
428
|
+
limit: int | None = None,
|
|
429
|
+
offset: int | None = None,
|
|
430
|
+
sample_count: int | None = None,
|
|
431
|
+
sort_by: str | None = None,
|
|
432
|
+
columns: Sequence[str] | None = None,
|
|
433
|
+
extras: Mapping[str, Any] | None = None,
|
|
434
|
+
types: Sequence[type[pl.DataType]]
|
|
435
|
+
| Mapping[str, type[pl.DataType]]
|
|
436
|
+
| None = None,
|
|
437
|
+
all_types: Sequence[type[pl.DataType]]
|
|
438
|
+
| Mapping[str, type[pl.DataType]]
|
|
439
|
+
| None = None,
|
|
440
|
+
cache: bool = True,
|
|
441
|
+
cache_dir: str | None = None,
|
|
442
|
+
save_to_cache: bool = True,
|
|
443
|
+
load_from_cache: bool = True,
|
|
444
|
+
include_execution: bool = False,
|
|
445
|
+
):
|
|
446
|
+
|
|
447
|
+
# determine whether target is a query or an execution
|
|
448
|
+
query_id, execution, parameters = _determine_input_type(
|
|
449
|
+
query_or_execution,
|
|
450
|
+
parameters,
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
# gather arguments
|
|
454
|
+
execute_kwargs: ExecuteKwargs = {
|
|
455
|
+
'query_id': query_id,
|
|
456
|
+
'api_key': api_key,
|
|
457
|
+
'parameters': parameters,
|
|
458
|
+
'performance': performance,
|
|
459
|
+
}
|
|
460
|
+
poll_kwargs: PollKwargs = {
|
|
461
|
+
'poll_interval': poll_interval,
|
|
462
|
+
'api_key': api_key,
|
|
463
|
+
'verbose': verbose,
|
|
464
|
+
'timeout_seconds': timeout_seconds,
|
|
465
|
+
}
|
|
466
|
+
result_kwargs: RetrievalKwargs = {
|
|
467
|
+
'limit': limit,
|
|
468
|
+
'offset': offset,
|
|
469
|
+
'sample_count': sample_count,
|
|
470
|
+
'sort_by': sort_by,
|
|
471
|
+
'columns': columns,
|
|
472
|
+
'extras': extras,
|
|
473
|
+
'types': types,
|
|
474
|
+
'all_types': all_types,
|
|
475
|
+
'verbose': verbose,
|
|
476
|
+
}
|
|
477
|
+
output_kwargs: OutputKwargs = {
|
|
478
|
+
'execute_kwargs': execute_kwargs,
|
|
479
|
+
'result_kwargs': result_kwargs,
|
|
480
|
+
'cache': cache,
|
|
481
|
+
'save_to_cache': save_to_cache,
|
|
482
|
+
'cache_dir': cache_dir,
|
|
483
|
+
'include_execution': include_execution,
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
# execute or retrieve query
|
|
487
|
+
if query_id:
|
|
488
|
+
if cache and cache_dir is not None and not refresh:
|
|
489
|
+
cache_result, cache_execution = await _cache.async_load_from_cache(
|
|
490
|
+
execute_kwargs, result_kwargs, output_kwargs
|
|
491
|
+
)
|
|
492
|
+
if cache_result is not None:
|
|
493
|
+
return cache_result
|
|
494
|
+
if execution is None and cache_execution is not None:
|
|
495
|
+
execution = cache_execution
|
|
496
|
+
if max_age is not None and not refresh:
|
|
497
|
+
age = await async_get_query_latest_age(**execute_kwargs, verbose=verbose) # type: ignore
|
|
498
|
+
if age is None or age > max_age:
|
|
499
|
+
refresh = True
|
|
500
|
+
if not refresh:
|
|
501
|
+
df = await async_get_results(**execute_kwargs, **result_kwargs)
|
|
502
|
+
if df is not None:
|
|
503
|
+
return await async_process_result(df, execution, **output_kwargs)
|
|
504
|
+
execution = await async_execute_query(**execute_kwargs, verbose=verbose)
|
|
505
|
+
|
|
506
|
+
# await execution completion
|
|
507
|
+
if execution is None:
|
|
508
|
+
raise Exception('could not determine execution')
|
|
509
|
+
if poll:
|
|
510
|
+
await async_poll_execution(execution, **poll_kwargs)
|
|
511
|
+
df = await async_get_results(execution, api_key, **result_kwargs)
|
|
512
|
+
if df is not None:
|
|
513
|
+
return await async_process_result(df, execution, **output_kwargs)
|
|
514
|
+
else:
|
|
515
|
+
raise Exception('no successful execution for query')
|
|
516
|
+
else:
|
|
517
|
+
return execution
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def _process_result(
|
|
521
|
+
df: pl.DataFrame,
|
|
522
|
+
execution: Execution | None,
|
|
523
|
+
execute_kwargs: ExecuteKwargs,
|
|
524
|
+
result_kwargs: RetrievalKwargs,
|
|
525
|
+
cache: bool,
|
|
526
|
+
save_to_cache: bool,
|
|
527
|
+
cache_dir: str | None,
|
|
528
|
+
include_execution: bool,
|
|
529
|
+
) -> pl.DataFrame | tuple[pl.DataFrame, Execution]:
|
|
530
|
+
if cache and save_to_cache and execute_kwargs['query_id'] is not None:
|
|
531
|
+
if execution is None:
|
|
532
|
+
execution = get_latest_execution(execute_kwargs)
|
|
533
|
+
if execution is None:
|
|
534
|
+
raise Exception('could not get execution')
|
|
535
|
+
_cache.save_to_cache(
|
|
536
|
+
df, execution, execute_kwargs, result_kwargs, cache_dir
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
if include_execution:
|
|
540
|
+
if execution is None:
|
|
541
|
+
execution = get_latest_execution(execute_kwargs)
|
|
542
|
+
if execution is None:
|
|
543
|
+
raise Exception('could not get execution')
|
|
544
|
+
return df, execution
|
|
545
|
+
else:
|
|
546
|
+
return df
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
async def _async_process_result(
|
|
550
|
+
df: pl.DataFrame,
|
|
551
|
+
execution: Execution | None,
|
|
552
|
+
execute_kwargs: ExecuteKwargs,
|
|
553
|
+
result_kwargs: RetrievalKwargs,
|
|
554
|
+
cache: bool,
|
|
555
|
+
save_to_cache: bool,
|
|
556
|
+
cache_dir: str | None,
|
|
557
|
+
include_execution: bool,
|
|
558
|
+
) -> pl.DataFrame | tuple[pl.DataFrame, Execution]:
|
|
559
|
+
if cache and save_to_cache and execute_kwargs['query_id'] is not None:
|
|
560
|
+
if execution is None:
|
|
561
|
+
execution = await async_get_latest_execution(execute_kwargs)
|
|
562
|
+
if execution is None:
|
|
563
|
+
raise Exception('could not get execution')
|
|
564
|
+
_cache.save_to_cache(
|
|
565
|
+
df, execution, execute_kwargs, result_kwargs, cache_dir
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
if include_execution:
|
|
569
|
+
if execution is None:
|
|
570
|
+
execution = await async_get_latest_execution(execute_kwargs)
|
|
571
|
+
if execution is None:
|
|
572
|
+
raise Exception('could not get execution')
|
|
573
|
+
return df, execution
|
|
574
|
+
else:
|
|
575
|
+
return df
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def _get_query_latest_age(
|
|
579
|
+
query_id: int,
|
|
580
|
+
*,
|
|
581
|
+
verbose: bool = True,
|
|
582
|
+
parameters: Mapping[str, Any] | None = None,
|
|
583
|
+
performance: Performance = 'medium',
|
|
584
|
+
api_key: str | None = None,
|
|
585
|
+
) -> float | None:
|
|
586
|
+
import datetime
|
|
587
|
+
import json
|
|
588
|
+
|
|
589
|
+
# process inputs
|
|
590
|
+
if api_key is None:
|
|
591
|
+
api_key = _urls.get_api_key()
|
|
592
|
+
headers = {'X-Dune-API-Key': api_key, 'User-Agent': get_user_agent()}
|
|
593
|
+
data = {}
|
|
594
|
+
if parameters is not None:
|
|
595
|
+
data['query_parameters'] = parameters
|
|
596
|
+
url = _urls.get_query_results_url(query_id, parameters=data, csv=False)
|
|
597
|
+
|
|
598
|
+
# print summary
|
|
599
|
+
if verbose:
|
|
600
|
+
print('checking age of last execution, query_id = ' + str(query_id))
|
|
601
|
+
|
|
602
|
+
response = _transport_get(url, headers=headers, timeout=15.0)
|
|
603
|
+
|
|
604
|
+
# check if result is error
|
|
605
|
+
result = response.json()
|
|
606
|
+
try:
|
|
607
|
+
if 'error' in result:
|
|
608
|
+
if (
|
|
609
|
+
result['error']
|
|
610
|
+
== 'not found: No execution found for the latest version of the given query'
|
|
611
|
+
):
|
|
612
|
+
if verbose:
|
|
613
|
+
print(
|
|
614
|
+
'no age for query, because no previous executions exist'
|
|
615
|
+
)
|
|
616
|
+
return None
|
|
617
|
+
raise Exception(result['error'])
|
|
618
|
+
except json.JSONDecodeError:
|
|
619
|
+
pass
|
|
620
|
+
|
|
621
|
+
# process result
|
|
622
|
+
if 'execution_started_at' in result:
|
|
623
|
+
now = datetime.datetime.now(datetime.UTC).timestamp()
|
|
624
|
+
started = _parse_timestamp(result['execution_started_at'])
|
|
625
|
+
age = now - started
|
|
626
|
+
|
|
627
|
+
if verbose:
|
|
628
|
+
print('latest result age:', age)
|
|
629
|
+
|
|
630
|
+
return age
|
|
631
|
+
else:
|
|
632
|
+
if verbose:
|
|
633
|
+
print('no age for query, because no previous executions exist')
|
|
634
|
+
return None
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def _parse_timestamp(timestamp: str) -> int:
|
|
638
|
+
import datetime
|
|
639
|
+
|
|
640
|
+
# reduce number of decimals in
|
|
641
|
+
if len(timestamp) > 27 and timestamp[-1] == 'Z':
|
|
642
|
+
timestamp = timestamp[:26] + 'Z'
|
|
643
|
+
|
|
644
|
+
timestamp_float = (
|
|
645
|
+
datetime.datetime.strptime(timestamp, '%Y-%m-%dT%H:%M:%S.%fZ')
|
|
646
|
+
.replace(tzinfo=datetime.UTC)
|
|
647
|
+
.timestamp()
|
|
648
|
+
)
|
|
649
|
+
return int(timestamp_float)
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
async def _async_get_query_latest_age(
|
|
653
|
+
query_id: int,
|
|
654
|
+
*,
|
|
655
|
+
verbose: bool = True,
|
|
656
|
+
parameters: Mapping[str, Any] | None = None,
|
|
657
|
+
performance: Performance = 'medium',
|
|
658
|
+
api_key: str | None = None,
|
|
659
|
+
) -> float | None:
|
|
660
|
+
import datetime
|
|
661
|
+
import json
|
|
662
|
+
|
|
663
|
+
import aiohttp
|
|
664
|
+
|
|
665
|
+
# process inputs
|
|
666
|
+
if api_key is None:
|
|
667
|
+
api_key = _urls.get_api_key()
|
|
668
|
+
headers = {'X-Dune-API-Key': api_key, 'User-Agent': get_user_agent()}
|
|
669
|
+
data = {}
|
|
670
|
+
if parameters is not None:
|
|
671
|
+
data['query_parameters'] = parameters
|
|
672
|
+
url = _urls.get_query_results_url(query_id, parameters=data, csv=False)
|
|
673
|
+
|
|
674
|
+
# print summary
|
|
675
|
+
if verbose:
|
|
676
|
+
print('checking age of last execution, query_id = ' + str(query_id))
|
|
677
|
+
|
|
678
|
+
# perform request with retry/backoff for 429/502
|
|
679
|
+
timeout = aiohttp.ClientTimeout(total=30)
|
|
680
|
+
async with aiohttp.ClientSession(timeout=timeout) as session:
|
|
681
|
+
attempts = 0
|
|
682
|
+
backoff = 0.5
|
|
683
|
+
while True:
|
|
684
|
+
async with session.get(url, headers=headers) as response:
|
|
685
|
+
if response.status in (429, 502):
|
|
686
|
+
attempts += 1
|
|
687
|
+
if attempts >= 3:
|
|
688
|
+
break
|
|
689
|
+
import asyncio
|
|
690
|
+
import random
|
|
691
|
+
await asyncio.sleep(backoff * random.uniform(1.5, 2.5))
|
|
692
|
+
backoff = min(5.0, backoff * 2)
|
|
693
|
+
continue
|
|
694
|
+
result: Mapping[str, Any] = await response.json()
|
|
695
|
+
break
|
|
696
|
+
|
|
697
|
+
# check if result is error
|
|
698
|
+
try:
|
|
699
|
+
if 'error' in result:
|
|
700
|
+
if (
|
|
701
|
+
result['error']
|
|
702
|
+
== 'not found: No execution found for the latest version of the given query'
|
|
703
|
+
):
|
|
704
|
+
if verbose:
|
|
705
|
+
print(
|
|
706
|
+
'no age for query, because no previous executions exist'
|
|
707
|
+
)
|
|
708
|
+
return None
|
|
709
|
+
raise Exception(result['error'])
|
|
710
|
+
except json.JSONDecodeError:
|
|
711
|
+
pass
|
|
712
|
+
|
|
713
|
+
# process result
|
|
714
|
+
if 'execution_started_at' in result:
|
|
715
|
+
now = datetime.datetime.now(datetime.UTC).timestamp()
|
|
716
|
+
started = _parse_timestamp(result['execution_started_at'])
|
|
717
|
+
age = now - started
|
|
718
|
+
|
|
719
|
+
if verbose:
|
|
720
|
+
print('latest result age:', age)
|
|
721
|
+
|
|
722
|
+
return age
|
|
723
|
+
else:
|
|
724
|
+
if verbose:
|
|
725
|
+
print('no age for query, because no previous executions exist')
|
|
726
|
+
return None
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
def _execute(
|
|
730
|
+
query_id: int | str,
|
|
731
|
+
*,
|
|
732
|
+
parameters: Mapping[str, Any] | None = None,
|
|
733
|
+
performance: Performance = 'medium',
|
|
734
|
+
api_key: str | None = None,
|
|
735
|
+
verbose: bool = True,
|
|
736
|
+
) -> Execution:
|
|
737
|
+
# process inputs
|
|
738
|
+
url = _urls.get_query_execute_url(query_id)
|
|
739
|
+
if api_key is None:
|
|
740
|
+
api_key = _urls.get_api_key()
|
|
741
|
+
headers = {'X-Dune-API-Key': api_key, 'User-Agent': get_user_agent()}
|
|
742
|
+
data = {}
|
|
743
|
+
if parameters is not None:
|
|
744
|
+
data['query_parameters'] = parameters
|
|
745
|
+
data['performance'] = performance
|
|
746
|
+
|
|
747
|
+
# print summary
|
|
748
|
+
if verbose:
|
|
749
|
+
print('executing query, query_id = ' + str(query_id))
|
|
750
|
+
|
|
751
|
+
# perform request
|
|
752
|
+
response = _transport_post(url, headers=headers, json=data, timeout=15.0)
|
|
753
|
+
result: Mapping[str, Any] = response.json()
|
|
754
|
+
|
|
755
|
+
# check for errors
|
|
756
|
+
if 'execution_id' not in result:
|
|
757
|
+
raise Exception(result['error'])
|
|
758
|
+
|
|
759
|
+
# process result
|
|
760
|
+
execution_id = result['execution_id']
|
|
761
|
+
return {'execution_id': execution_id, 'timestamp': None}
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
async def _async_execute(
|
|
765
|
+
query_id: int | str,
|
|
766
|
+
*,
|
|
767
|
+
parameters: Mapping[str, Any] | None = None,
|
|
768
|
+
performance: Performance = 'medium',
|
|
769
|
+
api_key: str | None = None,
|
|
770
|
+
verbose: bool = True,
|
|
771
|
+
) -> Execution:
|
|
772
|
+
import aiohttp
|
|
773
|
+
|
|
774
|
+
# process inputs
|
|
775
|
+
url = _urls.get_query_execute_url(query_id)
|
|
776
|
+
if api_key is None:
|
|
777
|
+
api_key = _urls.get_api_key()
|
|
778
|
+
headers = {'X-Dune-API-Key': api_key, 'User-Agent': get_user_agent()}
|
|
779
|
+
data = {}
|
|
780
|
+
if parameters is not None:
|
|
781
|
+
data['query_parameters'] = parameters
|
|
782
|
+
data['performance'] = performance
|
|
783
|
+
|
|
784
|
+
# print summary
|
|
785
|
+
if verbose:
|
|
786
|
+
print('executing query, query_id = ' + str(query_id))
|
|
787
|
+
|
|
788
|
+
# perform request
|
|
789
|
+
timeout = aiohttp.ClientTimeout(total=30)
|
|
790
|
+
async with aiohttp.ClientSession(timeout=timeout) as session:
|
|
791
|
+
async with session.post(url, headers=headers, json=data) as response:
|
|
792
|
+
result: Mapping[str, Any] = await response.json()
|
|
793
|
+
|
|
794
|
+
# check for errors
|
|
795
|
+
if 'execution_id' not in result:
|
|
796
|
+
raise Exception(result['error'])
|
|
797
|
+
|
|
798
|
+
# process result
|
|
799
|
+
execution_id = result['execution_id']
|
|
800
|
+
return {'execution_id': execution_id, 'timestamp': None}
|
|
801
|
+
|
|
802
|
+
|
|
803
|
+
def _get_results(
|
|
804
|
+
execution: Execution | None = None,
|
|
805
|
+
api_key: str | None = None,
|
|
806
|
+
*,
|
|
807
|
+
query_id: int | None = None,
|
|
808
|
+
parameters: Mapping[str, Any] | None = None,
|
|
809
|
+
performance: Performance = 'medium',
|
|
810
|
+
limit: int | None = None,
|
|
811
|
+
offset: int | None = None,
|
|
812
|
+
sample_count: int | None = None,
|
|
813
|
+
sort_by: str | None = None,
|
|
814
|
+
columns: Sequence[str] | None = None,
|
|
815
|
+
extras: Mapping[str, Any] | None = None,
|
|
816
|
+
types: Sequence[type[pl.DataType]]
|
|
817
|
+
| Mapping[str, type[pl.DataType]]
|
|
818
|
+
| None = None,
|
|
819
|
+
all_types: Sequence[type[pl.DataType]]
|
|
820
|
+
| Mapping[str, type[pl.DataType]]
|
|
821
|
+
| None = None,
|
|
822
|
+
verbose: bool = True,
|
|
823
|
+
) -> pl.DataFrame | None:
|
|
824
|
+
import random
|
|
825
|
+
import time as _time
|
|
826
|
+
|
|
827
|
+
import polars as pl
|
|
828
|
+
|
|
829
|
+
# process inputs similar to upstream
|
|
830
|
+
if api_key is None:
|
|
831
|
+
api_key = _urls.get_api_key()
|
|
832
|
+
headers = {'X-Dune-API-Key': api_key, 'User-Agent': get_user_agent()}
|
|
833
|
+
params: dict[str, Any] = {
|
|
834
|
+
'limit': limit,
|
|
835
|
+
'offset': offset,
|
|
836
|
+
'sample_count': sample_count,
|
|
837
|
+
'sort_by': sort_by,
|
|
838
|
+
'columns': columns,
|
|
839
|
+
}
|
|
840
|
+
if extras is not None:
|
|
841
|
+
params.update(extras)
|
|
842
|
+
if performance is not None:
|
|
843
|
+
params['performance'] = performance
|
|
844
|
+
if parameters is not None:
|
|
845
|
+
params['query_parameters'] = parameters
|
|
846
|
+
if query_id is not None:
|
|
847
|
+
url = _urls.get_query_results_url(query_id, parameters=params)
|
|
848
|
+
elif execution is not None:
|
|
849
|
+
url = _urls.get_execution_results_url(execution['execution_id'], params)
|
|
850
|
+
else:
|
|
851
|
+
raise Exception('must specify query_id or execution')
|
|
852
|
+
|
|
853
|
+
# print summary
|
|
854
|
+
if verbose:
|
|
855
|
+
if query_id is not None:
|
|
856
|
+
print('getting results, query_id = ' + str(query_id))
|
|
857
|
+
elif execution is not None:
|
|
858
|
+
print('getting results, execution_id = ' + str(execution['execution_id']))
|
|
859
|
+
|
|
860
|
+
# perform request
|
|
861
|
+
# GET with simple retry/backoff for 429/502
|
|
862
|
+
def _get_with_retries(u: str):
|
|
863
|
+
client = current_http_client()
|
|
864
|
+
if client is not None:
|
|
865
|
+
return client.request("GET", u, headers=headers, timeout=30.0)
|
|
866
|
+
|
|
867
|
+
attempts = 0
|
|
868
|
+
backoff = 0.5
|
|
869
|
+
while True:
|
|
870
|
+
resp = _transport_get(u, headers=headers, timeout=30.0)
|
|
871
|
+
if resp.status_code in (429, 502):
|
|
872
|
+
attempts += 1
|
|
873
|
+
if attempts >= 3:
|
|
874
|
+
return resp
|
|
875
|
+
sleep_for = backoff * random.uniform(1.5, 2.5)
|
|
876
|
+
_time.sleep(sleep_for)
|
|
877
|
+
backoff = min(5.0, backoff * 2)
|
|
878
|
+
continue
|
|
879
|
+
return resp
|
|
880
|
+
|
|
881
|
+
response = _get_with_retries(url)
|
|
882
|
+
if response.status_code == 404:
|
|
883
|
+
return None
|
|
884
|
+
result = response.text
|
|
885
|
+
|
|
886
|
+
# process result
|
|
887
|
+
df = _process_raw_table(result, types=types, all_types=all_types)
|
|
888
|
+
|
|
889
|
+
# support pagination when using limit
|
|
890
|
+
response_headers = response.headers
|
|
891
|
+
if limit is not None:
|
|
892
|
+
n_rows = len(df)
|
|
893
|
+
pages = []
|
|
894
|
+
while 'x-dune-next-uri' in response_headers and n_rows < limit:
|
|
895
|
+
if verbose:
|
|
896
|
+
offset_hdr = response.headers.get('x-dune-next-offset', 'unknown')
|
|
897
|
+
print('gathering additional page, offset = ' + str(offset_hdr))
|
|
898
|
+
next_url = response_headers['x-dune-next-uri']
|
|
899
|
+
response = _get_with_retries(next_url)
|
|
900
|
+
result = response.text
|
|
901
|
+
response_headers = response.headers
|
|
902
|
+
page = _process_raw_table(result, types=types, all_types=all_types)
|
|
903
|
+
n_rows += len(page)
|
|
904
|
+
pages.append(page)
|
|
905
|
+
df = pl.concat([df, *pages]).limit(limit)
|
|
906
|
+
|
|
907
|
+
return df
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
async def _async_get_results(
|
|
911
|
+
execution: Execution | None = None,
|
|
912
|
+
api_key: str | None = None,
|
|
913
|
+
*,
|
|
914
|
+
query_id: int | None = None,
|
|
915
|
+
parameters: Mapping[str, Any] | None = None,
|
|
916
|
+
performance: Performance = 'medium',
|
|
917
|
+
limit: int | None = None,
|
|
918
|
+
offset: int | None = None,
|
|
919
|
+
sample_count: int | None = None,
|
|
920
|
+
sort_by: str | None = None,
|
|
921
|
+
columns: Sequence[str] | None = None,
|
|
922
|
+
extras: Mapping[str, Any] | None = None,
|
|
923
|
+
types: Sequence[type[pl.DataType]]
|
|
924
|
+
| Mapping[str, type[pl.DataType]]
|
|
925
|
+
| None = None,
|
|
926
|
+
all_types: Sequence[type[pl.DataType]]
|
|
927
|
+
| Mapping[str, type[pl.DataType]]
|
|
928
|
+
| None = None,
|
|
929
|
+
verbose: bool = True,
|
|
930
|
+
) -> pl.DataFrame | None:
|
|
931
|
+
import asyncio
|
|
932
|
+
import random
|
|
933
|
+
|
|
934
|
+
import aiohttp
|
|
935
|
+
import polars as pl
|
|
936
|
+
|
|
937
|
+
if api_key is None:
|
|
938
|
+
api_key = _urls.get_api_key()
|
|
939
|
+
headers = {'X-Dune-API-Key': api_key, 'User-Agent': get_user_agent()}
|
|
940
|
+
params: dict[str, Any] = {
|
|
941
|
+
'limit': limit,
|
|
942
|
+
'offset': offset,
|
|
943
|
+
'sample_count': sample_count,
|
|
944
|
+
'sort_by': sort_by,
|
|
945
|
+
'columns': columns,
|
|
946
|
+
}
|
|
947
|
+
if extras is not None:
|
|
948
|
+
params.update(extras)
|
|
949
|
+
if parameters is not None:
|
|
950
|
+
params['query_parameters'] = parameters
|
|
951
|
+
if query_id is not None:
|
|
952
|
+
url = _urls.get_query_results_url(query_id, parameters=params)
|
|
953
|
+
elif execution is not None:
|
|
954
|
+
url = _urls.get_execution_results_url(execution['execution_id'], params)
|
|
955
|
+
else:
|
|
956
|
+
raise Exception('must specify query_id or execution')
|
|
957
|
+
|
|
958
|
+
# print summary
|
|
959
|
+
if verbose:
|
|
960
|
+
if query_id is not None:
|
|
961
|
+
print('getting results, query_id = ' + str(query_id))
|
|
962
|
+
elif execution is not None:
|
|
963
|
+
print('getting results, execution_id = ' + str(execution['execution_id']))
|
|
964
|
+
|
|
965
|
+
# perform request
|
|
966
|
+
timeout = aiohttp.ClientTimeout(total=30)
|
|
967
|
+
async with aiohttp.ClientSession(timeout=timeout) as session:
|
|
968
|
+
# GET with simple retry/backoff for 429/502
|
|
969
|
+
attempts = 0
|
|
970
|
+
backoff = 0.5
|
|
971
|
+
while True:
|
|
972
|
+
async with session.get(url, headers=headers) as response:
|
|
973
|
+
if response.status in (429, 502):
|
|
974
|
+
attempts += 1
|
|
975
|
+
if attempts >= 3:
|
|
976
|
+
break
|
|
977
|
+
await asyncio.sleep(backoff * random.uniform(1.5, 2.5))
|
|
978
|
+
backoff = min(5.0, backoff * 2)
|
|
979
|
+
continue
|
|
980
|
+
if response.status == 404:
|
|
981
|
+
return None
|
|
982
|
+
result = await response.text()
|
|
983
|
+
response_headers = response.headers
|
|
984
|
+
break
|
|
985
|
+
|
|
986
|
+
# process result
|
|
987
|
+
df = _process_raw_table(result, types=types, all_types=all_types)
|
|
988
|
+
|
|
989
|
+
# support pagination when using limit
|
|
990
|
+
if limit is not None:
|
|
991
|
+
import polars as pl
|
|
992
|
+
|
|
993
|
+
n_rows = len(df)
|
|
994
|
+
pages = []
|
|
995
|
+
timeout = aiohttp.ClientTimeout(total=30)
|
|
996
|
+
async with aiohttp.ClientSession(timeout=timeout) as session:
|
|
997
|
+
while 'x-dune-next-uri' in response_headers and n_rows < limit:
|
|
998
|
+
if verbose:
|
|
999
|
+
off = response.headers.get('x-dune-next-offset', 'unknown')
|
|
1000
|
+
print('gathering additional page, offset = ' + str(off))
|
|
1001
|
+
next_url = response_headers['x-dune-next-uri']
|
|
1002
|
+
# Pager GET with retry/backoff
|
|
1003
|
+
attempts = 0
|
|
1004
|
+
backoff = 0.5
|
|
1005
|
+
while True:
|
|
1006
|
+
async with session.get(next_url, headers=headers) as response:
|
|
1007
|
+
if response.status in (429, 502):
|
|
1008
|
+
attempts += 1
|
|
1009
|
+
if attempts >= 3:
|
|
1010
|
+
break
|
|
1011
|
+
await asyncio.sleep(backoff * random.uniform(1.5, 2.5))
|
|
1012
|
+
backoff = min(5.0, backoff * 2)
|
|
1013
|
+
continue
|
|
1014
|
+
result = await response.text()
|
|
1015
|
+
response_headers = response.headers
|
|
1016
|
+
break
|
|
1017
|
+
page = _process_raw_table(
|
|
1018
|
+
result, types=types, all_types=all_types
|
|
1019
|
+
)
|
|
1020
|
+
n_rows += len(page)
|
|
1021
|
+
pages.append(page)
|
|
1022
|
+
|
|
1023
|
+
df = pl.concat([df, *pages]).limit(limit)
|
|
1024
|
+
|
|
1025
|
+
return df
|
|
1026
|
+
|
|
1027
|
+
|
|
1028
|
+
def _process_raw_table(
|
|
1029
|
+
raw_csv: str,
|
|
1030
|
+
types: Sequence[type[pl.DataType] | None]
|
|
1031
|
+
| Mapping[str, type[pl.DataType] | None]
|
|
1032
|
+
| None,
|
|
1033
|
+
all_types: Sequence[type[pl.DataType]]
|
|
1034
|
+
| Mapping[str, type[pl.DataType]]
|
|
1035
|
+
| None = None,
|
|
1036
|
+
) -> pl.DataFrame:
|
|
1037
|
+
import polars as pl
|
|
1038
|
+
|
|
1039
|
+
# convert from map to sequence
|
|
1040
|
+
first_line = raw_csv.split('\n', maxsplit=1)[0]
|
|
1041
|
+
column_order = first_line.split(',')
|
|
1042
|
+
|
|
1043
|
+
# parse data as csv
|
|
1044
|
+
df = pl.read_csv(
|
|
1045
|
+
io.StringIO(raw_csv),
|
|
1046
|
+
infer_schema_length=len(raw_csv),
|
|
1047
|
+
null_values='<nil>',
|
|
1048
|
+
truncate_ragged_lines=True,
|
|
1049
|
+
schema_overrides=[pl.String for column in column_order],
|
|
1050
|
+
)
|
|
1051
|
+
|
|
1052
|
+
# check if using all_types
|
|
1053
|
+
if all_types is not None and types is not None:
|
|
1054
|
+
raise Exception('cannot specify both types and all_types')
|
|
1055
|
+
elif all_types is not None:
|
|
1056
|
+
types = all_types
|
|
1057
|
+
|
|
1058
|
+
# cast types
|
|
1059
|
+
new_types = []
|
|
1060
|
+
for c, column in enumerate(df.columns):
|
|
1061
|
+
new_type = None
|
|
1062
|
+
if types is not None:
|
|
1063
|
+
if isinstance(types, list):
|
|
1064
|
+
if len(types) > c and types[c] is not None:
|
|
1065
|
+
new_type = types[c]
|
|
1066
|
+
elif isinstance(types, dict):
|
|
1067
|
+
if column in types and types[column] is not None:
|
|
1068
|
+
new_type = types[column]
|
|
1069
|
+
else:
|
|
1070
|
+
raise Exception('invalid format for types')
|
|
1071
|
+
|
|
1072
|
+
if new_type is None:
|
|
1073
|
+
new_type = infer_type(df[column])
|
|
1074
|
+
|
|
1075
|
+
if new_type == pl.Datetime or isinstance(new_type, pl.Datetime):
|
|
1076
|
+
time_format = '%Y-%m-%d %H:%M:%S%.3f %Z'
|
|
1077
|
+
df = df.with_columns(pl.col(column).str.to_datetime(time_format))
|
|
1078
|
+
new_type = None
|
|
1079
|
+
|
|
1080
|
+
new_types.append(new_type)
|
|
1081
|
+
|
|
1082
|
+
# check that all types were used
|
|
1083
|
+
if isinstance(types, dict):
|
|
1084
|
+
missing_columns = [
|
|
1085
|
+
name for name in types.keys() if name not in df.columns
|
|
1086
|
+
]
|
|
1087
|
+
if len(missing_columns) > 0:
|
|
1088
|
+
raise Exception(
|
|
1089
|
+
'types specified for missing columns: ' + str(missing_columns)
|
|
1090
|
+
)
|
|
1091
|
+
if all_types is not None:
|
|
1092
|
+
missing_columns = [name for name in df.columns if name not in all_types]
|
|
1093
|
+
if len(missing_columns) > 0:
|
|
1094
|
+
raise Exception(
|
|
1095
|
+
'types not specified for columns: ' + str(missing_columns)
|
|
1096
|
+
)
|
|
1097
|
+
|
|
1098
|
+
new_columns = []
|
|
1099
|
+
for column, type in zip(df.columns, new_types):
|
|
1100
|
+
if type is not None:
|
|
1101
|
+
if type == pl.Boolean:
|
|
1102
|
+
new_column = pl.col(column) == 'true'
|
|
1103
|
+
else:
|
|
1104
|
+
new_column = pl.col(column).cast(type)
|
|
1105
|
+
new_columns.append(new_column)
|
|
1106
|
+
df = df.with_columns(*new_columns)
|
|
1107
|
+
|
|
1108
|
+
return df
|
|
1109
|
+
|
|
1110
|
+
|
|
1111
|
+
def infer_type(s: pl.Series) -> pl.DataType:
|
|
1112
|
+
import re
|
|
1113
|
+
|
|
1114
|
+
import polars as pl
|
|
1115
|
+
|
|
1116
|
+
# Heuristic: detect common UTC timestamp format used by Dune exports
|
|
1117
|
+
try:
|
|
1118
|
+
non_null = [v for v in s.to_list() if v is not None and v != '<nil>']
|
|
1119
|
+
if non_null and all(
|
|
1120
|
+
isinstance(v, str)
|
|
1121
|
+
and re.match(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} UTC$", v)
|
|
1122
|
+
for v in non_null
|
|
1123
|
+
):
|
|
1124
|
+
return pl.Datetime
|
|
1125
|
+
except Exception:
|
|
1126
|
+
pass
|
|
1127
|
+
|
|
1128
|
+
try:
|
|
1129
|
+
as_str = pl.DataFrame(s).write_csv(None)
|
|
1130
|
+
return pl.read_csv(io.StringIO(as_str))[s.name].dtype
|
|
1131
|
+
except Exception:
|
|
1132
|
+
return pl.String()
|
|
1133
|
+
|
|
1134
|
+
|
|
1135
|
+
|
|
1136
|
+
def _poll_execution(
|
|
1137
|
+
execution: Execution,
|
|
1138
|
+
*,
|
|
1139
|
+
api_key: str | None,
|
|
1140
|
+
poll_interval: float,
|
|
1141
|
+
verbose: bool,
|
|
1142
|
+
timeout_seconds: float | None,
|
|
1143
|
+
) -> None:
|
|
1144
|
+
import random
|
|
1145
|
+
import time
|
|
1146
|
+
|
|
1147
|
+
# process inputs
|
|
1148
|
+
url = _urls.get_execution_status_url(execution['execution_id'])
|
|
1149
|
+
execution_id = execution['execution_id']
|
|
1150
|
+
if api_key is None:
|
|
1151
|
+
api_key = _urls.get_api_key()
|
|
1152
|
+
headers = {'X-Dune-API-Key': api_key, 'User-Agent': get_user_agent()}
|
|
1153
|
+
|
|
1154
|
+
# print summary
|
|
1155
|
+
t_start = time.time()
|
|
1156
|
+
|
|
1157
|
+
# poll until completion
|
|
1158
|
+
sleep_amount = poll_interval
|
|
1159
|
+
while True:
|
|
1160
|
+
t_poll = time.time()
|
|
1161
|
+
|
|
1162
|
+
# print summary
|
|
1163
|
+
if verbose:
|
|
1164
|
+
print(
|
|
1165
|
+
'waiting for results, execution_id = '
|
|
1166
|
+
+ str(execution['execution_id'])
|
|
1167
|
+
+ ', t = '
|
|
1168
|
+
+ '%.02f' % (t_poll - t_start)
|
|
1169
|
+
)
|
|
1170
|
+
|
|
1171
|
+
# poll
|
|
1172
|
+
response = _http_get(url, headers=headers, timeout=15.0)
|
|
1173
|
+
result = response.json()
|
|
1174
|
+
if (
|
|
1175
|
+
'is_execution_finished' not in result
|
|
1176
|
+
and response.status_code == 429
|
|
1177
|
+
):
|
|
1178
|
+
sleep_amount = sleep_amount * random.uniform(1, 2)
|
|
1179
|
+
time.sleep(sleep_amount)
|
|
1180
|
+
continue
|
|
1181
|
+
if result['is_execution_finished']:
|
|
1182
|
+
if result['state'] == 'QUERY_STATE_FAILED':
|
|
1183
|
+
# Enrich error message with state and any error details if present
|
|
1184
|
+
err_detail = ''
|
|
1185
|
+
try:
|
|
1186
|
+
if 'error' in result and result['error']:
|
|
1187
|
+
err_detail = f", error={result['error']}"
|
|
1188
|
+
except Exception:
|
|
1189
|
+
pass
|
|
1190
|
+
raise Exception(
|
|
1191
|
+
f"QUERY FAILED execution_id={execution_id} state={result.get('state')}{err_detail}"
|
|
1192
|
+
)
|
|
1193
|
+
execution['timestamp'] = _parse_timestamp(
|
|
1194
|
+
result['execution_started_at']
|
|
1195
|
+
)
|
|
1196
|
+
break
|
|
1197
|
+
|
|
1198
|
+
# timeout check
|
|
1199
|
+
if timeout_seconds is not None and (t_poll - t_start) > timeout_seconds:
|
|
1200
|
+
raise TimeoutError(
|
|
1201
|
+
f'query polling timed out after {timeout_seconds} seconds'
|
|
1202
|
+
)
|
|
1203
|
+
|
|
1204
|
+
# wait until polling interval
|
|
1205
|
+
t_wait = time.time() - t_poll
|
|
1206
|
+
if t_wait < poll_interval:
|
|
1207
|
+
time.sleep(poll_interval - t_wait)
|
|
1208
|
+
|
|
1209
|
+
# check for errors
|
|
1210
|
+
if result['state'] == 'QUERY_STATE_FAILED':
|
|
1211
|
+
err_detail = ''
|
|
1212
|
+
try:
|
|
1213
|
+
if 'error' in result and result['error']:
|
|
1214
|
+
err_detail = f", error={result['error']}"
|
|
1215
|
+
except Exception:
|
|
1216
|
+
pass
|
|
1217
|
+
raise Exception(
|
|
1218
|
+
f"QUERY FAILED execution_id={execution_id} state={result.get('state')}{err_detail}"
|
|
1219
|
+
)
|
|
1220
|
+
|
|
1221
|
+
|
|
1222
|
+
async def _async_poll_execution(
|
|
1223
|
+
execution: Execution,
|
|
1224
|
+
*,
|
|
1225
|
+
api_key: str | None,
|
|
1226
|
+
poll_interval: float,
|
|
1227
|
+
verbose: bool,
|
|
1228
|
+
timeout_seconds: float | None,
|
|
1229
|
+
) -> None:
|
|
1230
|
+
import asyncio
|
|
1231
|
+
import random
|
|
1232
|
+
|
|
1233
|
+
import aiohttp
|
|
1234
|
+
|
|
1235
|
+
# process inputs
|
|
1236
|
+
url = _urls.get_execution_status_url(execution['execution_id'])
|
|
1237
|
+
execution_id = execution['execution_id']
|
|
1238
|
+
if api_key is None:
|
|
1239
|
+
api_key = _urls.get_api_key()
|
|
1240
|
+
headers = {'X-Dune-API-Key': api_key, 'User-Agent': get_user_agent()}
|
|
1241
|
+
|
|
1242
|
+
# print summary
|
|
1243
|
+
t_start = time.time()
|
|
1244
|
+
|
|
1245
|
+
# poll until completion
|
|
1246
|
+
timeout = aiohttp.ClientTimeout(total=30)
|
|
1247
|
+
async with aiohttp.ClientSession(timeout=timeout) as session:
|
|
1248
|
+
sleep_amount = poll_interval
|
|
1249
|
+
while True:
|
|
1250
|
+
t_poll = time.time()
|
|
1251
|
+
|
|
1252
|
+
# print summary
|
|
1253
|
+
if verbose:
|
|
1254
|
+
print(
|
|
1255
|
+
'waiting for results, execution_id = '
|
|
1256
|
+
+ str(execution['execution_id'])
|
|
1257
|
+
+ ', t = '
|
|
1258
|
+
+ str(t_poll - t_start)
|
|
1259
|
+
)
|
|
1260
|
+
|
|
1261
|
+
# poll
|
|
1262
|
+
async with session.get(url, headers=headers) as response:
|
|
1263
|
+
result = await response.json()
|
|
1264
|
+
if (
|
|
1265
|
+
'is_execution_finished' not in result
|
|
1266
|
+
and response.status == 429
|
|
1267
|
+
):
|
|
1268
|
+
sleep_amount = sleep_amount * random.uniform(1, 2)
|
|
1269
|
+
await asyncio.sleep(sleep_amount)
|
|
1270
|
+
continue
|
|
1271
|
+
if result['is_execution_finished']:
|
|
1272
|
+
if result['state'] == 'QUERY_STATE_FAILED':
|
|
1273
|
+
err_detail = ''
|
|
1274
|
+
try:
|
|
1275
|
+
if 'error' in result and result['error']:
|
|
1276
|
+
err_detail = f", error={result['error']}"
|
|
1277
|
+
except Exception:
|
|
1278
|
+
pass
|
|
1279
|
+
raise Exception(
|
|
1280
|
+
f"QUERY FAILED execution_id={execution_id} state={result.get('state')}{err_detail}"
|
|
1281
|
+
)
|
|
1282
|
+
execution['timestamp'] = _parse_timestamp(
|
|
1283
|
+
result['execution_started_at']
|
|
1284
|
+
)
|
|
1285
|
+
break
|
|
1286
|
+
|
|
1287
|
+
# timeout check
|
|
1288
|
+
if timeout_seconds is not None and (t_poll - t_start) > timeout_seconds:
|
|
1289
|
+
raise TimeoutError(
|
|
1290
|
+
f'query polling timed out after {timeout_seconds} seconds'
|
|
1291
|
+
)
|
|
1292
|
+
|
|
1293
|
+
# wait until polling interval
|
|
1294
|
+
t_wait = time.time() - t_poll
|
|
1295
|
+
if t_wait < poll_interval:
|
|
1296
|
+
import asyncio
|
|
1297
|
+
|
|
1298
|
+
await asyncio.sleep(poll_interval - t_wait)
|
|
1299
|
+
|
|
1300
|
+
# check for errors
|
|
1301
|
+
if result['state'] == 'QUERY_STATE_FAILED':
|
|
1302
|
+
err_detail = ''
|
|
1303
|
+
try:
|
|
1304
|
+
if 'error' in result and result['error']:
|
|
1305
|
+
err_detail = f", error={result['error']}"
|
|
1306
|
+
except Exception:
|
|
1307
|
+
pass
|
|
1308
|
+
raise Exception(
|
|
1309
|
+
f"QUERY FAILED execution_id={execution_id} state={result.get('state')}{err_detail}"
|
|
1310
|
+
)
|
|
1311
|
+
|
|
1312
|
+
|
|
1313
|
+
def get_latest_execution(
|
|
1314
|
+
execute_kwargs: ExecuteKwargs,
|
|
1315
|
+
*,
|
|
1316
|
+
allow_unfinished: bool = False,
|
|
1317
|
+
) -> Execution | None:
|
|
1318
|
+
import json
|
|
1319
|
+
import random
|
|
1320
|
+
|
|
1321
|
+
|
|
1322
|
+
query_id = execute_kwargs['query_id']
|
|
1323
|
+
api_key = execute_kwargs['api_key']
|
|
1324
|
+
parameters = execute_kwargs['parameters']
|
|
1325
|
+
if query_id is None:
|
|
1326
|
+
raise Exception('query_id required for get_latest_execution')
|
|
1327
|
+
|
|
1328
|
+
# process inputs
|
|
1329
|
+
if api_key is None:
|
|
1330
|
+
api_key = _urls.get_api_key()
|
|
1331
|
+
headers = {'X-Dune-API-Key': api_key, 'User-Agent': get_user_agent()}
|
|
1332
|
+
data: dict[str, Any] = {}
|
|
1333
|
+
if parameters is not None:
|
|
1334
|
+
data['query_parameters'] = parameters
|
|
1335
|
+
data['limit'] = 0
|
|
1336
|
+
url = _urls.get_query_results_url(query_id, parameters=data, csv=False)
|
|
1337
|
+
|
|
1338
|
+
sleep_amount = 1.0
|
|
1339
|
+
while True:
|
|
1340
|
+
# perform request
|
|
1341
|
+
response = _http_get(url, headers=headers, timeout=15.0)
|
|
1342
|
+
if response.status_code in (429, 502):
|
|
1343
|
+
sleep_amount = sleep_amount * random.uniform(1, 2)
|
|
1344
|
+
time.sleep(sleep_amount)
|
|
1345
|
+
continue
|
|
1346
|
+
|
|
1347
|
+
# check if result is error
|
|
1348
|
+
result = response.json()
|
|
1349
|
+
try:
|
|
1350
|
+
if 'error' in result:
|
|
1351
|
+
if (
|
|
1352
|
+
result['error']
|
|
1353
|
+
== 'not found: No execution found for the latest version of the given query'
|
|
1354
|
+
):
|
|
1355
|
+
return None
|
|
1356
|
+
if response.status_code == 429:
|
|
1357
|
+
sleep_amount = sleep_amount * random.uniform(1, 2)
|
|
1358
|
+
time.sleep(sleep_amount)
|
|
1359
|
+
raise Exception(result['error'])
|
|
1360
|
+
except json.JSONDecodeError:
|
|
1361
|
+
pass
|
|
1362
|
+
|
|
1363
|
+
break
|
|
1364
|
+
|
|
1365
|
+
# process result
|
|
1366
|
+
if not result['is_execution_finished'] and not allow_unfinished:
|
|
1367
|
+
return None
|
|
1368
|
+
execution: Execution = {'execution_id': result['execution_id']}
|
|
1369
|
+
if 'execution_started_at' in result:
|
|
1370
|
+
execution['timestamp'] = int(
|
|
1371
|
+
_parse_timestamp(result['execution_started_at'])
|
|
1372
|
+
)
|
|
1373
|
+
return execution
|
|
1374
|
+
|
|
1375
|
+
|
|
1376
|
+
async def async_get_latest_execution(
|
|
1377
|
+
execute_kwargs: ExecuteKwargs,
|
|
1378
|
+
*,
|
|
1379
|
+
allow_unfinished: bool = False,
|
|
1380
|
+
) -> Execution | None:
|
|
1381
|
+
import json
|
|
1382
|
+
import random
|
|
1383
|
+
|
|
1384
|
+
import aiohttp
|
|
1385
|
+
|
|
1386
|
+
query_id = execute_kwargs['query_id']
|
|
1387
|
+
api_key = execute_kwargs['api_key']
|
|
1388
|
+
parameters = execute_kwargs['parameters']
|
|
1389
|
+
if query_id is None:
|
|
1390
|
+
raise Exception('query_id required for async_get_latest_execution')
|
|
1391
|
+
|
|
1392
|
+
# process inputs
|
|
1393
|
+
if api_key is None:
|
|
1394
|
+
api_key = _urls.get_api_key()
|
|
1395
|
+
headers = {'X-Dune-API-Key': api_key, 'User-Agent': get_user_agent()}
|
|
1396
|
+
data: dict[str, Any] = {}
|
|
1397
|
+
if parameters is not None:
|
|
1398
|
+
data['query_parameters'] = parameters
|
|
1399
|
+
data['limit'] = 0
|
|
1400
|
+
url = _urls.get_query_results_url(query_id, parameters=data, csv=False)
|
|
1401
|
+
|
|
1402
|
+
# perform request
|
|
1403
|
+
timeout = aiohttp.ClientTimeout(total=30)
|
|
1404
|
+
async with aiohttp.ClientSession(timeout=timeout) as session:
|
|
1405
|
+
sleep_amount = 1.0
|
|
1406
|
+
while True:
|
|
1407
|
+
async with session.get(url, headers=headers) as response:
|
|
1408
|
+
if response.status in (429, 502):
|
|
1409
|
+
sleep_amount = sleep_amount * random.uniform(1, 2)
|
|
1410
|
+
import asyncio
|
|
1411
|
+
await asyncio.sleep(sleep_amount)
|
|
1412
|
+
continue
|
|
1413
|
+
result: Mapping[str, Any] = await response.json()
|
|
1414
|
+
|
|
1415
|
+
# check if result is error
|
|
1416
|
+
try:
|
|
1417
|
+
if 'error' in result:
|
|
1418
|
+
if (
|
|
1419
|
+
result['error']
|
|
1420
|
+
== 'not found: No execution found for the latest version of the given query'
|
|
1421
|
+
):
|
|
1422
|
+
return None
|
|
1423
|
+
if response.status == 429:
|
|
1424
|
+
import asyncio
|
|
1425
|
+
|
|
1426
|
+
sleep_amount = sleep_amount * random.uniform(1, 2)
|
|
1427
|
+
await asyncio.sleep(sleep_amount)
|
|
1428
|
+
raise Exception(result['error'])
|
|
1429
|
+
except json.JSONDecodeError:
|
|
1430
|
+
pass
|
|
1431
|
+
break
|
|
1432
|
+
|
|
1433
|
+
# process result
|
|
1434
|
+
if not result['is_execution_finished'] and not allow_unfinished:
|
|
1435
|
+
return None
|
|
1436
|
+
execution: Execution = {'execution_id': result['execution_id']}
|
|
1437
|
+
if 'execution_started_at' in result:
|
|
1438
|
+
execution['timestamp'] = int(
|
|
1439
|
+
_parse_timestamp(result['execution_started_at'])
|
|
1440
|
+
)
|
|
1441
|
+
return execution
|
|
1442
|
+
|
|
1443
|
+
|
|
1444
|
+
def get_user_agent() -> str:
|
|
1445
|
+
# Identify as spice-mcp vendored spice client
|
|
1446
|
+
return 'spice-mcp/' + ADAPTER_VERSION
|
|
1447
|
+
|
|
1448
|
+
# ---------------------------------------------------------------------------
|
|
1449
|
+
# Public aliases expected by adapter/tests (non-underscored names)
|
|
1450
|
+
|
|
1451
|
+
determine_input_type = _determine_input_type
|
|
1452
|
+
get_query_latest_age = _get_query_latest_age
|
|
1453
|
+
async_get_query_latest_age = _async_get_query_latest_age
|
|
1454
|
+
execute_query = _execute
|
|
1455
|
+
async_execute_query = _async_execute
|
|
1456
|
+
get_results = _get_results
|
|
1457
|
+
async_get_results = _async_get_results
|
|
1458
|
+
process_result = _process_result
|
|
1459
|
+
async_process_result = _async_process_result
|
|
1460
|
+
poll_execution = _poll_execution
|
|
1461
|
+
async_poll_execution = _async_poll_execution
|