singlestoredb 0.3.3__py3-none-any.whl → 1.0.3__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 (121) hide show
  1. singlestoredb/__init__.py +33 -2
  2. singlestoredb/alchemy/__init__.py +90 -0
  3. singlestoredb/auth.py +6 -4
  4. singlestoredb/config.py +116 -16
  5. singlestoredb/connection.py +489 -523
  6. singlestoredb/converters.py +275 -26
  7. singlestoredb/exceptions.py +30 -4
  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/__init__.py +0 -0
  22. singlestoredb/fusion/handlers/stage.py +257 -0
  23. singlestoredb/fusion/handlers/utils.py +162 -0
  24. singlestoredb/fusion/handlers/workspace.py +412 -0
  25. singlestoredb/fusion/registry.py +164 -0
  26. singlestoredb/fusion/result.py +399 -0
  27. singlestoredb/http/__init__.py +27 -0
  28. singlestoredb/http/connection.py +1192 -0
  29. singlestoredb/management/__init__.py +3 -2
  30. singlestoredb/management/billing_usage.py +148 -0
  31. singlestoredb/management/cluster.py +19 -14
  32. singlestoredb/management/manager.py +100 -40
  33. singlestoredb/management/organization.py +188 -0
  34. singlestoredb/management/region.py +6 -8
  35. singlestoredb/management/utils.py +253 -4
  36. singlestoredb/management/workspace.py +1153 -35
  37. singlestoredb/mysql/__init__.py +177 -0
  38. singlestoredb/mysql/_auth.py +298 -0
  39. singlestoredb/mysql/charset.py +214 -0
  40. singlestoredb/mysql/connection.py +1814 -0
  41. singlestoredb/mysql/constants/CLIENT.py +38 -0
  42. singlestoredb/mysql/constants/COMMAND.py +32 -0
  43. singlestoredb/mysql/constants/CR.py +78 -0
  44. singlestoredb/mysql/constants/ER.py +474 -0
  45. singlestoredb/mysql/constants/FIELD_TYPE.py +32 -0
  46. singlestoredb/mysql/constants/FLAG.py +15 -0
  47. singlestoredb/mysql/constants/SERVER_STATUS.py +10 -0
  48. singlestoredb/mysql/constants/__init__.py +0 -0
  49. singlestoredb/mysql/converters.py +271 -0
  50. singlestoredb/mysql/cursors.py +713 -0
  51. singlestoredb/mysql/err.py +92 -0
  52. singlestoredb/mysql/optionfile.py +20 -0
  53. singlestoredb/mysql/protocol.py +388 -0
  54. singlestoredb/mysql/tests/__init__.py +19 -0
  55. singlestoredb/mysql/tests/base.py +126 -0
  56. singlestoredb/mysql/tests/conftest.py +37 -0
  57. singlestoredb/mysql/tests/test_DictCursor.py +132 -0
  58. singlestoredb/mysql/tests/test_SSCursor.py +141 -0
  59. singlestoredb/mysql/tests/test_basic.py +452 -0
  60. singlestoredb/mysql/tests/test_connection.py +851 -0
  61. singlestoredb/mysql/tests/test_converters.py +58 -0
  62. singlestoredb/mysql/tests/test_cursor.py +141 -0
  63. singlestoredb/mysql/tests/test_err.py +16 -0
  64. singlestoredb/mysql/tests/test_issues.py +514 -0
  65. singlestoredb/mysql/tests/test_load_local.py +75 -0
  66. singlestoredb/mysql/tests/test_nextset.py +88 -0
  67. singlestoredb/mysql/tests/test_optionfile.py +27 -0
  68. singlestoredb/mysql/tests/thirdparty/__init__.py +6 -0
  69. singlestoredb/mysql/tests/thirdparty/test_MySQLdb/__init__.py +9 -0
  70. singlestoredb/mysql/tests/thirdparty/test_MySQLdb/capabilities.py +323 -0
  71. singlestoredb/mysql/tests/thirdparty/test_MySQLdb/dbapi20.py +865 -0
  72. singlestoredb/mysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_capabilities.py +110 -0
  73. singlestoredb/mysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_dbapi20.py +224 -0
  74. singlestoredb/mysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_nonstandard.py +101 -0
  75. singlestoredb/mysql/times.py +23 -0
  76. singlestoredb/pytest.py +283 -0
  77. singlestoredb/tests/empty.sql +0 -0
  78. singlestoredb/tests/ext_funcs/__init__.py +385 -0
  79. singlestoredb/tests/test.sql +210 -0
  80. singlestoredb/tests/test2.sql +1 -0
  81. singlestoredb/tests/test_basics.py +482 -117
  82. singlestoredb/tests/test_config.py +13 -15
  83. singlestoredb/tests/test_connection.py +241 -289
  84. singlestoredb/tests/test_dbapi.py +27 -0
  85. singlestoredb/tests/test_exceptions.py +0 -2
  86. singlestoredb/tests/test_ext_func.py +1193 -0
  87. singlestoredb/tests/test_ext_func_data.py +1101 -0
  88. singlestoredb/tests/test_fusion.py +465 -0
  89. singlestoredb/tests/test_http.py +32 -28
  90. singlestoredb/tests/test_management.py +588 -10
  91. singlestoredb/tests/test_plugin.py +33 -0
  92. singlestoredb/tests/test_results.py +11 -14
  93. singlestoredb/tests/test_types.py +0 -2
  94. singlestoredb/tests/test_udf.py +687 -0
  95. singlestoredb/tests/test_xdict.py +0 -2
  96. singlestoredb/tests/utils.py +3 -4
  97. singlestoredb/types.py +4 -5
  98. singlestoredb/utils/config.py +71 -12
  99. singlestoredb/utils/convert_rows.py +0 -2
  100. singlestoredb/utils/debug.py +13 -0
  101. singlestoredb/utils/mogrify.py +151 -0
  102. singlestoredb/utils/results.py +4 -3
  103. singlestoredb/utils/xdict.py +12 -12
  104. singlestoredb-1.0.3.dist-info/METADATA +139 -0
  105. singlestoredb-1.0.3.dist-info/RECORD +112 -0
  106. {singlestoredb-0.3.3.dist-info → singlestoredb-1.0.3.dist-info}/WHEEL +1 -1
  107. singlestoredb-1.0.3.dist-info/entry_points.txt +2 -0
  108. singlestoredb/drivers/__init__.py +0 -46
  109. singlestoredb/drivers/base.py +0 -200
  110. singlestoredb/drivers/cymysql.py +0 -40
  111. singlestoredb/drivers/http.py +0 -49
  112. singlestoredb/drivers/mariadb.py +0 -42
  113. singlestoredb/drivers/mysqlconnector.py +0 -51
  114. singlestoredb/drivers/mysqldb.py +0 -62
  115. singlestoredb/drivers/pymysql.py +0 -39
  116. singlestoredb/drivers/pyodbc.py +0 -67
  117. singlestoredb/http.py +0 -794
  118. singlestoredb-0.3.3.dist-info/METADATA +0 -105
  119. singlestoredb-0.3.3.dist-info/RECORD +0 -46
  120. {singlestoredb-0.3.3.dist-info → singlestoredb-1.0.3.dist-info}/LICENSE +0 -0
  121. {singlestoredb-0.3.3.dist-info → singlestoredb-1.0.3.dist-info}/top_level.txt +0 -0
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env python
2
- from __future__ import annotations
3
-
4
2
  from .cluster import manage_cluster
