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
@@ -1,11 +1,167 @@
1
1
  #!/usr/bin/env python
2
2
  """SingleStoreDB Cluster Management."""
3
3
  import datetime
4
+ import functools
5
+ import os
6
+ import re
7
+ import sys
4
8
  from typing import Any
9
+ from typing import Callable
10
+ from typing import Dict
11
+ from typing import List
12
+ from typing import Mapping
5
13
  from typing import Optional
14
+ from typing import SupportsIndex
15
+ from typing import TypeVar
6
16
  from typing import Union
17
+ from urllib.parse import urlparse
18
+
19
+ import jwt
7
20
 
8
21
  from .. import converters
22
+ from ..config import get_option
23
+
24
+ JSON = Union[str, List[str], Dict[str, 'JSON']]
25
+ JSONObj = Dict[str, JSON]
26
+ JSONList = List[JSON]
27
+ T = TypeVar('T')
28
+
29
+ if sys.version_info < (3, 10):
30
+ PathLike = Union[str, os.PathLike]
31
+ PathLikeABC = os.PathLike
32
+ else:
33
+ PathLike = Union[str, os.PathLike[str]]
34
+ PathLikeABC = os.PathLike[str]
35
+
36
+
37
+ class TTLProperty(object):
38
+ """Property with time limit."""
39
+
40
+ def __init__(self, fget: Callable[[Any], Any], ttl: datetime.timedelta):
41
+ self.fget = fget
42
+ self.ttl = ttl
43
+ self._last_executed = datetime.datetime(2000, 1, 1)
44
+ self._last_result = None
45
+ self.__doc__ = fget.__doc__
46
+ self._name = ''
47
+
48
+ def reset(self) -> None:
49
+ self._last_executed = datetime.datetime(2000, 1, 1)
50
+ self._last_result = None
51
+
52
+ def __set_name__(self, owner: Any, name: str) -> None:
53
+ self._name = name
54
+
55
+ def __get__(self, obj: Any, objtype: Any = None) -> Any:
56
+ if obj is None:
57
+ return self
58
+
59
+ if self._last_result is not None \
60
+ and (datetime.datetime.now() - self._last_executed) < self.ttl:
61
+ return self._last_result
62
+
63
+ self._last_result = self.fget(obj)
64
+ self._last_executed = datetime.datetime.now()
65
+
66
+ return self._last_result
67
+
68
+
69
+ def ttl_property(ttl: datetime.timedelta) -> Callable[[Any], Any]:
70
+ """Property with a time-to-live."""
71
+ def wrapper(func: Callable[[Any], Any]) -> Any:
72
+ out = TTLProperty(func, ttl=ttl)
73
+ return functools.wraps(func)(out)
74
+ return wrapper
75
+
76
+
77
+ class NamedList(List[T]):
78
+ """List class which also allows selection by ``name`` and ``id`` attribute."""
79
+
80
+ def _find_item(self, key: str) -> T:
81
+ for item in self:
82
+ if getattr(item, 'name', '') == key:
83
+ return item
84
+ if getattr(item, 'id', '') == key:
85
+ return item
86
+ raise KeyError(key)
87
+
88
+ def __getitem__(self, key: Union[SupportsIndex, slice, str]) -> Any:
89
+ if isinstance(key, str):
90
+ return self._find_item(key)
91
+ return super().__getitem__(key)
92
+
93
+ def __contains__(self, key: Any) -> bool:
94
+ if isinstance(key, str):
95
+ try:
96
+ self._find_item(key)
97
+ return True
98
+ except KeyError:
99
+ return False
100
+ return super().__contains__(key)
101
+
102
+ def names(self) -> List[str]:
103
+ """Return ``name`` attribute of each item."""
104
+ return [y for y in [getattr(x, 'name', None) for x in self] if y is not None]
105
+
106
+ def ids(self) -> List[str]:
107
+ """Return ``id`` attribute of each item."""
108
+ return [y for y in [getattr(x, 'id', None) for x in self] if y is not None]
109
+
110
+ def get(self, name_or_id: str, *default: Any) -> Any:
111
+ """Return object with name / ID if it exists, or return default value."""
112
+ try:
113
+ return self._find_item(name_or_id)
114
+ except KeyError:
115
+ if default:
116
+ return default[0]
117
+ raise
118
+
119
+
120
+ def get_token() -> Optional[str]:
121
+ """Return the token for the Management API."""
122
+ # See if an API key is configured
123
+ tok = get_option('management.token')
124
+ if tok:
125
+ return tok
126
+
127
+ url = os.environ.get('SINGLESTOREDB_URL')
128
+ if not url:
129
+ # See if the connection URL contains a JWT
130
+ url = get_option('host')
131
+ if not url:
132
+ return None
133
+
134
+ urlp = urlparse(url, scheme='singlestoredb', allow_fragments=True)
135
+ if urlp.password:
136
+ try:
137
+ jwt.decode(urlp.password, options={'verify_signature': False})
138
+ return urlp.password
139
+ except jwt.DecodeError:
140
+ pass
141
+
142
+ # Didn't find a key anywhere
143
+ return None
144
+
145
+
146
+ def get_organization() -> Optional[str]:
147
+ """Return the organization for the current token or environment."""
148
+ org = os.environ.get('SINGLESTOREDB_ORGANIZATION')
149
+ if org:
150
+ return org
151
+
152
+ return None
153
+
154
+
155
+ def enable_http_tracing() -> None:
156
+ """Enable tracing of HTTP requests."""
157
+ import logging
158
+ import http.client as http_client
159
+ http_client.HTTPConnection.debuglevel = 1
160
+ logging.basicConfig()
161
+ logging.getLogger().setLevel(logging.DEBUG)
162
+ requests_log = logging.getLogger('requests.packages.urllib3')
163
+ requests_log.setLevel(logging.DEBUG)
164
+ requests_log.propagate = True
9
165
 
