diff --git a/.github/workflows/ci-integration.yml b/.github/workflows/ci-integration.yml index 36e1d380..390903b0 100644 --- a/.github/workflows/ci-integration.yml +++ b/.github/workflows/ci-integration.yml @@ -33,7 +33,8 @@ jobs: steps: - uses: actions/checkout@v2 - - uses: mamba-org/setup-micromamba@v1 + - name: Install environment + uses: mamba-org/setup-micromamba@v1 with: environment-file: devtools/conda-envs/test.yml create-args: >- diff --git a/alchemiscale/compute/api.py b/alchemiscale/compute/api.py index db21d5b8..32070760 100644 --- a/alchemiscale/compute/api.py +++ b/alchemiscale/compute/api.py @@ -106,7 +106,7 @@ def register_computeservice( ): now = datetime.utcnow() csreg = ComputeServiceRegistration( - identifier=compute_service_id, registered=now, heartbeat=now + identifier=ComputeServiceID(compute_service_id), registered=now, heartbeat=now ) compute_service_id_ = n4js.register_computeservice(csreg) diff --git a/alchemiscale/models.py b/alchemiscale/models.py index 75edd1d0..e708aa2b 100644 --- a/alchemiscale/models.py +++ b/alchemiscale/models.py @@ -5,7 +5,7 @@ """ from typing import Optional, Union -from pydantic import BaseModel, Field, validator, root_validator +from pydantic import BaseModel, Field, field_validator, model_validator, ConfigDict from gufe.tokenization import GufeKey from re import fullmatch @@ -34,8 +34,9 @@ def __eq__(self, other): return str(self) == str(other) - class Config: - frozen = True + model_config = ConfigDict( + frozen=True, + ) @staticmethod def _validate_component(v, component): @@ -61,19 +62,19 @@ def _validate_component(v, component): return v - @validator("org") + @field_validator("org") def valid_org(cls, v): return cls._validate_component(v, "org") - @validator("campaign") + @field_validator("campaign") def valid_campaign(cls, v): return cls._validate_component(v, "campaign") - @validator("project") + @field_validator("project") def valid_project(cls, v): return cls._validate_component(v, "project") - @root_validator + @model_validator(mode="before") def check_scope_hierarchy(cls, values): if not _hierarchy_valid(values): raise InvalidScopeError( @@ -122,15 +123,14 @@ class ScopedKey(BaseModel): """ - gufe_key: GufeKey + gufe_key: Union[GufeKey, str] org: str campaign: str project: str - class Config: - frozen = True + model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True) - @validator("gufe_key") + @field_validator("gufe_key") def cast_gufe_key(cls, v): return GufeKey(v) diff --git a/alchemiscale/security/auth.py b/alchemiscale/security/auth.py index f4782ea1..44b520b6 100644 --- a/alchemiscale/security/auth.py +++ b/alchemiscale/security/auth.py @@ -25,12 +25,31 @@ def generate_secret_key(): return secrets.token_hex(32) -def authenticate(db, cls, identifier: str, key: str) -> CredentialedEntity: +def authenticate(db, cls, identifier: str, key: str) -> Optional[CredentialedEntity]: + """Authenticate the given identity+key against the db instance. + + Parameters + ---------- + db + State store instance featuring a `get_credentialed_entity` method. + cls + The `CredentialedEntity` subclass the identity corresponds to. + identity + String identifier for the the identity. + key + Secret key string for the identity. + + Returns + ------- + If successfully authenticated, returns the `CredentialedEntity` subclass instance. + If not, returns `None`. + + """ entity: CredentialedEntity = db.get_credentialed_entity(identifier, cls) if entity is None: - return False + return None if not pwd_context.verify(key, entity.hashed_key): - return False + return None return entity diff --git a/alchemiscale/security/models.py b/alchemiscale/security/models.py index 62f0395c..25a63db3 100644 --- a/alchemiscale/security/models.py +++ b/alchemiscale/security/models.py @@ -7,7 +7,7 @@ from datetime import datetime, timedelta from typing import List, Union, Optional -from pydantic import BaseModel, validator +from pydantic import BaseModel, field_validator from ..models import Scope @@ -30,22 +30,24 @@ class CredentialedEntity(BaseModel): class ScopedIdentity(BaseModel): identifier: str disabled: bool = False - scopes: List[str] = [] + scopes: List[Union[Scope, str]] = [] - @validator("scopes", pre=True, each_item=True) - def cast_scopes_to_str(cls, scope): + @field_validator("scopes") + def cast_scopes_to_str(cls, scopes): """Ensure that each scope object is correctly cast to its str representation""" - if isinstance(scope, Scope): - scope = str(scope) - elif isinstance(scope, str): - try: - Scope.from_str(scope) - except: + scopes_ = [] + for scope in scopes: + if isinstance(scope, Scope): + scopes_.append(str(scope)) + elif isinstance(scope, str): + try: + scopes_.append(str(Scope.from_str(scope))) + except: + raise ValueError(f"Invalid scope `{scope}` set for `{cls}`") + else: raise ValueError(f"Invalid scope `{scope}` set for `{cls}`") - else: - raise ValueError(f"Invalid scope `{scope}` set for `{cls}`") - return scope + return scopes_ class UserIdentity(ScopedIdentity): diff --git a/alchemiscale/settings.py b/alchemiscale/settings.py index 951d27d9..c3b95cf8 100644 --- a/alchemiscale/settings.py +++ b/alchemiscale/settings.py @@ -7,12 +7,13 @@ from functools import lru_cache from typing import Optional -from pydantic import BaseSettings +from pydantic_settings import BaseSettings, SettingsConfigDict class FrozenSettings(BaseSettings): - class Config: - frozen = True + model_config = SettingsConfigDict( + frozen=True, + ) class Neo4jStoreSettings(FrozenSettings): diff --git a/alchemiscale/storage/models.py b/alchemiscale/storage/models.py index c9b000b8..a9f3f404 100644 --- a/alchemiscale/storage/models.py +++ b/alchemiscale/storage/models.py @@ -13,7 +13,7 @@ import hashlib -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, ConfigDict from gufe.tokenization import GufeTokenizable, GufeKey from ..models import ScopedKey, Scope @@ -29,6 +29,8 @@ class ComputeServiceRegistration(BaseModel): registered: datetime heartbeat: datetime + model_config = ConfigDict(arbitrary_types_allowed=True) + def __repr__(self): # pragma: no cover return f"" @@ -59,6 +61,8 @@ class TaskProvenance(BaseModel): datetime_start: datetime datetime_end: datetime + model_config = ConfigDict(arbitrary_types_allowed=True) + # this should include versions of various libraries diff --git a/devtools/conda-envs/alchemiscale-client.yml b/devtools/conda-envs/alchemiscale-client.yml index 6f583986..48ff25c9 100644 --- a/devtools/conda-envs/alchemiscale-client.yml +++ b/devtools/conda-envs/alchemiscale-client.yml @@ -15,7 +15,8 @@ dependencies: - requests - click - httpx - - pydantic<2.0 + - pydantic >1 + - pydantic-settings ## user client printing - rich diff --git a/devtools/conda-envs/alchemiscale-compute.yml b/devtools/conda-envs/alchemiscale-compute.yml index 56b21cef..9d43b054 100644 --- a/devtools/conda-envs/alchemiscale-compute.yml +++ b/devtools/conda-envs/alchemiscale-compute.yml @@ -15,7 +15,8 @@ dependencies: - requests - click - httpx - - pydantic<2.0 + - pydantic >1 + - pydantic-settings # perses dependencies - openeye-toolkits diff --git a/devtools/conda-envs/alchemiscale-server.yml b/devtools/conda-envs/alchemiscale-server.yml index f8909d40..90e1fd77 100644 --- a/devtools/conda-envs/alchemiscale-server.yml +++ b/devtools/conda-envs/alchemiscale-server.yml @@ -15,7 +15,8 @@ dependencies: - openmmforcefields>=0.12.0 - requests - click - - pydantic<2.0 + - pydantic >1 + - pydantic-settings ## state store - neo4j-python-driver diff --git a/devtools/conda-envs/test.yml b/devtools/conda-envs/test.yml index fdab69d7..5e981d7c 100644 --- a/devtools/conda-envs/test.yml +++ b/devtools/conda-envs/test.yml @@ -10,7 +10,8 @@ dependencies: - gufe>=1.0.0 - openfe>=1.0.1 - openmmforcefields>=0.12.0 - - pydantic<2.0 + - pydantic >1 + - pydantic-settings ## state store - neo4j-python-driver diff --git a/docs/conf.py b/docs/conf.py index 895c4f15..16f8c060 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -46,6 +46,7 @@ "passlib", "py2neo", "pydantic", + "pydantic_settings", "starlette", "yaml", ]