diff --git a/cyclops/report/model_card/base.py b/cyclops/report/model_card/base.py index d0f6dc0db..d8ab4829c 100644 --- a/cyclops/report/model_card/base.py +++ b/cyclops/report/model_card/base.py @@ -1,100 +1,130 @@ """Base classes for model card fields and sections.""" import keyword -from typing import Any, Callable, Dict, List, Literal, Optional, Union +import typing +from typing import Any, ClassVar, Dict, List, Literal, Optional, Union import numpy as np -from pydantic import BaseConfig, BaseModel, Extra, root_validator -from pydantic.fields import FieldInfo, ModelField +from pydantic import BaseModel, ConfigDict, model_validator +from pydantic.fields import FieldInfo + + +def _unwrap_optional(annotation: Any) -> Any: + """Unwrap `Optional[SomeType]`/`Union[SomeType, None]` to `SomeType`.""" + if typing.get_origin(annotation) is Union: + args = [arg for arg in typing.get_args(annotation) if arg is not type(None)] + if len(args) == 1: + return args[0] + return annotation + + +def unwrap_optional_type(field_info: FieldInfo) -> Any: + """Get the annotated type of a field, unwrapping `Optional`/`Union[..., None]`. + + All model card fields/sections are declared as `Optional[SomeType]`, so + `field_info.annotation` is `Union[SomeType, None]`. This returns `SomeType`. + """ + return _unwrap_optional(field_info.annotation) + + +def _get_list_item_type(field_info: FieldInfo) -> Any: + """Get the item type of a `List[X]`/`Optional[List[X]]` field annotation.""" + annotation = _unwrap_optional(field_info.annotation) + if typing.get_origin(annotation) is list: + args = typing.get_args(annotation) + if args: + return args[0] + return annotation def _check_composable_fields( - cls: BaseModel, + cls: type, values: Dict[str, Any], ) -> Dict[str, Any]: """Check that the type of the field is allowed in the section.""" for attr, value in values.items(): if issubclass(type(value), BaseModelCardField) and ( - value.__config__.composable_with is None + value.composable_with is None or ( - value.__config__.composable_with != "Any" - and cls.__name__ not in value.__config__.composable_with # type: ignore [attr-defined] # noqa: E501 + value.composable_with != "Any" + and cls.__name__ not in value.composable_with ) ): - print(issubclass(type(value), BaseModelCardField)) - print(value.__config__.composable_with) raise ValueError( f"Field `{attr}`(type={type(value)}) is not allowed in " - f"`{cls.__name__}` section.", # type: ignore[attr-defined] + f"`{cls.__name__}` section.", ) return values -class BaseModelCardConfig(BaseConfig): - """Global config for model card fields. - - Attributes - ---------- - extra : Extra - Whether to allow extra attributes in the field object. - smart_union : bool - Whether `Union` should check all allowed types before even trying to - coerce. - validate_all : bool - Whether to validate all attributes in the field object. - validate_assignment : bool - Whether to validate assignments of attributes in the field object. - json_encoders : Dict[Any, Callable] - Custom JSON encoders. - - """ - - extra: Extra = Extra.allow - smart_union: bool = True - validate_all: bool = True - validate_assignment: bool = True - json_encoders: Dict[Any, Callable[..., Any]] = {np.ndarray: lambda v: v.tolist()} +# Shared config for model card fields and sections. +# - extra: allow extra attributes on the field/section object. +# - validate_default: validate default values. +# - validate_assignment: validate attribute assignments. +# - json_encoders: custom JSON encoders. +_MODEL_CARD_CONFIG = ConfigDict( + extra="allow", + validate_default=True, + validate_assignment=True, + json_encoders={np.ndarray: lambda v: v.tolist()}, +) class BaseModelCardField(BaseModel): - """Base class for model card fields.""" + """Base class for model card fields. + + Class Parameters + ---------------- + composable_with : Literal["Any"], List[str], optional + The sections this field can be dynamically composed with. If "Any", + the field can be composed with any subclass of `BaseModelCardSection` + or `BaseModelCardField`. If None, the field cannot be dynamically + composed with any other fields or sections - it must be explicitly + added to a section. If a list of strings, the strings are the names + treated as class names for subclasses of `BaseModelCardSection` or + `BaseModelCardField`. + list_factory : bool, default=False + Whether multiple instances of this field can be added to a section + in a list. - class Config(BaseModelCardConfig): - """Global config for model card fields. + """ - Attributes - ---------- - composable_with : Literal["Any"], List[str], optional - The sections this field can be dynamically composed with. If "Any", - the field can be composed with any subclass of `BaseModelCardSection` - or `BaseModelCardField`. If None, the field cannot be dynamically - composed with any other fields or sections - it must be explicitly - added to a section. If a list of strings, the strings are the names - treated as class names for subclasses of `BaseModelCardSection` or - `BaseModelCardField`. - list_factory : bool, default=False - Whether multiple instances of this field can be added to a section - in a list. + composable_with: ClassVar[Optional[Union[Literal["Any"], List[str]]]] = "Any" + list_factory: ClassVar[bool] = False - """ + model_config = _MODEL_CARD_CONFIG - composable_with: Optional[Union[Literal["Any"], List[str]]] = "Any" - list_factory: bool = False + def __init_subclass__( + cls, + composable_with: Optional[Union[Literal["Any"], List[str]]] = "Any", + list_factory: bool = False, + **kwargs: Any, + ) -> None: + """Capture the `composable_with`/`list_factory` class arguments.""" + super().__init_subclass__(**kwargs) + cls.composable_with = composable_with + cls.list_factory = list_factory - _validate_composition = root_validator(pre=True, allow_reuse=True)( - _check_composable_fields, - ) + @model_validator(mode="before") + @classmethod + def _validate_composition(cls, values: Any) -> Any: + if isinstance(values, dict): + return _check_composable_fields(cls, values) + return values class BaseModelCardSection(BaseModel): """Base class for model card sections.""" - Config = BaseModelCardConfig + model_config = _MODEL_CARD_CONFIG - _validate_composition = root_validator(pre=True, allow_reuse=True)( - _check_composable_fields, - ) + @model_validator(mode="before") + @classmethod + def _validate_composition(cls, values: Any) -> Any: + if isinstance(values, dict): + return _check_composable_fields(cls, values) + return values def update_field(self, name: str, value: Any) -> None: """Update the field with the given name to the given value. @@ -114,11 +144,23 @@ def update_field(self, name: str, value: Any) -> None: If the field does not exist. """ - if name not in self.__fields__: + if name not in type(self).model_fields: raise ValueError(f"Field {name} does not exist.") - field = self.__fields__[name] + field = type(self).model_fields[name] if field.default_factory == list or isinstance(getattr(self, name), list): # noqa: E721 + item_type = _get_list_item_type(field) + if ( + isinstance(value, BaseModel) + and isinstance(item_type, type) + and not isinstance(value, item_type) + ): + # pydantic v2 does not automatically coerce an arbitrary + # BaseModel instance into a differently-typed nested model + # (unlike pydantic v1); dump it to a dict first so the list + # validation below can construct the expected item type. + value = value.model_dump() + # NOTE: pydantic does not trigger validation when appending to a list, # but if `validate_assignment` is set to `True`, then validation will # be triggered when the list is assigned to the field. @@ -150,29 +192,10 @@ def add_field(self, name: str, value: Any) -> None: f" Got {name} instead.", ) - type_ = type(value) - default_factory = None - if isinstance(value, list): - default_factory = list - if ( - isinstance(value, BaseModelCardField) - and value.__config__.list_factory is True # type: ignore[attr-defined] - ): - default_factory = list + if isinstance(value, BaseModelCardField) and value.list_factory is True: value = [value] # add field as a list - type_ = List[type_] # type: ignore[valid-type] + # with `extra="allow"`, assigning a name that is not a declared model + # field stores it as an extra attribute, which is included in + # `model_dump`/`model_dump_json` output like any other field setattr(self, name, value) - - # modify __fields__ to include new field - self.__fields__[name] = ModelField( - name=name, - type_=type_, - required=False, - class_validators=None, - model_config=BaseModelCardField.Config, - default_factory=default_factory, - field_info=FieldInfo(unique_items=True) - if default_factory == list # noqa: E721 - else None, - ) diff --git a/cyclops/report/model_card/fields.py b/cyclops/report/model_card/fields.py index f4f8162b0..eeb548577 100644 --- a/cyclops/report/model_card/fields.py +++ b/cyclops/report/model_card/fields.py @@ -3,7 +3,7 @@ import inspect from datetime import date as dt_date from datetime import datetime as dt_datetime -from typing import Any, Dict, List, Literal, Optional, Tuple, Union +from typing import Any, List, Literal, Optional, Tuple, Union import numpy as np import numpy.typing as npt @@ -17,8 +17,8 @@ StrictFloat, StrictInt, StrictStr, - root_validator, - validator, + field_validator, + model_validator, ) from cyclops.report.model_card.base import BaseModelCardField @@ -90,24 +90,33 @@ class License( ) text_url: Optional[AnyUrl] = Field(None, description="A URL to the license text.") - @root_validator(skip_on_failure=True) - def validate_spdx_identifier( - cls: "License", # noqa: N805 - values: Dict[str, StrictStr], - ) -> Dict[str, Union[StrictStr, AnyUrl]]: + @model_validator(mode="after") + def validate_spdx_identifier(self) -> "License": """Validate the SPDX license identifier.""" - spdx_id = values["identifier"] + spdx_id = self.identifier try: get_spdx_licensing().parse(spdx_id, validate=True) - if spdx_id not in [None, ""] and values.get("text_url") is None: - values["text_url"] = cls._get_license_text_url(spdx_id) # type: ignore + if spdx_id not in [None, ""] and self.text_url is None: + text_url = self._get_license_text_url(spdx_id) + # bypass the validated `__setattr__` (which would re-run this + # "after" validator and recurse indefinitely); coerce to + # `AnyUrl` ourselves since validation is being skipped + object.__setattr__( + self, + "text_url", + AnyUrl(text_url) if text_url is not None else None, + ) except ExpressionError as exc: - if spdx_id.lower() not in ["proprietary", "unlicensed", "unknown"]: + if spdx_id is None or spdx_id.lower() not in [ + "proprietary", + "unlicensed", + "unknown", + ]: raise ValueError( "Expected a valid SPDX license identifier " f"(https://spdx.org/licenses/). Got {spdx_id} instead.", ) from exc - return values + return self @staticmethod def _get_license_text_url(identifier: Optional[str]) -> Optional[str]: @@ -154,9 +163,10 @@ class Citation( description="The citation content in BibTeX format.", ) - @validator("content") + @field_validator("content") + @classmethod def parse_content( - cls: "Citation", # noqa: N805 + cls: "type[Citation]", # noqa: N805 value: StrictStr, ) -> StrictStr: """Parse the citation content.""" @@ -226,12 +236,10 @@ class SensitiveData(BaseModelCardField, composable_with=["Dataset"], list_factor "aggregated. Please describe any such fields here." ), default_factory=list, - unique_items=True, ) sensitive_data_used: Optional[List[StrictStr]] = Field( description="A list of sensitive data used in the deployed model.", default_factory=list, - unique_items=True, ) justification: Optional[StrictStr] = Field( None, @@ -253,23 +261,19 @@ class Dataset(BaseModelCardField, composable_with=["Datasets"], list_factory=Tru citations: Optional[List[Citation]] = Field( description="How should the dataset be cited?", default_factory=list, - unique_items=True, ) references: Optional[List[Reference]] = Field( description="Provide any additional links to resources the reader may need.", default_factory=list, - unique_items=True, ) licenses: Optional[List[License]] = Field( description="The license information for the dataset.", default_factory=list, - unique_items=True, ) version: Optional[Version] = Field(None, description="The version of the dataset.") features: Optional[List[StrictStr]] = Field( description="A list of features in the dataset.", default_factory=list, - unique_items=True, ) graphics: Optional[GraphicsCollection] = Field( None, @@ -419,9 +423,10 @@ class UseCase( ), ) - @validator("kind") + @field_validator("kind") + @classmethod def kind_must_be_valid( - cls: "UseCase", # noqa: N805 + cls: "type[UseCase]", # noqa: N805 value: str, ) -> str: """Validate the use case kind.""" @@ -595,7 +600,7 @@ class MetricCard( ) history: List[StrictFloat] = Field( - None, + default_factory=list, description="History of the metric over time.", ) diff --git a/cyclops/report/model_card/model_card.py b/cyclops/report/model_card/model_card.py index dc75640ef..85a44a8f1 100644 --- a/cyclops/report/model_card/model_card.py +++ b/cyclops/report/model_card/model_card.py @@ -3,9 +3,10 @@ import inspect from typing import Optional -from pydantic import BaseModel, Extra, Field +import numpy as np +from pydantic import BaseModel, ConfigDict, Field -from cyclops.report.model_card.base import BaseModelCardConfig, BaseModelCardSection +from cyclops.report.model_card.base import BaseModelCardSection, unwrap_optional_type from cyclops.report.model_card.sections import ( Considerations, Datasets, @@ -26,10 +27,15 @@ class ModelCard(BaseModel): """ - class Config(BaseModelCardConfig): - """Model Card configuration.""" - - extra: Extra = Extra.forbid + model_config = ConfigDict( + extra="forbid", + validate_default=True, + validate_assignment=True, + # `json_encoders` must be set on the outermost model for pydantic to + # apply it while serializing nested fields (e.g. PerformanceMetric.value + # holding a numpy array) via `.model_dump_json()`. + json_encoders={np.ndarray: lambda v: v.tolist()}, + ) overview: Optional[Overview] = Field( None, @@ -80,7 +86,7 @@ def get_section(self, section_name: str) -> BaseModelCardSection: If the given `section_name` is not a subclass of `BaseModel`. """ - sections = self.__fields__ + sections = type(self).model_fields if section_name not in sections: raise ValueError( f"Section `{section_name}` not found in model card. " @@ -89,7 +95,7 @@ def get_section(self, section_name: str) -> BaseModelCardSection: section: Optional[BaseModelCardSection] = getattr(self, section_name) if section is None: - section = sections[section_name].type_() + section = unwrap_optional_type(sections[section_name])() setattr(self, section_name, section) if not issubclass(section.__class__, BaseModel): diff --git a/cyclops/report/model_card/sections.py b/cyclops/report/model_card/sections.py index f6dca06ed..d2d4aa674 100644 --- a/cyclops/report/model_card/sections.py +++ b/cyclops/report/model_card/sections.py @@ -60,22 +60,18 @@ class ModelDetails(BaseModelCardSection): owners: Optional[List[Owner]] = Field( description="The individuals or teams who own the model.", default_factory=list, - unique_items=True, ) licenses: Optional[List[License]] = Field( description="The license information for the model.", default_factory=list, - unique_items=True, ) citations: Optional[List[Citation]] = Field( description="How should the model be cited?", default_factory=list, - unique_items=True, ) references: Optional[List[Reference]] = Field( description="Provide any additional references the reader may need.", default_factory=list, - unique_items=True, ) path: Optional[StrictStr] = Field(None, description="Where is this model stored?") regulatory_requirements: Optional[List[RegulatoryRequirement]] = Field( @@ -83,7 +79,6 @@ class ModelDetails(BaseModelCardSection): "Provide any regulatory requirements that the model should comply to." ), default_factory=list, - unique_items=True, ) @@ -140,12 +135,10 @@ class Considerations(BaseModelCardSection): users: Optional[List[User]] = Field( description="Who are the primary intended users of the model?", default_factory=list, - unique_items=True, ) use_cases: Optional[List[UseCase]] = Field( description="What are the intended use cases of the model?", default_factory=list, - unique_items=True, ) fairness_assessment: Optional[List[FairnessAssessment]] = Field( description=""" @@ -157,7 +150,6 @@ class Considerations(BaseModelCardSection): ethical_considerations: Optional[List[Risk]] = Field( description="What are the ethical risks involved in application of this model?", default_factory=list, - unique_items=True, ) diff --git a/cyclops/report/report.py b/cyclops/report/report.py index 19f916581..df5f9e6cc 100644 --- a/cyclops/report/report.py +++ b/cyclops/report/report.py @@ -17,7 +17,7 @@ from scour import scour from cyclops.report.model_card import ModelCard # type: ignore[attr-defined] -from cyclops.report.model_card.base import BaseModelCardField +from cyclops.report.model_card.base import BaseModelCardField, unwrap_optional_type from cyclops.report.model_card.fields import ( Citation, Dataset, @@ -107,7 +107,8 @@ def from_json_file( The model card report. """ - model_card = ModelCard.parse_file(path) + with open(path, encoding="utf-8") as f_handle: + model_card = ModelCard.model_validate_json(f_handle.read()) report = ModelCardReport(output_dir=output_dir) report._model_card = model_card return report @@ -136,9 +137,9 @@ def _log_field( """ section_name = str_to_snake_case(section_name) section = self._model_card.get_section(section_name) - field_value = field_type.parse_obj(data) + field_value = field_type.model_validate(data) - if field_name in section.__fields__: + if field_name in type(section).model_fields: section.update_field(field_name, field_value) else: field_name = str_to_snake_case(field_name) @@ -162,10 +163,10 @@ def log_from_dict(self, data: Dict[str, Any], section_name: str) -> None: section = self._model_card.get_section(section_name) # get data already in section and update with new data - section_data = section.dict() + section_data = section.model_dump() section_data.update(data) - populated_section = section.__class__.parse_obj(section_data) + populated_section = section.__class__.model_validate(section_data) setattr(self._model_card, section_name, populated_section) def log_descriptor( @@ -247,9 +248,10 @@ def _log_graphic_collection( section = self._model_card.get_section(section_name) # append graphic to existing GraphicsCollection or create new one + section_fields = type(section).model_fields if ( - "graphics" in section.__fields__ - and section.__fields__["graphics"].type_ is GraphicsCollection + "graphics" in section_fields + and unwrap_optional_type(section_fields["graphics"]) is GraphicsCollection and section.graphics is not None # type: ignore ): section.graphics.collection.append(graphic) # type: ignore @@ -275,9 +277,11 @@ def _log_metric_card_collection( section = self._model_card.get_section(section_name) # append graphic to existing GraphicsCollection or create new one + section_fields = type(section).model_fields if ( - "metric_cards" in section.__fields__ - and section.__fields__["metric_cards"].type_ is MetricCardCollection + "metric_cards" in section_fields + and unwrap_optional_type(section_fields["metric_cards"]) + is MetricCardCollection and section.metric_cards is not None # type: ignore ): section.metric_cards.collection.append(metric_cards) # type: ignore @@ -323,7 +327,7 @@ def log_image(self, img_path: str, caption: str, section_name: str) -> None: img.save(buffered, format=img.format) img_base64 = base64.b64encode(buffered.getvalue()).decode() - graphic = Graphic.parse_obj( + graphic = Graphic.model_validate( {"name": caption, "image": f"data:image/{img.format};base64,{img_base64}"}, ) @@ -373,7 +377,7 @@ def log_plotly_figure( data = {"name": caption, "image": f"data:image/svg+xml;base64,{svg}"} - graphic = Graphic.parse_obj(data) # create Graphic object from data + graphic = Graphic.model_validate(data) # create Graphic object from data self._log_graphic_collection(graphic, "Plots", section_name) @@ -1030,7 +1034,7 @@ def log_performance_metrics( def _validate(self) -> None: """Validate the model card.""" - ModelCard.validate(self._model_card.dict()) + ModelCard.model_validate(self._model_card.model_dump()) def _write_file(self, path: str, content: str) -> None: """Write a file to the given path. @@ -1152,9 +1156,8 @@ def export( if len(report_paths) != 0: latest_report_path = sorted(report_paths)[-1] - latest_report = ModelCard.parse_file( - latest_report_path, - ) + with open(latest_report_path, encoding="utf-8") as f_handle: + latest_report = ModelCard.model_validate_json(f_handle.read()) latest_report_metric_cards: List[List[MetricCard]] = [] sweep_metric_cards(latest_report, latest_report_metric_cards) latest_report_metric_cards_set = latest_report_metric_cards[0] @@ -1210,7 +1213,7 @@ def export( json_path = report_path.replace(".html", ".json") self._write_file( json_path, - self._model_card.json(indent=2, exclude_unset=True), + self._model_card.model_dump_json(indent=2, exclude_unset=True), ) return report_path diff --git a/cyclops/report/utils.py b/cyclops/report/utils.py index a4f9a3a04..7f20e82d9 100644 --- a/cyclops/report/utils.py +++ b/cyclops/report/utils.py @@ -12,6 +12,7 @@ import numpy as np import plotly.graph_objects as go +from pydantic import BaseModel from cyclops.report.model_card import ModelCard # type: ignore[attr-defined] from cyclops.report.model_card.fields import ( @@ -350,7 +351,7 @@ def sweep_tests(model_card: Any, tests: List[Any]) -> None: field = field[1] # noqa: PLW2901 if isinstance(field, Test): tests.append(field) - if hasattr(field, "__fields__"): + if isinstance(field, BaseModel): sweep_tests(field, tests) if ( isinstance(field, list) @@ -384,7 +385,7 @@ def sweep_metrics(model_card: Any, metrics: List[Any]) -> None: field = field[1] # noqa: PLW2901 if isinstance(field, PerformanceMetric): metrics.append(field) - if hasattr(field, "__fields__"): + if isinstance(field, BaseModel): sweep_metrics(field, metrics) if ( isinstance(field, list) @@ -418,7 +419,7 @@ def sweep_metric_cards(model_card: Any, metric_cards: List[Any]) -> None: field = field[1] # noqa: PLW2901 if isinstance(field, MetricCard): metric_cards.append(field) - if hasattr(field, "__fields__"): + if isinstance(field, BaseModel): sweep_metric_cards(field, metric_cards) if ( isinstance(field, list) @@ -454,7 +455,7 @@ def sweep_graphics(model_card: Any, graphics: list[Any], caption: str) -> None: field = field[1] # noqa: PLW2901 if isinstance(field, Graphic) and field.name == caption: graphics.append(field) - if hasattr(field, "__fields__"): + if isinstance(field, BaseModel): sweep_graphics(field, graphics, caption) if ( isinstance(field, list) @@ -1044,7 +1045,7 @@ def create_metric_card_plot( config={"displayModeBar": False}, ), } - graphic = Graphic.parse_obj(data) + graphic = Graphic.model_validate(data) return GraphicsCollection(description="plot", collection=[graphic]) diff --git a/deploy/report/api/main.py b/deploy/report/api/main.py index 1426e510b..457a87d7d 100644 --- a/deploy/report/api/main.py +++ b/deploy/report/api/main.py @@ -11,7 +11,7 @@ from fastapi import FastAPI, HTTPException, Request from fastapi.responses import HTMLResponse from fastapi.templating import Jinja2Templates -from pydantic import BaseModel, Field, validator +from pydantic import BaseModel, Field, ValidationInfo, field_validator from cyclops.data.slicer import SliceSpec from cyclops.evaluate import evaluator @@ -34,23 +34,21 @@ class EvaluationInput(BaseModel): """Input data for evaluation.""" - preds_prob: List[float] = Field(..., min_items=1) - target: List[float] = Field(..., min_items=1) + preds_prob: List[float] = Field(..., min_length=1) + target: List[float] = Field(..., min_length=1) metadata: Dict[str, List[Any]] = Field(default_factory=dict) + @field_validator("preds_prob", "target") @classmethod - @validator("preds_prob", "target") - def check_list_length( - cls, v: List[float], values: Dict[str, List[Any]], **kwargs: Any - ) -> List[float]: + def check_list_length(cls, v: List[float], info: ValidationInfo) -> List[float]: """Check if preds_prob and target have the same length. Parameters ---------- v : List[float] List of values. - values : Dict[str, List[Any]] - Dictionary of values. + info : ValidationInfo + Validation info, including previously-validated field values. Returns ------- @@ -63,14 +61,15 @@ def check_list_length( If preds_prob and target have different lengths. """ - if "preds_prob" in values and len(v) != len(values["preds_prob"]): + preds_prob = info.data.get("preds_prob") + if preds_prob is not None and len(v) != len(preds_prob): raise ValueError("preds_prob and target must have the same length") return v + @field_validator("metadata") @classmethod - @validator("metadata") def check_metadata_length( - cls, v: Dict[str, List[Any]], values: Dict[str, List[Any]], **kwargs: Any + cls, v: Dict[str, List[Any]], info: ValidationInfo ) -> Dict[str, List[Any]]: """Check if metadata columns have the same length as preds_prob and target. @@ -78,8 +77,8 @@ def check_metadata_length( ---------- v : Dict[str, List[Any]] Dictionary of values. - values : Dict[str, List[Any]] - Dictionary of values. + info : ValidationInfo + Validation info, including previously-validated field values. Returns ------- @@ -92,9 +91,10 @@ def check_metadata_length( If metadata columns have different lengths than preds_prob and target. """ - if "preds_prob" in values: + preds_prob = info.data.get("preds_prob") + if preds_prob is not None: for column in v.values(): - if len(column) != len(values["preds_prob"]): + if len(column) != len(preds_prob): raise ValueError( "All metadata columns must have the same length as preds_prob and target" ) diff --git a/pyproject.toml b/pyproject.toml index 916ed4b29..a4ec9e067 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ dependencies = [ "datasets>=2.15,<3", "psutil>=5.9.4,<6", "pyarrow>=17.0.0,<24", - "pydantic>=1.10.11,<2", + "pydantic>=2.7.0,<3", "Jinja2>=3.1.3,<4", "spdx-tools>=0.8.1,<0.9", "pybtex>=0.24.0,<0.25", diff --git a/tests/cyclops/report/test_report.py b/tests/cyclops/report/test_report.py index 2c5e72965..7ffc85783 100644 --- a/tests/cyclops/report/test_report.py +++ b/tests/cyclops/report/test_report.py @@ -227,7 +227,8 @@ def test_log_reference(self): ) self.model_card_report.log_reference(ref) assert ( - self.model_card_report._model_card.model_details.references[0].link == ref + str(self.model_card_report._model_card.model_details.references[0].link) + == ref ) def test_log_regulation(self): diff --git a/uv.lock b/uv.lock index 5388b9431..77c114d41 100644 --- a/uv.lock +++ b/uv.lock @@ -177,6 +177,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3e/30/e900b21425a860e195f32e37657aa1f7c7f2b1bfb26f03ca209b90933c06/annotated_doc-0.0.5-py3-none-any.whl", hash = "sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101", size = 5302 }, ] +[[package]] +name = "annotated-types" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427 }, +] + [[package]] name = "antlr4-python3-runtime" version = "4.9.3" @@ -3595,7 +3604,7 @@ requires-dist = [ { name = "psutil", specifier = ">=5.9.4,<6" }, { name = "pyarrow", specifier = ">=17.0.0,<24" }, { name = "pybtex", specifier = ">=0.24.0,<0.25" }, - { name = "pydantic", specifier = ">=1.10.11,<2" }, + { name = "pydantic", specifier = ">=2.7.0,<3" }, { name = "scikit-learn", specifier = ">=1.3,<1.8" }, { name = "scipy", specifier = ">=1.11,<2" }, { name = "scour", specifier = ">=0.38.2,<0.39" }, @@ -3674,19 +3683,51 @@ test = [ [[package]] name = "pydantic" -version = "1.10.26" +version = "2.13.4" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, { name = "typing-extensions" }, + { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7b/da/fd89f987a376c807cd81ea0eff4589aade783bbb702637b4734ef2c743a2/pydantic-1.10.26.tar.gz", hash = "sha256:8c6aa39b494c5af092e690127c283d84f363ac36017106a9e66cb33a22ac412e", size = 357906 } +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775 } wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/c1/d521e64c8130e1ad9d22c270bed3fabcc0940c9539b076b639c88fd32a8d/pydantic-1.10.26-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:116233e53889bcc536f617e38c1b8337d7fa9c280f0fd7a4045947515a785637", size = 2428347 }, - { url = "https://files.pythonhosted.org/packages/2c/08/f4b804a00c16e3ea994cb640a7c25c579b4f1fa674cde6a19fa0dfb0ae4f/pydantic-1.10.26-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c3cfdd361addb6eb64ccd26ac356ad6514cee06a61ab26b27e16b5ed53108f77", size = 2212605 }, - { url = "https://files.pythonhosted.org/packages/5d/78/0df4b9efef29bbc5e39f247fcba99060d15946b4463d82a5589cf7923d71/pydantic-1.10.26-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e4451951a9a93bf9a90576f3e25240b47ee49ab5236adccb8eff6ac943adf0f", size = 2753560 }, - { url = "https://files.pythonhosted.org/packages/68/66/6ab6c1d3a116d05d2508fce64f96e35242938fac07544d611e11d0d363a0/pydantic-1.10.26-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9858ed44c6bea5f29ffe95308db9e62060791c877766c67dd5f55d072c8612b5", size = 2859235 }, - { url = "https://files.pythonhosted.org/packages/61/4e/f1676bb0fcdf6ed2ce4670d7d1fc1d6c3a06d84497644acfbe02649503f1/pydantic-1.10.26-cp311-cp311-win_amd64.whl", hash = "sha256:ac1089f723e2106ebde434377d31239e00870a7563245072968e5af5cc4d33df", size = 2066646 }, - { url = "https://files.pythonhosted.org/packages/1f/98/556e82f00b98486def0b8af85da95e69d2be7e367cf2431408e108bc3095/pydantic-1.10.26-py3-none-any.whl", hash = "sha256:c43ad70dc3ce7787543d563792426a16fd7895e14be4b194b5665e36459dd917", size = 166975 }, + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262 }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872 }, + { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255 }, + { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827 }, + { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051 }, + { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314 }, + { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146 }, + { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685 }, + { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420 }, + { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122 }, + { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573 }, + { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139 }, + { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433 }, + { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513 }, + { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114 }, + { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298 }, + { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782 }, + { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146 }, + { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492 }, + { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604 }, + { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828 }, + { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000 }, + { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286 }, + { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071 }, ] [[package]] @@ -5130,6 +5171,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571 }, ] +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611 }, +] + [[package]] name = "tzdata" version = "2026.3"