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,3 +1,6 @@
1
1
  #!/usr/bin/env python
2
2
  from .cluster import manage_cluster
3
+ from .manager import get_organization
4
+ from .manager import get_token
5
+ from .workspace import get_secret
3
6
  from .workspace import manage_workspaces
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env python
2
+ """SingleStoreDB Cloud Billing Usage."""
3
+ import datetime
4
+ from typing import Any
5
+ from typing import Dict
6
+ from typing import List
7
+ from typing import Optional
8
+
9
+ from .manager import Manager
10
+ from .utils import camel_to_snake
11
+ from .utils import vars_to_str
12
+
13
+
14
+ class UsageItem(object):
15
+ """Usage statistics."""
16
+
17
+ def __init__(
18
+ self,
19
+ start_time: datetime.datetime,
20
+ end_time: datetime.datetime,
21
+ owner_id: str,
22
+ resource_id: str,
23
+ resource_name: str,
24
+ resource_type: str,
25
+ value: str,
26
+ ):
27
+ #: Starting time for the usage duration
28
+ self.start_time = start_time
29
+
30
+ #: Ending time for the usage duration
31
+ self.end_time = end_time
32
+
33
+ #: Owner ID
34
+ self.owner_id = owner_id
35
+
36
+ #: Resource ID
37
+ self.resource_id = resource_id
38
+
39
+ #: Resource name
40
+ self.resource_name = resource_name
41
+
42
+ #: Resource type
43
+ self.resource_type = resource_type
44
+
45
+ #: Usage statistic value
46
+ self.value = value
47
+
48
+ self._manager: Optional[Manager] = None
49
+
50
+ def __str__(self) -> str:
51
+ """Return string representation."""
52
+ return vars_to_str(self)
53
+
54
+ def __repr__(self) -> str:
55
+ """Return string representation."""
56
+ return str(self)
57
+
58
+ @classmethod
59
+ def from_dict(
60
+ cls,
61
+ obj: Dict[str, Any],
62
+ manager: Manager,
63
+ ) -> 'UsageItem':
64
+ """
65
+ Convert dictionary to a ``UsageItem`` object.
66
+
67
+ Parameters
68
+ ----------
69
+ obj : dict
70
+ Key-value pairs to retrieve billling usage information from
71
+ manager : WorkspaceManager, optional
72
+ The WorkspaceManager the UsageItem belongs to
73
+
74
+ Returns
75
+ -------
76
+ :class:`UsageItem`
77
+
78
+ """
79
+ out = cls(
80
+ end_time=datetime.datetime.fromisoformat(obj['endTime']),
81
+ start_time=datetime.datetime.fromisoformat(obj['startTime']),
82
+ owner_id=obj['ownerId'],
83
+ resource_id=obj['resourceId'],
84
+ resource_name=obj['resourceName'],
85
+ resource_type=obj['resource_type'],
86
+ value=obj['value'],
87
+ )
88
+ out._manager = manager
89
+ return out
90
+
91
+
92
+ class BillingUsageItem(object):
93
+ """Billing usage item."""
94
+
95
+ def __init__(
96
+ self,
97
+ description: str,
98
+ metric: str,
99
+ usage: List[UsageItem],
100
+ ):
101
+ """Use :attr:`WorkspaceManager.billing.usage` instead."""
102
+ #: Description of the usage metric
103
+ self.description = description
104
+
105
+ #: Name of the usage metric
106
+ self.metric = metric
107
+
108
+ #: Usage statistics
109
+ self.usage = list(usage)
110
+
111
+ self._manager: Optional[Manager] = None
112
+
113
+ def __str__(self) -> str:
114
+ """Return string representation."""
115
+ return vars_to_str(self)
116
+
117
+ def __repr__(self) -> str:
118
+ """Return string representation."""
119
+ return str(self)
120
+
121
+ @ classmethod
122
+ def from_dict(
123
+ cls,
124
+ obj: Dict[str, Any],
125
+ manager: Manager,
126
+ ) -> 'BillingUsageItem':
127
+ """
128
+ Convert dictionary to a ``BillingUsageItem`` object.
129
+
130
+ Parameters
131
+ ----------
132
+ obj : dict
133
+ Key-value pairs to retrieve billling usage information from
134
+ manager : WorkspaceManager, optional
135
+ The WorkspaceManager the BillingUsageItem belongs to
136
+
137
+ Returns
138
+ -------
139
+ :class:`BillingUsageItem`
140
+
141
+ """
142
+ out = cls(
143
+ description=obj['description'],
144
+ metric=str(camel_to_snake(obj['metric'])),
145
+ usage=[UsageItem.from_dict(x, manager) for x in obj['Usage']],
146
+ )
147
+ out._manager = manager
148
+ return out
@@ -12,6 +12,7 @@ from .. import connection
12
12
  from ..exceptions import ManagementError
