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.
Files changed (183) hide show
  1. singlestoredb/__init__.py +75 -0
  2. singlestoredb/ai/__init__.py +2 -0
  3. singlestoredb/ai/chat.py +139 -0
  4. singlestoredb/ai/embeddings.py +128 -0
  5. singlestoredb/alchemy/__init__.py +90 -0
  6. singlestoredb/apps/__init__.py +3 -0
  7. singlestoredb/apps/_cloud_functions.py +90 -0
  8. singlestoredb/apps/_config.py +72 -0
  9. singlestoredb/apps/_connection_info.py +18 -0
  10. singlestoredb/apps/_dashboards.py +47 -0
  11. singlestoredb/apps/_process.py +32 -0
  12. singlestoredb/apps/_python_udfs.py +100 -0
  13. singlestoredb/apps/_stdout_supress.py +30 -0
  14. singlestoredb/apps/_uvicorn_util.py +36 -0
  15. singlestoredb/auth.py +245 -0
  16. singlestoredb/config.py +484 -0
  17. singlestoredb/connection.py +1487 -0
  18. singlestoredb/converters.py +950 -0
  19. singlestoredb/docstring/__init__.py +33 -0
  20. singlestoredb/docstring/attrdoc.py +126 -0
  21. singlestoredb/docstring/common.py +230 -0
  22. singlestoredb/docstring/epydoc.py +267 -0
  23. singlestoredb/docstring/google.py +412 -0
  24. singlestoredb/docstring/numpydoc.py +562 -0
  25. singlestoredb/docstring/parser.py +100 -0
  26. singlestoredb/docstring/py.typed +1 -0
  27. singlestoredb/docstring/rest.py +256 -0
  28. singlestoredb/docstring/tests/__init__.py +1 -0
  29. singlestoredb/docstring/tests/_pydoctor.py +21 -0
  30. singlestoredb/docstring/tests/test_epydoc.py +729 -0
  31. singlestoredb/docstring/tests/test_google.py +1007 -0
  32. singlestoredb/docstring/tests/test_numpydoc.py +1100 -0
  33. singlestoredb/docstring/tests/test_parse_from_object.py +109 -0
  34. singlestoredb/docstring/tests/test_parser.py +248 -0
  35. singlestoredb/docstring/tests/test_rest.py +547 -0
  36. singlestoredb/docstring/tests/test_util.py +70 -0
  37. singlestoredb/docstring/util.py +141 -0
  38. singlestoredb/exceptions.py +120 -0
  39. singlestoredb/functions/__init__.py +16 -0
  40. singlestoredb/functions/decorator.py +201 -0
  41. singlestoredb/functions/dtypes.py +1793 -0
  42. singlestoredb/functions/ext/__init__.py +1 -0
  43. singlestoredb/functions/ext/arrow.py +375 -0
  44. singlestoredb/functions/ext/asgi.py +2133 -0
  45. singlestoredb/functions/ext/json.py +420 -0
  46. singlestoredb/functions/ext/mmap.py +413 -0
  47. singlestoredb/functions/ext/rowdat_1.py +724 -0
  48. singlestoredb/functions/ext/timer.py +89 -0
  49. singlestoredb/functions/ext/utils.py +218 -0
  50. singlestoredb/functions/signature.py +1578 -0
  51. singlestoredb/functions/typing/__init__.py +41 -0
  52. singlestoredb/functions/typing/numpy.py +20 -0
  53. singlestoredb/functions/typing/pandas.py +2 -0
  54. singlestoredb/functions/typing/polars.py +2 -0
  55. singlestoredb/functions/typing/pyarrow.py +2 -0
  56. singlestoredb/functions/utils.py +421 -0
  57. singlestoredb/fusion/__init__.py +11 -0
  58. singlestoredb/fusion/graphql.py +213 -0
  59. singlestoredb/fusion/handler.py +916 -0
  60. singlestoredb/fusion/handlers/__init__.py +0 -0
  61. singlestoredb/fusion/handlers/export.py +525 -0
  62. singlestoredb/fusion/handlers/files.py +690 -0
  63. singlestoredb/fusion/handlers/job.py +660 -0
  64. singlestoredb/fusion/handlers/models.py +250 -0
  65. singlestoredb/fusion/handlers/stage.py +502 -0
  66. singlestoredb/fusion/handlers/utils.py +324 -0
  67. singlestoredb/fusion/handlers/workspace.py +956 -0
  68. singlestoredb/fusion/registry.py +249 -0
  69. singlestoredb/fusion/result.py +399 -0
  70. singlestoredb/http/__init__.py +27 -0
  71. singlestoredb/http/connection.py +1267 -0
  72. singlestoredb/magics/__init__.py +34 -0
  73. singlestoredb/magics/run_personal.py +137 -0
  74. singlestoredb/magics/run_shared.py +134 -0
  75. singlestoredb/management/__init__.py +9 -0
  76. singlestoredb/management/billing_usage.py +148 -0
  77. singlestoredb/management/cluster.py +462 -0
  78. singlestoredb/management/export.py +295 -0
  79. singlestoredb/management/files.py +1102 -0
  80. singlestoredb/management/inference_api.py +105 -0
  81. singlestoredb/management/job.py +887 -0
  82. singlestoredb/management/manager.py +373 -0
  83. singlestoredb/management/organization.py +226 -0
  84. singlestoredb/management/region.py +169 -0
  85. singlestoredb/management/utils.py +423 -0
  86. singlestoredb/management/workspace.py +1927 -0
  87. singlestoredb/mysql/__init__.py +177 -0
  88. singlestoredb/mysql/_auth.py +298 -0
  89. singlestoredb/mysql/charset.py +214 -0
  90. singlestoredb/mysql/connection.py +2032 -0
  91. singlestoredb/mysql/constants/CLIENT.py +38 -0
  92. singlestoredb/mysql/constants/COMMAND.py +32 -0
  93. singlestoredb/mysql/constants/CR.py +78 -0
  94. singlestoredb/mysql/constants/ER.py +474 -0
  95. singlestoredb/mysql/constants/EXTENDED_TYPE.py +3 -0
  96. singlestoredb/mysql/constants/FIELD_TYPE.py +48 -0
  97. singlestoredb/mysql/constants/FLAG.py +15 -0
  98. singlestoredb/mysql/constants/SERVER_STATUS.py +10 -0
  99. singlestoredb/mysql/constants/VECTOR_TYPE.py +6 -0
  100. singlestoredb/mysql/constants/__init__.py +0 -0
  101. singlestoredb/mysql/converters.py +271 -0
  102. singlestoredb/mysql/cursors.py +896 -0
  103. singlestoredb/mysql/err.py +92 -0
  104. singlestoredb/mysql/optionfile.py +20 -0
  105. singlestoredb/mysql/protocol.py +450 -0
  106. singlestoredb/mysql/tests/__init__.py +19 -0
  107. singlestoredb/mysql/tests/base.py +126 -0
  108. singlestoredb/mysql/tests/conftest.py +37 -0
  109. singlestoredb/mysql/tests/test_DictCursor.py +132 -0
  110. singlestoredb/mysql/tests/test_SSCursor.py +141 -0
  111. singlestoredb/mysql/tests/test_basic.py +452 -0
  112. singlestoredb/mysql/tests/test_connection.py +851 -0
  113. singlestoredb/mysql/tests/test_converters.py +58 -0
  114. singlestoredb/mysql/tests/test_cursor.py +141 -0
  115. singlestoredb/mysql/tests/test_err.py +16 -0
  116. singlestoredb/mysql/tests/test_issues.py +514 -0
  117. singlestoredb/mysql/tests/test_load_local.py +75 -0
  118. singlestoredb/mysql/tests/test_nextset.py +88 -0
  119. singlestoredb/mysql/tests/test_optionfile.py +27 -0
  120. singlestoredb/mysql/tests/thirdparty/__init__.py +6 -0
  121. singlestoredb/mysql/tests/thirdparty/test_MySQLdb/__init__.py +9 -0
  122. singlestoredb/mysql/tests/thirdparty/test_MySQLdb/capabilities.py +323 -0
  123. singlestoredb/mysql/tests/thirdparty/test_MySQLdb/dbapi20.py +865 -0
  124. singlestoredb/mysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_capabilities.py +110 -0
  125. singlestoredb/mysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_dbapi20.py +224 -0
  126. singlestoredb/mysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_nonstandard.py +101 -0
  127. singlestoredb/mysql/times.py +23 -0
  128. singlestoredb/notebook/__init__.py +16 -0
  129. singlestoredb/notebook/_objects.py +213 -0
  130. singlestoredb/notebook/_portal.py +352 -0
  131. singlestoredb/py.typed +0 -0
  132. singlestoredb/pytest.py +352 -0
  133. singlestoredb/server/__init__.py +0 -0
  134. singlestoredb/server/docker.py +452 -0
  135. singlestoredb/server/free_tier.py +267 -0
  136. singlestoredb/tests/__init__.py +0 -0
  137. singlestoredb/tests/alltypes.sql +307 -0
  138. singlestoredb/tests/alltypes_no_nulls.sql +208 -0
  139. singlestoredb/tests/empty.sql +0 -0
  140. singlestoredb/tests/ext_funcs/__init__.py +702 -0
  141. singlestoredb/tests/local_infile.csv +3 -0
  142. singlestoredb/tests/test.ipynb +18 -0
  143. singlestoredb/tests/test.sql +680 -0
  144. singlestoredb/tests/test2.ipynb +18 -0
  145. singlestoredb/tests/test2.sql +1 -0
  146. singlestoredb/tests/test_basics.py +1332 -0
  147. singlestoredb/tests/test_config.py +318 -0
  148. singlestoredb/tests/test_connection.py +3103 -0
  149. singlestoredb/tests/test_dbapi.py +27 -0
  150. singlestoredb/tests/test_exceptions.py +45 -0
  151. singlestoredb/tests/test_ext_func.py +1472 -0
  152. singlestoredb/tests/test_ext_func_data.py +1101 -0
  153. singlestoredb/tests/test_fusion.py +1527 -0
  154. singlestoredb/tests/test_http.py +288 -0
  155. singlestoredb/tests/test_management.py +1599 -0
  156. singlestoredb/tests/test_plugin.py +33 -0
  157. singlestoredb/tests/test_results.py +171 -0
  158. singlestoredb/tests/test_types.py +132 -0
  159. singlestoredb/tests/test_udf.py +737 -0
  160. singlestoredb/tests/test_udf_returns.py +459 -0
  161. singlestoredb/tests/test_vectorstore.py +51 -0
  162. singlestoredb/tests/test_xdict.py +333 -0
  163. singlestoredb/tests/utils.py +141 -0
  164. singlestoredb/types.py +373 -0
  165. singlestoredb/utils/__init__.py +0 -0
  166. singlestoredb/utils/config.py +950 -0
  167. singlestoredb/utils/convert_rows.py +69 -0
  168. singlestoredb/utils/debug.py +13 -0
  169. singlestoredb/utils/dtypes.py +205 -0
  170. singlestoredb/utils/events.py +65 -0
  171. singlestoredb/utils/mogrify.py +151 -0
  172. singlestoredb/utils/results.py +585 -0
  173. singlestoredb/utils/xdict.py +425 -0
  174. singlestoredb/vectorstore.py +192 -0
  175. singlestoredb/warnings.py +5 -0
  176. singlestoredb-1.16.1.dist-info/METADATA +165 -0
  177. singlestoredb-1.16.1.dist-info/RECORD +183 -0
  178. singlestoredb-1.16.1.dist-info/WHEEL +5 -0
  179. singlestoredb-1.16.1.dist-info/entry_points.txt +2 -0
  180. singlestoredb-1.16.1.dist-info/licenses/LICENSE +201 -0
  181. singlestoredb-1.16.1.dist-info/top_level.txt +3 -0
  182. sqlx/__init__.py +4 -0
  183. 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')