singlestoredb 1.15.0__py3-none-any.whl → 1.15.1__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.

Files changed (42) hide show
  1. singlestoredb/__init__.py +1 -1
  2. singlestoredb/ai/chat.py +14 -0
  3. singlestoredb/config.py +6 -0
  4. singlestoredb/docstring/__init__.py +33 -0
  5. singlestoredb/docstring/attrdoc.py +126 -0
  6. singlestoredb/docstring/common.py +230 -0
  7. singlestoredb/docstring/epydoc.py +267 -0
  8. singlestoredb/docstring/google.py +412 -0
  9. singlestoredb/docstring/numpydoc.py +562 -0
  10. singlestoredb/docstring/parser.py +100 -0
  11. singlestoredb/docstring/py.typed +1 -0
  12. singlestoredb/docstring/rest.py +256 -0
  13. singlestoredb/docstring/tests/__init__.py +1 -0
  14. singlestoredb/docstring/tests/_pydoctor.py +21 -0
  15. singlestoredb/docstring/tests/test_epydoc.py +729 -0
  16. singlestoredb/docstring/tests/test_google.py +1007 -0
  17. singlestoredb/docstring/tests/test_numpydoc.py +1100 -0
  18. singlestoredb/docstring/tests/test_parse_from_object.py +109 -0
  19. singlestoredb/docstring/tests/test_parser.py +248 -0
  20. singlestoredb/docstring/tests/test_rest.py +547 -0
  21. singlestoredb/docstring/tests/test_util.py +70 -0
  22. singlestoredb/docstring/util.py +141 -0
  23. singlestoredb/functions/decorator.py +19 -18
  24. singlestoredb/functions/ext/asgi.py +97 -11
  25. singlestoredb/functions/signature.py +374 -241
  26. singlestoredb/fusion/handlers/files.py +4 -4
  27. singlestoredb/fusion/handlers/models.py +1 -1
  28. singlestoredb/fusion/handlers/stage.py +4 -4
  29. singlestoredb/management/cluster.py +1 -1
  30. singlestoredb/management/manager.py +15 -5
  31. singlestoredb/management/region.py +12 -2
  32. singlestoredb/management/workspace.py +17 -25
  33. singlestoredb/tests/ext_funcs/__init__.py +39 -0
  34. singlestoredb/tests/test_connection.py +18 -8
  35. singlestoredb/tests/test_management.py +24 -57
  36. singlestoredb/tests/test_udf.py +43 -15
  37. {singlestoredb-1.15.0.dist-info → singlestoredb-1.15.1.dist-info}/METADATA +1 -1
  38. {singlestoredb-1.15.0.dist-info → singlestoredb-1.15.1.dist-info}/RECORD +42 -23
  39. {singlestoredb-1.15.0.dist-info → singlestoredb-1.15.1.dist-info}/LICENSE +0 -0
  40. {singlestoredb-1.15.0.dist-info → singlestoredb-1.15.1.dist-info}/WHEEL +0 -0
  41. {singlestoredb-1.15.0.dist-info → singlestoredb-1.15.1.dist-info}/entry_points.txt +0 -0
  42. {singlestoredb-1.15.0.dist-info → singlestoredb-1.15.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,141 @@
1
+ """Utility functions for working with docstrings."""
2
+ import typing as T
3
+ from collections import ChainMap
4
+ from inspect import Signature
5
+ from itertools import chain
6
+
7
+ from .common import DocstringMeta
8
+ from .common import DocstringParam
9
+ from .common import DocstringReturns # noqa: F401
10
+ from .common import DocstringStyle
11
+ from .common import RenderingStyle
12
+ from .parser import compose
13
+ from .parser import parse
14
+
15
+ _Func = T.Callable[..., T.Any]
16
+
17
+
18
+ def combine_docstrings(
19
+ *others: _Func,
20
+ exclude: T.Iterable[T.Type[DocstringMeta]] = (),
21
+ style: DocstringStyle = DocstringStyle.AUTO,
22
+ rendering_style: RenderingStyle = RenderingStyle.COMPACT,
23
+ ) -> _Func:
24
+ """A function decorator that parses the docstrings from `others`,
25
+ programmatically combines them with the parsed docstring of the decorated
26
+ function, and replaces the docstring of the decorated function with the
27
+ composed result. Only parameters that are part of the decorated functions
28
+ signature are included in the combined docstring. When multiple sources for
29
+ a parameter or docstring metadata exists then the decorator will first
30
+ default to the wrapped function's value (when available) and otherwise use
31
+ the rightmost definition from ``others``.
32
+
33
+ The following example illustrates its usage:
34
+
35
+ >>> def fun1(a, b, c, d):
36
+ ... '''short_description: fun1
37
+ ...
38
+ ... :param a: fun1
39
+ ... :param b: fun1
40
+ ... :return: fun1
41
+ ... '''
42
+ >>> def fun2(b, c, d, e):
43
+ ... '''short_description: fun2
44
+ ...
45
+ ... long_description: fun2
46
+ ...
47
+ ... :param b: fun2
48
+ ... :param c: fun2
49
+ ... :param e: fun2
50
+ ... '''
51
+ >>> @combine_docstrings(fun1, fun2)
52
+ >>> def decorated(a, b, c, d, e, f):
53
+ ... '''
54
+ ... :param e: decorated
55
+ ... :param f: decorated
56
+ ... '''
57
+ >>> print(decorated.__doc__)
58
+ short_description: fun2
59
+ <BLANKLINE>
60
+ long_description: fun2
61
+ <BLANKLINE>
62
+ :param a: fun1
63
+ :param b: fun1
64
+ :param c: fun2
65
+ :param e: fun2
66
+ :param f: decorated
67
+ :returns: fun1
68
+ >>> @combine_docstrings(fun1, fun2, exclude=[DocstringReturns])
69
+ >>> def decorated(a, b, c, d, e, f): pass
70
+ >>> print(decorated.__doc__)
71
+ short_description: fun2
72
+ <BLANKLINE>
73
+ long_description: fun2
74
+ <BLANKLINE>
75
+ :param a: fun1
76
+ :param b: fun1
77
+ :param c: fun2
78
+ :param e: fun2
79
+
80
+ :param others: callables from which to parse docstrings.
81
+ :param exclude: an iterable of ``DocstringMeta`` subclasses to exclude when
82
+ combining docstrings.
83
+ :param style: style composed docstring. The default will infer the style
84
+ from the decorated function.
85
+ :param rendering_style: The rendering style used to compose a docstring.
86
+ :return: the decorated function with a modified docstring.
87
+ """
88
+
89
+ def wrapper(func: _Func) -> _Func:
90
+ sig = Signature.from_callable(func)
91
+
92
+ comb_doc = parse(func.__doc__ or '')
93
+ docs = [parse(other.__doc__ or '') for other in others] + [comb_doc]
94
+ params = dict(
95
+ ChainMap(
96
+ *(
97
+ {param.arg_name: param for param in doc.params}
98
+ for doc in docs
99
+ ),
100
+ ),
101
+ )
102
+
103
+ for doc in reversed(docs):
104
+ if not doc.short_description:
105
+ continue
106
+ comb_doc.short_description = doc.short_description
107
+ comb_doc.blank_after_short_description = (
108
+ doc.blank_after_short_description
109
+ )
110
+ break
111
+
112
+ for doc in reversed(docs):
113
+ if not doc.long_description:
114
+ continue
115
+ comb_doc.long_description = doc.long_description
116
+ comb_doc.blank_after_long_description = (
117
+ doc.blank_after_long_description
118
+ )
119
+ break
120
+
121
+ combined: T.Dict[T.Type[DocstringMeta], T.List[DocstringMeta]] = {}
122
+ for doc in docs:
123
+ metas: T.Dict[T.Type[DocstringMeta], T.List[DocstringMeta]] = {}
124
+ for meta in doc.meta:
125
+ meta_type = type(meta)
126
+ if meta_type in exclude:
127
+ continue
128
+ metas.setdefault(meta_type, []).append(meta)
129
+ for meta_type, meta_list in metas.items():
130
+ combined[meta_type] = meta_list
131
+
132
+ combined[DocstringParam] = [
133
+ params[name] for name in sig.parameters if name in params
134
+ ]
135
+ comb_doc.meta = list(chain(*combined.values()))
136
+ func.__doc__ = compose(
137
+ comb_doc, style=style, rendering_style=rendering_style,
138
+ )
139
+ return func
140
+
141
+ return wrapper
@@ -45,23 +45,20 @@ def is_valid_type(obj: Any) -> bool:
45
45
  return False
46
46
 
47
47
 
48
- def is_valid_callable(obj: Any) -> bool:
48
+ def is_sqlstr_callable(obj: Any) -> bool:
49
49
  """Check if the object is a valid callable for a parameter type."""
50
50
  if not callable(obj):
51
51
  return False
52
52
 
53
53
  returns = utils.get_annotations(obj).get('return', None)
54
54
 
55
- if inspect.isclass(returns) and issubclass(returns, str):
55
+ if inspect.isclass(returns) and issubclass(returns, SQLString):
56
56
  return True
57
57
 
58
- raise TypeError(
59
- f'callable {obj} must return a str, '
60
- f'but got {returns}',
61
- )
58
+ return False
62
59
 
63
60
 
64
- def expand_types(args: Any) -> Optional[Union[List[str], Type[Any]]]:
61
+ def expand_types(args: Any) -> Optional[List[Any]]:
65
62
  """Expand the types for the function arguments / return values."""
66
63
  if args is None:
67
64
  return None
@@ -70,28 +67,32 @@ def expand_types(args: Any) -> Optional[Union[List[str], Type[Any]]]:
70
67
  if isinstance(args, str):
71
68
  return [args]
72
69
 
73
- # General way of accepting pydantic.BaseModel, NamedTuple, TypedDict
74
- elif is_valid_type(args):
75
- return args
76
-
77
70
  # List of SQL strings or callables
78
71
  elif isinstance(args, list):
79
- new_args = []
72
+ new_args: List[Any] = []
80
73
  for arg in args:
81
74
  if isinstance(arg, str):
82
75
  new_args.append(arg)
83
- elif callable(arg):
76
+ elif is_sqlstr_callable(arg):
84
77
  new_args.append(arg())
78
+ elif type(arg) is type:
79
+ new_args.append(arg)
80
+ elif is_valid_type(arg):
81
+ new_args.append(arg)
85
82
  else:
86
83
  raise TypeError(f'unrecognized type for parameter: {arg}')
87
84
  return new_args
88
85
 
89
86
  # Callable that returns a SQL string
90
- elif is_valid_callable(args):
91
- out = args()
92
- if not isinstance(out, str):
93
- raise TypeError(f'unrecognized type for parameter: {args}')
94
- return [out]
87
+ elif is_sqlstr_callable(args):
88
+ return [args()]
89
+
90
+ # General way of accepting pydantic.BaseModel, NamedTuple, TypedDict
91
+ elif is_valid_type(args):
92
+ return [args]
93
+
94
+ elif type(args) is type:
95
+ return [args]
95
96
 
96
97
  raise TypeError(f'unrecognized type for parameter: {args}')
97
98
 
@@ -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
@@ -538,6 +540,8 @@ def make_func(
538
540
  Name of the function to create
539
541
  func : Callable
540
542
  The function to call as the endpoint
543
+ database : str, optional
544
+ The database to use for the function definition
541
545
 
542
546
  Returns
543
547
  -------
@@ -615,7 +619,7 @@ async def cancel_on_disconnect(
615
619
  """Cancel request if client disconnects."""
616
620
  while True:
617
621
  message = await receive()
618
- if message['type'] == 'http.disconnect':
622
+ if message.get('type', '') == 'http.disconnect':
619
623
  raise asyncio.CancelledError(
620
624
  'Function call was cancelled by client',
621
625
  )
@@ -674,6 +678,8 @@ class Application(object):
674
678
  link_credentials : Dict[str, Any], optional
675
679
  The CREDENTIALS section of a LINK definition. This dictionary gets
676
680
  converted to JSON for the CREATE LINK call.
681
+ function_database : str, optional
682
+ The database to use for external function definitions.
677
683
 
678
684
  """
679
685
 
@@ -816,6 +822,7 @@ class Application(object):
816
822
  invoke_path = ('invoke',)
817
823
  show_create_function_path = ('show', 'create_function')
818
824
  show_function_info_path = ('show', 'function_info')
825
+ status = ('status',)
819
826
 
820
827
  def __init__(
821
828
  self,
@@ -838,6 +845,7 @@ class Application(object):
838
845
  link_credentials: Optional[Dict[str, Any]] = None,
839
846
  name_prefix: str = get_option('external_function.name_prefix'),
840
847
  name_suffix: str = get_option('external_function.name_suffix'),
848
+ function_database: Optional[str] = None,
841
849
  ) -> None:
842
850
  if link_name and (link_config or link_credentials):
843
851
  raise ValueError(
@@ -944,6 +952,7 @@ class Application(object):
944
952
  self.link_credentials = link_credentials
945
953
  self.endpoints = endpoints
946
954
  self.external_functions = external_functions
955
+ self.function_database = function_database
947
956
 
948
957
  async def __call__(
949
958
  self,
@@ -992,6 +1001,7 @@ class Application(object):
992
1001
  accepts = headers.get(b'accepts', content_type)
993
1002
  func_name = headers.get(b's2-ef-name', b'')
994
1003
  func_endpoint = self.endpoints.get(func_name)
1004
+ ignore_cancel = headers.get(b's2-ef-ignore-cancel', b'false') == b'true'
995
1005
 
996
1006
  timer.metadata['function'] = func_name.decode('utf-8') if func_name else ''
997
1007
  call_timer.metadata['function'] = timer.metadata['function']
@@ -1021,7 +1031,7 @@ class Application(object):
1021
1031
  with timer('receive_data'):
1022
1032
  while more_body:
1023
1033
  request = await receive()
1024
- if request['type'] == 'http.disconnect':
1034
+ if request.get('type', '') == 'http.disconnect':
1025
1035
  raise RuntimeError('client disconnected')
1026
1036
  data.append(request['body'])
1027
1037
  more_body = request.get('more_body', False)
@@ -1051,7 +1061,8 @@ class Application(object):
1051
1061
  ),
1052
1062
  )
1053
1063
  disconnect_task = asyncio.create_task(
1054
- cancel_on_disconnect(receive),
1064
+ asyncio.sleep(int(1e9))
1065
+ if ignore_cancel else cancel_on_disconnect(receive),
1055
1066
  )
1056
1067
  timeout_task = asyncio.create_task(
1057
1068
  cancel_on_timeout(func_info['timeout']),
@@ -1130,6 +1141,7 @@ class Application(object):
1130
1141
  endpoint_info['signature'],
1131
1142
  url=self.url or reflected_url,
1132
1143
  data_format=self.data_format,
1144
+ database=self.function_database or None,
1133
1145
  ),
1134
1146
  )
1135
1147
  body = '\n'.join(syntax).encode('utf-8')
@@ -1142,6 +1154,11 @@ class Application(object):
1142
1154
  body = json.dumps(dict(functions=functions)).encode('utf-8')
1143
1155
  await send(self.text_response_dict)
1144
1156
 
1157
+ # Return status
1158
+ elif method == 'GET' and path == self.status:
1159
+ body = json.dumps(dict(status='ok')).encode('utf-8')
1160
+ await send(self.text_response_dict)
1161
+
1145
1162
  # Path not found
1146
1163
  else:
1147
1164
  body = b''
@@ -1184,20 +1201,27 @@ class Application(object):
1184
1201
  def _locate_app_functions(self, cur: Any) -> Tuple[Set[str], Set[str]]:
1185
1202
  """Locate all current functions and links belonging to this app."""
1186
1203
  funcs, links = set(), set()
1187
- cur.execute('SHOW FUNCTIONS')
1204
+ if self.function_database:
1205
+ database_prefix = escape_name(self.function_database) + '.'
1206
+ cur.execute(f'SHOW FUNCTIONS IN {escape_name(self.function_database)}')
1207
+ else:
1208
+ database_prefix = ''
1209
+ cur.execute('SHOW FUNCTIONS')
1210
+
1188
1211
  for row in list(cur):
1189
1212
  name, ftype, link = row[0], row[1], row[-1]
1190
1213
  # Only look at external functions
1191
1214
  if 'external' not in ftype.lower():
1192
1215
  continue
1193
1216
  # See if function URL matches url
1194
- cur.execute(f'SHOW CREATE FUNCTION `{name}`')
1217
+ cur.execute(f'SHOW CREATE FUNCTION {database_prefix}{escape_name(name)}')
1195
1218
  for fname, _, code, *_ in list(cur):
1196
1219
  m = re.search(r" (?:\w+) (?:SERVICE|MANAGED) '([^']+)'", code)
1197
1220
  if m and m.group(1) == self.url:
1198
- funcs.add(fname)
1221
+ funcs.add(f'{database_prefix}{escape_name(fname)}')
1199
1222
  if link and re.match(r'^py_ext_func_link_\S{14}$', link):
1200
1223
  links.add(link)
1224
+
1201
1225
  return funcs, links
1202
1226
 
1203
1227
  def get_function_info(
@@ -1220,20 +1244,66 @@ class Application(object):
1220
1244
  sig = info['signature']
1221
1245
  sql_map[sig['name']] = sql
1222
1246
 
1223
- for key, (_, info) in self.endpoints.items():
1247
+ for key, (func, info) in self.endpoints.items():
1248
+ # Get info from docstring
1249
+ doc_summary = ''
1250
+ doc_long_description = ''
1251
+ doc_params = {}
1252
+ doc_returns = None
1253
+ doc_examples = []
1254
+ if func.__doc__:
1255
+ try:
1256
+ docs = parse(func.__doc__)
1257
+ doc_params = {p.arg_name: p for p in docs.params}
1258
+ doc_returns = docs.returns
1259
+ if not docs.short_description and docs.long_description:
1260
+ doc_summary = docs.long_description or ''
1261
+ else:
1262
+ doc_summary = docs.short_description or ''
1263
+ doc_long_description = docs.long_description or ''
1264
+ for ex in docs.examples:
1265
+ ex_dict: Dict[str, Any] = {
1266
+ 'description': None,
1267
+ 'code': None,
1268
+ 'output': None,
1269
+ }
1270
+ if ex.description:
1271
+ ex_dict['description'] = ex.description
1272
+ if ex.snippet:
1273
+ code, output = [], []
1274
+ for line in ex.snippet.split('\n'):
1275
+ line = line.rstrip()
1276
+ if re.match(r'^(\w+>|>>>|\.\.\.)', line):
1277
+ code.append(line)
1278
+ else:
1279
+ output.append(line)
1280
+ ex_dict['code'] = '\n'.join(code) or None
1281
+ ex_dict['output'] = '\n'.join(output) or None
1282
+ if ex.post_snippet:
1283
+ ex_dict['postscript'] = ex.post_snippet
1284
+ doc_examples.append(ex_dict)
1285
+
1286
+ except Exception as e:
1287
+ logger.warning(
1288
+ f'Could not parse docstring for function {key}: {e}',
1289
+ )
1290
+
1224
1291
  if not func_name or key == func_name:
1225
1292
  sig = info['signature']
1226
1293
  args = []
1227
1294
 
1228
1295
  # Function arguments
1229
- for a in sig.get('args', []):
1296
+ for i, a in enumerate(sig.get('args', [])):
1297
+ name = a['name']
1230
1298
  dtype = a['dtype']
1231
1299
  nullable = '?' in dtype
1232
1300
  args.append(
1233
1301
  dict(
1234
- name=a['name'],
1302
+ name=name,
1235
1303
  dtype=dtype.replace('?', ''),
1236
1304
  nullable=nullable,
1305
+ description=(doc_params[name].description or '')
1306
+ if name in doc_params else '',
1237
1307
  ),
1238
1308
  )
1239
1309
  if a.get('default', no_default) is not no_default:
@@ -1250,6 +1320,8 @@ class Application(object):
1250
1320
  dict(
1251
1321
  dtype=dtype.replace('?', ''),
1252
1322
  nullable=nullable,
1323
+ description=doc_returns.description
1324
+ if doc_returns else '',
1253
1325
  ),
1254
1326
  )
1255
1327
  if a.get('name', None):
@@ -1263,6 +1335,9 @@ class Application(object):
1263
1335
  returns=returns,
1264
1336
  function_type=info['function_type'],
1265
1337
  sql_statement=sql,
1338
+ summary=doc_summary,
1339
+ long_description=doc_long_description,
1340
+ examples=doc_examples,
1266
1341
  )
1267
1342
 
1268
1343
  return functions
@@ -1303,6 +1378,7 @@ class Application(object):
1303
1378
  app_mode=self.app_mode,
1304
1379
  replace=replace,
1305
1380
  link=link or None,
1381
+ database=self.function_database or None,
1306
1382
  ),
1307
1383
  )
1308
1384
 
@@ -1332,7 +1408,7 @@ class Application(object):
1332
1408
  if replace:
1333
1409
  funcs, links = self._locate_app_functions(cur)
1334
1410
  for fname in funcs:
1335
- cur.execute(f'DROP FUNCTION IF EXISTS `{fname}`')
1411
+ cur.execute(f'DROP FUNCTION IF EXISTS {fname}')
1336
1412
  for link in links:
1337
1413
  cur.execute(f'DROP LINK {link}')
1338
1414
  for func in self.get_create_functions(replace=replace):
@@ -1358,7 +1434,7 @@ class Application(object):
1358
1434
  with conn.cursor() as cur:
1359
1435
  funcs, links = self._locate_app_functions(cur)
1360
1436
  for fname in funcs:
1361
- cur.execute(f'DROP FUNCTION IF EXISTS `{fname}`')
1437
+ cur.execute(f'DROP FUNCTION IF EXISTS {fname}')
1362
1438
  for link in links:
1363
1439
  cur.execute(f'DROP LINK {link}')
1364
1440
 
@@ -1415,6 +1491,7 @@ class Application(object):
1415
1491
  b'accepts': accepts[data_format.lower()],
1416
1492
  b's2-ef-name': name.encode('utf-8'),
1417
1493
  b's2-ef-version': data_version.encode('utf-8'),
1494
+ b's2-ef-ignore-cancel': b'true',
1418
1495
  },
1419
1496
  )
1420
1497
 
@@ -1679,6 +1756,14 @@ def main(argv: Optional[List[str]] = None) -> None:
1679
1756
  ),
1680
1757
  help='Suffix to add to function names',
1681
1758
  )
1759
+ parser.add_argument(
1760
+ '--function-database', metavar='function_database',
1761
+ default=defaults.get(
1762
+ 'function_database',
1763
+ get_option('external_function.function_database'),
1764
+ ),
1765
+ help='Database to use for the function definition',
1766
+ )
1682
1767
  parser.add_argument(
1683
1768
  'functions', metavar='module.or.func.path', nargs='*',
1684
1769
  help='functions or modules to export in UDF server',
@@ -1778,6 +1863,7 @@ def main(argv: Optional[List[str]] = None) -> None:
1778
1863
  app_mode='remote',
1779
1864
  name_prefix=args.name_prefix,
1780
1865
  name_suffix=args.name_suffix,
1866
+ function_database=args.function_database or None,
1781
1867
  )
1782
1868
 
1783
1869
  funcs = app.get_create_functions(replace=args.replace_existing)