content: #  Copyright (c) ZenML GmbH 2022. All Rights Reserved.
  #
  #  Licensed under the Apache License, Version 2.0 (the "License");
  #  you may not use this file except in compliance with the License.
  #  You may obtain a copy of the License at:
  #
  #       https://www.apache.org/licenses/LICENSE-2.0
  #
  #  Unless required by applicable law or agreed to in writing, software
  #  distributed under the License is distributed on an "AS IS" BASIS,
  #  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.
  """REST Zen Store implementation."""
  
  import os
  import re
  import time
  from datetime import datetime
  from pathlib import Path
  from threading import RLock
  from typing import (
      Any,
      ClassVar,
      Dict,
      List,
      Optional,
      Sequence,
      Tuple,
      Type,
      TypeVar,
      Union,
  )
  from urllib.parse import urlparse
  from uuid import UUID, uuid4
  
  import requests
  import urllib3
  from pydantic import (
      BaseModel,
      ConfigDict,
      Field,
      PrivateAttr,
      ValidationError,
      field_validator,
      model_validator,
  )
  from requests.adapters import HTTPAdapter, Retry
  
  import zenml
  from zenml.analytics import source_context
  from zenml.config.global_config import GlobalConfiguration
  from zenml.config.pipeline_run_configuration import PipelineRunConfiguration
  from zenml.config.store_config import StoreConfiguration
  from zenml.constants import (
      ACTIONS,
      API,
      API_KEY_ROTATE,
      API_KEYS,
      API_TOKEN,
      ARTIFACT_VERSIONS,
      ARTIFACT_VISUALIZATIONS,
      ARTIFACTS,
      BATCH,
      CODE_REFERENCES,
      CODE_REPOSITORIES,
      CONFIG,
      CURATED_VISUALIZATIONS,
      CURRENT_USER,
      DEACTIVATE,
      DEFAULT_HTTP_TIMEOUT,
      DEPLOYMENTS,
      DEVICES,
      DISABLE_CLIENT_SERVER_MISMATCH_WARNING,
      ENV_ZENML_DISABLE_CLIENT_SERVER_MISMATCH_WARNING,
      EVENT_SOURCES,
      FLAVORS,
      HEARTBEAT,
      INFO,
      LOGIN,
      LOGS,
      MODEL_VERSION_ARTIFACTS,
      MODEL_VERSION_PIPELINE_RUNS,
      MODEL_VERSIONS,
      MODELS,
      PIPELINE_BUILDS,
      PIPELINE_SNAPSHOTS,
      PIPELINES,
      PROJECTS,
      RUN_METADATA,
      RUN_TEMPLATES,
      RUNS,
      SCHEDULES,
      SECRETS,
      SECRETS_BACKUP,
      SECRETS_OPERATIONS,
      SECRETS_RESTORE,
      SERVER_SETTINGS,
      SERVICE_ACCOUNTS,
      SERVICE_CONNECTOR_CLIENT,
      SERVICE_CONNECTOR_RESOURCES,
      SERVICE_CONNECTOR_TYPES,
      SERVICE_CONNECTOR_VERIFY,
      SERVICE_CONNECTOR_VERIFY_REQUEST_TIMEOUT,
      SERVICE_CONNECTORS,
      SERVICES,
      STACK,
      STACK_COMPONENTS,
      STACK_DEPLOYMENT,
      STACKS,
      STEPS,
      TAG_RESOURCES,
      TAGS,
      TRIGGER_EXECUTIONS,
      TRIGGERS,
      USERS,
      VERSION_1,
      ZENML_PRO_API_KEY_PREFIX,
  )
  from zenml.enums import (
      APITokenType,
      OAuthGrantTypes,
      StackDeploymentProvider,
      StoreType,
  )
  from zenml.exceptions import (
      AuthorizationException,
      CredentialsNotValid,
      MethodNotAllowedError,
  )
  from zenml.io import fileio
  from zenml.logger import get_logger
  from zenml.login.credentials import APIToken
  from zenml.login.credentials_store import get_credentials_store
  from zenml.login.pro.client import ZenMLProClient
  from zenml.login.pro.constants import ZENML_PRO_API_URL
  from zenml.login.pro.utils import (
      get_troubleshooting_instructions,
  )
  from zenml.models import (
      ActionFilter,
      ActionRequest,
      ActionResponse,
      ActionUpdate,
      APIKeyFilter,
      APIKeyRequest,
      APIKeyResponse,
      APIKeyRotateRequest,
      APIKeyUpdate,
      ArtifactFilter,
      ArtifactRequest,
      ArtifactResponse,
      ArtifactUpdate,
      ArtifactVersionFilter,
      ArtifactVersionRequest,
      ArtifactVersionResponse,
      ArtifactVersionUpdate,
      ArtifactVisualizationResponse,
      BaseFilter,
      BaseIdentifiedResponse,
      BaseRequest,
      CodeReferenceResponse,
      CodeRepositoryFilter,
      CodeRepositoryRequest,
      CodeRepositoryResponse,
      CodeRepositoryUpdate,
      ComponentFilter,
      ComponentRequest,
      ComponentResponse,
      ComponentUpdate,
      CuratedVisualizationRequest,
      CuratedVisualizationResponse,
      CuratedVisualizationUpdate,
      DeployedStack,
      DeploymentFilter,
      DeploymentRequest,
      DeploymentResponse,
      DeploymentUpdate,
      EventSourceFilter,
      EventSourceRequest,
      EventSourceResponse,
      EventSourceUpdate,
      FlavorFilter,
      FlavorRequest,
      FlavorResponse,
      FlavorUpdate,
      LogsResponse,
      ModelFilter,
      ModelRequest,
      ModelResponse,
      ModelUpdate,
      ModelVersionArtifactFilter,
      ModelVersionArtifactRequest,
      ModelVersionArtifactResponse,
      ModelVersionFilter,
      ModelVersionPipelineRunFilter,
      ModelVersionPipelineRunRequest,
      ModelVersionPipelineRunResponse,
      ModelVersionRequest,
      ModelVersionResponse,
      ModelVersionUpdate,
      OAuthDeviceFilter,
      OAuthDeviceResponse,
      OAuthDeviceUpdate,
      OAuthTokenResponse,
      Page,
      PipelineBuildFilter,
      PipelineBuildRequest,
      PipelineBuildResponse,
      PipelineFilter,
      PipelineRequest,
      PipelineResponse,
      PipelineRunFilter,
      PipelineRunRequest,
      PipelineRunResponse,
      PipelineRunUpdate,
      PipelineSnapshotFilter,
      PipelineSnapshotRequest,
      PipelineSnapshotResponse,
      PipelineSnapshotRunRequest,
      PipelineSnapshotUpdate,
      PipelineUpdate,
      ProjectFilter,
      ProjectRequest,
      ProjectResponse,
      ProjectUpdate,
      RunMetadataRequest,
      RunTemplateFilter,
      RunTemplateRequest,
      RunTemplateResponse,
      RunTemplateUpdate,
      ScheduleFilter,
      ScheduleRequest,
      ScheduleResponse,
      ScheduleUpdate,
      SecretFilter,
      SecretRequest,
      SecretResponse,
      SecretUpdate,
      ServerModel,
      ServerSettingsResponse,
      ServerSettingsUpdate,
      ServiceAccountFilter,
      ServiceAccountRequest,
      ServiceAccountResponse,
      ServiceAccountUpdate,
      ServiceConnectorFilter,
      ServiceConnectorRequest,
      ServiceConnectorResourcesModel,
      ServiceConnectorResponse,
      ServiceConnectorTypeModel,
      ServiceConnectorUpdate,
      ServiceFilter,
      ServiceRequest,
      ServiceResponse,
      ServiceUpdate,
      StackDeploymentConfig,
      StackDeploymentInfo,
      StackFilter,
      StackRequest,
      StackResponse,
      StackUpdate,
      StepHeartbeatResponse,
      StepRunFilter,
      StepRunRequest,
      StepRunResponse,
      StepRunUpdate,
      TagFilter,
      TagRequest,
      TagResourceRequest,
      TagResourceResponse,
      TagResponse,
      TagUpdate,
      TriggerExecutionFilter,
      TriggerExecutionResponse,
      TriggerFilter,
      TriggerRequest,
      TriggerResponse,
      TriggerUpdate,
      UserFilter,
      UserRequest,
      UserResponse,
      UserUpdate,
  )
  from zenml.service_connectors.service_connector_registry import (
      service_connector_registry,
  )
  from zenml.utils.networking_utils import (
      replace_localhost_with_internal_hostname,
  )
  from zenml.utils.pydantic_utils import before_validator_handler
  from zenml.utils.time_utils import utc_now
  from zenml.zen_server.exceptions import exception_from_response
  from zenml.zen_stores.base_zen_store import BaseZenStore
  
  logger = get_logger(__name__)
  
  # type alias for possible json payloads (the Anys are recursive Json instances)
  Json = Union[Dict[str, Any], List[Any], str, int, float, bool, None]
  
  
  AnyRequest = TypeVar("AnyRequest", bound=BaseRequest)
  AnyResponse = TypeVar("AnyResponse", bound=BaseIdentifiedResponse)  # type: ignore[type-arg]
  
  
  class RestZenStoreConfiguration(StoreConfiguration):
      """REST ZenML store configuration.
  
      Attributes:
          type: The type of the store.
          username: The username to use to connect to the Zen server.
          password: The password to use to connect to the Zen server.
          api_key: The service account API key to use to connect to the Zen
              server. This is only set if the API key is configured explicitly via
              environment variables or the ZenML global configuration file. API
              keys configured via the CLI are stored in the credentials store
              instead.
          verify_ssl: Either a boolean, in which case it controls whether we
              verify the server's TLS certificate, or a string, in which case it
              must be a path to a CA bundle to use or the CA bundle value itself.
          http_timeout: The timeout to use for all requests.
          connection_pool_size: The size of the connection pool to use for all
              requests.
  
      """
  
      type: StoreType = StoreType.REST
  
      verify_ssl: Union[bool, str] = Field(
          default=True, union_mode="left_to_right"
      )
      http_timeout: int = DEFAULT_HTTP_TIMEOUT
      connection_pool_size: int = 10
  
      @field_validator("url")
      @classmethod
      def validate_url(cls, url: str) -> str:
          """Validates that the URL is a well-formed REST store URL.
  
          Args:
              url: The URL to be validated.
  
          Returns:
              The validated URL without trailing slashes.
  
          Raises:
              ValueError: If the URL is not a well-formed REST store URL.
          """
          url = url.rstrip("/")
          scheme = re.search("^([a-z0-9]+://)", url)
          if scheme is None or scheme.group() not in ("https://", "http://"):
              raise ValueError(
                  "Invalid URL for REST store: {url}. Should be in the form "
                  "https://hostname[:port] or http://hostname[:port]."
              )
  
          # When running inside a container, if the URL uses localhost, the
          # target service will not be available. We try to replace localhost
          # with one of the special Docker or K3D internal hostnames.
          url = replace_localhost_with_internal_hostname(url)
  
          return url
  
      @field_validator("verify_ssl")
      @classmethod
      def validate_verify_ssl(
          cls, verify_ssl: Union[bool, str]
      ) -> Union[bool, str]:
          """Validates that the verify_ssl either points to a file or is a bool.
  
          Args:
              verify_ssl: The verify_ssl value to be validated.
  
          Returns:
              The validated verify_ssl value.
          """
          secret_folder = Path(
              GlobalConfiguration().local_stores_path,
              "certificates",
          )
          if isinstance(verify_ssl, bool) or verify_ssl.startswith(
              str(secret_folder)
          ):
              return verify_ssl
  
          if os.path.isfile(verify_ssl):
              with open(verify_ssl, "r") as f:
                  cert_content = f.read()
          else:
              cert_content = verify_ssl
  
          fileio.makedirs(str(secret_folder))
          file_path = Path(secret_folder, "ca_bundle.pem")
          with os.fdopen(
              os.open(file_path, flags=os.O_RDWR | os.O_CREAT, mode=0o600), "w"
          ) as f:
              f.write(cert_content)
  
          return str(file_path)
  
      @classmethod
      def supports_url_scheme(cls, url: str) -> bool:
          """Check if a URL scheme is supported by this store.
  
          Args:
              url: The URL to check.
  
          Returns:
              True if the URL scheme is supported, False otherwise.
          """
          return urlparse(url).scheme in ("http", "https")
  
      @model_validator(mode="before")
      @classmethod
      @before_validator_handler
      def _move_credentials(cls, data: Dict[str, Any]) -> Dict[str, Any]:
          """Moves credentials (API keys, API tokens, passwords) from the config to the credentials store.
  
          Args:
              data: The values dict used to instantiate the model.
  
          Returns:
              The values dict without credentials.
          """
          url = data.get("url")
          if not url:
              return data
  
          url = replace_localhost_with_internal_hostname(url)
  
          if api_token := data.pop("api_token", None):
              credentials_store = get_credentials_store()
              credentials_store.set_bare_token(url, api_token)
  
          username = data.pop("username", None)
          password = data.pop("password", None)
          if username is not None and password is not None:
              credentials_store = get_credentials_store()
              credentials_store.set_password(url, username, password)
  
          if api_key := data.pop("api_key", None):
              credentials_store = get_credentials_store()
              if api_key.startswith(ZENML_PRO_API_KEY_PREFIX):
                  credentials_store.set_api_key(
                      ZENML_PRO_API_URL, api_key, is_zenml_pro=True
                  )
              else:
                  credentials_store.set_api_key(url, api_key)
  
          return data
  
      model_config = ConfigDict(
          # Don't validate attributes when assigning them. This is necessary
          # because the `verify_ssl` attribute can be expanded to the contents
          # of the certificate file.
          validate_assignment=False,
          # Ignore extra attributes set in the class.
          extra="ignore",
      )
  
  
  class RestZenStore(BaseZenStore):
      """Store implementation for accessing data from a REST API."""
  
      config: RestZenStoreConfiguration
      TYPE: ClassVar[StoreType] = StoreType.REST
      CONFIG_TYPE: ClassVar[Type[StoreConfiguration]] = RestZenStoreConfiguration
      _api_token: Optional[APIToken] = None
      _session: Optional[requests.Session] = None
      _server_info: Optional[ServerModel] = None
      _session_lock: RLock = PrivateAttr(default_factory=RLock)
      _last_authenticated: Optional[datetime] = None
  
      # ====================================
      # ZenML Store interface implementation
      # ====================================
  
      # --------------------------------
      # Initialization and configuration
      # --------------------------------
  
      def _initialize(self) -> None:
          """Initialize the REST store.
  
          Raises:
              RuntimeError: If the store cannot be initialized.
              AuthorizationException: If the store cannot be initialized due to
                  authentication errors.
          """
          try:
              client_version = zenml.__version__
              server_version = self.server_info.version
  
          # Handle cases where the ZenML server is not available
          except ConnectionError as e:
              error_message = (
                  f"Cannot connect to the ZenML server at {self.url}."
              )
              if urlparse(self.url).hostname in [
                  "localhost",
                  "127.0.0.1",
                  "host.docker.internal",
              ]:
                  recommendation = (
                      "Please run `zenml login --local --restart` to restart the "
                      "server."
                  )
              else:
                  recommendation = (
                      f"Please run `zenml login {self.url}` to reconnect to the "
                      "server."
                  )
              raise RuntimeError(f"{error_message}\n{recommendation}") from e
  
          except AuthorizationException as e:
              raise AuthorizationException(
                  f"Authorization failed for store at '{self.url}'. Please check "
                  f"your credentials: {str(e)}"
              )
  
          except Exception as e:
              zenml_pro_extra = ""
              if ".zenml.io" in self.url:
                  zenml_pro_extra = (
                      "\nHINT: " + get_troubleshooting_instructions(self.url)
                  )
              raise RuntimeError(
                  f"Error connecting to URL "
                  f"'{self.url}': {str(e)}" + zenml_pro_extra
              ) from e
  
          if not DISABLE_CLIENT_SERVER_MISMATCH_WARNING and (
              server_version != client_version
          ):
              logger.warning(
                  "Your ZenML client version (%s) does not match the server "
                  "version (%s). This version mismatch might lead to errors or "
                  "unexpected behavior. \nTo disable this warning message, set "
                  "the environment variable `%s=True`",
                  client_version,
                  server_version,
                  ENV_ZENML_DISABLE_CLIENT_SERVER_MISMATCH_WARNING,
              )
  
      @property
      def server_info(self) -> ServerModel:
          """Get cached information about the server.
  
          Returns:
              Cached information about the server.
          """
          if self._server_info is None:
              return self.get_store_info()
          return self._server_info
  
      def get_store_info(self) -> ServerModel:
          """Get information about the server.
  
          Returns:
              Information about the server.
          """
          body = self.get(INFO)
          self._server_info = ServerModel.model_validate(body)
          return self._server_info
  
      def get_deployment_id(self) -> UUID:
          """Get the ID of the deployment.
  
          Returns:
              The ID of the deployment.
          """
          return self.server_info.id
  
      # -------------------- Server Settings --------------------
  
      def get_server_settings(
          self, hydrate: bool = True
      ) -> ServerSettingsResponse:
          """Get the server settings.
  
          Args:
              hydrate: Flag deciding whether to hydrate the output model(s)
                  by including metadata fields in the response.
  
          Returns:
              The server settings.
          """
          response_body = self.get(SERVER_SETTINGS, params={"hydrate": hydrate})
          return ServerSettingsResponse.model_validate(response_body)
  
      def update_server_settings(
          self, settings_update: ServerSettingsUpdate
      ) -> ServerSettingsResponse:
          """Update the server settings.
  
          Args:
              settings_update: The server settings update.
  
          Returns:
              The updated server settings.
          """
          response_body = self.put(SERVER_SETTINGS, body=settings_update)
          return ServerSettingsResponse.model_validate(response_body)
  
      # -------------------- Actions  --------------------
  
      def create_action(self, action: ActionRequest) -> ActionResponse:
          """Create an action.
  
          Args:
              action: The action to create.
  
          Returns:
              The created action.
          """
          return self._create_resource(
              resource=action,
              route=ACTIONS,
              response_model=ActionResponse,
          )
  
      def get_action(
          self,
          action_id: UUID,
          hydrate: bool = True,
      ) -> ActionResponse:
          """Get an action by ID.
  
          Args:
              action_id: The ID of the action to get.
              hydrate: Flag deciding whether to hydrate the output model(s)
                  by including metadata fields in the response.
  
          Returns:
              The action.
          """
          return self._get_resource(
              resource_id=action_id,
              route=ACTIONS,
              response_model=ActionResponse,
              params={"hydrate": hydrate},
          )
  
      def list_actions(
          self,
          action_filter_model: ActionFilter,
          hydrate: bool = False,
      ) -> Page[ActionResponse]:
          """List all actions matching the given filter criteria.
  
          Args:
              action_filter_model: All filter parameters including pagination
                  params.
              hydrate: Flag deciding whether to hydrate the output model(s)
                  by including metadata fields in the response.
  
          Returns:
              A list of all actions matching the filter criteria.
          """
          return self._list_paginated_resources(
              route=ACTIONS,
              response_model=ActionResponse,
              filter_model=action_filter_model,
              params={"hydrate": hydrate},
          )
  
      def update_action(
          self,
          action_id: UUID,
          action_update: ActionUpdate,
      ) -> ActionResponse:
          """Update an existing action.
  
          Args:
              action_id: The ID of the action to update.
              action_update: The update to be applied to the action.
  
          Returns:
              The updated action.
          """
          return self._update_resource(
              resource_id=action_id,
              resource_update=action_update,
              route=ACTIONS,
              response_model=ActionResponse,
          )
  
      def delete_action(self, action_id: UUID) -> None:
          """Delete an action.
  
          Args:
              action_id: The ID of the action to delete.
          """
          self._delete_resource(
              resource_id=action_id,
              route=ACTIONS,
          )
  
      # ----------------------------- API Keys -----------------------------
  
      def create_api_key(
          self, service_account_id: UUID, api_key: APIKeyRequest
      ) -> APIKeyResponse:
          """Create a new API key for a service account.
  
          Args:
              service_account_id: The ID of the service account for which to
                  create the API key.
              api_key: The API key to create.
  
          Returns:
              The created API key.
          """
          return self._create_resource(
              resource=api_key,
              route=f"{SERVICE_ACCOUNTS}/{str(service_account_id)}{API_KEYS}",
              response_model=APIKeyResponse,
          )
  
      def get_api_key(
          self,
          service_account_id: UUID,
          api_key_name_or_id: Union[str, UUID],
          hydrate: bool = True,
      ) -> APIKeyResponse:
          """Get an API key for a service account.
  
          Args:
              service_account_id: The ID of the service account for which to fetch
                  the API key.
              api_key_name_or_id: The name or ID of the API key to get.
              hydrate: Flag deciding whether to hydrate the output model(s)
                  by including metadata fields in the response.
  
          Returns:
              The API key with the given ID.
          """
          return self._get_resource(
              resource_id=api_key_name_or_id,
              route=f"{SERVICE_ACCOUNTS}/{str(service_account_id)}{API_KEYS}",
              response_model=APIKeyResponse,
              params={"hydrate": hydrate},
          )
  
      def list_api_keys(
          self,
          service_account_id: UUID,
          filter_model: APIKeyFilter,
          hydrate: bool = False,
      ) -> Page[APIKeyResponse]:
          """List all API keys for a service account matching the given filter criteria.
  
          Args:
              service_account_id: The ID of the service account for which to list
                  the API keys.
              filter_model: All filter parameters including pagination
                  params
              hydrate: Flag deciding whether to hydrate the output model(s)
                  by including metadata fields in the response.
  
          Returns:
              A list of all API keys matching the filter criteria.
          """
          return self._list_paginated_resources(
              route=f"{SERVICE_ACCOUNTS}/{str(service_account_id)}{API_KEYS}",
              response_model=APIKeyResponse,
              filter_model=filter_model,
              params={"hydrate": hydrate},
          )
  
      def update_api_key(
          self,
          service_account_id: UUID,
          api_key_name_or_id: Union[str, UUID],
          api_key_update: APIKeyUpdate,
      ) -> APIKeyResponse:
          """Update an API key for a service account.
  
          Args:
              service_account_id: The ID of the service account for which to
                  update the API key.
              api_key_name_or_id: The name or ID of the API key to update.
              api_key_update: The update request on the API key.
  
          Returns:
              The updated API key.
          """
          return self._update_resource(
              resource_id=api_key_name_or_id,
              resource_update=api_key_update,
              route=f"{SERVICE_ACCOUNTS}/{str(service_account_id)}{API_KEYS}",
              response_model=APIKeyResponse,
          )
  
      def rotate_api_key(
          self,
          service_account_id: UUID,
          api_key_name_or_id: Union[str, UUID],
          rotate_request: APIKeyRotateRequest,
      ) -> APIKeyResponse:
          """Rotate an API key for a service account.
  
          Args:
              service_account_id: The ID of the service account for which to
                  rotate the API key.
              api_key_name_or_id: The name or ID of the API key to rotate.
              rotate_request: The rotate request on the API key.
  
          Returns:
              The updated API key.
          """
          response_body = self.put(
              f"{SERVICE_ACCOUNTS}/{str(service_account_id)}{API_KEYS}/{str(api_key_name_or_id)}{API_KEY_ROTATE}",
              body=rotate_request,
          )
          return APIKeyResponse.model_validate(response_body)
  
      def delete_api_key(
          self,
          service_account_id: UUID,
          api_key_name_or_id: Union[str, UUID],
      ) -> None:
          """Delete an API key for a service account.
  
          Args:
              service_account_id: The ID of the service account for which to
                  delete the API key.
              api_key_name_or_id: The name or ID of the API key to delete.
          """
          self._delete_resource(
              resource_id=api_key_name_or_id,
              route=f"{SERVICE_ACCOUNTS}/{str(service_account_id)}{API_KEYS}",
          )
  
      # ----------------------------- Services -----------------------------
  
      def create_service(
          self, service_request: ServiceRequest
      ) -> ServiceResponse:
          """Create a new service.
  
          Args:
              service_request: The service to create.
  
          Returns:
              The created service.
          """
          return self._create_resource(
              resource=service_request,
              response_model=ServiceResponse,
              route=SERVICES,
          )
  
      def get_service(
          self, service_id: UUID, hydrate: bool = True
      ) -> ServiceResponse:
          """Get a service.
  
          Args:
              service_id: The ID of the service to get.
              hydrate: Flag deciding whether to hydrate the output model(s)
                  by including metadata fields in the response.
  
          Returns:
              The service.
          """
          return self._get_resource(
              resource_id=service_id,
              route=SERVICES,
              response_model=ServiceResponse,
              params={"hydrate": hydrate},
          )
  
      def list_services(
          self, filter_model: ServiceFilter, hydrate: bool = False
      ) -> Page[ServiceResponse]:
          """List all services matching the given filter criteria.
  
          Args:
              filter_model: All filter parameters including pagination
                  params.
              hydrate: Flag deciding whether to hydrate the output model(s)
                  by including metadata fields in the response.
  
          Returns:
              A list of all services matching the filter criteria.
          """
          return self._list_paginated_resources(
              route=SERVICES,
              response_model=ServiceResponse,
              filter_model=filter_model,
              params={"hydrate": hydrate},
          )
  
      def update_service(
          self, service_id: UUID, update: ServiceUpdate
      ) -> ServiceResponse:
          """Update a service.
  
          Args:
              service_id: The ID of the service to update.
              update: The update to be applied to the service.
  
          Returns:
              The updated service.
          """
          return self._update_resource(
              resource_id=service_id,
              resource_update=update,
              response_model=ServiceResponse,
              route=SERVICES,
          )
  
      def delete_service(self, service_id: UUID) -> None:
          """Delete a service.
  
          Args:
              service_id: The ID of the service to delete.
          """
          self._delete_resource(resource_id=service_id, route=SERVICES)
  
      # ----------------------------- Artifacts -----------------------------
  
      def create_artifact(self, artifact: ArtifactRequest) -> ArtifactResponse:
          """Creates a new artifact.
  
          Args:
              artifact: The artifact to create.
  
          Returns:
              The newly created artifact.
          """
          return self._create_resource(
              resource=artifact,
              response_model=ArtifactResponse,
              route=ARTIFACTS,
          )
  
      def get_artifact(
          self, artifact_id: UUID, hydrate: bool = True
      ) -> ArtifactResponse:
          """Gets an artifact.
  
          Args:
              artifact_id: The ID of the artifact to get.
              hydrate: Flag deciding whether to hydrate the output model(s)
                  by including metadata fields in the response.
  
          Returns:
              The artifact.
          """
          return self._get_resource(
              resource_id=artifact_id,
              route=ARTIFACTS,
              response_model=ArtifactResponse,
              params={"hydrate": hydrate},
          )
  
      def list_artifacts(
          self, filter_model: ArtifactFilter, hydrate: bool = False
      ) -> Page[ArtifactResponse]:
          """List all artifacts matching the given filter criteria.
  
          Args:
              filter_model: All filter parameters including pagination
                  params.
              hydrate: Flag deciding whether to hydrate the output model(s)
                  by including metadata fields in the response.
  
          Returns:
              A list of all artifacts matching the filter criteria.
          """
          return self._list_paginated_resources(
              route=ARTIFACTS,
              response_model=ArtifactResponse,
              filter_model=filter_model,
              params={"hydrate": hydrate},
          )
  
      def update_artifact(
          self, artifact_id: UUID, artifact_update: ArtifactUpdate
      ) -> ArtifactResponse:
          """Updates an artifact.
  
          Args:
              artifact_id: The ID of the artifact to update.
              artifact_update: The update to be applied to the artifact.
  
          Returns:
              The updated artifact.
          """
          return self._update_resource(
              resource_id=artifact_id,
              resource_update=artifact_update,
              response_model=ArtifactResponse,
              route=ARTIFACTS,
          )
  
      def delete_artifact(self, artifact_id: UUID) -> None:
          """Deletes an artifact.
  
          Args:
              artifact_id: The ID of the artifact to delete.
          """
          self._delete_resource(resource_id=artifact_id, route=ARTIFACTS)
  
      # -------------------- Artifact Versions --------------------
  
      def create_artifact_version(
          self, artifact_version: ArtifactVersionRequest
      ) -> ArtifactVersionResponse:
          """Creates an artifact version.
  
          Args:
              artifact_version: The artifact version to create.
  
          Returns:
              The created artifact version.
          """
          return self._create_resource(
              resource=artifact_version,
              response_model=ArtifactVersionResponse,
              route=ARTIFACT_VERSIONS,
          )
  
      def batch_create_artifact_versions(
          self, artifact_versions: List[ArtifactVersionRequest]
      ) -> List[ArtifactVersionResponse]:
          """Creates a batch of artifact versions.
  
          Args:
              artifact_versions: The artifact versions to create.
  
          Returns:
              The created artifact versions.
          """
          return self._batch_create_resources(
              resources=artifact_versions,
              response_model=ArtifactVersionResponse,
              route=ARTIFACT_VERSIONS,
          )
  
      def get_artifact_version(
          self, artifact_version_id: UUID, hydrate: bool = True
      ) -> ArtifactVersionResponse:
          """Gets an artifact.
  
          Args:
              artifact_version_id: The ID of the artifact version to get.
              hydrate: Flag deciding whether to hydrate the output model(s)
                  by including metadata fields in the response.
  
          Returns:
              The artifact version.
          """
          return self._get_resource(
              resource_id=artifact_version_id,
              route=ARTIFACT_VERSIONS,
              response_model=ArtifactVersionResponse,
              params={"hydrate": hydrate},
          )
  
      def list_artifact_versions(
          self,
          artifact_version_filter_model: ArtifactVersionFilter,
          hydrate: bool = False,
      ) -> Page[ArtifactVersionResponse]:
          """List all artifact versions matching the given filter criteria.
  
          Args:
              artifact_version_filter_model: All filter parameters including
                  pagination params.
              hydrate: Flag deciding whether to hydrate the output model(s)
                  by including metadata fields in the response.
  
          Returns:
              A list of all artifact versions matching the filter criteria.
          """
          return self._list_paginated_resources(
              route=ARTIFACT_VERSIONS,
  