Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upgrade pydantic to v2+ #864

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 5 additions & 7 deletions pydatalab/pydatalab/apps/xrd/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import List, Optional

from pydantic import BaseModel
from pydantic import BaseModel, ConfigDict

__all__ = ("XRDPattern", "XRDMeasurement")

Expand Down Expand Up @@ -30,15 +30,13 @@ class XRDProcessing(BaseModel):
peak_widths: List[float]

baselines: List[List[float]]

class Config:
extra = "allow"
model_config = ConfigDict(extra="allow")


class XRDMetadata(BaseModel): ...


class XRDMeasurement(BaseModel):
data: Optional[XRDPattern]
processing: Optional[XRDProcessing]
metadata: Optional[XRDMetadata]
data: Optional[XRDPattern] = None
processing: Optional[XRDProcessing] = None
metadata: Optional[XRDMetadata] = None
24 changes: 16 additions & 8 deletions pydatalab/pydatalab/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@
from pydantic import (
AnyUrl,
BaseModel,
BaseSettings,
ConfigDict,
Field,
ValidationError,
field_validator,
root_validator,
validator,
)
from pydantic_settings import BaseSettings

from pydatalab.models import Person
from pydatalab.models.utils import RandomAlphabeticalRefcodeFactory, RefCodeFactory
Expand Down Expand Up @@ -49,20 +51,20 @@ def config_file_settings(settings: BaseSettings) -> Dict[str, Any]:
class DeploymentMetadata(BaseModel):
"""A model for specifying metadata about a datalab deployment."""

maintainer: Optional[Person]
maintainer: Optional[Person] = None
issue_tracker: Optional[AnyUrl] = Field("https://github.com/datalab-org/datalab/issues")
homepage: Optional[AnyUrl]
homepage: Optional[AnyUrl] = None
source_repository: Optional[AnyUrl] = Field("https://github.com/datalab-org/datalab")

@validator("maintainer")
@field_validator("maintainer")
@classmethod
def strip_fields_from_person(cls, v):
if not v.contact_email:
raise ValueError("Must provide contact email for maintainer.")

return Person(contact_email=v.contact_email, display_name=v.display_name)

class Config:
extra = "allow"
model_config = ConfigDict(extra="allow")


class BackupStrategy(BaseModel):
Expand All @@ -73,7 +75,8 @@ class BackupStrategy(BaseModel):
description="Whether this backup strategy is active; i.e., whether it is actually used. All strategies will be disabled in testing scenarios.",
)
hostname: str | None = Field(
description="The hostname of the SSH-accessible server on which to store the backup (`None` indicates local backups)."
None,
description="The hostname of the SSH-accessible server on which to store the backup (`None` indicates local backups).",
)
location: Path = Field(
description="The location under which to store the backups on the host. Each backup will be date-stamped and stored in a subdirectory of this location."
Expand Down Expand Up @@ -271,6 +274,8 @@ def validate_cache_ages(cls, values):
)
return values

