databricks-labs-pytester 0.1.0__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 (29) hide show
  1. databricks/__init__.py +3 -0
  2. databricks/labs/__init__.py +3 -0
  3. databricks/labs/pytester/__about__.py +1 -0
  4. databricks/labs/pytester/__init__.py +5 -0
  5. databricks/labs/pytester/fixtures/__init__.py +0 -0
  6. databricks/labs/pytester/fixtures/baseline.py +170 -0
  7. databricks/labs/pytester/fixtures/catalog.py +433 -0
  8. databricks/labs/pytester/fixtures/compute.py +316 -0
  9. databricks/labs/pytester/fixtures/connect.py +33 -0
  10. databricks/labs/pytester/fixtures/environment.py +148 -0
  11. databricks/labs/pytester/fixtures/iam.py +113 -0
  12. databricks/labs/pytester/fixtures/ml.py +147 -0
  13. databricks/labs/pytester/fixtures/notebooks.py +119 -0
  14. databricks/labs/pytester/fixtures/permissions.py +459 -0
  15. databricks/labs/pytester/fixtures/plugin.py +134 -0
  16. databricks/labs/pytester/fixtures/redash.py +59 -0
  17. databricks/labs/pytester/fixtures/secrets.py +71 -0
  18. databricks/labs/pytester/fixtures/sql.py +31 -0
  19. databricks/labs/pytester/fixtures/unwrap.py +107 -0
  20. databricks/labs/pytester/fixtures/watchdog.py +26 -0
  21. databricks/labs/pytester/py.typed +1 -0
  22. databricks_labs_pytester-0.1.0.dist-info/METADATA +1064 -0
  23. databricks_labs_pytester-0.1.0.dist-info/RECORD +29 -0
  24. databricks_labs_pytester-0.1.0.dist-info/WHEEL +4 -0
  25. databricks_labs_pytester-0.1.0.dist-info/entry_points.txt +2 -0
  26. databricks_labs_pytester-0.1.0.dist-info/licenses/LICENSE +69 -0
  27. databricks_labs_pytester-0.1.0.dist-info/licenses/NOTICE +19 -0
  28. tests/resources/hatchling-whl/src/hatchling_whl/__about__.py +1 -0
  29. tests/resources/hatchling-whl/src/hatchling_whl/__init__.py +6 -0