3
+ from .manager import get_organization
4
+ from .manager import get_token
5
+ from .workspace import get_secret
5
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
@@ -1,10 +1,7 @@
1
1
  #!/usr/bin/env python
2
2
  """SingleStoreDB Cluster Management."""
3
- from __future__ import annotations
4
-
5
3
  import datetime
6
4
  import warnings
7
- from collections.abc import Sequence
8
5
  from typing import Any
9
6
  from typing import Dict
10
7
  from typing import List
@@ -15,6 +12,7 @@ from .. import connection
15
12
  from ..exceptions import ManagementError
16
13
  from .manager import Manager
17
14
  from .region import Region
15
+ from .utils import NamedList
18
16
  from .utils import to_datetime
19
17
  from .utils import vars_to_str
20
18
 
@@ -41,7 +39,7 @@ class Cluster(object):
41
39
  units: float, state: str, version: str,
42
40
  created_at: Union[str, datetime.datetime],
43
41
  expires_at: Optional[Union[str, datetime.datetime]] = None,
44
- firewall_ranges: Optional[Sequence[str]] = None,
42
+ firewall_ranges: Optional[List[str]] = None,
45
43
  terminated_at: Optional[Union[str, datetime.datetime]] = None,
46
44
  endpoint: Optional[str] = None,
47
45
  ):
@@ -94,7 +92,7 @@ class Cluster(object):
94
92
  return str(self)
95
93
 
96
94
  @classmethod
