singlestoredb 1.16.1__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.
- singlestoredb/__init__.py +75 -0
- singlestoredb/ai/__init__.py +2 -0
- singlestoredb/ai/chat.py +139 -0
- singlestoredb/ai/embeddings.py +128 -0
- singlestoredb/alchemy/__init__.py +90 -0
- singlestoredb/apps/__init__.py +3 -0
- singlestoredb/apps/_cloud_functions.py +90 -0
- singlestoredb/apps/_config.py +72 -0
- singlestoredb/apps/_connection_info.py +18 -0
- singlestoredb/apps/_dashboards.py +47 -0
- singlestoredb/apps/_process.py +32 -0
- singlestoredb/apps/_python_udfs.py +100 -0
- singlestoredb/apps/_stdout_supress.py +30 -0
- singlestoredb/apps/_uvicorn_util.py +36 -0
- singlestoredb/auth.py +245 -0
- singlestoredb/config.py +484 -0
- singlestoredb/connection.py +1487 -0
- singlestoredb/converters.py +950 -0
- singlestoredb/docstring/__init__.py +33 -0
- singlestoredb/docstring/attrdoc.py +126 -0
- singlestoredb/docstring/common.py +230 -0
- singlestoredb/docstring/epydoc.py +267 -0
- singlestoredb/docstring/google.py +412 -0
- singlestoredb/docstring/numpydoc.py +562 -0
- singlestoredb/docstring/parser.py +100 -0
- singlestoredb/docstring/py.typed +1 -0
- singlestoredb/docstring/rest.py +256 -0
- singlestoredb/docstring/tests/__init__.py +1 -0
- singlestoredb/docstring/tests/_pydoctor.py +21 -0
- singlestoredb/docstring/tests/test_epydoc.py +729 -0
- singlestoredb/docstring/tests/test_google.py +1007 -0
- singlestoredb/docstring/tests/test_numpydoc.py +1100 -0
- singlestoredb/docstring/tests/test_parse_from_object.py +109 -0
- singlestoredb/docstring/tests/test_parser.py +248 -0
- singlestoredb/docstring/tests/test_rest.py +547 -0
- singlestoredb/docstring/tests/test_util.py +70 -0
- singlestoredb/docstring/util.py +141 -0
- singlestoredb/exceptions.py +120 -0
- singlestoredb/functions/__init__.py +16 -0
- singlestoredb/functions/decorator.py +201 -0
- singlestoredb/functions/dtypes.py +1793 -0
- singlestoredb/functions/ext/__init__.py +1 -0
- singlestoredb/functions/ext/arrow.py +375 -0
- singlestoredb/functions/ext/asgi.py +2133 -0
- singlestoredb/functions/ext/json.py +420 -0
- singlestoredb/functions/ext/mmap.py +413 -0
- singlestoredb/functions/ext/rowdat_1.py +724 -0
- singlestoredb/functions/ext/timer.py +89 -0
- singlestoredb/functions/ext/utils.py +218 -0
- singlestoredb/functions/signature.py +1578 -0
- singlestoredb/functions/typing/__init__.py +41 -0
- singlestoredb/functions/typing/numpy.py +20 -0
- singlestoredb/functions/typing/pandas.py +2 -0
- singlestoredb/functions/typing/polars.py +2 -0
- singlestoredb/functions/typing/pyarrow.py +2 -0
- singlestoredb/functions/utils.py +421 -0
- singlestoredb/fusion/__init__.py +11 -0
- singlestoredb/fusion/graphql.py +213 -0
- singlestoredb/fusion/handler.py +916 -0
- singlestoredb/fusion/handlers/__init__.py +0 -0
- singlestoredb/fusion/handlers/export.py +525 -0
- singlestoredb/fusion/handlers/files.py +690 -0
- singlestoredb/fusion/handlers/job.py +660 -0
- singlestoredb/fusion/handlers/models.py +250 -0
- singlestoredb/fusion/handlers/stage.py +502 -0
- singlestoredb/fusion/handlers/utils.py +324 -0
- singlestoredb/fusion/handlers/workspace.py +956 -0
- singlestoredb/fusion/registry.py +249 -0
- singlestoredb/fusion/result.py +399 -0
- singlestoredb/http/__init__.py +27 -0
- singlestoredb/http/connection.py +1267 -0
- singlestoredb/magics/__init__.py +34 -0
- singlestoredb/magics/run_personal.py +137 -0
- singlestoredb/magics/run_shared.py +134 -0
- singlestoredb/management/__init__.py +9 -0
- singlestoredb/management/billing_usage.py +148 -0
- singlestoredb/management/cluster.py +462 -0
- singlestoredb/management/export.py +295 -0
- singlestoredb/management/files.py +1102 -0
- singlestoredb/management/inference_api.py +105 -0
- singlestoredb/management/job.py +887 -0
- singlestoredb/management/manager.py +373 -0
- singlestoredb/management/organization.py +226 -0
- singlestoredb/management/region.py +169 -0
- singlestoredb/management/utils.py +423 -0
- singlestoredb/management/workspace.py +1927 -0
- singlestoredb/mysql/__init__.py +177 -0
- singlestoredb/mysql/_auth.py +298 -0
- singlestoredb/mysql/charset.py +214 -0
- singlestoredb/mysql/connection.py +2032 -0
- singlestoredb/mysql/constants/CLIENT.py +38 -0
- singlestoredb/mysql/constants/COMMAND.py +32 -0
- singlestoredb/mysql/constants/CR.py +78 -0
- singlestoredb/mysql/constants/ER.py +474 -0
- singlestoredb/mysql/constants/EXTENDED_TYPE.py +3 -0
- singlestoredb/mysql/constants/FIELD_TYPE.py +48 -0
- singlestoredb/mysql/constants/FLAG.py +15 -0
- singlestoredb/mysql/constants/SERVER_STATUS.py +10 -0
- singlestoredb/mysql/constants/VECTOR_TYPE.py +6 -0
- singlestoredb/mysql/constants/__init__.py +0 -0
- singlestoredb/mysql/converters.py +271 -0
- singlestoredb/mysql/cursors.py +896 -0
- singlestoredb/mysql/err.py +92 -0
- singlestoredb/mysql/optionfile.py +20 -0
- singlestoredb/mysql/protocol.py +450 -0
- singlestoredb/mysql/tests/__init__.py +19 -0
- singlestoredb/mysql/tests/base.py +126 -0
- singlestoredb/mysql/tests/conftest.py +37 -0
- singlestoredb/mysql/tests/test_DictCursor.py +132 -0
- singlestoredb/mysql/tests/test_SSCursor.py +141 -0
- singlestoredb/mysql/tests/test_basic.py +452 -0
- singlestoredb/mysql/tests/test_connection.py +851 -0
- singlestoredb/mysql/tests/test_converters.py +58 -0
- singlestoredb/mysql/tests/test_cursor.py +141 -0
- singlestoredb/mysql/tests/test_err.py +16 -0
- singlestoredb/mysql/tests/test_issues.py +514 -0
- singlestoredb/mysql/tests/test_load_local.py +75 -0
- singlestoredb/mysql/tests/test_nextset.py +88 -0
- singlestoredb/mysql/tests/test_optionfile.py +27 -0
- singlestoredb/mysql/tests/thirdparty/__init__.py +6 -0
- singlestoredb/mysql/tests/thirdparty/test_MySQLdb/__init__.py +9 -0
- singlestoredb/mysql/tests/thirdparty/test_MySQLdb/capabilities.py +323 -0
- singlestoredb/mysql/tests/thirdparty/test_MySQLdb/dbapi20.py +865 -0
- singlestoredb/mysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_capabilities.py +110 -0
- singlestoredb/mysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_dbapi20.py +224 -0
- singlestoredb/mysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_nonstandard.py +101 -0
- singlestoredb/mysql/times.py +23 -0
- singlestoredb/notebook/__init__.py +16 -0
- singlestoredb/notebook/_objects.py +213 -0
- singlestoredb/notebook/_portal.py +352 -0
- singlestoredb/py.typed +0 -0
- singlestoredb/pytest.py +352 -0
- singlestoredb/server/__init__.py +0 -0
- singlestoredb/server/docker.py +452 -0
- singlestoredb/server/free_tier.py +267 -0
- singlestoredb/tests/__init__.py +0 -0
- singlestoredb/tests/alltypes.sql +307 -0
- singlestoredb/tests/alltypes_no_nulls.sql +208 -0
- singlestoredb/tests/empty.sql +0 -0
- singlestoredb/tests/ext_funcs/__init__.py +702 -0
- singlestoredb/tests/local_infile.csv +3 -0
- singlestoredb/tests/test.ipynb +18 -0
- singlestoredb/tests/test.sql +680 -0
- singlestoredb/tests/test2.ipynb +18 -0
- singlestoredb/tests/test2.sql +1 -0
- singlestoredb/tests/test_basics.py +1332 -0
- singlestoredb/tests/test_config.py +318 -0
- singlestoredb/tests/test_connection.py +3103 -0
- singlestoredb/tests/test_dbapi.py +27 -0
- singlestoredb/tests/test_exceptions.py +45 -0
- singlestoredb/tests/test_ext_func.py +1472 -0
- singlestoredb/tests/test_ext_func_data.py +1101 -0
- singlestoredb/tests/test_fusion.py +1527 -0
- singlestoredb/tests/test_http.py +288 -0
- singlestoredb/tests/test_management.py +1599 -0
- singlestoredb/tests/test_plugin.py +33 -0
- singlestoredb/tests/test_results.py +171 -0
- singlestoredb/tests/test_types.py +132 -0
- singlestoredb/tests/test_udf.py +737 -0
- singlestoredb/tests/test_udf_returns.py +459 -0
- singlestoredb/tests/test_vectorstore.py +51 -0
- singlestoredb/tests/test_xdict.py +333 -0
- singlestoredb/tests/utils.py +141 -0
- singlestoredb/types.py +373 -0
- singlestoredb/utils/__init__.py +0 -0
- singlestoredb/utils/config.py +950 -0
- singlestoredb/utils/convert_rows.py +69 -0
- singlestoredb/utils/debug.py +13 -0
- singlestoredb/utils/dtypes.py +205 -0
- singlestoredb/utils/events.py +65 -0
- singlestoredb/utils/mogrify.py +151 -0
- singlestoredb/utils/results.py +585 -0
- singlestoredb/utils/xdict.py +425 -0
- singlestoredb/vectorstore.py +192 -0
- singlestoredb/warnings.py +5 -0
- singlestoredb-1.16.1.dist-info/METADATA +165 -0
- singlestoredb-1.16.1.dist-info/RECORD +183 -0
- singlestoredb-1.16.1.dist-info/WHEEL +5 -0
- singlestoredb-1.16.1.dist-info/entry_points.txt +2 -0
- singlestoredb-1.16.1.dist-info/licenses/LICENSE +201 -0
- singlestoredb-1.16.1.dist-info/top_level.txt +3 -0
- sqlx/__init__.py +4 -0
- sqlx/magic.py +113 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import signal
|
|
3
|
+
import typing
|
|
4
|
+
if typing.TYPE_CHECKING:
|
|
5
|
+
from psutil import Process
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def kill_process_by_port(port: int) -> None:
|
|
9
|
+
existing_process = _find_process_by_port(port)
|
|
10
|
+
kernel_pid = os.getpid()
|
|
11
|
+
# Make sure we are not killing current kernel
|
|
12
|
+
if existing_process is not None and kernel_pid != existing_process.pid:
|
|
13
|
+
print(f'Killing process {existing_process.pid} which is using port {port}')
|
|
14
|
+
os.kill(existing_process.pid, signal.SIGKILL)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _find_process_by_port(port: int) -> 'Process | None':
|
|
18
|
+
try:
|
|
19
|
+
import psutil
|
|
20
|
+
except ImportError:
|
|
21
|
+
raise ImportError('package psutil is required')
|
|
22
|
+
|
|
23
|
+
for proc in psutil.process_iter(['pid']):
|
|
24
|
+
try:
|
|
25
|
+
connections = proc.connections()
|
|
26
|
+
for conn in connections:
|
|
27
|
+
if conn.laddr.port == port:
|
|
28
|
+
return proc
|
|
29
|
+
except psutil.AccessDenied:
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
return None
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
from ..functions.ext.asgi import Application
|
|
6
|
+
from ._config import AppConfig
|
|
7
|
+
from ._connection_info import UdfConnectionInfo
|
|
8
|
+
from ._process import kill_process_by_port
|
|
9
|
+
|
|
10
|
+
if typing.TYPE_CHECKING:
|
|
11
|
+
from ._uvicorn_util import AwaitableUvicornServer
|
|
12
|
+
|
|
13
|
+
# Keep track of currently running server
|
|
14
|
+
_running_server: 'typing.Optional[AwaitableUvicornServer]' = None
|
|
15
|
+
|
|
16
|
+
# Maximum number of UDFs allowed
|
|
17
|
+
MAX_UDFS_LIMIT = 10
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def run_udf_app(
|
|
21
|
+
log_level: str = 'error',
|
|
22
|
+
kill_existing_app_server: bool = True,
|
|
23
|
+
) -> UdfConnectionInfo:
|
|
24
|
+
global _running_server
|
|
25
|
+
from ._uvicorn_util import AwaitableUvicornServer
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
import uvicorn
|
|
29
|
+
except ImportError:
|
|
30
|
+
raise ImportError('package uvicorn is required to run python udfs')
|
|
31
|
+
|
|
32
|
+
app_config = AppConfig.from_env()
|
|
33
|
+
|
|
34
|
+
if kill_existing_app_server:
|
|
35
|
+
# Shutdown the server gracefully if it was started by us.
|
|
36
|
+
# Since the uvicorn server doesn't start a new subprocess
|
|
37
|
+
# killing the process would result in kernel dying.
|
|
38
|
+
if _running_server is not None:
|
|
39
|
+
await _running_server.shutdown()
|
|
40
|
+
_running_server = None
|
|
41
|
+
|
|
42
|
+
# Kill if any other process is occupying the port
|
|
43
|
+
kill_process_by_port(app_config.listen_port)
|
|
44
|
+
|
|
45
|
+
base_url = generate_base_url(app_config)
|
|
46
|
+
|
|
47
|
+
udf_suffix = ''
|
|
48
|
+
if app_config.running_interactively:
|
|
49
|
+
udf_suffix = '_test'
|
|
50
|
+
app = Application(
|
|
51
|
+
url=base_url,
|
|
52
|
+
app_mode='managed',
|
|
53
|
+
name_suffix=udf_suffix,
|
|
54
|
+
log_level=log_level,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
if not app.endpoints:
|
|
58
|
+
raise ValueError('You must define at least one function.')
|
|
59
|
+
if len(app.endpoints) > MAX_UDFS_LIMIT:
|
|
60
|
+
raise ValueError(
|
|
61
|
+
f'You can only define a maximum of {MAX_UDFS_LIMIT} functions.',
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
config = uvicorn.Config(
|
|
65
|
+
app,
|
|
66
|
+
host='0.0.0.0',
|
|
67
|
+
port=app_config.listen_port,
|
|
68
|
+
log_config=app.get_uvicorn_log_config(),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Register the functions only if the app is running interactively.
|
|
72
|
+
if app_config.running_interactively:
|
|
73
|
+
app.register_functions(replace=True)
|
|
74
|
+
|
|
75
|
+
_running_server = AwaitableUvicornServer(config)
|
|
76
|
+
asyncio.create_task(_running_server.serve())
|
|
77
|
+
await _running_server.wait_for_startup()
|
|
78
|
+
|
|
79
|
+
print(f'Python UDF registered at {base_url}')
|
|
80
|
+
|
|
81
|
+
return UdfConnectionInfo(base_url, app.get_function_info())
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def generate_base_url(app_config: AppConfig) -> str:
|
|
85
|
+
if not app_config.is_gateway_enabled:
|
|
86
|
+
raise RuntimeError('Python UDFs are not available if Nova Gateway is not enabled')
|
|
87
|
+
|
|
88
|
+
if not app_config.running_interactively:
|
|
89
|
+
return app_config.base_url
|
|
90
|
+
|
|
91
|
+
# generate python udf endpoint for interactive notebooks
|
|
92
|
+
gateway_url = os.environ.get('SINGLESTOREDB_NOVA_GATEWAY_ENDPOINT')
|
|
93
|
+
if app_config.is_local_dev:
|
|
94
|
+
gateway_url = os.environ.get('SINGLESTOREDB_NOVA_GATEWAY_DEV_ENDPOINT')
|
|
95
|
+
if gateway_url is None:
|
|
96
|
+
raise RuntimeError(
|
|
97
|
+
'Missing SINGLESTOREDB_NOVA_GATEWAY_DEV_ENDPOINT environment variable.',
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
return f'{gateway_url}/pythonudfs/{app_config.notebook_server_id}/interactive/'
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import sys
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class StdoutSuppressor:
|
|
7
|
+
"""
|
|
8
|
+
Supresses the stdout for code executed within the context.
|
|
9
|
+
This should not be used for asynchronous or threaded executions.
|
|
10
|
+
|
|
11
|
+
```py
|
|
12
|
+
with StdoutSupressor():
|
|
13
|
+
print("This won't be printed")
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __enter__(self) -> None:
|
|
19
|
+
self.stdout = sys.stdout
|
|
20
|
+
self.buffer = io.StringIO()
|
|
21
|
+
sys.stdout = self.buffer
|
|
22
|
+
|
|
23
|
+
def __exit__(
|
|
24
|
+
self,
|
|
25
|
+
exc_type: Optional[object],
|
|
26
|
+
exc_value: Optional[Exception],
|
|
27
|
+
exc_traceback: Optional[str],
|
|
28
|
+
) -> None:
|
|
29
|
+
del self.buffer
|
|
30
|
+
sys.stdout = self.stdout
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import socket
|
|
3
|
+
from typing import List
|
|
4
|
+
from typing import Optional
|
|
5
|
+
try:
|
|
6
|
+
import uvicorn
|
|
7
|
+
except ImportError:
|
|
8
|
+
raise ImportError('package uvicorn is required')
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AwaitableUvicornServer(uvicorn.Server):
|
|
12
|
+
"""
|
|
13
|
+
Adds `wait_for_startup` method.
|
|
14
|
+
The function (asynchronously) blocks until the server
|
|
15
|
+
starts listening or throws an error.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, config: 'uvicorn.Config') -> None:
|
|
19
|
+
super().__init__(config)
|
|
20
|
+
self._startup_future = asyncio.get_event_loop().create_future()
|
|
21
|
+
|
|
22
|
+
async def startup(self, sockets: Optional[List[socket.socket]] = None) -> None:
|
|
23
|
+
try:
|
|
24
|
+
result = await super().startup(sockets)
|
|
25
|
+
self._startup_future.set_result(True)
|
|
26
|
+
return result
|
|
27
|
+
except Exception as error:
|
|
28
|
+
self._startup_future.set_exception(error)
|
|
29
|
+
raise error
|
|
30
|
+
|
|
31
|
+
async def wait_for_startup(self) -> None:
|
|
32
|
+
await self._startup_future
|
|
33
|
+
|
|
34
|
+
async def shutdown(self, sockets: Optional[list[socket.socket]] = None) -> None:
|
|
35
|
+
if self.started:
|
|
36
|
+
await super().shutdown(sockets)
|
singlestoredb/auth.py
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
import datetime
|
|
3
|
+
from typing import Any
|
|
4
|
+
from typing import List
|
|
5
|
+
from typing import Optional
|
|
6
|
+
from typing import Union
|
|
7
|
+
|
|
8
|
+
import jwt
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# Credential types
|
|
12
|
+
PASSWORD = 'password'
|
|
13
|
+
JWT = 'jwt'
|
|
14
|
+
BROWSER_SSO = 'browser_sso'
|
|
15
|
+
|
|
16
|
+
# Single Sign-On URL
|
|
17
|
+
SSO_URL = 'https://portal.singlestore.com/engine-sso'
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class JSONWebToken(object):
|
|
21
|
+
"""Container for JWT information."""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self, token: str, expires: datetime.datetime,
|
|
25
|
+
email: str, username: str, url: str = SSO_URL,
|
|
26
|
+
clusters: Optional[Union[str, List[str]]] = None,
|
|
27
|
+
databases: Optional[Union[str, List[str]]] = None,
|
|
28
|
+
timeout: int = 60,
|
|
29
|
+
):
|
|
30
|
+
self.token = token
|
|
31
|
+
self.expires = expires
|
|
32
|
+
self.email = email
|
|
33
|
+
self.username = username
|
|
34
|
+
self.model_version_number = 1
|
|
35
|
+
|
|
36
|
+
# Attributes needed for refreshing tokens
|
|
37
|
+
self.url = url
|
|
38
|
+
self.clusters = clusters
|
|
39
|
+
self.databases = databases
|
|
40
|
+
self.timeout = timeout
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def from_token(cls, token: bytes, verify_signature: bool = False) -> 'JSONWebToken':
|
|
44
|
+
"""Validate the contents of the JWT."""
|
|
45
|
+
info = jwt.decode(token, options={'verify_signature': verify_signature})
|
|
46
|
+
|
|
47
|
+
if not info.get('sub', None) and not info.get('username', None):
|
|
48
|
+
raise ValueError("Missing 'sub' and 'username' in claims")
|
|
49
|
+
if not info.get('email', None):
|
|
50
|
+
raise ValueError("Missing 'email' in claims")
|
|
51
|
+
if not info.get('exp', None):
|
|
52
|
+
raise ValueError("Missing 'exp' in claims")
|
|
53
|
+
try:
|
|
54
|
+
expires = datetime.datetime.fromtimestamp(info['exp'], datetime.timezone.utc)
|
|
55
|
+
except Exception as exc:
|
|
56
|
+
raise ValueError("Invalid 'exp' in claims: {}".format(str(exc)))
|
|
57
|
+
|
|
58
|
+
username = info.get('username', info.get('sub', None))
|
|
59
|
+
email = info['email']
|
|
60
|
+
|
|
61
|
+
return cls(token.decode('utf-8'), expires=expires, email=email, username=username)
|
|
62
|
+
|
|
63
|
+
def __str__(self) -> str:
|
|
64
|
+
return self.token
|
|
65
|
+
|
|
66
|
+
def __repr__(self) -> str:
|
|
67
|
+
return repr(self.token)
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def is_expired(self) -> bool:
|
|
71
|
+
"""Determine if the token has expired."""
|
|
72
|
+
return self.expires >= datetime.datetime.now()
|
|
73
|
+
|
|
74
|
+
def refresh(self, force: bool = False) -> bool:
|
|
75
|
+
"""
|
|
76
|
+
Refresh the token as needed.
|
|
77
|
+
|
|
78
|
+
Parameters
|
|
79
|
+
----------
|
|
80
|
+
force : bool, optional
|
|
81
|
+
Should a new token be generated even if the existing
|
|
82
|
+
one has not expired yet?
|
|
83
|
+
|
|
84
|
+
Returns
|
|
85
|
+
-------
|
|
86
|
+
bool : Indicating whether the token was refreshed or not
|
|
87
|
+
|
|
88
|
+
"""
|
|
89
|
+
if force or self.is_expired:
|
|
90
|
+
out = get_jwt(
|
|
91
|
+
self.email, url=self.url, clusters=self.clusters,
|
|
92
|
+
databases=self.databases, timeout=self.timeout,
|
|
93
|
+
)
|
|
94
|
+
self.token = out.token
|
|
95
|
+
self.expires = out.expires
|
|
96
|
+
return True
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _listify(s: Optional[Union[str, List[str]]]) -> Optional[str]:
|
|
101
|
+
"""Return a list of strings in a comma-separated string."""
|
|
102
|
+
if s is None:
|
|
103
|
+
return None
|
|
104
|
+
if not isinstance(s, str):
|
|
105
|
+
return ','.join(s)
|
|
106
|
+
return s
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def get_jwt(
|
|
110
|
+
email: str, url: str = SSO_URL,
|
|
111
|
+
clusters: Optional[Union[str, List[str]]] = None,
|
|
112
|
+
databases: Optional[Union[str, List[str]]] = None,
|
|
113
|
+
timeout: int = 60, browser: Optional[Union[str, List[str]]] = None,
|
|
114
|
+
) -> JSONWebToken:
|
|
115
|
+
"""
|
|
116
|
+
Retrieve a JWT token from the SingleStoreDB single-sign-on URL.
|
|
117
|
+
|
|
118
|
+
Parameters
|
|
119
|
+
----------
|
|
120
|
+
email : str
|
|
121
|
+
EMail of the database user
|
|
122
|
+
url : str, optional
|
|
123
|
+
The URL of the single-sign-on token generator
|
|
124
|
+
clusters : str or list[str], optional
|
|
125
|
+
The name of the cluster being connected to
|
|
126
|
+
databases : str or list[str], optional
|
|
127
|
+
The name of the database being connected to
|
|
128
|
+
timeout : int, optional
|
|
129
|
+
Number of seconds to wait before timing out the authentication request
|
|
130
|
+
browser : str or list[str], optional
|
|
131
|
+
Browser to use instead of the default. This value can be any of the
|
|
132
|
+
names specified in Python's `webbrowser` module. This includes
|
|
133
|
+
'google-chrome', 'chrome', 'chromium', 'chromium-browser', 'firefox',
|
|
134
|
+
etc. Note that at the time of this writing, Safari was not
|
|
135
|
+
compatible. If a list of names is specified, each one tried until
|
|
136
|
+
a working browser is located.
|
|
137
|
+
|
|
138
|
+
Returns
|
|
139
|
+
-------
|
|
140
|
+
JSONWebToken
|
|
141
|
+
|
|
142
|
+
"""
|
|
143
|
+
import platform
|
|
144
|
+
import webbrowser
|
|
145
|
+
import time
|
|
146
|
+
import threading
|
|
147
|
+
import urllib
|
|
148
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
149
|
+
|
|
150
|
+
from .config import get_option
|
|
151
|
+
|
|
152
|
+
token = []
|
|
153
|
+
error = []
|
|
154
|
+
|
|
155
|
+
class AuthServer(BaseHTTPRequestHandler):
|
|
156
|
+
|
|
157
|
+
def log_message(self, format: str, *args: Any) -> None:
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
def do_POST(self) -> None:
|
|
161
|
+
content_len = int(self.headers.get('Content-Length', 0))
|
|
162
|
+
post_body = self.rfile.read(content_len)
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
out = JSONWebToken.from_token(post_body)
|
|
166
|
+
except Exception as exc:
|
|
167
|
+
self.send_response(400, exc.args[0])
|
|
168
|
+
self.send_header('Content-Type', 'text/plain')
|
|
169
|
+
self.end_headers()
|
|
170
|
+
error.append(exc)
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
token.append(out)
|
|
174
|
+
|
|
175
|
+
self.send_response(204)
|
|
176
|
+
self.send_header('Access-Control-Allow-Origin', '*')
|
|
177
|
+
self.send_header('Content-Type', 'text/plain')
|
|
178
|
+
self.end_headers()
|
|
179
|
+
|
|
180
|
+
server = None
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
server = HTTPServer(('127.0.0.1', 0), AuthServer)
|
|
184
|
+
threading.Thread(target=server.serve_forever).start()
|
|
185
|
+
|
|
186
|
+
host = server.server_address[0]
|
|
187
|
+
if isinstance(host, bytes):
|
|
188
|
+
host = host.decode('utf-8')
|
|
189
|
+
|
|
190
|
+
query = urllib.parse.urlencode({
|
|
191
|
+
k: v for k, v in dict(
|
|
192
|
+
email=email,
|
|
193
|
+
returnTo=f'http://{host}:{server.server_address[1]}',
|
|
194
|
+
db=_listify(databases),
|
|
195
|
+
cluster=_listify(clusters),
|
|
196
|
+
).items() if v is not None
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
if browser is None:
|
|
200
|
+
browser = get_option('sso_browser')
|
|
201
|
+
|
|
202
|
+
# On Mac, always specify a list of browsers to check because Safari
|
|
203
|
+
# is not compatible.
|
|
204
|
+
if browser is None and platform.platform().lower().startswith('mac'):
|
|
205
|
+
browser = [
|
|
206
|
+
'chrome', 'google-chrome', 'chromium',
|
|
207
|
+
'chromium-browser', 'firefox',
|
|
208
|
+
]
|
|
209
|
+
|
|
210
|
+
if browser and isinstance(browser, str):
|
|
211
|
+
browser = [browser]
|
|
212
|
+
|
|
213
|
+
if browser:
|
|
214
|
+
exc: Optional[Exception] = None
|
|
215
|
+
for item in browser:
|
|
216
|
+
try:
|
|
217
|
+
webbrowser.get(item).open(f'{url}?{query}')
|
|
218
|
+
break
|
|
219
|
+
except webbrowser.Error as wexc:
|
|
220
|
+
exc = wexc
|
|
221
|
+
pass
|
|
222
|
+
if exc is not None:
|
|
223
|
+
raise RuntimeError(
|
|
224
|
+
'Could not find compatible web browser for accessing JWT',
|
|
225
|
+
)
|
|
226
|
+
else:
|
|
227
|
+
webbrowser.open(f'{url}?{query}')
|
|
228
|
+
|
|
229
|
+
for i in range(timeout * 2):
|
|
230
|
+
if error:
|
|
231
|
+
raise error[0]
|
|
232
|
+
if token:
|
|
233
|
+
out = token[0]
|
|
234
|
+
out.url = url
|
|
235
|
+
out.clusters = clusters
|
|
236
|
+
out.databases = databases
|
|
237
|
+
out.timeout = timeout
|
|
238
|
+
return out
|
|
239
|
+
time.sleep(0.5)
|
|
240
|
+
|
|
241
|
+
finally:
|
|
242
|
+
if server is not None:
|
|
243
|
+
server.shutdown()
|
|
244
|
+
|
|
245
|
+
raise RuntimeError('Timeout waiting for token')
|