singlestoredb 1.15.1__cp38-abi3-macosx_10_9_universal2.whl → 1.15.3__cp38-abi3-macosx_10_9_universal2.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.

Binary file
singlestoredb/__init__.py CHANGED
@@ -13,7 +13,7 @@ Examples
13
13
 
14
14
  """
15
15
 
16
- __version__ = '1.15.1'
16
+ __version__ = '1.15.3'
17
17
 
18
18
  from typing import Any
19
19
 
@@ -13,6 +13,9 @@ if typing.TYPE_CHECKING:
13
13
  # Keep track of currently running server
14
14
  _running_server: 'typing.Optional[AwaitableUvicornServer]' = None
15
15
 
16
+ # Maximum number of UDFs allowed
17
+ MAX_UDFS_LIMIT = 10
18
+
16
19
 
17
20
  async def run_udf_app(
18
21
  log_level: str = 'error',
@@ -44,20 +47,32 @@ async def run_udf_app(
44
47
  udf_suffix = ''
45
48
  if app_config.running_interactively:
46
49
  udf_suffix = '_test'
47
- app = Application(url=base_url, app_mode='managed', name_suffix=udf_suffix)
50
+ app = Application(
51
+ url=base_url,
52
+ app_mode='managed',
53
+ name_suffix=udf_suffix,
54
+ log_level=log_level,
55
+ )
56
+
57
+ if not app.endpoints:
58
+ raise ValueError('You must define at least one function.')
59
+ if len(app.endpoints) > MAX_UDFS_LIMIT:
60
+ raise ValueError(
61
+ f'You can only define a maximum of {MAX_UDFS_LIMIT} functions.',
62
+ )
48
63
 
49
64
  config = uvicorn.Config(
50
65
  app,
51
66
  host='0.0.0.0',
52
67
  port=app_config.listen_port,
53
- log_level=log_level,
68
+ log_config=app.get_uvicorn_log_config(),
54
69
  )
55
- _running_server = AwaitableUvicornServer(config)
56
70
 
57
71
  # Register the functions only if the app is running interactively.
58
72
  if app_config.running_interactively:
59
73
  app.register_functions(replace=True)
60
74
 
75
+ _running_server = AwaitableUvicornServer(config)
61
76
  asyncio.create_task(_running_server.serve())
62
77
  await _running_server.wait_for_startup()
63
78
 
@@ -9,7 +9,7 @@ class StdoutSuppressor:
9
9
  This should not be used for asynchronous or threaded executions.
10
10
 
11
11
  ```py
12
- with Supressor():
12
+ with StdoutSupressor():
13
13
  print("This won't be printed")
14
14
  ```
15
15
 
@@ -30,3 +30,7 @@ class AwaitableUvicornServer(uvicorn.Server):
30
30
 
31
31
  async def wait_for_startup(self) -> None:
32
32
  await self._startup_future
33
+
34
+ async def shutdown(self, sockets: Optional[list[socket.socket]] = None) -> None:
35
+ if self.started:
36
+ await super().shutdown(sockets)
singlestoredb/config.py CHANGED
@@ -407,6 +407,12 @@ register_option(
407
407
  environ=['SINGLESTOREDB_EXT_FUNC_LOG_LEVEL'],
408
408
  )
409
409
 
410
+ register_option(
411
+ 'external_function.log_file', 'string', check_str, None,
412
+ 'File path to write logs to instead of console.',
413
+ environ=['SINGLESTOREDB_EXT_FUNC_LOG_FILE'],
414
+ )
415
+
410
416
  register_option(
411
417
  'external_function.name_prefix', 'string', check_str, '',
412
418
  'Prefix to add to external function names.',
@@ -450,6 +456,18 @@ register_option(
450
456
  environ=['SINGLESTOREDB_EXT_FUNC_TIMEOUT'],
451
457
  )
452
458
 
459
+ register_option(
460
+ 'external_function.disable_metrics', 'bool', check_bool, False,
461
+ 'Disable logging of function call metrics.',
462
+ environ=['SINGLESTOREDB_EXT_FUNC_DISABLE_METRICS'],
463
+ )
464
+
465
+ register_option(
466
+ 'external_function.app_name', 'string', check_str, None,
467
+ 'Name for the external function application instance.',
468
+ environ=['SINGLESTOREDB_EXT_FUNC_APP_NAME'],
469
+ )
470
+
453
471
  #
454
472
  # Debugging options
455
473
  #
@@ -23,7 +23,7 @@ except ImportError:
23
23
  try:
24
24
  import pygeos
25
25
  has_pygeos = True
26
- except ImportError:
26
+ except (AttributeError, ImportError):
27
27
  has_pygeos = False
28
28
 
29
29
  try:
@@ -91,7 +91,6 @@ except ImportError:
91
91
 
92
92
  logger = utils.get_logger('singlestoredb.functions.ext.asgi')
93
93
 
94
-
95
94
  # If a number of processes is specified, create a pool of workers
96
95
  num_processes = max(0, int(os.environ.get('SINGLESTOREDB_EXT_NUM_PROCESSES', 0)))
97
96
  if num_processes > 1:
@@ -678,8 +677,24 @@ class Application(object):
678
677
  link_credentials : Dict[str, Any], optional
679
678
  The CREDENTIALS section of a LINK definition. This dictionary gets
680
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
681
684
  function_database : str, optional
682
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.
683
698
 
684
699
  """
