singlestoredb 1.12.3__cp38-abi3-win32.whl → 1.13.0__cp38-abi3-win32.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 singlestoredb might be problematic. Click here for more details.
- _singlestoredb_accel.pyd +0 -0
- singlestoredb/__init__.py +1 -1
- singlestoredb/apps/__init__.py +1 -0
- singlestoredb/apps/_config.py +6 -0
- singlestoredb/apps/_connection_info.py +8 -0
- singlestoredb/apps/_python_udfs.py +85 -0
- singlestoredb/config.py +14 -2
- singlestoredb/functions/__init__.py +11 -1
- singlestoredb/functions/decorator.py +102 -252
- singlestoredb/functions/dtypes.py +545 -198
- singlestoredb/functions/ext/asgi.py +288 -90
- singlestoredb/functions/ext/json.py +29 -36
- singlestoredb/functions/ext/mmap.py +1 -1
- singlestoredb/functions/ext/rowdat_1.py +50 -70
- singlestoredb/functions/signature.py +816 -144
- singlestoredb/functions/typing.py +41 -0
- singlestoredb/functions/utils.py +342 -0
- singlestoredb/http/connection.py +3 -1
- singlestoredb/management/manager.py +6 -1
- singlestoredb/management/utils.py +2 -2
- singlestoredb/mysql/connection.py +17 -11
- singlestoredb/tests/ext_funcs/__init__.py +476 -237
- singlestoredb/tests/test_basics.py +2 -0
- singlestoredb/tests/test_ext_func.py +192 -3
- singlestoredb/tests/test_udf.py +101 -131
- singlestoredb/tests/test_udf_returns.py +459 -0
- {singlestoredb-1.12.3.dist-info → singlestoredb-1.13.0.dist-info}/METADATA +2 -1
- {singlestoredb-1.12.3.dist-info → singlestoredb-1.13.0.dist-info}/RECORD +32 -28
- {singlestoredb-1.12.3.dist-info → singlestoredb-1.13.0.dist-info}/LICENSE +0 -0
- {singlestoredb-1.12.3.dist-info → singlestoredb-1.13.0.dist-info}/WHEEL +0 -0
- {singlestoredb-1.12.3.dist-info → singlestoredb-1.13.0.dist-info}/entry_points.txt +0 -0
- {singlestoredb-1.12.3.dist-info → singlestoredb-1.13.0.dist-info}/top_level.txt +0 -0
|
@@ -26,6 +26,7 @@ import argparse
|
|
|
26
26
|
import asyncio
|
|
27
27
|
import dataclasses
|
|
28
28
|
import importlib.util
|
|
29
|
+
import inspect
|
|
29
30
|
import io
|
|
30
31
|
import itertools
|
|
31
32
|
import json
|
|
@@ -36,6 +37,7 @@ import secrets
|
|
|
36
37
|
import sys
|
|
37
38
|
import tempfile
|
|
38
39
|
import textwrap
|
|
40
|
+
import typing
|
|
39
41
|
import urllib
|
|
40
42
|
import zipfile
|
|
41
43
|
import zipimport
|
|
@@ -62,6 +64,8 @@ from ...config import get_option
|
|
|
62
64
|
from ...mysql.constants import FIELD_TYPE as ft
|
|
63
65
|
from ..signature import get_signature
|
|
64
66
|
from ..signature import signature_to_sql
|
|
67
|
+
from ..typing import Masked
|
|
68
|
+
from ..typing import Table
|
|
65
69
|
|
|
66
70
|
try:
|
|
67
71
|
import cloudpickle
|
|
@@ -148,20 +152,100 @@ def as_tuple(x: Any) -> Any:
|
|
|
148
152
|
if has_pydantic and isinstance(x, BaseModel):
|
|
149
153
|
return tuple(x.model_dump().values())
|
|
150
154
|
if dataclasses.is_dataclass(x):
|
|
151
|
-
return dataclasses.astuple(x)
|
|
152
|
-
|
|
155
|
+
return dataclasses.astuple(x) # type: ignore
|
|
156
|
+
if isinstance(x, dict):
|
|
157
|
+
return tuple(x.values())
|
|
158
|
+
return tuple(x)
|
|
153
159
|
|
|
154
160
|
|
|
155
161
|
def as_list_of_tuples(x: Any) -> Any:
|
|
156
162
|
"""Convert object to a list of tuples."""
|
|
163
|
+
if isinstance(x, Table):
|
|
164
|
+
x = x[0]
|
|
157
165
|
if isinstance(x, (list, tuple)) and len(x) > 0:
|
|
166
|
+
if isinstance(x[0], (list, tuple)):
|
|
167
|
+
return x
|
|
158
168
|
if has_pydantic and isinstance(x[0], BaseModel):
|
|
159
169
|
return [tuple(y.model_dump().values()) for y in x]
|
|
160
170
|
if dataclasses.is_dataclass(x[0]):
|
|
161
171
|
return [dataclasses.astuple(y) for y in x]
|
|
172
|
+
if isinstance(x[0], dict):
|
|
173
|
+
return [tuple(y.values()) for y in x]
|
|
174
|
+
return [(y,) for y in x]
|
|
162
175
|
return x
|
|
163
176
|
|
|
164
177
|
|
|
178
|
+
def get_dataframe_columns(df: Any) -> List[Any]:
|
|
179
|
+
"""Return columns of data from a dataframe/table."""
|
|
180
|
+
if isinstance(df, Table):
|
|
181
|
+
if len(df) == 1:
|
|
182
|
+
df = df[0]
|
|
183
|
+
else:
|
|
184
|
+
return list(df)
|
|
185
|
+
|
|
186
|
+
if isinstance(df, Masked):
|
|
187
|
+
return [df]
|
|
188
|
+
|
|
189
|
+
if isinstance(df, tuple):
|
|
190
|
+
return list(df)
|
|
191
|
+
|
|
192
|
+
rtype = str(type(df)).lower()
|
|
193
|
+
if 'dataframe' in rtype:
|
|
194
|
+
return [df[x] for x in df.columns]
|
|
195
|
+
elif 'table' in rtype:
|
|
196
|
+
return df.columns
|
|
197
|
+
elif 'series' in rtype:
|
|
198
|
+
return [df]
|
|
199
|
+
elif 'array' in rtype:
|
|
200
|
+
return [df]
|
|
201
|
+
elif 'tuple' in rtype:
|
|
202
|
+
return list(df)
|
|
203
|
+
|
|
204
|
+
raise TypeError(
|
|
205
|
+
'Unsupported data type for dataframe columns: '
|
|
206
|
+
f'{rtype}',
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def get_array_class(data_format: str) -> Callable[..., Any]:
|
|
211
|
+
"""
|
|
212
|
+
Get the array class for the current data format.
|
|
213
|
+
|
|
214
|
+
"""
|
|
215
|
+
if data_format == 'polars':
|
|
216
|
+
import polars as pl
|
|
217
|
+
array_cls = pl.Series
|
|
218
|
+
elif data_format == 'arrow':
|
|
219
|
+
import pyarrow as pa
|
|
220
|
+
array_cls = pa.array
|
|
221
|
+
elif data_format == 'pandas':
|
|
222
|
+
import pandas as pd
|
|
223
|
+
array_cls = pd.Series
|
|
224
|
+
else:
|
|
225
|
+
import numpy as np
|
|
226
|
+
array_cls = np.array
|
|
227
|
+
return array_cls
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def get_masked_params(func: Callable[..., Any]) -> List[bool]:
|
|
231
|
+
"""
|
|
232
|
+
Get the list of masked parameters for the function.
|
|
233
|
+
|
|
234
|
+
Parameters
|
|
235
|
+
----------
|
|
236
|
+
func : Callable
|
|
237
|
+
The function to call as the endpoint
|
|
238
|
+
|
|
239
|
+
Returns
|
|
240
|
+
-------
|
|
241
|
+
List[bool]
|
|
242
|
+
Boolean list of masked parameters
|
|
243
|
+
|
|
244
|
+
"""
|
|
245
|
+
params = inspect.signature(func).parameters
|
|
246
|
+
return [typing.get_origin(x.annotation) is Masked for x in params.values()]
|
|
247
|
+
|
|
248
|
+
|
|
165
249
|
def make_func(
|
|
166
250
|
name: str,
|
|
167
251
|
func: Callable[..., Any],
|
|
@@ -181,104 +265,107 @@ def make_func(
|
|
|
181
265
|
(Callable, Dict[str, Any])
|
|
182
266
|
|
|
183
267
|
"""
|
|
184
|
-
attrs = getattr(func, '_singlestoredb_attrs', {})
|
|
185
|
-
data_format = attrs.get('data_format') or 'python'
|
|
186
|
-
include_masks = attrs.get('include_masks', False)
|
|
187
|
-
function_type = attrs.get('function_type', 'udf').lower()
|
|
188
268
|
info: Dict[str, Any] = {}
|
|
189
269
|
|
|
270
|
+
sig = get_signature(func, func_name=name)
|
|
271
|
+
|
|
272
|
+
function_type = sig.get('function_type', 'udf')
|
|
273
|
+
args_data_format = sig.get('args_data_format', 'scalar')
|
|
274
|
+
returns_data_format = sig.get('returns_data_format', 'scalar')
|
|
275
|
+
|
|
276
|
+
masks = get_masked_params(func)
|
|
277
|
+
|
|
190
278
|
if function_type == 'tvf':
|
|
191
|
-
|
|
279
|
+
# Scalar / list types (row-based)
|
|
280
|
+
if returns_data_format in ['scalar', 'list']:
|
|
192
281
|
async def do_func(
|
|
193
282
|
row_ids: Sequence[int],
|
|
194
283
|
rows: Sequence[Sequence[Any]],
|
|
195
|
-
) -> Tuple[
|
|
196
|
-
Sequence[int],
|
|
197
|
-
List[Tuple[Any]],
|
|
198
|
-
]:
|
|
284
|
+
) -> Tuple[Sequence[int], List[Tuple[Any, ...]]]:
|
|
199
285
|
'''Call function on given rows of data.'''
|
|
200
286
|
out_ids: List[int] = []
|
|
201
287
|
out = []
|
|
288
|
+
# Call function on each row of data
|
|
202
289
|
for i, res in zip(row_ids, func_map(func, rows)):
|
|
203
290
|
out.extend(as_list_of_tuples(res))
|
|
204
291
|
out_ids.extend([row_ids[i]] * (len(out)-len(out_ids)))
|
|
205
292
|
return out_ids, out
|
|
206
293
|
|
|
294
|
+
# Vector formats (column-based)
|
|
207
295
|
else:
|
|
208
|
-
|
|
296
|
+
array_cls = get_array_class(returns_data_format)
|
|
297
|
+
|
|
209
298
|
async def do_func( # type: ignore
|
|
210
299
|
row_ids: Sequence[int],
|
|
211
300
|
cols: Sequence[Tuple[Sequence[Any], Optional[Sequence[bool]]]],
|
|
212
|
-
) -> Tuple[
|
|
301
|
+
) -> Tuple[
|
|
302
|
+
Sequence[int],
|
|
303
|
+
List[Tuple[Sequence[Any], Optional[Sequence[bool]]]],
|
|
304
|
+
]:
|
|
213
305
|
'''Call function on given cols of data.'''
|
|
214
|
-
if include_masks:
|
|
215
|
-
out = func(*cols)
|
|
216
|
-
assert isinstance(out, tuple)
|
|
217
|
-
return row_ids, [out]
|
|
218
|
-
|
|
219
|
-
out = []
|
|
220
|
-
res = func(*[x[0] for x in cols])
|
|
221
|
-
rtype = str(type(res)).lower()
|
|
222
|
-
|
|
223
|
-
# Map tables / dataframes to a list of columns
|
|
224
|
-
if 'dataframe' in rtype:
|
|
225
|
-
res = [res[x] for x in res.columns]
|
|
226
|
-
elif 'table' in rtype:
|
|
227
|
-
res = res.columns
|
|
228
|
-
|
|
229
|
-
for vec in res:
|
|
230
|
-
# C extension only supports Python objects as strings
|
|
231
|
-
if data_format == 'numpy' and str(vec.dtype)[:2] in ['<U', '<S']:
|
|
232
|
-
vec = vec.astype(object)
|
|
233
|
-
out.append((vec, None))
|
|
234
|
-
|
|
235
306
|
# NOTE: There is no way to determine which row ID belongs to
|
|
236
307
|
# each result row, so we just have to use the same
|
|
237
308
|
# row ID for all rows in the result.
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
309
|
+
|
|
310
|
+
def build_tuple(x: Any) -> Any:
|
|
311
|
+
return tuple(x) if isinstance(x, Masked) else (x, None)
|
|
312
|
+
|
|
313
|
+
# Call function on each column of data
|
|
314
|
+
if cols and cols[0]:
|
|
315
|
+
res = get_dataframe_columns(
|
|
316
|
+
func(*[x if m else x[0] for x, m in zip(cols, masks)]),
|
|
317
|
+
)
|
|
318
|
+
else:
|
|
319
|
+
res = get_dataframe_columns(func())
|
|
320
|
+
|
|
321
|
+
# Generate row IDs
|
|
322
|
+
if isinstance(res[0], Masked):
|
|
323
|
+
row_ids = array_cls([row_ids[0]] * len(res[0][0]))
|
|
247
324
|
else:
|
|
248
|
-
|
|
249
|
-
array_cls = np.array
|
|
325
|
+
row_ids = array_cls([row_ids[0]] * len(res[0]))
|
|
250
326
|
|
|
251
|
-
return
|
|
327
|
+
return row_ids, [build_tuple(x) for x in res]
|
|
252
328
|
|
|
253
329
|
else:
|
|
254
|
-
|
|
330
|
+
# Scalar / list types (row-based)
|
|
331
|
+
if returns_data_format in ['scalar', 'list']:
|
|
255
332
|
async def do_func(
|
|
256
333
|
row_ids: Sequence[int],
|
|
257
334
|
rows: Sequence[Sequence[Any]],
|
|
258
|
-
) -> Tuple[
|
|
259
|
-
Sequence[int],
|
|
260
|
-
List[Tuple[Any]],
|
|
261
|
-
]:
|
|
335
|
+
) -> Tuple[Sequence[int], List[Tuple[Any, ...]]]:
|
|
262
336
|
'''Call function on given rows of data.'''
|
|
263
337
|
return row_ids, [as_tuple(x) for x in zip(func_map(func, rows))]
|
|
264
338
|
|
|
339
|
+
# Vector formats (column-based)
|
|
265
340
|
else:
|
|
266
|
-
|
|
341
|
+
array_cls = get_array_class(returns_data_format)
|
|
342
|
+
|
|
267
343
|
async def do_func( # type: ignore
|
|
268
344
|
row_ids: Sequence[int],
|
|
269
345
|
cols: Sequence[Tuple[Sequence[Any], Optional[Sequence[bool]]]],
|
|
270
|
-
) -> Tuple[
|
|
346
|
+
) -> Tuple[
|
|
347
|
+
Sequence[int],
|
|
348
|
+
List[Tuple[Sequence[Any], Optional[Sequence[bool]]]],
|
|
349
|
+
]:
|
|
271
350
|
'''Call function on given cols of data.'''
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
return
|
|
351
|
+
row_ids = array_cls(row_ids)
|
|
352
|
+
|
|
353
|
+
def build_tuple(x: Any) -> Any:
|
|
354
|
+
return tuple(x) if isinstance(x, Masked) else (x, None)
|
|
355
|
+
|
|
356
|
+
# Call the function with `cols` as the function parameters
|
|
357
|
+
if cols and cols[0]:
|
|
358
|
+
out = func(*[x if m else x[0] for x, m in zip(cols, masks)])
|
|
359
|
+
else:
|
|
360
|
+
out = func()
|
|
276
361
|
|
|
277
|
-
|
|
362
|
+
# Single masked value
|
|
363
|
+
if isinstance(out, Masked):
|
|
364
|
+
return row_ids, [tuple(out)]
|
|
278
365
|
|
|
279
366
|
# Multiple return values
|
|
280
367
|
if isinstance(out, tuple):
|
|
281
|
-
return row_ids, [(x
|
|
368
|
+
return row_ids, [build_tuple(x) for x in out]
|
|
282
369
|
|
|
283
370
|
# Single return value
|
|
284
371
|
return row_ids, [(out, None)]
|
|
@@ -286,13 +373,12 @@ def make_func(
|
|
|
286
373
|
do_func.__name__ = name
|
|
287
374
|
do_func.__doc__ = func.__doc__
|
|
288
375
|
|
|
289
|
-
sig = get_signature(func, name=name)
|
|
290
|
-
|
|
291
376
|
# Store signature for generating CREATE FUNCTION calls
|
|
292
377
|
info['signature'] = sig
|
|
293
378
|
|
|
294
379
|
# Set data format
|
|
295
|
-
info['
|
|
380
|
+
info['args_data_format'] = args_data_format
|
|
381
|
+
info['returns_data_format'] = returns_data_format
|
|
296
382
|
|
|
297
383
|
# Set function type
|
|
298
384
|
info['function_type'] = function_type
|
|
@@ -306,20 +392,13 @@ def make_func(
|
|
|
306
392
|
colspec.append((x['name'], rowdat_1_type_map[dtype]))
|
|
307
393
|
info['colspec'] = colspec
|
|
308
394
|
|
|
309
|
-
def parse_return_type(s: str) -> List[str]:
|
|
310
|
-
if s.startswith('tuple['):
|
|
311
|
-
return s[6:-1].split(',')
|
|
312
|
-
if s.startswith('array[tuple['):
|
|
313
|
-
return s[12:-2].split(',')
|
|
314
|
-
return [s]
|
|
315
|
-
|
|
316
395
|
# Setup return type
|
|
317
396
|
returns = []
|
|
318
|
-
for x in
|
|
319
|
-
dtype = x.replace('?', '')
|
|
397
|
+
for x in sig['returns']:
|
|
398
|
+
dtype = x['dtype'].replace('?', '')
|
|
320
399
|
if dtype not in rowdat_1_type_map:
|
|
321
400
|
raise TypeError(f'no data type mapping for {dtype}')
|
|
322
|
-
returns.append(rowdat_1_type_map[dtype])
|
|
401
|
+
returns.append((x['name'], rowdat_1_type_map[dtype]))
|
|
323
402
|
info['returns'] = returns
|
|
324
403
|
|
|
325
404
|
return do_func, info
|
|
@@ -371,6 +450,13 @@ class Application(object):
|
|
|
371
450
|
headers=[(b'content-type', b'text/plain')],
|
|
372
451
|
)
|
|
373
452
|
|
|
453
|
+
# Error response start
|
|
454
|
+
error_response_dict: Dict[str, Any] = dict(
|
|
455
|
+
type='http.response.start',
|
|
456
|
+
status=401,
|
|
457
|
+
headers=[(b'content-type', b'text/plain')],
|
|
458
|
+
)
|
|
459
|
+
|
|
374
460
|
# JSON response start
|
|
375
461
|
json_response_dict: Dict[str, Any] = dict(
|
|
376
462
|
type='http.response.start',
|
|
@@ -405,7 +491,12 @@ class Application(object):
|
|
|
405
491
|
|
|
406
492
|
# Data format + version handlers
|
|
407
493
|
handlers = {
|
|
408
|
-
(b'application/octet-stream', b'1.0', '
|
|
494
|
+
(b'application/octet-stream', b'1.0', 'scalar'): dict(
|
|
495
|
+
load=rowdat_1.load,
|
|
496
|
+
dump=rowdat_1.dump,
|
|
497
|
+
response=rowdat_1_response_dict,
|
|
498
|
+
),
|
|
499
|
+
(b'application/octet-stream', b'1.0', 'list'): dict(
|
|
409
500
|
load=rowdat_1.load,
|
|
410
501
|
dump=rowdat_1.dump,
|
|
411
502
|
response=rowdat_1_response_dict,
|
|
@@ -430,7 +521,12 @@ class Application(object):
|
|
|
430
521
|
dump=rowdat_1.dump_arrow,
|
|
431
522
|
response=rowdat_1_response_dict,
|
|
432
523
|
),
|
|
433
|
-
(b'application/json', b'1.0', '
|
|
524
|
+
(b'application/json', b'1.0', 'scalar'): dict(
|
|
525
|
+
load=jdata.load,
|
|
526
|
+
dump=jdata.dump,
|
|
527
|
+
response=json_response_dict,
|
|
528
|
+
),
|
|
529
|
+
(b'application/json', b'1.0', 'list'): dict(
|
|
434
530
|
load=jdata.load,
|
|
435
531
|
dump=jdata.dump,
|
|
436
532
|
response=json_response_dict,
|
|
@@ -455,7 +551,7 @@ class Application(object):
|
|
|
455
551
|
dump=jdata.dump_arrow,
|
|
456
552
|
response=json_response_dict,
|
|
457
553
|
),
|
|
458
|
-
(b'application/vnd.apache.arrow.file', b'1.0', '
|
|
554
|
+
(b'application/vnd.apache.arrow.file', b'1.0', 'scalar'): dict(
|
|
459
555
|
load=arrow.load,
|
|
460
556
|
dump=arrow.dump,
|
|
461
557
|
response=arrow_response_dict,
|
|
@@ -485,6 +581,7 @@ class Application(object):
|
|
|
485
581
|
# Valid URL paths
|
|
486
582
|
invoke_path = ('invoke',)
|
|
487
583
|
show_create_function_path = ('show', 'create_function')
|
|
584
|
+
show_function_info_path = ('show', 'function_info')
|
|
488
585
|
|
|
489
586
|
def __init__(
|
|
490
587
|
self,
|
|
@@ -505,6 +602,8 @@ class Application(object):
|
|
|
505
602
|
link_name: Optional[str] = get_option('external_function.link_name'),
|
|
506
603
|
link_config: Optional[Dict[str, Any]] = None,
|
|
507
604
|
link_credentials: Optional[Dict[str, Any]] = None,
|
|
605
|
+
name_prefix: str = get_option('external_function.name_prefix'),
|
|
606
|
+
name_suffix: str = get_option('external_function.name_suffix'),
|
|
508
607
|
) -> None:
|
|
509
608
|
if link_name and (link_config or link_credentials):
|
|
510
609
|
raise ValueError(
|
|
@@ -561,6 +660,7 @@ class Application(object):
|
|
|
561
660
|
if not hasattr(x, '_singlestoredb_attrs'):
|
|
562
661
|
continue
|
|
563
662
|
name = x._singlestoredb_attrs.get('name', x.__name__)
|
|
663
|
+
name = f'{name_prefix}{name}{name_suffix}'
|
|
564
664
|
external_functions[x.__name__] = x
|
|
565
665
|
func, info = make_func(name, x)
|
|
566
666
|
endpoints[name.encode('utf-8')] = func, info
|
|
@@ -576,6 +676,7 @@ class Application(object):
|
|
|
576
676
|
# Add endpoint for each exported function
|
|
577
677
|
for name, alias in get_func_names(func_names):
|
|
578
678
|
item = getattr(pkg, name)
|
|
679
|
+
alias = f'{name_prefix}{name}{name_suffix}'
|
|
579
680
|
external_functions[name] = item
|
|
580
681
|
func, info = make_func(alias, item)
|
|
581
682
|
endpoints[alias.encode('utf-8')] = func, info
|
|
@@ -588,6 +689,7 @@ class Application(object):
|
|
|
588
689
|
if not hasattr(x, '_singlestoredb_attrs'):
|
|
589
690
|
continue
|
|
590
691
|
name = x._singlestoredb_attrs.get('name', x.__name__)
|
|
692
|
+
name = f'{name_prefix}{name}{name_suffix}'
|
|
591
693
|
external_functions[x.__name__] = x
|
|
592
694
|
func, info = make_func(name, x)
|
|
593
695
|
endpoints[name.encode('utf-8')] = func, info
|
|
@@ -595,6 +697,7 @@ class Application(object):
|
|
|
595
697
|
else:
|
|
596
698
|
alias = funcs.__name__
|
|
597
699
|
external_functions[funcs.__name__] = funcs
|
|
700
|
+
alias = f'{name_prefix}{alias}{name_suffix}'
|
|
598
701
|
func, info = make_func(alias, funcs)
|
|
599
702
|
endpoints[alias.encode('utf-8')] = func, info
|
|
600
703
|
|
|
@@ -648,7 +751,8 @@ class Application(object):
|
|
|
648
751
|
|
|
649
752
|
# Call the endpoint
|
|
650
753
|
if method == 'POST' and func is not None and path == self.invoke_path:
|
|
651
|
-
|
|
754
|
+
args_data_format = func_info['args_data_format']
|
|
755
|
+
returns_data_format = func_info['returns_data_format']
|
|
652
756
|
data = []
|
|
653
757
|
more_body = True
|
|
654
758
|
while more_body:
|
|
@@ -657,17 +761,24 @@ class Application(object):
|
|
|
657
761
|
more_body = request.get('more_body', False)
|
|
658
762
|
|
|
659
763
|
data_version = headers.get(b's2-ef-version', b'')
|
|
660
|
-
input_handler = self.handlers[(content_type, data_version,
|
|
661
|
-
output_handler = self.handlers[(accepts, data_version,
|
|
764
|
+
input_handler = self.handlers[(content_type, data_version, args_data_format)]
|
|
765
|
+
output_handler = self.handlers[(accepts, data_version, returns_data_format)]
|
|
662
766
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
767
|
+
try:
|
|
768
|
+
out = await func(
|
|
769
|
+
*input_handler['load']( # type: ignore
|
|
770
|
+
func_info['colspec'], b''.join(data),
|
|
771
|
+
),
|
|
772
|
+
)
|
|
773
|
+
body = output_handler['dump'](
|
|
774
|
+
[x[1] for x in func_info['returns']], *out, # type: ignore
|
|
775
|
+
)
|
|
776
|
+
await send(output_handler['response'])
|
|
669
777
|
|
|
670
|
-
|
|
778
|
+
except Exception as e:
|
|
779
|
+
logging.exception('Error in function call')
|
|
780
|
+
body = f'[{type(e).__name__}] {str(e).strip()}'.encode('utf-8')
|
|
781
|
+
await send(self.error_response_dict)
|
|
671
782
|
|
|
672
783
|
# Handle api reflection
|
|
673
784
|
elif method == 'GET' and path == self.show_create_function_path:
|
|
@@ -688,6 +799,12 @@ class Application(object):
|
|
|
688
799
|
|
|
689
800
|
await send(self.text_response_dict)
|
|
690
801
|
|
|
802
|
+
# Return function info
|
|
803
|
+
elif method == 'GET' and (path == self.show_function_info_path or not path):
|
|
804
|
+
functions = self.get_function_info()
|
|
805
|
+
body = json.dumps(dict(functions=functions)).encode('utf-8')
|
|
806
|
+
await send(self.text_response_dict)
|
|
807
|
+
|
|
691
808
|
# Path not found
|
|
692
809
|
else:
|
|
693
810
|
body = b''
|
|
@@ -725,21 +842,78 @@ class Application(object):
|
|
|
725
842
|
"""Locate all current functions and links belonging to this app."""
|
|
726
843
|
funcs, links = set(), set()
|
|
727
844
|
cur.execute('SHOW FUNCTIONS')
|
|
728
|
-
for
|
|
845
|
+
for row in list(cur):
|
|
846
|
+
name, ftype, link = row[0], row[1], row[-1]
|
|
729
847
|
# Only look at external functions
|
|
730
848
|
if 'external' not in ftype.lower():
|
|
731
849
|
continue
|
|
732
850
|
# See if function URL matches url
|
|
733
851
|
cur.execute(f'SHOW CREATE FUNCTION `{name}`')
|
|
734
852
|
for fname, _, code, *_ in list(cur):
|
|
735
|
-
m = re.search(r" (?:\w+) SERVICE '([^']+)'", code)
|
|
853
|
+
m = re.search(r" (?:\w+) (?:SERVICE|MANAGED) '([^']+)'", code)
|
|
736
854
|
if m and m.group(1) == self.url:
|
|
737
855
|
funcs.add(fname)
|
|
738
856
|
if link and re.match(r'^py_ext_func_link_\S{14}$', link):
|
|
739
857
|
links.add(link)
|
|
740
858
|
return funcs, links
|
|
741
859
|
|
|
742
|
-
def
|
|
860
|
+
def get_function_info(
|
|
861
|
+
self,
|
|
862
|
+
func_name: Optional[str] = None,
|
|
863
|
+
) -> Dict[str, Any]:
|
|
864
|
+
"""
|
|
865
|
+
Return the functions and function signature information.
|
|
866
|
+
Returns
|
|
867
|
+
-------
|
|
868
|
+
Dict[str, Any]
|
|
869
|
+
"""
|
|
870
|
+
functions = {}
|
|
871
|
+
no_default = object()
|
|
872
|
+
|
|
873
|
+
for key, (_, info) in self.endpoints.items():
|
|
874
|
+
if not func_name or key == func_name:
|
|
875
|
+
sig = info['signature']
|
|
876
|
+
args = []
|
|
877
|
+
|
|
878
|
+
# Function arguments
|
|
879
|
+
for a in sig.get('args', []):
|
|
880
|
+
dtype = a['dtype']
|
|
881
|
+
nullable = '?' in dtype
|
|
882
|
+
args.append(
|
|
883
|
+
dict(
|
|
884
|
+
name=a['name'],
|
|
885
|
+
dtype=dtype.replace('?', ''),
|
|
886
|
+
nullable=nullable,
|
|
887
|
+
),
|
|
888
|
+
)
|
|
889
|
+
if a.get('default', no_default) is not no_default:
|
|
890
|
+
args[-1]['default'] = a['default']
|
|
891
|
+
|
|
892
|
+
# Return values
|
|
893
|
+
ret = sig.get('returns', [])
|
|
894
|
+
returns = []
|
|
895
|
+
|
|
896
|
+
for a in ret:
|
|
897
|
+
dtype = a['dtype']
|
|
898
|
+
nullable = '?' in dtype
|
|
899
|
+
returns.append(
|
|
900
|
+
dict(
|
|
901
|
+
dtype=dtype.replace('?', ''),
|
|
902
|
+
nullable=nullable,
|
|
903
|
+
),
|
|
904
|
+
)
|
|
905
|
+
if a.get('name', None):
|
|
906
|
+
returns[-1]['name'] = a['name']
|
|
907
|
+
if a.get('default', no_default) is not no_default:
|
|
908
|
+
returns[-1]['default'] = a['default']
|
|
909
|
+
|
|
910
|
+
functions[sig['name']] = dict(
|
|
911
|
+
args=args, returns=returns, function_type=info['function_type'],
|
|
912
|
+
)
|
|
913
|
+
|
|
914
|
+
return functions
|
|
915
|
+
|
|
916
|
+
def get_create_functions(
|
|
743
917
|
self,
|
|
744
918
|
replace: bool = False,
|
|
745
919
|
) -> List[str]:
|
|
@@ -807,7 +981,7 @@ class Application(object):
|
|
|
807
981
|
cur.execute(f'DROP FUNCTION IF EXISTS `{fname}`')
|
|
808
982
|
for link in links:
|
|
809
983
|
cur.execute(f'DROP LINK {link}')
|
|
810
|
-
for func in self.
|
|
984
|
+
for func in self.get_create_functions(replace=replace):
|
|
811
985
|
cur.execute(func)
|
|
812
986
|
|
|
813
987
|
def drop_functions(
|
|
@@ -1135,6 +1309,22 @@ def main(argv: Optional[List[str]] = None) -> None:
|
|
|
1135
1309
|
),
|
|
1136
1310
|
help='logging level',
|
|
1137
1311
|
)
|
|
1312
|
+
parser.add_argument(
|
|
1313
|
+
'--name-prefix', metavar='name_prefix',
|
|
1314
|
+
default=defaults.get(
|
|
1315
|
+
'name_prefix',
|
|
1316
|
+
get_option('external_function.name_prefix'),
|
|
1317
|
+
),
|
|
1318
|
+
help='Prefix to add to function names',
|
|
1319
|
+
)
|
|
1320
|
+
parser.add_argument(
|
|
1321
|
+
'--name-suffix', metavar='name_suffix',
|
|
1322
|
+
default=defaults.get(
|
|
1323
|
+
'name_suffix',
|
|
1324
|
+
get_option('external_function.name_suffix'),
|
|
1325
|
+
),
|
|
1326
|
+
help='Suffix to add to function names',
|
|
1327
|
+
)
|
|
1138
1328
|
parser.add_argument(
|
|
1139
1329
|
'functions', metavar='module.or.func.path', nargs='*',
|
|
1140
1330
|
help='functions or modules to export in UDF server',
|
|
@@ -1217,6 +1407,11 @@ def main(argv: Optional[List[str]] = None) -> None:
|
|
|
1217
1407
|
or defaults.get('replace_existing') \
|
|
1218
1408
|
or get_option('external_function.replace_existing')
|
|
1219
1409
|
|
|
1410
|
+
# Substitute in host / port if specified
|
|
1411
|
+
if args.host != defaults.get('host') or args.port != defaults.get('port'):
|
|
1412
|
+
u = urllib.parse.urlparse(args.url)
|
|
1413
|
+
args.url = u._replace(netloc=f'{args.host}:{args.port}').geturl()
|
|
1414
|
+
|
|
1220
1415
|
# Create application from functions / module
|
|
1221
1416
|
app = Application(
|
|
1222
1417
|
functions=args.functions,
|
|
@@ -1227,9 +1422,11 @@ def main(argv: Optional[List[str]] = None) -> None:
|
|
|
1227
1422
|
link_config=json.loads(args.link_config) or None,
|
|
1228
1423
|
link_credentials=json.loads(args.link_credentials) or None,
|
|
1229
1424
|
app_mode='remote',
|
|
1425
|
+
name_prefix=args.name_prefix,
|
|
1426
|
+
name_suffix=args.name_suffix,
|
|
1230
1427
|
)
|
|
1231
1428
|
|
|
1232
|
-
funcs = app.
|
|
1429
|
+
funcs = app.get_create_functions(replace=args.replace_existing)
|
|
1233
1430
|
if not funcs:
|
|
1234
1431
|
raise RuntimeError('no functions specified')
|
|
1235
1432
|
|
|
@@ -1249,6 +1446,7 @@ def main(argv: Optional[List[str]] = None) -> None:
|
|
|
1249
1446
|
host=args.host or None,
|
|
1250
1447
|
port=args.port or None,
|
|
1251
1448
|
log_level=args.log_level,
|
|
1449
|
+
lifespan='off',
|
|
1252
1450
|
).items() if v is not None
|
|
1253
1451
|
}
|
|
1254
1452
|
|