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,8 +1,8 @@
1
1
  #!/usr/bin/env python
2
2
  """Data value conversion utilities."""
3
3
  import datetime
4
+ import re
4
5
  from base64 import b64decode
5
- from datetime import timezone
6
6
  from decimal import Decimal
7
7
  from json import loads as json_loads
8
8
  from typing import Any
@@ -13,160 +13,246 @@ from typing import Optional
13
13
  from typing import Set
14
14
  from typing import Union
15
15
 
16
+ try:
17
+ import shapely.wkt
18
+ has_shapely = True
19
+ except ImportError:
20
+ has_shapely = False
21
+
22
+ try:
23
+ import pygeos
24
+ has_pygeos = True
25
+ except ImportError:
26
+ has_pygeos = False
27
+
28
+
29
+ # Cache fromisoformat methods if they exist
30
+ _dt_datetime_fromisoformat = None
31
+ if hasattr(datetime.datetime, 'fromisoformat'):
32
+ _dt_datetime_fromisoformat = datetime.datetime.fromisoformat # type: ignore
33
+ _dt_time_fromisoformat = None
34
+ if hasattr(datetime.time, 'fromisoformat'):
35
+ _dt_time_fromisoformat = datetime.time.fromisoformat # type: ignore
36
+ _dt_date_fromisoformat = None
37
+ if hasattr(datetime.date, 'fromisoformat'):
38
+ _dt_date_fromisoformat = datetime.date.fromisoformat # type: ignore
39
+
40
+
41
+ def _convert_second_fraction(s: str) -> int:
42
+ if not s:
43
+ return 0
44
+ # Pad zeros to ensure the fraction length in microseconds
45
+ s = s.ljust(6, '0')
46
+ return int(s[:6])
47
+
48
+
49
+ DATETIME_RE = re.compile(
50
+ r'(\d{1,4})-(\d{1,2})-(\d{1,2})[T ](\d{1,2}):(\d{1,2}):(\d{1,2})(?:.(\d{1,6}))?',
51
+ )
52
+
53
+ ZERO_DATETIMES = set([
54
+ '0000-00-00 00:00:00',
55
+ '0000-00-00 00:00:00.000',
56
+ '0000-00-00 00:00:00.000000',
57
+ '0000-00-00T00:00:00',
58
+ '0000-00-00T00:00:00.000',
59
+ '0000-00-00T00:00:00.000000',
60
+ ])
61
+ ZERO_DATES = set([
62
+ '0000-00-00',
63
+ ])
64
+
65
+
66
+ def datetime_fromisoformat(
67
+ obj: Union[str, bytes, bytearray],
68
+ ) -> Union[datetime.datetime, str, None]:
69
+ """Returns a DATETIME or TIMESTAMP column value as a datetime object:
70
+
71
+ >>> datetime_fromisoformat('2007-02-25 23:06:20')
72
+ datetime.datetime(2007, 2, 25, 23, 6, 20)
73
+ >>> datetime_fromisoformat('2007-02-25T23:06:20')
74
+ datetime.datetime(2007, 2, 25, 23, 6, 20)
75
+
76
+ Illegal values are returned as str or None:
77
+
78
+ >>> datetime_fromisoformat('2007-02-31T23:06:20')
79
+ '2007-02-31T23:06:20'
80
+ >>> datetime_fromisoformat('0000-00-00 00:00:00')
81
+ None
16
82
 
17
- def _parse_isoformat_date(dtstr: str) -> List[Any]:
18
- # It is assumed that this function will only be called with a
19
- # string of length exactly 10, and (though this is not used) ASCII-only
20
- year = int(dtstr[0:4])
21
- if dtstr[4] != '-':
22
- raise ValueError('Invalid date separator: %s' % dtstr[4])
23
-
24
- month = int(dtstr[5:7])
25
-
26
- if dtstr[7] != '-':
27
- raise ValueError('Invalid date separator')
28
-
29
- day = int(dtstr[8:10])
30
-
31
- return [year, month, day]
32
-
33
-
34
- def _parse_hh_mm_ss_ff(tstr: str) -> List[Any]:
35
- # Parses things of the form HH[:MM[:SS[.fff[fff]]]]
36
- len_str = len(tstr)
83
+ """
84
+ if isinstance(obj, (bytes, bytearray)):
85
+ obj = obj.decode('ascii')
37
86
 