685
700
 
@@ -846,6 +861,10 @@ class Application(object):
846
861
  name_prefix: str = get_option('external_function.name_prefix'),
847
862
  name_suffix: str = get_option('external_function.name_suffix'),
848
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'),
849
868
  ) -> None:
850
869
  if link_name and (link_config or link_credentials):
851
870
  raise ValueError(
@@ -862,6 +881,15 @@ class Application(object):
862
881
  get_option('external_function.link_credentials') or '{}',
863
882
  ) or None
864
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
+
865
893
  # List of functions specs
866
894
  specs: List[Union[str, Callable[..., Any], ModuleType]] = []
867
895
 
@@ -953,6 +981,97 @@ class Application(object):
953
981
  self.endpoints = endpoints
954
982
  self.external_functions = external_functions
955
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
956
1075
 
957
1076
  async def __call__(
958
1077
  self,
@@ -976,19 +1095,22 @@ class Application(object):
976
1095
  request_id = str(uuid.uuid4())
977
1096
 
978
1097
  timer = Timer(
1098
+ app_name=self.name,
979
1099
  id=request_id,
980
1100
  timestamp=datetime.datetime.now(
981
1101
  datetime.timezone.utc,
982
1102
  ).strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
983
1103
  )
984
1104
  call_timer = Timer(
1105
+ app_name=self.name,
985
1106
  id=request_id,
986
1107
  timestamp=datetime.datetime.now(
987
1108
  datetime.timezone.utc,
988
1109
  ).strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
989
1110
  )
990
1111
 
991
- assert scope['type'] == 'http'
1112
+ if scope['type'] != 'http':
1113
+ raise ValueError(f"Expected HTTP scope, got {scope['type']}")
992
1114
 
993
1115
  method = scope['method']
994
1116
  path = tuple(x for x in scope['path'].split('/') if x)
@@ -1014,14 +1136,15 @@ class Application(object):
1014
1136
  # Call the endpoint
1015
1137
  if method == 'POST' and func is not None and path == self.invoke_path:
1016
1138
 
1017
- logger.info(
1018
- json.dumps({
1019
- 'type': 'function_call',
1020
- 'id': request_id,
1021
- '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'),
1022
1145
  'content_type': content_type.decode('utf-8'),
1023
1146
  'accepts': accepts.decode('utf-8'),
1024
- }),
1147
+ },
1025
1148
  )
1026
1149
 
1027
1150
  args_data_format = func_info['args_data_format']
@@ -1101,8 +1224,14 @@ class Application(object):
1101
1224
  await send(output_handler['response'])
1102
1225
 
1103
1226
  except asyncio.TimeoutError:
1104
- logging.exception(
1105
- '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
+ },
1106
1235
  )
1107
1236
  body = (
1108
1237
  '[TimeoutError] Function call timed out after ' +
@@ -1112,15 +1241,26 @@ class Application(object):
1112
1241
  await send(self.error_response_dict)
1113
1242
 
1114
1243
  except asyncio.CancelledError:
1115
- logging.exception(
1116
- '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
+ },
1117
1251
  )
1118
1252
  body = b'[CancelledError] Function call was cancelled'
1119
1253
  await send(self.error_response_dict)
1120
1254
 
1121
1255
  except Exception as e:
1122
- logging.exception(
1123
- '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
+ },
1124
1264
  )
1125
1265
  body = f'[{type(e).__name__}] {str(e).strip()}'.encode('utf-8')
1126
1266
  await send(self.error_response_dict)
@@ -1173,7 +1313,17 @@ class Application(object):
1173
1313
  for k, v in call_timer.metrics.items():
1174
1314
  timer.metrics[k] = v
1175
1315
 
1176
- 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
+ )
1177
1327
 
1178
1328
  def _create_link(
1179
1329
  self,
@@ -1230,9 +1380,11 @@ class Application(object):
1230
1380
  ) -> Dict[str, Any]:
