diff --git a/mlrun/api/utils/clients/iguazio.py b/mlrun/api/utils/clients/iguazio.py index 1870a78ecbc..0aa1a151c82 100644 --- a/mlrun/api/utils/clients/iguazio.py +++ b/mlrun/api/utils/clients/iguazio.py @@ -1,5 +1,6 @@ import copy import datetime +import enum import http import json import typing @@ -416,6 +417,14 @@ def _send_request_to_api( kwargs.setdefault("headers", {})[ mlrun.api.schemas.HeaderNames.projects_role ] = "mlrun" + + # requests no longer supports header values to be enum (https://github.com/psf/requests/pull/6154) + # convert to strings. Do the same for params for niceness + for kwarg in ["headers", "params"]: + dict_ = kwargs.get(kwarg, {}) + for key in dict_.keys(): + if isinstance(dict_[key], enum.Enum): + dict_[key] = dict_[key].value response = self._session.request(method, url, verify=False, **kwargs) if not response.ok: log_kwargs = copy.deepcopy(kwargs) diff --git a/mlrun/api/utils/clients/nuclio.py b/mlrun/api/utils/clients/nuclio.py index 7bc31ba234f..da857dc7c26 100644 --- a/mlrun/api/utils/clients/nuclio.py +++ b/mlrun/api/utils/clients/nuclio.py @@ -1,4 +1,5 @@ import copy +import enum import http import typing @@ -189,6 +190,14 @@ def _send_request_to_api(self, method, path, **kwargs): url = f"{self._api_url}/api/{path}" if kwargs.get("timeout") is None: kwargs["timeout"] = 20 + + # requests no longer supports header values to be enum (https://github.com/psf/requests/pull/6154) + # convert to strings. Do the same for params for niceness + for kwarg in ["headers", "params"]: + dict_ = kwargs.get(kwarg, {}) + for key in dict_.keys(): + if isinstance(dict_[key], enum.Enum): + dict_[key] = dict_[key].value response = self._session.request(method, url, verify=False, **kwargs) if not response.ok: log_kwargs = copy.deepcopy(kwargs) diff --git a/mlrun/db/httpdb.py b/mlrun/db/httpdb.py index 07468e1cdf8..12955d0362b 100644 --- a/mlrun/db/httpdb.py +++ b/mlrun/db/httpdb.py @@ -11,7 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - +import enum import http import os import tempfile @@ -203,6 +203,14 @@ def api_call( {mlrun.api.schemas.HeaderNames.client_version: self.client_version} ) + # requests no longer supports header values to be enum (https://github.com/psf/requests/pull/6154) + # convert to strings. Do the same for params for niceness + for dict_ in [headers, params]: + if dict_ is not None: + for key in dict_.keys(): + if isinstance(dict_[key], enum.Enum): + dict_[key] = dict_[key].value + if not self.session: self.session = requests.Session() self.session.mount("http://", http_adapter) @@ -692,8 +700,6 @@ def list_artifacts( """ project = project or config.default_project - if category and isinstance(category, schemas.ArtifactCategories): - category = category.value params = { "name": name, @@ -822,8 +828,6 @@ def list_runtime_resources( resources are per Function, for which the identifier is the Function's name. :param group_by: Object to group results by. Allowed values are `job` and `project`. """ - if isinstance(group_by, mlrun.api.schemas.ListRuntimeResourcesGroupByField): - group_by = group_by.value params = { "label_selector": label_selector, "group-by": group_by, @@ -1431,8 +1435,6 @@ def list_pipelines( raise mlrun.errors.MLRunInvalidArgumentError( "Filtering by project can not be used together with pagination, or sorting" ) - if isinstance(format_, mlrun.api.schemas.PipelinesFormat): - format_ = format_.value params = { "namespace": namespace, "sort_by": sort_by, @@ -1460,8 +1462,6 @@ def get_pipeline( ): """Retrieve details of a specific pipeline using its run ID (as provided when the pipeline was executed).""" - if isinstance(format_, mlrun.api.schemas.PipelinesFormat): - format_ = format_.value try: params = {} if namespace: @@ -1614,12 +1614,6 @@ def _generate_partition_by_params( order, max_partitions=None, ): - if isinstance(partition_by, partition_by_cls): - partition_by = partition_by.value - if isinstance(sort_by, schemas.SortField): - sort_by = sort_by.value - if isinstance(order, schemas.OrderType): - order = order.value partition_params = { "partition-by": partition_by, @@ -1765,8 +1759,6 @@ def patch_feature_set( """ project = project or config.default_project reference = self._resolve_reference(tag, uid) - if isinstance(patch_mode, schemas.PatchMode): - patch_mode = patch_mode.value headers = {schemas.HeaderNames.patch_mode: patch_mode} path = f"projects/{project}/feature-sets/{name}/references/{reference}" error_message = f"Failed updating feature-set {project}/{name}" @@ -1968,8 +1960,6 @@ def patch_feature_vector( """ reference = self._resolve_reference(tag, uid) project = project or config.default_project - if isinstance(patch_mode, schemas.PatchMode): - patch_mode = patch_mode.value headers = {schemas.HeaderNames.patch_mode: patch_mode} path = f"projects/{project}/feature-vectors/{name}/references/{reference}" error_message = f"Failed updating feature-vector {project}/{name}" @@ -2017,10 +2007,6 @@ def list_projects( :param state: Filter by project's state. Can be either ``online`` or ``archived``. """ - if isinstance(state, mlrun.api.schemas.ProjectState): - state = state.value - if isinstance(format_, mlrun.api.schemas.ProjectsFormat): - format_ = format_.value params = { "owner": owner, "state": state, @@ -2071,8 +2057,6 @@ def delete_project( """ path = f"projects/{name}" - if isinstance(deletion_strategy, schemas.DeletionStrategy): - deletion_strategy = deletion_strategy.value headers = {schemas.HeaderNames.deletion_strategy: deletion_strategy} error_message = f"Failed deleting project {name}" response = self.api_call("DELETE", path, error_message, headers=headers) @@ -2117,8 +2101,6 @@ def patch_project( """ path = f"projects/{name}" - if isinstance(patch_mode, schemas.PatchMode): - patch_mode = patch_mode.value headers = {schemas.HeaderNames.patch_mode: patch_mode} error_message = f"Failed patching project {name}" response = self.api_call( @@ -2240,8 +2222,6 @@ def create_project_secrets( secrets=secrets ) """ - if isinstance(provider, schemas.SecretProviderName): - provider = provider.value path = f"projects/{project}/secrets" secrets_input = schemas.SecretsData(secrets=secrets, provider=provider) body = secrets_input.dict() @@ -2276,9 +2256,6 @@ def list_project_secrets( to this specific project. ``kubernetes`` provider only supports an empty list. """ - if isinstance(provider, schemas.SecretProviderName): - provider = provider.value - if provider == schemas.SecretProviderName.vault.value and not token: raise MLRunInvalidArgumentError( "A vault token must be provided when accessing vault secrets" @@ -2318,9 +2295,6 @@ def list_project_secret_keys( Must be a valid Vault token, with permissions to retrieve secrets of the project in question. """ - if isinstance(provider, schemas.SecretProviderName): - provider = provider.value - if provider == schemas.SecretProviderName.vault.value and not token: raise MLRunInvalidArgumentError( "A vault token must be provided when accessing vault secrets" @@ -2358,8 +2332,6 @@ def delete_project_secrets( :param secrets: A list of secret names to delete. An empty list will delete all secrets assigned to this specific project. """ - if isinstance(provider, schemas.SecretProviderName): - provider = provider.value path = f"projects/{project}/secrets" params = {"provider": provider, "secret": secrets} @@ -2390,8 +2362,6 @@ def create_user_secrets( :param provider: The name of the secrets-provider to work with. Currently only ``vault`` is supported. :param secrets: A set of secret values to store within the Vault. """ - if isinstance(provider, schemas.SecretProviderName): - provider = provider.value path = "user-secrets" secrets_creation_request = schemas.UserSecretCreationRequest( user=user, diff --git a/tests/api/api/test_projects.py b/tests/api/api/test_projects.py index 15270ab446b..917bfe8533d 100644 --- a/tests/api/api/test_projects.py +++ b/tests/api/api/test_projects.py @@ -117,7 +117,7 @@ def test_delete_project_with_resources( response = client.delete( f"projects/{project_to_remove}", headers={ - mlrun.api.schemas.HeaderNames.deletion_strategy: mlrun.api.schemas.DeletionStrategy.check + mlrun.api.schemas.HeaderNames.deletion_strategy: mlrun.api.schemas.DeletionStrategy.check.value }, ) assert response.status_code == HTTPStatus.PRECONDITION_FAILED.value @@ -126,7 +126,7 @@ def test_delete_project_with_resources( response = client.delete( f"projects/{project_to_remove}", headers={ - mlrun.api.schemas.HeaderNames.deletion_strategy: mlrun.api.schemas.DeletionStrategy.restricted + mlrun.api.schemas.HeaderNames.deletion_strategy: mlrun.api.schemas.DeletionStrategy.restricted.value }, ) assert response.status_code == HTTPStatus.PRECONDITION_FAILED.value @@ -135,7 +135,7 @@ def test_delete_project_with_resources( response = client.delete( f"projects/{project_to_remove}", headers={ - mlrun.api.schemas.HeaderNames.deletion_strategy: mlrun.api.schemas.DeletionStrategy.cascading + mlrun.api.schemas.HeaderNames.deletion_strategy: mlrun.api.schemas.DeletionStrategy.cascading.value }, ) assert response.status_code == HTTPStatus.NO_CONTENT.value @@ -174,7 +174,7 @@ def test_delete_project_with_resources( response = client.delete( f"projects/{project_to_remove}", headers={ - mlrun.api.schemas.HeaderNames.deletion_strategy: mlrun.api.schemas.DeletionStrategy.check + mlrun.api.schemas.HeaderNames.deletion_strategy: mlrun.api.schemas.DeletionStrategy.check.value }, ) assert response.status_code == HTTPStatus.NO_CONTENT.value @@ -183,7 +183,7 @@ def test_delete_project_with_resources( response = client.delete( f"projects/{project_to_remove}", headers={ - mlrun.api.schemas.HeaderNames.deletion_strategy: mlrun.api.schemas.DeletionStrategy.restricted + mlrun.api.schemas.HeaderNames.deletion_strategy: mlrun.api.schemas.DeletionStrategy.restricted.value }, ) assert response.status_code == HTTPStatus.NO_CONTENT.value @@ -445,7 +445,7 @@ def test_delete_project_deletion_strategy_check( response = client.delete( f"projects/{project.metadata.name}", headers={ - mlrun.api.schemas.HeaderNames.deletion_strategy: mlrun.api.schemas.DeletionStrategy.check + mlrun.api.schemas.HeaderNames.deletion_strategy: mlrun.api.schemas.DeletionStrategy.check.value }, ) assert response.status_code == HTTPStatus.NO_CONTENT.value @@ -467,7 +467,7 @@ def test_delete_project_deletion_strategy_check( response = client.delete( f"projects/{project.metadata.name}", headers={ - mlrun.api.schemas.HeaderNames.deletion_strategy: mlrun.api.schemas.DeletionStrategy.check + mlrun.api.schemas.HeaderNames.deletion_strategy: mlrun.api.schemas.DeletionStrategy.check.value }, ) assert response.status_code == HTTPStatus.PRECONDITION_FAILED.value @@ -533,7 +533,7 @@ def test_delete_project_not_deleting_versioned_objects_multiple_times( response = client.delete( f"projects/{project_name}", headers={ - mlrun.api.schemas.HeaderNames.deletion_strategy: mlrun.api.schemas.DeletionStrategy.cascading + mlrun.api.schemas.HeaderNames.deletion_strategy: mlrun.api.schemas.DeletionStrategy.cascading.value }, ) assert response.status_code == HTTPStatus.NO_CONTENT.value @@ -576,7 +576,7 @@ def test_delete_project_deletion_strategy_check_external_resource( response = client.delete( f"projects/{project.metadata.name}", headers={ - mlrun.api.schemas.HeaderNames.deletion_strategy: mlrun.api.schemas.DeletionStrategy.restricted + mlrun.api.schemas.HeaderNames.deletion_strategy: mlrun.api.schemas.DeletionStrategy.restricted.value }, ) assert response.status_code == HTTPStatus.PRECONDITION_FAILED.value @@ -586,7 +586,7 @@ def test_delete_project_deletion_strategy_check_external_resource( response = client.delete( f"projects/{project.metadata.name}", headers={ - mlrun.api.schemas.HeaderNames.deletion_strategy: mlrun.api.schemas.DeletionStrategy.restricted + mlrun.api.schemas.HeaderNames.deletion_strategy: mlrun.api.schemas.DeletionStrategy.restricted.value }, ) assert response @@ -771,7 +771,7 @@ def test_projects_crud( response = client.delete( f"projects/{name1}", headers={ - mlrun.api.schemas.HeaderNames.deletion_strategy: mlrun.api.schemas.DeletionStrategy.restricted + mlrun.api.schemas.HeaderNames.deletion_strategy: mlrun.api.schemas.DeletionStrategy.restricted.value }, ) assert response.status_code == HTTPStatus.PRECONDITION_FAILED.value @@ -780,7 +780,7 @@ def test_projects_crud( response = client.delete( f"projects/{name1}", headers={ - mlrun.api.schemas.HeaderNames.deletion_strategy: mlrun.api.schemas.DeletionStrategy.cascading + mlrun.api.schemas.HeaderNames.deletion_strategy: mlrun.api.schemas.DeletionStrategy.cascading.value }, ) assert response.status_code == HTTPStatus.NO_CONTENT.value diff --git a/tests/rundb/test_unit_httpdb.py b/tests/rundb/test_unit_httpdb.py new file mode 100644 index 00000000000..f2b16aaf0c5 --- /dev/null +++ b/tests/rundb/test_unit_httpdb.py @@ -0,0 +1,29 @@ +# test_httpdb.py actually holds integration tests (that should be migrated to tests/integration/sdk_api/httpdb) +# currently we are running it in the integration tests CI step so adding this file for unit tests for the httpdb +import enum +import unittest.mock + +import mlrun.db.httpdb + + +class SomeEnumClass(str, enum.Enum): + value1 = "value1" + value2 = "value2" + + +def test_api_call_enum_conversion(): + db = mlrun.db.httpdb.HTTPRunDB("fake-url") + db.session = unittest.mock.Mock() + + # ensure not exploding when no headers/params + db.api_call("GET", "some-path") + + db.api_call( + "GET", + "some-path", + headers={"enum-value": SomeEnumClass.value1, "string-value": "value"}, + params={"enum-value": SomeEnumClass.value2, "string-value": "value"}, + ) + for dict_key in ["headers", "params"]: + for value in db.session.request.call_args_list[1][1][dict_key].values(): + assert type(value) == str