38
- time_comps = [0, 0, 0, 0]
39
- pos = 0
40
- for comp in range(0, 3):
41
- if (len_str - pos) < 2:
42
- raise ValueError('Incomplete time component')
87
+ if obj in ZERO_DATETIMES:
88
+ return None
43
89
 
44
- time_comps[comp] = int(tstr[pos:pos+2])
90
+ # Use datetime methods if possible
91
+ if _dt_datetime_fromisoformat is not None:
92
+ try:
93
+ if ' ' in obj or 'T' in obj:
94
+ return _dt_datetime_fromisoformat(obj)
95
+ if _dt_date_fromisoformat is not None:
96
+ date = _dt_date_fromisoformat(obj)
97
+ return datetime.datetime(date.year, date.month, date.day)
98
+ except ValueError:
99
+ return obj
45
100
 
46
- pos += 2
47
- next_char = tstr[pos:pos+1]
101
+ m = DATETIME_RE.match(obj)
102
+ if not m:
103
+ mdate = date_fromisoformat(obj)
104
+ if type(mdate) is str:
105
+ return mdate
106
+ return datetime.datetime(mdate.year, mdate.month, mdate.day) # type: ignore
48
107
 
49
- if not next_char or comp >= 2:
50
- break
108
+ try:
109
+ groups = list(m.groups())
110
+ groups[-1] = _convert_second_fraction(groups[-1])
111
+ return datetime.datetime(*[int(x) for x in groups]) # type: ignore
112
+ except ValueError:
113
+ mdate = date_fromisoformat(obj)
114
+ if type(mdate) is str:
115
+ return mdate
116
+ return datetime.datetime(mdate.year, mdate.month, mdate.day) # type: ignore
51
117
 
52
- if next_char != ':':
53
- raise ValueError('Invalid time separator: %c' % next_char)
54
118
 
55
- pos += 1
119
+ TIMEDELTA_RE = re.compile(r'(-)?(\d{1,3}):(\d{1,2}):(\d{1,2})(?:.(\d{1,6}))?')
56
120
 
57
- if pos < len_str:
58
- if tstr[pos] != '.':
59
- raise ValueError('Invalid microsecond component')
60
- else:
61
- pos += 1
62
121
 
63
- len_remainder = len_str - pos
64
- if len_remainder not in (3, 6):
65
- raise ValueError('Invalid microsecond component')
122
+ def timedelta_fromisoformat(
123
+ obj: Union[str, bytes, bytearray],
124
+ ) -> Union[datetime.timedelta, str, None]:
125
+ """Returns a TIME column as a timedelta object:
66
126
 
67
- time_comps[3] = int(tstr[pos:])
68
- if len_remainder == 3:
69
- time_comps[3] *= 1000
127
+ >>> timedelta_fromisoformat('25:06:17')
128
+ datetime.timedelta(days=1, seconds=3977)
129
+ >>> timedelta_fromisoformat('-25:06:17')
130
+ datetime.timedelta(days=-2, seconds=82423)
70
131
 
71
- return time_comps
132
+ Illegal values are returned as string:
72
133
 
134
+ >>> timedelta_fromisoformat('random crap')
135
+ 'random crap'
73
136
 
74
- def _parse_isoformat_time(tstr: str) -> List[Any]:
75
- # Format supported is HH[:MM[:SS[.fff[fff]]]][+HH:MM[:SS[.ffffff]]]
76
- len_str = len(tstr)
77
- if len_str < 2:
78
- raise ValueError('Isoformat time too short')
137
+ Note that MySQL always returns TIME columns as (+|-)HH:MM:SS, but
138
+ can accept values as (+|-)DD HH:MM:SS. The latter format will not
139
+ be parsed correctly by this function.
140
+ """
141
+ if isinstance(obj, (bytes, bytearray)):
142
+ obj = obj.decode('ascii')
79
143
 
80
- # This is equivalent to re.search('[+-]', tstr), but faster
81
- tz_pos = (tstr.find('-') + 1 or tstr.find('+') + 1)
82
- timestr = tstr[:tz_pos-1] if tz_pos > 0 else tstr
144
+ m = TIMEDELTA_RE.match(obj)
145
+ if not m:
146
+ return obj
83
147
 
84
- time_comps = _parse_hh_mm_ss_ff(timestr)
148
+ try:
149
+ groups = list(m.groups())
150
+ groups[-1] = _convert_second_fraction(groups[-1])
151
+ negate = -1 if groups[0] else 1
152
+ hours, minutes, seconds, microseconds = groups[1:]
153
+
154
+ tdelta = (
155
+ datetime.timedelta(
156
+ hours=int(hours),
157
+ minutes=int(minutes),
158
+ seconds=int(seconds),
159
+ microseconds=int(microseconds),
160
+ )
161
+ * negate
162
+ )
163
+ return tdelta
164
+ except ValueError:
165
+ return obj
85
166
 