# TODO[pydantic]: We couldn't refactor the `validator`, please replace it by `field_validator` manually.
# Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-validators for more information.
@validator("IDENTIFIER_PREFIX", pre=True, always=True)
def validate_identifier_prefix(cls, v, values):
"""Make sure that the identifier prefix is set and is valid, raising clear error messages if not.
Expand Down Expand Up @@ -307,7 +312,8 @@ def deactivate_backup_strategies_during_testing(cls, values):

return values

@validator("LOG_FILE")
@field_validator("LOG_FILE")
@classmethod
def make_missing_log_directory(cls, v):
"""Make sure that the log directory exists and is writable."""
if v is None:
Expand All @@ -320,6 +326,8 @@ def make_missing_log_directory(cls, v):
raise RuntimeError(f"Unable to create log file at {v}") from exc
return v

# TODO[pydantic]: We couldn't refactor this class, please create the `model_config` manually.
# Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-config for more information.
class Config:
env_prefix = "pydatalab_"
extra = "allow"
Expand Down
19 changes: 12 additions & 7 deletions pydatalab/pydatalab/models/cells.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
from pydatalab.models.items import Item
from pydatalab.models.utils import Constituent

# from pydatalab.logger import LOGGER


class CellComponent(Constituent): ...

Expand All @@ -31,25 +29,30 @@ class Cell(Item):
type: str = Field("cells", const="cells", pattern="^cells$")

cell_format: Optional[CellFormat] = Field(
None,
description="The form factor of the cell, e.g., coin, pouch, in situ or otherwise.",
)

cell_format_description: Optional[str] = Field(
description="Additional human-readable description of the cell form factor, e.g., 18650, AMPIX, CAMPIX"
None,
description="Additional human-readable description of the cell form factor, e.g., 18650, AMPIX, CAMPIX",
)

cell_preparation_description: Optional[str] = Field()
cell_preparation_description: Optional[str] = Field(None)

characteristic_mass: Optional[float] = Field(
description="The characteristic mass of the cell in milligrams. Can be used to normalize capacities."
None,
description="The characteristic mass of the cell in milligrams. Can be used to normalize capacities.",
)

characteristic_chemical_formula: Optional[str] = Field(
description="The chemical formula of the active material. Can be used to calculated molar mass in g/mol for normalizing capacities."
None,
description="The chemical formula of the active material. Can be used to calculated molar mass in g/mol for normalizing capacities.",
)

characteristic_molar_mass: Optional[float] = Field(
description="The molar mass of the active material, in g/mol. Will be inferred from the chemical formula, or can be supplied if it cannot be supplied"
None,
description="The molar mass of the active material, in g/mol. Will be inferred from the chemical formula, or can be supplied if it cannot be supplied",
)

positive_electrode: List[CellComponent] = Field([])
Expand All @@ -60,6 +63,8 @@ class Cell(Item):

active_ion_charge: float = Field(1)

# TODO[pydantic]: We couldn't refactor the `validator`, please replace it by `field_validator` manually.
# Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-validators for more information.
@validator("characteristic_molar_mass", always=True, pre=True)
def set_molar_mass(cls, v, values):
from periodictable import formula
Expand Down
4 changes: 2 additions & 2 deletions pydatalab/pydatalab/models/collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ class Collection(Entry, HasOwner, HasBlocks):
collection_id: HumanReadableIdentifier = Field(None)
"""A short human-readable/usable name for the collection."""

title: Optional[str]
title: Optional[str] = None
"""A descriptive title for the collection."""

description: Optional[str]
description: Optional[str] = None
"""A description of the collection, either in plain-text or a markup language."""

num_items: Optional[int] = Field(None)
Expand Down
12 changes: 6 additions & 6 deletions pydatalab/pydatalab/models/entries.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import abc
from typing import List, Optional

from pydantic import BaseModel, Field, root_validator
from pydantic import BaseModel, ConfigDict, Field, model_validator

from pydatalab.models.relationships import TypedRelationship
from pydatalab.models.utils import (
Expand Down Expand Up @@ -34,7 +34,8 @@ class Entry(BaseModel, abc.ABC):
relationships: Optional[List[TypedRelationship]] = None
"""A list of related entries and their types."""

@root_validator(pre=True)
@model_validator(mode="before")
@classmethod
def check_id_names(cls, values):
"""Slightly upsetting hack: this case *should* be covered by the pydantic setting for
populating fields by alias names.
Expand Down Expand Up @@ -63,7 +64,6 @@ def to_reference(self, additional_fields: Optional[List[str]] = None) -> "EntryR

return EntryReference(**data)

class Config:
allow_population_by_field_name = True
json_encoders = JSON_ENCODERS
extra = "ignore"
# TODO[pydantic]: The following keys were removed: `json_encoders`.
# Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-config for more information.
model_config = ConfigDict(populate_by_name=True, json_encoders=JSON_ENCODERS, extra="ignore")
8 changes: 4 additions & 4 deletions pydatalab/pydatalab/models/equipment.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ class Equipment(Item):

type: str = Field("equipment", const="equipment", pattern="^equipment$")

serial_numbers: Optional[str]
serial_numbers: Optional[str] = None
"""A string describing one or more serial numbers for the instrument."""

manufacturer: Optional[str]
manufacturer: Optional[str] = None
"""The manufacturer of this piece of equipment"""

location: Optional[str]
location: Optional[str] = None
"""Place where the equipment is located"""

contact: Optional[str]
contact: Optional[str] = None
"""Contact information for equipment (e.g., email address or phone number)."""
22 changes: 12 additions & 10 deletions pydatalab/pydatalab/models/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ class File(Entry, HasOwner, HasRevisionControl):

type: str = Field("files", const="files", pattern="^files$")

size: Optional[int] = Field(description="The size of the file on disk in bytes.")
size: Optional[int] = Field(None, description="The size of the file on disk in bytes.")

last_modified_remote: Optional[IsoformatDateTime] = Field(
description="The last date/time at which the remote file was modified."
None, description="The last date/time at which the remote file was modified."
)

item_ids: List[str] = Field(description="A list of item IDs associated with this file.")
Expand All @@ -27,27 +27,29 @@ class File(Entry, HasOwner, HasRevisionControl):

extension: str = Field(description="The file extension that the file was uploaded with.")

original_name: Optional[str] = Field(description="The raw filename as uploaded.")
original_name: Optional[str] = Field(None, description="The raw filename as uploaded.")

location: Optional[str] = Field(description="The location of the file on disk.")
location: Optional[str] = Field(None, description="The location of the file on disk.")

url_path: Optional[str] = Field(description="The path to a remote file.")
url_path: Optional[str] = Field(None, description="The path to a remote file.")

