singlestoredb 1.14.1__cp38-abi3-win_amd64.whl → 1.15.0__cp38-abi3-win_amd64.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 (30) hide show
  1. _singlestoredb_accel.pyd +0 -0
  2. singlestoredb/__init__.py +14 -10
  3. singlestoredb/apps/_python_udfs.py +3 -3
  4. singlestoredb/config.py +5 -0
  5. singlestoredb/functions/decorator.py +32 -13
  6. singlestoredb/functions/ext/asgi.py +287 -27
  7. singlestoredb/functions/ext/timer.py +98 -0
  8. singlestoredb/functions/typing/numpy.py +20 -0
  9. singlestoredb/functions/typing/pandas.py +2 -0
  10. singlestoredb/functions/typing/polars.py +2 -0
  11. singlestoredb/functions/typing/pyarrow.py +2 -0
  12. singlestoredb/fusion/handler.py +17 -4
  13. singlestoredb/magics/run_personal.py +82 -1
  14. singlestoredb/magics/run_shared.py +82 -1
  15. singlestoredb/management/__init__.py +1 -0
  16. singlestoredb/management/export.py +1 -1
  17. singlestoredb/management/region.py +92 -0
  18. singlestoredb/management/workspace.py +180 -1
  19. singlestoredb/tests/ext_funcs/__init__.py +94 -55
  20. singlestoredb/tests/test.sql +22 -0
  21. singlestoredb/tests/test_ext_func.py +90 -0
  22. singlestoredb/tests/test_fusion.py +4 -1
  23. singlestoredb/tests/test_management.py +253 -20
  24. {singlestoredb-1.14.1.dist-info → singlestoredb-1.15.0.dist-info}/METADATA +3 -2
  25. {singlestoredb-1.14.1.dist-info → singlestoredb-1.15.0.dist-info}/RECORD +30 -25
  26. /singlestoredb/functions/{typing.py → typing/__init__.py} +0 -0
  27. {singlestoredb-1.14.1.dist-info → singlestoredb-1.15.0.dist-info}/LICENSE +0 -0
  28. {singlestoredb-1.14.1.dist-info → singlestoredb-1.15.0.dist-info}/WHEEL +0 -0
  29. {singlestoredb-1.14.1.dist-info → singlestoredb-1.15.0.dist-info}/entry_points.txt +0 -0
  30. {singlestoredb-1.14.1.dist-info → singlestoredb-1.15.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,98 @@
1
+ import json
2
+ import time
3
+ from typing import Any
4
+ from typing import Dict
5
+ from typing import Optional
6
+
7
+ from . import utils
8
+
9
+ logger = utils.get_logger('singlestoredb.functions.ext.metrics')
10
+
11
+
12
+ class RoundedFloatEncoder(json.JSONEncoder):
13
+
14
+ def encode(self, obj: Any) -> str:
15
+ if isinstance(obj, dict):
16
+ return '{' + ', '.join(
17
+ f'"{k}": {self._format_value(v)}'
18
+ for k, v in obj.items()
19
+ ) + '}'
20
+ return super().encode(obj)
21
+
22
+ def _format_value(self, value: Any) -> str:
23
+ if isinstance(value, float):
24
+ return f'{value:.2f}'
25
+ return json.dumps(value)
26
+
27
+
28
+ class Timer:
29
+ """
30
+ Timer context manager that supports nested timing using a stack.
31
+
32
+ Example
33
+ -------
34
+ timer = Timer()
35
+
36
+ with timer('total'):
37
+ with timer('receive_data'):
38
+ time.sleep(0.1)
39
+ with timer('parse_input'):
40
+ time.sleep(0.2)
41
+ with timer('call_function'):
42
+ with timer('inner_operation'):
43
+ time.sleep(0.05)
44
+ time.sleep(0.3)
45
+
46
+ print(timer.metrics)
47
+ # {'receive_data': 0.1, 'parse_input': 0.2, 'inner_operation': 0.05,
48
+ # 'call_function': 0.35, 'total': 0.65}
49
+
50
+ """
51
+
52
+ def __init__(self, **kwargs: Any) -> None:
53
+ self.metadata: Dict[str, Any] = kwargs
54
+ self.metrics: Dict[str, float] = dict()
55
+ self.entries: Dict[str, float] = dict()
56
+ self._current_key: Optional[str] = None
57
+ self.start_time = time.perf_counter()
58
+
59
+ def __call__(self, key: str) -> 'Timer':
60
+ self._current_key = key
61
+ return self
62
+
63
+ def __enter__(self) -> 'Timer':
64
+ if self._current_key is None:
65
+ raise ValueError(
66
+ "No key specified. Use timer('key_name') as context manager.",
67
+ )
68
+ self.entries[self._current_key] = time.perf_counter()
69
+ return self
70
+
71
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
72
+ key = self._current_key
73
+ if key and key in self.entries:
74
+ start = self.entries.pop(key)
75
+ elapsed = time.perf_counter() - start
76
+ self.metrics[key] = elapsed
77
+ self._current_key = None
78
+
79
+ async def __aenter__(self) -> 'Timer':
80
+ return self.__enter__()
81
+
82
+ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
83
+ self.__exit__(exc_type, exc_val, exc_tb)
84
+
85
+ def reset(self) -> None:
86
+ self.metrics.clear()
87
+ self.entries.clear()
88
+ self._current_key = None
89
+
90
+ def finish(self) -> None:
91
+ """Finish the current timing context and store the elapsed time."""
92
+ self.metrics['total'] = time.perf_counter() - self.start_time
93
+ self.log_metrics()
94
+
95
+ def log_metrics(self) -> None:
96
+ if self.metadata.get('function'):
97
+ result = dict(type='function_metrics', **self.metadata, **self.metrics)
98
+ logger.info(json.dumps(result, cls=RoundedFloatEncoder))
@@ -0,0 +1,20 @@
1
+ import numpy as np
2
+ import numpy.typing as npt
3
+
4
+ NDArray = npt.NDArray
5
+
6
+ StringArray = StrArray = npt.NDArray[np.str_]
7
+ BytesArray = npt.NDArray[np.bytes_]
8
+ Float32Array = FloatArray = npt.NDArray[np.float32]
9
+ Float64Array = DoubleArray = npt.NDArray[np.float64]
10
+ IntArray = npt.NDArray[np.int_]
11
+ Int8Array = npt.NDArray[np.int8]
12
+ Int16Array = npt.NDArray[np.int16]
13
+ Int32Array = npt.NDArray[np.int32]
14
+ Int64Array = npt.NDArray[np.int64]
15
+ UInt8Array = npt.NDArray[np.uint8]
16
+ UInt16Array = npt.NDArray[np.uint16]
17
+ UInt32Array = npt.NDArray[np.uint32]
18
+ UInt64Array = npt.NDArray[np.uint64]
19
+ DateTimeArray = npt.NDArray[np.datetime64]
20
+ TimeDeltaArray = npt.NDArray[np.timedelta64]
@@ -0,0 +1,2 @@
1
+ from pandas import DataFrame # noqa: F401
2
+ from pandas import Series # noqa: F401
@@ -0,0 +1,2 @@
1
+ from polars import DataFrame # noqa: F401
2
+ from polars import Series # noqa: F401
@@ -0,0 +1,2 @@
1
+ from pyarrow import Array # noqa: F401
2
+ from pyarrow import Table # noqa: F401
@@ -33,7 +33,7 @@ CORE_GRAMMAR = r'''
33
33
  close_paren = ws* ")" ws*
34
34
  open_repeats = ws* ~r"[\(\[\{]" ws*
35
35
  close_repeats = ws* ~r"[\)\]\}]" ws*
36
- select = ~r"SELECT"i ws+ ~r".+" ws*
36
+ statement = ~r"[\s\S]*" ws*
37
37
  table = ~r"(?:([A-Za-z0-9_\-]+)|`([^\`]+)`)(?:\.(?:([A-Za-z0-9_\-]+)|`([^\`]+)`))?" ws*
38
38
  column = ~r"(?:([A-Za-z0-9_\-]+)|`([^\`]+)`)(?:\.(?:([A-Za-z0-9_\-]+)|`([^\`]+)`))?" ws*
39
39
  link_name = ~r"(?:([A-Za-z0-9_\-]+)|`([^\`]+)`)(?:\.(?:([A-Za-z0-9_\-]+)|`([^\`]+)`))?" ws*
@@ -77,6 +77,7 @@ BUILTINS = {
77
77
  '<file-type>': r'''
78
78
  file_type = { FILE | FOLDER }
79
79
  ''',
80
+ '<statement>': '',
80
81
  }
81
82
 
82
83
  BUILTIN_DEFAULTS = { # type: ignore
@@ -627,6 +628,18 @@ class SQLHandler(NodeVisitor):
627
628
  cls.compile()
628
629
  registry.register_handler(cls, overwrite=overwrite)
629
630
 
631
+ def create_result(self) -> result.FusionSQLResult:
632
+ """
633
+ Create a new result object.
634
+
635
+ Returns
636
+ -------
637
+ FusionSQLResult
638
+ A new result object for this handler
639
+
640
+ """
641
+ return result.FusionSQLResult()
642
+
630
643
  def execute(self, sql: str) -> result.FusionSQLResult:
631
644
  """
632
645
  Parse the SQL and invoke the handler method.
@@ -746,9 +759,9 @@ class SQLHandler(NodeVisitor):
746
759
  _, out, *_ = visited_children
747
760
  return out
748
761
 
749
- def visit_select(self, node: Node, visited_children: Iterable[Any]) -> Any:
750
- out = ' '.join(flatten(visited_children))
751
- return {'select': out}
762
+ def visit_statement(self, node: Node, visited_children: Iterable[Any]) -> Any:
763
+ out = ' '.join(flatten(visited_children)).strip()
764
+ return {'statement': out}
752
765
 
753
766
  def visit_order_by(self, node: Node, visited_children: Iterable[Any]) -> Any:
754
767
  """Handle ORDER BY."""
@@ -1,6 +1,8 @@
1
1
  import os
2
2
  import tempfile
3
+ from pathlib import Path
3
4
  from typing import Any
5
+ from warnings import warn
4
6
 
5
7
  from IPython.core.interactiveshell import InteractiveShell
6
8
  from IPython.core.magic import line_magic
@@ -8,6 +10,8 @@ from IPython.core.magic import Magics
8
10
  from IPython.core.magic import magics_class
9
11
  from IPython.core.magic import needs_local_scope
10
12
  from IPython.core.magic import no_var_expand
13
+ from IPython.utils.contexts import preserve_keys
14
+ from IPython.utils.syspathcontext import prepended_to_syspath
11
15
  from jinja2 import Template
12
16
 
13
17
 
@@ -53,4 +57,81 @@ class RunPersonalMagic(Magics):
53
57
  # Execute the SQL command
54
58
  self.shell.run_line_magic('sql', sql_command)
55
59
  # Run the downloaded file
56
- self.shell.run_line_magic('run', f'"{temp_file_path}"')
60
+ with preserve_keys(self.shell.user_ns, '__file__'):
61
+ self.shell.user_ns['__file__'] = temp_file_path
62
+ self.safe_execfile_ipy(temp_file_path, raise_exceptions=True)
63
+
64
+ def safe_execfile_ipy(
65
+ self,
66
+ fname: str,
67
+ shell_futures: bool = False,
68
+ raise_exceptions: bool = False,
69
+ ) -> None:
70
+ """Like safe_execfile, but for .ipy or .ipynb files with IPython syntax.
71
+
72
+ Parameters
73
+ ----------
74
+ fname : str
75
+ The name of the file to execute. The filename must have a
76
+ .ipy or .ipynb extension.
77
+ shell_futures : bool (False)
78
+ If True, the code will share future statements with the interactive
79
+ shell. It will both be affected by previous __future__ imports, and
80
+ any __future__ imports in the code will affect the shell. If False,
81
+ __future__ imports are not shared in either direction.
82
+ raise_exceptions : bool (False)
83
+ If True raise exceptions everywhere. Meant for testing.
84
+ """
85
+ fpath = Path(fname).expanduser().resolve()
86
+
87
+ # Make sure we can open the file
88
+ try:
89
+ with fpath.open('rb'):
90
+ pass
91
+ except Exception:
92
+ warn('Could not open file <%s> for safe execution.' % fpath)
93
+ return
94
+
95
+ # Find things also in current directory. This is needed to mimic the
96
+ # behavior of running a script from the system command line, where
97
+ # Python inserts the script's directory into sys.path
98
+ dname = str(fpath.parent)
99
+
100
+ def get_cells() -> Any:
101
+ """generator for sequence of code blocks to run"""
102
+ if fpath.suffix == '.ipynb':
103
+ from nbformat import read
104
+ nb = read(fpath, as_version=4)
105
+ if not nb.cells:
106
+ return
107
+ for cell in nb.cells:
108
+ if cell.cell_type == 'code':
109
+ if not cell.source.strip():
110
+ continue
111
+ if getattr(cell, 'metadata', {}).get('language', '') == 'sql':
112
+ output_redirect = getattr(
113
+ cell, 'metadata', {},
114
+ ).get('output_variable', '') or ''
115
+ if output_redirect:
116
+ output_redirect = f' {output_redirect} <<'
117
+ yield f'%%sql{output_redirect}\n{cell.source}'
118
+ else:
119
+ yield cell.source
120
+ else:
121
+ yield fpath.read_text(encoding='utf-8')
122
+
123
+ with prepended_to_syspath(dname):
124
+ try:
125
+ for cell in get_cells():
126
+ result = self.shell.run_cell(
127
+ cell, silent=True, shell_futures=shell_futures,
128
+ )
129
+ if raise_exceptions:
130
+ result.raise_error()
131
+ elif not result.success:
132
+ break
133
+ except Exception:
134
+ if raise_exceptions:
135
+ raise
136
+ self.shell.showtraceback()
137
+ warn('Unknown failure executing file: <%s>' % fpath)
@@ -1,6 +1,8 @@
1
1
  import os
2
2
  import tempfile
3
+ from pathlib import Path
3
4
  from typing import Any
5
+ from warnings import warn
4
6
 
5
7
  from IPython.core.interactiveshell import InteractiveShell
6
8
  from IPython.core.magic import line_magic
@@ -8,6 +10,8 @@ from IPython.core.magic import Magics
8
10
  from IPython.core.magic import magics_class
9
11
  from IPython.core.magic import needs_local_scope
10
12
  from IPython.core.magic import no_var_expand
13
+ from IPython.utils.contexts import preserve_keys
14
+ from IPython.utils.syspathcontext import prepended_to_syspath
11
15
  from jinja2 import Template
12
16
 
13
17
 
@@ -50,4 +54,81 @@ class RunSharedMagic(Magics):
50
54
  # Execute the SQL command
51
55
  self.shell.run_line_magic('sql', sql_command)
52
56
  # Run the downloaded file
53
- self.shell.run_line_magic('run', f'"{temp_file_path}"')
57
+ with preserve_keys(self.shell.user_ns, '__file__'):
58
+ self.shell.user_ns['__file__'] = temp_file_path
59
+ self.safe_execfile_ipy(temp_file_path, raise_exceptions=True)
60
+
61
+ def safe_execfile_ipy(
62
+ self,
63
+ fname: str,
64
+ shell_futures: bool = False,
65
+ raise_exceptions: bool = False,
66
+ ) -> None:
67
+ """Like safe_execfile, but for .ipy or .ipynb files with IPython syntax.
68
+
69
+ Parameters
70
+ ----------
71
+ fname : str
72
+ The name of the file to execute. The filename must have a
73
+ .ipy or .ipynb extension.
74
+ shell_futures : bool (False)
75
+ If True, the code will share future statements with the interactive
76
+ shell. It will both be affected by previous __future__ imports, and
77
+ any __future__ imports in the code will affect the shell. If False,
78
+ __future__ imports are not shared in either direction.
79
+ raise_exceptions : bool (False)
80
+ If True raise exceptions everywhere. Meant for testing.
81
+ """
82
+ fpath = Path(fname).expanduser().resolve()
83
+
84
+ # Make sure we can open the file
85
+ try:
86
+ with fpath.open('rb'):
87
+ pass
88
+ except Exception:
89
+ warn('Could not open file <%s> for safe execution.' % fpath)
90
+ return
91
+
92
+ # Find things also in current directory. This is needed to mimic the
93
+ # behavior of running a script from the system command line, where
94
+ # Python inserts the script's directory into sys.path
95
+ dname = str(fpath.parent)
96
+
97
+ def get_cells() -> Any:
98
+ """generator for sequence of code blocks to run"""
99
+ if fpath.suffix == '.ipynb':
100
+ from nbformat import read
101
+ nb = read(fpath, as_version=4)
102
+ if not nb.cells:
103
+ return
104
+ for cell in nb.cells:
105
+ if cell.cell_type == 'code':
106
+ if not cell.source.strip():
107
+ continue
108
+ if getattr(cell, 'metadata', {}).get('language', '') == 'sql':
109
+ output_redirect = getattr(
110
+ cell, 'metadata', {},
111
+ ).get('output_variable', '') or ''
112
+ if output_redirect:
113
+ output_redirect = f' {output_redirect} <<'
114
+ yield f'%%sql{output_redirect}\n{cell.source}'
115
+ else:
116
+ yield cell.source
117
+ else:
118
+ yield fpath.read_text(encoding='utf-8')
119
+
120
+ with prepended_to_syspath(dname):
121
+ try:
122
+ for cell in get_cells():
123
+ result = self.shell.run_cell(
124
+ cell, silent=True, shell_futures=shell_futures,
125
+ )
126
+ if raise_exceptions:
127
+ result.raise_error()
128
+ elif not result.success:
129
+ break
130
+ except Exception:
131
+ if raise_exceptions:
132
+ raise
133
+ self.shell.showtraceback()
134
+ warn('Unknown failure executing file: <%s>' % fpath)
@@ -2,6 +2,7 @@
2
2
  from .cluster import manage_cluster
3
3
  from .files import manage_files
4
4
  from .manager import get_token
5
+ from .region import manage_regions
5
6
  from .workspace import get_organization
6
7
  from .workspace import get_secret
7
8
  from .workspace import get_stage
@@ -215,7 +215,7 @@ class ExportService(object):
215
215
  msg='Export ID is not set. You must start the export first.',
216
216
  )
217
217
 
218
- self._manager._post(
218
+ self._manager._delete(
219
219
  f'workspaceGroups/{self.workspace_group.id}/egress/dropTableEgress',
220
220
  json=dict(egressID=self.export_id),
221
221
  )
@@ -4,6 +4,7 @@ from typing import Dict
4
4
  from typing import Optional
5
5
 
6
6
  from .manager import Manager
7
+ from .utils import NamedList
7
8
  from .utils import vars_to_str
8
9
 
9
10
 
@@ -65,3 +66,94 @@ class Region(object):
65
66
  )
66
67
  out._manager = manager
67
68
  return out
69
+
70
+
71
+ class RegionManager(Manager):
72
+ """
73
+ SingleStoreDB region manager.
74
+
75
+ This class should be instantiated using :func:`singlestoredb.manage_regions`.
76
+
77
+ Parameters
78
+ ----------
79
+ access_token : str, optional
80
+ The API key or other access token for the workspace management API
81
+ version : str, optional
82
+ Version of the API to use
83
+ base_url : str, optional
84
+ Base URL of the workspace management API
85
+
86
+ See Also
87
+ --------
88
+ :func:`singlestoredb.manage_regions`
89
+ """
90
+
91
+ #: Object type
92
+ obj_type = 'region'
93
+
94
+ def list_regions(self) -> NamedList[Region]:
95
+ """
96
+ List all available regions.
97
+
98
+ Returns
99
+ -------
100
+ NamedList[Region]
101
+ List of available regions
102
+
103
+ Raises
104
+ ------
105
+ ManagementError
106
+ If there is an error getting the regions
107
+ """
108
+ res = self._get('regions')
109
+ return NamedList(
110
+ [Region.from_dict(item, self) for item in res.json()],
111
+ )
112
+
113
+ def list_shared_tier_regions(self) -> NamedList[Region]:
114
+ """
115
+ List regions that support shared tier workspaces.
116
+
117
+ Returns
118
+ -------
119
+ NamedList[Region]
120
+ List of regions that support shared tier workspaces
121
+
122
+ Raises
123
+ ------
124
+ ManagementError
125
+ If there is an error getting the regions
126
+ """
127
+ res = self._get('regions/sharedtier')
128
+ return NamedList(
129
+ [Region.from_dict(item, self) for item in res.json()],
130
+ )
131
+
132
+
133
+ def manage_regions(
134
+ access_token: Optional[str] = None,
135
+ version: Optional[str] = None,
136
+ base_url: Optional[str] = None,
137
+ ) -> RegionManager:
138
+ """
139
+ Retrieve a SingleStoreDB region manager.
140
+
141
+ Parameters
142
+ ----------
143
+ access_token : str, optional
144
+ The API key or other access token for the workspace management API
145
+ version : str, optional
146
+ Version of the API to use
147
+ base_url : str, optional
148
+ Base URL of the workspace management API
149
+
150
+ Returns
151
+ -------
152
+ :class:`RegionManager`
153
+
154
+ """
155
+ return RegionManager(
156
+ access_token=access_token,
157
+ version=version,
158
+ base_url=base_url,
159
+ )