1231
1381
  """
1232
1382
  Return the functions and function signature information.
1383
+
1233
1384
  Returns
1234
1385
  -------
1235
1386
  Dict[str, Any]
1387
+
1236
1388
  """
1237
1389
  functions = {}
1238
1390
  no_default = object()
@@ -1284,8 +1436,13 @@ class Application(object):
1284
1436
  doc_examples.append(ex_dict)
1285
1437
 
1286
1438
  except Exception as e:
1287
- logger.warning(
1288
- f'Could not parse docstring for function {key}: {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
+ },
1289
1446
  )
1290
1447
 
1291
1448
  if not func_name or key == func_name:
@@ -1740,6 +1897,22 @@ def main(argv: Optional[List[str]] = None) -> None:
1740
1897
  ),
1741
1898
  help='logging level',
1742
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
+ )
1743
1916
  parser.add_argument(
1744
1917
  '--name-prefix', metavar='name_prefix',
1745
1918
  default=defaults.get(
@@ -1764,6 +1937,14 @@ def main(argv: Optional[List[str]] = None) -> None:
1764
1937
  ),
1765
1938
  help='Database to use for the function definition',
1766
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
+ )
1767
1948
  parser.add_argument(
1768
1949
  'functions', metavar='module.or.func.path', nargs='*',
1769
1950
  help='functions or modules to export in UDF server',
@@ -1771,8 +1952,6 @@ def main(argv: Optional[List[str]] = None) -> None:
1771
1952
 
1772
1953
  args = parser.parse_args(argv)
1773
1954
 
1774
- logger.setLevel(getattr(logging, args.log_level.upper()))
1775
-
1776
1955
  if i > 0:
1777
1956
  break
1778
1957
 
@@ -1864,6 +2043,10 @@ def main(argv: Optional[List[str]] = None) -> None:
1864
2043
  name_prefix=args.name_prefix,
1865
2044
  name_suffix=args.name_suffix,
1866
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,
1867
2050
  )
1868
2051
 
1869
2052
  funcs = app.get_create_functions(replace=args.replace_existing)
@@ -1871,11 +2054,11 @@ def main(argv: Optional[List[str]] = None) -> None:
1871
2054
  raise RuntimeError('no functions specified')
1872
2055
 
1873
2056
  for f in funcs:
1874
- logger.info(f)
2057
+ app.logger.info(f)
1875
2058
 
1876
2059
  try:
1877
2060
  if args.db:
1878
- logger.info('registering functions with database')
2061
+ app.logger.info('Registering functions with database')
1879
2062
  app.register_functions(
1880
2063
  args.db,
1881
2064
  replace=args.replace_existing,
@@ -1890,6 +2073,9 @@ def main(argv: Optional[List[str]] = None) -> None:
1890
2073
  ).items() if v is not None
1891
2074
  }
1892
2075
 
2076
+ # Configure uvicorn logging to use JSON format matching Application's format
2077
+ app_args['log_config'] = app.get_uvicorn_log_config()
2078
+
1893
2079
  if use_async:
1894
2080
  asyncio.create_task(_run_uvicorn(uvicorn, app, app_args, db=args.db))
1895
2081
  else:
@@ -1897,7 +2083,7 @@ def main(argv: Optional[List[str]] = None) -> None:
1897
2083
 
1898
2084
  finally:
1899
2085
  if not use_async and args.db:
1900
- logger.info('dropping functions from database')
2086
+ app.logger.info('Dropping functions from database')
1901
2087
  app.drop_functions(args.db)
1902
2088
 
1903
2089
 
@@ -1910,7 +2096,7 @@ async def _run_uvicorn(
1910
2096
  """Run uvicorn server and clean up functions after shutdown."""
1911
2097
  await uvicorn.Server(uvicorn.Config(app, **app_args)).serve()
1912
2098
  if db:
1913
- logger.info('dropping functions from database')
2099
+ app.logger.info('Dropping functions from database')
1914
2100
  app.drop_functions(db)
1915
2101
 
1916
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)
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env python
2
+ import datetime
2
3
  import json
3
4
  import logging
4
5
  import re
@@ -30,14 +31,62 @@ except ImportError:
30
31
  return super().formatMessage(recordcopy)
31
32
 
32
33
 
