singlestoredb 1.0.3__py3-none-any.whl → 1.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 singlestoredb might be problematic. Click here for more details.
- singlestoredb/__init__.py +1 -1
- singlestoredb/config.py +125 -0
- singlestoredb/functions/dtypes.py +5 -198
- singlestoredb/functions/ext/__init__.py +0 -1
- singlestoredb/functions/ext/asgi.py +665 -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 +109 -9
- singlestoredb/fusion/handlers/stage.py +150 -0
- singlestoredb/fusion/handlers/workspace.py +265 -4
- singlestoredb/fusion/registry.py +69 -1
- singlestoredb/http/connection.py +40 -2
- singlestoredb/management/utils.py +30 -0
- singlestoredb/management/workspace.py +209 -35
- singlestoredb/mysql/connection.py +69 -0
- singlestoredb/mysql/cursors.py +176 -4
- singlestoredb/tests/test.sql +210 -0
- singlestoredb/tests/test_connection.py +1408 -0
- 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.3.dist-info → singlestoredb-1.1.0.dist-info}/METADATA +2 -1
- {singlestoredb-1.0.3.dist-info → singlestoredb-1.1.0.dist-info}/RECORD +30 -28
- {singlestoredb-1.0.3.dist-info → singlestoredb-1.1.0.dist-info}/LICENSE +0 -0
- {singlestoredb-1.0.3.dist-info → singlestoredb-1.1.0.dist-info}/WHEEL +0 -0
- {singlestoredb-1.0.3.dist-info → singlestoredb-1.1.0.dist-info}/entry_points.txt +0 -0
- {singlestoredb-1.0.3.dist-info → singlestoredb-1.1.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,124 @@ 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
|
+
else:
|
|
459
|
+
pkg_path, func_names = funcs.rsplit('.', 1)
|
|
460
|
+
pkg = importlib.import_module(pkg_path)
|
|
461
|
+
|
|
462
|
+
# Add endpoint for each exported function
|
|
463
|
+
for name, alias in get_func_names(func_names):
|
|
464
|
+
item = getattr(pkg, name)
|
|
465
|
+
external_functions[name] = item
|
|
466
|
+
func, info = make_func(alias, item)
|
|
467
|
+
endpoints[alias.encode('utf-8')] = func, info
|
|
468
|
+
|
|
469
|
+
elif isinstance(funcs, ModuleType):
|
|
470
|
+
for x in vars(funcs).values():
|
|
471
|
+
if not hasattr(x, '_singlestoredb_attrs'):
|
|
472
|
+
continue
|
|
473
|
+
name = x._singlestoredb_attrs.get('name', x.__name__)
|
|
474
|
+
external_functions[x.__name__] = x
|
|
475
|
+
func, info = make_func(name, x)
|
|
476
|
+
endpoints[name.encode('utf-8')] = func, info
|
|
477
|
+
|
|
478
|
+
else:
|
|
479
|
+
alias = funcs.__name__
|
|
480
|
+
external_functions[funcs.__name__] = funcs
|
|
481
|
+
func, info = make_func(alias, funcs)
|
|
482
|
+
endpoints[alias.encode('utf-8')] = func, info
|
|
483
|
+
|
|
484
|
+
self.app_mode = app_mode
|
|
485
|
+
self.url = url
|
|
486
|
+
self.data_format = data_format
|
|
487
|
+
self.data_version = data_version
|
|
488
|
+
self.link_name = link_name
|
|
489
|
+
self.link_config = link_config
|
|
490
|
+
self.link_credentials = link_credentials
|
|
491
|
+
self.endpoints = endpoints
|
|
492
|
+
self.external_functions = external_functions
|
|
493
|
+
|
|
494
|
+
async def __call__(
|
|
495
|
+
self,
|
|
436
496
|
scope: Dict[str, Any],
|
|
437
497
|
receive: Callable[..., Awaitable[Any]],
|
|
438
498
|
send: Callable[..., Awaitable[Any]],
|
|
@@ -462,11 +522,16 @@ def create_app( # noqa: C901
|
|
|
462
522
|
)
|
|
463
523
|
accepts = headers.get(b'accepts', content_type)
|
|
464
524
|
func_name = headers.get(b's2-ef-name', b'')
|
|
465
|
-
|
|
525
|
+
func_endpoint = self.endpoints.get(func_name)
|
|
526
|
+
|
|
527
|
+
func = None
|
|
528
|
+
func_info: Dict[str, Any] = {}
|
|
529
|
+
if func_endpoint is not None:
|
|
530
|
+
func, func_info = func_endpoint
|
|
466
531
|
|
|
467
532
|
# Call the endpoint
|
|
468
|
-
if method == 'POST' and func is not None and path == invoke_path:
|
|
469
|
-
data_format =
|
|
533
|
+
if method == 'POST' and func is not None and path == self.invoke_path:
|
|
534
|
+
data_format = func_info['data_format']
|
|
470
535
|
data = []
|
|
471
536
|
more_body = True
|
|
472
537
|
while more_body:
|
|
@@ -475,57 +540,61 @@ def create_app( # noqa: C901
|
|
|
475
540
|
more_body = request.get('more_body', False)
|
|
476
541
|
|
|
477
542
|
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)]
|
|
543
|
+
input_handler = self.handlers[(content_type, data_version, data_format)]
|
|
544
|
+
output_handler = self.handlers[(accepts, data_version, data_format)]
|
|
480
545
|
|
|
481
546
|
out = await func(
|
|
482
|
-
*input_handler['load'](
|
|
483
|
-
|
|
547
|
+
*input_handler['load']( # type: ignore
|
|
548
|
+
func_info['colspec'], b''.join(data),
|
|
484
549
|
),
|
|
485
550
|
)
|
|
486
|
-
body = output_handler['dump'](
|
|
551
|
+
body = output_handler['dump'](func_info['returns'], *out) # type: ignore
|
|
487
552
|
|
|
488
553
|
await send(output_handler['response'])
|
|
489
554
|
|
|
490
555
|
# Handle api reflection
|
|
491
|
-
elif method == 'GET' and path == show_create_function_path:
|
|
556
|
+
elif method == 'GET' and path == self.show_create_function_path:
|
|
492
557
|
host = headers.get(b'host', b'localhost:80')
|
|
493
558
|
reflected_url = f'{scope["scheme"]}://{host.decode("utf-8")}/invoke'
|
|
494
559
|
data_format = 'json' if b'json' in content_type else 'rowdat_1'
|
|
495
560
|
|
|
496
561
|
syntax = []
|
|
497
|
-
for key, endpoint in endpoints.items():
|
|
562
|
+
for key, (endpoint, endpoint_info) in self.endpoints.items():
|
|
498
563
|
if not func_name or key == func_name:
|
|
499
564
|
syntax.append(
|
|
500
565
|
signature_to_sql(
|
|
501
|
-
|
|
502
|
-
url=url or reflected_url,
|
|
566
|
+
endpoint_info['signature'],
|
|
567
|
+
url=self.url or reflected_url,
|
|
503
568
|
data_format=data_format,
|
|
504
569
|
),
|
|
505
570
|
)
|
|
506
571
|
body = '\n'.join(syntax).encode('utf-8')
|
|
507
572
|
|
|
508
|
-
await send(text_response_dict)
|
|
573
|
+
await send(self.text_response_dict)
|
|
509
574
|
|
|
510
575
|
# Path not found
|
|
511
576
|
else:
|
|
512
577
|
body = b''
|
|
513
|
-
await send(path_not_found_response_dict)
|
|
578
|
+
await send(self.path_not_found_response_dict)
|
|
514
579
|
|
|
515
580
|
# Send body
|
|
516
|
-
out = body_response_dict.copy()
|
|
581
|
+
out = self.body_response_dict.copy()
|
|
517
582
|
out['body'] = body
|
|
518
583
|
await send(out)
|
|
519
584
|
|
|
520
585
|
def _create_link(
|
|
586
|
+
self,
|
|
521
587
|
config: Optional[Dict[str, Any]],
|
|
522
588
|
credentials: Optional[Dict[str, Any]],
|
|
523
589
|
) -> Tuple[str, str]:
|
|
524
590
|
"""Generate CREATE LINK command."""
|
|
591
|
+
if self.link_name:
|
|
592
|
+
return self.link_name, ''
|
|
593
|
+
|
|
525
594
|
if not config and not credentials:
|
|
526
595
|
return '', ''
|
|
527
596
|
|
|
528
|
-
link_name = f'
|
|
597
|
+
link_name = f'py_ext_func_link_{secrets.token_hex(14)}'
|
|
529
598
|
out = [f'CREATE LINK {link_name} AS HTTP']
|
|
530
599
|
|
|
531
600
|
if config:
|
|
@@ -536,7 +605,7 @@ def create_app( # noqa: C901
|
|
|
536
605
|
|
|
537
606
|
return link_name, ' '.join(out) + ';'
|
|
538
607
|
|
|
539
|
-
def _locate_app_functions(cur: Any) -> Tuple[Set[str], Set[str]]:
|
|
608
|
+
def _locate_app_functions(self, cur: Any) -> Tuple[Set[str], Set[str]]:
|
|
540
609
|
"""Locate all current functions and links belonging to this app."""
|
|
541
610
|
funcs, links = set(), set()
|
|
542
611
|
cur.execute('SHOW FUNCTIONS')
|
|
@@ -548,33 +617,46 @@ def create_app( # noqa: C901
|
|
|
548
617
|
cur.execute(f'SHOW CREATE FUNCTION `{name}`')
|
|
549
618
|
for fname, _, code, *_ in list(cur):
|
|
550
619
|
m = re.search(r" (?:\w+) SERVICE '([^']+)'", code)
|
|
551
|
-
if m and m.group(1) == url:
|
|
620
|
+
if m and m.group(1) == self.url:
|
|
552
621
|
funcs.add(fname)
|
|
553
|
-
if link:
|
|
622
|
+
if link and re.match(r'^py_ext_func_link_\S{14}$', link):
|
|
554
623
|
links.add(link)
|
|
555
624
|
return funcs, links
|
|
556
625
|
|
|
557
626
|
def show_create_functions(
|
|
627
|
+
self,
|
|
558
628
|
replace: bool = False,
|
|
559
629
|
) -> List[str]:
|
|
560
|
-
"""
|
|
561
|
-
|
|
630
|
+
"""
|
|
631
|
+
Generate CREATE FUNCTION code for all functions.
|
|
632
|
+
|
|
633
|
+
Parameters
|
|
634
|
+
----------
|
|
635
|
+
replace : bool, optional
|
|
636
|
+
Should existing functions be replaced?
|
|
637
|
+
|
|
638
|
+
Returns
|
|
639
|
+
-------
|
|
640
|
+
List[str]
|
|
641
|
+
|
|
642
|
+
"""
|
|
643
|
+
if not self.endpoints:
|
|
562
644
|
return []
|
|
563
645
|
|
|
564
646
|
out = []
|
|
565
647
|
link = ''
|
|
566
|
-
if app_mode.lower() == 'remote':
|
|
567
|
-
link, link_str = _create_link(link_config, link_credentials)
|
|
648
|
+
if self.app_mode.lower() == 'remote':
|
|
649
|
+
link, link_str = self._create_link(self.link_config, self.link_credentials)
|
|
568
650
|
if link and link_str:
|
|
569
651
|
out.append(link_str)
|
|
570
652
|
|
|
571
|
-
for key, endpoint in endpoints.items():
|
|
653
|
+
for key, (endpoint, endpoint_info) in self.endpoints.items():
|
|
572
654
|
out.append(
|
|
573
655
|
signature_to_sql(
|
|
574
|
-
|
|
575
|
-
url=url,
|
|
576
|
-
data_format=data_format,
|
|
577
|
-
app_mode=app_mode,
|
|
656
|
+
endpoint_info['signature'],
|
|
657
|
+
url=self.url,
|
|
658
|
+
data_format=self.data_format,
|
|
659
|
+
app_mode=self.app_mode,
|
|
578
660
|
replace=replace,
|
|
579
661
|
link=link or None,
|
|
580
662
|
),
|
|
@@ -582,49 +664,87 @@ def create_app( # noqa: C901
|
|
|
582
664
|
|
|
583
665
|
return out
|
|
584
666
|
|
|
585
|
-
app.show_create_functions = show_create_functions # type: ignore
|
|
586
|
-
|
|
587
667
|
def register_functions(
|
|
668
|
+
self,
|
|
588
669
|
*connection_args: Any,
|
|
589
670
|
replace: bool = False,
|
|
590
671
|
**connection_kwargs: Any,
|
|
591
672
|
) -> None:
|
|
592
|
-
"""
|
|
673
|
+
"""
|
|
674
|
+
Register functions with the database.
|
|
675
|
+
|
|
676
|
+
Parameters
|
|
677
|
+
----------
|
|
678
|
+
*connection_args : Any
|
|
679
|
+
Database connection parameters
|
|
680
|
+
replace : bool, optional
|
|
681
|
+
Should existing functions be replaced?
|
|
682
|
+
**connection_kwargs : Any
|
|
683
|
+
Database connection parameters
|
|
684
|
+
|
|
685
|
+
"""
|
|
593
686
|
with connection.connect(*connection_args, **connection_kwargs) as conn:
|
|
594
687
|
with conn.cursor() as cur:
|
|
595
688
|
if replace:
|
|
596
|
-
funcs, links = _locate_app_functions(cur)
|
|
689
|
+
funcs, links = self._locate_app_functions(cur)
|
|
597
690
|
for fname in funcs:
|
|
598
691
|
cur.execute(f'DROP FUNCTION IF EXISTS `{fname}`')
|
|
599
692
|
for link in links:
|
|
600
693
|
cur.execute(f'DROP LINK {link}')
|
|
601
|
-
for func in
|
|
694
|
+
for func in self.show_create_functions(replace=replace):
|
|
602
695
|
cur.execute(func)
|
|
603
696
|
|
|
604
|
-
app.register_functions = register_functions # type: ignore
|
|
605
|
-
|
|
606
697
|
def drop_functions(
|
|
698
|
+
self,
|
|
607
699
|
*connection_args: Any,
|
|
608
700
|
**connection_kwargs: Any,
|
|
609
701
|
) -> None:
|
|
610
|
-
"""
|
|
702
|
+
"""
|
|
703
|
+
Drop registered functions from database.
|
|
704
|
+
|
|
705
|
+
Parameters
|
|
706
|
+
----------
|
|
707
|
+
*connection_args : Any
|
|
708
|
+
Database connection parameters
|
|
709
|
+
**connection_kwargs : Any
|
|
710
|
+
Database connection parameters
|
|
711
|
+
|
|
712
|
+
"""
|
|
611
713
|
with connection.connect(*connection_args, **connection_kwargs) as conn:
|
|
612
714
|
with conn.cursor() as cur:
|
|
613
|
-
funcs, links = _locate_app_functions(cur)
|
|
715
|
+
funcs, links = self._locate_app_functions(cur)
|
|
614
716
|
for fname in funcs:
|
|
615
717
|
cur.execute(f'DROP FUNCTION IF EXISTS `{fname}`')
|
|
616
718
|
for link in links:
|
|
617
719
|
cur.execute(f'DROP LINK {link}')
|
|
618
720
|
|
|
619
|
-
app.drop_functions = drop_functions # type: ignore
|
|
620
|
-
|
|
621
721
|
async def call(
|
|
722
|
+
self,
|
|
622
723
|
name: str,
|
|
623
724
|
data_in: io.BytesIO,
|
|
624
725
|
data_out: io.BytesIO,
|
|
625
|
-
data_format: str =
|
|
626
|
-
data_version: str =
|
|
726
|
+
data_format: Optional[str] = None,
|
|
727
|
+
data_version: Optional[str] = None,
|
|
627
728
|
) -> None:
|
|
729
|
+
"""
|
|
730
|
+
Call a function in the application.
|
|
731
|
+
|
|
732
|
+
Parameters
|
|
733
|
+
----------
|
|
734
|
+
name : str
|
|
735
|
+
Name of the function to call
|
|
736
|
+
data_in : io.BytesIO
|
|
737
|
+
The input data rows
|
|
738
|
+
data_out : io.BytesIO
|
|
739
|
+
The output data rows
|
|
740
|
+
data_format : str, optional
|
|
741
|
+
The format of the input and output data
|
|
742
|
+
data_version : str, optional
|
|
743
|
+
The version of the data format
|
|
744
|
+
|
|
745
|
+
"""
|
|
746
|
+
data_format = data_format or self.data_format
|
|
747
|
+
data_version = data_version or self.data_version
|
|
628
748
|
|
|
629
749
|
async def receive() -> Dict[str, Any]:
|
|
630
750
|
return dict(body=data_in.read())
|
|
@@ -654,8 +774,400 @@ def create_app( # noqa: C901
|
|
|
654
774
|
},
|
|
655
775
|
)
|
|
656
776
|
|
|
657
|
-
await
|
|
777
|
+
await self(scope, receive, send)
|
|
778
|
+
|
|
779
|
+
def to_environment(
|
|
780
|
+
self,
|
|
781
|
+
name: str,
|
|
782
|
+
destination: str = '.',
|
|
783
|
+
version: Optional[str] = None,
|
|
784
|
+
dependencies: Optional[List[str]] = None,
|
|
785
|
+
authors: Optional[List[Dict[str, str]]] = None,
|
|
786
|
+
maintainers: Optional[List[Dict[str, str]]] = None,
|
|
787
|
+
description: Optional[str] = None,
|
|
788
|
+
container_service: Optional[Dict[str, Any]] = None,
|
|
789
|
+
external_function: Optional[Dict[str, Any]] = None,
|
|
790
|
+
external_function_remote: Optional[Dict[str, Any]] = None,
|
|
791
|
+
external_function_collocated: Optional[Dict[str, Any]] = None,
|
|
792
|
+
overwrite: bool = False,
|
|
793
|
+
) -> None:
|
|
794
|
+
"""
|
|
795
|
+
Convert application to an environment file.
|
|
796
|
+
|
|
797
|
+
Parameters
|
|
798
|
+
----------
|
|
799
|
+
name : str
|
|
800
|
+
Name of the output environment
|
|
801
|
+
destination : str, optional
|
|
802
|
+
Location of the output file
|
|
803
|
+
version : str, optional
|
|
804
|
+
Version of the package
|
|
805
|
+
dependencies : List[str], optional
|
|
806
|
+
List of dependency specifications like in a requirements.txt file
|
|
807
|
+
authors : List[Dict[str, Any]], optional
|
|
808
|
+
Dictionaries of author information. Keys may include: email, name
|
|
809
|
+
maintainers : List[Dict[str, Any]], optional
|
|
810
|
+
Dictionaries of maintainer information. Keys may include: email, name
|
|
811
|
+
description : str, optional
|
|
812
|
+
Description of package
|
|
813
|
+
container_service : Dict[str, Any], optional
|
|
814
|
+
Container service specifications
|
|
815
|
+
external_function : Dict[str, Any], optional
|
|
816
|
+
External function specifications (applies to both remote and collocated)
|
|
817
|
+
external_function_remote : Dict[str, Any], optional
|
|
818
|
+
Remote external function specifications
|
|
819
|
+
external_function_collocated : Dict[str, Any], optional
|
|
820
|
+
Collocated external function specifications
|
|
821
|
+
overwrite : bool, optional
|
|
822
|
+
Should destination file be overwritten if it exists?
|
|
823
|
+
|
|
824
|
+
"""
|
|
825
|
+
if not has_cloudpickle:
|
|
826
|
+
raise RuntimeError('the cloudpicke package is required for this operation')
|
|
827
|
+
|
|
828
|
+
# Write to temporary location if a remote destination is specified
|
|
829
|
+
tmpdir = None
|
|
830
|
+
if destination.startswith('stage://'):
|
|
831
|
+
tmpdir = tempfile.TemporaryDirectory()
|
|
832
|
+
local_path = os.path.join(tmpdir.name, f'{name}.env')
|
|
833
|
+
else:
|
|
834
|
+
local_path = os.path.join(destination, f'{name}.env')
|
|
835
|
+
if not overwrite and os.path.exists(local_path):
|
|
836
|
+
raise OSError(f'path already exists: {local_path}')
|
|
837
|
+
|
|
838
|
+
with zipfile.ZipFile(local_path, mode='w') as z:
|
|
839
|
+
# Write metadata
|
|
840
|
+
z.writestr(
|
|
841
|
+
'pyproject.toml', utils.to_toml({
|
|
842
|
+
'project': dict(
|
|
843
|
+
name=name,
|
|
844
|
+
version=version,
|
|
845
|
+
dependencies=dependencies,
|
|
846
|
+
requires_python='== ' +
|
|
847
|
+
'.'.join(str(x) for x in sys.version_info[:3]),
|
|
848
|
+
authors=authors,
|
|
849
|
+
maintainers=maintainers,
|
|
850
|
+
description=description,
|
|
851
|
+
),
|
|
852
|
+
'tool.container-service': container_service,
|
|
853
|
+
'tool.external-function': external_function,
|
|
854
|
+
'tool.external-function.remote': external_function_remote,
|
|
855
|
+
'tool.external-function.collocated': external_function_collocated,
|
|
856
|
+
}),
|
|
857
|
+
)
|
|
858
|
+
|
|
859
|
+
# Write Python package
|
|
860
|
+
z.writestr(
|
|
861
|
+
f'{name}/__init__.py',
|
|
862
|
+
textwrap.dedent(f'''
|
|
863
|
+
import pickle as _pkl
|
|
864
|
+
globals().update(
|
|
865
|
+
_pkl.loads({cloudpickle.dumps(self.external_functions)}),
|
|
866
|
+
)
|
|
867
|
+
__all__ = {list(self.external_functions.keys())}''').strip(),
|
|
868
|
+
)
|
|
869
|
+
|
|
870
|
+
# Upload to Stage as needed
|
|
871
|
+
if destination.startswith('stage://'):
|
|
872
|
+
url = urllib.parse.urlparse(re.sub(r'/+$', r'', destination) + '/')
|
|
873
|
+
if not url.path or url.path == '/':
|
|
874
|
+
raise ValueError(f'no stage path was specified: {destination}')
|
|
875
|
+
|
|
876
|
+
mgr = manage_workspaces()
|
|
877
|
+
if url.hostname:
|
|
878
|
+
wsg = mgr.get_workspace_group(url.hostname)
|
|
879
|
+
elif os.environ.get('SINGLESTOREDB_WORKSPACE_GROUP'):
|
|
880
|
+
wsg = mgr.get_workspace_group(
|
|
881
|
+
os.environ['SINGLESTOREDB_WORKSPACE_GROUP'],
|
|
882
|
+
)
|
|
883
|
+
else:
|
|
884
|
+
raise ValueError(f'no workspace group specified: {destination}')
|
|
885
|
+
|
|
886
|
+
# Make intermediate directories
|
|
887
|
+
if url.path.count('/') > 1:
|
|
888
|
+
wsg.stage.mkdirs(os.path.dirname(url.path))
|
|
889
|
+
|
|
890
|
+
wsg.stage.upload_file(
|
|
891
|
+
local_path, url.path + f'{name}.env',
|
|
892
|
+
overwrite=overwrite,
|
|
893
|
+
)
|
|
894
|
+
os.remove(local_path)
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
def main(argv: Optional[List[str]] = None) -> None:
|
|
898
|
+
"""
|
|
899
|
+
Main program for HTTP-based Python UDFs
|
|
900
|
+
|
|
901
|
+
Parameters
|
|
902
|
+
----------
|
|
903
|
+
argv : List[str], optional
|
|
904
|
+
List of command-line parameters
|
|
905
|
+
|
|
906
|
+
"""
|
|
907
|
+
try:
|
|
908
|
+
import uvicorn
|
|
909
|
+
except ImportError:
|
|
910
|
+
raise ImportError('the uvicorn package is required to run this command')
|
|
911
|
+
|
|
912
|
+
# Should we run in embedded mode (typically for Jupyter)
|
|
913
|
+
try:
|
|
914
|
+
asyncio.get_running_loop()
|
|
915
|
+
use_async = True
|
|
916
|
+
except RuntimeError:
|
|
917
|
+
use_async = False
|
|
918
|
+
|
|
919
|
+
# Temporary directory for Stage environment files
|
|
920
|
+
tmpdir = None
|
|
921
|
+
|
|
922
|
+
# Depending on whether we find an environment file specified, we
|
|
923
|
+
# may have to process the command line twice.
|
|
924
|
+
functions = []
|
|
925
|
+
defaults: Dict[str, Any] = {}
|
|
926
|
+
for i in range(2):
|
|
927
|
+
|
|
928
|
+
parser = argparse.ArgumentParser(
|
|
929
|
+
prog='python -m singlestoredb.functions.ext.asgi',
|
|
930
|
+
description='Run an HTTP-based Python UDF server',
|
|
931
|
+
)
|
|
932
|
+
parser.add_argument(
|
|
933
|
+
'--url', metavar='url',
|
|
934
|
+
default=defaults.get(
|
|
935
|
+
'url',
|
|
936
|
+
get_option('external_function.url'),
|
|
937
|
+
),
|
|
938
|
+
help='URL of the UDF server endpoint',
|
|
939
|
+
)
|
|
940
|
+
parser.add_argument(
|
|
941
|
+
'--host', metavar='host',
|
|
942
|
+
default=defaults.get(
|
|
943
|
+
'host',
|
|
944
|
+
get_option('external_function.host'),
|
|
945
|
+
),
|
|
946
|
+
help='bind socket to this host',
|
|
947
|
+
)
|
|
948
|
+
parser.add_argument(
|
|
949
|
+
'--port', metavar='port', type=int,
|
|
950
|
+
default=defaults.get(
|
|
951
|
+
'port',
|
|
952
|
+
get_option('external_function.port'),
|
|
953
|
+
),
|
|
954
|
+
help='bind socket to this port',
|
|
955
|
+
)
|
|
956
|
+
parser.add_argument(
|
|
957
|
+
'--db', metavar='conn-str',
|
|
958
|
+
default=defaults.get(
|
|
959
|
+
'connection',
|
|
960
|
+
get_option('external_function.connection'),
|
|
961
|
+
),
|
|
962
|
+
help='connection string to use for registering functions',
|
|
963
|
+
)
|
|
964
|
+
parser.add_argument(
|
|
965
|
+
'--replace-existing', action='store_true',
|
|
966
|
+
help='should existing functions of the same name '
|
|
967
|
+
'in the database be replaced?',
|
|
968
|
+
)
|
|
969
|
+
parser.add_argument(
|
|
970
|
+
'--data-format', metavar='format',
|
|
971
|
+
default=defaults.get(
|
|
972
|
+
'data_format',
|
|
973
|
+
get_option('external_function.data_format'),
|
|
974
|
+
),
|
|
975
|
+
choices=['rowdat_1', 'json'],
|
|
976
|
+
help='format of the data rows',
|
|
977
|
+
)
|
|
978
|
+
parser.add_argument(
|
|
979
|
+
'--data-version', metavar='version',
|
|
980
|
+
default=defaults.get(
|
|
981
|
+
'data_version',
|
|
982
|
+
get_option('external_function.data_version'),
|
|
983
|
+
),
|
|
984
|
+
help='version of the data row format',
|
|
985
|
+
)
|
|
986
|
+
parser.add_argument(
|
|
987
|
+
'--link-name', metavar='name',
|
|
988
|
+
default=defaults.get(
|
|
989
|
+
'link_name',
|
|
990
|
+
get_option('external_function.link_name'),
|
|
991
|
+
) or '',
|
|
992
|
+
help='name of the link to use for connections',
|
|
993
|
+
)
|
|
994
|
+
parser.add_argument(
|
|
995
|
+
'--link-config', metavar='json',
|
|
996
|
+
default=str(
|
|
997
|
+
defaults.get(
|
|
998
|
+
'link_config',
|
|
999
|
+
get_option('external_function.link_config'),
|
|
1000
|
+
) or '{}',
|
|
1001
|
+
),
|
|
1002
|
+
help='link config in JSON format',
|
|
1003
|
+
)
|
|
1004
|
+
parser.add_argument(
|
|
1005
|
+
'--link-credentials', metavar='json',
|
|
1006
|
+
default=str(
|
|
1007
|
+
defaults.get(
|
|
1008
|
+
'link_credentials',
|
|
1009
|
+
get_option('external_function.link_credentials'),
|
|
1010
|
+
) or '{}',
|
|
1011
|
+
),
|
|
1012
|
+
help='link credentials in JSON format',
|
|
1013
|
+
)
|
|
1014
|
+
parser.add_argument(
|
|
1015
|
+
'--log-level', metavar='[info|debug|warning|error]',
|
|
1016
|
+
default=defaults.get(
|
|
1017
|
+
'log_level',
|
|
1018
|
+
get_option('external_function.log_level'),
|
|
1019
|
+
),
|
|
1020
|
+
help='logging level',
|
|
1021
|
+
)
|
|
1022
|
+
parser.add_argument(
|
|
1023
|
+
'functions', metavar='module.or.func.path', nargs='*',
|
|
1024
|
+
help='functions or modules to export in UDF server',
|
|
1025
|
+
)
|
|
1026
|
+
|
|
1027
|
+
args = parser.parse_args(argv)
|
|
1028
|
+
|
|
1029
|
+
logger.setLevel(getattr(logging, args.log_level.upper()))
|
|
1030
|
+
|
|
1031
|
+
if i > 0:
|
|
1032
|
+
break
|
|
658
1033
|
|
|
659
|
-
|
|
1034
|
+
# Download Stage files as needed
|
|
1035
|
+
for i, f in enumerate(args.functions):
|
|
1036
|
+
if f.startswith('stage://'):
|
|
1037
|
+
url = urllib.parse.urlparse(f)
|
|
1038
|
+
if not url.path or url.path == '/':
|
|
1039
|
+
raise ValueError(f'no stage path was specified: {f}')
|
|
1040
|
+
if url.path.endswith('/'):
|
|
1041
|
+
raise ValueError(f'an environment file must be specified: {f}')
|
|
1042
|
+
|
|
1043
|
+
mgr = manage_workspaces()
|
|
1044
|
+
if url.hostname:
|
|
1045
|
+
wsg = mgr.get_workspace_group(url.hostname)
|
|
1046
|
+
elif os.environ.get('SINGLESTOREDB_WORKSPACE_GROUP'):
|
|
1047
|
+
wsg = mgr.get_workspace_group(
|
|
1048
|
+
os.environ['SINGLESTOREDB_WORKSPACE_GROUP'],
|
|
1049
|
+
)
|
|
1050
|
+
else:
|
|
1051
|
+
raise ValueError(f'no workspace group specified: {f}')
|
|
1052
|
+
|
|
1053
|
+
if tmpdir is None:
|
|
1054
|
+
tmpdir = tempfile.TemporaryDirectory()
|
|
1055
|
+
|
|
1056
|
+
local_path = os.path.join(tmpdir.name, url.path.split('/')[-1])
|
|
1057
|
+
wsg.stage.download_file(url.path, local_path)
|
|
1058
|
+
args.functions[i] = local_path
|
|
1059
|
+
|
|
1060
|
+
elif f.startswith('http://') or f.startswith('https://'):
|
|
1061
|
+
if tmpdir is None:
|
|
1062
|
+
tmpdir = tempfile.TemporaryDirectory()
|
|
1063
|
+
|
|
1064
|
+
local_path = os.path.join(tmpdir.name, f.split('/')[-1])
|
|
1065
|
+
urllib.request.urlretrieve(f, local_path)
|
|
1066
|
+
args.functions[i] = local_path
|
|
1067
|
+
|
|
1068
|
+
# See if any of the args are zip files (assume they are environment files)
|
|
1069
|
+
modules = [(x, zipfile.is_zipfile(x)) for x in args.functions]
|
|
1070
|
+
envs = [x[0] for x in modules if x[1]]
|
|
1071
|
+
others = [x[0] for x in modules if not x[1]]
|
|
1072
|
+
|
|
1073
|
+
if envs and len(envs) > 1:
|
|
1074
|
+
raise RuntimeError('only one environment file may be specified')
|
|
1075
|
+
|
|
1076
|
+
if envs and others:
|
|
1077
|
+
raise RuntimeError('environment files and other modules can not be mixed.')
|
|
1078
|
+
|
|
1079
|
+
# See if an environment file was specified. If so, use those settings
|
|
1080
|
+
# as the defaults and reprocess command line.
|
|
1081
|
+
if envs:
|
|
1082
|
+
# Add pyproject.toml variables and redo command-line processing
|
|
1083
|
+
defaults = utils.read_config(
|
|
1084
|
+
envs[0],
|
|
1085
|
+
['tool.external-function', 'tool.external-function.remote'],
|
|
1086
|
+
)
|
|
1087
|
+
|
|
1088
|
+
# Load zip file as a module
|
|
1089
|
+
modname = os.path.splitext(os.path.basename(envs[0]))[0]
|
|
1090
|
+
zi = zipimport.zipimporter(envs[0])
|
|
1091
|
+
mod = zi.load_module(modname)
|
|
1092
|
+
if mod is None:
|
|
1093
|
+
raise RuntimeError(f'environment file could not be imported: {envs[0]}')
|
|
1094
|
+
functions = [mod]
|
|
1095
|
+
|
|
1096
|
+
if defaults:
|
|
1097
|
+
continue
|
|
1098
|
+
|
|
1099
|
+
args.functions = functions or args.functions or None
|
|
1100
|
+
args.replace_existing = args.replace_existing \
|
|
1101
|
+
or defaults.get('replace_existing') \
|
|
1102
|
+
or get_option('external_function.replace_existing')
|
|
1103
|
+
|
|
1104
|
+
# Create application from functions / module
|
|
1105
|
+
app = Application(
|
|
1106
|
+
functions=args.functions,
|
|
1107
|
+
url=args.url,
|
|
1108
|
+
data_format=args.data_format,
|
|
1109
|
+
data_version=args.data_version,
|
|
1110
|
+
link_name=args.link_name or None,
|
|
1111
|
+
link_config=json.loads(args.link_config) or None,
|
|
1112
|
+
link_credentials=json.loads(args.link_credentials) or None,
|
|
1113
|
+
app_mode='remote',
|
|
1114
|
+
)
|
|
660
1115
|
|
|
661
|
-
|
|
1116
|
+
funcs = app.show_create_functions(replace=args.replace_existing)
|
|
1117
|
+
if not funcs:
|
|
1118
|
+
raise RuntimeError('no functions specified')
|
|
1119
|
+
|
|
1120
|
+
for f in funcs:
|
|
1121
|
+
logger.info(f)
|
|
1122
|
+
|
|
1123
|
+
try:
|
|
1124
|
+
if args.db:
|
|
1125
|
+
logger.info('registering functions with database')
|
|
1126
|
+
app.register_functions(
|
|
1127
|
+
args.db,
|
|
1128
|
+
replace=args.replace_existing,
|
|
1129
|
+
)
|
|
1130
|
+
|
|
1131
|
+
app_args = {
|
|
1132
|
+
k: v for k, v in dict(
|
|
1133
|
+
host=args.host or None,
|
|
1134
|
+
port=args.port or None,
|
|
1135
|
+
log_level=args.log_level,
|
|
1136
|
+
).items() if v is not None
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
if use_async:
|
|
1140
|
+
asyncio.create_task(_run_uvicorn(uvicorn, app, app_args, db=args.db))
|
|
1141
|
+
else:
|
|
1142
|
+
uvicorn.run(app, **app_args)
|
|
1143
|
+
|
|
1144
|
+
finally:
|
|
1145
|
+
if not use_async and args.db:
|
|
1146
|
+
logger.info('dropping functions from database')
|
|
1147
|
+
app.drop_functions(args.db)
|
|
1148
|
+
|
|
1149
|
+
|
|
1150
|
+
async def _run_uvicorn(
|
|
1151
|
+
uvicorn: Any,
|
|
1152
|
+
app: Any,
|
|
1153
|
+
app_args: Any,
|
|
1154
|
+
db: Optional[str] = None,
|
|
1155
|
+
) -> None:
|
|
1156
|
+
"""Run uvicorn server and clean up functions after shutdown."""
|
|
1157
|
+
await uvicorn.Server(uvicorn.Config(app, **app_args)).serve()
|
|
1158
|
+
if db:
|
|
1159
|
+
logger.info('dropping functions from database')
|
|
1160
|
+
app.drop_functions(db)
|
|
1161
|
+
|
|
1162
|
+
|
|
1163
|
+
create_app = Application
|
|
1164
|
+
|
|
1165
|
+
|
|
1166
|
+
if __name__ == '__main__':
|
|
1167
|
+
try:
|
|
1168
|
+
main()
|
|
1169
|
+
except RuntimeError as exc:
|
|
1170
|
+
logger.error(str(exc))
|
|
1171
|
+
sys.exit(1)
|
|
1172
|
+
except KeyboardInterrupt:
|
|
1173
|
+
pass
|