databricks/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ # DO NOT ADD ANYTHING ELSE TO THIS FILE FOR COMPATIBILITY WITH OTHER databricks.* PACKAGES
2
+ # SEE https://packaging.python.org/guides/packaging-namespace-packages/#pkgutil-style-namespace-packages
3
+ __path__ = __import__("pkgutil").extend_path(__path__, __name__)
@@ -0,0 +1,3 @@
1
+ # DO NOT ADD ANYTHING ELSE TO THIS FILE FOR COMPATIBILITY WITH OTHER databricks.* PACKAGES
2
+ # SEE https://packaging.python.org/guides/packaging-namespace-packages/#pkgutil-style-namespace-packages
3
+ __path__ = __import__("pkgutil").extend_path(__path__, __name__)
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,5 @@
1
+ from databricks.sdk.useragent import with_extra
2
+
3
+ from databricks.labs.pytester.__about__ import __version__
4
+
5
+ with_extra("pytester", __version__)
File without changes
@@ -0,0 +1,170 @@
1
+ import logging
2
+ import random
3
+ import string
4
+
5
+ from pytest import fixture
6
+
7
+ from databricks.sdk import WorkspaceClient
8
+ from databricks.sdk.errors import DatabricksError
9
+
10
+ _LOG = logging.getLogger(__name__)
11
+
12
+
13
+ @fixture
14
+ def make_random():
15
+ """
16
+ Fixture to generate random strings.
17
+
18
+ This fixture provides a function to generate random strings of a specified length.
19
+ The generated strings are created using a character set consisting of uppercase letters,
20
+ lowercase letters, and digits.
21
+
22
+ To generate a random string with default length of 16 characters:
23
+
24
+ ```python
25
+ random_string = make_random()
26
+ assert len(random_string) == 16
27
+ ```
28
+
29
+ To generate a random string with a specified length:
30
+
31
+ ```python
32
+ random_string = make_random(k=8)
33
+ assert len(random_string) == 8
34
+ ```
35
+ """
36
+
37
+ def inner(k=16) -> str:
38
+ """
39
+ Generate a random string.
40
+
41
+ Parameters:
42
+ -----------
43
+ k : int, optional
44
+ Length of the random string (default is 16).
45
+
46
+ Returns:
47
+ --------
48
+ str:
49
+ A randomly generated string.
50
+ """
51
+ charset = string.ascii_uppercase + string.ascii_lowercase + string.digits
52
+ return "".join(random.choices(charset, k=int(k)))
53
+
54
+ return inner
55
+
56
+
57
+ def factory(name, create, remove):
58
+ """
59
+ Factory function for creating fixtures.
60
+
61
+ This function creates a fixture for managing resources (e.g., secret scopes) within test functions.
62
+ The provided ``create`` function is used to create a resource, and the provided ``remove`` function
63
+ is used to remove the resource after the test is complete.
64
+
65
+ Parameters:
66
+ -----------
67
+ name : str
68
+ The name of the resource (e.g., "secret scope").
69
+ create : function
70
+ A function to create the resource.
71
+ remove : function
72
+ A function to remove the resource.
73
+
74
+ Returns:
75
+ --------
76
+ function:
77
+ A function to create and manage the resource fixture.
78
+
79
+ Usage Example:
80
+ --------------
81
+ To create a fixture for managing secret scopes:
82
+
83
+ .. code-block:: python
84
+
85
+ @pytest.fixture
86
+ def make_secret_scope(ws, make_random):
87
+ def create(**kwargs):
88
+ name = f"sdk-{make_random(4)}"
89
+ ws.secrets.create_scope(name, **kwargs)
90
+ return name
91
+
92
+ def cleanup(scope):
93
+ ws.secrets.delete_scope(scope)
94
+
95
+ yield from factory("secret scope", create, cleanup)
96
+ """
97
+ cleanup = []
98
+
99
+ def inner(**kwargs):
100
+ some = create(**kwargs)
101
+ _LOG.debug(f"added {name} fixture: {some}")
102
+ cleanup.append(some)
103
+ return some
104
+
105
+ yield inner
106
+ _LOG.debug(f"clearing {len(cleanup)} {name} fixtures")
107
+ for some in cleanup:
108
+ try:
109
+ _LOG.debug(f"removing {name} fixture: {some}")
110
+ remove(some)
111
+ except DatabricksError as e:
112
+ _LOG.debug(f"ignoring error while {name} {some} teardown: {e}")
113
+
114
+
115
+ @fixture
116
+ def product_info():
117
+ return None, None
118
+
119
+
120
+ @fixture
121
+ def ws(debug_env: dict[str, str], product_info: tuple[str, str]) -> WorkspaceClient:
122
+ """
123
+ Create and provide a Databricks WorkspaceClient object.
124
+
125
+ This fixture initializes a Databricks WorkspaceClient object, which can be used
126
+ to interact with the Databricks workspace API. The created instance of WorkspaceClient
127
+ is shared across all test functions within the test session.
128
+
129
+ See [detailed documentation](https://databricks-sdk-py.readthedocs.io/en/latest/authentication.html) for the list
130
+ of environment variables that can be used to authenticate the WorkspaceClient.
131
+
132
+ In your test functions, include this fixture as an argument to use the WorkspaceClient:
133
+
134
+ ```python
135
+ def test_workspace_operations(ws):
136
+ clusters = ws.clusters.list_clusters()
137
+ assert len(clusters) >= 0
138
+ ```
139
+ """
140
+ product_name, product_version = product_info
141
+ # ignores fixed in https://github.com/databricks/databricks-sdk-py/pull/760
142
+ return WorkspaceClient(
143
+ host=debug_env["DATABRICKS_HOST"],
144
+ auth_type=debug_env.get("DATABRICKS_AUTH_TYPE"), # type: ignore
145
+ token=debug_env.get("DATABRICKS_TOKEN"), # type: ignore
146
+ username=debug_env.get("DATABRICKS_USERNAME"), # type: ignore
147
+ password=debug_env.get("DATABRICKS_PASSWORD"), # type: ignore
148
+ client_id=debug_env.get("DATABRICKS_CLIENT_ID"), # type: ignore
149
+ client_secret=debug_env.get("DATABRICKS_CLIENT_SECRET"), # type: ignore
150
+ debug_truncate_bytes=debug_env.get("DATABRICKS_DEBUG_TRUNCATE_BYTES"), # type: ignore
151
+ debug_headers=debug_env.get("DATABRICKS_DEBUG_HEADERS"), # type: ignore
152
+ azure_client_id=debug_env.get("ARM_CLIENT_ID"), # type: ignore
153
+ azure_tenant_id=debug_env.get("ARM_TENANT_ID"), # type: ignore
154
+ azure_client_secret=debug_env.get("ARM_CLIENT_SECRET"), # type: ignore
155
+ cluster_id=debug_env.get("DATABRICKS_CLUSTER_ID"), # type: ignore
156
+ product=product_name,
157
+ product_version=product_version,
158
+ )
159
+
160
+
161
+ @fixture
162
+ def log_workspace_link(ws):
163
+ """Returns a function to log a workspace link."""
164
+
165
+ def inner(name: str, path: str, *, anchor: bool = True):
166
+ a = '#' if anchor else ''
167
+ url = f'https://{ws.config.hostname}/{a}{path}'
168
+ _LOG.info(f'Created {name}: {url}')
169
+
170
+ return inner
@@ -0,0 +1,433 @@
1
+ import logging
2
+ from collections.abc import Generator, Callable
3
+ from pytest import fixture
4
+ from databricks.labs.blueprint.commands import CommandExecutor
5
+ from databricks.sdk.errors import DatabricksError
6
+ from databricks.sdk.service.catalog import (
7
+ FunctionInfo,
8
+ SchemaInfo,
9
+ TableInfo,
10
+ TableType,
11
+ DataSourceFormat,
12
+ CatalogInfo,
13
+ StorageCredentialInfo,
14
+ AwsIamRoleRequest,
15
+ AzureServicePrincipal,
16
+ )
17
+ from databricks.sdk.service.compute import Language
18
+ from databricks.labs.pytester.fixtures.baseline import factory
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ def escape_sql_identifier(path: str, *, maxsplit: int = 2) -> str:
24
+ """
25
+ Escapes the path components to make them SQL safe.
26
+
27
+ Args:
28
+ path (str): The dot-separated path of a catalog object.
29
+ maxsplit (int): The maximum number of splits to perform.
30
+
31
+ Returns:
32
+ str: The path with all parts escaped in backticks.
33
+ """
34
+ if not path:
35
+ return path
36
+ parts = path.split(".", maxsplit=maxsplit)
37
+ escaped = [f"`{part.strip('`').replace('`', '``')}`" for part in parts]
38
+ return ".".join(escaped)
39
+
40
+
41
+ @fixture
42
+ # pylint: disable-next=too-many-statements,too-complex
43
+ def make_table(
44
+ sql_backend,
45
+ make_schema,
46
+ make_random,
47
+ log_workspace_link,
48
+ watchdog_remove_after,
49
+ ) -> Generator[Callable[..., TableInfo], None, None]:
50
+ """
51
+ Create a table and return its info. Remove it after the test. Returns instance of `databricks.sdk.service.catalog.TableInfo`.
52
+
53
+ Keyword Arguments:
54
+ * `catalog_name` (str): The name of the catalog where the table will be created. Default is `hive_metastore`.
55
+ * `schema_name` (str): The name of the schema where the table will be created. Default is a random string.
56
+ * `name` (str): The name of the table. Default is a random string.
57
+ * `ctas` (str): The CTAS statement to create the table. Default is `None`.
58
+ * `non_delta` (bool): If `True`, the table will be created as a non-delta table. Default is `False`.
59
+ * `external` (bool): If `True`, the table will be created as an external table. Default is `False`.
60
+ * `external_csv` (str): The location of the external CSV table. Default is `None`.
61
+ * `external_delta` (str): The location of the external Delta table. Default is `None`.
62
+ * `view` (bool): If `True`, the table will be created as a view. Default is `False`.
63
+ * `tbl_properties` (dict): The table properties. Default is `None`.
64
+ * `hiveserde_ddl` (str): The DDL statement to create the table. Default is `None`.
65
+ * `storage_override` (str): The storage location override. Default is `None`.
66
+ * `columns` (list): The list of columns. Default is `None`.
67
+
68
+ Usage:
69
+ ```python
70
+ def test_catalog_fixture(make_catalog, make_schema, make_table):
71
+ from_catalog = make_catalog()
72
+ from_schema = make_schema(catalog_name=from_catalog.name)
73
+ from_table_1 = make_table(catalog_name=from_catalog.name, schema_name=from_schema.name)
74
+ logger.info(f"Created new schema: {from_table_1}")
75
+ ```
76
+ """
77
+
78
+ def generate_sql_schema(columns: list[tuple[str, str]]) -> str:
79
+ """Generate a SQL schema from columns."""
80
+ schema = "("
81
+ for index, (col_name, type_name) in enumerate(columns):
82
+ schema += escape_sql_identifier(col_name or str(index), maxsplit=0)
83
+ schema += f" {type_name}, "
84
+ schema = schema[:-2] + ")" # Remove the last ', '
85
+ return schema
86
+
87
+ def generate_sql_column_casting(existing_columns: list[tuple[str, str]], new_columns: list[tuple[str, str]]) -> str:
88
+ """Generate the SQL to cast columns"""
89
+ if len(new_columns) > len(existing_columns):
90
+ raise ValueError(f"Too many columns: {new_columns}")
91
+ select_expressions = []
92
+ for index, ((existing_name, _), (new_name, new_type)) in enumerate(zip(existing_columns, new_columns)):
93
+ column_name_new = escape_sql_identifier(new_name or str(index), maxsplit=0)
94
+ select_expression = f"CAST({existing_name} AS {new_type}) AS {column_name_new}"
95
+ select_expressions.append(select_expression)
96
+ select = ", ".join(select_expressions)
97
+ return select
98
+
99
+ def create( # pylint: disable=too-many-locals,too-many-arguments,too-many-statements
100
+ *,
101
+ catalog_name="hive_metastore",
102
+ schema_name: str | None = None,
103
+ name: str | None = None,
104
+ ctas: str | None = None,
105
+ non_delta: bool = False,
106
+ external: bool = False,
107
+ external_csv: str | None = None,
108
+ external_delta: str | None = None,
109
+ view: bool = False,
110
+ tbl_properties: dict[str, str] | None = None,
111
+ hiveserde_ddl: str | None = None,
112
+ storage_override: str | None = None,
113
+ columns: list[tuple[str, str]] | None = None,
114
+ ) -> TableInfo:
115
+ if schema_name is None:
116
+ schema = make_schema(catalog_name=catalog_name)
117
+ catalog_name = schema.catalog_name
118
+ schema_name = schema.name
119
+ if name is None:
120
+ name = f"ucx_T{make_random(4)}".lower()
121
+ table_type: TableType | None = None
122
+ data_source_format = None
123
+ storage_location = None
124
+ view_text = None
125
+ full_name = f"{catalog_name}.{schema_name}.{name}".lower()
126
+ ddl = f'CREATE {"VIEW" if view else "TABLE"} {full_name}'
127
+ if columns is None:
128
+ schema = "(id INT, value STRING)"
129
+ else:
130
+ schema = generate_sql_schema(columns)
131
+ if view:
132
+ table_type = TableType.VIEW
133
+ view_text = ctas
134
+ if ctas is not None:
135
+ # temporary (if not view)
136
+ ddl = f"{ddl} AS {ctas}"
137
+ elif non_delta:
138
+ table_type = TableType.EXTERNAL # pylint: disable=redefined-variable-type
139
+ data_source_format = DataSourceFormat.JSON
140
+ # DBFS locations are not purged; no suffix necessary.
141
+ storage_location = f"dbfs:/tmp/ucx_test_{make_random(4)}"
142
+ if columns is None:
143
+ select = "*"
144
+ else:
145
+ # These are the columns from the JSON dataset below
146
+ dataset_columns = [
147
+ ('calories_burnt', 'STRING'),
148
+ ('device_id', 'STRING'),
149
+ ('id', 'STRING'),
150
+ ('miles_walked', 'STRING'),
151
+ ('num_steps', 'STRING'),
152
+ ('timestamp', 'STRING'),
153
+ ('user_id', 'STRING'),
154
+ ('value', 'STRING'),
155
+ ]
156
+ select = generate_sql_column_casting(dataset_columns, columns)
157
+ # Modified, otherwise it will identify the table as a DB Dataset
158
+ ddl = (
159
+ f"{ddl} USING json location '{storage_location}' as SELECT {select} FROM "
160
+ f"JSON.`dbfs:/databricks-datasets/iot-stream/data-device`"
161
+ )
162
+ elif external_csv is not None:
163
+ table_type = TableType.EXTERNAL
164
+ data_source_format = DataSourceFormat.CSV
165
+ storage_location = external_csv
166
+ ddl = f"{ddl} {schema} USING CSV OPTIONS (header=true) LOCATION '{storage_location}'"
167
+ elif external_delta is not None:
168
+ table_type = TableType.EXTERNAL
169
+ data_source_format = DataSourceFormat.DELTA
170
+ storage_location = external_delta
171
+ ddl = f"{ddl} {schema} LOCATION '{storage_location}'"
172
+ elif external:
173
+ # external table
174
+ table_type = TableType.EXTERNAL
175
+ data_source_format = DataSourceFormat.DELTASHARING
176
+ url = "s3a://databricks-datasets-oregon/delta-sharing/share/open-datasets.share"
177
+ storage_location = f"{url}#delta_sharing.default.lending_club"
178
+ ddl = f"{ddl} USING deltaSharing LOCATION '{storage_location}'"
179
+ else:
180
+ # managed table
181
+ table_type = TableType.MANAGED
182
+ data_source_format = DataSourceFormat.DELTA
183
+ storage_location = f"dbfs:/user/hive/warehouse/{schema_name}/{name}"
184
+ ddl = f"{ddl} {schema}"
185
+ if tbl_properties:
186
+ tbl_properties.update({"RemoveAfter": watchdog_remove_after})
187
+ else:
188
+ tbl_properties = {"RemoveAfter": watchdog_remove_after}
189
+
190
+ str_properties = ",".join([f" '{k}' = '{v}' " for k, v in tbl_properties.items()])
191
+
192
+ # table properties fails with CTAS statements
193
+ alter_table_tbl_properties = ""
194
+ if ctas or non_delta:
195
+ alter_table_tbl_properties = (
196
+ f'ALTER {"VIEW" if view else "TABLE"} {full_name} SET TBLPROPERTIES ({str_properties})'
197
+ )
198
+ else:
199
+ ddl = f"{ddl} TBLPROPERTIES ({str_properties})"
200
+
201
+ if hiveserde_ddl:
202
+ ddl = hiveserde_ddl
203
+ data_source_format = None
204
+ table_type = TableType.EXTERNAL
205
+ storage_location = storage_override
206
+
207
+ sql_backend.execute(ddl)
208
+
209
+ # CTAS AND NON_DELTA does not support TBLPROPERTIES
210
+ if ctas or non_delta:
211
+ sql_backend.execute(alter_table_tbl_properties)
212
+
213
+ table_info = TableInfo(
214
+ catalog_name=catalog_name,
215
+ schema_name=schema_name,
216
+ name=name,
217
+ full_name=full_name,
218
+ properties=tbl_properties,
219
+ storage_location=storage_location,
220
+ table_type=table_type,
221
+ view_definition=view_text,
222
+ data_source_format=data_source_format,
223
+ )
224
+ path = f'explore/data/{table_info.catalog_name}/{table_info.schema_name}/{table_info.name}'
225
+ log_workspace_link(f'{table_info.full_name} schema', path)
226
+ return table_info
227
+
228
+ def remove(table_info: TableInfo):
229
+ try:
230
+ sql_backend.execute(f"DROP TABLE IF EXISTS {table_info.full_name}")
231
+ except DatabricksError as e:
232
+ if "Cannot drop a view" in str(e):
233
+ sql_backend.execute(f"DROP VIEW IF EXISTS {table_info.full_name}")
234
+ else:
235
+ raise e
236
+
237
+ yield from factory("table", create, remove)
238
+
239
+
240
+ @fixture
241
+ def make_schema(
242
+ sql_backend,
243
+ make_random,
244
+ log_workspace_link,
245
+ watchdog_remove_after,
246
+ ) -> Generator[Callable[..., SchemaInfo], None, None]:
247
+ """
248
+ Create a schema and return its info. Remove it after the test. Returns instance of `databricks.sdk.service.catalog.SchemaInfo`.
249
+
250
+ Keyword Arguments:
251
+ * `catalog_name` (str): The name of the catalog where the schema will be created. Default is `hive_metastore`.
252
+ * `name` (str): The name of the schema. Default is a random string.
253
+
254
+ Usage:
255
+ ```python
256
+ def test_catalog_fixture(make_catalog, make_schema, make_table):
257
+ from_catalog = make_catalog()
258
+ from_schema = make_schema(catalog_name=from_catalog.name)
259
+ from_table_1 = make_table(catalog_name=from_catalog.name, schema_name=from_schema.name)
260
+ logger.info(f"Created new schema: {from_table_1}")
261
+ ```
262
+ """
263
+
264
+ def create(*, catalog_name: str = "hive_metastore", name: str | None = None) -> SchemaInfo:
265
+ if name is None:
266
+ name = f"dummy_S{make_random(4)}".lower()
267
+ full_name = f"{catalog_name}.{name}".lower()
268
+ sql_backend.execute(f"CREATE SCHEMA {full_name} WITH DBPROPERTIES (RemoveAfter={watchdog_remove_after})")
269
+ schema_info = SchemaInfo(catalog_name=catalog_name, name=name, full_name=full_name)
270
+ path = f'explore/data/{schema_info.catalog_name}/{schema_info.name}'
271
+ log_workspace_link(f'{schema_info.full_name} schema', path)
272
+ return schema_info
273
+
274
+ def remove(schema_info: SchemaInfo):
275
+ sql_backend.execute(f"DROP SCHEMA IF EXISTS {schema_info.full_name} CASCADE")
276
+
277
+ yield from factory("schema", create, remove)
278
+
279
+
280
+ @fixture
281
+ def make_catalog(ws, sql_backend, make_random, log_workspace_link) -> Generator[Callable[..., CatalogInfo], None, None]:
282
+ """
283
+ Create a catalog and return its info. Remove it after the test.
284
+ Returns instance of `databricks.sdk.service.catalog.CatalogInfo`.
285
+
286
+ Usage:
287
+ ```python
288
+ def test_catalog_fixture(make_catalog, make_schema, make_table):
289
+ from_catalog = make_catalog()
290
+ from_schema = make_schema(catalog_name=from_catalog.name)
291
+ from_table_1 = make_table(catalog_name=from_catalog.name, schema_name=from_schema.name)
292
+ logger.info(f"Created new schema: {from_table_1}")
293
+ ```
294
+ """
295
+
296
+ def create() -> CatalogInfo:
297
+ # Warning: As of 2024-09-04 there is no way to mark this catalog for protection against the watchdog.
298
+ # Ref: https://github.com/databrickslabs/watchdog/blob/cdc97afdac1567e89d3b39d938f066fd6267b3ba/scan/objects/uc.go#L68
299
+ name = f"dummy_C{make_random(4)}".lower()
300
+ sql_backend.execute(f"CREATE CATALOG {name}")
301
+ catalog_info = ws.catalogs.get(name)
302
+ log_workspace_link(f'{name} catalog', f'explore/data/{name}')
303
+ return catalog_info
304
+
305
+ yield from factory(
306
+ "catalog",
307
+ create,
308
+ lambda catalog_info: ws.catalogs.delete(catalog_info.full_name, force=True),
309
+ )
310
+
311
+
312
+ @fixture
313
+ def make_udf(
314
+ ws,
315
+ env_or_skip,
316
+ sql_backend,
317
+ make_schema,
318
+ make_random,
319
+ ) -> Generator[Callable[..., FunctionInfo], None, None]:
320
+ """
321
+ Create a UDF and return its info. Remove it after the test. Returns instance of `databricks.sdk.service.catalog.FunctionInfo`.
322
+
323
+ Keyword Arguments:
324
+ * `catalog_name` (str): The name of the catalog where the UDF will be created. Default is `hive_metastore`.
325
+ * `schema_name` (str): The name of the schema where the UDF will be created. Default is a random string.
326
+ * `name` (str): The name of the UDF. Default is a random string.
327
+ * `hive_udf` (bool): If `True`, the UDF will be created as a Hive UDF. Default is `False`.
328
+
329
+ Usage:
330
+ ```python
331
+ def test_make_some_udfs(make_schema, make_udf):
332
+ schema_a = make_schema(catalog_name="hive_metastore")
333
+ make_udf(schema_name=schema_a.name)
334
+ make_udf(schema_name=schema_a.name, hive_udf=True)
335
+ ```
336
+ """
337
+
338
+ def create(
339
+ *,
340
+ catalog_name="hive_metastore",
341
+ schema_name: str | None = None,
342
+ name: str | None = None,
343
+ hive_udf: bool = False,
344
+ ) -> FunctionInfo:
345
+ if schema_name is None:
346
+ schema = make_schema(catalog_name=catalog_name)
347
+ catalog_name = schema.catalog_name
348
+ schema_name = schema.name
349
+
350
+ if name is None:
351
+ name = f"ucx_t{make_random(4).lower()}"
352
+
353
+ # Note: the Watchdog does not explicitly scan for functions; they are purged along with their parent schema.
354
+ # As such the function can't be marked (and doesn't need to be if the schema as marked) for purge protection.
355
+
356
+ full_name = f"{catalog_name}.{schema_name}.{name}".lower()
357
+ if hive_udf:
358
+ cmd_exec = CommandExecutor(
359
+ ws.clusters,
360
+ ws.command_execution,
361
+ lambda: env_or_skip("TEST_DEFAULT_CLUSTER_ID"),
362
+ language=Language.SQL,
363
+ )
364
+ ddl = f"CREATE FUNCTION {full_name} AS 'org.apache.hadoop.hive.ql.udf.generic.GenericUDFAbs';"
365
+ cmd_exec.run(ddl)
366
+ else:
367
+ ddl = f"CREATE FUNCTION {full_name}(x INT) RETURNS FLOAT CONTAINS SQL DETERMINISTIC RETURN 0;"
368
+ sql_backend.execute(ddl)
369
+ udf_info = FunctionInfo(
370
+ catalog_name=catalog_name,
371
+ schema_name=schema_name,
372
+ name=name,
373
+ full_name=full_name,
374
+ )
375
+
376
+ logger.info(f"Function {udf_info.full_name} created")
377
+ return udf_info
378
+
379
+ def remove(udf_info: FunctionInfo):
380
+ sql_backend.execute(f"DROP FUNCTION IF EXISTS {udf_info.full_name}")
381
+
382
+ yield from factory("table", create, remove)
383
+
384
+
385
+ @fixture
386
+ def make_storage_credential(ws) -> Generator[Callable[..., StorageCredentialInfo], None, None]:
387
+ """
388
+ Create a storage credential and return its info. Remove it after the test. Returns instance of `databricks.sdk.service.catalog.StorageCredentialInfo`.
389
+
390
+ Keyword Arguments:
391
+ * `credential_name` (str): The name of the storage credential. Default is a random string.
392
+ * `application_id` (str): The application ID for the Azure service principal. Default is an empty string.
393
+ * `client_secret` (str): The client secret for the Azure service principal. Default is an empty string.
394
+ * `directory_id` (str): The directory ID for the Azure service principal. Default is an empty string.
395
+ * `aws_iam_role_arn` (str): The ARN of the AWS IAM role. Default is an empty string.
396
+ * `read_only` (bool): If `True`, the storage credential will be read-only. Default is `False`.
397
+
398
+ Usage:
399
+ ```python
400
+ def test_storage_credential(env_or_skip, make_storage_credential, make_random):
401
+ random = make_random(6).lower()
402
+ credential_name = f"dummy-{random}"
403
+ make_storage_credential(
404
+ credential_name=credential_name,
405
+ aws_iam_role_arn=env_or_skip("TEST_UBER_ROLE_ID"),
406
+ )
407
+ ```
408
+ """
409
+
410
+ def create(
411
+ *,
412
+ credential_name: str,
413
+ application_id: str = "",
414
+ client_secret: str = "",
415
+ directory_id: str = "",
416
+ aws_iam_role_arn: str = "",
417
+ read_only=False,
418
+ ) -> StorageCredentialInfo:
419
+ if aws_iam_role_arn != "":
420
+ storage_credential = ws.storage_credentials.create(
421
+ credential_name, aws_iam_role=AwsIamRoleRequest(role_arn=aws_iam_role_arn), read_only=read_only
422
+ )
423
+ else:
424
+ azure_service_principal = AzureServicePrincipal(directory_id, application_id, client_secret)
425
+ storage_credential = ws.storage_credentials.create(
426
+ credential_name, azure_service_principal=azure_service_principal, read_only=read_only
427
+ )
428
+ return storage_credential
429
+
430
+ def remove(storage_credential: StorageCredentialInfo):
431
+ ws.storage_credentials.delete(storage_credential.name, force=True)
432
+
433
+ yield from factory("storage_credential", create, remove)