singlestoredb 1.0.4__cp38-abi3-win_amd64.whl → 1.2.0__cp38-abi3-win_amd64.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/config.py +131 -0
- singlestoredb/connection.py +3 -0
- singlestoredb/converters.py +390 -0
- singlestoredb/functions/dtypes.py +5 -198
- singlestoredb/functions/ext/__init__.py +0 -1
- singlestoredb/functions/ext/asgi.py +671 -153
- singlestoredb/functions/ext/json.py +2 -2
- singlestoredb/functions/ext/mmap.py +174 -67
- singlestoredb/functions/ext/rowdat_1.py +2 -2
- singlestoredb/functions/ext/utils.py +169 -0
- singlestoredb/fusion/handler.py +115 -9
- singlestoredb/fusion/handlers/stage.py +246 -13
- singlestoredb/fusion/handlers/workspace.py +417 -14
- singlestoredb/fusion/registry.py +86 -1
- singlestoredb/http/connection.py +40 -2
- singlestoredb/management/__init__.py +1 -0
- singlestoredb/management/organization.py +4 -0
- singlestoredb/management/utils.py +2 -2
- singlestoredb/management/workspace.py +79 -6
- singlestoredb/mysql/connection.py +81 -0
- singlestoredb/mysql/constants/EXTENDED_TYPE.py +3 -0
- singlestoredb/mysql/constants/FIELD_TYPE.py +16 -0
- singlestoredb/mysql/constants/VECTOR_TYPE.py +6 -0
- singlestoredb/mysql/cursors.py +177 -4
- singlestoredb/mysql/protocol.py +50 -1
- singlestoredb/notebook/__init__.py +15 -0
- singlestoredb/notebook/_objects.py +212 -0
- singlestoredb/tests/test.sql +259 -0
- singlestoredb/tests/test_connection.py +1715 -133
- singlestoredb/tests/test_ext_func.py +2 -2
- singlestoredb/tests/test_ext_func_data.py +1 -1
- singlestoredb/utils/dtypes.py +205 -0
- singlestoredb/utils/results.py +367 -14
- {singlestoredb-1.0.4.dist-info → singlestoredb-1.2.0.dist-info}/METADATA +2 -1
- {singlestoredb-1.0.4.dist-info → singlestoredb-1.2.0.dist-info}/RECORD +41 -35
- {singlestoredb-1.0.4.dist-info → singlestoredb-1.2.0.dist-info}/LICENSE +0 -0
- {singlestoredb-1.0.4.dist-info → singlestoredb-1.2.0.dist-info}/WHEEL +0 -0
- {singlestoredb-1.0.4.dist-info → singlestoredb-1.2.0.dist-info}/entry_points.txt +0 -0
- {singlestoredb-1.0.4.dist-info → singlestoredb-1.2.0.dist-info}/top_level.txt +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
|
|
2
|
+
"""
|
|
3
3
|
Web application for SingleStoreDB external functions.
|
|
4
4
|
|
|
5
5
|
This module supplies a function that can create web apps intended for use
|
|
@@ -18,17 +18,26 @@ An example of starting a server is shown below.
|
|
|
18
18
|
|
|
19
19
|
Example
|
|
20
20
|
-------
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
> SINGLESTOREDB_EXT_FUNCTIONS='myfuncs.[percentile_90,percentile_95]' \
|
|
22
|
+
python3 -m singlestoredb.functions.ext.asgi
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
"""
|
|
25
|
+
import argparse
|
|
26
|
+
import asyncio
|
|
25
27
|
import importlib.util
|
|
26
28
|
import io
|
|
27
29
|
import itertools
|
|
28
30
|
import json
|
|
31
|
+
import logging
|
|
29
32
|
import os
|
|
30
33
|
import re
|
|
31
34
|
import secrets
|
|
35
|
+
import sys
|
|
36
|
+
import tempfile
|
|
37
|
+
import textwrap
|
|
38
|
+
import urllib
|
|
39
|
+
import zipfile
|
|
40
|
+
import zipimport
|
|
32
41
|
from types import ModuleType
|
|
33
42
|
from typing import Any
|
|
34
43
|
from typing import Awaitable
|
|
@@ -45,11 +54,24 @@ from typing import Union
|
|
|
45
54
|
from . import arrow
|
|
46
55
|
from . import json as jdata
|
|
47
56
|
from . import rowdat_1
|
|
57
|
+
from . import utils
|
|
48
58
|
from ... import connection
|
|
59
|
+
from ... import manage_workspaces
|
|
60
|
+
from ...config import get_option
|
|
49
61
|
from ...mysql.constants import FIELD_TYPE as ft
|
|
50
62
|
from ..signature import get_signature
|
|
51
63
|
from ..signature import signature_to_sql
|
|
52
64
|
|
|
65
|
+
try:
|
|
66
|
+
import cloudpickle
|
|
67
|
+
has_cloudpickle = True
|
|
68
|
+
except ImportError:
|
|
69
|
+
has_cloudpickle = False
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
logger = utils.get_logger('singlestoredb.functions.ext.asgi')
|
|
73
|
+
|
|
74
|
+
|
|
53
75
|
# If a number of processes is specified, create a pool of workers
|
|
54
76
|
num_processes = max(0, int(os.environ.get('SINGLESTOREDB_EXT_NUM_PROCESSES', 0)))
|
|
55
77
|
if num_processes > 1:
|
|
@@ -81,7 +103,7 @@ rowdat_1_type_map = {
|
|
|
81
103
|
|
|
82
104
|
|
|
83
105
|
def get_func_names(funcs: str) -> List[Tuple[str, str]]:
|
|
84
|
-
|
|
106
|
+
"""
|
|
85
107
|
Parse all function names from string.
|
|
86
108
|
|
|
87
109
|
Parameters
|
|
@@ -97,7 +119,7 @@ def get_func_names(funcs: str) -> List[Tuple[str, str]]:
|
|
|
97
119
|
List[Tuple[str]] : a list of tuples containing the names and aliases
|
|
98
120
|
of each function.
|
|
99
121
|
|
|
100
|
-
|
|
122
|
+
"""
|
|
101
123
|
if funcs.startswith('['):
|
|
102
124
|
func_names = funcs.replace('[', '').replace(']', '').split(',')
|
|
103
125
|
func_names = [x.strip() for x in func_names]
|
|
@@ -114,8 +136,11 @@ def get_func_names(funcs: str) -> List[Tuple[str, str]]:
|
|
|
114
136
|
return out
|
|
115
137
|
|
|
116
138
|
|
|
117
|
-
def make_func(
|
|
118
|
-
|
|
139
|
+
def make_func(
|
|
140
|
+
name: str,
|
|
141
|
+
func: Callable[..., Any],
|
|
142
|
+
) -> Tuple[Callable[..., Any], Dict[str, Any]]:
|
|
143
|
+
"""
|
|
119
144
|
Make a function endpoint.
|
|
120
145
|
|
|
121
146
|
Parameters
|
|
@@ -127,12 +152,13 @@ def make_func(name: str, func: Callable[..., Any]) -> Callable[..., Any]:
|
|
|
127
152
|
|
|
128
153
|
Returns
|
|
129
154
|
-------
|
|
130
|
-
Callable
|
|
155
|
+
(Callable, Dict[str, Any])
|
|
131
156
|
|
|
132
|
-
|
|
157
|
+
"""
|
|
133
158
|
attrs = getattr(func, '_singlestoredb_attrs', {})
|
|
134
159
|
data_format = attrs.get('data_format') or 'python'
|
|
135
160
|
include_masks = attrs.get('include_masks', False)
|
|
161
|
+
info: Dict[str, Any] = {}
|
|
136
162
|
|
|
137
163
|
if data_format == 'python':
|
|
138
164
|
async def do_func(
|
|
@@ -165,10 +191,10 @@ def make_func(name: str, func: Callable[..., Any]) -> Callable[..., Any]:
|
|
|
165
191
|
sig = get_signature(func, name=name)
|
|
166
192
|
|
|
167
193
|
# Store signature for generating CREATE FUNCTION calls
|
|
168
|
-
|
|
194
|
+
info['signature'] = sig
|
|
169
195
|
|
|
170
196
|
# Set data format
|
|
171
|
-
|
|
197
|
+
info['data_format'] = data_format
|
|
172
198
|
|
|
173
199
|
# Setup argument types for rowdat_1 parser
|
|
174
200
|
colspec = []
|
|
@@ -177,36 +203,19 @@ def make_func(name: str, func: Callable[..., Any]) -> Callable[..., Any]:
|
|
|
177
203
|
if dtype not in rowdat_1_type_map:
|
|
178
204
|
raise TypeError(f'no data type mapping for {dtype}')
|
|
179
205
|
colspec.append((x['name'], rowdat_1_type_map[dtype]))
|
|
180
|
-
|
|
206
|
+
info['colspec'] = colspec
|
|
181
207
|
|
|
182
208
|
# Setup return type
|
|
183
209
|
dtype = sig['returns']['dtype'].replace('?', '')
|
|
184
210
|
if dtype not in rowdat_1_type_map:
|
|
185
211
|
raise TypeError(f'no data type mapping for {dtype}')
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
return do_func
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
Union[
|
|
194
|
-
str,
|
|
195
|
-
Iterable[str],
|
|
196
|
-
Callable[..., Any],
|
|
197
|
-
Iterable[Callable[..., Any]],
|
|
198
|
-
ModuleType,
|
|
199
|
-
Iterable[ModuleType],
|
|
200
|
-
]
|
|
201
|
-
] = None,
|
|
202
|
-
app_mode: str = 'remote',
|
|
203
|
-
url: str = 'http://localhost:8000/invoke',
|
|
204
|
-
data_format: str = 'rowdat_1',
|
|
205
|
-
data_version: str = '1.0',
|
|
206
|
-
link_config: Optional[Dict[str, Any]] = None,
|
|
207
|
-
link_credentials: Optional[Dict[str, Any]] = None,
|
|
208
|
-
) -> Callable[..., Any]:
|
|
209
|
-
'''
|
|
212
|
+
info['returns'] = [rowdat_1_type_map[dtype]]
|
|
213
|
+
|
|
214
|
+
return do_func, info
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class Application(object):
|
|
218
|
+
"""
|
|
210
219
|
Create an external function application.
|
|
211
220
|
|
|
212
221
|
If `functions` is None, the environment is searched for function
|
|
@@ -231,6 +240,10 @@ def create_app( # noqa: C901
|
|
|
231
240
|
The format of the data rows: 'rowdat_1' or 'json'
|
|
232
241
|
data_version : str, optional
|
|
233
242
|
The version of the call format to expect: '1.0'
|
|
243
|
+
link_name : str, optional
|
|
244
|
+
The link name to use for the external function application. This is
|
|
245
|
+
only for pre-existing links, and can only be used without
|
|
246
|
+
``link_config`` and ``link_credentials``.
|
|
234
247
|
link_config : Dict[str, Any], optional
|
|
235
248
|
The CONFIG section of a LINK definition. This dictionary gets
|
|
236
249
|
converted to JSON for the CREATE LINK call.
|
|
@@ -238,77 +251,7 @@ def create_app( # noqa: C901
|
|
|
238
251
|
The CREDENTIALS section of a LINK definition. This dictionary gets
|
|
239
252
|
converted to JSON for the CREATE LINK call.
|
|
240
253
|
|
|
241
|
-
|
|
242
|
-
-------
|
|
243
|
-
Callable : the application request handler
|
|
244
|
-
|
|
245
|
-
'''
|
|
246
|
-
|
|
247
|
-
# List of functions specs
|
|
248
|
-
specs: List[Union[str, Callable[..., Any], ModuleType]] = []
|
|
249
|
-
|
|
250
|
-
# Look up Python function specifications
|
|
251
|
-
if functions is None:
|
|
252
|
-
env_vars = [
|
|
253
|
-
x for x in os.environ.keys()
|
|
254
|
-
if x.startswith('SINGLESTOREDB_EXT_FUNCTIONS')
|
|
255
|
-
]
|
|
256
|
-
if env_vars:
|
|
257
|
-
specs = [os.environ[x] for x in env_vars]
|
|
258
|
-
else:
|
|
259
|
-
import __main__
|
|
260
|
-
specs = [__main__]
|
|
261
|
-
|
|
262
|
-
elif isinstance(functions, ModuleType):
|
|
263
|
-
specs = [functions]
|
|
264
|
-
|
|
265
|
-
elif isinstance(functions, str):
|
|
266
|
-
specs = [functions]
|
|
267
|
-
|
|
268
|
-
elif callable(functions):
|
|
269
|
-
specs = [functions]
|
|
270
|
-
|
|
271
|
-
else:
|
|
272
|
-
specs = list(functions)
|
|
273
|
-
|
|
274
|
-
# Add functions to application
|
|
275
|
-
endpoints = dict()
|
|
276
|
-
for funcs in itertools.chain(specs):
|
|
277
|
-
|
|
278
|
-
if isinstance(funcs, str):
|
|
279
|
-
# Module name
|
|
280
|
-
if importlib.util.find_spec(funcs) is not None:
|
|
281
|
-
items = importlib.import_module(funcs)
|
|
282
|
-
for x in vars(items).values():
|
|
283
|
-
if not hasattr(x, '_singlestoredb_attrs'):
|
|
284
|
-
continue
|
|
285
|
-
name = x._singlestoredb_attrs.get('name', x.__name__)
|
|
286
|
-
func = make_func(name, x)
|
|
287
|
-
endpoints[name.encode('utf-8')] = func
|
|
288
|
-
|
|
289
|
-
# Fully qualified function name
|
|
290
|
-
else:
|
|
291
|
-
pkg_path, func_names = funcs.rsplit('.', 1)
|
|
292
|
-
pkg = importlib.import_module(pkg_path)
|
|
293
|
-
|
|
294
|
-
# Add endpoint for each exported function
|
|
295
|
-
for name, alias in get_func_names(func_names):
|
|
296
|
-
item = getattr(pkg, name)
|
|
297
|
-
func = make_func(alias, item)
|
|
298
|
-
endpoints[alias.encode('utf-8')] = func
|
|
299
|
-
|
|
300
|
-
elif isinstance(funcs, ModuleType):
|
|
301
|
-
for x in vars(funcs).values():
|
|
302
|
-
if not hasattr(x, '_singlestoredb_attrs'):
|
|
303
|
-
continue
|
|
304
|
-
name = x._singlestoredb_attrs.get('name', x.__name__)
|
|
305
|
-
func = make_func(name, x)
|
|
306
|
-
endpoints[name.encode('utf-8')] = func
|
|
307
|
-
|
|
308
|
-
else:
|
|
309
|
-
alias = funcs.__name__
|
|
310
|
-
func = make_func(alias, funcs)
|
|
311
|
-
endpoints[alias.encode('utf-8')] = func
|
|
254
|
+
"""
|
|
312
255
|
|
|
313
256
|
# Plain text response start
|
|
314
257
|
text_response_dict: Dict[str, Any] = dict(
|
|
@@ -432,7 +375,130 @@ def create_app( # noqa: C901
|
|
|
432
375
|
invoke_path = ('invoke',)
|
|
433
376
|
show_create_function_path = ('show', 'create_function')
|
|
434
377
|
|
|
435
|
-
|
|
378
|
+
def __init__(
|
|
379
|
+
self,
|
|
380
|
+
functions: Optional[
|
|
381
|
+
Union[
|
|
382
|
+
str,
|
|
383
|
+
Iterable[str],
|
|
384
|
+
Callable[..., Any],
|
|
385
|
+
Iterable[Callable[..., Any]],
|
|
386
|
+
ModuleType,
|
|
387
|
+
Iterable[ModuleType],
|
|
388
|
+
]
|
|
389
|
+
] = None,
|
|
390
|
+
app_mode: str = get_option('external_function.app_mode'),
|
|
391
|
+
url: str = get_option('external_function.url'),
|
|
392
|
+
data_format: str = get_option('external_function.data_format'),
|
|
393
|
+
data_version: str = get_option('external_function.data_version'),
|
|
394
|
+
link_name: Optional[str] = get_option('external_function.link_name'),
|
|
395
|
+
link_config: Optional[Dict[str, Any]] = None,
|
|
396
|
+
link_credentials: Optional[Dict[str, Any]] = None,
|
|
397
|
+
) -> None:
|
|
398
|
+
if link_name and (link_config or link_credentials):
|
|
399
|
+
raise ValueError(
|
|
400
|
+
'`link_name` can not be used with `link_config` or `link_credentials`',
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
if link_config is None:
|
|
404
|
+
link_config = json.loads(
|
|
405
|
+
get_option('external_function.link_config') or '{}',
|
|
406
|
+
) or None
|
|
407
|
+
|
|
408
|
+
if link_credentials is None:
|
|
409
|
+
link_credentials = json.loads(
|
|
410
|
+
get_option('external_function.link_credentials') or '{}',
|
|
411
|
+
) or None
|
|
412
|
+
|
|
413
|
+
# List of functions specs
|
|
414
|
+
specs: List[Union[str, Callable[..., Any], ModuleType]] = []
|
|
415
|
+
|
|
416
|
+
# Look up Python function specifications
|
|
417
|
+
if functions is None:
|
|
418
|
+
env_vars = [
|
|
419
|
+
x for x in os.environ.keys()
|
|
420
|
+
if x.startswith('SINGLESTOREDB_EXT_FUNCTIONS')
|
|
421
|
+
]
|
|
422
|
+
if env_vars:
|
|
423
|
+
specs = [os.environ[x] for x in env_vars]
|
|
424
|
+
else:
|
|
425
|
+
import __main__
|
|
426
|
+
specs = [__main__]
|
|
427
|
+
|
|
428
|
+
elif isinstance(functions, ModuleType):
|
|
429
|
+
specs = [functions]
|
|
430
|
+
|
|
431
|
+
elif isinstance(functions, str):
|
|
432
|
+
specs = [functions]
|
|
433
|
+
|
|
434
|
+
elif callable(functions):
|
|
435
|
+
specs = [functions]
|
|
436
|
+
|
|
437
|
+
else:
|
|
438
|
+
specs = list(functions)
|
|
439
|
+
|
|
440
|
+
# Add functions to application
|
|
441
|
+
endpoints = dict()
|
|
442
|
+
external_functions = dict()
|
|
443
|
+
for funcs in itertools.chain(specs):
|
|
444
|
+
|
|
445
|
+
if isinstance(funcs, str):
|
|
446
|
+
# Module name
|
|
447
|
+
if importlib.util.find_spec(funcs) is not None:
|
|
448
|
+
items = importlib.import_module(funcs)
|
|
449
|
+
for x in vars(items).values():
|
|
450
|
+
if not hasattr(x, '_singlestoredb_attrs'):
|
|
451
|
+
continue
|
|
452
|
+
name = x._singlestoredb_attrs.get('name', x.__name__)
|
|
453
|
+
external_functions[x.__name__] = x
|
|
454
|
+
func, info = make_func(name, x)
|
|
455
|
+
endpoints[name.encode('utf-8')] = func, info
|
|
456
|
+
|
|
457
|
+
# Fully qualified function name
|
|
458
|
+
elif '.' in funcs:
|
|
459
|
+
pkg_path, func_names = funcs.rsplit('.', 1)
|
|
460
|
+
pkg = importlib.import_module(pkg_path)
|
|
461
|
+
|
|
462
|
+
if pkg is None:
|
|
463
|
+
raise RuntimeError(f'Could not locate module: {pkg}')
|
|
464
|
+
|
|
465
|
+
# Add endpoint for each exported function
|
|
466
|
+
for name, alias in get_func_names(func_names):
|
|
467
|
+
item = getattr(pkg, name)
|
|
468
|
+
external_functions[name] = item
|
|
469
|
+
func, info = make_func(alias, item)
|
|
470
|
+
endpoints[alias.encode('utf-8')] = func, info
|
|
471
|
+
|
|
472
|
+
else:
|
|
473
|
+
raise RuntimeError(f'Could not locate module: {funcs}')
|
|
474
|
+
|
|
475
|
+
elif isinstance(funcs, ModuleType):
|
|
476
|
+
for x in vars(funcs).values():
|
|
477
|
+
if not hasattr(x, '_singlestoredb_attrs'):
|
|
478
|
+
continue
|
|
479
|
+
name = x._singlestoredb_attrs.get('name', x.__name__)
|
|
480
|
+
external_functions[x.__name__] = x
|
|
481
|
+
func, info = make_func(name, x)
|
|
482
|
+
endpoints[name.encode('utf-8')] = func, info
|
|
483
|
+
|
|
484
|
+
else:
|
|
485
|
+
alias = funcs.__name__
|
|
486
|
+
external_functions[funcs.__name__] = funcs
|
|
487
|
+
func, info = make_func(alias, funcs)
|
|
488
|
+
endpoints[alias.encode('utf-8')] = func, info
|
|
489
|
+
|
|
490
|
+
self.app_mode = app_mode
|
|
491
|
+
self.url = url
|
|
492
|
+
self.data_format = data_format
|
|
493
|
+
self.data_version = data_version
|
|
494
|
+
self.link_name = link_name
|
|
495
|
+
self.link_config = link_config
|
|
496
|
+
self.link_credentials = link_credentials
|
|
497
|
+
self.endpoints = endpoints
|
|
498
|
+
self.external_functions = external_functions
|
|
499
|
+
|
|
500
|
+
async def __call__(
|
|
501
|
+
self,
|
|
436
502
|
scope: Dict[str, Any],
|
|
437
503
|
receive: Callable[..., Awaitable[Any]],
|
|
438
504
|
send: Callable[..., Awaitable[Any]],
|
|
@@ -462,11 +528,16 @@ def create_app( # noqa: C901
|
|
|
462
528
|
)
|
|
463
529
|
accepts = headers.get(b'accepts', content_type)
|
|
464
530
|
func_name = headers.get(b's2-ef-name', b'')
|
|
465
|
-
|
|
531
|
+
func_endpoint = self.endpoints.get(func_name)
|
|
532
|
+
|
|
533
|
+
func = None
|
|
534
|
+
func_info: Dict[str, Any] = {}
|
|
535
|
+
if func_endpoint is not None:
|
|
536
|
+
func, func_info = func_endpoint
|
|
466
537
|
|
|
467
538
|
# Call the endpoint
|
|
468
|
-
if method == 'POST' and func is not None and path == invoke_path:
|
|
469
|
-
data_format =
|
|
539
|
+
if method == 'POST' and func is not None and path == self.invoke_path:
|
|
540
|
+
data_format = func_info['data_format']
|
|
470
541
|
data = []
|
|
471
542
|
more_body = True
|
|
472
543
|
while more_body:
|
|
@@ -475,57 +546,61 @@ def create_app( # noqa: C901
|
|
|
475
546
|
more_body = request.get('more_body', False)
|
|
476
547
|
|
|
477
548
|
data_version = headers.get(b's2-ef-version', b'')
|
|
478
|
-
input_handler = handlers[(content_type, data_version, data_format)]
|
|
479
|
-
output_handler = handlers[(accepts, data_version, data_format)]
|
|
549
|
+
input_handler = self.handlers[(content_type, data_version, data_format)]
|
|
550
|
+
output_handler = self.handlers[(accepts, data_version, data_format)]
|
|
480
551
|
|
|
481
552
|
out = await func(
|
|
482
|
-
*input_handler['load'](
|
|
483
|
-
|
|
553
|
+
*input_handler['load']( # type: ignore
|
|
554
|
+
func_info['colspec'], b''.join(data),
|
|
484
555
|
),
|
|
485
556
|
)
|
|
486
|
-
body = output_handler['dump'](
|
|
557
|
+
body = output_handler['dump'](func_info['returns'], *out) # type: ignore
|
|
487
558
|
|
|
488
559
|
await send(output_handler['response'])
|
|
489
560
|
|
|
490
561
|
# Handle api reflection
|
|
491
|
-
elif method == 'GET' and path == show_create_function_path:
|
|
562
|
+
elif method == 'GET' and path == self.show_create_function_path:
|
|
492
563
|
host = headers.get(b'host', b'localhost:80')
|
|
493
564
|
reflected_url = f'{scope["scheme"]}://{host.decode("utf-8")}/invoke'
|
|
494
565
|
data_format = 'json' if b'json' in content_type else 'rowdat_1'
|
|
495
566
|
|
|
496
567
|
syntax = []
|
|
497
|
-
for key, endpoint in endpoints.items():
|
|
568
|
+
for key, (endpoint, endpoint_info) in self.endpoints.items():
|
|
498
569
|
if not func_name or key == func_name:
|
|
499
570
|
syntax.append(
|
|
500
571
|
signature_to_sql(
|
|
501
|
-
|
|
502
|
-
url=url or reflected_url,
|
|
572
|
+
endpoint_info['signature'],
|
|
573
|
+
url=self.url or reflected_url,
|
|
503
574
|
data_format=data_format,
|
|
504
575
|
),
|
|
505
576
|
)
|
|
506
577
|
body = '\n'.join(syntax).encode('utf-8')
|
|
507
578
|
|
|
508
|
-
await send(text_response_dict)
|
|
579
|
+
await send(self.text_response_dict)
|
|
509
580
|
|
|
510
581
|
# Path not found
|
|
511
582
|
else:
|
|
512
583
|
body = b''
|
|
513
|
-
await send(path_not_found_response_dict)
|
|
584
|
+
await send(self.path_not_found_response_dict)
|
|
514
585
|
|
|
515
586
|
# Send body
|
|
516
|
-
out = body_response_dict.copy()
|
|
587
|
+
out = self.body_response_dict.copy()
|
|
517
588
|
out['body'] = body
|
|
518
589
|
await send(out)
|
|
519
590
|
|
|
520
591
|
def _create_link(
|
|
592
|
+
self,
|
|
521
593
|
config: Optional[Dict[str, Any]],
|
|
522
594
|
credentials: Optional[Dict[str, Any]],
|
|
523
595
|
) -> Tuple[str, str]:
|
|
524
596
|
"""Generate CREATE LINK command."""
|
|
597
|
+
if self.link_name:
|
|
598
|
+
return self.link_name, ''
|
|
599
|
+
|
|
525
600
|
if not config and not credentials:
|
|
526
601
|
return '', ''
|
|
527
602
|
|
|
528
|
-
link_name = f'
|
|
603
|
+
link_name = f'py_ext_func_link_{secrets.token_hex(14)}'
|
|
529
604
|
out = [f'CREATE LINK {link_name} AS HTTP']
|
|
530
605
|
|
|
531
606
|
if config:
|
|
@@ -536,7 +611,7 @@ def create_app( # noqa: C901
|
|
|
536
611
|
|
|
537
612
|
return link_name, ' '.join(out) + ';'
|
|
538
613
|
|
|
539
|
-
def _locate_app_functions(cur: Any) -> Tuple[Set[str], Set[str]]:
|
|
614
|
+
def _locate_app_functions(self, cur: Any) -> Tuple[Set[str], Set[str]]:
|
|
540
615
|
"""Locate all current functions and links belonging to this app."""
|
|
541
616
|
funcs, links = set(), set()
|
|
542
617
|
cur.execute('SHOW FUNCTIONS')
|
|
@@ -548,33 +623,46 @@ def create_app( # noqa: C901
|
|
|
548
623
|
cur.execute(f'SHOW CREATE FUNCTION `{name}`')
|
|
549
624
|
for fname, _, code, *_ in list(cur):
|
|
550
625
|
m = re.search(r" (?:\w+) SERVICE '([^']+)'", code)
|
|
551
|
-
if m and m.group(1) == url:
|
|
626
|
+
if m and m.group(1) == self.url:
|
|
552
627
|
funcs.add(fname)
|
|
553
|
-
if link:
|
|
628
|
+
if link and re.match(r'^py_ext_func_link_\S{14}$', link):
|
|
554
629
|
links.add(link)
|
|
555
630
|
return funcs, links
|
|
556
631
|
|
|
557
632
|
def show_create_functions(
|
|
633
|
+
self,
|
|
558
634
|
replace: bool = False,
|
|
559
635
|
) -> List[str]:
|
|
560
|
-
"""
|
|
561
|
-
|
|
636
|
+
"""
|
|
637
|
+
Generate CREATE FUNCTION code for all functions.
|
|
638
|
+
|
|
639
|
+
Parameters
|
|
640
|
+
----------
|
|
641
|
+
replace : bool, optional
|
|
642
|
+
Should existing functions be replaced?
|
|
643
|
+
|
|
644
|
+
Returns
|
|
645
|
+
-------
|
|
646
|
+
List[str]
|
|
647
|
+
|
|
648
|
+
"""
|
|
649
|
+
if not self.endpoints:
|
|
562
650
|
return []
|
|
563
651
|
|
|
564
652
|
out = []
|
|
565
653
|
link = ''
|
|
566
|
-
if app_mode.lower() == 'remote':
|
|
567
|
-
link, link_str = _create_link(link_config, link_credentials)
|
|
654
|
+
if self.app_mode.lower() == 'remote':
|
|
655
|
+
link, link_str = self._create_link(self.link_config, self.link_credentials)
|
|
568
656
|
if link and link_str:
|
|
569
657
|
out.append(link_str)
|
|
570
658
|
|
|
571
|
-
for key, endpoint in endpoints.items():
|
|
659
|
+
for key, (endpoint, endpoint_info) in self.endpoints.items():
|
|
572
660
|
out.append(
|
|
573
661
|
signature_to_sql(
|
|
574
|
-
|
|
575
|
-
url=url,
|
|
576
|
-
data_format=data_format,
|
|
577
|
-
app_mode=app_mode,
|
|
662
|
+
endpoint_info['signature'],
|
|
663
|
+
url=self.url,
|
|
664
|
+
data_format=self.data_format,
|
|
665
|
+
app_mode=self.app_mode,
|
|
578
666
|
replace=replace,
|
|
579
667
|
link=link or None,
|
|
580
668
|
),
|
|
@@ -582,49 +670,87 @@ def create_app( # noqa: C901
|
|
|
582
670
|
|
|
583
671
|
return out
|
|
584
672
|
|
|
585
|
-
app.show_create_functions = show_create_functions # type: ignore
|
|
586
|
-
|
|
587
673
|
def register_functions(
|
|
674
|
+
self,
|
|
588
675
|
*connection_args: Any,
|
|
589
676
|
replace: bool = False,
|
|
590
677
|
**connection_kwargs: Any,
|
|
591
678
|
) -> None:
|
|
592
|
-
"""
|
|
679
|
+
"""
|
|
680
|
+
Register functions with the database.
|
|
681
|
+
|
|
682
|
+
Parameters
|
|
683
|
+
----------
|
|
684
|
+
*connection_args : Any
|
|
685
|
+
Database connection parameters
|
|
686
|
+
replace : bool, optional
|
|
687
|
+
Should existing functions be replaced?
|
|
688
|
+
**connection_kwargs : Any
|
|
689
|
+
Database connection parameters
|
|
690
|
+
|
|
691
|
+
"""
|
|
593
692
|
with connection.connect(*connection_args, **connection_kwargs) as conn:
|
|
594
693
|
with conn.cursor() as cur:
|
|
595
694
|
if replace:
|
|
596
|
-
funcs, links = _locate_app_functions(cur)
|
|
695
|
+
funcs, links = self._locate_app_functions(cur)
|
|
597
696
|
for fname in funcs:
|
|
598
697
|
cur.execute(f'DROP FUNCTION IF EXISTS `{fname}`')
|
|
599
698
|
for link in links:
|
|
600
699
|
cur.execute(f'DROP LINK {link}')
|
|
601
|
-
for func in
|
|
700
|
+
for func in self.show_create_functions(replace=replace):
|
|
602
701
|
cur.execute(func)
|
|
603
702
|
|
|
604
|
-
app.register_functions = register_functions # type: ignore
|
|
605
|
-
|
|
606
703
|
def drop_functions(
|
|
704
|
+
self,
|
|
607
705
|
*connection_args: Any,
|
|
608
706
|
**connection_kwargs: Any,
|
|
609
707
|
) -> None:
|
|
610
|
-
"""
|
|
708
|
+
"""
|
|
709
|
+
Drop registered functions from database.
|
|
710
|
+
|
|
711
|
+
Parameters
|
|
712
|
+
----------
|
|
713
|
+
*connection_args : Any
|
|
714
|
+
Database connection parameters
|
|
715
|
+
**connection_kwargs : Any
|
|
716
|
+
Database connection parameters
|
|
717
|
+
|
|
718
|
+
"""
|
|
611
719
|
with connection.connect(*connection_args, **connection_kwargs) as conn:
|
|
612
720
|
with conn.cursor() as cur:
|
|
613
|
-
funcs, links = _locate_app_functions(cur)
|
|
721
|
+
funcs, links = self._locate_app_functions(cur)
|
|
614
722
|
for fname in funcs:
|
|
615
723
|
cur.execute(f'DROP FUNCTION IF EXISTS `{fname}`')
|
|
616
724
|
for link in links:
|
|
617
725
|
cur.execute(f'DROP LINK {link}')
|
|
618
726
|
|
|
619
|
-
app.drop_functions = drop_functions # type: ignore
|
|
620
|
-
|
|
621
727
|
async def call(
|
|
728
|
+
self,
|
|
622
729
|
name: str,
|
|
623
730
|
data_in: io.BytesIO,
|
|
624
731
|
data_out: io.BytesIO,
|
|
625
|
-
data_format: str =
|
|
626
|
-
data_version: str =
|
|
732
|
+
data_format: Optional[str] = None,
|
|
733
|
+
data_version: Optional[str] = None,
|
|
627
734
|
) -> None:
|
|
735
|
+
"""
|
|
736
|
+
Call a function in the application.
|
|
737
|
+
|
|
738
|
+
Parameters
|
|
739
|
+
----------
|
|
740
|
+
name : str
|
|
741
|
+
Name of the function to call
|
|
742
|
+
data_in : io.BytesIO
|
|
743
|
+
The input data rows
|
|
744
|
+
data_out : io.BytesIO
|
|
745
|
+
The output data rows
|
|
746
|
+
data_format : str, optional
|
|
747
|
+
The format of the input and output data
|
|
748
|
+
data_version : str, optional
|
|
749
|
+
The version of the data format
|
|
750
|
+
|
|
751
|
+
"""
|
|
752
|
+
data_format = data_format or self.data_format
|
|
753
|
+
data_version = data_version or self.data_version
|
|
628
754
|
|
|
629
755
|
async def receive() -> Dict[str, Any]:
|
|
630
756
|
return dict(body=data_in.read())
|
|
@@ -654,8 +780,400 @@ def create_app( # noqa: C901
|
|
|
654
780
|
},
|
|
655
781
|
)
|
|
656
782
|
|
|
657
|
-
await
|
|
783
|
+
await self(scope, receive, send)
|
|
784
|
+
|
|
785
|
+
def to_environment(
|
|
786
|
+
self,
|
|
787
|
+
name: str,
|
|
788
|
+
destination: str = '.',
|
|
789
|
+
version: Optional[str] = None,
|
|
790
|
+
dependencies: Optional[List[str]] = None,
|
|
791
|
+
authors: Optional[List[Dict[str, str]]] = None,
|
|
792
|
+
maintainers: Optional[List[Dict[str, str]]] = None,
|
|
793
|
+
description: Optional[str] = None,
|
|
794
|
+
container_service: Optional[Dict[str, Any]] = None,
|
|
795
|
+
external_function: Optional[Dict[str, Any]] = None,
|
|
796
|
+
external_function_remote: Optional[Dict[str, Any]] = None,
|
|
797
|
+
external_function_collocated: Optional[Dict[str, Any]] = None,
|
|
798
|
+
overwrite: bool = False,
|
|
799
|
+
) -> None:
|
|
800
|
+
"""
|
|
801
|
+
Convert application to an environment file.
|
|
802
|
+
|
|
803
|
+
Parameters
|
|
804
|
+
----------
|
|
805
|
+
name : str
|
|
806
|
+
Name of the output environment
|
|
807
|
+
destination : str, optional
|
|
808
|
+
Location of the output file
|
|
809
|
+
version : str, optional
|
|
810
|
+
Version of the package
|
|
811
|
+
dependencies : List[str], optional
|
|
812
|
+
List of dependency specifications like in a requirements.txt file
|
|
813
|
+
authors : List[Dict[str, Any]], optional
|
|
814
|
+
Dictionaries of author information. Keys may include: email, name
|
|
815
|
+
maintainers : List[Dict[str, Any]], optional
|
|
816
|
+
Dictionaries of maintainer information. Keys may include: email, name
|
|
817
|
+
description : str, optional
|
|
818
|
+
Description of package
|
|
819
|
+
container_service : Dict[str, Any], optional
|
|
820
|
+
Container service specifications
|
|
821
|
+
external_function : Dict[str, Any], optional
|
|
822
|
+
External function specifications (applies to both remote and collocated)
|
|
823
|
+
external_function_remote : Dict[str, Any], optional
|
|
824
|
+
Remote external function specifications
|
|
825
|
+
external_function_collocated : Dict[str, Any], optional
|
|
826
|
+
Collocated external function specifications
|
|
827
|
+
overwrite : bool, optional
|
|
828
|
+
Should destination file be overwritten if it exists?
|
|
829
|
+
|
|
830
|
+
"""
|
|
831
|
+
if not has_cloudpickle:
|
|
832
|
+
raise RuntimeError('the cloudpicke package is required for this operation')
|
|
833
|
+
|
|
834
|
+
# Write to temporary location if a remote destination is specified
|
|
835
|
+
tmpdir = None
|
|
836
|
+
if destination.startswith('stage://'):
|
|
837
|
+
tmpdir = tempfile.TemporaryDirectory()
|
|
838
|
+
local_path = os.path.join(tmpdir.name, f'{name}.env')
|
|
839
|
+
else:
|
|
840
|
+
local_path = os.path.join(destination, f'{name}.env')
|
|
841
|
+
if not overwrite and os.path.exists(local_path):
|
|
842
|
+
raise OSError(f'path already exists: {local_path}')
|
|
843
|
+
|
|
844
|
+
with zipfile.ZipFile(local_path, mode='w') as z:
|
|
845
|
+
# Write metadata
|
|
846
|
+
z.writestr(
|
|
847
|
+
'pyproject.toml', utils.to_toml({
|
|
848
|
+
'project': dict(
|
|
849
|
+
name=name,
|
|
850
|
+
version=version,
|
|
851
|
+
dependencies=dependencies,
|
|
852
|
+
requires_python='== ' +
|
|
853
|
+
'.'.join(str(x) for x in sys.version_info[:3]),
|
|
854
|
+
authors=authors,
|
|
855
|
+
maintainers=maintainers,
|
|
856
|
+
description=description,
|
|
857
|
+
),
|
|
858
|
+
'tool.container-service': container_service,
|
|
859
|
+
'tool.external-function': external_function,
|
|
860
|
+
'tool.external-function.remote': external_function_remote,
|
|
861
|
+
'tool.external-function.collocated': external_function_collocated,
|
|
862
|
+
}),
|
|
863
|
+
)
|
|
864
|
+
|
|
865
|
+
# Write Python package
|
|
866
|
+
z.writestr(
|
|
867
|
+
f'{name}/__init__.py',
|
|
868
|
+
textwrap.dedent(f'''
|
|
869
|
+
import pickle as _pkl
|
|
870
|
+
globals().update(
|
|
871
|
+
_pkl.loads({cloudpickle.dumps(self.external_functions)}),
|
|
872
|
+
)
|
|
873
|
+
__all__ = {list(self.external_functions.keys())}''').strip(),
|
|
874
|
+
)
|
|
875
|
+
|
|
876
|
+
# Upload to Stage as needed
|
|
877
|
+
if destination.startswith('stage://'):
|
|
878
|
+
url = urllib.parse.urlparse(re.sub(r'/+$', r'', destination) + '/')
|
|
879
|
+
if not url.path or url.path == '/':
|
|
880
|
+
raise ValueError(f'no stage path was specified: {destination}')
|
|
881
|
+
|
|
882
|
+
mgr = manage_workspaces()
|
|
883
|
+
if url.hostname:
|
|
884
|
+
wsg = mgr.get_workspace_group(url.hostname)
|
|
885
|
+
elif os.environ.get('SINGLESTOREDB_WORKSPACE_GROUP'):
|
|
886
|
+
wsg = mgr.get_workspace_group(
|
|
887
|
+
os.environ['SINGLESTOREDB_WORKSPACE_GROUP'],
|
|
888
|
+
)
|
|
889
|
+
else:
|
|
890
|
+
raise ValueError(f'no workspace group specified: {destination}')
|
|
891
|
+
|
|
892
|
+
# Make intermediate directories
|
|
893
|
+
if url.path.count('/') > 1:
|
|
894
|
+
wsg.stage.mkdirs(os.path.dirname(url.path))
|
|
895
|
+
|
|
896
|
+
wsg.stage.upload_file(
|
|
897
|
+
local_path, url.path + f'{name}.env',
|
|
898
|
+
overwrite=overwrite,
|
|
899
|
+
)
|
|
900
|
+
os.remove(local_path)
|
|
901
|
+
|
|
902
|
+
|
|
903
|
+
def main(argv: Optional[List[str]] = None) -> None:
|
|
904
|
+
"""
|
|
905
|
+
Main program for HTTP-based Python UDFs
|
|
906
|
+
|
|
907
|
+
Parameters
|
|
908
|
+
----------
|
|
909
|
+
argv : List[str], optional
|
|
910
|
+
List of command-line parameters
|
|
911
|
+
|
|
912
|
+
"""
|
|
913
|
+
try:
|
|
914
|
+
import uvicorn
|
|
915
|
+
except ImportError:
|
|
916
|
+
raise ImportError('the uvicorn package is required to run this command')
|
|
917
|
+
|
|
918
|
+
# Should we run in embedded mode (typically for Jupyter)
|
|
919
|
+
try:
|
|
920
|
+
asyncio.get_running_loop()
|
|
921
|
+
use_async = True
|
|
922
|
+
except RuntimeError:
|
|
923
|
+
use_async = False
|
|
924
|
+
|
|
925
|
+
# Temporary directory for Stage environment files
|
|
926
|
+
tmpdir = None
|
|
927
|
+
|
|
928
|
+
# Depending on whether we find an environment file specified, we
|
|
929
|
+
# may have to process the command line twice.
|
|
930
|
+
functions = []
|
|
931
|
+
defaults: Dict[str, Any] = {}
|
|
932
|
+
for i in range(2):
|
|
933
|
+
|
|
934
|
+
parser = argparse.ArgumentParser(
|
|
935
|
+
prog='python -m singlestoredb.functions.ext.asgi',
|
|
936
|
+
description='Run an HTTP-based Python UDF server',
|
|
937
|
+
)
|
|
938
|
+
parser.add_argument(
|
|
939
|
+
'--url', metavar='url',
|
|
940
|
+
default=defaults.get(
|
|
941
|
+
'url',
|
|
942
|
+
get_option('external_function.url'),
|
|
943
|
+
),
|
|
944
|
+
help='URL of the UDF server endpoint',
|
|
945
|
+
)
|
|
946
|
+
parser.add_argument(
|
|
947
|
+
'--host', metavar='host',
|
|
948
|
+
default=defaults.get(
|
|
949
|
+
'host',
|
|
950
|
+
get_option('external_function.host'),
|
|
951
|
+
),
|
|
952
|
+
help='bind socket to this host',
|
|
953
|
+
)
|
|
954
|
+
parser.add_argument(
|
|
955
|
+
'--port', metavar='port', type=int,
|
|
956
|
+
default=defaults.get(
|
|
957
|
+
'port',
|
|
958
|
+
get_option('external_function.port'),
|
|
959
|
+
),
|
|
960
|
+
help='bind socket to this port',
|
|
961
|
+
)
|
|
962
|
+
parser.add_argument(
|
|
963
|
+
'--db', metavar='conn-str',
|
|
964
|
+
default=defaults.get(
|
|
965
|
+
'connection',
|
|
966
|
+
get_option('external_function.connection'),
|
|
967
|
+
),
|
|
968
|
+
help='connection string to use for registering functions',
|
|
969
|
+
)
|
|
970
|
+
parser.add_argument(
|
|
971
|
+
'--replace-existing', action='store_true',
|
|
972
|
+
help='should existing functions of the same name '
|
|
973
|
+
'in the database be replaced?',
|
|
974
|
+
)
|
|
975
|
+
parser.add_argument(
|
|
976
|
+
'--data-format', metavar='format',
|
|
977
|
+
default=defaults.get(
|
|
978
|
+
'data_format',
|
|
979
|
+
get_option('external_function.data_format'),
|
|
980
|
+
),
|
|
981
|
+
choices=['rowdat_1', 'json'],
|
|
982
|
+
help='format of the data rows',
|
|
983
|
+
)
|
|
984
|
+
parser.add_argument(
|
|
985
|
+
'--data-version', metavar='version',
|
|
986
|
+
default=defaults.get(
|
|
987
|
+
'data_version',
|
|
988
|
+
get_option('external_function.data_version'),
|
|
989
|
+
),
|
|
990
|
+
help='version of the data row format',
|
|
991
|
+
)
|
|
992
|
+
parser.add_argument(
|
|
993
|
+
'--link-name', metavar='name',
|
|
994
|
+
default=defaults.get(
|
|
995
|
+
'link_name',
|
|
996
|
+
get_option('external_function.link_name'),
|
|
997
|
+
) or '',
|
|
998
|
+
help='name of the link to use for connections',
|
|
999
|
+
)
|
|
1000
|
+
parser.add_argument(
|
|
1001
|
+
'--link-config', metavar='json',
|
|
1002
|
+
default=str(
|
|
1003
|
+
defaults.get(
|
|
1004
|
+
'link_config',
|
|
1005
|
+
get_option('external_function.link_config'),
|
|
1006
|
+
) or '{}',
|
|
1007
|
+
),
|
|
1008
|
+
help='link config in JSON format',
|
|
1009
|
+
)
|
|
1010
|
+
parser.add_argument(
|
|
1011
|
+
'--link-credentials', metavar='json',
|
|
1012
|
+
default=str(
|
|
1013
|
+
defaults.get(
|
|
1014
|
+
'link_credentials',
|
|
1015
|
+
get_option('external_function.link_credentials'),
|
|
1016
|
+
) or '{}',
|
|
1017
|
+
),
|
|
1018
|
+
help='link credentials in JSON format',
|
|
1019
|
+
)
|
|
1020
|
+
parser.add_argument(
|
|
1021
|
+
'--log-level', metavar='[info|debug|warning|error]',
|
|
1022
|
+
default=defaults.get(
|
|
1023
|
+
'log_level',
|
|
1024
|
+
get_option('external_function.log_level'),
|
|
1025
|
+
),
|
|
1026
|
+
help='logging level',
|
|
1027
|
+
)
|
|
1028
|
+
parser.add_argument(
|
|
1029
|
+
'functions', metavar='module.or.func.path', nargs='*',
|
|
1030
|
+
help='functions or modules to export in UDF server',
|
|
1031
|
+
)
|
|
1032
|
+
|
|
1033
|
+
args = parser.parse_args(argv)
|
|
1034
|
+
|
|
1035
|
+
logger.setLevel(getattr(logging, args.log_level.upper()))
|
|
1036
|
+
|
|
1037
|
+
if i > 0:
|
|
1038
|
+
break
|
|
658
1039
|
|
|
659
|
-
|
|
1040
|
+
# Download Stage files as needed
|
|
1041
|
+
for i, f in enumerate(args.functions):
|
|
1042
|
+
if f.startswith('stage://'):
|
|
1043
|
+
url = urllib.parse.urlparse(f)
|
|
1044
|
+
if not url.path or url.path == '/':
|
|
1045
|
+
raise ValueError(f'no stage path was specified: {f}')
|
|
1046
|
+
if url.path.endswith('/'):
|
|
1047
|
+
raise ValueError(f'an environment file must be specified: {f}')
|
|
1048
|
+
|
|
1049
|
+
mgr = manage_workspaces()
|
|
1050
|
+
if url.hostname:
|
|
1051
|
+
wsg = mgr.get_workspace_group(url.hostname)
|
|
1052
|
+
elif os.environ.get('SINGLESTOREDB_WORKSPACE_GROUP'):
|
|
1053
|
+
wsg = mgr.get_workspace_group(
|
|
1054
|
+
os.environ['SINGLESTOREDB_WORKSPACE_GROUP'],
|
|
1055
|
+
)
|
|
1056
|
+
else:
|
|
1057
|
+
raise ValueError(f'no workspace group specified: {f}')
|
|
1058
|
+
|
|
1059
|
+
if tmpdir is None:
|
|
1060
|
+
tmpdir = tempfile.TemporaryDirectory()
|
|
1061
|
+
|
|
1062
|
+
local_path = os.path.join(tmpdir.name, url.path.split('/')[-1])
|
|
1063
|
+
wsg.stage.download_file(url.path, local_path)
|
|
1064
|
+
args.functions[i] = local_path
|
|
1065
|
+
|
|
1066
|
+
elif f.startswith('http://') or f.startswith('https://'):
|
|
1067
|
+
if tmpdir is None:
|
|
1068
|
+
tmpdir = tempfile.TemporaryDirectory()
|
|
1069
|
+
|
|
1070
|
+
local_path = os.path.join(tmpdir.name, f.split('/')[-1])
|
|
1071
|
+
urllib.request.urlretrieve(f, local_path)
|
|
1072
|
+
args.functions[i] = local_path
|
|
1073
|
+
|
|
1074
|
+
# See if any of the args are zip files (assume they are environment files)
|
|
1075
|
+
modules = [(x, zipfile.is_zipfile(x)) for x in args.functions]
|
|
1076
|
+
envs = [x[0] for x in modules if x[1]]
|
|
1077
|
+
others = [x[0] for x in modules if not x[1]]
|
|
1078
|
+
|
|
1079
|
+
if envs and len(envs) > 1:
|
|
1080
|
+
raise RuntimeError('only one environment file may be specified')
|
|
1081
|
+
|
|
1082
|
+
if envs and others:
|
|
1083
|
+
raise RuntimeError('environment files and other modules can not be mixed.')
|
|
1084
|
+
|
|
1085
|
+
# See if an environment file was specified. If so, use those settings
|
|
1086
|
+
# as the defaults and reprocess command line.
|
|
1087
|
+
if envs:
|
|
1088
|
+
# Add pyproject.toml variables and redo command-line processing
|
|
1089
|
+
defaults = utils.read_config(
|
|
1090
|
+
envs[0],
|
|
1091
|
+
['tool.external-function', 'tool.external-function.remote'],
|
|
1092
|
+
)
|
|
1093
|
+
|
|
1094
|
+
# Load zip file as a module
|
|
1095
|
+
modname = os.path.splitext(os.path.basename(envs[0]))[0]
|
|
1096
|
+
zi = zipimport.zipimporter(envs[0])
|
|
1097
|
+
mod = zi.load_module(modname)
|
|
1098
|
+
if mod is None:
|
|
1099
|
+
raise RuntimeError(f'environment file could not be imported: {envs[0]}')
|
|
1100
|
+
functions = [mod]
|
|
1101
|
+
|
|
1102
|
+
if defaults:
|
|
1103
|
+
continue
|
|
1104
|
+
|
|
1105
|
+
args.functions = functions or args.functions or None
|
|
1106
|
+
args.replace_existing = args.replace_existing \
|
|
1107
|
+
or defaults.get('replace_existing') \
|
|
1108
|
+
or get_option('external_function.replace_existing')
|
|
1109
|
+
|
|
1110
|
+
# Create application from functions / module
|
|
1111
|
+
app = Application(
|
|
1112
|
+
functions=args.functions,
|
|
1113
|
+
url=args.url,
|
|
1114
|
+
data_format=args.data_format,
|
|
1115
|
+
data_version=args.data_version,
|
|
1116
|
+
link_name=args.link_name or None,
|
|
1117
|
+
link_config=json.loads(args.link_config) or None,
|
|
1118
|
+
link_credentials=json.loads(args.link_credentials) or None,
|
|
1119
|
+
app_mode='remote',
|
|
1120
|
+
)
|
|
660
1121
|
|
|
661
|
-
|
|
1122
|
+
funcs = app.show_create_functions(replace=args.replace_existing)
|
|
1123
|
+
if not funcs:
|
|
1124
|
+
raise RuntimeError('no functions specified')
|
|
1125
|
+
|
|
1126
|
+
for f in funcs:
|
|
1127
|
+
logger.info(f)
|
|
1128
|
+
|
|
1129
|
+
try:
|
|
1130
|
+
if args.db:
|
|
1131
|
+
logger.info('registering functions with database')
|
|
1132
|
+
app.register_functions(
|
|
1133
|
+
args.db,
|
|
1134
|
+
replace=args.replace_existing,
|
|
1135
|
+
)
|
|
1136
|
+
|
|
1137
|
+
app_args = {
|
|
1138
|
+
k: v for k, v in dict(
|
|
1139
|
+
host=args.host or None,
|
|
1140
|
+
port=args.port or None,
|
|
1141
|
+
log_level=args.log_level,
|
|
1142
|
+
).items() if v is not None
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
if use_async:
|
|
1146
|
+
asyncio.create_task(_run_uvicorn(uvicorn, app, app_args, db=args.db))
|
|
1147
|
+
else:
|
|
1148
|
+
uvicorn.run(app, **app_args)
|
|
1149
|
+
|
|
1150
|
+
finally:
|
|
1151
|
+
if not use_async and args.db:
|
|
1152
|
+
logger.info('dropping functions from database')
|
|
1153
|
+
app.drop_functions(args.db)
|
|
1154
|
+
|
|
1155
|
+
|
|
1156
|
+
async def _run_uvicorn(
|
|
1157
|
+
uvicorn: Any,
|
|
1158
|
+
app: Any,
|
|
1159
|
+
app_args: Any,
|
|
1160
|
+
db: Optional[str] = None,
|
|
1161
|
+
) -> None:
|
|
1162
|
+
"""Run uvicorn server and clean up functions after shutdown."""
|
|
1163
|
+
await uvicorn.Server(uvicorn.Config(app, **app_args)).serve()
|
|
1164
|
+
if db:
|
|
1165
|
+
logger.info('dropping functions from database')
|
|
1166
|
+
app.drop_functions(db)
|
|
1167
|
+
|
|
1168
|
+
|
|
1169
|
+
create_app = Application
|
|
1170
|
+
|
|
1171
|
+
|
|
1172
|
+
if __name__ == '__main__':
|
|
1173
|
+
try:
|
|
1174
|
+
main()
|
|
1175
|
+
except RuntimeError as exc:
|
|
1176
|
+
logger.error(str(exc))
|
|
1177
|
+
sys.exit(1)
|
|
1178
|
+
except KeyboardInterrupt:
|
|
1179
|
+
pass
|