86
- tzi = None
87
- if tz_pos > 0:
88
- tzstr = tstr[tz_pos:]
89
167
 
90
- # Valid time zone strings are:
91
- # HH:MM len: 5
92
- # HH:MM:SS len: 8
93
- # HH:MM:SS.ffffff len: 15
168
+ TIME_RE = re.compile(r'(\d{1,2}):(\d{1,2}):(\d{1,2})(?:.(\d{1,6}))?')
94
169
 
95
- if len(tzstr) not in (5, 8, 15):
96
- raise ValueError('Malformed time zone string')
97
170
 
98
- tz_comps = _parse_hh_mm_ss_ff(tzstr)
99
- if all(x == 0 for x in tz_comps):
100
- tzi = timezone.utc
101
- else:
102
- tzsign = -1 if tstr[tz_pos - 1] == '-' else 1
171
+ def time_fromisoformat(
172
+ obj: Union[str, bytes, bytearray],
173
+ ) -> Union[datetime.time, str, None]:
174
+ """Returns a TIME column as a time object:
103
175
 
104
- td = datetime.timedelta(
105
- hours=tz_comps[0], minutes=tz_comps[1],
106
- seconds=tz_comps[2], microseconds=tz_comps[3],
107
- )
176
+ >>> time_fromisoformat('15:06:17')
177
+ datetime.time(15, 6, 17)
108
178
 
109
- tzi = timezone(tzsign * td)
179
+ Illegal values are returned as str:
110
180
 
111
- time_comps.append(tzi)
181
+ >>> time_fromisoformat('-25:06:17')
182
+ '-25:06:17'
183
+ >>> time_fromisoformat('random crap')
184
+ 'random crap'
112
185
 
113
- return time_comps
186
+ Note that MySQL always returns TIME columns as (+|-)HH:MM:SS, but
187
+ can accept values as (+|-)DD HH:MM:SS. The latter format will not
188
+ be parsed correctly by this function.
114
189
 
190
+ Also note that MySQL's TIME column corresponds more closely to
191
+ Python's timedelta and not time. However if you want TIME columns
192
+ to be treated as time-of-day and not a time offset, then you can
193
+ use set this function as the converter for FIELD_TYPE.TIME.
194
+ """
195
+ if isinstance(obj, (bytes, bytearray)):
196
+ obj = obj.decode('ascii')
115
197
 
116
- def datetime_fromisoformat(date_string: str) -> datetime.datetime:
117
- """Construct a datetime from the output of datetime.isoformat()."""
118
- if not isinstance(date_string, str):
119
- raise TypeError('fromisoformat: argument must be str')
198
+ # Use datetime methods if possible
199
+ if _dt_time_fromisoformat is not None:
200
+ try:
201
+ return _dt_time_fromisoformat(obj)
202
+ except ValueError:
203
+ return obj
120
204
 
121
- # Split this at the separator
122
- dstr = date_string[0:10]
123
- tstr = date_string[11:]
205
+ m = TIME_RE.match(obj)
206
+ if not m:
207
+ return obj
124
208
 
125
209
  try:
126
- date_components = _parse_isoformat_date(dstr)
210
+ groups = list(m.groups())
211
+ groups[-1] = _convert_second_fraction(groups[-1])
212
+ hours, minutes, seconds, microseconds = groups
213
+ return datetime.time(
214
+ hour=int(hours),
215
+ minute=int(minutes),
216
+ second=int(seconds),
217
+ microsecond=int(microseconds),
218
+ )
127
219
  except ValueError:
128
- raise ValueError(f'Invalid isoformat string: {date_string!r}')
220
+ return obj
129
221
 
130
- if tstr:
131
- try:
132
- time_components = _parse_isoformat_time(tstr)
133
- except ValueError:
134
- raise ValueError(f'Invalid isoformat string: {date_string!r}')
135
- else:
136
- time_components = [0, 0, 0, 0, None]
137
222
 