source: Optional[str] = Field(
description="The source of the file, e.g. 'remote' or 'uploaded'."
None, description="The source of the file, e.g. 'remote' or 'uploaded'."
)

time_added: datetime.datetime = Field(description="The timestamp for the original file upload.")

metadata: Optional[Dict[Any, Any]] = Field(description="Any additional metadata.")
metadata: Optional[Dict[Any, Any]] = Field(None, description="Any additional metadata.")

representation: Optional[Any] = Field()
representation: Optional[Any] = Field(None)

source_server_name: Optional[str] = Field(
description="The server name at which the file is stored."
None, description="The server name at which the file is stored."
)

source_path: Optional[str] = Field(description="The path to the file on the remote resource.")
source_path: Optional[str] = Field(
None, description="The path to the file on the remote resource."
)

is_live: bool = Field(
description="Whether or not the file should be watched for future updates."
Expand Down
10 changes: 6 additions & 4 deletions pydatalab/pydatalab/models/items.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,23 @@ class Item(Entry, HasOwner, HasRevisionControl, IsCollectable, HasBlocks, abc.AB
item_id: HumanReadableIdentifier
"""A locally unique, human-readable identifier for the entry. This ID is mutable."""

description: Optional[str]
description: Optional[str] = None
"""A description of the item, either in plain-text or a markup language."""

date: Optional[IsoformatDateTime]
date: Optional[IsoformatDateTime] = None
"""A relevant 'creation' timestamp for the entry (e.g., purchase date, synthesis date)."""

name: Optional[str]
name: Optional[str] = None
"""An optional human-readable/usable name for the entry."""

files: Optional[List[File]]
files: Optional[List[File]] = None
"""Any files attached to this sample."""

file_ObjectIds: List[PyObjectId] = Field([])
"""Links to object IDs of files stored within the database."""

# TODO[pydantic]: We couldn't refactor the `validator`, please replace it by `field_validator` manually.
# Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-validators for more information.
@validator("refcode", pre=True, always=True)
def refcode_validator(cls, v):
"""Generate a refcode if not provided."""
Expand Down
23 changes: 15 additions & 8 deletions pydatalab/pydatalab/models/people.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from enum import Enum
from typing import List, Optional
from typing import List, Literal, Optional

import bson
import bson.errors
from pydantic import BaseModel, ConstrainedStr, Field, parse_obj_as, validator
from pydantic import BaseModel, ConstrainedStr, Field, field_validator, parse_obj_as, validator
from pydantic import EmailStr as PydanticEmailStr

from pydatalab.models.entries import Entry
Expand Down Expand Up @@ -36,9 +36,11 @@ class Identity(BaseModel):
verified: bool = Field(False)
"""Whether the identity has been verified (by some means, e.g., OAuth2 or email)"""

display_name: Optional[str]
display_name: Optional[str] = None
"""The user's display name associated with the identity, also to be exposed in free text searches."""

# TODO[pydantic]: We couldn't refactor the `validator`, please replace it by `field_validator` manually.
# Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-validators for more information.
@validator("name", pre=True, always=True)
def add_missing_name(cls, v, values):
"""If the identity is created without a free-text 'name', then
Expand All @@ -55,6 +57,8 @@ def add_missing_name(cls, v, values):

return v

# TODO[pydantic]: We couldn't refactor the `validator`, please replace it by `field_validator` manually.
# Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-validators for more information.
@validator("verified", pre=True, always=True)
def add_missing_verification(cls, v):
"""Fills in missing value for `verified` if not given."""
Expand Down Expand Up @@ -101,32 +105,35 @@ class AccountStatus(str, Enum):
class Person(Entry):
"""A model that describes an individual and their digital identities."""

type: str = Field("people", const=True)
type: Literal["people"] = "people"
"""The entry type as a string."""

identities: List[Identity] = Field(default_factory=list)
"""A list of identities attached to this person, e.g., email addresses, OAuth accounts."""

display_name: Optional[DisplayName]
display_name: Optional[DisplayName] = None
"""The user-chosen display name."""

contact_email: Optional[EmailStr]
contact_email: Optional[EmailStr] = None
"""In the case of multiple *verified* email identities, this email will be used as the primary contact."""

managers: Optional[List[PyObjectId]]
managers: Optional[List[PyObjectId]] = None
"""A list of user IDs that can manage this person's items."""

account_status: AccountStatus = Field(AccountStatus.UNVERIFIED)
"""The status of the user's account."""

# TODO[pydantic]: We couldn't refactor the `validator`, please replace it by `field_validator` manually.
# Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-validators for more information.
@validator("type", pre=True, always=True)
def add_missing_type(cls, v):
"""Fill in missing `type` field if not provided."""
if v is None:
v = "people"
return v

@validator("type", pre=True)
@field_validator("type", mode="before")
@classmethod
def set_default_type(cls, _):
return "people"

Expand Down
Loading
Loading