13
13
  from .manager import Manager
14
14
  from .region import Region
15
+ from .utils import NamedList
15
16
  from .utils import to_datetime
16
17
  from .utils import vars_to_str
17
18
 
@@ -337,16 +338,16 @@ class ClusterManager(Manager):
337
338
  obj_type = 'cluster'
338
339
 
339
340
  @property
340
- def clusters(self) -> List[Cluster]:
341
+ def clusters(self) -> NamedList[Cluster]:
341
342
  """Return a list of available clusters."""
342
343
  res = self._get('clusters')
343
- return [Cluster.from_dict(item, self) for item in res.json()]
344
+ return NamedList([Cluster.from_dict(item, self) for item in res.json()])
344
345
 
345
346
  @property
346
- def regions(self) -> List[Region]:
347
+ def regions(self) -> NamedList[Region]:
347
348
  """Return a list of available regions."""
348
349
  res = self._get('regions')
349
- return [Region.from_dict(item, self) for item in res.json()]
350
+ return NamedList([Region.from_dict(item, self) for item in res.json()])
350
351
 
351
352
  def create_cluster(
352
353
  self, name: str, region: Union[str, Region], admin_password: str,
@@ -426,6 +427,8 @@ def manage_cluster(
426
427
  access_token: Optional[str] = None,
427
428
  version: str = ClusterManager.default_version,
428
429
  base_url: str = ClusterManager.default_base_url,
430
+ *,
431
+ organization_id: Optional[str] = None,
429
432
  ) -> ClusterManager:
430
433
  """
431
434
  Retrieve a SingleStoreDB cluster manager.
@@ -438,6 +441,8 @@ def manage_cluster(
438
441
  Version of the API to use
439
442
  base_url : str, optional
440
443
  Base URL of the cluster management API
444
+ organization_id: str, optional
445
+ ID of organization, if using a JWT for authentication
441
446
 
442
447
  Returns
443
448
  -------
@@ -446,7 +451,10 @@ def manage_cluster(
446
451
  """
447
452
  warnings.warn(
448
453
  'The cluster management API is deprecated; '
449
- 'use manage_workspace instead.',
454
+ 'use manage_workspaces instead.',
450
455
  category=DeprecationWarning,
451
456
  )
452
- return ClusterManager(access_token=access_token, base_url=base_url, version=version)
457
+ return ClusterManager(
458
+ access_token=access_token, base_url=base_url,
459
+ version=version, organization_id=organization_id,
460
+ )
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env python
2
2
  """SingleStoreDB Base Manager."""
3
+ import os
4
+ import sys
3
5
  import time
4
6
  from typing import Any
5
7
  from typing import Dict
@@ -8,44 +10,73 @@ from typing import Optional
8
10
  from typing import Union
9
11
  from urllib.parse import urljoin
10
12
 
13
+ import jwt
11
14
  import requests
12
15
 
13
16
  from .. import config
14
17
  from ..exceptions import ManagementError
18
+ from .utils import get_organization
19
+ from .utils import get_token
20
+
21
+
22
+ def set_organization(kwargs: Dict[str, Any]) -> None:
23
+ """Set the organization ID in the dictionary."""
24
+ if kwargs.get('params', {}).get('organizationID', None):
25
+ return
26
+
27
+ org = get_organization()
28
+ if org:
29
+ if 'params' not in kwargs:
30
+ kwargs['params'] = {}
31
+ kwargs['params']['organizationID'] = org
32
+
33
+
34
+ def is_jwt(token: str) -> bool:
35
+ """Is the given token a JWT?"""
36
+ try:
37
+ jwt.decode(token, options={'verify_signature': False})
38
+ return True
39
+ except jwt.DecodeError:
40
+ return False
15
41
 
16
42
 
17
43
  class Manager(object):
18
44
  """SingleStoreDB manager base class."""
19
45
 
20
46
  #: Management API version if none is specified.
21
- default_version = 'v1'
47
+ default_version = config.get_option('management.version')
22
48
 
23
49
  #: Base URL if none is specified.
24
- default_base_url = 'https://api.singlestore.com'
50
+ default_base_url = config.get_option('management.base_url')
25
51
 
26
52
  #: Object type
27
53
  obj_type = ''
28
54
 
29
55
  def __init__(
30
56
  self, access_token: Optional[str] = None, version: Optional[str] = None,
31
- base_url: Optional[str] = None,
57
+ base_url: Optional[str] = None, *, organization_id: Optional[str] = None,
32
58
  ):
33
- access_token = (
34
- access_token or
35
- config.get_option('management.token')
59
+ from .. import __version__ as client_version
60
+ new_access_token = (
61
+ access_token or get_token()
36
62
  )
37
- if not access_token:
63
+ if not new_access_token:
38
64
  raise ManagementError(msg='No management token was configured.')
65
+ self._is_jwt = not access_token and new_access_token and is_jwt(new_access_token)
39
66
  self._sess = requests.Session()
40
67
  self._sess.headers.update({
41
- 'Authorization': f'Bearer {access_token}',
68
+ 'Authorization': f'Bearer {new_access_token}',
42
69
  'Content-Type': 'application/json',
43
70
  'Accept': 'application/json',
71
+ 'User-Agent': f'SingleStoreDB-Python/{client_version}',
44
72
  })
45
73
  self._base_url = urljoin(
46
74
  base_url or type(self).default_base_url,
47
75
  version or type(self).default_version,
48
76
  ) + '/'
77
+ self._params: Dict[str, str] = {}
78
+ if organization_id:
79
+ self._params['organizationID'] = organization_id
49
80
 
50
81
  def _check(
51
82
  self, res: requests.Response, url: str, params: Dict[str, Any],
@@ -63,6 +94,8 @@ class Manager(object):
63
94
  requests.Response
64
95
 
65
96
  """
97
+ if config.get_option('debug.queries'):
98
+ print(os.path.join(self._base_url, url), params, file=sys.stderr)
66
99
  if res.status_code >= 400:
67
100
  txt = res.text.strip()
68
101
  msg = f'{txt}: /{url}'
@@ -70,12 +103,27 @@ class Manager(object):
70
103
  new_params = params.copy()
71
104
  if 'json' in new_params:
72
105
  for k, v in new_params['json'].items():
73
- if 'password' in k.lower():
106
+ if 'password' in k.lower() and v:
74
107
  new_params['json'][k] = '*' * len(v)
75
108
  msg += ': {}'.format(str(new_params))
76
- raise ManagementError(errno=res.status_code, msg=msg)
109
+ raise ManagementError(errno=res.status_code, msg=msg, response=txt)
77
110
  return res
78
111
 
112
+ def _doit(
113
+ self,
114
+ method: str,
115
+ path: str,
116
+ *args: Any,
117
+ **kwargs: Any,
118
+ ) -> requests.Response:
119
+ """Perform HTTP request."""
120
+ # Refresh the JWT as needed
121
+ if self._is_jwt:
122
+ self._sess.headers.update({'Authorization': f'Bearer {get_token()}'})
123
+ return getattr(self._sess, method.lower())(
124
+ urljoin(self._base_url, path), *args, **kwargs,
125
+ )
126
+
79
127
  def _get(self, path: str, *args: Any, **kwargs: Any) -> requests.Response:
80
128
  """
81
129
  Invoke a GET request.
@@ -94,13 +142,10 @@ class Manager(object):
94
142
  requests.Response
95
143
 
96
144
  """
97
- return self._check(
98
- self._sess.get(
99
- urljoin(self._base_url, path),
100
- *args, **kwargs,
101
- ),
102
- path, kwargs,
103
- )
145
+ if self._params:
146
+ kwargs['params'] = self._params
147
+ set_organization(kwargs)
148
+ return self._check(self._doit('get', path, *args, **kwargs), path, kwargs)
104
149
 
105
150
  def _post(self, path: str, *args: Any, **kwargs: Any) -> requests.Response:
106
151
  """
@@ -120,13 +165,33 @@ class Manager(object):
120
165
  requests.Response
121
166
 
122
167
  """
123
- return self._check(
124
- self._sess.post(
125
- urljoin(self._base_url, path),
126
- *args, **kwargs,
127
- ),
128
- path, kwargs,
129
- )
168
+ if self._params:
169
+ kwargs['params'] = self._params
170
+ set_organization(kwargs)
171
+ return self._check(self._doit('post', path, *args, **kwargs), path, kwargs)
172
+
173
+ def _put(self, path: str, *args: Any, **kwargs: Any) -> requests.Response:
174
+ """
175
+ Invoke a PUT request.
176
+
177
+ Parameters
178
+ ----------
179
+ path : str
180
+ Path of the resource
181
+ *args : positional arguments, optional
182
+ Arguments to add to the POST request
183
+ **kwargs : keyword arguments, optional
184
+ Keyword arguments to add to the POST request
185
+
186
+ Returns
187
+ -------
188
+ requests.Response
189
+
190
+ """
191
+ if self._params:
192
+ kwargs['params'] = self._params
193
+ set_organization(kwargs)
194
+ return self._check(self._doit('put', path, *args, **kwargs), path, kwargs)
130
195
 
131
196
  def _delete(self, path: str, *args: Any, **kwargs: Any) -> requests.Response:
132
197
  """
@@ -146,13 +211,10 @@ class Manager(object):
146
211
  requests.Response
147
212
 
148
213
  """
149
- return self._check(
150
- self._sess.delete(
151
- urljoin(self._base_url, path),
152
- *args, **kwargs,
153
- ),
154
- path, kwargs,
155
- )
214
+ if self._params:
215
+ kwargs['params'] = self._params
216
+ set_organization(kwargs)
217
+ return self._check(self._doit('delete', path, *args, **kwargs), path, kwargs)
156
218
 
157
219
  def _patch(self, path: str, *args: Any, **kwargs: Any) -> requests.Response:
158
220
  """
@@ -172,13 +234,10 @@ class Manager(object):
172
234
  requests.Response
173
235
 
174
236
  """
175
- return self._check(
176
- self._sess.patch(
177
- urljoin(self._base_url, path),
178
- *args, **kwargs,
179
- ),
180
- path, kwargs,
181
- )
237
+ if self._params:
238
+ kwargs['params'] = self._params
239
+ set_organization(kwargs)
240
+ return self._check(self._doit('patch', path, *args, **kwargs), path, kwargs)
182
241
 
183
242
  def _wait_on_state(
184
243
  self,
@@ -215,12 +274,14 @@ class Manager(object):
215
274
  x.lower().strip()
216
275
  for x in (isinstance(state, str) and [state] or state)
217
276
  ]
277
+
218
278
  if getattr(out, 'state', None) is None:
219
279
  raise ManagementError(
220
280
  msg='{} object does not have a `state` attribute'.format(
221
281
  type(out).__name__,
222
282
  ),
223
283
  )
284
+
224
285
  while True:
225
286
  if getattr(out, 'state').lower() in states:
226
287
  break
@@ -232,4 +293,5 @@ class Manager(object):
232
293
  time.sleep(interval)
233
294
  timeout -= interval
234
295
  out = getattr(self, f'get_{self.obj_type}')(out.id)
296
+
235
297
  return out
@@ -0,0 +1,188 @@
1
+ #!/usr/bin/env python
2
+ """SingleStoreDB Cloud Organization."""
3
+ import datetime
4
+ from typing import Dict
5
+ from typing import List
6
+ from typing import Optional
7
+ from typing import Union
8
+
9
+ from ..exceptions import ManagementError
10
+ from .manager import Manager
11
+ from .utils import vars_to_str
12
+
13
+
14
+ def listify(x: Union[str, List[str]]) -> List[str]:
15
+ if isinstance(x, list):
16
+ return x
17
+ return [x]
18
+
19
+
20
+ def stringify(x: Union[str, List[str]]) -> str:
21
+ if isinstance(x, list):
22
+ return x[0]
23
+ return x
24
+
25
+
26
+ class Secret(object):
27
+ """
28
+ SingleStoreDB secrets definition.
29
+
30
+ This object is not directly instantiated. It is used in results
31
+ of API calls on the :class:`Organization`. See :meth:`Organization.get_secret`.
32
+ """
33
+
34
+ def __init__(
35
+ self,
36
+ id: str,
37
+ name: str,
38
+ value: str,
39
+ created_by: str,
40
+ created_at: Union[str, datetime.datetime],
41
+ last_updated_by: str,
42
+ last_updated_at: Union[str, datetime.datetime],
43
+ deleted_by: Optional[str] = None,
44
+ deleted_at: Optional[Union[str, datetime.datetime]] = None,
45
+ ):
46
+ # UUID of the secret
47
+ self.id = id
48
+
49
+ # Name of the secret
50
+ self.name = name
51
+
52
+ # Value of the secret
53
+ self.value = value
54
+
55
+ # User who created the secret
56
+ self.created_by = created_by
57
+
58
+ # Time when the secret was created
59
+ self.created_at = created_at
60
+
61
+ # UUID of the user who last updated the secret
62
+ self.last_updated_by = last_updated_by
63
+
64
+ # Time when the secret was last updated
65
+ self.last_updated_at = last_updated_at
66
+
67
+ # UUID of the user who deleted the secret
68
+ self.deleted_by = deleted_by
69
+
70
+ # Time when the secret was deleted
71
+ self.deleted_at = deleted_at
72
+
73
+ @classmethod
74
+ def from_dict(cls, obj: Dict[str, str]) -> 'Secret':
75
+ """
76
+ Construct a Secret from a dictionary of values.
77
+
78
+ Parameters
79
+ ----------
80
+ obj : dict
81
+ Dictionary of values
82
+
83
+ Returns
84
+ -------
85
+ :class:`Secret`
86
+
87
+ """
88
+ out = cls(
89
+ id=obj['secretID'],
90
+ name=obj['name'],
91
+ value=obj['value'],
92
+ created_by=obj['createdBy'],
93
+ created_at=obj['createdAt'],
94
+ last_updated_by=obj['lastUpdatedBy'],
95
+ last_updated_at=obj['lastUpdatedAt'],
96
+ deleted_by=obj.get('deletedBy'),
97
+ deleted_at=obj.get('deletedAt'),
98
+ )
99
+
100
+ return out
101
+
102
+ def __str__(self) -> str:
103
+ """Return string representation."""
104
+ return vars_to_str(self)
105
+
106
+ def __repr__(self) -> str:
107
+ """Return string representation."""
108
+ return str(self)
109
+
110
+
111
+ class Organization(object):
112
+ """
113
+ Organization in SingleStoreDB Cloud portal.
114
+
115
+ This object is not directly instantiated. It is used in results
116
+ of ``WorkspaceManager`` API calls.
117
+
118
+ See Also
119
+ --------
120
+ :attr:`WorkspaceManager.organization`
121
+
122
+ """
123
+
124
+ def __init__(self, id: str, name: str, firewall_ranges: List[str]):
125
+ """Use :attr:`WorkspaceManager.organization` instead."""
126
+ #: Unique ID of the organization
127
+ self.id = id
128
+
129
+ #: Name of the organization
130
+ self.name = name
131
+
132
+ #: Firewall ranges of the organization
133
+ self.firewall_ranges = list(firewall_ranges)
134
+
135
+ self._manager: Optional[Manager] = None
136
+
137
+ def __str__(self) -> str:
138
+ """Return string representation."""
139
+ return vars_to_str(self)
140
+
141
+ def __repr__(self) -> str:
142
+ """Return string representation."""
143
+ return str(self)
144
+
145
+ def get_secret(self, name: str) -> Secret:
146
+ if self._manager is None:
147
+ raise ManagementError(msg='Organization not initialized')
148
+
149
+ res = self._manager._get('secrets', params=dict(name=name))
150
+
151
+ secrets = [Secret.from_dict(item) for item in res.json()['secrets']]
152
+
153
+ if len(secrets) == 0:
154
+ raise ManagementError(msg=f'Secret {name} not found')
155
+
156
+ if len(secrets) > 1:
157
+ raise ManagementError(msg=f'Multiple secrets found for {name}')
158
+
159
+ return secrets[0]
160
+
161
+ @classmethod
162
+ def from_dict(
163
+ cls,
164
+ obj: Dict[str, Union[str, List[str]]],
165
+ manager: Manager,
166
+ ) -> 'Organization':
167
+ """
168
+ Convert dictionary to an ``Organization`` object.
169
+
170
+ Parameters
171
+ ----------
172
+ obj : dict
173
+ Key-value pairs to retrieve organization information from
174
+ manager : WorkspaceManager, optional
175
+ The WorkspaceManager the Organization belongs to
176
+
177
+ Returns
178
+ -------
179
+ :class:`Organization`
180
+
181
+ """
182
+ out = cls(
183
+ id=stringify(obj['orgID']),
184
+ name=stringify(obj.get('name', '<unknown>')),
185
+ firewall_ranges=listify(obj.get('firewallRanges', [])),
186
+ )
187
+ out._manager = manager
188
+ return out
@@ -12,16 +12,16 @@ class Region(object):
12
12
  Cluster region information.
13
13
 
14
14
  This object is not directly instantiated. It is used in results
15
- of `ClusterManager` API calls.
15
+ of ``WorkspaceManager`` API calls.
16
16
 
17
17
  See Also
18
18
  --------
19
- :attr:`ClusterManager.regions`
19
+ :attr:`WorkspaceManager.regions`
20
20
 
21
21
  """
22
22
 
23
23
  def __init__(self, id: str, name: str, provider: str):
24
- """Use :attr:`ClusterManager.regions` instead."""
24
+ """Use :attr:`WorkspaceManager.regions` instead."""
25
25
  #: Unique ID of the region
26
26
  self.id = id
27
27
 
@@ -50,8 +50,8 @@ class Region(object):
50
50
  ----------
51
51
  obj : dict
52
52
  Key-value pairs to retrieve region information from
53
- manager : ClusterManager, optional
54
- The ClusterManager the Region belongs to
53
+ manager : WorkspaceManager, optional
54
+ The WorkspaceManager the Region belongs to
55
55
 
56
56
  Returns
57
57
  -------