138
- return datetime.datetime(*(date_components + time_components))
223
+ def date_fromisoformat(
224
+ obj: Union[str, bytes, bytearray],
225
+ ) -> Union[datetime.date, str, None]:
226
+ """Returns a DATE column as a date object:
139
227
 
228
+ >>> date_fromisoformat('2007-02-26')
229
+ datetime.date(2007, 2, 26)
140
230
 
141
- def date_fromisoformat(date_string: str) -> datetime.date:
142
- """Construct a date from the output of date.isoformat()."""
143
- if not isinstance(date_string, str):
144
- raise TypeError('fromisoformat: argument must be str')
231
+ Illegal values are returned as str or None:
145
232
 
146
- try:
147
- assert len(date_string) == 10
148
- return datetime.date(*_parse_isoformat_date(date_string))
149
- except Exception:
150
- raise ValueError(f'Invalid isoformat string: {date_string!r}')
233
+ >>> date_fromisoformat('2007-02-31')
234
+ '2007-02-31'
235
+ >>> date_fromisoformat('0000-00-00')
236
+ None
151
237
 
238
+ """
239
+ if isinstance(obj, (bytes, bytearray)):
240
+ obj = obj.decode('ascii')
152
241
 
153
- def time_fromisoformat(time_string: str) -> datetime.time:
154
- """Construct a time from the output of isoformat()."""
155
- if not isinstance(time_string, str):
156
- raise TypeError('fromisoformat: argument must be str')
157
-
158
- try:
159
- return datetime.time(*_parse_isoformat_time(time_string))
160
- except Exception:
161
- raise ValueError(f'Invalid isoformat string: {time_string!r}')
242
+ if obj in ZERO_DATES:
243
+ return None
162
244
 
245
+ # Use datetime methods if possible
246
+ if _dt_date_fromisoformat is not None:
247
+ try:
248
+ return _dt_date_fromisoformat(obj)
249
+ except ValueError:
250
+ return obj
163
251
 
164
- # datetime_fromisoformat = datetime.datetime.fromisoformat
165
- # time_fromisoformat = datetime.time.fromisoformat
166
- # date_fromisoformat = datetime.date.fromisoformat
167
- datetime_min = datetime.datetime.min
168
- date_min = datetime.date.min
169
- datetime_combine = datetime.datetime.combine
252
+ try:
253
+ return datetime.date(*[int(x) for x in obj.split('-', 2)])
254
+ except ValueError:
255
+ return obj
170
256
 
171
257
 
172
258
  def identity(x: Any) -> Optional[Any]:
@@ -264,7 +350,7 @@ def decimal_or_none(x: Any) -> Optional[Decimal]:
264
350
  return Decimal(x)
265
351
 
266
352
 
267
- def date_or_none(x: Optional[str]) -> Optional[datetime.date]:
353
+ def date_or_none(x: Optional[str]) -> Optional[Union[datetime.date, str]]:
268
354
  """
269
355
  Convert value to a date.
270
356
 
@@ -283,13 +369,10 @@ def date_or_none(x: Optional[str]) -> Optional[datetime.date]:
283
369
  """
284
370
  if x is None:
285
371
  return None
286
- try:
287
- return date_fromisoformat(x)
288
- except ValueError:
289
- return None
372
+ return date_fromisoformat(x)
290
373
 
291
374
 
292
- def time_or_none(x: Optional[str]) -> Optional[datetime.timedelta]:
375
+ def timedelta_or_none(x: Optional[str]) -> Optional[Union[datetime.timedelta, str]]:
293
376
  """
294
377
  Convert value to a timedelta.
295
378
 
@@ -308,13 +391,32 @@ def time_or_none(x: Optional[str]) -> Optional[datetime.timedelta]:
308
391
  """
309
392
  if x is None:
310
393
  return None
311
- try:
312
- return datetime_combine(date_min, time_fromisoformat(x)) - datetime_min
313
- except ValueError:
394
+ return timedelta_fromisoformat(x)
395
+
396
+
397
+ def time_or_none(x: Optional[str]) -> Optional[Union[datetime.time, str]]:
398
+ """
399
+ Convert value to a time.
400
+
401
+ Parameters
402
+ ----------
403
+ x : Any
404
+ Arbitrary value
405
+
406
+ Returns
407
+ -------
408
+ datetime.time
409
+ If value can be cast to a time
410
+ None
411
+ If input value is None
412
+
413
+ """
414
+ if x is None:
314
415
  return None
416
+ return time_fromisoformat(x)
315
417
 
316
418
 
317
- def datetime_or_none(x: Optional[str]) -> Optional[datetime.datetime]:
419
+ def datetime_or_none(x: Optional[str]) -> Optional[Union[datetime.datetime, str]]:
318
420
  """
319
421
  Convert value to a datetime.
320
422
 
@@ -333,10 +435,7 @@ def datetime_or_none(x: Optional[str]) -> Optional[datetime.datetime]:
333
435
  """