34
+ class JSONFormatter(logging.Formatter):
35
+ """Custom JSON formatter for structured logging."""
36
+
37
+ def format(self, record: logging.LogRecord) -> str:
38
+ # Create proper ISO timestamp with microseconds
39
+ timestamp = datetime.datetime.fromtimestamp(
40
+ record.created, tz=datetime.timezone.utc,
41
+ )
42
+ # Keep only 3 digits for milliseconds
43
+ iso_timestamp = timestamp.strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z'
44
+
45
+ log_entry = {
46
+ 'timestamp': iso_timestamp,
47
+ 'level': record.levelname,
48
+ 'logger': record.name,
49
+ 'message': record.getMessage(),
50
+ }
51
+
52
+ # Add extra fields if present
53
+ allowed_fields = [
54
+ 'app_name', 'request_id', 'function_name',
55
+ 'content_type', 'accepts', 'metrics',
56
+ ]
57
+ for field in allowed_fields:
58
+ if hasattr(record, field):
59
+ log_entry[field] = getattr(record, field)
60
+
61
+ # Add exception info if present
62
+ if record.exc_info:
63
+ log_entry['exception'] = self.formatException(record.exc_info)
64
+
65
+ return json.dumps(log_entry)
66
+
67
+
33
68
  def get_logger(name: str) -> logging.Logger:
34
- """Return a new logger."""
69
+ """Return a logger with JSON formatting."""
35
70
  logger = logging.getLogger(name)
36
- handler = logging.StreamHandler()
37
- formatter = DefaultFormatter('%(levelprefix)s %(message)s')
38
- handler.setFormatter(formatter)
39
- logger.addHandler(handler)
40
- logger.setLevel(logging.INFO)
71
+
72
+ # Only configure if not already configured with JSON formatter
73
+ has_json_formatter = any(
74
+ isinstance(getattr(handler, 'formatter', None), JSONFormatter)
75
+ for handler in logger.handlers
76
+ )
77
+
78
+ if not logger.handlers or not has_json_formatter:
79
+ # Clear handlers only if we need to reconfigure
80
+ logger.handlers.clear()
81
+ handler = logging.StreamHandler()
82
+ formatter = JSONFormatter()
83
+ handler.setFormatter(formatter)
84
+ logger.addHandler(handler)
85
+ logger.setLevel(logging.INFO)
86
+
87
+ # Prevent propagation to avoid duplicate messages or different formatting
88
+ logger.propagate = False
89
+
41
90
  return logger
42
91
 
43
92
 
@@ -269,7 +269,7 @@ class TestRowdat1(unittest.TestCase):
269
269
  def test_numpy_accel(self):
270
270
  dump_res = rowdat_1._dump_numpy_accel(
271
271
  col_types, numpy_row_ids, numpy_data,
272
- ).tobytes()
272
+ )
273
273
  load_res = rowdat_1._load_numpy_accel(col_spec, dump_res)
274
274
 
275
275
  ids = load_res[0]
@@ -294,7 +294,7 @@ class TestRowdat1(unittest.TestCase):
294
294
  def test_numpy(self):
295
295
  dump_res = rowdat_1._dump_numpy(
296
296
  col_types, numpy_row_ids, numpy_data,
297
- ).tobytes()
297
+ )
298
298
  load_res = rowdat_1._load_numpy(col_spec, dump_res)
299
299
 
300
300
  ids = load_res[0]
@@ -387,7 +387,7 @@ class TestRowdat1(unittest.TestCase):
387
387
  with self.assertRaises(res, msg=f'Expected {res} for {data} in {dtype}'):
388
388
  rowdat_1._dump_numpy_accel(
389
389
  [dtype], numpy_row_ids, [(arr, None)],
390
- ).tobytes()
390
+ )
391
391
 
392
392
  # Pure Python
393
393
  if 'mediumint exceeds' in name:
@@ -396,13 +396,13 @@ class TestRowdat1(unittest.TestCase):
396
396
  with self.assertRaises(res, msg=f'Expected {res} for {data} in {dtype}'):
397
397
  rowdat_1._dump_numpy(
398
398
  [dtype], numpy_row_ids, [(arr, None)],
399
- ).tobytes()
399
+ )
400
400
 
401
401
  else:
402
402
  # Accelerated
403
403
  dump_res = rowdat_1._dump_numpy_accel(
404
404
  [dtype], numpy_row_ids, [(arr, None)],
405
- ).tobytes()
405
+ )
406
406
  load_res = rowdat_1._load_numpy_accel([('x', dtype)], dump_res)
407
407
  assert load_res[1][0][0] == res, \
408
408
  f'Expected {res} for {data}, but got {load_res[1][0][0]} in {dtype}'
@@ -410,7 +410,7 @@ class TestRowdat1(unittest.TestCase):
410
410
  # Pure Python
