singlestoredb 0.4.0__py3-none-any.whl → 1.0.4__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 (120) hide show
  1. singlestoredb/__init__.py +33 -1
  2. singlestoredb/alchemy/__init__.py +90 -0
  3. singlestoredb/auth.py +5 -1
  4. singlestoredb/config.py +116 -14
  5. singlestoredb/connection.py +483 -516
  6. singlestoredb/converters.py +238 -135
  7. singlestoredb/exceptions.py +30 -2
  8. singlestoredb/functions/__init__.py +1 -0
  9. singlestoredb/functions/decorator.py +142 -0
  10. singlestoredb/functions/dtypes.py +1639 -0
  11. singlestoredb/functions/ext/__init__.py +2 -0
  12. singlestoredb/functions/ext/arrow.py +375 -0
  13. singlestoredb/functions/ext/asgi.py +661 -0
  14. singlestoredb/functions/ext/json.py +427 -0
  15. singlestoredb/functions/ext/mmap.py +306 -0
  16. singlestoredb/functions/ext/rowdat_1.py +744 -0
  17. singlestoredb/functions/signature.py +673 -0
  18. singlestoredb/fusion/__init__.py +11 -0
  19. singlestoredb/fusion/graphql.py +213 -0
  20. singlestoredb/fusion/handler.py +621 -0
  21. singlestoredb/fusion/handlers/stage.py +257 -0
  22. singlestoredb/fusion/handlers/utils.py +162 -0
  23. singlestoredb/fusion/handlers/workspace.py +412 -0
  24. singlestoredb/fusion/registry.py +164 -0
  25. singlestoredb/fusion/result.py +399 -0
  26. singlestoredb/http/__init__.py +27 -0
  27. singlestoredb/{http.py → http/connection.py} +555 -154
  28. singlestoredb/management/__init__.py +3 -0
  29. singlestoredb/management/billing_usage.py +148 -0
  30. singlestoredb/management/cluster.py +14 -6
  31. singlestoredb/management/manager.py +100 -38
  32. singlestoredb/management/organization.py +188 -0
  33. singlestoredb/management/region.py +5 -5
  34. singlestoredb/management/utils.py +281 -2
  35. singlestoredb/management/workspace.py +1344 -49
  36. singlestoredb/{clients/pymysqlsv → mysql}/__init__.py +16 -21
  37. singlestoredb/{clients/pymysqlsv → mysql}/_auth.py +39 -8
  38. singlestoredb/{clients/pymysqlsv → mysql}/charset.py +26 -23
  39. singlestoredb/{clients/pymysqlsv/connections.py → mysql/connection.py} +532 -165
  40. singlestoredb/{clients/pymysqlsv → mysql}/constants/CLIENT.py +0 -1
  41. singlestoredb/{clients/pymysqlsv → mysql}/constants/COMMAND.py +0 -1
  42. singlestoredb/{clients/pymysqlsv → mysql}/constants/CR.py +0 -2
  43. singlestoredb/{clients/pymysqlsv → mysql}/constants/ER.py +0 -1
  44. singlestoredb/{clients/pymysqlsv → mysql}/constants/FIELD_TYPE.py +1 -1
  45. singlestoredb/{clients/pymysqlsv → mysql}/constants/FLAG.py +0 -1
  46. singlestoredb/{clients/pymysqlsv → mysql}/constants/SERVER_STATUS.py +0 -1
  47. singlestoredb/mysql/converters.py +271 -0
  48. singlestoredb/{clients/pymysqlsv → mysql}/cursors.py +228 -112
  49. singlestoredb/mysql/err.py +92 -0
  50. singlestoredb/{clients/pymysqlsv → mysql}/optionfile.py +5 -4
  51. singlestoredb/{clients/pymysqlsv → mysql}/protocol.py +49 -20
  52. singlestoredb/mysql/tests/__init__.py +19 -0
  53. singlestoredb/{clients/pymysqlsv → mysql}/tests/base.py +32 -12
  54. singlestoredb/mysql/tests/conftest.py +37 -0
  55. singlestoredb/{clients/pymysqlsv → mysql}/tests/test_DictCursor.py +11 -7
  56. singlestoredb/{clients/pymysqlsv → mysql}/tests/test_SSCursor.py +17 -12
  57. singlestoredb/{clients/pymysqlsv → mysql}/tests/test_basic.py +32 -24
  58. singlestoredb/{clients/pymysqlsv → mysql}/tests/test_connection.py +130 -119
  59. singlestoredb/{clients/pymysqlsv → mysql}/tests/test_converters.py +9 -7
  60. singlestoredb/mysql/tests/test_cursor.py +141 -0
  61. singlestoredb/{clients/pymysqlsv → mysql}/tests/test_err.py +3 -2
  62. singlestoredb/{clients/pymysqlsv → mysql}/tests/test_issues.py +35 -27
  63. singlestoredb/{clients/pymysqlsv → mysql}/tests/test_load_local.py +13 -11
  64. singlestoredb/{clients/pymysqlsv → mysql}/tests/test_nextset.py +7 -3
  65. singlestoredb/{clients/pymysqlsv → mysql}/tests/test_optionfile.py +2 -1
  66. singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/__init__.py +1 -1
  67. singlestoredb/mysql/tests/thirdparty/test_MySQLdb/__init__.py +9 -0
  68. singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/test_MySQLdb/capabilities.py +19 -17
  69. singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/test_MySQLdb/dbapi20.py +31 -22
  70. singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/test_MySQLdb/test_MySQLdb_capabilities.py +3 -4
  71. singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/test_MySQLdb/test_MySQLdb_dbapi20.py +24 -20
  72. singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/test_MySQLdb/test_MySQLdb_nonstandard.py +4 -4
  73. singlestoredb/{clients/pymysqlsv → mysql}/times.py +3 -4
  74. singlestoredb/pytest.py +283 -0
  75. singlestoredb/tests/empty.sql +0 -0
  76. singlestoredb/tests/ext_funcs/__init__.py +385 -0
  77. singlestoredb/tests/test.sql +210 -0
  78. singlestoredb/tests/test2.sql +1 -0
  79. singlestoredb/tests/test_basics.py +482 -115
  80. singlestoredb/tests/test_config.py +13 -13
  81. singlestoredb/tests/test_connection.py +241 -305
  82. singlestoredb/tests/test_dbapi.py +27 -0
  83. singlestoredb/tests/test_ext_func.py +1193 -0
  84. singlestoredb/tests/test_ext_func_data.py +1101 -0
  85. singlestoredb/tests/test_fusion.py +465 -0
  86. singlestoredb/tests/test_http.py +32 -26
  87. singlestoredb/tests/test_management.py +588 -8
  88. singlestoredb/tests/test_plugin.py +33 -0
  89. singlestoredb/tests/test_results.py +11 -12
  90. singlestoredb/tests/test_udf.py +687 -0
  91. singlestoredb/tests/utils.py +3 -2
  92. singlestoredb/utils/config.py +58 -0
  93. singlestoredb/utils/debug.py +13 -0
  94. singlestoredb/utils/mogrify.py +151 -0
  95. singlestoredb/utils/results.py +4 -1
  96. singlestoredb-1.0.4.dist-info/METADATA +139 -0
  97. singlestoredb-1.0.4.dist-info/RECORD +112 -0
  98. {singlestoredb-0.4.0.dist-info → singlestoredb-1.0.4.dist-info}/WHEEL +1 -1
  99. singlestoredb-1.0.4.dist-info/entry_points.txt +2 -0
  100. singlestoredb/clients/pymysqlsv/converters.py +0 -365
  101. singlestoredb/clients/pymysqlsv/err.py +0 -144
  102. singlestoredb/clients/pymysqlsv/tests/__init__.py +0 -19
  103. singlestoredb/clients/pymysqlsv/tests/test_cursor.py +0 -133
  104. singlestoredb/clients/pymysqlsv/tests/thirdparty/test_MySQLdb/__init__.py +0 -9
  105. singlestoredb/drivers/__init__.py +0 -45
  106. singlestoredb/drivers/base.py +0 -198
  107. singlestoredb/drivers/cymysql.py +0 -38
  108. singlestoredb/drivers/http.py +0 -47
  109. singlestoredb/drivers/mariadb.py +0 -40
  110. singlestoredb/drivers/mysqlconnector.py +0 -49
  111. singlestoredb/drivers/mysqldb.py +0 -60
  112. singlestoredb/drivers/pymysql.py +0 -37
  113. singlestoredb/drivers/pymysqlsv.py +0 -35
  114. singlestoredb/drivers/pyodbc.py +0 -65
  115. singlestoredb-0.4.0.dist-info/METADATA +0 -111
  116. singlestoredb-0.4.0.dist-info/RECORD +0 -86
  117. /singlestoredb/{clients → fusion/handlers}/__init__.py +0 -0
  118. /singlestoredb/{clients/pymysqlsv → mysql}/constants/__init__.py +0 -0
  119. {singlestoredb-0.4.0.dist-info → singlestoredb-1.0.4.dist-info}/LICENSE +0 -0
  120. {singlestoredb-0.4.0.dist-info → singlestoredb-1.0.4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,673 @@
1
+ #!/usr/bin/env python3
2
+ import datetime
3
+ import inspect
4
+ import numbers
5
+ import os
6
+ import re
7
+ import string
8
+ import typing
9
+ from typing import Any
10
+ from typing import Callable
11
+ from typing import Dict
12
+ from typing import List
13
+ from typing import Optional
14
+ from typing import Sequence
15
+ from typing import Tuple
16
+ from typing import TypeVar
17
+ from typing import Union
18
+
19
+ try:
20
+ import numpy as np
21
+ has_numpy = True
22
+ except ImportError:
23
+ has_numpy = False
24
+
25
+ from . import dtypes as dt
26
+ from ..mysql.converters import escape_item # type: ignore
27
+
28
+
29
+ array_types: Tuple[Any, ...]
30
+
31
+ if has_numpy:
32
+ array_types = (Sequence, np.ndarray)
33
+ numpy_type_map = {
34
+ np.integer: 'int64',
35
+ np.int_: 'int64',
36
+ np.int64: 'int64',
37
+ np.int32: 'int32',
38
+ np.int16: 'int16',
39
+ np.int8: 'int8',
40
+ np.uint: 'uint64',
41
+ np.unsignedinteger: 'uint64',
42
+ np.uint64: 'uint64',
43
+ np.uint32: 'uint32',
44
+ np.uint16: 'uint16',
45
+ np.uint8: 'uint8',
46
+ np.longlong: 'uint64',
47
+ np.ulonglong: 'uint64',
48
+ np.unicode_: 'str',
49
+ np.str_: 'str',
50
+ np.bytes_: 'bytes',
51
+ np.float_: 'float64',
52
+ np.float64: 'float64',
53
+ np.float32: 'float32',
54
+ np.float16: 'float16',
55
+ np.double: 'float64',
56
+ }
57
+ else:
58
+ array_types = (Sequence,)
59
+ numpy_type_map = {}
60
+
61
+ float_type_map = {
62
+ 'float': 'float64',
63
+ 'float_': 'float64',
64
+ 'float64': 'float64',
65
+ 'f8': 'float64',
66
+ 'double': 'float64',
67
+ 'float32': 'float32',
68
+ 'f4': 'float32',
69
+ 'float16': 'float16',
70
+ 'f2': 'float16',
71
+ 'float8': 'float8',
72
+ 'f1': 'float8',
73
+ }
74
+
75
+ int_type_map = {
76
+ 'int': 'int64',
77
+ 'integer': 'int64',
78
+ 'int_': 'int64',
79
+ 'int64': 'int64',
80
+ 'i8': 'int64',
81
+ 'int32': 'int32',
82
+ 'i4': 'int32',
83
+ 'int16': 'int16',
84
+ 'i2': 'int16',
85
+ 'int8': 'int8',
86
+ 'i1': 'int8',
87
+ 'uint': 'uint64',
88
+ 'uinteger': 'uint64',
89
+ 'uint_': 'uint64',
90
+ 'uint64': 'uint64',
91
+ 'u8': 'uint64',
92
+ 'uint32': 'uint32',
93
+ 'u4': 'uint32',
94
+ 'uint16': 'uint16',
95
+ 'u2': 'uint16',
96
+ 'uint8': 'uint8',
97
+ 'u1': 'uint8',
98
+ }
99
+
100
+ sql_type_map = {
101
+ 'bool': 'BOOL',
102
+ 'int8': 'TINYINT',
103
+ 'int16': 'SMALLINT',
104
+ 'int32': 'INT',
105
+ 'int64': 'BIGINT',
106
+ 'uint8': 'TINYINT UNSIGNED',
107
+ 'uint16': 'SMALLINT UNSIGNED',
108
+ 'uint32': 'INT UNSIGNED',
109
+ 'uint64': 'BIGINT UNSIGNED',
110
+ 'float32': 'FLOAT',
111
+ 'float64': 'DOUBLE',
112
+ 'str': 'TEXT',
113
+ 'bytes': 'BLOB',
114
+ 'null': 'NULL',
115
+ 'datetime': 'DATETIME',
116
+ 'datetime6': 'DATETIME(6)',
117
+ 'date': 'DATE',
118
+ 'time': 'TIME',
119
+ 'time6': 'TIME(6)',
120
+ }
121
+
122
+ sql_to_type_map = {
123
+ 'BOOL': 'bool',
124
+ 'TINYINT': 'int8',
125
+ 'TINYINT UNSIGNED': 'uint8',
126
+ 'SMALLINT': 'int16',
127
+ 'SMALLINT UNSIGNED': 'int16',
128
+ 'MEDIUMINT': 'int32',
129
+ 'MEDIUMINT UNSIGNED': 'int32',
130
+ 'INT24': 'int32',
131
+ 'INT24 UNSIGNED': 'int32',
132
+ 'INT': 'int32',
133
+ 'INT UNSIGNED': 'int32',
134
+ 'INTEGER': 'int32',
135
+ 'INTEGER UNSIGNED': 'int32',
136
+ 'BIGINT': 'int64',
137
+ 'BIGINT UNSIGNED': 'int64',
138
+ 'FLOAT': 'float32',
139
+ 'DOUBLE': 'float64',
140
+ 'REAL': 'float64',
141
+ 'DATE': 'date',
142
+ 'TIME': 'time',
143
+ 'TIME(6)': 'time6',
144
+ 'DATETIME': 'datetime',
145
+ 'DATETIME(6)': 'datetime',
146
+ 'TIMESTAMP': 'datetime',
147
+ 'TIMESTAMP(6)': 'datetime',
148
+ 'YEAR': 'uint64',
149
+ 'CHAR': 'str',
150
+ 'VARCHAR': 'str',
151
+ 'TEXT': 'str',
152
+ 'TINYTEXT': 'str',
153
+ 'MEDIUMTEXT': 'str',
154
+ 'LONGTEXT': 'str',
155
+ 'BINARY': 'bytes',
156
+ 'VARBINARY': 'bytes',
157
+ 'BLOB': 'bytes',
158
+ 'TINYBLOB': 'bytes',
159
+ 'MEDIUMBLOB': 'bytes',
160
+ 'LONGBLOB': 'bytes',
161
+ }
162
+
163
+
164
+ class Collection:
165
+ """Base class for collection data types."""
166
+
167
+ def __init__(self, *item_dtypes: Union[List[type], type]):
168
+ self.item_dtypes = item_dtypes
169
+
170
+
171
+ class TupleCollection(Collection):
172
+ pass
173
+
174
+
175
+ class ArrayCollection(Collection):
176
+ pass
177
+
178
+
179
+ def escape_name(name: str) -> str:
180
+ """Escape a function parameter name."""
181
+ if '`' in name:
182
+ name = name.replace('`', '``')
183
+ return f'`{name}`'
184
+
185
+
186
+ def simplify_dtype(dtype: Any) -> List[Any]:
187
+ """
188
+ Expand a type annotation to a flattened list of atomic types.
189
+
190
+ Parameters
191
+ ----------
192
+ dtype : Any
193
+ Python type annotation
194
+
195
+ Returns
196
+ -------
197
+ List[Any] -- list of dtype strings, TupleCollections, and ArrayCollections
198
+
199
+ """
200
+ origin = typing.get_origin(dtype)
201
+ atype = type(dtype)
202
+ args = []
203
+
204
+ # Flatten Unions
205
+ if origin is Union:
206
+ for x in typing.get_args(dtype):
207
+ args.extend(simplify_dtype(x))
208
+
209
+ # Expand custom types to individual types (does not support bounds)
210
+ elif atype is TypeVar:
211
+ for x in dtype.__constraints__:
212
+ args.extend(simplify_dtype(x))
213
+ if dtype.__bound__:
214
+ args.extend(simplify_dtype(dtype.__bound__))
215
+
216
+ # Sequence types
217
+ elif origin is not None and issubclass(origin, Sequence):
218
+ item_args: List[Union[List[type], type]] = []
219
+ for x in typing.get_args(dtype):
220
+ item_dtype = simplify_dtype(x)
221
+ if len(item_dtype) == 1:
222
+ item_args.append(item_dtype[0])
223
+ else:
224
+ item_args.append(item_dtype)
225
+ if origin is tuple or origin is Tuple:
226
+ args.append(TupleCollection(*item_args))
227
+ elif len(item_args) > 1:
228
+ raise TypeError('sequence types may only contain one item data type')
229
+ else:
230
+ args.append(ArrayCollection(*item_args))
231
+
232
+ # Not a Union or TypeVar
233
+ else:
234
+ args.append(dtype)
235
+
236
+ return args
237
+
238
+
239
+ def classify_dtype(dtype: Any) -> str:
240
+ """Classify the type annotation into a type name."""
241
+ if isinstance(dtype, list):
242
+ return '|'.join(classify_dtype(x) for x in dtype)
243
+
244
+ # Specific types
245
+ if dtype is None or dtype is type(None): # noqa: E721
246
+ return 'null'
247
+ if dtype is int:
248
+ return 'int64'
249
+ if dtype is float:
250
+ return 'float64'
251
+ if dtype is bool:
252
+ return 'bool'
253
+
254
+ if not inspect.isclass(dtype):
255
+ # Check for compound types
256
+ origin = typing.get_origin(dtype)
257
+ if origin is not None:
258
+ # Tuple type
259
+ if origin is Tuple:
260
+ args = typing.get_args(dtype)
261
+ item_dtypes = ','.join(classify_dtype(x) for x in args)
262
+ return f'tuple:{item_dtypes}'
263
+
264
+ # Array types
265
+ elif issubclass(origin, array_types):
266
+ args = typing.get_args(dtype)
267
+ item_dtype = classify_dtype(args[0])
268
+ return f'array[{item_dtype}]'
269
+
270
+ raise TypeError(f'unsupported type annotation: {dtype}')
271
+
272
+ if isinstance(dtype, ArrayCollection):
273
+ item_dtypes = ','.join(classify_dtype(x) for x in dtype.item_dtypes)
274
+ return f'array[{item_dtypes}]'
275
+
276
+ if isinstance(dtype, TupleCollection):
277
+ item_dtypes = ','.join(classify_dtype(x) for x in dtype.item_dtypes)
278
+ return f'tuple[{item_dtypes}]'
279
+
280
+ # Check numpy types if it's available
281
+ if dtype in numpy_type_map:
282
+ return numpy_type_map[dtype]
283
+
284
+ # Broad numeric types
285
+ if issubclass(dtype, int):
286
+ return 'int64'
287
+ if issubclass(dtype, float):
288
+ return 'float64'
289
+
290
+ # Strings / Bytes
291
+ if issubclass(dtype, str):
292
+ return 'str'
293
+ if issubclass(dtype, (bytes, bytearray)):
294
+ return 'bytes'
295
+
296
+ # Date / Times
297
+ if issubclass(dtype, datetime.datetime):
298
+ return 'datetime'
299
+ if issubclass(dtype, datetime.date):
300
+ return 'date'
301
+ if issubclass(dtype, datetime.timedelta):
302
+ return 'time'
303
+
304
+ # Last resort, guess it by the name...
305
+ name = dtype.__name__.lower()
306
+ is_float = issubclass(dtype, numbers.Real)
307
+ is_int = issubclass(dtype, numbers.Integral)
308
+ if is_float:
309
+ return float_type_map.get(name, 'float64')
310
+ if is_int:
311
+ return int_type_map.get(name, 'int64')
312
+
313
+ raise TypeError(f'unsupported type annotation: {dtype}')
314
+
315
+
316
+ def collapse_dtypes(dtypes: Union[str, List[str]]) -> str:
317
+ """
318
+ Collapse a dtype possibly containing multiple data types to one type.
319
+
320
+ Parameters
321
+ ----------
322
+ dtypes : str or list[str]
323
+ The data types to collapse
324
+
325
+ Returns
326
+ -------
327
+ str
328
+
329
+ """
330
+ if not isinstance(dtypes, list):
331
+ return dtypes
332
+
333
+ orig_dtypes = dtypes
334
+ dtypes = list(set(dtypes))
335
+
336
+ is_nullable = 'null' in dtypes
337
+
338
+ dtypes = [x for x in dtypes if x != 'null']
339
+
340
+ if 'uint64' in dtypes:
341
+ dtypes = [x for x in dtypes if x not in ('uint8', 'uint16', 'uint32')]
342
+ if 'uint32' in dtypes:
343
+ dtypes = [x for x in dtypes if x not in ('uint8', 'uint16')]
344
+ if 'uint16' in dtypes:
345
+ dtypes = [x for x in dtypes if x not in ('uint8',)]
346
+
347
+ if 'int64' in dtypes:
348
+ dtypes = [
349
+ x for x in dtypes if x not in (
350
+ 'int8', 'int16', 'int32',
351
+ 'uint8', 'uint16', 'uint32',
352
+ )
353
+ ]
354
+ if 'int32' in dtypes:
355
+ dtypes = [
356
+ x for x in dtypes if x not in (
357
+ 'int8', 'int16',
358
+ 'uint8', 'uint16',
359
+ )
360
+ ]
361
+ if 'int16' in dtypes:
362
+ dtypes = [x for x in dtypes if x not in ('int8', 'uint8')]
363
+
364
+ if 'float64' in dtypes:
365
+ dtypes = [
366
+ x for x in dtypes if x not in (
367
+ 'float32',
368
+ 'int8', 'int16', 'int32', 'int64',
369
+ 'uint8', 'uint16', 'uint32', 'uint64',
370
+ )
371
+ ]
372
+ if 'float32' in dtypes:
373
+ dtypes = [
374
+ x for x in dtypes if x not in (
375
+ 'int8', 'int16', 'int32',
376
+ 'uint8', 'uint16', 'uint32',
377
+ )
378
+ ]
379
+
380
+ for i, item in enumerate(dtypes):
381
+
382
+ if item.startswith('array[') and '|' in item:
383
+ _, item_spec = item.split('[', 1)
384
+ item_spec = item_spec[:-1]
385
+ item = collapse_dtypes(item_spec.split('|'))
386
+ dtypes[i] = f'array[{item}]'
387
+
388
+ elif item.startswith('tuple[') and '|' in item:
389
+ _, item_spec = item.split('[', 1)
390
+ item_spec = item_spec[:-1]
391
+ sub_dtypes = []
392
+ for subitem_spec in item_spec.split(','):
393
+ item = collapse_dtypes(subitem_spec.split('|'))
394
+ sub_dtypes.append(item)
395
+ dtypes[i] = f'tuple[{",".join(sub_dtypes)}]'
396
+
397
+ if len(dtypes) > 1:
398
+ raise TypeError(
399
+ 'types can not be collapsed to a single type: '
400
+ f'{", ".join(orig_dtypes)}',
401
+ )
402
+
403
+ if not dtypes:
404
+ return 'null'
405
+
406
+ return dtypes[0] + ('?' if is_nullable else '')
407
+
408
+
409
+ def get_signature(func: Callable[..., Any], name: Optional[str] = None) -> Dict[str, Any]:
410
+ '''
411
+ Print the UDF signature of the Python callable.
412
+
413
+ Parameters
414
+ ----------
415
+ func : Callable
416
+ The function to extract the signature of
417
+ name : str, optional
418
+ Name override for function
419
+
420
+ Returns
421
+ -------
422
+ Dict[str, Any]
423
+
424
+ '''
425
+ signature = inspect.signature(func)
426
+ args: List[Dict[str, Any]] = []
427
+ attrs = getattr(func, '_singlestoredb_attrs', {})
428
+ name = attrs.get('name', name if name else func.__name__)
429
+ out: Dict[str, Any] = dict(name=name, args=args)
430
+
431
+ arg_names = [x for x in signature.parameters]
432
+ defaults = [
433
+ x.default if x.default is not inspect.Parameter.empty else None
434
+ for x in signature.parameters.values()
435
+ ]
436
+ annotations = {
437
+ k: x.annotation for k, x in signature.parameters.items()
438
+ if x.annotation is not inspect.Parameter.empty
439
+ }
440
+
441
+ for p in signature.parameters.values():
442
+ if p.kind == inspect.Parameter.VAR_POSITIONAL:
443
+ raise TypeError('variable positional arguments are not supported')
444
+ elif p.kind == inspect.Parameter.VAR_KEYWORD:
445
+ raise TypeError('variable keyword arguments are not supported')
446
+
447
+ args_overrides = attrs.get('args', None)
448
+ returns_overrides = attrs.get('returns', None)
449
+
450
+ spec_diff = set(arg_names).difference(set(annotations.keys()))
451
+
452
+ # Make sure all arguments are annotated
453
+ if spec_diff and args_overrides is None:
454
+ raise TypeError(
455
+ 'missing annotations for {} in {}'
456
+ .format(', '.join(spec_diff), name),
457
+ )
458
+ elif isinstance(args_overrides, dict):
459
+ for s in spec_diff:
460
+ if s not in args_overrides:
461
+ raise TypeError(
462
+ 'missing annotations for {} in {}'
463
+ .format(', '.join(spec_diff), name),
464
+ )
465
+ elif isinstance(args_overrides, list):
466
+ if len(arg_names) != len(args_overrides):
467
+ raise TypeError(
468
+ 'number of annotations does not match in {}: {}'
469
+ .format(name, ', '.join(spec_diff)),
470
+ )
471
+
472
+ for i, arg in enumerate(arg_names):
473
+ if isinstance(args_overrides, list):
474
+ sql = args_overrides[i]
475
+ arg_type = sql_to_dtype(sql)
476
+ elif isinstance(args_overrides, dict) and arg in args_overrides:
477
+ sql = args_overrides[arg]
478
+ arg_type = sql_to_dtype(sql)
479
+ elif isinstance(args_overrides, str):
480
+ sql = args_overrides
481
+ arg_type = sql_to_dtype(sql)
482
+ elif args_overrides is not None \
483
+ and not isinstance(args_overrides, (list, dict, str)):
484
+ raise TypeError(f'unrecognized type for arguments: {args_overrides}')
485
+ else:
486
+ arg_type = collapse_dtypes([
487
+ classify_dtype(x) for x in simplify_dtype(annotations[arg])
488
+ ])
489
+ sql = dtype_to_sql(arg_type)
490
+ args.append(dict(name=arg, dtype=arg_type, sql=sql, default=defaults[i]))
491
+
492
+ if returns_overrides is None \
493
+ and signature.return_annotation is inspect.Signature.empty:
494
+ raise TypeError(f'no return value annotation in function {name}')
495
+
496
+ if isinstance(returns_overrides, str):
497
+ sql = returns_overrides
498
+ out_type = sql_to_dtype(sql)
499
+ elif returns_overrides is not None and not isinstance(returns_overrides, str):
500
+ raise TypeError(f'unrecognized type for return value: {returns_overrides}')
501
+ else:
502
+ out_type = collapse_dtypes([
503
+ classify_dtype(x) for x in simplify_dtype(signature.return_annotation)
504
+ ])
505
+ sql = dtype_to_sql(out_type)
506
+ out['returns'] = dict(dtype=out_type, sql=sql, default=None)
507
+
508
+ copied_keys = ['database', 'environment', 'packages', 'resources', 'replace']
509
+ for key in copied_keys:
510
+ if attrs.get(key):
511
+ out[key] = attrs[key]
512
+
513
+ out['endpoint'] = '/invoke'
514
+ out['doc'] = func.__doc__
515
+
516
+ return out
517
+
518
+
519
+ def sql_to_dtype(sql: str) -> str:
520
+ """
521
+ Convert a SQL type into a normalized data type identifier.
522
+
523
+ Parameters
524
+ ----------
525
+ sql : str
526
+ SQL data type specification
527
+
528
+ Returns
529
+ -------
530
+ str
531
+
532
+ """
533
+ sql = re.sub(r'\s+', r' ', sql.upper().strip())
534
+
535
+ m = re.match(r'(\w+)(\([^\)]+\))?', sql)
536
+ if not m:
537
+ raise TypeError(f'unrecognized data type: {sql}')
538
+
539
+ sql_type = m.group(1)
540
+ type_attrs = re.split(r'\s*,\s*', m.group(2) or '')
541
+
542
+ if sql_type in ('DATETIME', 'TIME', 'TIMESTAMP') and \
543
+ type_attrs and type_attrs[0] == '6':
544
+ sql_type += '(6)'
545
+
546
+ elif ' UNSIGNED' in sql:
547
+ sql_type += ' UNSIGNED'
548
+
549
+ try:
550
+ dtype = sql_to_type_map[sql_type]
551
+ except KeyError:
552
+ raise TypeError(f'unrecognized data type: {sql_type}')
553
+
554
+ if ' NOT NULL' not in sql:
555
+ dtype += '?'
556
+
557
+ return dtype
558
+
559
+
560
+ def dtype_to_sql(dtype: str, default: Any = None) -> str:
561
+ """
562
+ Convert a collapsed dtype string to a SQL type.
563
+
564
+ Parameters
565
+ ----------
566
+ dtype : str
567
+ Simplified data type string
568
+ default : Any, optional
569
+ Default value
570
+
571
+ Returns
572
+ -------
573
+ str
574
+
575
+ """
576
+ nullable = ' NOT NULL'
577
+ if dtype.endswith('?'):
578
+ nullable = ' NULL'
579
+ dtype = dtype[:-1]
580
+
581
+ if dtype == 'null':
582
+ nullable = ''
583
+
584
+ default_clause = ''
585
+ if default is not None:
586
+ if default is dt.NULL:
587
+ default = None
588
+ default_clause = f' DEFAULT {escape_item(default, "utf8")}'
589
+
590
+ if dtype.startswith('array['):
591
+ _, dtypes = dtype.split('[', 1)
592
+ dtypes = dtypes[:-1]
593
+ item_dtype = dtype_to_sql(dtypes)
594
+ return f'ARRAY({item_dtype}){nullable}{default_clause}'
595
+
596
+ if dtype.startswith('tuple['):
597
+ _, dtypes = dtype.split('[', 1)
598
+ dtypes = dtypes[:-1]
599
+ item_dtypes = []
600
+ for i, item in enumerate(dtypes.split(',')):
601
+ name = string.ascii_letters[i]
602
+ if '=' in item:
603
+ name, item = item.split('=', 1)
604
+ item_dtypes.append(name + ' ' + dtype_to_sql(item))
605
+ return f'RECORD({", ".join(item_dtypes)}){nullable}{default_clause}'
606
+
607
+ return f'{sql_type_map[dtype]}{nullable}{default_clause}'
608
+
609
+
610
+ def signature_to_sql(
611
+ signature: Dict[str, Any],
612
+ url: Optional[str] = None,
613
+ data_format: str = 'rowdat_1',
614
+ app_mode: str = 'remote',
615
+ link: Optional[str] = None,
616
+ replace: bool = False,
617
+ ) -> str:
618
+ '''
619
+ Convert a dictionary function signature into SQL.
620
+
621
+ Parameters
622
+ ----------
623
+ signature : Dict[str, Any]
624
+ Function signature in the form of a dictionary as returned by
625
+ the `get_signature` function
626
+
627
+ Returns
628
+ -------
629
+ str : SQL formatted function signature
630
+
631
+ '''
632
+ args = []
633
+ for arg in signature['args']:
634
+ # Use default value from Python function if SQL doesn't set one
635
+ default = ''
636
+ if not re.search(r'\s+default\s+\S', arg['sql'], flags=re.I):
637
+ default = ''
638
+ if arg.get('default', None) is not None:
639
+ default = f' DEFAULT {escape_item(arg["default"], "utf8")}'
640
+ args.append(escape_name(arg['name']) + ' ' + arg['sql'] + default)
641
+
642
+ returns = ''
643
+ if signature.get('returns'):
644
+ res = signature['returns']['sql']
645
+ returns = f' RETURNS {res}'
646
+
647
+ host = os.environ.get('SINGLESTOREDB_EXT_HOST', '127.0.0.1')
648
+ port = os.environ.get('SINGLESTOREDB_EXT_PORT', '8000')
649
+
650
+ if app_mode.lower() == 'remote':
651
+ url = url or f'https://{host}:{port}/invoke'
652
+ elif url is None:
653
+ raise ValueError('url can not be `None`')
654
+
655
+ database = ''
656
+ if signature.get('database'):
657
+ database = escape_name(signature['database']) + '.'
658
+
659
+ or_replace = 'OR REPLACE ' if (bool(signature.get('replace')) or replace) else ''
660
+
661
+ link_str = ''
662
+ if link:
663
+ if not re.match(r'^[\w_]+$', link):
664
+ raise ValueError(f'invalid LINK name: {link}')
665
+ link_str = f' LINK {link}'
666
+
667
+ return (
668
+ f'CREATE {or_replace}EXTERNAL FUNCTION ' +
669
+ f'{database}{escape_name(signature["name"])}' +
670
+ '(' + ', '.join(args) + ')' + returns +
671
+ f' AS {app_mode.upper()} SERVICE "{url}" FORMAT {data_format.upper()}'
672
+ f'{link_str};'
673
+ )
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env python3
2
+ import importlib
3
+ import os
4
+
5
+ from .registry import execute
6
+ from .registry import get_handler
7
+
8
+ # Load all files in handlers directory
9
+ for f in os.listdir(os.path.join(os.path.dirname(__file__), 'handlers')):
10
+ if f.endswith('.py') and not f.startswith('_'):
11
+ importlib.import_module(f'singlestoredb.fusion.handlers.{f[:-3]}')