334
436
  if x is None:
335
437
  return None
336
- try:
337
- return datetime_fromisoformat(x)
338
- except ValueError:
339
- return None
438
+ return datetime_fromisoformat(x)
340
439
 
341
440
 
342
441
  def none(x: Any) -> None:
@@ -413,14 +512,18 @@ def geometry_or_none(x: Optional[str]) -> Optional[Any]:
413
512
 
414
513
  Returns
415
514
  -------
416
- ???
515
+ shapely object or pygeos object or str
417
516
  If value is valid geometry value
418
517
  None
419
- If input value is None
518
+ If input value is None or empty
420
519
 
421
520
  """
422
- if x is None:
521
+ if x is None or not x:
423
522
  return None
523
+ if has_shapely:
524
+ return shapely.wkt.loads(x)
525
+ if has_pygeos:
526
+ return pygeos.io.from_wkt(x)
424
527
  return x
425
528
 
426
529
 
@@ -437,7 +540,7 @@ converters: Dict[int, Callable[..., Any]] = {
437
540
  8: int_or_none,
438
541
  9: int_or_none,
439
542
  10: date_or_none,
440
- 11: time_or_none,
543
+ 11: timedelta_or_none,
441
544
  12: datetime_or_none,
442
545
  13: int_or_none,
443
546
  14: date_or_none,
@@ -3,7 +3,11 @@
3
3
  from typing import Optional
4
4
 
5
5
 
6
- class Error(Exception):
6
+ class MySQLError(Exception):
7
+ """All MySQL-related exceptions."""
8
+
9
+
10
+ class Error(MySQLError):
7
11
  """
8
12
  Generic database exception.
9
13
 
@@ -52,7 +56,7 @@ class Error(Exception):
52
56
  return self.errmsg
53
57
 
54
58
 
55
- class Warning(Exception):
59
+ class Warning(Warning, MySQLError): # type: ignore
56
60
  """Exception for important warnings like data truncations, etc."""
57
61
 
58
62
 
@@ -90,3 +94,27 @@ class NotSupportedError(DatabaseError):
90
94
 
91
95
  class ManagementError(Error):
92
96
  """Exception for errors in the management API."""