411
411
  dump_res = rowdat_1._dump_numpy(
412
412
  [dtype], numpy_row_ids, [(arr, None)],
413
- ).tobytes()
413
+ )
414
414
  load_res = rowdat_1._load_numpy([('x', dtype)], dump_res)
415
415
  assert load_res[1][0][0] == res, \
416
416
  f'Expected {res} for {data}, but got {load_res[1][0][0]} in {dtype}'
@@ -788,7 +788,7 @@ class TestRowdat1(unittest.TestCase):
788
788
  # Accelerated
789
789
  dump_res = rowdat_1._dump_numpy_accel(
790
790
  [dtype], numpy_row_ids, [(data, None)],
791
- ).tobytes()
791
+ )
792
792
  load_res = rowdat_1._load_numpy_accel([('x', dtype)], dump_res)
793
793
 
794
794
  if name == 'double from float32':
@@ -800,7 +800,7 @@ class TestRowdat1(unittest.TestCase):
800
800
  # Pure Python
801
801
  dump_res = rowdat_1._dump_numpy(
802
802
  [dtype], numpy_row_ids, [(data, None)],
803
- ).tobytes()
803
+ )
804
804
  load_res = rowdat_1._load_numpy([('x', dtype)], dump_res)
805
805
 
806
806
  if name == 'double from float32':
@@ -812,7 +812,7 @@ class TestRowdat1(unittest.TestCase):
812
812
  def test_python(self):
813
813
  dump_res = rowdat_1._dump(
814
814
  col_types, py_row_ids, py_col_data,
815
- ).tobytes()
815
+ )
816
816
  load_res = rowdat_1._load(col_spec, dump_res)
817
817
 
818
818
  ids = load_res[0]
@@ -824,7 +824,7 @@ class TestRowdat1(unittest.TestCase):
824
824
  def test_python_accel(self):
825
825
  dump_res = rowdat_1._dump_accel(
826
826
  col_types, py_row_ids, py_col_data,
827
- ).tobytes()
827
+ )
828
828
  load_res = rowdat_1._load_accel(col_spec, dump_res)
829
829
 
830
830
  ids = load_res[0]
@@ -836,7 +836,7 @@ class TestRowdat1(unittest.TestCase):
836
836
  def test_polars(self):
837
837
  dump_res = rowdat_1._dump_polars(
838
838
  col_types, polars_row_ids, polars_data,
839
- ).tobytes()
839
+ )
840
840
  load_res = rowdat_1._load_polars(col_spec, dump_res)
841
841
 
842
842
  ids = load_res[0]
@@ -861,7 +861,7 @@ class TestRowdat1(unittest.TestCase):
861
861
  def test_polars_accel(self):
862
862
  dump_res = rowdat_1._dump_polars_accel(
863
863
  col_types, polars_row_ids, polars_data,
864
- ).tobytes()
864
+ )
865
865
  load_res = rowdat_1._load_polars_accel(col_spec, dump_res)
866
866
 
867
867
  ids = load_res[0]
@@ -886,7 +886,7 @@ class TestRowdat1(unittest.TestCase):
886
886
  def test_pandas(self):
887
887
  dump_res = rowdat_1._dump_pandas(
888
888
  col_types, pandas_row_ids, pandas_data,
889
- ).tobytes()
889
+ )
890
890
  load_res = rowdat_1._load_pandas(col_spec, dump_res)
891
891
 
892
892
  ids = load_res[0]
@@ -911,7 +911,7 @@ class TestRowdat1(unittest.TestCase):
911
911
  def test_pandas_accel(self):
912
912
  dump_res = rowdat_1._dump_pandas_accel(
913
913
  col_types, pandas_row_ids, pandas_data,
914
- ).tobytes()
914
+ )
915
915
  load_res = rowdat_1._load_pandas_accel(col_spec, dump_res)
916
916
 
917
917
  ids = load_res[0]
@@ -936,7 +936,7 @@ class TestRowdat1(unittest.TestCase):
936
936
  def test_pyarrow(self):
937
937
  dump_res = rowdat_1._dump_arrow(
938
938
  col_types, pyarrow_row_ids, pyarrow_data,
939
- ).tobytes()
939
+ )
940
940
  load_res = rowdat_1._load_arrow(col_spec, dump_res)
941
941
 
942
942
  ids = load_res[0]
@@ -961,7 +961,7 @@ class TestRowdat1(unittest.TestCase):
961
961
  def test_pyarrow_accel(self):
962
962
  dump_res = rowdat_1._dump_arrow_accel(
963
963
  col_types, pyarrow_row_ids, pyarrow_data,
964
- ).tobytes()
964
+ )
965
965
  load_res = rowdat_1._load_arrow_accel(col_spec, dump_res)