97
- def from_dict(cls, obj: Dict[str, Any], manager: 'ClusterManager') -> Cluster:
95
+ def from_dict(cls, obj: Dict[str, Any], manager: 'ClusterManager') -> 'Cluster':
98
96
  """
99
97
  Construct a Cluster from a dictionary of values.
100
98
 
@@ -123,7 +121,7 @@ class Cluster(object):
123
121
  out._manager = manager
124
122
  return out
125
123
 
126
- def refresh(self) -> Cluster:
124
+ def refresh(self) -> 'Cluster':
127
125
  """Update the object to the current state."""
128
126
  if self._manager is None:
129
127
  raise ManagementError(
@@ -138,7 +136,7 @@ class Cluster(object):
138
136
  self, name: Optional[str] = None,
139
137
  admin_password: Optional[str] = None,
140
138
  expires_at: Optional[str] = None,
141
- size: Optional[str] = None, firewall_ranges: Optional[Sequence[str]] = None,
139
+ size: Optional[str] = None, firewall_ranges: Optional[List[str]] = None,
142
140
  ) -> None:
143
141
  """
144
142
  Update the cluster definition.
@@ -340,20 +338,20 @@ class ClusterManager(Manager):
340
338
  obj_type = 'cluster'
341
339
 
342
340
  @property
343
- def clusters(self) -> List[Cluster]:
341
+ def clusters(self) -> NamedList[Cluster]:
344
342
  """Return a list of available clusters."""
345
343
  res = self._get('clusters')
346
- return [Cluster.from_dict(item, self) for item in res.json()]
344
+ return NamedList([Cluster.from_dict(item, self) for item in res.json()])
347
345
 
348
346
  @property
349
- def regions(self) -> List[Region]:
347
+ def regions(self) -> NamedList[Region]:
350
348
  """Return a list of available regions."""
351
349
  res = self._get('regions')
352
- return [Region.from_dict(item, self) for item in res.json()]
350
+ return NamedList([Region.from_dict(item, self) for item in res.json()])
353
351
 
