diff --git a/iib/web/models.py b/iib/web/models.py index ea172fde5..a47829fd3 100644 --- a/iib/web/models.py +++ b/iib/web/models.py @@ -18,7 +18,15 @@ from iib.exceptions import ValidationError from iib.web import db - +from iib.web.pydantic_models import ( + AddPydanticModel, + CreateEmptyIndexPydanticModel, + FbcOperationsPydanticModel, + MergeIndexImagePydanticModel, + RecursiveRelatedBundlesPydanticModel, + RegenerateBundlePydanticModel, + RmPydanticModel, +) from iib.web.iib_static_types import ( AddRequestPayload, @@ -1002,6 +1010,20 @@ def _from_json( db.session.add(batch) request_kwargs['batch'] = batch + @staticmethod + def from_json_replacement( + request_kwargs: RequestPayload, + batch: Optional[Batch] = None, + ): + # current_user.is_authenticated is only ever False when auth is disabled + if current_user.is_authenticated: + request_kwargs['user'] = current_user + + # Add the request to a new batch + batch = batch or Batch() + db.session.add(batch) + request_kwargs['batch'] = batch + def get_common_index_image_json(self) -> CommonIndexImageResponseBase: """ Return the common set of attributes for an index image request. @@ -1177,6 +1199,50 @@ def from_json( # type: ignore[override] # noqa: F821 request.add_state('in_progress', 'The request was initiated') return request + def from_json_replacement( + cls, + payload: AddPydanticModel, + batch: Optional[Batch] = None, + ): + """ + Handle JSON requests for the builds/add API endpoint. + + :param AddPydanticModel payload: the Pydantic model representing the request. + :param Batch batch: the batch to specify with the request. + """ + request_kwargs = payload.get_json_for_request() + + # setup user and batch in request + # cls.from_json_replacement( + # cast(RequestPayload, request_kwargs), + # batch=batch, + # ) + + # current_user.is_authenticated is only ever False when auth is disabled + if current_user.is_authenticated: + request_kwargs['user'] = current_user + + # Add the request to a new batch + batch = batch or Batch() + db.session.add(batch) + request_kwargs['batch'] = batch + + + request_kwargs["bundles"] = [ + Image.get_or_create(pull_specification=item) for item in payload.bundles + ] + request_kwargs["deprecation_list"] = [ + Image.get_or_create(pull_specification=item) for item in payload.deprecation_list + ] + + request = cls(**request_kwargs) + + for bt in payload.build_tags: + request.add_build_tag(bt) + + request.add_state('in_progress', 'The request was initiated') + return request + def to_json(self, verbose: Optional[bool] = True) -> AddRequestResponse: """ Provide the JSON representation of an "add" build request. @@ -1276,6 +1342,38 @@ def from_json( # type: ignore[override] # noqa: F821 return request + def from_json_replacement( + cls, + payload: RmPydanticModel, + batch: Optional[Batch] = None, + ): + """ + Handle JSON requests for the builds/rm API endpoint. + + :param RmPydanticModel payload: the Pydantic model representing the request. + :param Batch batch: the batch to specify with the request. + """ + request_kwargs = payload.get_json_for_request() + + request_kwargs['operators'] = [Operator.get_or_create(name=item) for item in payload.operators] + request_kwargs['from_index'] = Image.get_or_create(pull_specification=request_kwargs['from_index']) + + if current_user.is_authenticated: + request_kwargs['user'] = current_user + + # Add the request to a new batch + batch = batch or Batch() + db.session.add(batch) + request_kwargs['batch'] = batch + + request = cls(**request_kwargs) + request.add_state('in_progress', 'The request was initiated') + + for bt in payload.build_tags: + request.add_build_tag(bt) + + return request + def to_json(self, verbose: Optional[bool] = True) -> AddRmRequestResponseBase: """ Provide the JSON representation of an "rm" build request. @@ -1422,6 +1520,36 @@ def from_json( # type: ignore[override] # noqa: F821 request.add_state('in_progress', 'The request was initiated') return request + def from_json_replacement( + cls, + payload: RegenerateBundlePydanticModel, + batch: Optional[Batch] = None, + ): + """ + Handle JSON requests for the builds/egenerate-bundle API endpoint. + + :param RegenerateBundlePydanticModel payload: the Pydantic model representing the request. + :param Batch batch: the batch to specify with the request. + """ + request_kwargs = payload.get_json_for_request() + + request_kwargs['from_bundle_image'] = Image.get_or_create( + pull_specification=payload.from_bundle_image + ) + + # current_user.is_authenticated is only ever False when auth is disabled + if current_user.is_authenticated: + request_kwargs['user'] = current_user + + # Add the request to a new batch + batch = batch or Batch() + db.session.add(batch) + request_kwargs['batch'] = batch + + request = cls(**request_kwargs) + request.add_state('in_progress', 'The request was initiated') + return request + def to_json(self, verbose: Optional[bool] = True) -> RegenerateBundleRequestResponse: """ Provide the JSON representation of a "regenerate-bundle" build request. @@ -1624,6 +1752,45 @@ def from_json( # type: ignore[override] # noqa: F821 request.add_state('in_progress', 'The request was initiated') return request + def from_json_replacement( + cls, + payload: MergeIndexImagePydanticModel, + batch: Optional[Batch] = None, + ): + """ + Handle JSON requests for the builds/merge-index-image API endpoint. + + :param MergeIndexImagePydanticModel payload: the Pydantic model representing the request. + :param Batch batch: the batch to specify with the request. + """ + request_kwargs = payload.get_json_for_request() + + request_kwargs['deprecation_list'] = [ + Image.get_or_create(pull_specification=item) for item in payload.deprecation_list + ] + request_kwargs['source_from_index'] = Image.get_or_create( + pull_specification=payload.source_from_index + ) + request_kwargs['target_index'] = Image.get_or_create(pull_specification=payload.target_index) + request_kwargs['binary_image'] = Image.get_or_create(pull_specification=payload.binary_image) + + # current_user.is_authenticated is only ever False when auth is disabled + if current_user.is_authenticated: + request_kwargs['user'] = current_user + + # Add the request to a new batch + batch = batch or Batch() + db.session.add(batch) + request_kwargs['batch'] = batch + + request = cls(**request_kwargs) + + for bt in payload.build_tags: + request.add_build_tag(bt) + + request.add_state('in_progress', 'The request was initiated') + return request + def to_json(self, verbose: Optional[bool] = True) -> MergeIndexImageRequestResponse: """ Provide the JSON representation of an "merge-index-image" build request. @@ -1896,6 +2063,36 @@ def from_json( # type: ignore[override] # noqa: F821 return request + def from_json_replacement( + cls, + payload: CreateEmptyIndexPydanticModel, + batch: Optional[Batch] = None, + ): + """ + Handle JSON requests for the builds/create-empty-index API endpoint. + + :param CreateEmptyIndexPydanticModel payload: the Pydantic model representing the request. + :param Batch batch: the batch to specify with the request. + """ + request_kwargs = payload.get_json_for_request() + + request_kwargs['binary_image'] = Image.get_or_create(pull_specification=payload.binary_image) + request_kwargs['from_index'] = Image.get_or_create(pull_specification=payload.from_index) + + # current_user.is_authenticated is only ever False when auth is disabled + if current_user.is_authenticated: + request_kwargs['user'] = current_user + + # Add the request to a new batch + batch = batch or Batch() + db.session.add(batch) + request_kwargs['batch'] = batch + + request = cls(**request_kwargs) + request.add_state('in_progress', 'The request was initiated') + + return request + def to_json(self, verbose: Optional[bool] = True) -> CreateEmptyIndexRequestResponse: """ Provide the JSON representation of an "create-empty-index" build request. @@ -2020,6 +2217,35 @@ def from_json( # type: ignore[override] # noqa: F821 request.add_state('in_progress', 'The request was initiated') return request + def from_json_replacement( + cls, + payload: RecursiveRelatedBundlesPydanticModel, + batch: Optional[Batch] = None, + ): + """ + Handle JSON requests for the builds/recursive-related-bundles API endpoint. + + :param RecursiveRelatedBundlesPydanticModel payload: the Pydantic model representing the request. + :param Batch batch: the batch to specify with the request. + """ + + request_kwargs = payload.get_json_for_request() + + request_kwargs['parent_bundle_image'] = Image.get_or_create(pull_specification=payload.parent_bundle_image) + + # current_user.is_authenticated is only ever False when auth is disabled + if current_user.is_authenticated: + request_kwargs['user'] = current_user + + # Add the request to a new batch + batch = batch or Batch() + db.session.add(batch) + request_kwargs['batch'] = batch + + request = cls(**request_kwargs) + request.add_state('in_progress', 'The request was initiated') + return request + def to_json(self, verbose: Optional[bool] = True) -> RecursiveRelatedBundlesRequestResponse: """ Provide the JSON representation of a "recursive-related-bundles" build request. @@ -2128,6 +2354,35 @@ def from_json( # type: ignore[override] # noqa: F821 request.add_state('in_progress', 'The request was initiated') return request + def from_json_replacement( + cls, + payload: FbcOperationsPydanticModel, + ): + """ + Handle JSON requests for the builds/fbc-operations API endpoint. + + :param FbcOperationsPydanticModel payload: the Pydantic model representing the request. + :param Batch batch: the batch to specify with the request. + """ + request_kwargs = payload.get_json_for_request() + + request_kwargs['fbc_fragment'] = Image.get_or_create(pull_specification=payload.fbc_fragment) + request_kwargs['binary_image'] = Image.get_or_create(pull_specification=payload.binary_image) + request_kwargs['from_index'] = Image.get_or_create(pull_specification=payload.from_index) + + # current_user.is_authenticated is only ever False when auth is disabled + if current_user.is_authenticated: + request_kwargs['user'] = current_user + + request = cls(**request_kwargs) + + for bt in payload.build_tags: + request.add_build_tag(bt) + + request.add_state('in_progress', 'The request was initiated') + return request + + def to_json(self, verbose: Optional[bool] = True) -> FbcOperationRequestResponse: """ Provide the JSON representation of a "fbc-operation" build request. diff --git a/iib/web/pydantic_models.py b/iib/web/pydantic_models.py new file mode 100644 index 000000000..743b6b7c7 --- /dev/null +++ b/iib/web/pydantic_models.py @@ -0,0 +1,325 @@ +from typing import List, Optional, Dict +from typing_extensions import Annotated + +from pydantic import \ + BaseModel, \ + SecretStr,\ + model_validator,\ + AfterValidator,\ + BeforeValidator + +from iib.exceptions import ValidationError +from iib.web.pydantic_utils import ( + _image_format_check, + _images_format_check, + _get_unique_bundles, + GRAPH_MODE_LITERAL, + DISTRIBUTION_SCOPE_LITERAL, + _get_unique_deprecation_list_items, + validate_graph_mode_index_image, + validate_overwrite_params, + from_index_add_arches, + _binary_image_check, + _distribution_scope_lower, + _length_validator, +) + + +class AddPydanticModel(BaseModel): + """Datastructure of the request to /builds/add API point.""" + + add_arches: Optional[List[str]] = None + binary_image: Annotated[ + Optional[str], + AfterValidator(_length_validator), + AfterValidator(_binary_image_check), + ] = None + build_tags: Optional[List[str]] = [] + bundles: Annotated[ + List[str], + AfterValidator(_length_validator), + AfterValidator(_get_unique_bundles), + AfterValidator(_images_format_check), + ] = [] + cnr_token: Optional[str] = None # deprecated + check_related_images: Optional[bool] = False + deprecation_list: Annotated[ + Optional[List[str]], + AfterValidator(_get_unique_deprecation_list_items), + AfterValidator(_images_format_check), + ] = [] # deprecated + distribution_scope: Annotated[ + Optional[DISTRIBUTION_SCOPE_LITERAL], BeforeValidator(_distribution_scope_lower), + ] = None + force_backport: Optional[bool] = False # deprecated + from_index: Annotated[str, AfterValidator(_image_format_check)] + graph_update_mode: Optional[GRAPH_MODE_LITERAL] = None + organization: Optional[str] = None # deprecated + overwrite_from_index: Optional[bool] = False + overwrite_from_index_token: Optional[str] = None + + _from_index_add_arches_check = model_validator(mode='after')(from_index_add_arches) + + # TODO remove this comment -> Validator from RequestIndexImageMixin class + @model_validator(mode='after') + def verify_overwrite_from_index_token(self) -> 'AddPydanticModel': + """Check the 'overwrite_from_index' parameter in combination with 'overwrite_from_index_token' parameter.""" + validate_overwrite_params(self.overwrite_from_index, self.overwrite_from_index_token) + return self + + # TODO remove this comment -> Validator from RequestAdd class + @model_validator(mode='after') + def verify_graph_update_mode_with_index_image(self) -> 'AddPydanticModel': + """Validate graph mode and check if index image is allowed to use different graph mode.""" + validate_graph_mode_index_image(self.graph_update_mode, self.from_index) + return self + + # TODO remove this comment -> Validator from RequestAdd class + @model_validator(mode='after') + def from_index_needed_if_no_bundles(self) -> 'AddPydanticModel': + """ + Check if no bundles and `from_index is specified + + if no bundles and no from index then an empty index will be created which is a no-op + """ + if not (self.bundles or self.from_index): + raise ValidationError('"from_index" must be specified if no bundles are specified') + return self + + # TODO remove this comment -> Validator from RequestADD class + @model_validator(mode='after') + def bundles_needed_with_check_related_images(self) -> 'AddPydanticModel': + """Verify that `check_related_images` is specified when bundles are specified""" + if self.check_related_images and not self.bundles: + raise ValidationError( + '"check_related_images" must be specified only when bundles are specified' + ) + return self + + def get_json_for_request(self): + """Return json with the parameters we store in the db.""" + return { + "add_arches": self.add_arches, # not in db but used + "binary_image": self.binary_image, + # "build_tags": self.build_tags, # not in db but used + "bundles": self.bundles, + "check_related_images": self.check_related_images, + "deprecation_list": self.deprecation_list, + "distribution_scope": self.distribution_scope, + "from_index": self.from_index, + "graph_update_mode": self.graph_update_mode, + "organization": self.organization, + } + + +class RmPydanticModel(BaseModel): + """Datastructure of the request to /builds/rm API point.""" + + add_arches: Optional[List[str]] = None + binary_image: Annotated[ + Optional[str], + AfterValidator(_binary_image_check), + ] = None + build_tags: Optional[List[str]] = [] + distribution_scope: Annotated[ + Optional[DISTRIBUTION_SCOPE_LITERAL], BeforeValidator(_distribution_scope_lower), + ] = None + from_index: Annotated[str, AfterValidator(_image_format_check)] = None # TODO AGAIN = NONE??? + operators: Annotated[List[str], AfterValidator(_length_validator)] + overwrite_from_index: Optional[bool] = None + overwrite_from_index_token: Optional[str] = None + + _from_index_add_arches_check = model_validator(mode='after')(from_index_add_arches) + + @model_validator(mode='after') + def verify_overwrite_from_index_token(self) -> 'RmPydanticModel': + validate_overwrite_params(self.overwrite_from_index, self.overwrite_from_index_token) + return self + + def get_json_for_request(self): + """Return json with the parameters we store in the db.""" + return { + "add_arches": self.add_arches, # not in db, but used + "binary_image": self.binary_image, + # "build_tags": self.build_tags, # not in db, but used + "distribution_scope": self.distribution_scope, + "from_index": self.from_index, + "operators": self.operators, + "organization": self.organization, + } + + +class RegistryAuth(BaseModel): + auth: str + + +class RegistryAuths(BaseModel): # is {"auths":{}} allowed? + auths: Annotated[Dict[str, RegistryAuth], AfterValidator(_length_validator)] + + +class RegenerateBundlePydanticModel(BaseModel): + """Datastructure of the request to /builds/regenerate-bundle API point.""" + + # BUNDLE_IMAGE, from_bundle_image_resolved, build_tags? + bundle_replacements: Optional[Dict[str, str]] = {} + from_bundle_image: Annotated[str, AfterValidator(_image_format_check)] + organization: Optional[str] + registry_auths: Optional[RegistryAuths] = None # not in db + + def get_json_for_request(self): + """Return json with the parameters we store in the db.""" + return { + "bundle_replacements": self.bundle_replacements, + "from_bundle_image": self.from_bundle_image, + "organization": self.organization, + } + + +class MergeIndexImagePydanticModel(BaseModel): + """Datastructure of the request to /builds/regenerate-bundle API point.""" + + binary_image: Annotated[ + Optional[str], + AfterValidator(_image_format_check), + AfterValidator(_binary_image_check), + ] = None + build_tags: Optional[List[str]] = [] + deprecation_list: Annotated[ + Optional[List[str]], + AfterValidator(_get_unique_deprecation_list_items), + AfterValidator(_images_format_check), + ] = [] + distribution_scope: Annotated[ + Optional[DISTRIBUTION_SCOPE_LITERAL], BeforeValidator(_distribution_scope_lower), + ] = None + graph_update_mode: Optional[GRAPH_MODE_LITERAL] = None + overwrite_target_index: Optional[bool] = False # Why do we need this bool? Isn't the token enough? + overwrite_target_index_token: Optional[str] = None + source_from_index: Annotated[str, AfterValidator(_image_format_check)] = None + target_index: Annotated[Optional[str], AfterValidator(_image_format_check)] = None + batch: Optional[str] = None # TODO Not sure with presence + user: Optional[str] = None # TODO Not sure with presence + + @model_validator(mode='after') + def verify_graph_update_mode_with_target_index(self) -> 'MergeIndexImagePydanticModel': + validate_graph_mode_index_image(self.graph_update_mode, self.target_index) + return self + + @model_validator(mode='after') + def verify_overwrite_from_index_token(self) -> 'MergeIndexImagePydanticModel': + validate_overwrite_params(self.overwrite_target_index, self.overwrite_target_index_token) + return self + + def get_json_for_request(self): + """Return json with the parameters we store in the db.""" + return { + "binary_image": self.binary_image, + # "build_tags": self.build_tags, + "deprecation_list": self.deprecation_list, + "distribution_scope": self.distribution_scope, + "graph_update_mode": self.graph_update_mode, + "source_from_index": self.source_from_index, + "target_index": self.target_index, + "batch": self.batch, + "user": self.user, + } + + +class CreateEmptyIndexPydanticModel(BaseModel): + """Datastructure of the request to /builds/regenerate-bundle API point.""" + + binary_image: Annotated[ + Optional[str], + AfterValidator(_image_format_check), + AfterValidator(_binary_image_check), + ] = None + from_index: Annotated[ + str, + AfterValidator(_image_format_check), + AfterValidator(_length_validator), + ] + labels: Optional[Dict[str, str]] = None + output_fbc: Optional[bool] = False + + def get_json_for_request(self): + """Return json with the parameters we store in the db.""" + return { + "binary_image": self.binary_image, + "from_index": self.from_index, + "labels": self.labels, + "output_fbc": self.output_fbc, + } + + +class RecursiveRelatedBundlesPydanticModel(BaseModel): + batch: Optional[int] = None + organization: Optional[str] = None + parent_bundle_image: Annotated[ + str, + AfterValidator(_image_format_check), + AfterValidator(_length_validator), + ] + registry_auths: Optional[RegistryAuths] = None # not in db + # user: Optional[str] = None + + def get_json_for_request(self): + """Return json with the parameters we store in the db.""" + return { + "batch": self.batch, + "organization": self.organization, + "parent_bundle_image": self.parent_bundle_image, + # "user": self.user, + } + + +class FbcOperationsPydanticModel(BaseModel): + add_arches: Optional[List[str]] + binary_image: Annotated[ + Optional[str], + AfterValidator(_image_format_check), + AfterValidator(_binary_image_check), + ] = None + # BUNDLES? + bundles: Annotated[ + Optional[List[str]], + AfterValidator(_length_validator), + AfterValidator(_get_unique_bundles), + AfterValidator(_images_format_check), + ] = [] + build_tags: Optional[List[str]] = [] + distribution_scope: Annotated[ + Optional[DISTRIBUTION_SCOPE_LITERAL], BeforeValidator(_distribution_scope_lower), + ] = None + fbc_fragment: Annotated[ + str, + AfterValidator(_image_format_check), + AfterValidator(_length_validator), + ] + from_index: Annotated[ + str, + AfterValidator(_image_format_check), + AfterValidator(_length_validator), + ] + organization: Optional[str] = None + overwrite_from_index: Optional[bool] = None + overwrite_from_index_token: Optional[str] = None + + @model_validator(mode='after') + def verify_overwrite_from_index_token(self) -> 'FbcOperationsPydanticModel': + validate_overwrite_params(self.overwrite_from_index, self.overwrite_from_index_token) + return self + + + def get_json_for_request(self): + """Return json with the parameters we store in the db.""" + return { + "add_arches": self.add_arches, + "binary_image": self.binary_image, + "bundles": self.bundles, + # "build_tags": self.build_tags, + "distribution_scope": self.distribution_scope, + "fbc_fragment": self.fbc_fragment, + "from_index": self.from_index, + "organization": self.organization, + } + diff --git a/iib/web/pydantic_utils.py b/iib/web/pydantic_utils.py new file mode 100644 index 000000000..858175057 --- /dev/null +++ b/iib/web/pydantic_utils.py @@ -0,0 +1,118 @@ +from typing import List, Optional, Any, Literal + +import copy +from werkzeug.exceptions import Forbidden +from flask import current_app +from flask_login import current_user + +from iib.exceptions import ValidationError + + +GRAPH_MODE_LITERAL = Literal['replaces', 'semver', 'semver-skippatch'] +DISTRIBUTION_SCOPE_LITERAL = Literal['prod', 'stage', 'dev'] + + +# TODO add regex in future to not allow following values ":s", "s:", ":"? +def _image_format_check(image_name: str) -> str: + if '@' not in image_name and ':' not in image_name: + raise ValidationError( + f'Image {image_name} should have a tag or a digest specified.' + ) + return image_name + +def _images_format_check(image_list: List[str]) -> List[str]: + for image_name in image_list: + _image_format_check(image_name) + return image_list + +def _get_unique_bundles(bundles: List[str]) -> List[str]: + if not bundles: + return bundles + + unique_bundles = list(set(bundles)) + if len(unique_bundles) != len(bundles): + duplicate_bundles = copy.copy(bundles) + for bundle in unique_bundles: + duplicate_bundles.remove(bundle) + + # flask.current_app.logger.info( + # f'Removed duplicate bundles from request: {duplicate_bundles}' + # ) + return unique_bundles + + +# RequestIndexImageMixin +def _get_unique_deprecation_list_items(deprecation_list: Optional[List[str]]) -> Optional[List[str]]: + return list(set(deprecation_list)) + + +# TODO REMOVE this comment from requestADD (can be in other classes too, as it was called as method) +def validate_graph_mode_index_image(graph_update_mode: str, index_image: str) -> 'MergeIndexImageRequestPayload': + """ + Validate graph mode and check if index image is allowed to use different graph mode. + + :param str graph_update_mode: one of the graph mode options + :param str index_image: pullspec of index image to which graph mode should be applied to + :raises: ValidationError when incorrect graph_update_mode is set + :raises: Forbidden when graph_mode can't be used for given index image + """ + + if graph_update_mode: + allowed_from_indexes: List[str] = ["REMOVE_#:r"]#current_app.config['IIB_GRAPH_MODE_INDEX_ALLOW_LIST'] + if index_image not in allowed_from_indexes: + raise Forbidden( + '"graph_update_mode" can only be used on the' + f' following index image: {allowed_from_indexes}' + ) + return graph_update_mode + + +# RequestIndexImageMixin +def from_index_add_arches(model: 'AddRequestPydanticModel') -> 'AddRequestPydanticModel': + """ + Check if both `from_index` and `add_arches` are not specified + """ + if not model.from_index and not model.add_arches: + raise ValidationError('One of "from_index" or "add_arches" must be specified') + return model + + +# RequestIndexImageMixin +def _binary_image_check(binary_image: str) -> str: + """ + # Validate binary_image is correctly provided + """ + if not binary_image and not current_app.config['IIB_BINARY_IMAGE_CONFIG']: + raise ValidationError('The "binary_image" value must be a non-empty string') + return binary_image + + +# RequestIndexImageMixin +def validate_overwrite_params(overwrite_index_image: Optional[bool], overwrite_index_image_token: Optional[str]) -> None: + """ + Check if both `overwrite_index_image` and `overwrite_index_image_token` are specified + """ + if overwrite_index_image_token and not overwrite_index_image: + raise ValidationError( + 'The "overwrite_from_index" parameter is required when' + ' the "overwrite_from_index_token" parameter is used' + ) + + # Verify the user is authorized to use overwrite_from_index + # current_user.is_authenticated is only ever False when auth is disabled + # TODO Remove "1 or" -> if current_user.is_authenticated: + if 1 or current_user.is_authenticated: + if overwrite_index_image and not overwrite_index_image_token: + raise Forbidden( + 'You must set "overwrite_from_index_token" to use "overwrite_from_index"' + ) + + +# RequestIndexImageMixin +def _distribution_scope_lower(distribution_scope: str) -> str: + return distribution_scope.lower() + +def _length_validator(model_property: Any) -> Any: + if len(model_property) == 0: + raise ValidationError(f"The {type(model_property)} {model_property} should have at least 1 item.") + return model_property diff --git a/iib/web/test_models.py b/iib/web/test_models.py new file mode 100644 index 000000000..e69de29bb diff --git a/setup.py b/setup.py index bbfae1d87..40be34d8b 100644 --- a/setup.py +++ b/setup.py @@ -28,6 +28,7 @@ 'tenacity', 'typing-extensions', 'packaging', + 'pydantic', 'opentelemetry-api', 'opentelemetry-sdk', 'opentelemetry-exporter-otlp',