966
966
 
967
967
  ids = load_res[0]
@@ -1053,7 +1053,7 @@ class TestJSON(unittest.TestCase):
1053
1053
  def test_pandas(self):
1054
1054
  dump_res = rowdat_1._dump_pandas(
1055
1055
  col_types, pandas_row_ids, pandas_data,
1056
- ).tobytes()
1056
+ )
1057
1057
  load_res = rowdat_1._load_pandas(col_spec, dump_res)
1058
1058
 
1059
1059
  ids = load_res[0]
@@ -1078,7 +1078,7 @@ class TestJSON(unittest.TestCase):
1078
1078
  def test_pyarrow(self):
1079
1079
  dump_res = rowdat_1._dump_arrow(
1080
1080
  col_types, pyarrow_row_ids, pyarrow_data,
1081
- ).tobytes()
1081
+ )
1082
1082
  load_res = rowdat_1._load_arrow(col_spec, dump_res)
1083
1083
 
1084
1084
  ids = load_res[0]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: singlestoredb
3
- Version: 1.15.1
3
+ Version: 1.15.3
4
4
  Summary: Interface to the SingleStoreDB database and workspace management APIs
5
5
  Home-page: https://github.com/singlestore-labs/singlestoredb-python
6
6
  Author: SingleStore
@@ -1,22 +1,22 @@
1
- _singlestoredb_accel.abi3.so,sha256=k0UDmMDNTt8IdgVADmO4wl29Yt8OJvKB_dXa47OSqEk,207216
2
- singlestoredb-1.15.1.dist-info/RECORD,,
3
- singlestoredb-1.15.1.dist-info/LICENSE,sha256=Mlq78idURT-9G026aMYswwwnnrLcgzTLuXeAs5hjDLM,11341
4
- singlestoredb-1.15.1.dist-info/WHEEL,sha256=_VEguvlLpUd-c8RbFMA4yMIVNMBv2LhpxYLCEQ-Bogk,113
5
- singlestoredb-1.15.1.dist-info/entry_points.txt,sha256=bSLaTWB5zGjpVYPAaI46MkkDup0su-eb3uAhCNYuRV0,48
6
- singlestoredb-1.15.1.dist-info/top_level.txt,sha256=lA65Vf4qAMfg_s1oG3LEO90h4t1Z-SPDbRqkevI3bSY,40
7
- singlestoredb-1.15.1.dist-info/METADATA,sha256=PboWeUGiR17ArbHqeWX987eHZnBG6Q0M-eyugiRiBPI,5804
1
+ _singlestoredb_accel.abi3.so,sha256=ohVCunjDLBfTPdZgjGacsHPHr1hnHKpT7AGKZsvCvpE,207264
8
2
  sqlx/magic.py,sha256=JsS9_9aBFaOt91Torm1JPN0c8qB2QmYJmNSKtbSQIY0,3509
9
3
  sqlx/__init__.py,sha256=aBYiU8DZXCogvWu3yWafOz7bZS5WWwLZXj7oL0dXGyU,85
4
+ singlestoredb-1.15.3.dist-info/RECORD,,
5
+ singlestoredb-1.15.3.dist-info/LICENSE,sha256=Mlq78idURT-9G026aMYswwwnnrLcgzTLuXeAs5hjDLM,11341
6
+ singlestoredb-1.15.3.dist-info/WHEEL,sha256=_VEguvlLpUd-c8RbFMA4yMIVNMBv2LhpxYLCEQ-Bogk,113
7
+ singlestoredb-1.15.3.dist-info/entry_points.txt,sha256=bSLaTWB5zGjpVYPAaI46MkkDup0su-eb3uAhCNYuRV0,48
8
+ singlestoredb-1.15.3.dist-info/top_level.txt,sha256=lA65Vf4qAMfg_s1oG3LEO90h4t1Z-SPDbRqkevI3bSY,40
9
+ singlestoredb-1.15.3.dist-info/METADATA,sha256=V_BY23iSritv7kZErDT6ZrJ7XHRiw0ZLyhuvSR6kWxA,5804
10
10
  singlestoredb/auth.py,sha256=u8D9tpKzrqa4ssaHjyZnGDX1q8XBpGtuoOkTkSv7B28,7599
11
- singlestoredb/config.py,sha256=kas72OMNhvjpXSHxY5a0JaEIx9L5-M-Q8ZLVdA-W-Wo,13368
11
+ singlestoredb/config.py,sha256=aBdMrPEaNSH-QLi1AXoQaSJsZ9f6ZXoFPN-74Trr6sQ,13935
12
12
  singlestoredb/vectorstore.py,sha256=BZb8e7m02_XVHqOyu8tA94R6kHb3n-BC8F08JyJwDzY,8408