10
166
 
11
167
  def to_datetime(
@@ -16,20 +172,143 @@ def to_datetime(
16
172
  return None
17
173
  if isinstance(obj, datetime.datetime):
18
174
  return obj
175
+ if obj == '0001-01-01T00:00:00Z':
176
+ return None
19
177
  obj = obj.replace('Z', '')
20
178
  # Fix datetimes with truncated zeros
21
179
  if '.' in obj:
22
180
  obj, micros = obj.split('.', 1)
23
181
  micros = micros + '0' * (6 - len(micros))
24
182
  obj = obj + '.' + micros
25
- return converters.datetime_fromisoformat(obj)
183
+ out = converters.datetime_fromisoformat(obj)
184
+ if isinstance(out, str):
185
+ return None
186
+ if isinstance(out, datetime.date):
187
+ return datetime.datetime(out.year, out.month, out.day)
188
+ return out
189
+
190
+
191
+ def from_datetime(
192
+ obj: Union[str, datetime.datetime],
193
+ ) -> Optional[str]:
194
+ """Convert datetime to string."""
195
+ if not obj:
196
+ return None
197
+ if isinstance(obj, str):
198
+ return obj
199
+ out = obj.isoformat()
200
+ if not re.search(r'[A-Za-z]$', out):
201
+ out = f'{out}Z'
202
+ return out
26
203
 
27
204
 
28
205
  def vars_to_str(obj: Any) -> str:
29
206
  """Render a string representation of vars(obj)."""
30
207
  attrs = []
31
- for name, value in sorted(vars(obj).items()):
208
+ obj_vars = vars(obj)
209
+ if 'name' in obj_vars:
210
+ attrs.append('name={}'.format(repr(obj_vars['name'])))
211
+ if 'id' in obj_vars:
212
+ attrs.append('id={}'.format(repr(obj_vars['id'])))
213
+ for name, value in sorted(obj_vars.items()):
214
+ if name in ('name', 'id'):
215
+ continue
32
216
  if not value or name.startswith('_'):
33
217
  continue
34
218
  attrs.append('{}={}'.format(name, repr(value)))
35
219
  return '{}({})'.format(type(obj).__name__, ', '.join(attrs))
220
+
221
+
222
+ def single_item(s: Any) -> Any:
223
+ """Return only item if ``s`` is a list, otherwise return ``s``."""
224
+ if isinstance(s, list):
225
+ if len(s) != 1:
226
+ raise ValueError('list must only contain a singleitem')
227
+ return s[0]
228
+ return s
229
+
230
+
231
+ def stringify(s: JSON) -> str:
232
+ """Convert list of strings to single string."""
233
+ if isinstance(s, (tuple, list)):
234
+ if len(s) > 1:
235
+ raise ValueError('list contains more than one item')
236
+ return s[0]
237
+ if isinstance(s, dict):
238
+ raise TypeError('only strings and lists are valid arguments')
239
+ return s
240
+
241
+
242
+ def listify(s: JSON) -> List[str]:
243
+ """Convert string to list of strings."""
244
+ if isinstance(s, (tuple, list)):
245
+ return list(s)
246
+ if isinstance(s, dict):
247
+ raise TypeError('only strings and lists are valid arguments')
248
+ return [s]
249
+
250
+
251
+ def listify_obj(s: JSON) -> List[JSONObj]:
252
+ """Convert object to list of objects."""
253
+ if isinstance(s, (tuple, list)):
254
+ for item in s:
255
+ if not isinstance(item, dict):
256
+ raise TypeError('only dicts and lists of dicts are valid parameters')
257
+ return list(s) # type: ignore
258
+ if not isinstance(s, dict):
259
+ raise TypeError('only dicts and lists of dicts are valid parameters')
260
+ return [s]
261
+
262
+
263
+ def _upper_match(m: Any) -> str:
264
+ """Upper-case the first match group."""
265
+ return m.group(1).upper()
266
+
267
+
268
+ def snake_to_camel(s: Optional[str], cap_first: bool = False) -> Optional[str]:
269
+ """Convert snake-case to camel-case."""
270
+ if s is None:
271
+ return None
272
+ out = re.sub(r'_[A-Za-z]', _upper_match, s.lower())
273
+ if cap_first and out:
274
+ return out[0].upper() + out[1:]
275
+ return out
276
+
277
+
278
+ def camel_to_snake(s: Optional[str]) -> Optional[str]:
279
+ """Convert camel-case to snake-case."""
280
+ if s is None:
281
+ return None
282
+ out = re.sub(r'([A-Z]+)', r'_\1', s).lower()
283
+ if out and out[0] == '_':
284
+ return out[1:]
285
+ return out
286
+
287
+
288
+ def snake_to_camel_dict(
289
+ s: Optional[Mapping[str, Any]],
290
+ cap_first: bool = False,
291
+ ) -> Optional[Dict[str, Any]]:
292
+ """Convert snake-case keys to camel-case keys."""
293
+ if s is None:
294
+ return None
295
+ out = {}
296
+ for k, v in s.items():
297
+ if isinstance(s, Mapping):
298
+ out[str(snake_to_camel(k))] = snake_to_camel_dict(v, cap_first=cap_first)
299
+ else:
300
+ out[str(snake_to_camel(k))] = v
301
+ return out
302
+
303
+
304
+ def camel_to_snake_dict(s: Optional[Mapping[str, Any]]) -> Optional[Dict[str, Any]]:
305
+ """Convert camel-case keys to snake-case keys."""
306
+ if s is None:
307
+ return None
308
+ out = {}
309
+ for k, v in s.items():
310
+ if isinstance(s, Mapping):
311
+ out[str(camel_to_snake(k))] = camel_to_snake_dict(v)
312
+ else:
313
+ out[str(camel_to_snake(k))] = v
314
+ return out