97
+
98
+ def __init__(
99
+ self, errno: Optional[int] = None, msg: Optional[str] = None,
100
+ response: Optional[str] = None,
101
+ ):
102
+ self.errno = errno
103
+ self.errmsg = msg
104
+ self.response = response
105
+ super(Exception, self).__init__(errno, msg)
106
+
107
+ def __str__(self) -> str:
108
+ """Return string representation."""
109
+ prefix = []
110
+ if self.errno is not None:
111
+ prefix.append(f'{self.errno}')
112
+ if self.response is not None:
113
+ prefix.append(f'({self.response})')
114
+ if prefix and self.errmsg:
115
+ return ' '.join(prefix) + ': ' + self.errmsg
116
+ elif prefix:
117
+ return ' '.join(prefix)
118
+ elif self.errmsg:
119
+ return f'{self.errmsg}'
120
+ return 'Unknown error'
@@ -0,0 +1 @@
1
+ from .decorator import udf # noqa: F401
@@ -0,0 +1,142 @@
1
+ import functools
2
+ from typing import Any
3
+ from typing import Callable
4
+ from typing import Dict
5
+ from typing import List
6
+ from typing import Optional
7
+ from typing import Union
8
+
9
+ from .dtypes import DataType
10
+
11
+
12
+ def listify(x: Any) -> List[Any]:
13
+ """Make sure sure value is a list."""
14
+ if x is None:
15
+ return []
16
+ if isinstance(x, (list, tuple, set)):
17
+ return list(x)
18
+ return [x]
19
+
20
+
21
+ def udf(
22
+ func: Optional[Callable[..., Any]] = None,
23
+ *,
24
+ name: Optional[str] = None,
25
+ args: Optional[Union[DataType, List[DataType], Dict[str, DataType]]] = None,
26
+ returns: Optional[str] = None,
27
+ data_format: Optional[str] = None,
28
+ include_masks: bool = False,
29
+ ) -> Callable[..., Any]:
30
+ """
31
+ Apply attributes to a UDF.
32
+
33
+ Parameters
34
+ ----------
35
+ func : callable, optional
36
+ The UDF to apply parameters to
37
+ name : str, optional
38
+ The name to use for the UDF in the database
39
+ args : str | Callable | List[str | Callable] | Dict[str, str | Callable], optional
40
+ Specifies the data types of the function arguments. Typically,
41
+ the function data types are derived from the function parameter
42
+ annotations. These annotations can be overridden. If the function
43
+ takes a single type for all parameters, `args` can be set to a
44
+ SQL string describing all parameters. If the function takes more
45
+ than one parameter and all of the parameters are being manually
46
+ defined, a list of SQL strings may be used (one for each parameter).
47
+ A dictionary of SQL strings may be used to specify a parameter type
48
+ for a subset of parameters; the keys are the names of the
49
+ function parameters. Callables may also be used for datatypes. This
50
+ is primarily for using the functions in the ``dtypes`` module that
51
+ are associated with SQL types with all default options (e.g., ``dt.FLOAT``).
52
+ returns : str, optional
53
+ Specifies the return data type of the function. If not specified,
54
+ the type annotation from the function is used.
55
+ data_format : str, optional
56
+ The data format of each parameter: python, pandas, arrow, polars
57
+ include_masks : bool, optional
58
+ Should boolean masks be included with each input parameter to indicate
59
+ which elements are NULL? This is only used when a input parameters are
60
+ configured to a vector type (numpy, pandas, polars, arrow).
61
+
62
+ Returns
63
+ -------
64
+ Callable
65
+
66
+ """
67
+ if args is None:
68
+ pass
69
+ elif isinstance(args, (list, tuple)):
70
+ args = list(args)
71
+ for i, item in enumerate(args):
72
+ if callable(item):
73
+ args[i] = item()
74
+ for item in args:
75
+ if not isinstance(item, str):
76
+ raise TypeError(f'unrecognized type for parameter: {item}')
77
+ elif isinstance(args, dict):
78
+ args = dict(args)
79
+ for k, v in list(args.items()):
80
+ if callable(v):
81
+ args[k] = v()
82
+ for item in args.values():
83
+ if not isinstance(item, str):
84
+ raise TypeError(f'unrecognized type for parameter: {item}')
85
+ elif callable(args):
86
+ args = args()
87
+ elif isinstance(args, str):
88
+ args = args
89
+ else:
90
+ raise TypeError(f'unrecognized data type for args: {args}')
91
+
92
+ if returns is None:
93
+ pass
94
+ elif callable(returns):
95
+ returns = returns()
96
+ elif isinstance(returns, str):
97
+ returns = returns
98
+ else:
99
+ raise TypeError(f'unrecognized return type: {returns}')
100
+
101
+ if returns is not None and not isinstance(returns, str):
102
+ raise TypeError(f'unrecognized return type: {returns}')
103
+
104
+ if include_masks and data_format == 'python':
105
+ raise RuntimeError(
106
+ 'include_masks is only valid when using '
107
+ 'vectors for input parameters',
108
+ )
109
+
110
+ _singlestoredb_attrs = { # type: ignore
111
+ k: v for k, v in dict(
112
+ name=name,
113
+ args=args,
114
+ returns=returns,
115
+ data_format=data_format,
116
+ include_masks=include_masks,
117
+ ).items() if v is not None
118
+ }
119
+
120
+ # No func was specified, this is an uncalled decorator that will get
121
+ # called later, so the wrapper much be created with the func passed
122
+ # in at that time.
123
+ if func is None:
124
+ def decorate(func: Callable[..., Any]) -> Callable[..., Any]:
125
+ def wrapper(*args: Any, **kwargs: Any) -> Callable[..., Any]:
126
+ return func(*args, **kwargs) # type: ignore
127
+ wrapper._singlestoredb_attrs = _singlestoredb_attrs # type: ignore
128
+ return functools.wraps(func)(wrapper)
129
+ return decorate
130
+
131
+ def wrapper(*args: Any, **kwargs: Any) -> Callable[..., Any]:
132
+ return func(*args, **kwargs) # type: ignore
133
+
134
+ wrapper._singlestoredb_attrs = _singlestoredb_attrs # type: ignore
135
+
136
+ return functools.wraps(func)(wrapper)
137
+
138
+
139
+ udf.pandas = functools.partial(udf, data_format='pandas') # type: ignore
140
+ udf.polars = functools.partial(udf, data_format='polars') # type: ignore
141
+ udf.arrow = functools.partial(udf, data_format='arrow') # type: ignore
142
+ udf.numpy = functools.partial(udf, data_format='numpy') # type: ignore