13
- singlestoredb/__init__.py,sha256=0oMPzlsB1ZxWKQ0Gjrn1mLxlbPjH_dKuwkTsEX44TUU,2272
13
+ singlestoredb/__init__.py,sha256=du9fYfdFzwLGCCtsd8zQ0EK1P5GTZex-vbq_tAxpJyQ,2272
14
14
  singlestoredb/types.py,sha256=Qp_PWYjSYG6PRnmXAZZ7K2QehUqfoG4KSllI3O1stPE,10397
15
15
  singlestoredb/connection.py,sha256=ELk3-UpM6RaB993aIt08MydKiiDnejHQ1s8EFiacrAI,46055
16
16
  singlestoredb/pytest.py,sha256=OyF3BO9mgxenifYhOihnzGk8WzCJ_zN5_mxe8XyFPOc,9074
17
17
  singlestoredb/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
18
  singlestoredb/exceptions.py,sha256=HuoA6sMRL5qiCiee-_5ddTGmFbYC9Euk8TYUsh5GvTw,3234
19
- singlestoredb/converters.py,sha256=Ui-AqdW3pRAQ8A_YcK9EqVYyM4Pt1_Q-tjlotbpK6Cw,20686
19
+ singlestoredb/converters.py,sha256=0D54e-3E2iVzlMYPK0RbGilE9B-kcP380c1Mpze2Nz4,20704
20
20
  singlestoredb/fusion/graphql.py,sha256=ZA3HcDq5rER-dCEavwTqnF7KM0D2LCYIY7nLQk7lSso,5207
21
21
  singlestoredb/fusion/handler.py,sha256=M5iyNP4zOaGqUqnZg_b5xhRE-8tHgfZSHDH0zKTiJmE,27692
22
22
  singlestoredb/fusion/registry.py,sha256=jjdRTYZ3ylhy6gAoW5xBj0tkxGFBT-2yLQ0tztTgDIY,6112
@@ -40,7 +40,7 @@ singlestoredb/tests/test_basics.py,sha256=Dw1irrtf3gWN7tqGruSH6uhWi5zkmCpJl6ZMQx
40
40
  singlestoredb/tests/test_ext_func.py,sha256=_YREceW1Llwx9Wcamj0up2IXLuBTnuvQqCFOWphckKI,46271
41
41
  singlestoredb/tests/test_connection.py,sha256=ax2hBPahn0rFENBbYLoph_JIAR20eYeJLyQPIQGIdX4,120237
42
42
  singlestoredb/tests/test2.ipynb,sha256=yd1PE1VK-DwiRd6mYS4_0cPBtuVkvcDtycvTwD-YnDo,218
43
- singlestoredb/tests/test_ext_func_data.py,sha256=yTADD93nPxX6_rZXXLZaOWEI_yPvYyir9psn5PK9ctU,47695
43
+ singlestoredb/tests/test_ext_func_data.py,sha256=kyNklkX1RxSiahI0LZjpqhg3LGDs0iwv8iHuXW3AcSo,47515
44
44
  singlestoredb/tests/test_exceptions.py,sha256=tfr_8X2w1UmG4nkSBzWGB0C7ehrf1GAVgj6_ODaG-TM,1131
45
45
  singlestoredb/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
46
46
  singlestoredb/tests/test_management.py,sha256=R5g5QHWCSPxCrwMUOD0BL-touGIDvqQfDve-YmZyhX4,51973
@@ -153,11 +153,11 @@ singlestoredb/functions/__init__.py,sha256=I2GnxOhLb4_7xhgOxdIwmwD5NiK7QYPYaE3PU
153
153
  singlestoredb/functions/dtypes.py,sha256=DgJaNXouJ2t-qIqDiQlUYU9IhkXXUTigWeE_MAcmvHM,39814
154
154
  singlestoredb/functions/utils.py,sha256=1L0Phgzq0XdWK3ecfOOydq4zV955yCwpDoAaCYRGldk,10769
155
155
  singlestoredb/functions/signature.py,sha256=h2vFVNP07d5a3gi7zMiM_sztDUNK_HlJWR-Rl3nMxPA,45545
