singlestoredb 1.15.0__cp38-abi3-win_amd64.whl → 1.15.2__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/ai/chat.py +14 -0
- singlestoredb/apps/_python_udfs.py +18 -3
- singlestoredb/apps/_stdout_supress.py +1 -1
- singlestoredb/apps/_uvicorn_util.py +4 -0
- singlestoredb/config.py +24 -0
- singlestoredb/converters.py +1 -1
- singlestoredb/docstring/__init__.py +33 -0
- singlestoredb/docstring/attrdoc.py +126 -0
- singlestoredb/docstring/common.py +230 -0
- singlestoredb/docstring/epydoc.py +267 -0
- singlestoredb/docstring/google.py +412 -0
- singlestoredb/docstring/numpydoc.py +562 -0
- singlestoredb/docstring/parser.py +100 -0
- singlestoredb/docstring/py.typed +1 -0
- singlestoredb/docstring/rest.py +256 -0
- singlestoredb/docstring/tests/__init__.py +1 -0
- singlestoredb/docstring/tests/_pydoctor.py +21 -0
- singlestoredb/docstring/tests/test_epydoc.py +729 -0
- singlestoredb/docstring/tests/test_google.py +1007 -0
- singlestoredb/docstring/tests/test_numpydoc.py +1100 -0
- singlestoredb/docstring/tests/test_parse_from_object.py +109 -0
- singlestoredb/docstring/tests/test_parser.py +248 -0
- singlestoredb/docstring/tests/test_rest.py +547 -0
- singlestoredb/docstring/tests/test_util.py +70 -0
- singlestoredb/docstring/util.py +141 -0
- singlestoredb/functions/decorator.py +19 -18
- singlestoredb/functions/ext/asgi.py +304 -32
- singlestoredb/functions/ext/timer.py +2 -11
- singlestoredb/functions/ext/utils.py +55 -6
- singlestoredb/functions/signature.py +374 -241
- singlestoredb/fusion/handlers/files.py +4 -4
- singlestoredb/fusion/handlers/models.py +1 -1
- singlestoredb/fusion/handlers/stage.py +4 -4
- singlestoredb/management/cluster.py +1 -1
- singlestoredb/management/manager.py +15 -5
- singlestoredb/management/region.py +12 -2
- singlestoredb/management/workspace.py +17 -25
- singlestoredb/tests/ext_funcs/__init__.py +39 -0
- singlestoredb/tests/test_connection.py +18 -8
- singlestoredb/tests/test_management.py +24 -57
- singlestoredb/tests/test_udf.py +43 -15
- {singlestoredb-1.15.0.dist-info → singlestoredb-1.15.2.dist-info}/METADATA +1 -1
- {singlestoredb-1.15.0.dist-info → singlestoredb-1.15.2.dist-info}/RECORD +49 -30
- {singlestoredb-1.15.0.dist-info → singlestoredb-1.15.2.dist-info}/LICENSE +0 -0
- {singlestoredb-1.15.0.dist-info → singlestoredb-1.15.2.dist-info}/WHEEL +0 -0
- {singlestoredb-1.15.0.dist-info → singlestoredb-1.15.2.dist-info}/entry_points.txt +0 -0
- {singlestoredb-1.15.0.dist-info → singlestoredb-1.15.2.dist-info}/top_level.txt +0 -0
|
@@ -73,6 +73,8 @@ from ..signature import signature_to_sql
|
|
|
73
73
|
from ..typing import Masked
|
|
74
74
|
from ..typing import Table
|
|
75
75
|
from .timer import Timer
|
|
76
|
+
from singlestoredb.docstring.parser import parse
|
|
77
|
+
from singlestoredb.functions.dtypes import escape_name
|
|
76
78
|
|
|
77
79
|
try:
|
|
78
80
|
import cloudpickle
|
|
@@ -89,7 +91,6 @@ except ImportError:
|
|
|
89
91
|
|
|
90
92
|
logger = utils.get_logger('singlestoredb.functions.ext.asgi')
|
|
91
93
|
|
|
92
|
-
|
|
93
94
|
# If a number of processes is specified, create a pool of workers
|
|
94
95
|
num_processes = max(0, int(os.environ.get('SINGLESTOREDB_EXT_NUM_PROCESSES', 0)))
|
|
95
96
|
if num_processes > 1:
|
|
@@ -538,6 +539,8 @@ def make_func(
|
|
|
538
539
|
Name of the function to create
|
|
539
540
|
func : Callable
|
|
540
541
|
The function to call as the endpoint
|
|
542
|
+
database : str, optional
|
|
543
|
+
The database to use for the function definition
|
|
541
544
|
|
|
542
545
|
Returns
|
|
543
546
|
-------
|
|
@@ -615,7 +618,7 @@ async def cancel_on_disconnect(
|
|
|
615
618
|
"""Cancel request if client disconnects."""
|
|
616
619
|
while True:
|
|
617
620
|
message = await receive()
|
|
618
|
-
if message
|
|
621
|
+
if message.get('type', '') == 'http.disconnect':
|
|
619
622
|
raise asyncio.CancelledError(
|
|
620
623
|
'Function call was cancelled by client',
|
|
621
624
|
)
|
|
@@ -674,6 +677,24 @@ class Application(object):
|
|
|
674
677
|
link_credentials : Dict[str, Any], optional
|
|
675
678
|
The CREDENTIALS section of a LINK definition. This dictionary gets
|
|
676
679
|
converted to JSON for the CREATE LINK call.
|
|
680
|
+
name_prefix : str, optional
|
|
681
|
+
Prefix to add to function names when registering with the database
|
|
682
|
+
name_suffix : str, optional
|
|
683
|
+
Suffix to add to function names when registering with the database
|
|
684
|
+
function_database : str, optional
|
|
685
|
+
The database to use for external function definitions.
|
|
686
|
+
log_file : str, optional
|
|
687
|
+
File path to write logs to instead of console. If None, logs are
|
|
688
|
+
written to console. When specified, application logger handlers
|
|
689
|
+
are replaced with a file handler.
|
|
690
|
+
log_level : str, optional
|
|
691
|
+
Logging level for the application logger. Valid values are 'info',
|
|
692
|
+
'debug', 'warning', 'error'. Defaults to 'info'.
|
|
693
|
+
disable_metrics : bool, optional
|
|
694
|
+
Disable logging of function call metrics. Defaults to False.
|
|
695
|
+
app_name : str, optional
|
|
696
|
+
Name for the application instance. Used to create a logger-specific
|
|
697
|
+
name. If not provided, a random name will be generated.
|
|
677
698
|
|
|
678
699
|
"""
|
|
679
700
|
|
|
@@ -816,6 +837,7 @@ class Application(object):
|
|
|
816
837
|
invoke_path = ('invoke',)
|
|
817
838
|
show_create_function_path = ('show', 'create_function')
|
|
818
839
|
show_function_info_path = ('show', 'function_info')
|
|
840
|
+
status = ('status',)
|
|
819
841
|
|
|
820
842
|
def __init__(
|
|
821
843
|
self,
|
|
@@ -838,6 +860,11 @@ class Application(object):
|
|
|
838
860
|
link_credentials: Optional[Dict[str, Any]] = None,
|
|
839
861
|
name_prefix: str = get_option('external_function.name_prefix'),
|
|
840
862
|
name_suffix: str = get_option('external_function.name_suffix'),
|
|
863
|
+
function_database: Optional[str] = None,
|
|
864
|
+
log_file: Optional[str] = get_option('external_function.log_file'),
|
|
865
|
+
log_level: str = get_option('external_function.log_level'),
|
|
866
|
+
disable_metrics: bool = get_option('external_function.disable_metrics'),
|
|
867
|
+
app_name: Optional[str] = get_option('external_function.app_name'),
|
|
841
868
|
) -> None:
|
|
842
869
|
if link_name and (link_config or link_credentials):
|
|
843
870
|
raise ValueError(
|
|
@@ -854,6 +881,15 @@ class Application(object):
|
|
|
854
881
|
get_option('external_function.link_credentials') or '{}',
|
|
855
882
|
) or None
|
|
856
883
|
|
|
884
|
+
# Generate application name if not provided
|
|
885
|
+
if app_name is None:
|
|
886
|
+
app_name = f'udf_app_{secrets.token_hex(4)}'
|
|
887
|
+
|
|
888
|
+
self.name = app_name
|
|
889
|
+
|
|
890
|
+
# Create logger instance specific to this application
|
|
891
|
+
self.logger = utils.get_logger(f'singlestoredb.functions.ext.asgi.{self.name}')
|
|
892
|
+
|
|
857
893
|
# List of functions specs
|
|
858
894
|
specs: List[Union[str, Callable[..., Any], ModuleType]] = []
|
|
859
895
|
|
|
@@ -944,6 +980,98 @@ class Application(object):
|
|
|
944
980
|
self.link_credentials = link_credentials
|
|
945
981
|
self.endpoints = endpoints
|
|
946
982
|
self.external_functions = external_functions
|
|
983
|
+
self.function_database = function_database
|
|
984
|
+
self.log_file = log_file
|
|
985
|
+
self.log_level = log_level
|
|
986
|
+
self.disable_metrics = disable_metrics
|
|
987
|
+
|
|
988
|
+
# Configure logging
|
|
989
|
+
self._configure_logging()
|
|
990
|
+
|
|
991
|
+
def _configure_logging(self) -> None:
|
|
992
|
+
"""Configure logging based on the log_file settings."""
|
|
993
|
+
# Set logger level
|
|
994
|
+
self.logger.setLevel(getattr(logging, self.log_level.upper()))
|
|
995
|
+
|
|
996
|
+
# Remove all existing handlers to ensure clean configuration
|
|
997
|
+
self.logger.handlers.clear()
|
|
998
|
+
|
|
999
|
+
# Configure log file if specified
|
|
1000
|
+
if self.log_file:
|
|
1001
|
+
# Create file handler
|
|
1002
|
+
file_handler = logging.FileHandler(self.log_file)
|
|
1003
|
+
file_handler.setLevel(getattr(logging, self.log_level.upper()))
|
|
1004
|
+
|
|
1005
|
+
# Use JSON formatter for file logging
|
|
1006
|
+
formatter = utils.JSONFormatter()
|
|
1007
|
+
file_handler.setFormatter(formatter)
|
|
1008
|
+
|
|
1009
|
+
# Add the handler to the logger
|
|
1010
|
+
self.logger.addHandler(file_handler)
|
|
1011
|
+
else:
|
|
1012
|
+
# For console logging, create a new stream handler with JSON formatter
|
|
1013
|
+
console_handler = logging.StreamHandler()
|
|
1014
|
+
console_handler.setLevel(getattr(logging, self.log_level.upper()))
|
|
1015
|
+
console_handler.setFormatter(utils.JSONFormatter())
|
|
1016
|
+
self.logger.addHandler(console_handler)
|
|
1017
|
+
|
|
1018
|
+
# Prevent propagation to avoid duplicate or differently formatted messages
|
|
1019
|
+
self.logger.propagate = False
|
|
1020
|
+
|
|
1021
|
+
def get_uvicorn_log_config(self) -> Dict[str, Any]:
|
|
1022
|
+
"""
|
|
1023
|
+
Create uvicorn log config that matches the Application's logging format.
|
|
1024
|
+
|
|
1025
|
+
This method returns the log configuration used by uvicorn, allowing external
|
|
1026
|
+
users to match the logging format of the Application class.
|
|
1027
|
+
|
|
1028
|
+
Returns
|
|
1029
|
+
-------
|
|
1030
|
+
Dict[str, Any]
|
|
1031
|
+
Log configuration dictionary compatible with uvicorn's log_config parameter
|
|
1032
|
+
|
|
1033
|
+
"""
|
|
1034
|
+
log_config = {
|
|
1035
|
+
'version': 1,
|
|
1036
|
+
'disable_existing_loggers': False,
|
|
1037
|
+
'formatters': {
|
|
1038
|
+
'json': {
|
|
1039
|
+
'()': 'singlestoredb.functions.ext.utils.JSONFormatter',
|
|
1040
|
+
},
|
|
1041
|
+
},
|
|
1042
|
+
'handlers': {
|
|
1043
|
+
'default': {
|
|
1044
|
+
'class': (
|
|
1045
|
+
'logging.FileHandler' if self.log_file
|
|
1046
|
+
else 'logging.StreamHandler'
|
|
1047
|
+
),
|
|
1048
|
+
'formatter': 'json',
|
|
1049
|
+
},
|
|
1050
|
+
},
|
|
1051
|
+
'loggers': {
|
|
1052
|
+
'uvicorn': {
|
|
1053
|
+
'handlers': ['default'],
|
|
1054
|
+
'level': self.log_level.upper(),
|
|
1055
|
+
'propagate': False,
|
|
1056
|
+
},
|
|
1057
|
+
'uvicorn.error': {
|
|
1058
|
+
'handlers': ['default'],
|
|
1059
|
+
'level': self.log_level.upper(),
|
|
1060
|
+
'propagate': False,
|
|
1061
|
+
},
|
|
1062
|
+
'uvicorn.access': {
|
|
1063
|
+
'handlers': ['default'],
|
|
1064
|
+
'level': self.log_level.upper(),
|
|
1065
|
+
'propagate': False,
|
|
1066
|
+
},
|
|
1067
|
+
},
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
# Add filename to file handler if log file is specified
|
|
1071
|
+
if self.log_file:
|
|
1072
|
+
log_config['handlers']['default']['filename'] = self.log_file # type: ignore
|
|
1073
|
+
|
|
1074
|
+
return log_config
|
|
947
1075
|
|
|
948
1076
|
async def __call__(
|
|
949
1077
|
self,
|
|
@@ -967,19 +1095,22 @@ class Application(object):
|
|
|
967
1095
|
request_id = str(uuid.uuid4())
|
|
968
1096
|
|
|
969
1097
|
timer = Timer(
|
|
1098
|
+
app_name=self.name,
|
|
970
1099
|
id=request_id,
|
|
971
1100
|
timestamp=datetime.datetime.now(
|
|
972
1101
|
datetime.timezone.utc,
|
|
973
1102
|
).strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
|
|
974
1103
|
)
|
|
975
1104
|
call_timer = Timer(
|
|
1105
|
+
app_name=self.name,
|
|
976
1106
|
id=request_id,
|
|
977
1107
|
timestamp=datetime.datetime.now(
|
|
978
1108
|
datetime.timezone.utc,
|
|
979
1109
|
).strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
|
|
980
1110
|
)
|
|
981
1111
|
|
|
982
|
-
|
|
1112
|
+
if scope['type'] != 'http':
|
|
1113
|
+
raise ValueError(f"Expected HTTP scope, got {scope['type']}")
|
|
983
1114
|
|
|
984
1115
|
method = scope['method']
|
|
985
1116
|
path = tuple(x for x in scope['path'].split('/') if x)
|
|
@@ -992,6 +1123,7 @@ class Application(object):
|
|
|
992
1123
|
accepts = headers.get(b'accepts', content_type)
|
|
993
1124
|
func_name = headers.get(b's2-ef-name', b'')
|
|
994
1125
|
func_endpoint = self.endpoints.get(func_name)
|
|
1126
|
+
ignore_cancel = headers.get(b's2-ef-ignore-cancel', b'false') == b'true'
|
|
995
1127
|
|
|
996
1128
|
timer.metadata['function'] = func_name.decode('utf-8') if func_name else ''
|
|
997
1129
|
call_timer.metadata['function'] = timer.metadata['function']
|
|
@@ -1004,14 +1136,15 @@ class Application(object):
|
|
|
1004
1136
|
# Call the endpoint
|
|
1005
1137
|
if method == 'POST' and func is not None and path == self.invoke_path:
|
|
1006
1138
|
|
|
1007
|
-
logger.info(
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
'
|
|
1011
|
-
'
|
|
1139
|
+
self.logger.info(
|
|
1140
|
+
'Function call initiated',
|
|
1141
|
+
extra={
|
|
1142
|
+
'app_name': self.name,
|
|
1143
|
+
'request_id': request_id,
|
|
1144
|
+
'function_name': func_name.decode('utf-8'),
|
|
1012
1145
|
'content_type': content_type.decode('utf-8'),
|
|
1013
1146
|
'accepts': accepts.decode('utf-8'),
|
|
1014
|
-
}
|
|
1147
|
+
},
|
|
1015
1148
|
)
|
|
1016
1149
|
|
|
1017
1150
|
args_data_format = func_info['args_data_format']
|
|
@@ -1021,7 +1154,7 @@ class Application(object):
|
|
|
1021
1154
|
with timer('receive_data'):
|
|
1022
1155
|
while more_body:
|
|
1023
1156
|
request = await receive()
|
|
1024
|
-
if request
|
|
1157
|
+
if request.get('type', '') == 'http.disconnect':
|
|
1025
1158
|
raise RuntimeError('client disconnected')
|
|
1026
1159
|
data.append(request['body'])
|
|
1027
1160
|
more_body = request.get('more_body', False)
|
|
@@ -1051,7 +1184,8 @@ class Application(object):
|
|
|
1051
1184
|
),
|
|
1052
1185
|
)
|
|
1053
1186
|
disconnect_task = asyncio.create_task(
|
|
1054
|
-
|
|
1187
|
+
asyncio.sleep(int(1e9))
|
|
1188
|
+
if ignore_cancel else cancel_on_disconnect(receive),
|
|
1055
1189
|
)
|
|
1056
1190
|
timeout_task = asyncio.create_task(
|
|
1057
1191
|
cancel_on_timeout(func_info['timeout']),
|
|
@@ -1090,8 +1224,14 @@ class Application(object):
|
|
|
1090
1224
|
await send(output_handler['response'])
|
|
1091
1225
|
|
|
1092
1226
|
except asyncio.TimeoutError:
|
|
1093
|
-
|
|
1094
|
-
'
|
|
1227
|
+
self.logger.exception(
|
|
1228
|
+
'Function call timeout',
|
|
1229
|
+
extra={
|
|
1230
|
+
'app_name': self.name,
|
|
1231
|
+
'request_id': request_id,
|
|
1232
|
+
'function_name': func_name.decode('utf-8'),
|
|
1233
|
+
'timeout': func_info['timeout'],
|
|
1234
|
+
},
|
|
1095
1235
|
)
|
|
1096
1236
|
body = (
|
|
1097
1237
|
'[TimeoutError] Function call timed out after ' +
|
|
@@ -1101,15 +1241,26 @@ class Application(object):
|
|
|
1101
1241
|
await send(self.error_response_dict)
|
|
1102
1242
|
|
|
1103
1243
|
except asyncio.CancelledError:
|
|
1104
|
-
|
|
1105
|
-
'Function call cancelled
|
|
1244
|
+
self.logger.exception(
|
|
1245
|
+
'Function call cancelled',
|
|
1246
|
+
extra={
|
|
1247
|
+
'app_name': self.name,
|
|
1248
|
+
'request_id': request_id,
|
|
1249
|
+
'function_name': func_name.decode('utf-8'),
|
|
1250
|
+
},
|
|
1106
1251
|
)
|
|
1107
1252
|
body = b'[CancelledError] Function call was cancelled'
|
|
1108
1253
|
await send(self.error_response_dict)
|
|
1109
1254
|
|
|
1110
1255
|
except Exception as e:
|
|
1111
|
-
|
|
1112
|
-
'
|
|
1256
|
+
self.logger.exception(
|
|
1257
|
+
'Function call error',
|
|
1258
|
+
extra={
|
|
1259
|
+
'app_name': self.name,
|
|
1260
|
+
'request_id': request_id,
|
|
1261
|
+
'function_name': func_name.decode('utf-8'),
|
|
1262
|
+
'exception_type': type(e).__name__,
|
|
1263
|
+
},
|
|
1113
1264
|
)
|
|
1114
1265
|
body = f'[{type(e).__name__}] {str(e).strip()}'.encode('utf-8')
|
|
1115
1266
|
await send(self.error_response_dict)
|
|
@@ -1130,6 +1281,7 @@ class Application(object):
|
|
|
1130
1281
|
endpoint_info['signature'],
|
|
1131
1282
|
url=self.url or reflected_url,
|
|
1132
1283
|
data_format=self.data_format,
|
|
1284
|
+
database=self.function_database or None,
|
|
1133
1285
|
),
|
|
1134
1286
|
)
|
|
1135
1287
|
body = '\n'.join(syntax).encode('utf-8')
|
|
@@ -1142,6 +1294,11 @@ class Application(object):
|
|
|
1142
1294
|
body = json.dumps(dict(functions=functions)).encode('utf-8')
|
|
1143
1295
|
await send(self.text_response_dict)
|
|
1144
1296
|
|
|
1297
|
+
# Return status
|
|
1298
|
+
elif method == 'GET' and path == self.status:
|
|
1299
|
+
body = json.dumps(dict(status='ok')).encode('utf-8')
|
|
1300
|
+
await send(self.text_response_dict)
|
|
1301
|
+
|
|
1145
1302
|
# Path not found
|
|
1146
1303
|
else:
|
|
1147
1304
|
body = b''
|
|
@@ -1156,7 +1313,17 @@ class Application(object):
|
|
|
1156
1313
|
for k, v in call_timer.metrics.items():
|
|
1157
1314
|
timer.metrics[k] = v
|
|
1158
1315
|
|
|
1159
|
-
|
|
1316
|
+
if not self.disable_metrics:
|
|
1317
|
+
metrics = timer.finish()
|
|
1318
|
+
self.logger.info(
|
|
1319
|
+
'Function call metrics',
|
|
1320
|
+
extra={
|
|
1321
|
+
'app_name': self.name,
|
|
1322
|
+
'request_id': request_id,
|
|
1323
|
+
'function_name': timer.metadata.get('function', ''),
|
|
1324
|
+
'metrics': metrics,
|
|
1325
|
+
},
|
|
1326
|
+
)
|
|
1160
1327
|
|
|
1161
1328
|
def _create_link(
|
|
1162
1329
|
self,
|
|
@@ -1184,20 +1351,27 @@ class Application(object):
|
|
|
1184
1351
|
def _locate_app_functions(self, cur: Any) -> Tuple[Set[str], Set[str]]:
|
|
1185
1352
|
"""Locate all current functions and links belonging to this app."""
|
|
1186
1353
|
funcs, links = set(), set()
|
|
1187
|
-
|
|
1354
|
+
if self.function_database:
|
|
1355
|
+
database_prefix = escape_name(self.function_database) + '.'
|
|
1356
|
+
cur.execute(f'SHOW FUNCTIONS IN {escape_name(self.function_database)}')
|
|
1357
|
+
else:
|
|
1358
|
+
database_prefix = ''
|
|
1359
|
+
cur.execute('SHOW FUNCTIONS')
|
|
1360
|
+
|
|
1188
1361
|
for row in list(cur):
|
|
1189
1362
|
name, ftype, link = row[0], row[1], row[-1]
|
|
1190
1363
|
# Only look at external functions
|
|
1191
1364
|
if 'external' not in ftype.lower():
|
|
1192
1365
|
continue
|
|
1193
1366
|
# See if function URL matches url
|
|
1194
|
-
cur.execute(f'SHOW CREATE FUNCTION
|
|
1367
|
+
cur.execute(f'SHOW CREATE FUNCTION {database_prefix}{escape_name(name)}')
|
|
1195
1368
|
for fname, _, code, *_ in list(cur):
|
|
1196
1369
|
m = re.search(r" (?:\w+) (?:SERVICE|MANAGED) '([^']+)'", code)
|
|
1197
1370
|
if m and m.group(1) == self.url:
|
|
1198
|
-
funcs.add(fname)
|
|
1371
|
+
funcs.add(f'{database_prefix}{escape_name(fname)}')
|
|
1199
1372
|
if link and re.match(r'^py_ext_func_link_\S{14}$', link):
|
|
1200
1373
|
links.add(link)
|
|
1374
|
+
|
|
1201
1375
|
return funcs, links
|
|
1202
1376
|
|
|
1203
1377
|
def get_function_info(
|
|
@@ -1206,9 +1380,11 @@ class Application(object):
|
|
|
1206
1380
|
) -> Dict[str, Any]:
|
|
1207
1381
|
"""
|
|
1208
1382
|
Return the functions and function signature information.
|
|
1383
|
+
|
|
1209
1384
|
Returns
|
|
1210
1385
|
-------
|
|
1211
1386
|
Dict[str, Any]
|
|
1387
|
+
|
|
1212
1388
|
"""
|
|
1213
1389
|
functions = {}
|
|
1214
1390
|
no_default = object()
|
|
@@ -1220,20 +1396,71 @@ class Application(object):
|
|
|
1220
1396
|
sig = info['signature']
|
|
1221
1397
|
sql_map[sig['name']] = sql
|
|
1222
1398
|
|
|
1223
|
-
for key, (
|
|
1399
|
+
for key, (func, info) in self.endpoints.items():
|
|
1400
|
+
# Get info from docstring
|
|
1401
|
+
doc_summary = ''
|
|
1402
|
+
doc_long_description = ''
|
|
1403
|
+
doc_params = {}
|
|
1404
|
+
doc_returns = None
|
|
1405
|
+
doc_examples = []
|
|
1406
|
+
if func.__doc__:
|
|
1407
|
+
try:
|
|
1408
|
+
docs = parse(func.__doc__)
|
|
1409
|
+
doc_params = {p.arg_name: p for p in docs.params}
|
|
1410
|
+
doc_returns = docs.returns
|
|
1411
|
+
if not docs.short_description and docs.long_description:
|
|
1412
|
+
doc_summary = docs.long_description or ''
|
|
1413
|
+
else:
|
|
1414
|
+
doc_summary = docs.short_description or ''
|
|
1415
|
+
doc_long_description = docs.long_description or ''
|
|
1416
|
+
for ex in docs.examples:
|
|
1417
|
+
ex_dict: Dict[str, Any] = {
|
|
1418
|
+
'description': None,
|
|
1419
|
+
'code': None,
|
|
1420
|
+
'output': None,
|
|
1421
|
+
}
|
|
1422
|
+
if ex.description:
|
|
1423
|
+
ex_dict['description'] = ex.description
|
|
1424
|
+
if ex.snippet:
|
|
1425
|
+
code, output = [], []
|
|
1426
|
+
for line in ex.snippet.split('\n'):
|
|
1427
|
+
line = line.rstrip()
|
|
1428
|
+
if re.match(r'^(\w+>|>>>|\.\.\.)', line):
|
|
1429
|
+
code.append(line)
|
|
1430
|
+
else:
|
|
1431
|
+
output.append(line)
|
|
1432
|
+
ex_dict['code'] = '\n'.join(code) or None
|
|
1433
|
+
ex_dict['output'] = '\n'.join(output) or None
|
|
1434
|
+
if ex.post_snippet:
|
|
1435
|
+
ex_dict['postscript'] = ex.post_snippet
|
|
1436
|
+
doc_examples.append(ex_dict)
|
|
1437
|
+
|
|
1438
|
+
except Exception as e:
|
|
1439
|
+
self.logger.warning(
|
|
1440
|
+
'Could not parse docstring for function',
|
|
1441
|
+
extra={
|
|
1442
|
+
'app_name': self.name,
|
|
1443
|
+
'function_name': key.decode('utf-8'),
|
|
1444
|
+
'error': str(e),
|
|
1445
|
+
},
|
|
1446
|
+
)
|
|
1447
|
+
|
|
1224
1448
|
if not func_name or key == func_name:
|
|
1225
1449
|
sig = info['signature']
|
|
1226
1450
|
args = []
|
|
1227
1451
|
|
|
1228
1452
|
# Function arguments
|
|
1229
|
-
for a in sig.get('args', []):
|
|
1453
|
+
for i, a in enumerate(sig.get('args', [])):
|
|
1454
|
+
name = a['name']
|
|
1230
1455
|
dtype = a['dtype']
|
|
1231
1456
|
nullable = '?' in dtype
|
|
1232
1457
|
args.append(
|
|
1233
1458
|
dict(
|
|
1234
|
-
name=
|
|
1459
|
+
name=name,
|
|
1235
1460
|
dtype=dtype.replace('?', ''),
|
|
1236
1461
|
nullable=nullable,
|
|
1462
|
+
description=(doc_params[name].description or '')
|
|
1463
|
+
if name in doc_params else '',
|
|
1237
1464
|
),
|
|
1238
1465
|
)
|
|
1239
1466
|
if a.get('default', no_default) is not no_default:
|
|
@@ -1250,6 +1477,8 @@ class Application(object):
|
|
|
1250
1477
|
dict(
|
|
1251
1478
|
dtype=dtype.replace('?', ''),
|
|
1252
1479
|
nullable=nullable,
|
|
1480
|
+
description=doc_returns.description
|
|
1481
|
+
if doc_returns else '',
|
|
1253
1482
|
),
|
|
1254
1483
|
)
|
|
1255
1484
|
if a.get('name', None):
|
|
@@ -1263,6 +1492,9 @@ class Application(object):
|
|
|
1263
1492
|
returns=returns,
|
|
1264
1493
|
function_type=info['function_type'],
|
|
1265
1494
|
sql_statement=sql,
|
|
1495
|
+
summary=doc_summary,
|
|
1496
|
+
long_description=doc_long_description,
|
|
1497
|
+
examples=doc_examples,
|
|
1266
1498
|
)
|
|
1267
1499
|
|
|
1268
1500
|
return functions
|
|
@@ -1303,6 +1535,7 @@ class Application(object):
|
|
|
1303
1535
|
app_mode=self.app_mode,
|
|
1304
1536
|
replace=replace,
|
|
1305
1537
|
link=link or None,
|
|
1538
|
+
database=self.function_database or None,
|
|
1306
1539
|
),
|
|
1307
1540
|
)
|
|
1308
1541
|
|
|
@@ -1332,7 +1565,7 @@ class Application(object):
|
|
|
1332
1565
|
if replace:
|
|
1333
1566
|
funcs, links = self._locate_app_functions(cur)
|
|
1334
1567
|
for fname in funcs:
|
|
1335
|
-
cur.execute(f'DROP FUNCTION IF EXISTS
|
|
1568
|
+
cur.execute(f'DROP FUNCTION IF EXISTS {fname}')
|
|
1336
1569
|
for link in links:
|
|
1337
1570
|
cur.execute(f'DROP LINK {link}')
|
|
1338
1571
|
for func in self.get_create_functions(replace=replace):
|
|
@@ -1358,7 +1591,7 @@ class Application(object):
|
|
|
1358
1591
|
with conn.cursor() as cur:
|
|
1359
1592
|
funcs, links = self._locate_app_functions(cur)
|
|
1360
1593
|
for fname in funcs:
|
|
1361
|
-
cur.execute(f'DROP FUNCTION IF EXISTS
|
|
1594
|
+
cur.execute(f'DROP FUNCTION IF EXISTS {fname}')
|
|
1362
1595
|
for link in links:
|
|
1363
1596
|
cur.execute(f'DROP LINK {link}')
|
|
1364
1597
|
|
|
@@ -1415,6 +1648,7 @@ class Application(object):
|
|
|
1415
1648
|
b'accepts': accepts[data_format.lower()],
|
|
1416
1649
|
b's2-ef-name': name.encode('utf-8'),
|
|
1417
1650
|
b's2-ef-version': data_version.encode('utf-8'),
|
|
1651
|
+
b's2-ef-ignore-cancel': b'true',
|
|
1418
1652
|
},
|
|
1419
1653
|
)
|
|
1420
1654
|
|
|
@@ -1663,6 +1897,22 @@ def main(argv: Optional[List[str]] = None) -> None:
|
|
|
1663
1897
|
),
|
|
1664
1898
|
help='logging level',
|
|
1665
1899
|
)
|
|
1900
|
+
parser.add_argument(
|
|
1901
|
+
'--log-file', metavar='filepath',
|
|
1902
|
+
default=defaults.get(
|
|
1903
|
+
'log_file',
|
|
1904
|
+
get_option('external_function.log_file'),
|
|
1905
|
+
),
|
|
1906
|
+
help='File path to write logs to instead of console',
|
|
1907
|
+
)
|
|
1908
|
+
parser.add_argument(
|
|
1909
|
+
'--disable-metrics', action='store_true',
|
|
1910
|
+
default=defaults.get(
|
|
1911
|
+
'disable_metrics',
|
|
1912
|
+
get_option('external_function.disable_metrics'),
|
|
1913
|
+
),
|
|
1914
|
+
help='Disable logging of function call metrics',
|
|
1915
|
+
)
|
|
1666
1916
|
parser.add_argument(
|
|
1667
1917
|
'--name-prefix', metavar='name_prefix',
|
|
1668
1918
|
default=defaults.get(
|
|
@@ -1679,6 +1929,22 @@ def main(argv: Optional[List[str]] = None) -> None:
|
|
|
1679
1929
|
),
|
|
1680
1930
|
help='Suffix to add to function names',
|
|
1681
1931
|
)
|
|
1932
|
+
parser.add_argument(
|
|
1933
|
+
'--function-database', metavar='function_database',
|
|
1934
|
+
default=defaults.get(
|
|
1935
|
+
'function_database',
|
|
1936
|
+
get_option('external_function.function_database'),
|
|
1937
|
+
),
|
|
1938
|
+
help='Database to use for the function definition',
|
|
1939
|
+
)
|
|
1940
|
+
parser.add_argument(
|
|
1941
|
+
'--app-name', metavar='app_name',
|
|
1942
|
+
default=defaults.get(
|
|
1943
|
+
'app_name',
|
|
1944
|
+
get_option('external_function.app_name'),
|
|
1945
|
+
),
|
|
1946
|
+
help='Name for the application instance',
|
|
1947
|
+
)
|
|
1682
1948
|
parser.add_argument(
|
|
1683
1949
|
'functions', metavar='module.or.func.path', nargs='*',
|
|
1684
1950
|
help='functions or modules to export in UDF server',
|
|
@@ -1686,8 +1952,6 @@ def main(argv: Optional[List[str]] = None) -> None:
|
|
|
1686
1952
|
|
|
1687
1953
|
args = parser.parse_args(argv)
|
|
1688
1954
|
|
|
1689
|
-
logger.setLevel(getattr(logging, args.log_level.upper()))
|
|
1690
|
-
|
|
1691
1955
|
if i > 0:
|
|
1692
1956
|
break
|
|
1693
1957
|
|
|
@@ -1778,6 +2042,11 @@ def main(argv: Optional[List[str]] = None) -> None:
|
|
|
1778
2042
|
app_mode='remote',
|
|
1779
2043
|
name_prefix=args.name_prefix,
|
|
1780
2044
|
name_suffix=args.name_suffix,
|
|
2045
|
+
function_database=args.function_database or None,
|
|
2046
|
+
log_file=args.log_file,
|
|
2047
|
+
log_level=args.log_level,
|
|
2048
|
+
disable_metrics=args.disable_metrics,
|
|
2049
|
+
app_name=args.app_name,
|
|
1781
2050
|
)
|
|
1782
2051
|
|
|
1783
2052
|
funcs = app.get_create_functions(replace=args.replace_existing)
|
|
@@ -1785,11 +2054,11 @@ def main(argv: Optional[List[str]] = None) -> None:
|
|
|
1785
2054
|
raise RuntimeError('no functions specified')
|
|
1786
2055
|
|
|
1787
2056
|
for f in funcs:
|
|
1788
|
-
logger.info(f)
|
|
2057
|
+
app.logger.info(f)
|
|
1789
2058
|
|
|
1790
2059
|
try:
|
|
1791
2060
|
if args.db:
|
|
1792
|
-
logger.info('
|
|
2061
|
+
app.logger.info('Registering functions with database')
|
|
1793
2062
|
app.register_functions(
|
|
1794
2063
|
args.db,
|
|
1795
2064
|
replace=args.replace_existing,
|
|
@@ -1804,6 +2073,9 @@ def main(argv: Optional[List[str]] = None) -> None:
|
|
|
1804
2073
|
).items() if v is not None
|
|
1805
2074
|
}
|
|
1806
2075
|
|
|
2076
|
+
# Configure uvicorn logging to use JSON format matching Application's format
|
|
2077
|
+
app_args['log_config'] = app.get_uvicorn_log_config()
|
|
2078
|
+
|
|
1807
2079
|
if use_async:
|
|
1808
2080
|
asyncio.create_task(_run_uvicorn(uvicorn, app, app_args, db=args.db))
|
|
1809
2081
|
else:
|
|
@@ -1811,7 +2083,7 @@ def main(argv: Optional[List[str]] = None) -> None:
|
|
|
1811
2083
|
|
|
1812
2084
|
finally:
|
|
1813
2085
|
if not use_async and args.db:
|
|
1814
|
-
logger.info('
|
|
2086
|
+
app.logger.info('Dropping functions from database')
|
|
1815
2087
|
app.drop_functions(args.db)
|
|
1816
2088
|
|
|
1817
2089
|
|
|
@@ -1824,7 +2096,7 @@ async def _run_uvicorn(
|
|
|
1824
2096
|
"""Run uvicorn server and clean up functions after shutdown."""
|
|
1825
2097
|
await uvicorn.Server(uvicorn.Config(app, **app_args)).serve()
|
|
1826
2098
|
if db:
|
|
1827
|
-
logger.info('
|
|
2099
|
+
app.logger.info('Dropping functions from database')
|
|
1828
2100
|
app.drop_functions(db)
|
|
1829
2101
|
|
|
1830
2102
|
|
|
@@ -4,10 +4,6 @@ from typing import Any
|
|
|
4
4
|
from typing import Dict
|
|
5
5
|
from typing import Optional
|
|
6
6
|
|
|
7
|
-
from . import utils
|
|
8
|
-
|
|
9
|
-
logger = utils.get_logger('singlestoredb.functions.ext.metrics')
|
|
10
|
-
|
|
11
7
|
|
|
12
8
|
class RoundedFloatEncoder(json.JSONEncoder):
|
|
13
9
|
|
|
@@ -87,12 +83,7 @@ class Timer:
|
|
|
87
83
|
self.entries.clear()
|
|
88
84
|
self._current_key = None
|
|
89
85
|
|
|
90
|
-
def finish(self) ->
|
|
86
|
+
def finish(self) -> Dict[str, Any]:
|
|
91
87
|
"""Finish the current timing context and store the elapsed time."""
|
|
92
88
|
self.metrics['total'] = time.perf_counter() - self.start_time
|
|
93
|
-
self.
|
|
94
|
-
|
|
95
|
-
def log_metrics(self) -> None:
|
|
96
|
-
if self.metadata.get('function'):
|
|
97
|
-
result = dict(type='function_metrics', **self.metadata, **self.metrics)
|
|
98
|
-
logger.info(json.dumps(result, cls=RoundedFloatEncoder))
|
|
89
|
+
return dict(type='function_metrics', **self.metadata, **self.metrics)
|