diff --git a/conda-store-server/conda_store_server/_internal/action/generate_lockfile.py b/conda-store-server/conda_store_server/_internal/action/generate_lockfile.py index cb36ffda4..f4824ba15 100644 --- a/conda-store-server/conda_store_server/_internal/action/generate_lockfile.py +++ b/conda-store-server/conda_store_server/_internal/action/generate_lockfile.py @@ -28,7 +28,7 @@ def action_solve_lockfile( lockfile_filename = pathlib.Path.cwd() / "conda-lock.yaml" with environment_filename.open("w") as f: - json.dump(specification.dict(), f) + json.dump(specification.model_dump(), f) context.log.info( "Note that the output of `conda config --show` displayed below only reflects " @@ -82,7 +82,7 @@ def action_save_lockfile( ): # Note: this calls dict on specification so that the version field is # part of the output - lockfile = specification.dict()["lockfile"] + lockfile = specification.model_dump()["lockfile"] lockfile_filename = pathlib.Path.cwd() / "conda-lock.yaml" with lockfile_filename.open("w") as f: diff --git a/conda-store-server/conda_store_server/_internal/action/install_specification.py b/conda-store-server/conda_store_server/_internal/action/install_specification.py index 8398b0e30..31cbf5226 100644 --- a/conda-store-server/conda_store_server/_internal/action/install_specification.py +++ b/conda-store-server/conda_store_server/_internal/action/install_specification.py @@ -17,7 +17,7 @@ def action_install_specification( ): environment_filename = pathlib.Path.cwd() / "environment.yaml" with environment_filename.open("w") as f: - json.dump(specification.dict(), f) + json.dump(specification.model_dump(), f) command = [ conda_command, diff --git a/conda-store-server/conda_store_server/_internal/schema.py b/conda-store-server/conda_store_server/_internal/schema.py index bbd170a23..da3d5bcca 100644 --- a/conda-store-server/conda_store_server/_internal/schema.py +++ b/conda-store-server/conda_store_server/_internal/schema.py @@ -8,11 +8,17 @@ import os import re import sys -from typing import Any, Callable, Dict, List, Optional, TypeAlias, Union +from typing import Annotated, Any, Callable, Dict, List, Optional, TypeAlias, Union from conda_lock.lockfile.v1.models import Lockfile -from pydantic import BaseModel, Field, ValidationError, constr, validator -from pydantic.error_wrappers import ErrorWrapper +from pydantic import ( + AfterValidator, + BaseModel, + ConfigDict, + Field, + StringConstraints, + ValidationError, +) from conda_store_server._internal import conda_utils, utils @@ -36,7 +42,9 @@ def _datetime_factory(offset: datetime.timedelta): # Authentication Schema ######################### -RoleBindings: TypeAlias = Dict[constr(regex=ARN_ALLOWED), List[str]] +RoleBindings: TypeAlias = Dict[ + Annotated[str, StringConstraints(pattern=ARN_ALLOWED)], List[str] +] class Permissions(enum.Enum): @@ -82,40 +90,32 @@ class StorageBackend(enum.Enum): class CondaChannel(BaseModel): id: int name: str - last_update: Optional[datetime.datetime] - - class Config: - orm_mode = True + last_update: Optional[datetime.datetime] = None + model_config = ConfigDict(from_attributes=True) class CondaPackageBuild(BaseModel): id: int build: str sha256: str - - class Config: - orm_mode = True + model_config = ConfigDict(from_attributes=True) class CondaPackage(BaseModel): id: int channel: CondaChannel - license: Optional[str] + license: Optional[str] = None name: str version: str - summary: Optional[str] - - class Config: - orm_mode = True + summary: Optional[str] = None + model_config = ConfigDict(from_attributes=True) class NamespaceRoleMapping(BaseModel): id: int entity: str role: str - - class Config: - orm_mode = True + model_config = ConfigDict(from_attributes=True) class NamespaceRoleMappingV2(BaseModel): @@ -123,23 +123,19 @@ class NamespaceRoleMappingV2(BaseModel): namespace: str other_namespace: str role: str - - class Config: - orm_mode = True + model_config = ConfigDict(from_attributes=True) @classmethod def from_list(cls, lst): - return cls(**{k: v for k, v in zip(cls.__fields__.keys(), lst, strict=False)}) + return cls(**{k: v for k, v in zip(cls.model_fields.keys(), lst, strict=False)}) class Namespace(BaseModel): id: int - name: constr(regex=f"^[{ALLOWED_CHARACTERS}]+$") # noqa: F722 + name: Annotated[str, StringConstraints(pattern=f"^[{ALLOWED_CHARACTERS}]+$")] # noqa: F722 metadata_: Dict[str, Any] = None role_mappings: List[NamespaceRoleMapping] = [] - - class Config: - orm_mode = True + model_config = ConfigDict(from_attributes=True) class Specification(BaseModel): @@ -148,12 +144,11 @@ class Specification(BaseModel): spec: dict sha256: str created_on: datetime.datetime - - class Config: - orm_mode = True + model_config = ConfigDict(from_attributes=True) -class BuildArtifactType(enum.Enum): +# Use str mixin to allow pydantic to serialize Settings models +class BuildArtifactType(str, enum.Enum): DIRECTORY = "DIRECTORY" LOCKFILE = "LOCKFILE" LOGS = "LOGS" @@ -177,28 +172,22 @@ class BuildArtifact(BaseModel): id: int artifact_type: BuildArtifactType key: str - - class Config: - orm_mode = True - use_enum_values = True + model_config = ConfigDict(from_attributes=True, use_enum_values=True) class Build(BaseModel): id: int environment_id: int - specification: Optional[Specification] - packages: Optional[List[CondaPackage]] + specification: Optional[Specification] = None + packages: Optional[List[CondaPackage]] = None status: BuildStatus - status_info: Optional[str] + status_info: Optional[str] = None size: int scheduled_on: datetime.datetime - started_on: Optional[datetime.datetime] - ended_on: Optional[datetime.datetime] - build_artifacts: Optional[List[BuildArtifact]] - - class Config: - orm_mode = True - use_enum_values = True + started_on: Optional[datetime.datetime] = None + ended_on: Optional[datetime.datetime] = None + build_artifacts: Optional[List[BuildArtifact]] = None + model_config = ConfigDict(from_attributes=True, use_enum_values=True) class Environment(BaseModel): @@ -206,12 +195,10 @@ class Environment(BaseModel): namespace: Namespace name: str current_build_id: int - current_build: Optional[Build] - - description: Optional[str] + current_build: Optional[Build] = None - class Config: - orm_mode = True + description: Optional[str] = None + model_config = ConfigDict(from_attributes=True) class Settings(BaseModel): @@ -378,67 +365,25 @@ class Settings(BaseModel): metadata={"global": False}, ) - @validator("build_artifacts", each_item=True) - def check_build_artifacts(cls, v): - return BuildArtifactType(v) - @validator("build_artifacts_kept_on_deletion", each_item=True) - def check_build_artifacts_kept_on_deletion(cls, v): - return BuildArtifactType(v) - - class Config: - use_enum_values = True +PipArg = Annotated[str, AfterValidator(lambda v: check_pip(v))] # Conda Environment class CondaSpecificationPip(BaseModel): - pip: List[str] = [] - - @validator("pip", each_item=True) - def check_pip(cls, v): - from pkg_resources import Requirement - - allowed_pip_params = ["--index-url", "--extra-index-url", "--trusted-host"] + pip: List[PipArg] = [] - if v.startswith("--"): - match = re.fullmatch("(.+?)[ =](.*)", v) - if match is None or match.group(1) not in allowed_pip_params: - raise ValueError( - f"Invalid pip option '{v}' supported options are {allowed_pip_params}" - ) - else: - try: - Requirement.parse(v) - except Exception: - raise ValueError( - f'Invalid pypi package dependency "{v}" ensure it follows peps https://peps.python.org/pep-0508/ and https://peps.python.org/pep-0440/' - ) - return v +CondaDep = Annotated[str, AfterValidator(lambda v: check_dependencies(v))] class CondaSpecification(BaseModel): - name: constr(regex=f"^[{ALLOWED_CHARACTERS}]+$") # noqa: F722 channels: List[str] = [] - dependencies: List[Union[str, CondaSpecificationPip]] = [] - variables: Optional[Dict[str, Union[str, int]]] - prefix: Optional[str] + dependencies: List[CondaDep | CondaSpecificationPip] = [] description: Optional[str] = "" - - @validator("dependencies", each_item=True) - def check_dependencies(cls, v): - from conda.models.match_spec import MatchSpec - - if not isinstance(v, str): - return v # ignore pip field - - try: - MatchSpec(v) - except Exception as e: - print(e) - raise ValueError(f"Invalid conda package dependency specification {v}") - - return v + name: Annotated[str, StringConstraints(pattern=f"^[{ALLOWED_CHARACTERS}]+$")] + prefix: Optional[str] = None + variables: Optional[Dict[str, Union[str, int]]] = None @classmethod def parse_obj(cls, specification): @@ -478,7 +423,7 @@ def parse_obj(cls, specification): class LockfileSpecification(BaseModel): - name: constr(regex=f"^[{ALLOWED_CHARACTERS}]+$") # noqa: F722 + name: Annotated[str, StringConstraints(pattern=f"^[{ALLOWED_CHARACTERS}]+$")] # noqa: F722 description: Optional[str] = "" lockfile: Lockfile @@ -494,21 +439,14 @@ def parse_obj(cls, specification): lockfile = specification.get("lockfile") version = lockfile and lockfile.pop("version", None) if version not in (None, 1): - # https://stackoverflow.com/questions/73968566/with-pydantic-how-can-i-create-my-own-validationerror-reason raise ValidationError( - [ - ErrorWrapper( - ValueError("expected no version field or version equal to 1"), - "lockfile -> version", - ) - ], - LockfileSpecification, + "Expected lockfile to have no version field, or version=1", ) return super().parse_obj(specification) - def dict(self): - res = super().dict() + def model_dump(self): + res = super().model_dump() # The dict_for_output method includes the version field into the output # and excludes unset fields. Without the version field present, # conda-lock would reject a lockfile during parsing, so it wouldn't be @@ -567,7 +505,7 @@ class DockerConfigConfig(BaseModel): Cmd: List[str] = ["/bin/sh"] ArgsEscaped: bool = True Image: Optional[str] = None - Volumes: Optional[List[str]] + Volumes: Optional[List[str]] = None WorkingDir: str = "" Entrypoint: Optional[str] = None OnBuild: Optional[str] = None @@ -638,8 +576,8 @@ class APIStatus(enum.Enum): class APIResponse(BaseModel): status: APIStatus - data: Optional[Any] - message: Optional[str] + data: Optional[Any] = None + message: Optional[str] = None class APIPaginatedResponse(APIResponse): @@ -650,7 +588,7 @@ class APIPaginatedResponse(APIResponse): class APIAckResponse(BaseModel): status: APIStatus - message: Optional[str] + message: Optional[str] = None # GET /api/v1 @@ -668,7 +606,7 @@ class APIGetPermissionData(BaseModel): primary_namespace: str entity_permissions: Dict[str, List[str]] entity_roles: Dict[str, List[str]] - expiration: Optional[datetime.datetime] + expiration: Optional[datetime.datetime] = None class APIGetPermission(APIResponse): @@ -769,3 +707,64 @@ class APIPutSetting(APIResponse): # GET /api/v1/usage/ class APIGetUsage(APIResponse): data: Dict[str, Dict[str, Any]] + + +def check_pip(v: str) -> str: + """Check that pip options and dependencies are valid. + + Parameters + ---------- + v : str + Pip package name or CLI arg to validate + + Returns + ------- + str + Validated pip package name or CLI arg + """ + from pkg_resources import Requirement + + allowed_pip_params = ["--index-url", "--extra-index-url", "--trusted-host"] + + if v.startswith("--"): + match = re.fullmatch("(.+?)[ =](.*)", v) + if match is None or match.group(1) not in allowed_pip_params: + raise ValueError( + f"Invalid pip option '{v}' supported options are {allowed_pip_params}" + ) + else: + try: + Requirement.parse(v) + except Exception: + raise ValueError( + f'Invalid pypi package dependency "{v}" ensure it follows peps https://peps.python.org/pep-0508/ and https://peps.python.org/pep-0440/' + ) + + return v + + +def check_dependencies(v: str | CondaSpecificationPip) -> str | CondaSpecificationPip: + """Check that the dependency is either a list of pip args or a conda MatchSpec. + + Parameters + ---------- + v : str | CondaSpecificationPip + A list of pip args or a valid conda MatchSpec object + + Returns + ------- + str | CondaSpecificationPip + The validated dependency + """ + from conda.models.match_spec import MatchSpec + + if not isinstance(v, str): + return v # ignore pip field + + try: + MatchSpec(v) + except Exception as e: + print(e) + raise ValueError(f"Invalid conda package dependency specification {v}") + + return v diff --git a/conda-store-server/conda_store_server/_internal/server/templates/setting.html b/conda-store-server/conda_store_server/_internal/server/templates/setting.html index 57669d878..7fbfae500 100644 --- a/conda-store-server/conda_store_server/_internal/server/templates/setting.html +++ b/conda-store-server/conda_store_server/_internal/server/templates/setting.html @@ -20,11 +20,11 @@

Environment Settings

Settings must be JSON parsable

- {% for key, value in settings.dict().items() if not settings.__fields__[key].field_info.extra['metadata']['global'] %} + {% for key, value in settings.model_dump().items() if not settings.model_fields[key].json_schema_extra['metadata']['global'] %}
- {{ settings.__fields__[key].field_info.description }} + {{ settings.model_fields[key].description }}
{% endfor %} @@ -42,11 +42,11 @@

Global Settings

Settings must be JSON parsable

- {% for key, value in settings.dict().items() if settings.__fields__[key].field_info.extra['metadata']['global'] %} + {% for key, value in settings.model_dump().items() if settings.model_fields[key].json_schema_extra['metadata']['global'] %}
- {{ settings.__fields__[key].field_info.description }} + {{ settings.model_fields[key].description }}
{% endfor %} diff --git a/conda-store-server/conda_store_server/_internal/server/views/api.py b/conda-store-server/conda_store_server/_internal/server/views/api.py index e55df4462..32a0fe559 100644 --- a/conda-store-server/conda_store_server/_internal/server/views/api.py +++ b/conda-store-server/conda_store_server/_internal/server/views/api.py @@ -300,7 +300,7 @@ async def api_get_namespace( return { "status": "ok", - "data": schema.Namespace.from_orm(namespace).dict(), + "data": schema.Namespace.from_orm(namespace).model_dump(), } @@ -426,7 +426,7 @@ async def api_get_namespace_roles( db.commit() return { "status": "ok", - "data": [x.dict() for x in data], + "data": [x.model_dump() for x in data], } @@ -492,7 +492,7 @@ async def api_get_namespace_role( raise HTTPException(status_code=404, detail="failed to find role") return { "status": "ok", - "data": data.dict(), + "data": data.model_dump(), } @@ -1433,7 +1433,9 @@ async def api_get_settings( return { "status": "ok", - "data": conda_store.get_settings(db, namespace, environment_name).dict(), + "data": conda_store.get_settings( + db, namespace, environment_name + ).model_dump(), "message": None, } diff --git a/conda-store-server/conda_store_server/api.py b/conda-store-server/conda_store_server/api.py index 328d6bae4..e086d5c3c 100644 --- a/conda-store-server/conda_store_server/api.py +++ b/conda-store-server/conda_store_server/api.py @@ -110,7 +110,7 @@ def update_namespace_metadata( def get_namespace_roles( db, name: str, -): +) -> list[schema.NamespaceRoleMappingV2]: """Which namespaces can access namespace 'name'?""" namespace = get_namespace(db, name) if namespace is None: @@ -170,7 +170,7 @@ def get_namespace_role( db, name: str, other: str, -): +) -> schema.NamespaceRoleMappingV2 | None: namespace = get_namespace(db, name) if namespace is None: raise ValueError(f"Namespace='{name}' not found") @@ -433,7 +433,7 @@ def ensure_specification( specification: Union[schema.CondaSpecification, schema.LockfileSpecification], is_lockfile: bool = False, ): - specification_sha256 = utils.datastructure_hash(specification.dict()) + specification_sha256 = utils.datastructure_hash(specification.model_dump()) specification_orm = get_specification(db, sha256=specification_sha256) if specification_orm is None: @@ -450,7 +450,9 @@ def create_speficication( specification: Union[schema.CondaSpecification, schema.LockfileSpecification], is_lockfile: bool = False, ): - specification_orm = orm.Specification(specification.dict(), is_lockfile=is_lockfile) + specification_orm = orm.Specification( + specification.model_dump(), is_lockfile=is_lockfile + ) db.add(specification_orm) return specification_orm diff --git a/conda-store-server/conda_store_server/app.py b/conda-store-server/conda_store_server/app.py index 98400f637..226f913ea 100644 --- a/conda-store-server/conda_store_server/app.py +++ b/conda-store-server/conda_store_server/app.py @@ -498,7 +498,7 @@ def ensure_settings(self, db: Session): build_artifacts=self.build_artifacts, # default_docker_base_image=self.default_docker_base_image, ) - api.set_kvstore_key_values(db, "setting", settings.dict(), update=False) + api.set_kvstore_key_values(db, "setting", settings.model_dump(), update=False) def ensure_namespace(self, db: Session): """Ensure that conda-store default namespaces exists""" @@ -527,14 +527,14 @@ def set_settings( environment_name: str = None, data: Dict[str, Any] = {}, ): - setting_keys = schema.Settings.__fields__.keys() + setting_keys = schema.Settings.model_fields.keys() if not data.keys() <= setting_keys: invalid_keys = data.keys() - setting_keys raise ValueError(f"Invalid setting keys {invalid_keys}") for key, value in data.items(): - field = schema.Settings.__fields__[key] - global_setting = field.field_info.extra["metadata"]["global"] + field = schema.Settings.model_fields[key] + global_setting = field.json_schema_extra["metadata"]["global"] if global_setting and ( namespace is not None or environment_name is not None ): @@ -543,10 +543,11 @@ def set_settings( ) try: - pydantic.parse_obj_as(field.outer_type_, value) + validator = pydantic.TypeAdapter(field.annotation) + validator.validate_python(value) except Exception as e: raise ValueError( - f"Invalid parsing of setting {key} expected type {field.outer_type_} ran into error {e}" + f"Invalid parsing of setting {key} expected type {field.annotation} ran into error {e}" ) if namespace is not None and environment_name is not None: @@ -647,7 +648,7 @@ def register_environment( specification=schema.CondaSpecification.parse_obj(specification), ) - spec_sha256 = utils.datastructure_hash(specification_model.dict()) + spec_sha256 = utils.datastructure_hash(specification_model.model_dump()) matching_specification = api.get_specification(db, sha256=spec_sha256) if ( matching_specification is not None diff --git a/conda-store-server/conda_store_server/server/auth.py b/conda-store-server/conda_store_server/server/auth.py index d2ee4a43a..80b5ba27f 100644 --- a/conda-store-server/conda_store_server/server/auth.py +++ b/conda-store-server/conda_store_server/server/auth.py @@ -57,7 +57,7 @@ class AuthenticationBackend(LoggingConfigurable): ) def encrypt_token(self, token: schema.AuthenticationToken): - return jwt.encode(token.dict(), self.secret, algorithm=self.jwt_algorithm) + return jwt.encode(token.model_dump(), self.secret, algorithm=self.jwt_algorithm) def decrypt_token(self, token: str): return jwt.decode(token, self.secret, algorithms=[self.jwt_algorithm]) diff --git a/conda-store-server/pyproject.toml b/conda-store-server/pyproject.toml index 43e63abee..76518cea0 100644 --- a/conda-store-server/pyproject.toml +++ b/conda-store-server/pyproject.toml @@ -63,7 +63,7 @@ dependencies = [ "pyyaml >=6.0.1", "redis", "requests", - "pydantic >=1.10.16,<2.0a0", + "pydantic >=2.0", "python-multipart", # setuptools>=70 uses local version of packaging (and other deps) without # pinning them; conda-lock depends on this, but also doesn't pin the setuptools diff --git a/conda-store-server/tests/_internal/action/test_actions.py b/conda-store-server/tests/_internal/action/test_actions.py index 7fadb0ffa..d6e4a594c 100644 --- a/conda-store-server/tests/_internal/action/test_actions.py +++ b/conda-store-server/tests/_internal/action/test_actions.py @@ -174,6 +174,12 @@ def test_solve_lockfile_multiple_platforms(conda_store, specification, request): assert len(context.result["package"]) != 0 +def test_save_lockfile(simple_lockfile_specification): + """Ensure lockfile is saved in conda-lock `output` format""" + context = action.action_save_lockfile(specification=simple_lockfile_specification) + assert context.result == simple_lockfile_specification.lockfile.dict_for_output() + + @pytest.mark.parametrize( "specification_name", [ diff --git a/conda-store-server/tests/_internal/test_schema.py b/conda-store-server/tests/_internal/test_schema.py new file mode 100644 index 000000000..8bf70a816 --- /dev/null +++ b/conda-store-server/tests/_internal/test_schema.py @@ -0,0 +1,64 @@ +# Copyright (c) conda-store development team. All rights reserved. +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +import pytest + +from conda_store_server._internal import schema + + +@pytest.fixture +def test_lockfile(): + return { + "version": 1, + "metadata": { + "content_hash": { + "linux-64": "5514f6769db31a2037c24116f89239737c66d009b2d0c71e2872339c3cf1a8f6" + }, + "channels": [{"url": "conda-forge", "used_env_vars": []}], + "platforms": ["linux-64"], + "sources": ["environment.yaml"], + }, + "package": [ + { + "name": "_libgcc_mutex", + "version": "0.1", + "manager": "conda", + "platform": "linux-64", + "dependencies": {}, + "url": "https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2", + "hash": { + "md5": "d7c89558ba9fa0495403155b64376d81", + "sha256": "fe51de6107f9edc7aa4f786a70f4a883943bc9d39b3bb7307c04c41410990726", + }, + "category": "main", + "optional": False, + }, + { + "name": "_openmp_mutex", + "version": "4.5", + "manager": "conda", + "platform": "linux-64", + "dependencies": {"_libgcc_mutex": "0.1", "libgomp": ">=7.5.0"}, + "url": "https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2", + "hash": { + "md5": "73aaf86a425cc6e73fcf236a5a46396d", + "sha256": "fbe2c5e56a653bebb982eda4876a9178aedfc2b545f25d0ce9c4c0b508253d22", + }, + "category": "main", + "optional": False, + }, + ], + } + + +def test_parse_lockfile_obj(test_lockfile): + lockfile_spec = { + "name": "test", + "description": "test", + # use a copy of the lockfile so it's not mutated and we can compare + # against the original dict + "lockfile": test_lockfile.copy(), + } + specification = schema.LockfileSpecification.parse_obj(lockfile_spec) + assert specification.model_dump()["lockfile"] == test_lockfile diff --git a/conda-store-server/tests/test_app.py b/conda-store-server/tests/test_app.py index 36c8deff6..04b617a8d 100644 --- a/conda-store-server/tests/test_app.py +++ b/conda-store-server/tests/test_app.py @@ -51,12 +51,12 @@ def test_conda_store_register_environment_workflow( namespace_name = "pytest-namespace" build_id = conda_store.register_environment( - db, specification=simple_specification.dict(), namespace=namespace_name + db, specification=simple_specification.model_dump(), namespace=namespace_name ) build = api.get_build(db, build_id=build_id) assert build is not None - assert build.status == schema.BuildStatus.QUEUED + assert build.status in [schema.BuildStatus.QUEUED, schema.BuildStatus.COMPLETED] assert build.environment.name == simple_specification.name assert build.environment.namespace.name == namespace_name assert build.specification.spec["name"] == simple_specification.name @@ -103,14 +103,14 @@ def test_conda_store_register_environment_force_false_same_namespace(db, conda_s first_build_id = conda_store.register_environment( db, - specification=conda_specification.dict(), + specification=conda_specification.model_dump(), namespace=namespace_name, force=False, ) second_build_id = conda_store.register_environment( db, - specification=conda_specification.dict(), + specification=conda_specification.model_dump(), namespace=namespace_name, force=False, ) @@ -134,14 +134,14 @@ def test_conda_store_register_environment_force_false_different_namespace( first_build_id = conda_store.register_environment( db, - specification=conda_specification.dict(), + specification=conda_specification.model_dump(), namespace="pytest-namespace", force=False, ) second_build_id = conda_store.register_environment( db, - specification=conda_specification.dict(), + specification=conda_specification.model_dump(), namespace="pytest-different-namespace", force=False, ) @@ -164,14 +164,14 @@ def test_conda_store_register_environment_duplicate_force_true(db, conda_store): first_build_id = conda_store.register_environment( db, - specification=conda_specification.dict(), + specification=conda_specification.model_dump(), namespace=namespace_name, force=True, ) second_build_id = conda_store.register_environment( db, - specification=conda_specification.dict(), + specification=conda_specification.model_dump(), namespace=namespace_name, force=True, ) diff --git a/recipe/meta.yaml b/recipe/meta.yaml index 2bc1450c5..3bd7d8aa9 100644 --- a/recipe/meta.yaml +++ b/recipe/meta.yaml @@ -80,7 +80,7 @@ outputs: - itsdangerous - jinja2 - minio - - pydantic <2.0a0 + - pydantic >=2.0 - pyjwt - python >=3.10,<3.13 - python-docker