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,306 @@
1
+ #!/usr/bin/env python
2
+ '''
3
+ Module for creating collocated Python UDFs
4
+
5
+ This module implements the collocated form of external functions for
6
+ SingleStoreDB. This mode uses a socket for control communications
7
+ and memory mapped files for passing the data to and from the UDF.
8
+
9
+ The command below is a sample invocation. It exports all functions
10
+ within the `myfuncs` Python module that have a `@udf` decorator on
11
+ them. The `--db` option specifies a database connection string.
12
+ If this exists, the UDF application will connect to the database
13
+ and register all functions. The `--replace-existing` option indicates
14
+ that existing functions should be replaced::
15
+
16
+ python -m singlestoredb.functions.ext.mmap \
17
+ --db=root:@127.0.0.1:9306/cosmeticshop --replace-existing \
18
+ myfuncs
19
+
20
+ The `myfuncs` package can be any Python package in your Python path.
21
+ It must contain functions marked with a `@udf` decorator and the
22
+ types must be annotated or specified using the `@udf` decorator
23
+ similar to the following::
24
+
25
+ from singlestoredb.functions import udf
26
+
27
+ @udf
28
+ def print_it(x2: float, x3: str) -> str:
29
+ return int(x2) * x3
30
+
31
+ @udf.pandas
32
+ def print_it_pandas(x2: float, x3: str) -> str:
33
+ return x2.astype(np.int64) * x3.astype(str)
34
+
35
+ With the functions registered, you can now run the UDFs::
36
+
37
+ SELECT print_it(3.14, 'my string');
38
+ SELECT print_it_pandas(3.14, 'my string');
39
+
40
+ '''
41
+ import argparse
42
+ import array
43
+ import asyncio
44
+ import io
45
+ import logging
46
+ import mmap
47
+ import multiprocessing
48
+ import os
49
+ import secrets
50
+ import socket
51
+ import struct
52
+ import sys
53
+ import tempfile
54
+ import threading
55
+ import traceback
56
+ from typing import Any
57
+
58
+ from . import asgi
59
+
60
+
61
+ logger = logging.getLogger('singlestoredb.functions.ext.mmap')
62
+ handler = logging.StreamHandler()
63
+ formatter = logging.Formatter('%(asctime)s %(name)-12s %(levelname)-8s %(message)s')
64
+ handler.setFormatter(formatter)
65
+ logger.addHandler(handler)
66
+ logger.setLevel(logging.INFO)
67
+
68
+
69
+ def _handle_request(app: Any, connection: Any, client_address: Any) -> None:
70
+ '''
71
+ Handle function call request.
72
+
73
+ Parameters:
74
+ app : ASGI app
75
+ An ASGI application from the singlestoredb.functions.ext.asgi module
76
+ connection : socket connection
77
+ Socket connection for function control messages
78
+ client_address : string
79
+ Address of connecting client
80
+
81
+ '''
82
+ logger.info('connection from {}'.format(str(connection).split(', ')[0][-4:]))
83
+
84
+ # Receive the request header. Format:
85
+ # server version: uint64
86
+ # length of function name: uint64
87
+ buf = connection.recv(16)
88
+ version, namelen = struct.unpack('<qq', buf)
89
+
90
+ # Python's recvmsg returns a tuple. We only really care about the first
91
+ # two parts. The recvmsg call has a weird way of specifying the size for
92
+ # the file descriptor array; basically, we're indicating we want to read
93
+ # two 32-bit ints (for the input and output files).
94
+ fd_model = array.array('i', [0, 0])
95
+ msg, ancdata, flags, addr = connection.recvmsg(
96
+ namelen,
97
+ socket.CMSG_LEN(2 * fd_model.itemsize),
98
+ )
99
+ assert len(ancdata) == 1
100
+
101
+ # The function's name will be in the "message" area of the recvmsg response.
102
+ # It will be populated with `namelen` bytes.
103
+ name = msg.decode('utf8')
104
+
105
+ # Two file descriptors are transferred to us from the database via the
106
+ # `sendmsg` protocol. These are for reading the input rows and writing
107
+ # the output rows, respectively.
108
+ fd0, fd1 = struct.unpack('<ii', ancdata[0][2])
109
+ ifile = os.fdopen(fd0, 'rb')
110
+ ofile = os.fdopen(fd1, 'wb')
111
+
112
+ # Keep receiving data on this socket until we run out.
113
+ while True:
114
+
115
+ # Read in the length of this row, a uint64. No data means we're done
116
+ # receiving.
117
+ buf = connection.recv(8)
118
+ if not buf:
119
+ break
120
+ length = struct.unpack('<q', buf)[0]
121
+ if not length:
122
+ break
123
+
124
+ # Map in the input shared memory segment from the fd we received via
125
+ # recvmsg.
126
+ mem = mmap.mmap(
127
+ ifile.fileno(),
128
+ length,
129
+ mmap.MAP_SHARED,
130
+ mmap.PROT_READ,
131
+ )
132
+
133
+ # Read row data
134
+ response_size = 0
135
+ out = io.BytesIO()
136
+
137
+ ifile.seek(0)
138
+ try:
139
+ # Run the function
140
+ asyncio.run(
141
+ app.call(
142
+ name,
143
+ io.BytesIO(ifile.read(length)),
144
+ out,
145
+ data_format='rowdat_1',
146
+ data_version='1.0',
147
+ ),
148
+ )
149
+
150
+ # Write results
151
+ buf = out.getbuffer()
152
+ response_size = len(buf)
153
+ ofile.truncate(max(128*1024, response_size))
154
+ ofile.seek(0)
155
+ ofile.write(buf)
156
+ ofile.flush()
157
+
158
+ # Complete the request by send back the status as two uint64s on the
159
+ # socket:
160
+ # - http status
161
+ # - size of data in output shared memory
162
+ connection.send(struct.pack('<qq', 200, response_size))
163
+
164
+ except Exception as exc:
165
+ errmsg = f'error occurred in executing function `{name}`: {exc}\n'
166
+ logger.error(errmsg.rstrip())
167
+ for line in traceback.format_exception(exc): # type: ignore
168
+ logger.error(line.rstrip())
169
+ connection.send(
170
+ struct.pack(
171
+ f'<qq{len(errmsg)}s', 500,
172
+ len(errmsg), errmsg.encode('utf8'),
173
+ ),
174
+ )
175
+ break
176
+
177
+ finally:
178
+ # Close the shared memory object.
179
+ mem.close()
180
+
181
+ # Close shared memory files.
182
+ ifile.close()
183
+ ofile.close()
184
+
185
+ # Close the connection
186
+ connection.close()
187
+
188
+
189
+ if __name__ == '__main__':
190
+ parser = argparse.ArgumentParser(
191
+ prog='python -m singlestoredb.functions.ext.mmap',
192
+ description='Run a collacated Python UDF server',
193
+ )
194
+ parser.add_argument(
195
+ '--max-connections', metavar='n', type=int, default=32,
196
+ help='maximum number of server connections before refusing them',
197
+ )
198
+ parser.add_argument(
199
+ '--single-thread', default=False, action='store_true',
200
+ help='should the server run in single-thread mode?',
201
+ )
202
+ parser.add_argument(
203
+ '--socket-path', metavar='file-path',
204
+ default=os.path.join(tempfile.gettempdir(), secrets.token_hex(16)),
205
+ help='path to communications socket',
206
+ )
207
+ parser.add_argument(
208
+ '--db', metavar='conn-str', default='',
209
+ help='connection string to use for registering functions',
210
+ )
211
+ parser.add_argument(
212
+ '--replace-existing', action='store_true',
213
+ help='should existing functions of the same name '
214
+ 'in the database be replaced?',
215
+ )
216
+ parser.add_argument(
217
+ '--log-level', metavar='[info|debug|warning|error]', default='info',
218
+ help='logging level',
219
+ )
220
+ parser.add_argument(
221
+ '--process-mode', metavar='[thread|subprocess]', default='subprocess',
222
+ help='how to handle concurrent handlers',
223
+ )
224
+ parser.add_argument(
225
+ 'functions', metavar='module.or.func.path', nargs='*',
226
+ help='functions or modules to export in UDF server',
227
+ )
228
+ args = parser.parse_args()
229
+
230
+ logger.setLevel(getattr(logging, args.log_level.upper()))
231
+
232
+ if os.path.exists(args.socket_path):
233
+ try:
234
+ os.unlink(args.socket_path)
235
+ except (IOError, OSError):
236
+ logger.error(f'could not remove existing socket path: {args.socket_path}')
237
+ sys.exit(1)
238
+
239
+ # Create application
240
+ app = asgi.create_app(
241
+ args.functions,
242
+ app_mode='collocated',
243
+ data_format='rowdat_1',
244
+ url=args.socket_path,
245
+ )
246
+
247
+ funcs = app.show_create_functions(replace=True) # type: ignore
248
+ if not funcs:
249
+ logger.error('no functions specified')
250
+ sys.exit(1)
251
+
252
+ for f in funcs:
253
+ logger.info(f'function: {f}')
254
+
255
+ # Register functions with database
256
+ if args.db:
257
+ logger.info('registering functions with database')
258
+ app.register_functions(args.db, replace=args.replace_existing) # type: ignore
259
+
260
+ # Create the Unix socket server.
261
+ server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
262
+
263
+ # Bind our server to the path.
264
+ server.bind(args.socket_path)
265
+
266
+ logger.info(f'using socket path: {args.socket_path}')
267
+
268
+ # Listen for incoming connections. Argument is the number of connections to
269
+ # keep in the backlog before we begin refusing them; 32 is plenty for this
270
+ # simple case.
271
+ server.listen(args.max_connections)
272
+
273
+ # Accept connections forever.
274
+ try:
275
+ while True:
276
+ # Listen for the next connection on our port.
277
+ connection, client_address = server.accept()
278
+
279
+ if args.process_mode == 'thread':
280
+ tcls = threading.Thread
281
+ else:
282
+ tcls = multiprocessing.Process # type: ignore
283
+
284
+ t = tcls(
285
+ target=_handle_request,
286
+ args=(app, connection, client_address),
287
+ )
288
+
289
+ t.start()
290
+
291
+ # NOTE: The following line forces this process to handle requests
292
+ # serially. This makes it easier to understand what's going on.
293
+ # In real life, though, parallel is much faster. To use parallel
294
+ # handling, just comment out the next line.
295
+ if args.single_thread:
296
+ t.join()
297
+
298
+ except KeyboardInterrupt:
299
+ sys.exit(0)
300
+
301
+ finally:
302
+ # Remove the socket file before we exit.
303
+ try:
304
+ os.unlink(args.socket_path)
305
+ except (IOError, OSError):
306
+ logger.error(f'could not remove socket path: {args.socket_path}')