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.

Files changed (49) hide show
  1. _singlestoredb_accel.pyd +0 -0
  2. singlestoredb/__init__.py +1 -1
  3. singlestoredb/ai/chat.py +14 -0
  4. singlestoredb/apps/_python_udfs.py +18 -3
  5. singlestoredb/apps/_stdout_supress.py +1 -1
  6. singlestoredb/apps/_uvicorn_util.py +4 -0
  7. singlestoredb/config.py +24 -0
  8. singlestoredb/converters.py +1 -1
  9. singlestoredb/docstring/__init__.py +33 -0
  10. singlestoredb/docstring/attrdoc.py +126 -0
  11. singlestoredb/docstring/common.py +230 -0
  12. singlestoredb/docstring/epydoc.py +267 -0
  13. singlestoredb/docstring/google.py +412 -0
  14. singlestoredb/docstring/numpydoc.py +562 -0
  15. singlestoredb/docstring/parser.py +100 -0
  16. singlestoredb/docstring/py.typed +1 -0
  17. singlestoredb/docstring/rest.py +256 -0
  18. singlestoredb/docstring/tests/__init__.py +1 -0
  19. singlestoredb/docstring/tests/_pydoctor.py +21 -0
  20. singlestoredb/docstring/tests/test_epydoc.py +729 -0
  21. singlestoredb/docstring/tests/test_google.py +1007 -0
  22. singlestoredb/docstring/tests/test_numpydoc.py +1100 -0
  23. singlestoredb/docstring/tests/test_parse_from_object.py +109 -0
  24. singlestoredb/docstring/tests/test_parser.py +248 -0
  25. singlestoredb/docstring/tests/test_rest.py +547 -0
  26. singlestoredb/docstring/tests/test_util.py +70 -0
  27. singlestoredb/docstring/util.py +141 -0
  28. singlestoredb/functions/decorator.py +19 -18
  29. singlestoredb/functions/ext/asgi.py +304 -32
  30. singlestoredb/functions/ext/timer.py +2 -11
  31. singlestoredb/functions/ext/utils.py +55 -6
  32. singlestoredb/functions/signature.py +374 -241
  33. singlestoredb/fusion/handlers/files.py +4 -4
  34. singlestoredb/fusion/handlers/models.py +1 -1
  35. singlestoredb/fusion/handlers/stage.py +4 -4
  36. singlestoredb/management/cluster.py +1 -1
  37. singlestoredb/management/manager.py +15 -5
  38. singlestoredb/management/region.py +12 -2
  39. singlestoredb/management/workspace.py +17 -25
  40. singlestoredb/tests/ext_funcs/__init__.py +39 -0
  41. singlestoredb/tests/test_connection.py +18 -8
  42. singlestoredb/tests/test_management.py +24 -57
  43. singlestoredb/tests/test_udf.py +43 -15
  44. {singlestoredb-1.15.0.dist-info → singlestoredb-1.15.2.dist-info}/METADATA +1 -1
  45. {singlestoredb-1.15.0.dist-info → singlestoredb-1.15.2.dist-info}/RECORD +49 -30
  46. {singlestoredb-1.15.0.dist-info → singlestoredb-1.15.2.dist-info}/LICENSE +0 -0
  47. {singlestoredb-1.15.0.dist-info → singlestoredb-1.15.2.dist-info}/WHEEL +0 -0
  48. {singlestoredb-1.15.0.dist-info → singlestoredb-1.15.2.dist-info}/entry_points.txt +0 -0
  49. {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['type'] == 'http.disconnect':
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
- assert scope['type'] == 'http'
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
- json.dumps({
1009
- 'type': 'function_call',
1010
- 'id': request_id,
1011
- 'name': func_name.decode('utf-8'),
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['type'] == 'http.disconnect':
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
- cancel_on_disconnect(receive),
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
- logging.exception(
1094
- 'Timeout in function call: ' + func_name.decode('utf-8'),
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
- logging.exception(
1105
- 'Function call cancelled: ' + func_name.decode('utf-8'),
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
- logging.exception(
1112
- 'Error in function call: ' + func_name.decode('utf-8'),
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
- timer.finish()
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
- cur.execute('SHOW FUNCTIONS')
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 `{name}`')
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, (_, info) in self.endpoints.items():
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=a['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 `{fname}`')
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 `{fname}`')
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('registering functions with database')
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('dropping functions from database')
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('dropping functions from database')
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) -> None:
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.log_metrics()
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)