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,1599 @@
1
+ #!/usr/bin/env python
2
+ # type: ignore
3
+ """SingleStoreDB Management API testing."""
4
+ import os
5
+ import pathlib
6
+ import random
7
+ import re
8
+ import secrets
9
+ import unittest
10
+
11
+ import pytest
12
+
13
+ import singlestoredb as s2
14
+ from singlestoredb.management.job import Status
15
+ from singlestoredb.management.job import TargetType
16
+ from singlestoredb.management.region import Region
17
+ from singlestoredb.management.utils import NamedList
18
+
19
+
20
+ TEST_DIR = pathlib.Path(os.path.dirname(__file__))
21
+
22
+
23
+ def clean_name(s):
24
+ """Change all non-word characters to -."""
25
+ return re.sub(r'[^\w]', r'-', s).replace('_', '-').lower()
26
+
27
+
28
+ def shared_database_name(s):
29
+ """Return a shared database name. Cannot contain special characters except -"""
30
+ return re.sub(r'[^\w]', '', s).replace('-', '_').lower()
31
+
32
+
33
+ @pytest.mark.skip(reason='Legacy cluster Management API is going away')
34
+ @pytest.mark.management
35
+ class TestCluster(unittest.TestCase):
36
+
37
+ manager = None
38
+ cluster = None
39
+ password = None
40
+
41
+ @classmethod
42
+ def setUpClass(cls):
43
+ cls.manager = s2.manage_cluster()
44
+
45
+ us_regions = [x for x in cls.manager.regions if 'US' in x.name]
46
+ cls.password = secrets.token_urlsafe(20) + '-x&$'
47
+
48
+ cls.cluster = cls.manager.create_cluster(
49
+ clean_name('cm-test-{}'.format(secrets.token_urlsafe(20)[:20])),
50
+ region=random.choice(us_regions).id,
51
+ admin_password=cls.password,
52
+ firewall_ranges=['0.0.0.0/0'],
53
+ expires_at='1h',
54
+ size='S-00',
55
+ wait_on_active=True,
56
+ )
57
+
58
+ @classmethod
59
+ def tearDownClass(cls):
60
+ if cls.cluster is not None:
61
+ cls.cluster.terminate()
62
+ cls.cluster = None
63
+ cls.manager = None
64
+ cls.password = None
65
+
66
+ def test_str(self):
67
+ assert self.cluster.name in str(self.cluster.name)
68
+
69
+ def test_repr(self):
70
+ assert repr(self.cluster) == str(self.cluster)
71
+
72
+ def test_region_str(self):
73
+ s = str(self.cluster.region)
74
+ assert 'Azure' in s or 'GCP' in s or 'AWS' in s, s
75
+
76
+ def test_region_repr(self):
77
+ assert repr(self.cluster.region) == str(self.cluster.region)
78
+
79
+ def test_regions(self):
80
+ out = self.manager.regions
81
+ providers = {x.provider for x in out}
82
+ names = [x.name for x in out]
83
+ assert 'Azure' in providers, providers
84
+ assert 'GCP' in providers, providers
85
+ assert 'AWS' in providers, providers
86
+
87
+ objs = {}
88
+ ids = []
89
+ for item in out:
90
+ ids.append(item.id)
91
+ objs[item.id] = item
92
+ if item.name not in objs:
93
+ objs[item.name] = item
94
+
95
+ name = random.choice(names)
96
+ assert out[name] == objs[name]
97
+ id = random.choice(ids)
98
+ assert out[id] == objs[id]
99
+
100
+ def test_clusters(self):
101
+ clusters = self.manager.clusters
102
+ ids = [x.id for x in clusters]
103
+ assert self.cluster.id in ids, ids
104
+
105
+ def test_get_cluster(self):
106
+ clus = self.manager.get_cluster(self.cluster.id)
107
+ assert clus.id == self.cluster.id, clus.id
108
+
109
+ with self.assertRaises(s2.ManagementError) as cm:
110
+ clus = self.manager.get_cluster('bad id')
111
+
112
+ assert 'UUID' in cm.exception.msg, cm.exception.msg
113
+
114
+ def test_update(self):
115
+ assert self.cluster.name.startswith('cm-test-')
116
+
117
+ name = self.cluster.name.replace('cm-test-', 'cm-foo-')
118
+ self.cluster.update(name=name)
119
+
120
+ clus = self.manager.get_cluster(self.cluster.id)
121
+ assert clus.name == name, clus.name
122
+
123
+ def test_suspend_resume(self):
124
+ trues = ['1', 'on', 'true']
125
+ do_test = os.environ.get('SINGLESTOREDB_TEST_SUSPEND', '0').lower() in trues
126
+
127
+ if not do_test:
128
+ self.skipTest(
129
+ 'Suspend / resume tests skipped by default due to '
130
+ 'being time consuming; set SINGLESTOREDB_TEST_SUSPEND=1 '
131
+ 'to enable',
132
+ )
133
+
134
+ assert self.cluster.state != 'Suspended', self.cluster.state
135
+
136
+ self.cluster.suspend(wait_on_suspended=True)
137
+ assert self.cluster.state == 'Suspended', self.cluster.state
138
+
139
+ self.cluster.resume(wait_on_resumed=True)
140
+ assert self.cluster.state == 'Active', self.cluster.state
141
+
142
+ def test_no_manager(self):
143
+ clus = self.manager.get_cluster(self.cluster.id)
144
+ clus._manager = None
145
+
146
+ with self.assertRaises(s2.ManagementError) as cm:
147
+ clus.refresh()
148
+
149
+ assert 'No cluster manager' in cm.exception.msg, cm.exception.msg
150
+
151
+ with self.assertRaises(s2.ManagementError) as cm:
152
+ clus.update()
153
+
154
+ assert 'No cluster manager' in cm.exception.msg, cm.exception.msg
155
+
156
+ with self.assertRaises(s2.ManagementError) as cm:
157
+ clus.suspend()
158
+
159
+ assert 'No cluster manager' in cm.exception.msg, cm.exception.msg
160
+
161
+ with self.assertRaises(s2.ManagementError) as cm:
162
+ clus.resume()
163
+
164
+ assert 'No cluster manager' in cm.exception.msg, cm.exception.msg
165
+
166
+ with self.assertRaises(s2.ManagementError) as cm:
167
+ clus.terminate()
168
+
169
+ assert 'No cluster manager' in cm.exception.msg, cm.exception.msg
170
+
171
+ def test_connect(self):
172
+ trues = ['1', 'on', 'true']
173
+ pure_python = os.environ.get('SINGLESTOREDB_PURE_PYTHON', '0').lower() in trues
174
+
175
+ self.skipTest('Connection test is disable due to flakey server')
176
+
177
+ if pure_python:
178
+ self.skipTest('Connections through managed service are disabled')
179
+
180
+ try:
181
+ with self.cluster.connect(user='admin', password=self.password) as conn:
182
+ with conn.cursor() as cur:
183
+ cur.execute('show databases')
184
+ assert 'cluster' in [x[0] for x in list(cur)]
185
+ except s2.ManagementError as exc:
186
+ if 'endpoint has not been set' not in str(exc):
187
+ self.skipTest('No endpoint in response. Skipping connection test.')
188
+
189
+ # Test missing endpoint
190
+ clus = self.manager.get_cluster(self.cluster.id)
191
+ clus.endpoint = None
192
+
193
+ with self.assertRaises(s2.ManagementError) as cm:
194
+ clus.connect(user='admin', password=self.password)
195
+
196
+ assert 'endpoint' in cm.exception.msg, cm.exception.msg
197
+
198
+
199
+ @pytest.mark.management
200
+ class TestWorkspace(unittest.TestCase):
201
+
202
+ manager = None
203
+ workspace_group = None
204
+ workspace = None
205
+ password = None
206
+
207
+ @classmethod
208
+ def setUpClass(cls):
209
+ cls.manager = s2.manage_workspaces()
210
+
211
+ us_regions = [x for x in cls.manager.regions if 'US' in x.name]
212
+ cls.password = secrets.token_urlsafe(20) + '-x&$'
213
+
214
+ name = clean_name(secrets.token_urlsafe(20)[:20])
215
+
216
+ cls.workspace_group = cls.manager.create_workspace_group(
217
+ f'wg-test-{name}',
218
+ region=random.choice(us_regions).id,
219
+ admin_password=cls.password,
220
+ firewall_ranges=['0.0.0.0/0'],
221
+ )
222
+
223
+ try:
224
+ cls.workspace = cls.workspace_group.create_workspace(
225
+ f'ws-test-{name}-x',
226
+ wait_on_active=True,
227
+ )
228
+ except Exception:
229
+ cls.workspace_group.terminate(force=True)
230
+ raise
231
+
232
+ @classmethod
233
+ def tearDownClass(cls):
234
+ if cls.workspace_group is not None:
235
+ cls.workspace_group.terminate(force=True)
236
+ cls.workspace_group = None
237
+ cls.workspace = None
238
+ cls.manager = None
239
+ cls.password = None
240
+
241
+ def test_str(self):
242
+ assert self.workspace.name in str(self.workspace.name)
243
+ assert self.workspace_group.name in str(self.workspace_group.name)
244
+
245
+ def test_repr(self):
246
+ assert repr(self.workspace) == str(self.workspace)
247
+ assert repr(self.workspace_group) == str(self.workspace_group)
248
+
249
+ def test_region_str(self):
250
+ s = str(self.workspace_group.region)
251
+ assert 'Azure' in s or 'GCP' in s or 'AWS' in s, s
252
+
253
+ def test_region_repr(self):
254
+ assert repr(self.workspace_group.region) == str(self.workspace_group.region)
255
+
256
+ def test_regions(self):
257
+ out = self.manager.regions
258
+ providers = {x.provider for x in out}
259
+ names = [x.name for x in out]
260
+ assert 'Azure' in providers, providers
261
+ assert 'GCP' in providers, providers
262
+ assert 'AWS' in providers, providers
263
+
264
+ objs = {}
265
+ ids = []
266
+ for item in out:
267
+ ids.append(item.id)
268
+ objs[item.id] = item
269
+ if item.name not in objs:
270
+ objs[item.name] = item
271
+
272
+ name = random.choice(names)
273
+ assert out[name] == objs[name]
274
+ id = random.choice(ids)
275
+ assert out[id] == objs[id]
276
+
277
+ def test_workspace_groups(self):
278
+ workspace_groups = self.manager.workspace_groups
279
+ ids = [x.id for x in workspace_groups]
280
+ names = [x.name for x in workspace_groups]
281
+ assert self.workspace_group.id in ids
282
+ assert self.workspace_group.name in names
283
+
284
+ assert workspace_groups.ids() == ids
285
+ assert workspace_groups.names() == names
286
+
287
+ objs = {}
288
+ for item in workspace_groups:
289
+ objs[item.id] = item
290
+ objs[item.name] = item
291
+
292
+ name = random.choice(names)
293
+ assert workspace_groups[name] == objs[name]
294
+ id = random.choice(ids)
295
+ assert workspace_groups[id] == objs[id]
296
+
297
+ def test_workspaces(self):
298
+ spaces = self.workspace_group.workspaces
299
+ ids = [x.id for x in spaces]
300
+ names = [x.name for x in spaces]
301
+ assert self.workspace.id in ids
302
+ assert self.workspace.name in names
303
+
304
+ assert spaces.ids() == ids
305
+ assert spaces.names() == names
306
+
307
+ objs = {}
308
+ for item in spaces:
309
+ objs[item.id] = item
310
+ objs[item.name] = item
311
+
312
+ name = random.choice(names)
313
+ assert spaces[name] == objs[name]
314
+ id = random.choice(ids)
315
+ assert spaces[id] == objs[id]
316
+
317
+ def test_get_workspace_group(self):
318
+ group = self.manager.get_workspace_group(self.workspace_group.id)
319
+ assert group.id == self.workspace_group.id, group.id
320
+
321
+ with self.assertRaises(s2.ManagementError) as cm:
322
+ group = self.manager.get_workspace_group('bad id')
323
+
324
+ assert 'UUID' in cm.exception.msg, cm.exception.msg
325
+
326
+ def test_get_workspace(self):
327
+ space = self.manager.get_workspace(self.workspace.id)
328
+ assert space.id == self.workspace.id, space.id
329
+
330
+ with self.assertRaises(s2.ManagementError) as cm:
331
+ space = self.manager.get_workspace('bad id')
332
+
333
+ assert 'UUID' in cm.exception.msg, cm.exception.msg
334
+
335
+ def test_update(self):
336
+ assert self.workspace_group.name.startswith('wg-test-')
337
+
338
+ name = self.workspace_group.name.replace('wg-test-', 'wg-foo-')
339
+ self.workspace_group.update(name=name)
340
+
341
+ group = self.manager.get_workspace_group(self.workspace_group.id)
342
+ assert group.name == name, group.name
343
+
344
+ def test_no_manager(self):
345
+ space = self.manager.get_workspace(self.workspace.id)
346
+ space._manager = None
347
+
348
+ with self.assertRaises(s2.ManagementError) as cm:
349
+ space.refresh()
350
+
351
+ assert 'No workspace manager' in cm.exception.msg, cm.exception.msg
352
+
353
+ with self.assertRaises(s2.ManagementError) as cm:
354
+ space.terminate()
355
+
356
+ assert 'No workspace manager' in cm.exception.msg, cm.exception.msg
357
+
358
+ def test_connect(self):
359
+ with self.workspace.connect(user='admin', password=self.password) as conn:
360
+ with conn.cursor() as cur:
361
+ cur.execute('show databases')
362
+ assert 'cluster' in [x[0] for x in list(cur)]
363
+
364
+ # Test missing endpoint
365
+ space = self.manager.get_workspace(self.workspace.id)
366
+ space.endpoint = None
367
+
368
+ with self.assertRaises(s2.ManagementError) as cm:
369
+ space.connect(user='admin', password=self.password)
370
+
371
+ assert 'endpoint' in cm.exception.msg, cm.exception.msg
372
+
373
+
374
+ @pytest.mark.management
375
+ class TestStarterWorkspace(unittest.TestCase):
376
+
377
+ manager = None
378
+ starter_workspace = None
379
+
380
+ @classmethod
381
+ def setUpClass(cls):
382
+ cls.manager = s2.manage_workspaces()
383
+
384
+ shared_tier_regions: NamedList[Region] = [
385
+ x for x in cls.manager.shared_tier_regions if 'US' in x.name
386
+ ]
387
+ cls.starter_username = 'starter_user'
388
+ cls.password = secrets.token_urlsafe(20)
389
+
390
+ name = shared_database_name(secrets.token_urlsafe(20)[:20])
391
+
392
+ cls.database_name = f'starter_db_{name}'
393
+
394
+ shared_tier_region: Region = random.choice(shared_tier_regions)
395
+
396
+ if not shared_tier_region:
397
+ raise ValueError('No shared tier regions found')
398
+
399
+ cls.starter_workspace = cls.manager.create_starter_workspace(
400
+ f'starter-ws-test-{name}',
401
+ database_name=cls.database_name,
402
+ provider=shared_tier_region.provider,
403
+ region_name=shared_tier_region.region_name,
404
+ )
405
+
406
+ cls.starter_workspace.create_user(
407
+ username=cls.starter_username,
408
+ password=cls.password,
409
+ )
410
+
411
+ @classmethod
412
+ def tearDownClass(cls):
413
+ if cls.starter_workspace is not None:
414
+ cls.starter_workspace.terminate()
415
+ cls.manager = None
416
+ cls.password = None
417
+
418
+ def test_str(self):
419
+ assert self.starter_workspace.name in str(self.starter_workspace.name)
420
+
421
+ def test_repr(self):
422
+ assert repr(self.starter_workspace) == str(self.starter_workspace)
423
+
424
+ def test_get_starter_workspace(self):
425
+ workspace = self.manager.get_starter_workspace(self.starter_workspace.id)
426
+ assert workspace.id == self.starter_workspace.id, workspace.id
427
+
428
+ with self.assertRaises(s2.ManagementError) as cm:
429
+ workspace = self.manager.get_starter_workspace('bad id')
430
+
431
+ assert 'UUID' in cm.exception.msg, cm.exception.msg
432
+
433
+ def test_starter_workspaces(self):
434
+ workspaces = self.manager.starter_workspaces
435
+ ids = [x.id for x in workspaces]
436
+ names = [x.name for x in workspaces]
437
+ assert self.starter_workspace.id in ids
438
+ assert self.starter_workspace.name in names
439
+
440
+ objs = {}
441
+ for item in workspaces:
442
+ objs[item.id] = item
443
+ objs[item.name] = item
444
+
445
+ name = random.choice(names)
446
+ assert workspaces[name] == objs[name]
447
+ id = random.choice(ids)
448
+ assert workspaces[id] == objs[id]
449
+
450
+ def test_no_manager(self):
451
+ workspace = self.manager.get_starter_workspace(self.starter_workspace.id)
452
+ workspace._manager = None
453
+
454
+ with self.assertRaises(s2.ManagementError) as cm:
455
+ workspace.refresh()
456
+
457
+ assert 'No workspace manager' in cm.exception.msg, cm.exception.msg
458
+
459
+ with self.assertRaises(s2.ManagementError) as cm:
460
+ workspace.terminate()
461
+
462
+ assert 'No workspace manager' in cm.exception.msg, cm.exception.msg
463
+
464
+ def test_connect(self):
465
+ with self.starter_workspace.connect(
466
+ user=self.starter_username,
467
+ password=self.password,
468
+ ) as conn:
469
+ with conn.cursor() as cur:
470
+ cur.execute('show databases')
471
+ assert self.database_name in [x[0] for x in list(cur)]
472
+
473
+ # Test missing endpoint
474
+ workspace = self.manager.get_starter_workspace(self.starter_workspace.id)
475
+ workspace.endpoint = None
476
+
477
+ with self.assertRaises(s2.ManagementError) as cm:
478
+ workspace.connect(user=self.starter_username, password=self.password)
479
+
480
+ assert 'endpoint' in cm.exception.msg, cm.exception.msg
481
+
482
+
483
+ @pytest.mark.management
484
+ class TestStage(unittest.TestCase):
485
+
486
+ manager = None
487
+ wg = None
488
+ password = None
489
+
490
+ @classmethod
491
+ def setUpClass(cls):
492
+ cls.manager = s2.manage_workspaces()
493
+
494
+ us_regions = [x for x in cls.manager.regions if 'US' in x.name]
495
+ cls.password = secrets.token_urlsafe(20) + '-x&$'
496
+
497
+ name = clean_name(secrets.token_urlsafe(20)[:20])
498
+
499
+ cls.wg = cls.manager.create_workspace_group(
500
+ f'wg-test-{name}',
501
+ region=random.choice(us_regions).id,
502
+ admin_password=cls.password,
503
+ firewall_ranges=['0.0.0.0/0'],
504
+ )
505
+
506
+ @classmethod
507
+ def tearDownClass(cls):
508
+ if cls.wg is not None:
509
+ cls.wg.terminate(force=True)
510
+ cls.wg = None
511
+ cls.manager = None
512
+ cls.password = None
513
+
514
+ def test_upload_file(self):
515
+ st = self.wg.stage
516
+
517
+ upload_test_sql = f'upload_test_{id(self)}.sql'
518
+ upload_test2_sql = f'upload_test2_{id(self)}.sql'
519
+
520
+ root = st.info('/')
521
+ assert str(root.path) == '/'
522
+ assert root.type == 'directory'
523
+
524
+ # Upload file
525
+ f = st.upload_file(TEST_DIR / 'test.sql', upload_test_sql)
526
+ assert str(f.path) == upload_test_sql
527
+ assert f.type == 'file'
528
+
529
+ # Download and compare to original
530
+ txt = f.download(encoding='utf-8')
531
+ assert txt == open(TEST_DIR / 'test.sql').read()
532
+
533
+ # Make sure we can't overwrite
534
+ with self.assertRaises(OSError):
535
+ st.upload_file(TEST_DIR / 'test.sql', upload_test_sql)
536
+
537
+ # Force overwrite with new content; use file object this time
538
+ f = st.upload_file(
539
+ open(TEST_DIR / 'test2.sql', 'r'),
540
+ upload_test_sql,
541
+ overwrite=True,
542
+ )
543
+ assert str(f.path) == upload_test_sql
544
+ assert f.type == 'file'
545
+
546
+ # Verify new content
547
+ txt = f.download(encoding='utf-8')
548
+ assert txt == open(TEST_DIR / 'test2.sql').read()
549
+
550
+ # Try to upload folder
551
+ with self.assertRaises(IsADirectoryError):
552
+ st.upload_file(TEST_DIR, 'test3.sql')
553
+
554
+ lib = st.mkdir('/lib/')
555
+ assert str(lib.path) == 'lib/'
556
+ assert lib.type == 'directory'
557
+
558
+ # Try to overwrite stage folder with file
559
+ with self.assertRaises(IsADirectoryError):
560
+ st.upload_file(TEST_DIR / 'test2.sql', lib.path, overwrite=True)
561
+
562
+ # Write file into folder
563
+ f = st.upload_file(
564
+ TEST_DIR / 'test2.sql',
565
+ os.path.join(lib.path, upload_test2_sql),
566
+ )
567
+ assert str(f.path) == 'lib/' + upload_test2_sql
568
+ assert f.type == 'file'
569
+
570
+ def test_open(self):
571
+ st = self.wg.stage
572
+
573
+ open_test_sql = f'open_test_{id(self)}.sql'
574
+
575
+ # See if error is raised for non-existent file
576
+ with self.assertRaises(s2.ManagementError):
577
+ st.open(open_test_sql, 'r')
578
+
579
+ # Load test file
580
+ st.upload_file(TEST_DIR / 'test.sql', open_test_sql)
581
+
582
+ # Read file using `open`
583
+ with st.open(open_test_sql, 'r') as rfile:
584
+ assert rfile.read() == open(TEST_DIR / 'test.sql').read()
585
+
586
+ # Read file using `open` with 'rt' mode
587
+ with st.open(open_test_sql, 'rt') as rfile:
588
+ assert rfile.read() == open(TEST_DIR / 'test.sql').read()
589
+
590
+ # Read file using `open` with 'rb' mode
591
+ with st.open(open_test_sql, 'rb') as rfile:
592
+ assert rfile.read() == open(TEST_DIR / 'test.sql', 'rb').read()
593
+
594
+ # Read file using `open` with 'rb' mode
595
+ with self.assertRaises(ValueError):
596
+ with st.open(open_test_sql, 'b') as rfile:
597
+ pass
598
+
599
+ # Attempt overwrite file using `open` with mode 'x'
600
+ with self.assertRaises(OSError):
601
+ with st.open(open_test_sql, 'x') as wfile:
602
+ pass
603
+
604
+ # Attempt overwrite file using `open` with mode 'w'
605
+ with st.open(open_test_sql, 'w') as wfile:
606
+ wfile.write(open(TEST_DIR / 'test2.sql').read())
607
+
608
+ txt = st.download_file(open_test_sql, encoding='utf-8')
609
+
610
+ assert txt == open(TEST_DIR / 'test2.sql').read()
611
+
612
+ open_raw_test_sql = f'open_raw_test_{id(self)}.sql'
613
+
614
+ # Test writer without context manager
615
+ wfile = st.open(open_raw_test_sql, 'w')
616
+ for line in open(TEST_DIR / 'test.sql'):
617
+ wfile.write(line)
618
+ wfile.close()
619
+
620
+ txt = st.download_file(open_raw_test_sql, encoding='utf-8')
621
+
622
+ assert txt == open(TEST_DIR / 'test.sql').read()
623
+
624
+ # Test reader without context manager
625
+ rfile = st.open(open_raw_test_sql, 'r')
626
+ txt = ''
627
+ for line in rfile:
628
+ txt += line
629
+ rfile.close()
630
+
631
+ assert txt == open(TEST_DIR / 'test.sql').read()
632
+
633
+ def test_obj_open(self):
634
+ st = self.wg.stage
635
+
636
+ obj_open_test_sql = f'obj_open_test_{id(self)}.sql'
637
+ obj_open_dir = f'obj_open_dir_{id(self)}'
638
+
639
+ # Load test file
640
+ f = st.upload_file(TEST_DIR / 'test.sql', obj_open_test_sql)
641
+
642
+ # Read file using `open`
643
+ with f.open() as rfile:
644
+ assert rfile.read() == open(TEST_DIR / 'test.sql').read()
645
+
646
+ # Make sure directories error out
647
+ d = st.mkdir(obj_open_dir)
648
+ with self.assertRaises(IsADirectoryError):
649
+ d.open()
650
+
651
+ # Write file using `open`
652
+ with f.open('w', encoding='utf-8') as wfile:
653
+ wfile.write(open(TEST_DIR / 'test2.sql').read())
654
+
655
+ assert f.download(encoding='utf-8') == open(TEST_DIR / 'test2.sql').read()
656
+
657
+ # Test writer without context manager
658
+ wfile = f.open('w')
659
+ for line in open(TEST_DIR / 'test.sql'):
660
+ wfile.write(line)
661
+ wfile.close()
662
+
663
+ txt = st.download_file(f.path, encoding='utf-8')
664
+
665
+ assert txt == open(TEST_DIR / 'test.sql').read()
666
+
667
+ # Test reader without context manager
668
+ rfile = f.open('r')
669
+ txt = ''
670
+ for line in rfile:
671
+ txt += line
672
+ rfile.close()
673
+
674
+ assert txt == open(TEST_DIR / 'test.sql').read()
675
+
676
+ def test_os_directories(self):
677
+ st = self.wg.stage
678
+
679
+ mkdir_test_1 = f'mkdir_test_1_{id(self)}'
680
+ mkdir_test_2 = f'mkdir_test_2_{id(self)}'
681
+ mkdir_test_3 = f'mkdir_test_3_{id(self)}'
682
+
683
+ # mkdir
684
+ st.mkdir(mkdir_test_1)
685
+ st.mkdir(mkdir_test_2)
686
+ with self.assertRaises(s2.ManagementError):
687
+ st.mkdir(f'{mkdir_test_2}/nest_1/nest_2')
688
+ st.mkdir(f'{mkdir_test_2}/nest_1')
689
+ st.mkdir(f'{mkdir_test_2}/nest_1/nest_2')
690
+ st.mkdir(f'{mkdir_test_3}')
691
+
692
+ assert st.exists(f'{mkdir_test_1}/')
693
+ assert st.exists(f'{mkdir_test_2}/')
694
+ assert st.exists(f'{mkdir_test_2}/nest_1/')
695
+ assert st.exists(f'{mkdir_test_2}/nest_1/nest_2/')
696
+ assert not st.exists('foo/')
697
+ assert not st.exists('foo/bar/')
698
+
699
+ assert st.is_dir(f'{mkdir_test_1}/')
700
+ assert st.is_dir(f'{mkdir_test_2}/')
701
+ assert st.is_dir(f'{mkdir_test_2}/nest_1/')
702
+ assert st.is_dir(f'{mkdir_test_2}/nest_1/nest_2/')
703
+
704
+ assert not st.is_file(f'{mkdir_test_1}/')
705
+ assert not st.is_file(f'{mkdir_test_2}/')
706
+ assert not st.is_file(f'{mkdir_test_2}/nest_1/')
707
+ assert not st.is_file(f'{mkdir_test_2}/nest_1/nest_2/')
708
+
709
+ out = st.listdir('/')
710
+ assert f'{mkdir_test_1}/' in out
711
+ assert f'{mkdir_test_2}/' in out
712
+ assert f'{mkdir_test_2}/nest_1/nest_2/' not in out
713
+
714
+ out = st.listdir('/', recursive=True)
715
+ assert f'{mkdir_test_1}/' in out
716
+ assert f'{mkdir_test_2}/' in out
717
+ assert f'{mkdir_test_2}/nest_1/nest_2/' in out
718
+
719
+ out = st.listdir(mkdir_test_2)
720
+ assert f'{mkdir_test_1}/' not in out
721
+ assert 'nest_1/' in out
722
+ assert 'nest_2/' not in out
723
+ assert 'nest_1/nest_2/' not in out
724
+
725
+ out = st.listdir(mkdir_test_2, recursive=True)
726
+ assert f'{mkdir_test_1}/' not in out
727
+ assert 'nest_1/' in out
728
+ assert 'nest_2/' not in out
729
+ assert 'nest_1/nest_2/' in out
730
+
731
+ # rmdir
732
+ before = st.listdir('/', recursive=True)
733
+ st.rmdir(f'{mkdir_test_1}/')
734
+ after = st.listdir('/', recursive=True)
735
+ assert f'{mkdir_test_1}/' in before
736
+ assert f'{mkdir_test_1}/' not in after
737
+ assert list(sorted(before)) == list(sorted(after + [f'{mkdir_test_1}/']))
738
+
739
+ with self.assertRaises(OSError):
740
+ st.rmdir(f'{mkdir_test_2}/')
741
+
742
+ mkdir_test_sql = f'mkdir_test_{id(self)}.sql'
743
+
744
+ st.upload_file(TEST_DIR / 'test.sql', mkdir_test_sql)
745
+
746
+ with self.assertRaises(NotADirectoryError):
747
+ st.rmdir(mkdir_test_sql)
748
+
749
+ # removedirs
750
+ before = st.listdir('/')
751
+ st.removedirs(f'{mkdir_test_2}/')
752
+ after = st.listdir('/')
753
+ assert f'{mkdir_test_2}/' in before
754
+ assert f'{mkdir_test_2}/' not in after
755
+ assert list(sorted(before)) == list(sorted(after + [f'{mkdir_test_2}/']))
756
+
757
+ with self.assertRaises(s2.ManagementError):
758
+ st.removedirs(mkdir_test_sql)
759
+
760
+ def test_os_files(self):
761
+ st = self.wg.stage
762
+
763
+ files_test_sql = f'files_test_{id(self)}.sql'
764
+ files_test_1_dir = f'files_test_1_{id(self)}'
765
+
766
+ st.mkdir(files_test_1_dir)
767
+ st.mkdir(f'{files_test_1_dir}/nest_1')
768
+
769
+ st.upload_file(TEST_DIR / 'test.sql', files_test_sql)
770
+ st.upload_file(
771
+ TEST_DIR / 'test.sql',
772
+ f'{files_test_1_dir}/nest_1/nested_files_test.sql',
773
+ )
774
+ st.upload_file(
775
+ TEST_DIR / 'test.sql',
776
+ f'{files_test_1_dir}/nest_1/nested_files_test_2.sql',
777
+ )
778
+
779
+ # remove
780
+ with self.assertRaises(IsADirectoryError):
781
+ st.remove(f'{files_test_1_dir}/')
782
+
783
+ before = st.listdir('/')
784
+ st.remove(files_test_sql)
785
+ after = st.listdir('/')
786
+ assert files_test_sql in before
787
+ assert files_test_sql not in after
788
+ assert list(sorted(before)) == list(sorted(after + [files_test_sql]))
789
+
790
+ before = st.listdir(f'{files_test_1_dir}/nest_1/')
791
+ st.remove(f'{files_test_1_dir}/nest_1/nested_files_test.sql')
792
+ after = st.listdir(f'{files_test_1_dir}/nest_1/')
793
+ assert 'nested_files_test.sql' in before
794
+ assert 'nested_files_test.sql' not in after
795
+ assert st.is_dir(f'{files_test_1_dir}/nest_1/')
796
+
797
+ # Removing the last file does not remove empty directories
798
+ st.remove(f'{files_test_1_dir}/nest_1/nested_files_test_2.sql')
799
+ assert not st.is_file(f'{files_test_1_dir}/nest_1/nested_files_test_2.sql')
800
+ assert st.is_dir(f'{files_test_1_dir}/nest_1/')
801
+ assert st.is_dir(f'{files_test_1_dir}/')
802
+
803
+ st.removedirs(files_test_1_dir)
804
+ assert not st.is_dir(f'{files_test_1_dir}/nest_1/')
805
+ assert not st.is_dir(f'{files_test_1_dir}/')
806
+
807
+ def test_os_rename(self):
808
+ st = self.wg.stage
809
+
810
+ rename_test_sql = f'rename_test_{id(self)}.sql'
811
+ rename_test_2_sql = f'rename_test_2_{id(self)}.sql'
812
+ rename_test_1_dir = f'rename_test_1_{id(self)}'
813
+ rename_test_2_dir = f'rename_test_2_{id(self)}'
814
+
815
+ st.upload_file(TEST_DIR / 'test.sql', rename_test_sql)
816
+
817
+ with self.assertRaises(s2.ManagementError):
818
+ st.upload_file(
819
+ TEST_DIR / 'test.sql',
820
+ f'{rename_test_1_dir}/nest_1/nested_rename_test.sql',
821
+ )
822
+
823
+ st.mkdir(rename_test_1_dir)
824
+ st.mkdir(f'{rename_test_1_dir}/nest_1')
825
+
826
+ assert st.exists(f'/{rename_test_1_dir}/nest_1/')
827
+
828
+ st.upload_file(
829
+ TEST_DIR / 'test.sql',
830
+ f'{rename_test_1_dir}/nest_1/nested_rename_test.sql',
831
+ )
832
+
833
+ st.upload_file(
834
+ TEST_DIR / 'test.sql',
835
+ f'{rename_test_1_dir}/nest_1/nested_rename_test_2.sql',
836
+ )
837
+
838
+ # rename file
839
+ assert rename_test_sql in st.listdir('/')
840
+ assert rename_test_2_sql not in st.listdir('/')
841
+ st.rename(rename_test_sql, rename_test_2_sql)
842
+ assert rename_test_sql not in st.listdir('/')
843
+ assert rename_test_2_sql in st.listdir('/')
844
+
845
+ # rename directory
846
+ assert f'{rename_test_1_dir}/' in st.listdir('/')
847
+ assert f'{rename_test_2_dir}/' not in st.listdir('/')
848
+ st.rename(f'{rename_test_1_dir}/', f'{rename_test_2_dir}/')
849
+ assert f'{rename_test_1_dir}/' not in st.listdir('/')
850
+ assert f'{rename_test_2_dir}/' in st.listdir('/')
851
+ assert st.is_file(f'{rename_test_2_dir}/nest_1/nested_rename_test.sql')
852
+ assert st.is_file(f'{rename_test_2_dir}/nest_1/nested_rename_test_2.sql')
853
+
854
+ # rename nested
855
+ assert f'{rename_test_2_dir}/nest_1/nested_rename_test.sql' in st.listdir(
856
+ '/', recursive=True,
857
+ )
858
+ assert f'{rename_test_2_dir}/nest_1/nested_rename_test_3.sql' not in st.listdir(
859
+ '/', recursive=True,
860
+ )
861
+ st.rename(
862
+ f'{rename_test_2_dir}/nest_1/nested_rename_test.sql',
863
+ f'{rename_test_2_dir}/nest_1/nested_rename_test_3.sql',
864
+ )
865
+ assert f'{rename_test_2_dir}/nest_1/nested_rename_test.sql' not in st.listdir(
866
+ '/', recursive=True,
867
+ )
868
+ assert f'{rename_test_2_dir}/nest_1/nested_rename_test_3.sql' in st.listdir(
869
+ '/', recursive=True,
870
+ )
871
+ assert not st.is_file(f'{rename_test_2_dir}/nest_1/nested_rename_test.sql')
872
+ assert st.is_file(f'{rename_test_2_dir}/nest_1/nested_rename_test_2.sql')
873
+ assert st.is_file(f'{rename_test_2_dir}/nest_1/nested_rename_test_3.sql')
874
+
875
+ # non-existent file
876
+ with self.assertRaises(OSError):
877
+ st.rename('rename_foo.sql', 'rename_foo_2.sql')
878
+
879
+ # overwrite
880
+ with self.assertRaises(OSError):
881
+ st.rename(
882
+ rename_test_2_sql,
883
+ f'{rename_test_2_dir}/nest_1/nested_rename_test_3.sql',
884
+ )
885
+
886
+ st.rename(
887
+ rename_test_2_sql,
888
+ f'{rename_test_2_dir}/nest_1/nested_rename_test_3.sql', overwrite=True,
889
+ )
890
+
891
+ def test_file_object(self):
892
+ st = self.wg.stage
893
+
894
+ obj_test_dir = f'obj_test_{id(self)}'
895
+
896
+ st.mkdir(obj_test_dir)
897
+ st.mkdir(f'{obj_test_dir}/nest_1')
898
+
899
+ obj_test_sql = f'obj_test_{id(self)}.sql'
900
+ obj_test_2_sql = f'obj_test_2_{id(self)}.sql'
901
+
902
+ f1 = st.upload_file(TEST_DIR / 'test.sql', obj_test_sql)
903
+ f2 = st.upload_file(
904
+ TEST_DIR / 'test.sql',
905
+ f'{obj_test_dir}/nest_1/{obj_test_sql}',
906
+ )
907
+ d2 = st.info(f'{obj_test_dir}/nest_1/')
908
+
909
+ # is_file / is_dir
910
+ assert not f1.is_dir()
911
+ assert f1.is_file()
912
+ assert not f2.is_dir()
913
+ assert f2.is_file()
914
+ assert d2.is_dir()
915
+ assert not d2.is_file()
916
+
917
+ # abspath / basename / dirname / exists
918
+ assert f1.abspath() == obj_test_sql
919
+ assert f1.basename() == obj_test_sql
920
+ assert f1.dirname() == '/'
921
+ assert f1.exists()
922
+ assert f2.abspath() == f'{obj_test_dir}/nest_1/{obj_test_sql}'
923
+ assert f2.basename() == obj_test_sql
924
+ assert f2.dirname() == f'{obj_test_dir}/nest_1/'
925
+ assert f2.exists()
926
+ assert d2.abspath() == f'{obj_test_dir}/nest_1/'
927
+ assert d2.basename() == 'nest_1'
928
+ assert d2.dirname() == f'{obj_test_dir}/'
929
+ assert d2.exists()
930
+
931
+ # download
932
+ assert f1.download(encoding='utf-8') == open(TEST_DIR / 'test.sql', 'r').read()
933
+ assert f1.download() == open(TEST_DIR / 'test.sql', 'rb').read()
934
+
935
+ # remove
936
+ with self.assertRaises(IsADirectoryError):
937
+ d2.remove()
938
+
939
+ assert st.is_file(obj_test_sql)
940
+ f1.remove()
941
+ assert not st.is_file(obj_test_sql)
942
+
943
+ # removedirs
944
+ with self.assertRaises(NotADirectoryError):
945
+ f2.removedirs()
946
+
947
+ assert st.exists(d2.path)
948
+ d2.removedirs()
949
+ assert not st.exists(d2.path)
950
+
951
+ # rmdir
952
+ f1 = st.upload_file(TEST_DIR / 'test.sql', obj_test_sql)
953
+ d2 = st.mkdir(f'{obj_test_dir}/nest_1')
954
+
955
+ assert st.exists(f1.path)
956
+ assert st.exists(d2.path)
957
+
958
+ with self.assertRaises(NotADirectoryError):
959
+ f1.rmdir()
960
+
961
+ assert st.exists(f1.path)
962
+ assert st.exists(d2.path)
963
+
964
+ d2.rmdir()
965
+
966
+ assert not st.exists(f'{obj_test_dir}/nest_1/')
967
+ assert not st.exists(obj_test_dir)
968
+
969
+ # mtime / ctime
970
+ assert f1.getmtime() > 0
971
+ assert f1.getctime() > 0
972
+
973
+ # rename
974
+ assert st.exists(obj_test_sql)
975
+ assert not st.exists(obj_test_2_sql)
976
+ f1.rename(obj_test_2_sql)
977
+ assert not st.exists(obj_test_sql)
978
+ assert st.exists(obj_test_2_sql)
979
+ assert f1.abspath() == obj_test_2_sql
980
+
981
+
982
+ @pytest.mark.management
983
+ class TestSecrets(unittest.TestCase):
984
+
985
+ manager = None
986
+ wg = None
987
+ password = None
988
+
989
+ @classmethod
990
+ def setUpClass(cls):
991
+ cls.manager = s2.manage_workspaces()
992
+
993
+ us_regions = [x for x in cls.manager.regions if 'US' in x.name]
994
+ cls.password = secrets.token_urlsafe(20) + '-x&$'
995
+
996
+ name = clean_name(secrets.token_urlsafe(20)[:20])
997
+
998
+ cls.wg = cls.manager.create_workspace_group(
999
+ f'wg-test-{name}',
1000
+ region=random.choice(us_regions).id,
1001
+ admin_password=cls.password,
1002
+ firewall_ranges=['0.0.0.0/0'],
1003
+ )
1004
+
1005
+ @classmethod
1006
+ def tearDownClass(cls):
1007
+ if cls.wg is not None:
1008
+ cls.wg.terminate(force=True)
1009
+ cls.wg = None
1010
+ cls.manager = None
1011
+ cls.password = None
1012
+
1013
+ def test_get_secret(self):
1014
+ # manually create secret and then get secret
1015
+ # try to delete the secret if it exists
1016
+ try:
1017
+ secret = self.manager.organizations.current.get_secret('secret_name')
1018
+
1019
+ secret_id = secret.id
1020
+
1021
+ self.manager._delete(f'secrets/{secret_id}')
1022
+ except s2.ManagementError:
1023
+ pass
1024
+
1025
+ self.manager._post(
1026
+ 'secrets',
1027
+ json=dict(
1028
+ name='secret_name',
1029
+ value='secret_value',
1030
+ ),
1031
+ )
1032
+
1033
+ secret = self.manager.organizations.current.get_secret('secret_name')
1034
+
1035
+ assert secret.name == 'secret_name'
1036
+ assert secret.value == 'secret_value'
1037
+
1038
+
1039
+ @pytest.mark.management
1040
+ class TestJob(unittest.TestCase):
1041
+
1042
+ manager = None
1043
+ workspace_group = None
1044
+ workspace = None
1045
+ password = None
1046
+ job_ids = []
1047
+
1048
+ @classmethod
1049
+ def setUpClass(cls):
1050
+ cls.manager = s2.manage_workspaces()
1051
+
1052
+ us_regions = [x for x in cls.manager.regions if 'US' in x.name]
1053
+ cls.password = secrets.token_urlsafe(20) + '-x&$'
1054
+
1055
+ name = clean_name(secrets.token_urlsafe(20)[:20])
1056
+
1057
+ cls.workspace_group = cls.manager.create_workspace_group(
1058
+ f'wg-test-{name}',
1059
+ region=random.choice(us_regions).id,
1060
+ admin_password=cls.password,
1061
+ firewall_ranges=['0.0.0.0/0'],
1062
+ )
1063
+
1064
+ try:
1065
+ cls.workspace = cls.workspace_group.create_workspace(
1066
+ f'ws-test-{name}-x',
1067
+ wait_on_active=True,
1068
+ )
1069
+ except Exception:
1070
+ cls.workspace_group.terminate(force=True)
1071
+ raise
1072
+
1073
+ @classmethod
1074
+ def tearDownClass(cls):
1075
+ for job_id in cls.job_ids:
1076
+ try:
1077
+ cls.manager.organizations.current.jobs.delete(job_id)
1078
+ except Exception:
1079
+ pass
1080
+ if cls.workspace_group is not None:
1081
+ cls.workspace_group.terminate(force=True)
1082
+ cls.workspace_group = None
1083
+ cls.workspace = None
1084
+ cls.manager = None
1085
+ cls.password = None
1086
+ if os.environ.get('SINGLESTOREDB_WORKSPACE', None) is not None:
1087
+ del os.environ['SINGLESTOREDB_WORKSPACE']
1088
+ if os.environ.get('SINGLESTOREDB_DEFAULT_DATABASE', None) is not None:
1089
+ del os.environ['SINGLESTOREDB_DEFAULT_DATABASE']
1090
+
1091
+ def test_job_without_database_target(self):
1092
+ """
1093
+ Creates job without target database on a specific runtime
1094
+ Waits for job to finish
1095
+ Gets the job
1096
+ Deletes the job
1097
+ """
1098
+ if os.environ.get('SINGLESTOREDB_WORKSPACE', None) is not None:
1099
+ del os.environ['SINGLESTOREDB_WORKSPACE']
1100
+ if os.environ.get('SINGLESTOREDB_DEFAULT_DATABASE', None) is not None:
1101
+ del os.environ['SINGLESTOREDB_DEFAULT_DATABASE']
1102
+
1103
+ job_manager = self.manager.organizations.current.jobs
1104
+ job = job_manager.run(
1105
+ 'Scheduling Test.ipynb',
1106
+ 'notebooks-cpu-small',
1107
+ {'strParam': 'string', 'intParam': 1, 'floatParam': 1.0, 'boolParam': True},
1108
+ )
1109
+ self.job_ids.append(job.job_id)
1110
+ assert job.execution_config.notebook_path == 'Scheduling Test.ipynb'
1111
+ assert job.schedule.mode == job_manager.modes().ONCE
1112
+ assert not job.execution_config.create_snapshot
1113
+ assert job.completed_executions_count == 0
1114
+ assert job.name is None
1115
+ assert job.description is None
1116
+ assert job.job_metadata == []
1117
+ assert job.terminated_at is None
1118
+ assert job.target_config is None
1119
+ job.wait()
1120
+ job = job_manager.get(job.job_id)
1121
+ assert job.execution_config.notebook_path == 'Scheduling Test.ipynb'
1122
+ assert job.schedule.mode == job_manager.modes().ONCE
1123
+ assert not job.execution_config.create_snapshot
1124
+ assert job.completed_executions_count == 1
1125
+ assert job.name is None
1126
+ assert job.description is None
1127
+ assert job.job_metadata != []
1128
+ assert len(job.job_metadata) == 1
1129
+ assert job.job_metadata[0].count == 1
1130
+ assert job.job_metadata[0].status == Status.COMPLETED
1131
+ assert job.terminated_at is None
1132
+ assert job.target_config is None
1133
+ deleted = job.delete()
1134
+ assert deleted
1135
+ job = job_manager.get(job.job_id)
1136
+ assert job.terminated_at is not None
1137
+
1138
+ def test_job_with_database_target(self):
1139
+ """
1140
+ Creates job with target database on a specific runtime
1141
+ Waits for job to finish
1142
+ Gets the job
1143
+ Deletes the job
1144
+ """
1145
+ os.environ['SINGLESTOREDB_DEFAULT_DATABASE'] = 'information_schema'
1146
+ os.environ['SINGLESTOREDB_WORKSPACE'] = self.workspace.id
1147
+
1148
+ job_manager = self.manager.organizations.current.jobs
1149
+ job = job_manager.run(
1150
+ 'Scheduling Test.ipynb',
1151
+ 'notebooks-cpu-small',
1152
+ {'strParam': 'string', 'intParam': 1, 'floatParam': 1.0, 'boolParam': True},
1153
+ )
1154
+ self.job_ids.append(job.job_id)
1155
+ assert job.execution_config.notebook_path == 'Scheduling Test.ipynb'
1156
+ assert job.schedule.mode == job_manager.modes().ONCE
1157
+ assert not job.execution_config.create_snapshot
1158
+ assert job.completed_executions_count == 0
1159
+ assert job.name is None
1160
+ assert job.description is None
1161
+ assert job.job_metadata == []
1162
+ assert job.terminated_at is None
1163
+ assert job.target_config is not None
1164
+ assert job.target_config.database_name == 'information_schema'
1165
+ assert job.target_config.target_id == self.workspace.id
1166
+ assert job.target_config.target_type == TargetType.WORKSPACE
1167
+ assert not job.target_config.resume_target
1168
+ job.wait()
1169
+ job = job_manager.get(job.job_id)
1170
+ assert job.execution_config.notebook_path == 'Scheduling Test.ipynb'
1171
+ assert job.schedule.mode == job_manager.modes().ONCE
1172
+ assert not job.execution_config.create_snapshot
1173
+ assert job.completed_executions_count == 1
1174
+ assert job.name is None
1175
+ assert job.description is None
1176
+ assert job.job_metadata != []
1177
+ assert len(job.job_metadata) == 1
1178
+ assert job.job_metadata[0].count == 1
1179
+ assert job.job_metadata[0].status == Status.COMPLETED
1180
+ assert job.terminated_at is None
1181
+ assert job.target_config is not None
1182
+ assert job.target_config.database_name == 'information_schema'
1183
+ assert job.target_config.target_id == self.workspace.id
1184
+ assert job.target_config.target_type == TargetType.WORKSPACE
1185
+ assert not job.target_config.resume_target
1186
+ deleted = job.delete()
1187
+ assert deleted
1188
+ job = job_manager.get(job.job_id)
1189
+ assert job.terminated_at is not None
1190
+
1191
+
1192
+ @pytest.mark.management
1193
+ class TestFileSpaces(unittest.TestCase):
1194
+
1195
+ manager = None
1196
+ personal_space = None
1197
+ shared_space = None
1198
+
1199
+ @classmethod
1200
+ def setUpClass(cls):
1201
+ cls.manager = s2.manage_files()
1202
+ cls.personal_space = cls.manager.personal_space
1203
+ cls.shared_space = cls.manager.shared_space
1204
+
1205
+ @classmethod
1206
+ def tearDownClass(cls):
1207
+ cls.manager = None
1208
+ cls.personal_space = None
1209
+ cls.shared_space = None
1210
+
1211
+ def test_upload_file(self):
1212
+ upload_test_ipynb = f'upload_test_{id(self)}.ipynb'
1213
+
1214
+ for space in [self.personal_space, self.shared_space]:
1215
+ root = space.info('/')
1216
+ assert str(root.path) == '/'
1217
+ assert root.type == 'directory'
1218
+
1219
+ # Upload files
1220
+ f = space.upload_file(
1221
+ TEST_DIR / 'test.ipynb',
1222
+ upload_test_ipynb,
1223
+ )
1224
+ assert str(f.path) == upload_test_ipynb
1225
+ assert f.type == 'notebook'
1226
+
1227
+ # Download and compare to original
1228
+ txt = f.download(encoding='utf-8')
1229
+ assert txt == open(TEST_DIR / 'test.ipynb').read()
1230
+
1231
+ # Make sure we can't overwrite
1232
+ with self.assertRaises(OSError):
1233
+ space.upload_file(
1234
+ TEST_DIR / 'test.ipynb',
1235
+ upload_test_ipynb,
1236
+ )
1237
+
1238
+ # Force overwrite with new content
1239
+ f = space.upload_file(
1240
+ TEST_DIR / 'test2.ipynb',
1241
+ upload_test_ipynb, overwrite=True,
1242
+ )
1243
+ assert str(f.path) == upload_test_ipynb
1244
+ assert f.type == 'notebook'
1245
+
1246
+ # Verify new content
1247
+ txt = f.download(encoding='utf-8')
1248
+ assert txt == open(TEST_DIR / 'test2.ipynb').read()
1249
+
1250
+ # Make sure we can't upload a folder
1251
+ with self.assertRaises(s2.ManagementError):
1252
+ space.upload_folder(TEST_DIR, 'test')
1253
+
1254
+ # Cleanup
1255
+ space.remove(upload_test_ipynb)
1256
+
1257
+ def test_upload_file_io(self):
1258
+ upload_test_ipynb = f'upload_test_{id(self)}.ipynb'
1259
+
1260
+ for space in [self.personal_space, self.shared_space]:
1261
+ root = space.info('/')
1262
+ assert str(root.path) == '/'
1263
+ assert root.type == 'directory'
1264
+
1265
+ # Upload files
1266
+ f = space.upload_file(
1267
+ open(TEST_DIR / 'test.ipynb', 'r'),
1268
+ upload_test_ipynb,
1269
+ )
1270
+ assert str(f.path) == upload_test_ipynb
1271
+ assert f.type == 'notebook'
1272
+
1273
+ # Download and compare to original
1274
+ txt = f.download(encoding='utf-8')
1275
+ assert txt == open(TEST_DIR / 'test.ipynb').read()
1276
+
1277
+ # Make sure we can't overwrite
1278
+ with self.assertRaises(OSError):
1279
+ space.upload_file(
1280
+ open(TEST_DIR / 'test.ipynb', 'r'),
1281
+ upload_test_ipynb,
1282
+ )
1283
+
1284
+ # Force overwrite with new content
1285
+ f = space.upload_file(
1286
+ open(TEST_DIR / 'test2.ipynb', 'r'),
1287
+ upload_test_ipynb, overwrite=True,
1288
+ )
1289
+ assert str(f.path) == upload_test_ipynb
1290
+ assert f.type == 'notebook'
1291
+
1292
+ # Verify new content
1293
+ txt = f.download(encoding='utf-8')
1294
+ assert txt == open(TEST_DIR / 'test2.ipynb').read()
1295
+
1296
+ # Make sure we can't upload a folder
1297
+ with self.assertRaises(s2.ManagementError):
1298
+ space.upload_folder(TEST_DIR, 'test')
1299
+
1300
+ # Cleanup
1301
+ space.remove(upload_test_ipynb)
1302
+
1303
+ def test_open(self):
1304
+ for space in [self.personal_space, self.shared_space]:
1305
+ open_test_ipynb = f'open_test_ipynb_{id(self)}.ipynb'
1306
+
1307
+ # See if error is raised for non-existent file
1308
+ with self.assertRaises(s2.ManagementError):
1309
+ space.open(open_test_ipynb, 'r')
1310
+
1311
+ # Load test file
1312
+ space.upload_file(TEST_DIR / 'test.ipynb', open_test_ipynb)
1313
+
1314
+ # Read file using `open`
1315
+ with space.open(open_test_ipynb, 'r') as rfile:
1316
+ assert rfile.read() == open(TEST_DIR / 'test.ipynb').read()
1317
+
1318
+ # Read file using `open` with 'rt' mode
1319
+ with space.open(open_test_ipynb, 'rt') as rfile:
1320
+ assert rfile.read() == open(TEST_DIR / 'test.ipynb').read()
1321
+
1322
+ # Read file using `open` with 'rb' mode
1323
+ with space.open(open_test_ipynb, 'rb') as rfile:
1324
+ assert rfile.read() == open(TEST_DIR / 'test.ipynb', 'rb').read()
1325
+
1326
+ # Read file using `open` with 'rb' mode
1327
+ with self.assertRaises(ValueError):
1328
+ with space.open(open_test_ipynb, 'b') as rfile:
1329
+ pass
1330
+
1331
+ # Attempt overwrite file using `open` with mode 'x'
1332
+ with self.assertRaises(OSError):
1333
+ with space.open(open_test_ipynb, 'x') as wfile:
1334
+ pass
1335
+
1336
+ # Attempt overwrite file using `open` with mode 'w'
1337
+ with space.open(open_test_ipynb, 'w') as wfile:
1338
+ wfile.write(open(TEST_DIR / 'test2.ipynb').read())
1339
+
1340
+ txt = space.download_file(open_test_ipynb, encoding='utf-8')
1341
+
1342
+ assert txt == open(TEST_DIR / 'test2.ipynb').read()
1343
+
1344
+ open_raw_test_ipynb = f'open_raw_test_{id(self)}.ipynb'
1345
+
1346
+ # Test writer without context manager
1347
+ wfile = space.open(open_raw_test_ipynb, 'w')
1348
+ for line in open(TEST_DIR / 'test.ipynb'):
1349
+ wfile.write(line)
1350
+ wfile.close()
1351
+
1352
+ txt = space.download_file(
1353
+ open_raw_test_ipynb,
1354
+ encoding='utf-8',
1355
+ )
1356
+
1357
+ assert txt == open(TEST_DIR / 'test.ipynb').read()
1358
+
1359
+ # Test reader without context manager
1360
+ rfile = space.open(open_raw_test_ipynb, 'r')
1361
+ txt = ''
1362
+ for line in rfile:
1363
+ txt += line
1364
+ rfile.close()
1365
+
1366
+ assert txt == open(TEST_DIR / 'test.ipynb').read()
1367
+
1368
+ # Cleanup
1369
+ space.remove(open_test_ipynb)
1370
+ space.remove(open_raw_test_ipynb)
1371
+
1372
+ def test_obj_open(self):
1373
+ for space in [self.personal_space, self.shared_space]:
1374
+ obj_open_test_ipynb = f'obj_open_test_{id(self)}.ipynb'
1375
+ obj_open_dir = f'obj_open_dir_{id(self)}'
1376
+
1377
+ # Load test file
1378
+ f = space.upload_file(
1379
+ TEST_DIR / 'test.ipynb',
1380
+ obj_open_test_ipynb,
1381
+ )
1382
+
1383
+ # Read file using `open`
1384
+ with f.open() as rfile:
1385
+ assert rfile.read() == open(TEST_DIR / 'test.ipynb').read()
1386
+
1387
+ # Make sure directories error out
1388
+ with self.assertRaises(s2.ManagementError):
1389
+ space.mkdir(obj_open_dir)
1390
+
1391
+ # Write file using `open`
1392
+ with f.open('w', encoding='utf-8') as wfile:
1393
+ wfile.write(open(TEST_DIR / 'test2.ipynb').read())
1394
+
1395
+ assert f.download(encoding='utf-8') == open(TEST_DIR / 'test2.ipynb').read()
1396
+
1397
+ # Test writer without context manager
1398
+ wfile = f.open('w')
1399
+ for line in open(TEST_DIR / 'test.ipynb'):
1400
+ wfile.write(line)
1401
+ wfile.close()
1402
+
1403
+ txt = space.download_file(f.path, encoding='utf-8')
1404
+
1405
+ assert txt == open(TEST_DIR / 'test.ipynb').read()
1406
+
1407
+ # Test reader without context manager
1408
+ rfile = f.open('r')
1409
+ txt = ''
1410
+ for line in rfile:
1411
+ txt += line
1412
+ rfile.close()
1413
+
1414
+ assert txt == open(TEST_DIR / 'test.ipynb').read()
1415
+
1416
+ # Cleanup
1417
+ space.remove(obj_open_test_ipynb)
1418
+
1419
+ def test_os_directories(self):
1420
+ mkdir_test_1_dir = f'mkdir_test_1_{id(self)}'
1421
+
1422
+ for space in [self.personal_space, self.shared_space]:
1423
+ # Make sure directories error out
1424
+ with self.assertRaises(s2.ManagementError):
1425
+ space.mkdir(mkdir_test_1_dir)
1426
+
1427
+ with self.assertRaises(s2.ManagementError):
1428
+ space.exists(f'{mkdir_test_1_dir}/')
1429
+
1430
+ out = space.listdir('/')
1431
+ assert f'{mkdir_test_1_dir}/' not in out
1432
+
1433
+ with self.assertRaises(s2.ManagementError):
1434
+ space.rmdir(f'{mkdir_test_1_dir}/')
1435
+
1436
+ def test_os_rename(self):
1437
+ rename_test_ipynb = f'rename_test_{id(self)}.ipynb'
1438
+ rename_test_2_ipynb = f'rename_test_2_{id(self)}.ipynb'
1439
+ rename_test_3_ipynb = f'rename_test_3_{id(self)}.ipynb'
1440
+
1441
+ for space in [self.personal_space, self.shared_space]:
1442
+ space.upload_file(
1443
+ TEST_DIR / 'test.ipynb',
1444
+ rename_test_ipynb,
1445
+ )
1446
+ assert rename_test_ipynb in space.listdir('/')
1447
+ assert rename_test_2_ipynb not in space.listdir('/')
1448
+
1449
+ space.rename(
1450
+ rename_test_ipynb,
1451
+ rename_test_2_ipynb,
1452
+ )
1453
+ assert rename_test_ipynb not in space.listdir('/')
1454
+ assert rename_test_2_ipynb in space.listdir('/')
1455
+
1456
+ # non-existent file
1457
+ with self.assertRaises(OSError):
1458
+ space.rename('rename_foo.ipynb', 'rename_foo_2.ipynb')
1459
+
1460
+ space.upload_file(
1461
+ TEST_DIR / 'test.ipynb',
1462
+ rename_test_3_ipynb,
1463
+ )
1464
+
1465
+ # overwrite
1466
+ with self.assertRaises(OSError):
1467
+ space.rename(
1468
+ rename_test_2_ipynb,
1469
+ rename_test_3_ipynb,
1470
+ )
1471
+
1472
+ space.rename(
1473
+ rename_test_2_ipynb,
1474
+ rename_test_3_ipynb, overwrite=True,
1475
+ )
1476
+
1477
+ # Cleanup
1478
+ space.remove(rename_test_3_ipynb)
1479
+
1480
+ def test_file_object(self):
1481
+ obj_test_ipynb = f'obj_test_{id(self)}.ipynb'
1482
+ obj_test_2_ipynb = f'obj_test_2_{id(self)}.ipynb'
1483
+
1484
+ for space in [self.personal_space, self.shared_space]:
1485
+ f = space.upload_file(
1486
+ TEST_DIR / 'test.ipynb',
1487
+ obj_test_ipynb,
1488
+ )
1489
+
1490
+ assert not f.is_dir()
1491
+ assert f.is_file()
1492
+
1493
+ # abspath / basename / dirname / exists
1494
+ assert f.abspath() == obj_test_ipynb
1495
+ assert f.basename() == obj_test_ipynb
1496
+ assert f.dirname() == '/'
1497
+ assert f.exists()
1498
+
1499
+ # download
1500
+ assert f.download(encoding='utf-8') == \
1501
+ open(TEST_DIR / 'test.ipynb', 'r').read()
1502
+ assert f.download() == open(TEST_DIR / 'test.ipynb', 'rb').read()
1503
+
1504
+ assert space.is_file(obj_test_ipynb)
1505
+ f.remove()
1506
+ assert not space.is_file(obj_test_ipynb)
1507
+
1508
+ # mtime / ctime
1509
+ assert f.getmtime() > 0
1510
+ assert f.getctime() > 0
1511
+
1512
+ # rename
1513
+ f = space.upload_file(
1514
+ TEST_DIR / 'test.ipynb',
1515
+ obj_test_ipynb,
1516
+ )
1517
+ assert space.exists(obj_test_ipynb)
1518
+ assert not space.exists(obj_test_2_ipynb)
1519
+ f.rename(obj_test_2_ipynb)
1520
+ assert not space.exists(obj_test_ipynb)
1521
+ assert space.exists(obj_test_2_ipynb)
1522
+ assert f.abspath() == obj_test_2_ipynb
1523
+
1524
+ # Cleanup
1525
+ space.remove(obj_test_2_ipynb)
1526
+
1527
+
1528
+ @pytest.mark.management
1529
+ class TestRegions(unittest.TestCase):
1530
+ """Test cases for region management."""
1531
+
1532
+ manager = None
1533
+
1534
+ @classmethod
1535
+ def setUpClass(cls):
1536
+ """Set up the test environment."""
1537
+ cls.manager = s2.manage_regions()
1538
+
1539
+ @classmethod
1540
+ def tearDownClass(cls):
1541
+ """Clean up the test environment."""
1542
+ cls.manager = None
1543
+
1544
+ def test_list_regions(self):
1545
+ """Test listing all regions."""
1546
+ regions = self.manager.list_regions()
1547
+
1548
+ # Verify we get a NamedList
1549
+ assert isinstance(regions, NamedList)
1550
+
1551
+ # Verify we have at least one region
1552
+ assert len(regions) > 0
1553
+
1554
+ # Verify region properties
1555
+ region = regions[0]
1556
+ assert isinstance(region, Region)
1557
+ assert hasattr(region, 'id')
1558
+ assert hasattr(region, 'name')
1559
+ assert hasattr(region, 'provider')
1560
+
1561
+ # Verify provider values
1562
+ providers = {x.provider for x in regions}
1563
+ assert 'Azure' in providers or 'GCP' in providers or 'AWS' in providers
1564
+
1565
+ def test_list_shared_tier_regions(self):
1566
+ """Test listing shared tier regions."""
1567
+ regions = self.manager.list_shared_tier_regions()
1568
+
1569
+ # Verify we get a NamedList
1570
+ assert isinstance(regions, NamedList)
1571
+
1572
+ # Verify region properties if we have any shared tier regions
1573
+ if regions:
1574
+ region = regions[0]
1575
+ assert isinstance(region, Region)
1576
+ assert hasattr(region, 'name')
1577
+ assert hasattr(region, 'provider')
1578
+ assert hasattr(region, 'region_name')
1579
+
1580
+ # Verify provider values
1581
+ providers = {x.provider for x in regions}
1582
+ assert any(p in providers for p in ['Azure', 'GCP', 'AWS'])
1583
+
1584
+ def test_str_repr(self):
1585
+ """Test string representation of regions."""
1586
+ regions = self.manager.list_regions()
1587
+ if not regions:
1588
+ self.skipTest('No regions available for testing')
1589
+
1590
+ region = regions[0]
1591
+
1592
+ # Test __str__
1593
+ s = str(region)
1594
+ assert region.id in s
1595
+ assert region.name in s
1596
+ assert region.provider in s
1597
+
1598
+ # Test __repr__
1599
+ assert repr(region) == str(region)