354
352
  def create_cluster(
355
353
  self, name: str, region: Union[str, Region], admin_password: str,
356
- firewall_ranges: Sequence[str], expires_at: Optional[str] = None,
354
+ firewall_ranges: List[str], expires_at: Optional[str] = None,
357
355
  size: Optional[str] = None, plan: Optional[str] = None,
358
356
  wait_on_active: bool = False, wait_timeout: int = 600,
359
357
  wait_interval: int = 20,
@@ -429,6 +427,8 @@ def manage_cluster(
429
427
  access_token: Optional[str] = None,
430
428
  version: str = ClusterManager.default_version,
431
429
  base_url: str = ClusterManager.default_base_url,
430
+ *,
431
+ organization_id: Optional[str] = None,
432
432
  ) -> ClusterManager:
433
433
  """
434
434
  Retrieve a SingleStoreDB cluster manager.
@@ -441,6 +441,8 @@ def manage_cluster(
441
441
  Version of the API to use
442
442
  base_url : str, optional
443
443
  Base URL of the cluster management API
444
+ organization_id: str, optional
445
+ ID of organization, if using a JWT for authentication
444
446
 
445
447
  Returns
446
448
  -------
@@ -449,7 +451,10 @@ def manage_cluster(
449
451
  """
450
452
  warnings.warn(
451
453
  'The cluster management API is deprecated; '
452
- 'use manage_workspace instead.',
454
+ 'use manage_workspaces instead.',
453
455
  category=DeprecationWarning,
454
456
  )
455
- 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,7 +1,7 @@
1
1
  #!/usr/bin/env python
2
2
  """SingleStoreDB Base Manager."""
3
- from __future__ import annotations
4
-
3
+ import os
4
+ import sys
5
5
  import time
6
6
  from typing import Any
7
7
  from typing import Dict
@@ -10,44 +10,73 @@ from typing import Optional
10
10
  from typing import Union
11
11
  from urllib.parse import urljoin
12
12
 
13
+ import jwt
13
14
  import requests
14
15
 
15
16
  from .. import config
16
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
17
41
 
18
42
 
19
43
  class Manager(object):
20
44
  """SingleStoreDB manager base class."""
21
45
 
22
46
  #: Management API version if none is specified.
23
- default_version = 'v1'
47
+ default_version = config.get_option('management.version')
24
48
 
25
49
  #: Base URL if none is specified.
26
- default_base_url = 'https://api.singlestore.com'
50
+ default_base_url = config.get_option('management.base_url')
27
51
 
28
52
  #: Object type
29
53
  obj_type = ''
30
54
 
31
55
  def __init__(
32
56
  self, access_token: Optional[str] = None, version: Optional[str] = None,
33
- base_url: Optional[str] = None,
57
+ base_url: Optional[str] = None, *, organization_id: Optional[str] = None,
34
58
  ):
35
- access_token = (
36
- access_token or
37
- config.get_option('management.token')
59
+ from .. import __version__ as client_version
60
+ new_access_token = (
61
+ access_token or get_token()
38
62
  )
39
- if not access_token:
63
+ if not new_access_token:
40
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)
41
66
  self._sess = requests.Session()
42
67
  self._sess.headers.update({
43
- 'Authorization': f'Bearer {access_token}',
68
+ 'Authorization': f'Bearer {new_access_token}',
44
69
  'Content-Type': 'application/json',
45
70
  'Accept': 'application/json',
71
+ 'User-Agent': f'SingleStoreDB-Python/{client_version}',
46
72
  })
47
73
  self._base_url = urljoin(
48
74
  base_url or type(self).default_base_url,
49
75
  version or type(self).default_version,
50
76
  ) + '/'
77
+ self._params: Dict[str, str] = {}
78
+ if organization_id:
79
+ self._params['organizationID'] = organization_id
51
80
 
52
81
  def _check(
53
82
  self, res: requests.Response, url: str, params: Dict[str, Any],
@@ -65,6 +94,8 @@ class Manager(object):
65
94
  requests.Response
66
95
 
67
96
  """
97
+ if config.get_option('debug.queries'):
98
+ print(os.path.join(self._base_url, url), params, file=sys.stderr)
68
99
  if res.status_code >= 400:
69
100
  txt = res.text.strip()
70
101
  msg = f'{txt}: /{url}'
@@ -72,12 +103,27 @@ class Manager(object):
72
103
  new_params = params.copy()
73
104
  if 'json' in new_params:
74
105
  for k, v in new_params['json'].items():
75
- if 'password' in k.lower():
106
+ if 'password' in k.lower() and v:
76
107
  new_params['json'][k] = '*' * len(v)
77
108
  msg += ': {}'.format(str(new_params))
78
- raise ManagementError(errno=res.status_code, msg=msg)
109
+ raise ManagementError(errno=res.status_code, msg=msg, response=txt)
79
110
  return res
80
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
+
81
127
  def _get(self, path: str, *args: Any, **kwargs: Any) -> requests.Response:
82
128
  """
83
129
  Invoke a GET request.
@@ -96,13 +142,10 @@ class Manager(object):
96
142
  requests.Response
97
143
 
98
144
  """
99
- return self._check(
100
- self._sess.get(
101
- urljoin(self._base_url, path),
102
- *args, **kwargs,
103
- ),
104
- path, kwargs,
105
- )
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)
106
149
 
107
150
  def _post(self, path: str, *args: Any, **kwargs: Any) -> requests.Response:
108
151
  """
@@ -122,13 +165,33 @@ class Manager(object):
122
165
  requests.Response
123
166
 
124
167
  """
125
- return self._check(
126
- self._sess.post(
127
- urljoin(self._base_url, path),
128
- *args, **kwargs,
129
- ),
130
- path, kwargs,
131
- )
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)
132
195
 
133
196
  def _delete(self, path: str, *args: Any, **kwargs: Any) -> requests.Response:
134
197
  """
@@ -148,13 +211,10 @@ class Manager(object):
148
211
  requests.Response
149
212
 
150
213
  """
151
- return self._check(
152
- self._sess.delete(
153
- urljoin(self._base_url, path),
154
- *args, **kwargs,
155
- ),
156
- path, kwargs,
157
- )
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)
158
218
 
159
219
  def _patch(self, path: str, *args: Any, **kwargs: Any) -> requests.Response:
160
220
  """
@@ -174,13 +234,10 @@ class Manager(object):
174
234
  requests.Response
175
235
 
176
236
  """
177
- return self._check(
178
- self._sess.patch(
179
- urljoin(self._base_url, path),
180
- *args, **kwargs,
181
- ),
182
- path, kwargs,
183
- )
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)
184
241
 
185
242
  def _wait_on_state(
186
243
  self,
@@ -217,12 +274,14 @@ class Manager(object):
217
274
  x.lower().strip()
218
275
  for x in (isinstance(state, str) and [state] or state)
219
276
  ]
277
+
220
278
  if getattr(out, 'state', None) is None:
221
279
  raise ManagementError(
222
280
  msg='{} object does not have a `state` attribute'.format(
223
281
  type(out).__name__,
224
282
  ),
225
283
  )
284
+
226
285
  while True:
227
286
  if getattr(out, 'state').lower() in states:
228
287
  break
@@ -234,4 +293,5 @@ class Manager(object):
234
293
  time.sleep(interval)
235
294
  timeout -= interval
236
295
  out = getattr(self, f'get_{self.obj_type}')(out.id)
296
+
237
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