156
- singlestoredb/functions/ext/timer.py,sha256=kgmJACV6LAfev53XiGMwCv4DDlgF3Icg7G8VCuTQp4Q,2963
157
- singlestoredb/functions/ext/asgi.py,sha256=SuaNnatHY7Y5v2YtioSc6UHsOyYozK14xqKI-LbMy1I,64299
156
+ singlestoredb/functions/ext/timer.py,sha256=-PR__KbhwAMW4PXJ4fGri2FfrU0jRyz6e6yvmySmjaw,2706
157
+ singlestoredb/functions/ext/asgi.py,sha256=H7YgSsqKzhVun9cj5iXvM_8yXM_Zciuy8BEPBp_dT4Y,71650
158
158
  singlestoredb/functions/ext/arrow.py,sha256=WB7n1ACslyd8nlbFzUvlbxn1BVuEjA9-BGBEqCWlSOo,9061
159
159
  singlestoredb/functions/ext/__init__.py,sha256=1oLL20yLB1GL9IbFiZD8OReDqiCpFr-yetIR6x1cNkI,23
160
- singlestoredb/functions/ext/utils.py,sha256=2-B8YU_Iekv8JcpI-ochs9TIeuyatLaLAH-AyYyUUIg,5311
160
+ singlestoredb/functions/ext/utils.py,sha256=oU2NVmkjcS0QHLfdB8SBiRylVq-r0VzTy8nxGvAgjow,6938
161
161
  singlestoredb/functions/ext/mmap.py,sha256=RzyNSLRpI5ZJ8YN6k-AvZlRTLjj80j52byHLtW8c3ps,13710
162
162
  singlestoredb/functions/ext/json.py,sha256=RIuZdDybEdHuC-f2p6BdjhFjM3iGb3a1PRQ4k11P6N8,10102
163
163
  singlestoredb/functions/ext/rowdat_1.py,sha256=SlXbJ2042jEoaXw81y5llw1625w0aU2nZ8vI_O3qA-M,21112
@@ -169,13 +169,13 @@ singlestoredb/functions/typing/polars.py,sha256=b_UOIXLkvptHiAB7sXSzC7XPHMWNOglC
169
169
  singlestoredb/notebook/__init__.py,sha256=v0j1E3MFAtaC8wTrR-F7XY0nytUvQ4XpYhVXddv2xA0,533
170
170
  singlestoredb/notebook/_objects.py,sha256=MkB1eowEq5SQXFHY00xAKAyyeLqHu_uaZiA20BCJPaE,8043
171
171
  singlestoredb/notebook/_portal.py,sha256=DLerIEQmAUymtYcx8RBeuYJ4pJSy_xl1K6t1Oc-eTf8,9698
172
- singlestoredb/apps/_python_udfs.py,sha256=hwncnfx7MpOsvCH0Ic64otDIo-GGPT3UC89y2uAA_Uw,2773
172
+ singlestoredb/apps/_python_udfs.py,sha256=CwGt1ehR6CPvtUfLg8SK_ynXvvWHo_SeU_6xoVHQzys,3158
173
173
  singlestoredb/apps/_dashboards.py,sha256=_03fI-GJannamA5lxLvIoC6Mim-H1jTRuI8-dw_P--k,1474
174
174
  singlestoredb/apps/__init__.py,sha256=dfN97AZz7Np6JML3i9GJrv22ZbNCUletXmsJpQnKhKg,170
175
175
  singlestoredb/apps/_cloud_functions.py,sha256=NJJu0uJsK9TjY3yZjgftpFPR-ga-FrOyaiDD4jWFCtE,2704
176
- singlestoredb/apps/_uvicorn_util.py,sha256=rEK4nEmq5hbpRgsmK16UVlxe2DyQSq7C5w5WZSp0kX8,962
176
+ singlestoredb/apps/_uvicorn_util.py,sha256=tFcxd4XlPp_ULITN6aPi5MkPFRaEztD0HrbhBw0B1fk,1117
177
177
  singlestoredb/apps/_process.py,sha256=G37fk6bzIxzhfEqp2aJBk3JCij-T2HFtTd078k5Xq9I,944
178
- singlestoredb/apps/_stdout_supress.py,sha256=8s9zMIIRPpeu44yluJFc_0VueAxZDmr9QVGT6TGiFeY,659
178
+ singlestoredb/apps/_stdout_supress.py,sha256=wNL4YHEImqT3ptKsPPcolkCWN35vWxahEsi2rM7qpOY,665
179
179
  singlestoredb/apps/_config.py,sha256=FlV0ABP7qlBJoKo9NOme6Fpp4yUFm5QEpHEHbl1A24o,2441
180
180
  singlestoredb/apps/_connection_info.py,sha256=QOr-wcQJn6oCZw2kLEP0Uwzo85CGolGz0QIvlem3gug,303
181
181
  singlestoredb/alchemy/__init__.py,sha256=dXRThusYrs_9GjrhPOw0-vw94in_T8yY9jE7SGCqiQk,2523