From 1933c7d5a0afafd647110b2fcc2d20ec718ae4dc Mon Sep 17 00:00:00 2001 From: mike0sv Date: Fri, 13 Feb 2026 08:30:34 +0000 Subject: [PATCH 01/11] small fix --- src/evidently/_pydantic_compat.py | 94 ----- src/evidently/cli/utils.py | 7 +- src/evidently/core/base_types.py | 20 +- src/evidently/core/container.py | 3 +- src/evidently/core/datasets.py | 21 +- src/evidently/core/metric_types.py | 60 ++- src/evidently/core/serialization.py | 3 +- src/evidently/core/tests.py | 5 +- .../descriptors/_custom_descriptors.py | 13 +- .../descriptors/_generate_descriptors.py | 11 +- src/evidently/descriptors/llm_judges.py | 2 +- src/evidently/generators/column.py | 7 +- src/evidently/legacy/base_metric.py | 67 ++-- .../legacy/calculations/data_drift.py | 56 ++- .../legacy/calculations/data_quality.py | 3 +- src/evidently/legacy/collector/app.py | 2 +- src/evidently/legacy/collector/config.py | 26 +- src/evidently/legacy/collector/storage.py | 11 +- src/evidently/legacy/core.py | 175 +++++---- .../descriptors/BERTScore_descriptor.py | 6 +- .../descriptors/contains_link_descriptor.py | 6 +- .../legacy/descriptors/custom_descriptor.py | 8 +- .../descriptors/exact_match_descriptor.py | 6 +- .../legacy/descriptors/hf_descriptor.py | 7 +- .../descriptors/is_valid_json_descriptor.py | 6 +- .../descriptors/is_valid_python_descriptor.py | 6 +- .../descriptors/is_valid_sql_descriptor.py | 6 +- .../descriptors/json_match_descriptor.py | 6 +- .../json_schema_match_descriptor.py | 5 +- .../legacy/descriptors/llm_judges.py | 36 +- ..._letter_character_percentage_descriptor.py | 6 +- .../oov_words_percentage_descriptor.py | 5 +- .../legacy/descriptors/openai_descriptor.py | 4 +- .../legacy/descriptors/regexp_descriptor.py | 6 +- .../legacy/descriptors/semantic_similarity.py | 6 +- .../descriptors/sentence_count_descriptor.py | 6 +- .../descriptors/sentiment_descriptor.py | 6 +- .../descriptors/text_contains_descriptor.py | 14 +- .../descriptors/text_length_descriptor.py | 6 +- .../descriptors/text_part_descriptor.py | 9 +- .../trigger_words_presence_descriptor.py | 5 +- .../descriptors/word_count_descriptor.py | 6 +- .../legacy/descriptors/words_descriptor.py | 14 +- .../legacy/experimental/report_set.py | 3 +- .../legacy/features/BERTScore_feature.py | 4 +- .../features/OOV_words_percentage_feature.py | 3 +- .../legacy/features/contains_link_feature.py | 3 +- .../legacy/features/custom_feature.py | 17 +- .../legacy/features/exact_match_feature.py | 4 +- .../legacy/features/generated_features.py | 23 +- src/evidently/legacy/features/hf_feature.py | 6 +- .../legacy/features/is_valid_json_feature.py | 3 +- .../features/is_valid_python_feature.py | 3 +- .../legacy/features/is_valid_sql_feature.py | 3 +- .../legacy/features/json_match_feature.py | 5 +- .../features/json_schema_match_feature.py | 3 +- src/evidently/legacy/features/llm_judge.py | 5 +- ...non_letter_character_percentage_feature.py | 3 +- .../legacy/features/openai_feature.py | 4 +- .../legacy/features/regexp_feature.py | 3 +- .../features/semantic_similarity_feature.py | 4 +- .../legacy/features/sentence_count_feature.py | 3 +- .../legacy/features/sentiment_feature.py | 5 +- .../legacy/features/text_contains_feature.py | 12 +- .../legacy/features/text_length_feature.py | 3 +- .../legacy/features/text_part_feature.py | 6 +- .../trigger_words_presence_feature.py | 5 +- .../legacy/features/word_count_feature.py | 3 +- .../legacy/features/words_feature.py | 20 +- .../classification_performance.py | 4 +- .../legacy/metric_preset/data_drift.py | 4 +- .../legacy/metric_preset/data_quality.py | 4 +- .../legacy/metric_preset/metric_preset.py | 4 +- src/evidently/legacy/metric_preset/recsys.py | 4 +- .../metric_preset/regression_performance.py | 4 +- .../legacy/metric_preset/target_drift.py | 4 +- .../legacy/metric_preset/text_evals.py | 4 +- src/evidently/legacy/metric_results.py | 208 +++++----- .../base_classification_metric.py | 4 +- .../class_balance_metric.py | 11 +- .../class_separation_metric.py | 22 +- .../classification_dummy_metric.py | 18 +- .../classification_quality_metric.py | 22 +- .../confusion_matrix_metric.py | 23 +- .../lift_curve_metric.py | 15 +- .../lift_table_metric.py | 41 +- .../classification_performance/objects.py | 16 +- .../pr_curve_metric.py | 16 +- .../pr_table_metric.py | 33 +- .../probability_distribution_metric.py | 26 +- .../quality_by_class_metric.py | 27 +- .../quality_by_feature_table.py | 25 +- .../roc_curve_metric.py | 15 +- src/evidently/legacy/metrics/custom_metric.py | 15 +- .../metrics/data_drift/column_drift_metric.py | 8 +- .../data_drift/column_interaction_plot.py | 49 ++- .../metrics/data_drift/column_value_plot.py | 16 +- .../metrics/data_drift/data_drift_table.py | 21 +- .../data_drift/dataset_drift_metric.py | 9 +- .../data_drift/embedding_drift_methods.py | 16 +- .../metrics/data_drift/embeddings_drift.py | 26 +- .../metrics/data_drift/feature_importance.py | 9 +- .../data_drift/target_by_features_table.py | 29 +- .../text_descriptors_drift_metric.py | 7 +- .../text_domain_classifier_drift_metric.py | 21 +- .../legacy/metrics/data_drift/text_metric.py | 13 +- .../column_missing_values_metric.py | 30 +- .../data_integrity/column_regexp_metric.py | 39 +- .../data_integrity/column_summary_metric.py | 79 ++-- .../dataset_missing_values_metric.py | 83 ++-- .../data_integrity/dataset_summary_metric.py | 43 +- .../data_quality/column_category_metric.py | 31 +- .../column_correlations_metric.py | 17 +- .../column_distribution_metric.py | 18 +- .../data_quality/column_quantile_metric.py | 21 +- .../data_quality/column_value_list_metric.py | 33 +- .../data_quality/column_value_range_metric.py | 31 +- .../conflict_prediction_metric.py | 13 +- .../data_quality/conflict_target_metric.py | 20 +- .../dataset_correlations_metric.py | 50 ++- .../metrics/data_quality/stability_metric.py | 7 +- .../text_descriptors_correlation_metric.py | 19 +- .../text_descriptors_distribution.py | 19 +- .../legacy/metrics/recsys/base_top_k.py | 19 +- .../legacy/metrics/recsys/diversity.py | 23 +- .../legacy/metrics/recsys/f_beta_top_k.py | 10 +- .../legacy/metrics/recsys/hit_rate_k.py | 20 +- .../legacy/metrics/recsys/item_bias.py | 24 +- src/evidently/legacy/metrics/recsys/map_k.py | 6 +- src/evidently/legacy/metrics/recsys/mar_k.py | 6 +- src/evidently/legacy/metrics/recsys/mrr.py | 20 +- src/evidently/legacy/metrics/recsys/ndcg_k.py | 6 +- .../legacy/metrics/recsys/novelty.py | 24 +- .../metrics/recsys/pairwise_distance.py | 20 +- .../legacy/metrics/recsys/personalisation.py | 30 +- .../legacy/metrics/recsys/popularity_bias.py | 34 +- .../metrics/recsys/precision_recall_k.py | 13 +- .../legacy/metrics/recsys/precision_top_k.py | 6 +- .../legacy/metrics/recsys/rec_examples.py | 29 +- .../legacy/metrics/recsys/recall_top_k.py | 6 +- .../metrics/recsys/scores_distribution.py | 26 +- .../legacy/metrics/recsys/serendipity.py | 25 +- .../legacy/metrics/recsys/train_stats.py | 20 +- .../legacy/metrics/recsys/user_bias.py | 22 +- .../abs_perc_error_in_time.py | 4 +- .../error_bias_table.py | 51 ++- .../error_distribution.py | 18 +- .../regression_performance/error_in_time.py | 4 +- .../regression_performance/error_normality.py | 35 +- .../metrics/regression_performance/objects.py | 22 +- .../predicted_and_actual_in_time.py | 4 +- .../predicted_vs_actual.py | 21 +- .../regression_dummy_metric.py | 7 +- .../regression_performance_metrics.py | 54 +-- .../regression_quality.py | 46 ++- .../regression_performance/top_error.py | 21 +- src/evidently/legacy/model/dashboard.py | 3 +- src/evidently/legacy/model/widget.py | 12 +- src/evidently/legacy/options/base.py | 18 +- src/evidently/legacy/options/data_drift.py | 3 +- .../legacy/renderers/base_renderer.py | 6 +- src/evidently/legacy/suite/base_suite.py | 8 +- .../test_preset/classification_binary.py | 4 +- .../test_preset/classification_binary_topk.py | 4 +- .../test_preset/classification_multiclass.py | 4 +- .../legacy/test_preset/data_drift.py | 4 +- .../legacy/test_preset/data_quality.py | 4 +- .../legacy/test_preset/data_stability.py | 4 +- .../test_preset/no_target_performance.py | 4 +- src/evidently/legacy/test_preset/recsys.py | 4 +- .../legacy/test_preset/regression.py | 4 +- .../legacy/test_preset/test_preset.py | 4 +- src/evidently/legacy/tests/base_test.py | 44 +-- .../tests/classification_performance_tests.py | 44 +-- src/evidently/legacy/tests/custom_test.py | 5 +- .../legacy/tests/data_drift_tests.py | 18 +- .../legacy/tests/data_integrity_tests.py | 68 ++-- .../legacy/tests/data_quality_tests.py | 93 ++--- src/evidently/legacy/tests/recsys_tests.py | 48 +-- .../tests/regression_performance_tests.py | 18 +- src/evidently/legacy/tests/utils.py | 3 + src/evidently/legacy/ui/api/models.py | 10 +- src/evidently/legacy/ui/app.py | 2 +- src/evidently/legacy/ui/base.py | 13 +- src/evidently/legacy/ui/components/base.py | 8 +- .../legacy/ui/components/local_storage.py | 9 +- .../legacy/ui/components/security.py | 12 +- src/evidently/legacy/ui/components/storage.py | 12 +- src/evidently/legacy/ui/config.py | 14 +- src/evidently/legacy/ui/dashboards/base.py | 15 +- src/evidently/legacy/ui/dashboards/reports.py | 10 +- .../legacy/ui/dashboards/test_suites.py | 9 +- src/evidently/legacy/ui/dashboards/utils.py | 2 +- src/evidently/legacy/ui/managers/auth.py | 3 +- src/evidently/legacy/ui/managers/projects.py | 4 +- src/evidently/legacy/ui/storage/local/base.py | 10 +- src/evidently/legacy/ui/storage/utils.py | 7 +- src/evidently/legacy/ui/utils.py | 6 +- src/evidently/legacy/ui/workspace/cloud.py | 4 +- src/evidently/legacy/ui/workspace/remote.py | 4 +- .../legacy/utils/data_preprocessing.py | 24 +- src/evidently/legacy/utils/numpy_encoder.py | 2 + src/evidently/legacy/utils/types.py | 12 +- src/evidently/llm/datagen/base.py | 8 +- src/evidently/llm/optimization/optimizer.py | 17 +- src/evidently/llm/optimization/prompts.py | 10 +- src/evidently/llm/optimization/scorers.py | 3 +- src/evidently/llm/prompts/content.py | 3 +- src/evidently/llm/rag/index.py | 8 +- src/evidently/llm/rag/splitter.py | 6 +- src/evidently/llm/templates.py | 6 +- src/evidently/llm/utils/blocks.py | 7 +- src/evidently/llm/utils/templates.py | 5 +- src/evidently/llm/utils/wrapper.py | 8 +- src/evidently/metrics/column_statistics.py | 3 - src/evidently/presets/classification.py | 3 +- src/evidently/presets/dataset_stats.py | 5 +- src/evidently/presets/regression.py | 3 +- src/evidently/pydantic_utils.py | 371 ++++++++++++------ src/evidently/sdk/artifacts.py | 20 +- src/evidently/sdk/configs.py | 5 +- src/evidently/sdk/datasets.py | 6 +- src/evidently/sdk/models.py | 6 +- src/evidently/sdk/prompts.py | 13 +- src/evidently/tests/aliases.py | 2 +- src/evidently/tests/categorical_tests.py | 6 - src/evidently/ui/backport.py | 43 +- src/evidently/ui/runner/core.py | 10 +- src/evidently/ui/service/api/datasets.py | 28 +- src/evidently/ui/service/api/llm_judges.py | 2 +- src/evidently/ui/service/api/models.py | 3 +- src/evidently/ui/service/api/projects.py | 6 +- src/evidently/ui/service/app.py | 2 +- src/evidently/ui/service/base.py | 9 +- src/evidently/ui/service/components/base.py | 9 +- .../ui/service/components/local_storage.py | 18 +- .../ui/service/components/security.py | 12 +- .../ui/service/components/snapshot_links.py | 3 +- .../ui/service/components/storage.py | 21 +- .../ui/service/components/tracing.py | 3 +- src/evidently/ui/service/config.py | 14 +- .../ui/service/datasets/data_source.py | 20 +- src/evidently/ui/service/datasets/filters.py | 50 +-- src/evidently/ui/service/datasets/metadata.py | 11 +- src/evidently/ui/service/datasets/models.py | 3 +- src/evidently/ui/service/managers/auth.py | 3 +- src/evidently/ui/service/managers/projects.py | 4 +- .../ui/service/storage/local/artifacts.py | 3 +- .../ui/service/storage/local/base.py | 12 +- .../service/storage/local/snapshot_links.py | 7 +- .../ui/service/storage/sql/components.py | 27 +- .../ui/service/storage/sql/dashboard.py | 4 +- .../ui/service/storage/sql/models.py | 14 +- src/evidently/ui/service/storage/utils.py | 7 +- src/evidently/ui/service/tracing/api.py | 2 +- .../ui/service/tracing/storage/base.py | 2 +- src/evidently/ui/service/utils.py | 6 +- src/evidently/ui/storage/local/base.py | 11 +- src/evidently/ui/workspace.py | 2 +- src/evidently/utils/schema.py | 28 +- .../calculation_engine/test_python_engine.py | 8 +- tests/calculations/test_data_quality.py | 4 +- tests/collector/conftest.py | 3 + tests/collector/test_app.py | 2 +- tests/collector/test_config.py | 2 +- tests/conftest.py | 14 +- tests/descriptors/test_serialization.py | 2 +- tests/features/test_multicolumn.py | 6 +- tests/future/descriptors/test_conditions.py | 14 +- tests/future/descriptors/test_descriptors.py | 2 +- .../descriptors/test_feature_descriptors.py | 6 +- tests/future/descriptors/test_text_match.py | 2 +- tests/future/metrics/test_test_fields.py | 12 +- tests/future/presets/test_serialization.py | 2 +- tests/future/presets/test_test_fields.py | 24 +- tests/future/test_data_definition.py | 2 +- tests/future/test_ui/conftest.py | 2 +- .../test_column_value_list_metric.py | 2 +- tests/metrics/recsys/test_precision_top_k.py | 2 +- tests/metrics/test_base_metric.py | 26 +- tests/multitest/conftest.py | 46 ++- tests/multitest/metrics/test_all.py | 2 +- tests/report/test_report.py | 9 +- tests/report/test_report_profile.py | 1 + tests/test_core.py | 27 +- tests/test_metric_results.py | 110 +++--- tests/test_pydantic_aliases.py | 9 +- tests/test_pydantic_compat.py | 40 -- tests/test_suite/test_test_suite.py | 4 +- tests/ui/conftest.py | 6 +- tests/ui/test_app.py | 9 +- tests/ui/test_dashboards.py | 25 +- tests/utils/test_pydantic_utils.py | 135 +++---- 293 files changed, 2396 insertions(+), 2665 deletions(-) delete mode 100644 src/evidently/_pydantic_compat.py delete mode 100644 tests/test_pydantic_compat.py diff --git a/src/evidently/_pydantic_compat.py b/src/evidently/_pydantic_compat.py deleted file mode 100644 index 03c77b4ae5..0000000000 --- a/src/evidently/_pydantic_compat.py +++ /dev/null @@ -1,94 +0,0 @@ -from typing import TYPE_CHECKING - -import pydantic - -v = 1 if pydantic.__version__.startswith("1") else 2 - -if v == 1: - from pydantic import BaseConfig # type: ignore[assignment] - from pydantic import BaseModel # type: ignore[assignment] - from pydantic import EmailStr # type: ignore[attr-defined,no-redef] - from pydantic import Extra # type: ignore[assignment] - from pydantic import Field # type: ignore[assignment] - from pydantic import PrivateAttr - from pydantic import SecretStr # type: ignore[assignment] - from pydantic import StrictBool # type: ignore[assignment] - from pydantic import ValidationError # type: ignore[assignment] - from pydantic import create_model # type: ignore[attr-defined,no-redef] - from pydantic import parse_obj_as - from pydantic import root_validator # type: ignore[attr-defined,no-redef] - from pydantic import validator - from pydantic.fields import SHAPE_DICT # type: ignore[attr-defined,no-redef] - from pydantic.fields import SHAPE_LIST # type: ignore[attr-defined,no-redef] - from pydantic.fields import SHAPE_SET # type: ignore[attr-defined,no-redef] - from pydantic.fields import SHAPE_SINGLETON # type: ignore[attr-defined,no-redef] - from pydantic.fields import SHAPE_TUPLE # type: ignore[attr-defined,no-redef] - from pydantic.fields import ModelField # type: ignore[attr-defined,no-redef] - from pydantic.main import ModelMetaclass # type: ignore[attr-defined,no-redef] - from pydantic.utils import import_string # type: ignore[attr-defined,no-redef] - from pydantic.validators import _VALIDATORS # type: ignore[attr-defined,no-redef] - - if TYPE_CHECKING: - from pydantic.main import AbstractSetIntStr # type: ignore[attr-defined,no-redef] - from pydantic.main import MappingIntStrAny # type: ignore[attr-defined,no-redef] - from pydantic.main import Model # type: ignore[attr-defined,no-redef] - from pydantic.typing import DictStrAny # type: ignore[attr-defined,no-redef] - -else: - from pydantic.v1 import BaseConfig # type: ignore[assignment,no-redef] - from pydantic.v1 import BaseModel # type: ignore[assignment,no-redef] - from pydantic.v1 import EmailStr # type: ignore[assignment,no-redef] - from pydantic.v1 import Extra # type: ignore[assignment,no-redef] - from pydantic.v1 import Field # type: ignore[assignment,no-redef] - from pydantic.v1 import PrivateAttr # type: ignore[assignment,no-redef] - from pydantic.v1 import SecretStr # type: ignore[assignment,no-redef] - from pydantic.v1 import StrictBool # type: ignore[assignment,no-redef] - from pydantic.v1 import ValidationError # type: ignore[assignment,no-redef] - from pydantic.v1 import create_model # type: ignore[assignment,no-redef] - from pydantic.v1 import parse_obj_as # type: ignore[assignment,no-redef] - from pydantic.v1 import root_validator # type: ignore[assignment,no-redef] - from pydantic.v1 import validator # type: ignore[assignment,no-redef] - from pydantic.v1.fields import SHAPE_DICT # type: ignore[assignment,no-redef] - from pydantic.v1.fields import SHAPE_LIST # type: ignore[assignment,no-redef] - from pydantic.v1.fields import SHAPE_SET # type: ignore[assignment,no-redef] - from pydantic.v1.fields import SHAPE_SINGLETON # type: ignore[assignment,no-redef] - from pydantic.v1.fields import SHAPE_TUPLE # type: ignore[assignment,no-redef] - from pydantic.v1.fields import ModelField # type: ignore[assignment,no-redef] - from pydantic.v1.main import ModelMetaclass # type: ignore[assignment,no-redef] - from pydantic.v1.utils import import_string # type: ignore[assignment,no-redef] - from pydantic.v1.validators import _VALIDATORS # type: ignore[assignment,no-redef] - - if TYPE_CHECKING: - from pydantic.v1.main import AbstractSetIntStr # type: ignore[assignment,no-redef] - from pydantic.v1.main import MappingIntStrAny # type: ignore[assignment,no-redef] - from pydantic.v1.main import Model # type: ignore[assignment,no-redef] - from pydantic.v1.typing import DictStrAny # type: ignore[assignment,no-redef] - -__all__ = [ - "BaseConfig", - "BaseModel", - "Field", - "ValidationError", - "parse_obj_as", - "validator", - "SecretStr", - "StrictBool", - "SHAPE_DICT", - "SHAPE_LIST", - "SHAPE_SET", - "SHAPE_SINGLETON", - "SHAPE_TUPLE", - "ModelField", - "ModelMetaclass", - "import_string", - "_VALIDATORS", - "Model", - "MappingIntStrAny", - "AbstractSetIntStr", - "DictStrAny", - "PrivateAttr", - "Extra", - "create_model", - "EmailStr", - "root_validator", -] diff --git a/src/evidently/cli/utils.py b/src/evidently/cli/utils.py index 22262e832d..129f4071bd 100644 --- a/src/evidently/cli/utils.py +++ b/src/evidently/cli/utils.py @@ -8,9 +8,10 @@ from typing import TypeVar from typing import Union +from pydantic import BaseModel +from pydantic import TypeAdapter + from evidently import Dataset -from evidently._pydantic_compat import BaseModel -from evidently._pydantic_compat import parse_obj_as from evidently.core.report import Snapshot from evidently.legacy.ui.type_aliases import DatasetID from evidently.legacy.ui.type_aliases import ProjectID @@ -92,7 +93,7 @@ class _Config(BaseModel): @classmethod def load(cls: Type[T], path: str) -> "T": with open(path) as f: - return parse_obj_as(cls, json.load(f)) + return TypeAdapter(cls).validate_python(json.load(f)) def save(self, path: str) -> None: with open(path, "w") as f: diff --git a/src/evidently/core/base_types.py b/src/evidently/core/base_types.py index 277e669479..29e5cbbbda 100644 --- a/src/evidently/core/base_types.py +++ b/src/evidently/core/base_types.py @@ -1,5 +1,21 @@ +import math +from typing import Annotated from typing import Union -from evidently._pydantic_compat import StrictBool +from pydantic import BeforeValidator +from pydantic import StrictBool -Label = Union[StrictBool, int, str, None] + +def label_validator(value): + if isinstance(value, bool): + return value + if isinstance(value, int): + return value + if isinstance(value, str): + return value + if isinstance(value, float) and math.isnan(value): + return "nan" + return value + + +Label = Annotated[Union[StrictBool, int, str, float, None], BeforeValidator(label_validator)] diff --git a/src/evidently/core/container.py b/src/evidently/core/container.py index 056dd221b1..dc43f540d2 100644 --- a/src/evidently/core/container.py +++ b/src/evidently/core/container.py @@ -32,8 +32,7 @@ class MetricContainer(AutoAliasMixin, EvidentlyBaseModel, abc.ABC): __alias_type__: ClassVar[str] = "metric_container" - class Config: - is_base_type = True + __is_base_type__: ClassVar[bool] = True include_tests: bool = True """Whether to include default tests for generated metrics.""" diff --git a/src/evidently/core/datasets.py b/src/evidently/core/datasets.py index 84d9dd9993..dcb4c50a0e 100644 --- a/src/evidently/core/datasets.py +++ b/src/evidently/core/datasets.py @@ -19,9 +19,9 @@ import numpy as np import pandas as pd +from pydantic import BaseModel +from pydantic import TypeAdapter -from evidently._pydantic_compat import BaseModel -from evidently._pydantic_compat import parse_obj_as from evidently.core.base_types import Label from evidently.core.tests import GenericTest from evidently.legacy.base_metric import DisplayName @@ -321,8 +321,7 @@ class SpecialColumnInfo(AutoAliasMixin, EvidentlyBaseModel): __alias_type__: ClassVar = "special_column_info" """Alias type for serialization.""" - class Config: - is_base_type = True + __is_base_type__: ClassVar[bool] = True def get_metrics(self) -> List["MetricOrContainer"]: """Get metrics associated with this special column. @@ -659,8 +658,7 @@ class ColumnCondition(AutoAliasMixin, EvidentlyBaseModel, abc.ABC): __alias_type__: ClassVar[str] = "column_condition" """Alias type for serialization.""" - class Config: - is_base_type = True + __is_base_type__: ClassVar[bool] = True @abstractmethod def check(self, value: Any) -> bool: @@ -738,8 +736,7 @@ class Descriptor(AutoAliasMixin, EvidentlyBaseModel, abc.ABC): computed features for evaluation. """ - class Config: - is_base_type = True + __is_base_type__: ClassVar[bool] = True __alias_type__: ClassVar = "descriptor_v2" """Alias type for serialization.""" @@ -822,7 +819,7 @@ def __init__( ) -> None: self.column = column if isinstance(condition, dict): - condition = parse_obj_as(ColumnCondition, condition) # type: ignore[type-abstract] + condition = TypeAdapter(ColumnCondition).validate_python(condition) # type: ignore[type-abstract] descriptor_condition: ColumnCondition = ( condition if isinstance(condition, ColumnCondition) else condition.for_descriptor().condition ) @@ -1082,7 +1079,11 @@ def __init__( self, feature: GeneratedFeatures, alias: Optional[str] = None, tests: Optional[List[AnyDescriptorTest]] = None ): # this is needed because we try to access it before super call - feature = feature if isinstance(feature, GeneratedFeatures) else parse_obj_as(GeneratedFeatures, feature) # type: ignore[type-abstract] + feature = ( + feature + if isinstance(feature, GeneratedFeatures) + else TypeAdapter(GeneratedFeatures).validate_python(feature) + ) # type: ignore[type-abstract] feature_columns = feature.list_columns() super().__init__(feature=feature, alias=alias or f"{feature_columns[0].display_name}", tests=tests) diff --git a/src/evidently/core/metric_types.py b/src/evidently/core/metric_types.py index 62b2b3a90e..a700a7a43a 100644 --- a/src/evidently/core/metric_types.py +++ b/src/evidently/core/metric_types.py @@ -26,11 +26,11 @@ import numpy as np import pandas as pd import typing_inspect +from pydantic import BaseModel +from pydantic import Field +from pydantic import TypeAdapter +from pydantic import field_validator -from evidently._pydantic_compat import BaseModel -from evidently._pydantic_compat import Field -from evidently._pydantic_compat import parse_obj_as -from evidently._pydantic_compat import validator from evidently.core.base_types import Label from evidently.legacy.model.dashboard import DashboardInfo from evidently.legacy.model.widget import AdditionalGraphInfo @@ -170,8 +170,7 @@ class MetricResult(AutoAliasMixin, PolymorphicModel): return different result subclasses (e.g., `SingleValue`, `ByLabelValue`). """ - class Config: - is_base_type = True + __is_base_type__: ClassVar[bool] = True __alias_type__: ClassVar[str] = "metric_result_v2" @@ -436,9 +435,6 @@ class ByLabelValue(MetricResult): (e.g., precision per label, recall per label). """ - class Config: - smart_union = True - values: Dict[Label, SingleValue] """Dictionary mapping label to its corresponding metric value.""" @@ -472,15 +468,12 @@ def set_metric_location(self, metric: MetricConfig): def to_simple_dict(self) -> object: return {k: v.value for k, v in self.values.items()} - @validator("values", pre=True) + @field_validator("values", mode="before") def convert_labels(cls, value): return {convert_types(k): v for k, v in value.items()} class ByLabelCountValue(MetricResult): - class Config: - smart_union = True - counts: Dict[Label, SingleValue] shares: Dict[Label, SingleValue] count_display_name_template: str = "Missing label {label} count" @@ -531,7 +524,7 @@ def set_metric_location(self, metric: MetricConfig): for k, v in self.shares.items(): v.metric_value_location = by_label_count_value_location(metric, k, False) - @validator("counts", "shares", pre=True) + @field_validator("counts", "shares", mode="before") def convert_labels(cls, value): return {convert_types(k): v for k, v in value.items()} @@ -1012,8 +1005,7 @@ class MetricTest(AutoAliasMixin, EvidentlyBaseModel): bound to specific metric values and run automatically when metrics are calculated. """ - class Config: - is_base_type = True + __is_base_type__: ClassVar[bool] = True __alias_type__: ClassVar[str] = "test_v2" is_critical: bool = True @@ -1084,8 +1076,7 @@ class BoundTest(AutoAliasMixin, EvidentlyBaseModel, Generic[TResult], ABC): extract the relevant value from the metric result to run the test. """ - class Config: - is_base_type = True + __is_base_type__: ClassVar[bool] = True __alias_type__: ClassVar[str] = "bound_test" test: MetricTest @@ -1121,9 +1112,7 @@ class Metric(AutoAliasMixin, EvidentlyBaseModel, Generic[TCalculation]): __alias_type__: ClassVar[str] = "metric_v2" - class Config: - is_base_type = True - smart_union = True + __is_base_type__: ClassVar[bool] = True __calculation_type__: ClassVar[Type] @@ -1274,7 +1263,7 @@ def convert_test(test: Union[MetricTest, GenericTest]) -> MetricTest: if isinstance(test, MetricTest): return test if isinstance(test, dict): - return parse_obj_as(MetricTest, test) + return TypeAdapter(MetricTest).validate_python(test) raise ValueError(f"test {test} is not a subclass of MetricTest") @@ -1302,7 +1291,7 @@ def get_bound_tests(self, context: "Context") -> List[BoundTest]: fingerprint = self.get_fingerprint() return [t.bind_single(fingerprint) for t in (self.tests or [])] - @validator("tests", pre=True) + @field_validator("tests", mode="before") def validate_tests(cls, v): return convert_tests(v) @@ -1376,7 +1365,8 @@ def get_bound_tests(self, context: "Context") -> List[BoundTest]: fingerprint = self.get_fingerprint() return [t.bind_by_label(fingerprint, label=label) for label, tests in (self.tests or {}).items() for t in tests] - @validator("tests", pre=True) + @field_validator("tests", mode="before") + @classmethod def validate_tests(cls, val): return {k: convert_tests(v) for k, v in val.items()} if val is not None else None @@ -1514,7 +1504,8 @@ def get_bound_tests(self, context: "Context") -> List[BoundTest]: for t in tests ] - @validator("tests", "share_tests", pre=True) + @field_validator("tests", "share_tests", mode="before") + @classmethod def validate_tests(cls, v): return convert_tests(v) @@ -1597,11 +1588,13 @@ def get_bound_tests(self, context: "Context") -> Sequence[BoundTest]: t.bind_count(fingerprint, False) for t in (self.share_tests or []) ] - @validator("tests", pre=True) + @field_validator("tests", mode="before") + @classmethod def validate_tests(cls, v): return convert_tests(v) - @validator("share_tests", pre=True) + @field_validator("share_tests", mode="before") + @classmethod def validate_share_tests(cls, v): return convert_tests(v) @@ -1753,7 +1746,7 @@ def convert_to_mean_tests(tests: MeanStdMetricsPossibleTests) -> Optional[MeanSt if isinstance(tests, list): return MeanStdMetricTests(mean=tests) if isinstance(tests, dict): - return parse_obj_as(MeanStdMetricTests, tests) + return TypeAdapter(MeanStdMetricTests).validate_python(tests) raise ValueError(tests) @@ -1777,11 +1770,13 @@ def get_bound_tests(self, context: "Context") -> Sequence[BoundTest]: t.bind_mean_std(fingerprint, False) for t in (self.std_tests or []) ] - @validator("mean_tests", pre=True) + @field_validator("mean_tests", mode="before") + @classmethod def validate_mean_tests(cls, v): return convert_tests(v) - @validator("std_tests", pre=True) + @field_validator("std_tests", mode="before") + @classmethod def validate_std_tests(cls, v): return convert_tests(v) @@ -1807,7 +1802,8 @@ def get_bound_tests(self, context: "Context") -> Sequence[BoundTest]: bound_tests.append(test.bind_dataframe(fingerprint, column=column)) return bound_tests - @validator("tests", pre=True) + @field_validator("tests", mode="before") + @classmethod def validate_tests(cls, v): if v is None: return None @@ -1898,4 +1894,4 @@ class ColumnMetric(Metric, ABC): """Name of the column to compute the metric for.""" -MetricTestResult.update_forward_refs() +MetricTestResult.model_rebuild() diff --git a/src/evidently/core/serialization.py b/src/evidently/core/serialization.py index f40d043b1e..5cac67c94a 100644 --- a/src/evidently/core/serialization.py +++ b/src/evidently/core/serialization.py @@ -4,7 +4,8 @@ from typing import Optional from typing import Union -from evidently._pydantic_compat import BaseModel +from pydantic import BaseModel + from evidently.core.metric_types import MetricId from evidently.core.metric_types import MetricResult from evidently.legacy.model.widget import BaseWidgetInfo diff --git a/src/evidently/core/tests.py b/src/evidently/core/tests.py index 81450ac3b6..de5018504e 100644 --- a/src/evidently/core/tests.py +++ b/src/evidently/core/tests.py @@ -5,8 +5,9 @@ from typing import Union from typing import get_args -from evidently._pydantic_compat import BaseModel -from evidently._pydantic_compat import Field +from pydantic import BaseModel +from pydantic import Field + from evidently.core.base_types import Label from evidently.legacy.utils.types import ApproxValue from evidently.pydantic_utils import Fingerprint diff --git a/src/evidently/descriptors/_custom_descriptors.py b/src/evidently/descriptors/_custom_descriptors.py index 9b4a4a0e84..ea6df23aa9 100644 --- a/src/evidently/descriptors/_custom_descriptors.py +++ b/src/evidently/descriptors/_custom_descriptors.py @@ -4,7 +4,8 @@ from typing import Optional from typing import Union -from evidently._pydantic_compat import PrivateAttr +from pydantic import PrivateAttr + from evidently.core.datasets import AnyDescriptorTest from evidently.core.datasets import Dataset from evidently.core.datasets import DatasetColumn @@ -33,12 +34,13 @@ def __init__( ): self.column_name = column_name if callable(func): - self._func = func + _func = func func = f"{func.__module__}.{func.__name__}" else: - self._func = None + _func = None self.func = func super().__init__(alias=alias or f"custom_column_descriptor:{func}", tests=tests) + self._func = _func def generate_data(self, dataset: Dataset, options: Options) -> Union[DatasetColumn, Dict[str, DatasetColumn]]: """Apply custom function to column data.""" @@ -70,12 +72,13 @@ def __init__( tests: Optional[List[AnyDescriptorTest]] = None, ): if callable(func): - self._func = func + _func = func func = f"{func.__module__}.{func.__name__}" else: - self._func = None + _func = None self.func = func super().__init__(alias=alias or f"custom_descriptor:{func}", tests=tests) + self._func = _func def generate_data(self, dataset: "Dataset", options: Options) -> Union[DatasetColumn, Dict[str, DatasetColumn]]: """Apply custom function to dataset.""" diff --git a/src/evidently/descriptors/_generate_descriptors.py b/src/evidently/descriptors/_generate_descriptors.py index 435974ee20..d00df87ead 100644 --- a/src/evidently/descriptors/_generate_descriptors.py +++ b/src/evidently/descriptors/_generate_descriptors.py @@ -9,7 +9,8 @@ from typing import Type from typing import Union -from evidently._pydantic_compat import import_string +from pydantic._internal._validators import import_string + from evidently.core.datasets import Descriptor from evidently.core.datasets import DescriptorTest from evidently.core.datasets import FeatureDescriptor @@ -61,12 +62,14 @@ def get_args_kwargs(feature_class: Type[GeneratedFeatures]) -> Tuple[Dict[str, s if feature_class.__dict__.get("__init__") is None: # get from fields args = { - key: _get_type_name(field.annotation) for key, field in feature_class.__fields__.items() if field.required + key: _get_type_name(field.annotation) + for key, field in feature_class.model_fields.items() + if field.is_required() } kwargs = { key: (_get_type_name(field.annotation), _get_value_str(field.default)) - for key, field in feature_class.__fields__.items() - if not field.required and key != "type" + for key, field in feature_class.model_fields.items() + if not field.is_required() and key != "type" } return args, kwargs # get from constructor diff --git a/src/evidently/descriptors/llm_judges.py b/src/evidently/descriptors/llm_judges.py index d1e8ac7eeb..310c223972 100644 --- a/src/evidently/descriptors/llm_judges.py +++ b/src/evidently/descriptors/llm_judges.py @@ -7,10 +7,10 @@ from typing import cast import pandas as pd +from pydantic import PrivateAttr from evidently import ColumnType from evidently import Dataset -from evidently._pydantic_compat import PrivateAttr from evidently.core.datasets import AnyDescriptorTest from evidently.core.datasets import DatasetColumn from evidently.core.datasets import Descriptor diff --git a/src/evidently/generators/column.py b/src/evidently/generators/column.py index 977f210a06..7d089ca810 100644 --- a/src/evidently/generators/column.py +++ b/src/evidently/generators/column.py @@ -9,8 +9,9 @@ from typing import Type from typing import Union -from evidently._pydantic_compat import PrivateAttr -from evidently._pydantic_compat import ValidationError +from pydantic import PrivateAttr +from pydantic import ValidationError + from evidently.core.container import ColumnMetricContainer from evidently.core.container import MetricContainer from evidently.core.container import MetricOrContainer @@ -79,13 +80,13 @@ def __init__( ), "metric_type_alias must be an alias of ColumnMetric or ColumnMetricContainer subclass" assert metric_type_alias is not None, "metric_type_alias or metric_type must be specified" self.metric_type_alias = metric_type_alias - self._metric_type = _metric_type self.columns = columns self.column_types = column_types if metric_kwargs and kwargs: raise ValueError("only one of metric_kwargs or **kwargs may be specified") self.metric_kwargs = metric_kwargs or kwargs or {} super().__init__(include_tests=include_tests) + self._metric_type = _metric_type def _instantiate_metric(self, column: str) -> MetricOrContainer: """Create a metric instance for a specific column. diff --git a/src/evidently/legacy/base_metric.py b/src/evidently/legacy/base_metric.py index 5da2ab1bea..a5b821becf 100644 --- a/src/evidently/legacy/base_metric.py +++ b/src/evidently/legacy/base_metric.py @@ -16,11 +16,11 @@ from typing import Union import pandas as pd -import typing_inspect +from pydantic import ConfigDict +from pydantic import Field +from pydantic import PrivateAttr +from pydantic._internal._model_construction import ModelMetaclass -from evidently._pydantic_compat import Field -from evidently._pydantic_compat import ModelMetaclass -from evidently._pydantic_compat import PrivateAttr from evidently.legacy.core import BaseResult from evidently.legacy.core import ColumnType from evidently.legacy.core import IncludeTags @@ -49,17 +49,15 @@ def fields(cls) -> FieldPath: return FieldPath([], cls) -class MetricResult(PolymorphicModel, BaseResult, metaclass=WithFieldsPathMetaclass): # type: ignore[misc] # pydantic Config - class Config: - type_alias = "evidently:metric_result:MetricResult" - field_tags = {"type": {IncludeTags.TypeField}} - is_base_type = True - alias_required = True +class MetricResult(PolymorphicModel, BaseResult, metaclass=WithFieldsPathMetaclass): # type: ignore[misc] + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:MetricResult" + __is_base_type__: ClassVar[bool] = True + __alias_required__: ClassVar[bool] = True + __field_tags__: ClassVar[Dict[str, set]] = {"type": {IncludeTags.TypeField}} class ErrorResult(BaseResult): - class Config: - underscore_attrs_are_private = True + model_config = ConfigDict() _exception: Optional[BaseException] = None # todo: fix serialization of exceptions @@ -82,8 +80,7 @@ class DatasetType(Enum): @autoregister class ColumnName(EnumValueMixin, EvidentlyBaseModel): - class Config: - type_alias = "evidently:base:ColumnName" + __type_alias__: ClassVar[Optional[str]] = "evidently:base:ColumnName" name: str display_name: DisplayName @@ -93,8 +90,8 @@ class Config: def __init__( self, name: str, display_name: str, dataset: DatasetType, feature_class: Optional["GeneratedFeatures"] = None ): - self._feature_class = feature_class super().__init__(name=name, display_name=display_name, dataset=dataset) + self._feature_class = feature_class def is_main_dataset(self): return self.dataset == DatasetType.MAIN @@ -117,8 +114,8 @@ def feature_class(self) -> Optional["GeneratedFeatures"]: def get_fingerprint_parts(self) -> Tuple[FingerprintPart, ...]: return tuple( (name, self.get_field_fingerprint(name)) - for name, field in sorted(self.__fields__.items()) - if field.required or getattr(self, name) != field.get_default() and field.name != "display_name" + for name, field in sorted(self.model_fields.items()) + if field.is_required() or getattr(self, name) != field.default and name != "display_name" ) @@ -226,21 +223,28 @@ def __get__(self, instance: Optional["Metric"], type: Type["Metric"]) -> FieldPa class WithResultFieldPathMetaclass(FrozenBaseMeta): def result_type(cls) -> Type[MetricResult]: - return typing_inspect.get_args( - next(b for b in cls.__orig_bases__ if typing_inspect.is_generic_type(b)) # type: ignore[attr-defined] - )[0] + for parent_cls in cls.mro(): + generic_metadata = getattr(parent_cls, "__pydantic_generic_metadata__", None) + if generic_metadata is None: + continue + origin = generic_metadata["origin"] + if not isinstance(origin, type) or not issubclass(origin, Metric): + continue + for arg in generic_metadata["args"]: + if not isinstance(arg, type) or not issubclass(arg, MetricResult): + continue + return arg + raise TypeError(f"No result type found for {cls.__module__}.{cls.__name__}") class BasePreset(EvidentlyBaseModel): - class Config: - type_alias = "evidently:base:BasePreset" - transitive_aliases = True - is_base_type = True + __type_alias__: ClassVar[Optional[str]] = "evidently:base:BasePreset" + __transitive_aliases__: ClassVar[bool] = True + __is_base_type__: ClassVar[bool] = True class Metric(WithTestAndMetricDependencies, Generic[TResult], metaclass=WithResultFieldPathMetaclass): - class Config: - is_base_type = True + __is_base_type__: ClassVar[bool] = True _context: Optional["Context"] = None @@ -330,12 +334,11 @@ def get_options_fingerprint(self) -> FingerprintPart: class ColumnMetricResult(MetricResult): - class Config: - type_alias = "evidently:metric_result:ColumnMetricResult" - field_tags = { - "column_name": {IncludeTags.Parameter}, - "column_type": {IncludeTags.Parameter}, - } + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:ColumnMetricResult" + __field_tags__: ClassVar[Dict[str, set]] = { + "column_name": {IncludeTags.Parameter}, + "column_type": {IncludeTags.Parameter}, + } column_name: str # todo: use enum diff --git a/src/evidently/legacy/calculations/data_drift.py b/src/evidently/legacy/calculations/data_drift.py index 0b814a624e..5f25585860 100644 --- a/src/evidently/legacy/calculations/data_drift.py +++ b/src/evidently/legacy/calculations/data_drift.py @@ -1,6 +1,7 @@ """Methods and types for data drift calculations.""" from dataclasses import dataclass +from typing import ClassVar from typing import Dict from typing import List from typing import Optional @@ -34,36 +35,32 @@ class DriftStatsField(MetricResult): - class Config: - type_alias = "evidently:metric_result:DriftStatsField" - dict_exclude_fields = {"characteristic_examples", "characteristic_words", "correlations"} - # todo: after tests PR - field_tags = { - "characteristic_examples": {IncludeTags.Render}, - "characteristic_words": {IncludeTags.Render}, - "correlations": {IncludeTags.Render}, - "small_distribution": {IncludeTags.Extra}, - } - pd_include = False - - distribution: Optional[Distribution] - characteristic_examples: Optional[Examples] - characteristic_words: Optional[Words] - small_distribution: Optional[DistributionIncluded] - correlations: Optional[Dict[str, float]] + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:DriftStatsField" + __dict_exclude_fields__: ClassVar[set] = {"characteristic_examples", "characteristic_words", "correlations"} + __field_tags__: ClassVar[Dict[str, set]] = { + "characteristic_examples": {IncludeTags.Render}, + "characteristic_words": {IncludeTags.Render}, + "correlations": {IncludeTags.Render}, + "small_distribution": {IncludeTags.Extra}, + } + __pd_include__: ClassVar[bool] = False + + distribution: Optional[Distribution] = None + characteristic_examples: Optional[Examples] = None + characteristic_words: Optional[Words] = None + small_distribution: Optional[DistributionIncluded] = None + correlations: Optional[Dict[str, float]] = None class ColumnDataDriftMetrics(ColumnMetricResult): - class Config: - # todo: change to field_tags: render - type_alias = "evidently:metric_result:ColumnDataDriftMetrics" - dict_exclude_fields = {"scatter"} - pd_exclude_fields = {"scatter"} - field_tags = { - "stattest_name": {IncludeTags.Parameter}, - "current": {IncludeTags.Current}, - "reference": {IncludeTags.Reference}, - } + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:ColumnDataDriftMetrics" + __dict_exclude_fields__: ClassVar[set] = {"scatter"} + __pd_exclude_fields__: ClassVar[set] = {"scatter"} + __field_tags__: ClassVar[Dict[str, set]] = { + "stattest_name": {IncludeTags.Parameter}, + "current": {IncludeTags.Current}, + "reference": {IncludeTags.Reference}, + } stattest_name: str stattest_threshold: Optional[float] @@ -73,7 +70,7 @@ class Config: current: DriftStatsField reference: DriftStatsField - scatter: Optional[Union[ScatterField, ScatterAggField]] + scatter: Optional[Union[ScatterField, ScatterAggField]] = None scatter_raw, scatter_agg = raw_agg_properties("scatter", ScatterField, ScatterAggField, True) @@ -87,8 +84,7 @@ class DatasetDrift: class DatasetDriftMetrics(MetricResult): - class Config: - type_alias = "evidently:metric_result:DatasetDriftMetrics" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:DatasetDriftMetrics" number_of_columns: int number_of_drifted_columns: int diff --git a/src/evidently/legacy/calculations/data_quality.py b/src/evidently/legacy/calculations/data_quality.py index 6dc67e7a81..10e1500210 100644 --- a/src/evidently/legacy/calculations/data_quality.py +++ b/src/evidently/legacy/calculations/data_quality.py @@ -17,7 +17,6 @@ from evidently.legacy.core import ColumnType from evidently.legacy.metric_results import ColumnCorrelations from evidently.legacy.metric_results import DatasetColumns -from evidently.legacy.metric_results import Distribution from evidently.legacy.metric_results import DistributionIncluded from evidently.legacy.utils.data_preprocessing import DataDefinition from evidently.legacy.utils.types import ColumnDistribution @@ -421,7 +420,7 @@ def calculate_cramer_v_correlation(column_name: str, dataset: pd.DataFrame, colu return ColumnCorrelations( column_name=column_name, kind="cramer_v", - values=Distribution(x=result_x, y=result_y), + values=DistributionIncluded(x=result_x, y=result_y), ) diff --git a/src/evidently/legacy/collector/app.py b/src/evidently/legacy/collector/app.py index 70a834267e..05dc5b2e80 100644 --- a/src/evidently/legacy/collector/app.py +++ b/src/evidently/legacy/collector/app.py @@ -28,9 +28,9 @@ from litestar.types import Receive from litestar.types import Scope from litestar.types import Send +from pydantic import SecretStr from typing_extensions import Annotated -from evidently._pydantic_compat import SecretStr from evidently.legacy.collector.config import CONFIG_PATH from evidently.legacy.collector.config import CollectorConfig from evidently.legacy.collector.config import CollectorServiceConfig diff --git a/src/evidently/legacy/collector/config.py b/src/evidently/legacy/collector/config.py index 8e7e461288..ec3149562d 100644 --- a/src/evidently/legacy/collector/config.py +++ b/src/evidently/legacy/collector/config.py @@ -3,16 +3,17 @@ import time import warnings from typing import Any +from typing import ClassVar from typing import Dict from typing import List from typing import Optional from typing import Union import pandas as pd +from pydantic import BaseModel +from pydantic import Field +from pydantic import TypeAdapter -from evidently._pydantic_compat import BaseModel -from evidently._pydantic_compat import Field -from evidently._pydantic_compat import parse_obj_as from evidently.legacy.base_metric import Metric from evidently.legacy.collector.storage import CollectorStorage from evidently.legacy.collector.storage import InMemoryStorage @@ -35,7 +36,7 @@ class Config(BaseModel): @classmethod def load(cls, path: str): with open(path) as f: - return parse_obj_as(cls, json.load(f)) + return TypeAdapter(cls).validate_python(json.load(f)) def save(self, path: str): with open(path, "w") as f: @@ -43,8 +44,7 @@ def save(self, path: str): class CollectorTrigger(PolymorphicModel): - class Config: - is_base_type = True + __is_base_type__: ClassVar[bool] = True @abc.abstractmethod def is_ready(self, config: "CollectorConfig", storage: "CollectorStorage") -> bool: @@ -53,8 +53,7 @@ def is_ready(self, config: "CollectorConfig", storage: "CollectorStorage") -> bo @autoregister class IntervalTrigger(CollectorTrigger): - class Config: - type_alias = "evidently:collector_trigger:IntervalTrigger" + __type_alias__: ClassVar[Optional[str]] = "evidently:collector_trigger:IntervalTrigger" interval: float = Field(gt=0) last_triggered: float = 0 @@ -69,8 +68,7 @@ def is_ready(self, config: "CollectorConfig", storage: "CollectorStorage") -> bo @autoregister class RowsCountTrigger(CollectorTrigger): - class Config: - type_alias = "evidently:collector_trigger:RowsCountTrigger" + __type_alias__: ClassVar[Optional[str]] = "evidently:collector_trigger:RowsCountTrigger" rows_count: int = Field(default=1, gt=0) @@ -81,8 +79,7 @@ def is_ready(self, config: "CollectorConfig", storage: "CollectorStorage") -> bo @autoregister class RowsCountOrIntervalTrigger(CollectorTrigger): - class Config: - type_alias = "evidently:collector_trigger:RowsCountOrIntervalTrigger" + __type_alias__: ClassVar[Optional[str]] = "evidently:collector_trigger:RowsCountOrIntervalTrigger" rows_count_trigger: RowsCountTrigger interval_trigger: IntervalTrigger @@ -135,13 +132,10 @@ def to_report_base(self) -> Union[TestSuite, Report]: class CollectorConfig(Config): - class Config: - underscore_attrs_are_private = True - id: str = "" trigger: CollectorTrigger report_config: ReportConfig - reference_path: Optional[str] + reference_path: Optional[str] = None project_id: str api_url: str = "http://localhost:8000" diff --git a/src/evidently/legacy/collector/storage.py b/src/evidently/legacy/collector/storage.py index 7c0bc9f2ee..93d259f182 100644 --- a/src/evidently/legacy/collector/storage.py +++ b/src/evidently/legacy/collector/storage.py @@ -1,13 +1,15 @@ import abc from asyncio import Lock from typing import Any +from typing import ClassVar from typing import Dict from typing import List +from typing import Optional from typing import Sequence import pandas as pd +from pydantic import BaseModel -from evidently._pydantic_compat import BaseModel from evidently.legacy.suite.base_suite import ReportBase from evidently.pydantic_utils import PolymorphicModel from evidently.pydantic_utils import autoregister @@ -42,9 +44,7 @@ def __exit__(self, exc_type, exc_val, exc_tb): class CollectorStorage(PolymorphicModel): - class Config: - underscore_attrs_are_private = True - is_base_type = True + __is_base_type__: ClassVar[bool] = True _locks: Dict[str, Lock] = {} @@ -89,8 +89,7 @@ def take_reports(self, id: str) -> Sequence[ReportPopper]: @autoregister class InMemoryStorage(CollectorStorage): - class Config: - type_alias = "evidently:collector_storage:InMemoryStorage" + __type_alias__: ClassVar[Optional[str]] = "evidently:collector_storage:InMemoryStorage" max_log_events: int = 10 diff --git a/src/evidently/legacy/core.py b/src/evidently/legacy/core.py index 6121f2a199..f6779e0871 100644 --- a/src/evidently/legacy/core.py +++ b/src/evidently/legacy/core.py @@ -1,5 +1,7 @@ import uuid +from enum import Enum from typing import TYPE_CHECKING +from typing import Annotated from typing import Any from typing import ClassVar from typing import Dict @@ -10,33 +12,41 @@ from typing import TypeVar from typing import Union from typing import get_args +from typing import get_origin import numpy as np import pandas as pd import uuid6 +from pydantic import BaseModel +from pydantic import BeforeValidator +from pydantic import ConfigDict +from pydantic.fields import FieldInfo from typing_inspect import is_literal_type -from evidently._pydantic_compat import SHAPE_DICT -from evidently._pydantic_compat import SHAPE_LIST -from evidently._pydantic_compat import SHAPE_SET -from evidently._pydantic_compat import SHAPE_TUPLE -from evidently._pydantic_compat import ModelField from evidently.pydantic_utils import IncludeTags +from evidently.pydantic_utils import get_field_inner_type from evidently.pydantic_utils import pydantic_type_validator if TYPE_CHECKING: - from evidently._pydantic_compat import AbstractSetIntStr - from evidently._pydantic_compat import MappingIntStrAny - -from enum import Enum + from typing import AbstractSet + from typing import Mapping as MappingIntStrAny -from evidently._pydantic_compat import BaseConfig -from evidently._pydantic_compat import BaseModel + AbstractSetIntStr = AbstractSet[Union[int, str]] IncludeOptions = Union["AbstractSetIntStr", "MappingIntStrAny"] -Label = Union[int, str, None] +Label = Union[int, str, float, None] + + +def try_int_validator(value): + try: + return int(value) + except (TypeError, ValueError): + return value + + +LabelIntStr = Annotated[Label, BeforeValidator(try_int_validator)] class ColumnType(Enum): @@ -64,31 +74,20 @@ class ColumnType(Enum): """Columns containing lists or arrays.""" -def _is_mapping_field(field: ModelField): - return field.shape in (SHAPE_DICT,) - - -def _is_sequence_field(field: ModelField): - return field.shape in (SHAPE_LIST, SHAPE_SET, SHAPE_TUPLE) - +def _strip_union(type_): + if get_origin(type_) is Union: + args = [a for a in get_args(type_) if a is not type(None)] + if len(args) == 1: + return args[0] + return type_ -# workaround for https://github.com/pydantic/pydantic/issues/5301 -class AllDict(dict): - def __init__(self, value): - super().__init__() - self._value = value - def __contains__(self, item): - return True +def _is_mapping_field(field: FieldInfo): + return get_origin(_strip_union(field.annotation)) is dict - def get(self, __key): - return self._value - def __repr__(self): - return f"{{'__all__':{self._value}}}" - - def __bool__(self): - return True +def _is_sequence_field(field: FieldInfo): + return get_origin(_strip_union(field.annotation)) in (list, set, tuple) @pydantic_type_validator(pd.Series) @@ -106,34 +105,44 @@ def dataframe_validator(value): # return pd.Index(value) -@pydantic_type_validator(np.double) -def np_inf_valudator(value): - return np.float(value) +# @pydantic_type_validator(np.double) +# def np_inf_valudator(value): +# return np.float(value) + + +def np_inf_validator(value): + return float(value) if value is not None else None + + +PydanticNPDouble = Annotated[np.double, BeforeValidator(np_inf_validator)] +# @pydantic_type_validator(np.ndarray) +# def np_array_valudator(value): +# return np.array(value) -@pydantic_type_validator(np.ndarray) -def np_array_valudator(value): + +def np_array_validator(value): return np.array(value) +PydanticNPArray = Annotated[np.ndarray, BeforeValidator(np_array_validator)] + + class BaseResult(BaseModel): - class Config(BaseConfig): - arbitrary_types_allowed = True - dict_include: bool = True - pd_include: bool = True - pd_name_mapping: Dict[str, str] = {} + model_config = ConfigDict(arbitrary_types_allowed=True) - dict_include_fields: set = set() - dict_exclude_fields: set = set() - pd_include_fields: set = set() - pd_exclude_fields: set = set() + __dict_include__: ClassVar[bool] = True + __pd_include__: ClassVar[bool] = True + __pd_name_mapping__: ClassVar[Optional[Dict[str, str]]] = None - tags: Set[IncludeTags] = set() - field_tags: Dict[str, set] = {} - extract_as_obj: bool = False + __dict_include_fields__: ClassVar[Optional[set]] = None + __dict_exclude_fields__: ClassVar[Optional[set]] = None + __pd_include_fields__: ClassVar[Optional[set]] = None + __pd_exclude_fields__: ClassVar[Optional[set]] = None - if TYPE_CHECKING: - __config__: ClassVar[Type[Config]] = Config + __tags__: ClassVar[Optional[Set[IncludeTags]]] = None + __field_tags__: ClassVar[Optional[Dict[str, set]]] = None + __extract_as_obj__: ClassVar[bool] = False def get_dict( self, @@ -144,31 +153,31 @@ def get_dict( exclude_tags = {IncludeTags.TypeField} if not include_render: exclude_tags.add(IncludeTags.Render) - return self.dict(include=include or self._build_include(exclude_tags=exclude_tags), exclude=exclude) + return self.model_dump(include=include or self._build_include(exclude_tags=exclude_tags), exclude=exclude) def _build_include( self, exclude_tags: Set[IncludeTags], include=None, ) -> "MappingIntStrAny": - if not self.__config__.dict_include and not include or any(t in exclude_tags for t in self.__config__.tags): + cls = type(self) + if not cls.__dict_include__ and not include or any(t in exclude_tags for t in (cls.__tags__ or set())): return {} include = include or {} - dict_include_fields = ( - set(() if isinstance(include, bool) else include) - or self.__config__.dict_include_fields - or set(self.__fields__.keys()) + dict_include_fields = set(() if isinstance(include, bool) else include) or ( + cls.__dict_include_fields__ or set(self.model_fields.keys()) ) - dict_exclude_fields = self.__config__.dict_exclude_fields or set() + dict_exclude_fields = cls.__dict_exclude_fields__ or set() field_tags = get_all_fields_tags(self.__class__) result: Dict[str, Any] = {} - for name, field in self.__fields__.items(): + for name, field in self.model_fields.items(): if field_tags.get(name) and any(tag in exclude_tags for tag in field_tags.get(name, set())): continue - if isinstance(field.type_, type) and issubclass(field.type_, BaseResult): + inner_type = get_field_inner_type(field) + if isinstance(inner_type, type) and issubclass(inner_type, BaseResult): if ( - (not field.type_.__config__.dict_include or name in dict_exclude_fields) - and not field.field_info.include + (not inner_type.__dict_include__ or name in dict_exclude_fields) + and not getattr(field, "include", None) and name not in include ): continue @@ -178,22 +187,16 @@ def _build_include( build_include = {} elif _is_mapping_field(field): build_include = { - k: v._build_include( - exclude_tags=exclude_tags, include=field.field_info.include or include.get(name, {}) - ) + k: v._build_include(exclude_tags=exclude_tags, include=include.get(name, {})) for k, v in field_value.items() } elif _is_sequence_field(field): build_include = { - i: v._build_include( - exclude_tags=exclude_tags, include=field.field_info.include or include.get(name, {}) - ) + i: v._build_include(exclude_tags=exclude_tags, include=include.get(name, {})) for i, v in enumerate(field_value) } else: - build_include = field_value._build_include( - exclude_tags=exclude_tags, include=field.field_info.include or include.get(name, {}) - ) + build_include = field_value._build_include(exclude_tags=exclude_tags, include=include.get(name, {})) result[name] = build_include continue if name in dict_exclude_fields and name not in include: @@ -207,12 +210,13 @@ def __init_subclass__(cls, **kwargs): cls.__include_fields__ = None def collect_pandas_columns(self, prefix="", include: Set[str] = None, exclude: Set[str] = None) -> Dict[str, Any]: - include = include or self.__config__.pd_include_fields or set(self.__fields__) - exclude = exclude or self.__config__.pd_exclude_fields or set() + cls = type(self) + include = include or cls.__pd_include_fields__ or set(self.model_fields) + exclude = exclude or cls.__pd_exclude_fields__ or set() data = {} - field_tags = self.__config__.field_tags - for name, field in self.__fields__.items(): + field_tags = cls.__field_tags__ or {} + for name, field in self.model_fields.items(): if field_tags.get(name) and any( ft in {IncludeTags.TypeField, IncludeTags.Render} for ft in field_tags.get(name, set()) ): @@ -220,10 +224,10 @@ def collect_pandas_columns(self, prefix="", include: Set[str] = None, exclude: S if name not in include or name in exclude: continue field_value = getattr(self, name) - field_prefix = f"{prefix}{self.__config__.pd_name_mapping.get(name, name)}_" + field_prefix = f"{prefix}{(cls.__pd_name_mapping__ or {}).get(name, name)}_" if isinstance(field_value, BaseResult): field_type = type(field_value) - if field_type.__config__.pd_include: + if field_type.__pd_include__: if field_value is None: continue if isinstance(field_value, BaseResult): @@ -272,27 +276,28 @@ def _iterate_base_result_types(cls: Type[BaseModel]) -> Iterator[Type[BaseResult def get_cls_tags(cls: Type[BaseModel]) -> Set[IncludeTags]: if issubclass(cls, BaseResult): - return cls.__config__.tags + return cls.__tags__ or set() return set() def get_field_tags(cls: Type[BaseModel], field_name: str) -> Set[IncludeTags]: field_tags = set() for type_ in _iterate_base_result_types(cls): - if field_name not in type_.__config__.field_tags: + ft = type_.__field_tags__ or {} + if field_name not in ft: continue - field_tags = type_.__config__.field_tags[field_name] + field_tags = ft[field_name] break - field = cls.__fields__[field_name] - field_type = _get_actual_type(field.type_) - self_tags = set() if not issubclass(cls, BaseResult) else cls.__config__.tags + field = cls.model_fields[field_name] + field_type = _get_actual_type(field.annotation) + self_tags = set() if not issubclass(cls, BaseResult) else (cls.__tags__ or set()) cls_tags = get_cls_tags(field_type) return self_tags.union(field_tags).union(cls_tags) def get_all_fields_tags(cls: Type[BaseResult]) -> Dict[str, Set[IncludeTags]]: - return {field_name: get_field_tags(cls, field_name) for field_name in cls.__fields__} + return {field_name: get_field_tags(cls, field_name) for field_name in cls.model_fields} def new_id() -> uuid.UUID: diff --git a/src/evidently/legacy/descriptors/BERTScore_descriptor.py b/src/evidently/legacy/descriptors/BERTScore_descriptor.py index 3a498f6e27..545810f121 100644 --- a/src/evidently/legacy/descriptors/BERTScore_descriptor.py +++ b/src/evidently/legacy/descriptors/BERTScore_descriptor.py @@ -1,11 +1,13 @@ +from typing import ClassVar +from typing import Optional + from evidently.legacy.features.BERTScore_feature import BERTScoreFeature from evidently.legacy.features.generated_features import FeatureDescriptor from evidently.legacy.features.generated_features import GeneratedFeatures class BERTScore(FeatureDescriptor): - class Config: - type_alias = "evidently:descriptor:BERTScore" + __type_alias__: ClassVar[Optional[str]] = "evidently:descriptor:BERTScore" with_column: str diff --git a/src/evidently/legacy/descriptors/contains_link_descriptor.py b/src/evidently/legacy/descriptors/contains_link_descriptor.py index a1dee47957..eeeb4c325a 100644 --- a/src/evidently/legacy/descriptors/contains_link_descriptor.py +++ b/src/evidently/legacy/descriptors/contains_link_descriptor.py @@ -1,11 +1,13 @@ +from typing import ClassVar +from typing import Optional + from evidently.legacy.features import contains_link_feature from evidently.legacy.features.generated_features import FeatureDescriptor from evidently.legacy.features.generated_features import GeneratedFeature class ContainsLink(FeatureDescriptor): - class Config: - type_alias = "evidently:descriptor:ContainsLink" + __type_alias__: ClassVar[Optional[str]] = "evidently:descriptor:ContainsLink" def feature(self, column_name: str) -> GeneratedFeature: return contains_link_feature.ContainsLink(column_name, self.display_name) diff --git a/src/evidently/legacy/descriptors/custom_descriptor.py b/src/evidently/legacy/descriptors/custom_descriptor.py index 456d1b6563..8f09f2f014 100644 --- a/src/evidently/legacy/descriptors/custom_descriptor.py +++ b/src/evidently/legacy/descriptors/custom_descriptor.py @@ -1,4 +1,6 @@ from typing import Callable +from typing import ClassVar +from typing import Optional from typing import Union import pandas as pd @@ -13,8 +15,7 @@ class CustomColumnEval(FeatureDescriptor): - class Config: - type_alias = "evidently:descriptor:CustomColumnEval" + __type_alias__: ClassVar[Optional[str]] = "evidently:descriptor:CustomColumnEval" func: Callable[[pd.Series], pd.Series] display_name: str @@ -32,8 +33,7 @@ def feature(self, column_name: str) -> GeneratedFeature: class CustomPairColumnEval(GeneralDescriptor): - class Config: - type_alias = "evidently:descriptor:CustomPairColumnEval" + __type_alias__: ClassVar[Optional[str]] = "evidently:descriptor:CustomPairColumnEval" func: Callable[[pd.Series, pd.Series], pd.Series] display_name: str diff --git a/src/evidently/legacy/descriptors/exact_match_descriptor.py b/src/evidently/legacy/descriptors/exact_match_descriptor.py index 10a93e260d..9396151495 100644 --- a/src/evidently/legacy/descriptors/exact_match_descriptor.py +++ b/src/evidently/legacy/descriptors/exact_match_descriptor.py @@ -1,11 +1,13 @@ +from typing import ClassVar +from typing import Optional + from evidently.legacy.features.exact_match_feature import ExactMatchFeature from evidently.legacy.features.generated_features import FeatureDescriptor from evidently.legacy.features.generated_features import GeneratedFeatures class ExactMatch(FeatureDescriptor): - class Config: - type_alias = "evidently:descriptor:ExactMatch" + __type_alias__: ClassVar[Optional[str]] = "evidently:descriptor:ExactMatch" with_column: str diff --git a/src/evidently/legacy/descriptors/hf_descriptor.py b/src/evidently/legacy/descriptors/hf_descriptor.py index b666688d73..4c58ec455a 100644 --- a/src/evidently/legacy/descriptors/hf_descriptor.py +++ b/src/evidently/legacy/descriptors/hf_descriptor.py @@ -1,3 +1,4 @@ +from typing import ClassVar from typing import Optional from evidently.legacy.features.generated_features import FeatureDescriptor @@ -7,8 +8,7 @@ class HuggingFaceModel(FeatureDescriptor): - class Config: - type_alias = "evidently:descriptor:HuggingFaceModel" + __type_alias__: ClassVar[Optional[str]] = "evidently:descriptor:HuggingFaceModel" model: str params: Optional[dict] = None @@ -23,8 +23,7 @@ def feature(self, column_name: str) -> GeneratedFeature: class HuggingFaceToxicityModel(FeatureDescriptor): - class Config: - type_alias = "evidently:descriptor:HuggingFaceToxicityModel" + __type_alias__: ClassVar[Optional[str]] = "evidently:descriptor:HuggingFaceToxicityModel" model: Optional[str] = None toxic_label: Optional[str] = None diff --git a/src/evidently/legacy/descriptors/is_valid_json_descriptor.py b/src/evidently/legacy/descriptors/is_valid_json_descriptor.py index 7efa9b8455..1fbf010f20 100644 --- a/src/evidently/legacy/descriptors/is_valid_json_descriptor.py +++ b/src/evidently/legacy/descriptors/is_valid_json_descriptor.py @@ -1,11 +1,13 @@ +from typing import ClassVar +from typing import Optional + from evidently.legacy.features import is_valid_json_feature from evidently.legacy.features.generated_features import FeatureDescriptor from evidently.legacy.features.generated_features import GeneratedFeature class IsValidJSON(FeatureDescriptor): - class Config: - type_alias = "evidently:descriptor:IsValidJSON" + __type_alias__: ClassVar[Optional[str]] = "evidently:descriptor:IsValidJSON" def feature(self, column_name: str) -> GeneratedFeature: return is_valid_json_feature.IsValidJSON(column_name, self.display_name) diff --git a/src/evidently/legacy/descriptors/is_valid_python_descriptor.py b/src/evidently/legacy/descriptors/is_valid_python_descriptor.py index 82c6cb53fd..99cbff7331 100644 --- a/src/evidently/legacy/descriptors/is_valid_python_descriptor.py +++ b/src/evidently/legacy/descriptors/is_valid_python_descriptor.py @@ -1,11 +1,13 @@ +from typing import ClassVar +from typing import Optional + from evidently.legacy.features import is_valid_python_feature from evidently.legacy.features.generated_features import FeatureDescriptor from evidently.legacy.features.generated_features import GeneratedFeature class IsValidPython(FeatureDescriptor): - class Config: - type_alias = "evidently:descriptor:IsValidPython" + __type_alias__: ClassVar[Optional[str]] = "evidently:descriptor:IsValidPython" def feature(self, column_name: str) -> GeneratedFeature: return is_valid_python_feature.IsValidPython(column_name, self.display_name) diff --git a/src/evidently/legacy/descriptors/is_valid_sql_descriptor.py b/src/evidently/legacy/descriptors/is_valid_sql_descriptor.py index 2e435a3c15..352d3471d4 100644 --- a/src/evidently/legacy/descriptors/is_valid_sql_descriptor.py +++ b/src/evidently/legacy/descriptors/is_valid_sql_descriptor.py @@ -1,11 +1,13 @@ +from typing import ClassVar +from typing import Optional + from evidently.legacy.features import is_valid_sql_feature from evidently.legacy.features.generated_features import FeatureDescriptor from evidently.legacy.features.generated_features import GeneratedFeature class IsValidSQL(FeatureDescriptor): - class Config: - type_alias = "evidently:descriptor:IsValidSQL" + __type_alias__: ClassVar[Optional[str]] = "evidently:descriptor:IsValidSQL" def feature(self, column_name: str) -> GeneratedFeature: return is_valid_sql_feature.IsValidSQL(column_name, self.display_name) diff --git a/src/evidently/legacy/descriptors/json_match_descriptor.py b/src/evidently/legacy/descriptors/json_match_descriptor.py index b5f7285c3e..c252174175 100644 --- a/src/evidently/legacy/descriptors/json_match_descriptor.py +++ b/src/evidently/legacy/descriptors/json_match_descriptor.py @@ -1,11 +1,13 @@ +from typing import ClassVar +from typing import Optional + from evidently.legacy.features import json_match_feature from evidently.legacy.features.generated_features import FeatureDescriptor from evidently.legacy.features.generated_features import GeneratedFeature class JSONMatch(FeatureDescriptor): - class Config: - type_alias = "evidently:descriptor:JSONMatch" + __type_alias__: ClassVar[Optional[str]] = "evidently:descriptor:JSONMatch" with_column: str diff --git a/src/evidently/legacy/descriptors/json_schema_match_descriptor.py b/src/evidently/legacy/descriptors/json_schema_match_descriptor.py index 51757bafd7..779c7d68bf 100644 --- a/src/evidently/legacy/descriptors/json_schema_match_descriptor.py +++ b/src/evidently/legacy/descriptors/json_schema_match_descriptor.py @@ -1,4 +1,6 @@ +from typing import ClassVar from typing import Dict +from typing import Optional from evidently.legacy.features import json_schema_match_feature from evidently.legacy.features.generated_features import FeatureDescriptor @@ -6,8 +8,7 @@ class JSONSchemaMatch(FeatureDescriptor): - class Config: - type_alias = "evidently:descriptor:JSONSchemaMatch" + __type_alias__: ClassVar[Optional[str]] = "evidently:descriptor:JSONSchemaMatch" expected_schema: Dict[str, type] validate_types: bool = False diff --git a/src/evidently/legacy/descriptors/llm_judges.py b/src/evidently/legacy/descriptors/llm_judges.py index 029d5938df..38e4387d14 100644 --- a/src/evidently/legacy/descriptors/llm_judges.py +++ b/src/evidently/legacy/descriptors/llm_judges.py @@ -51,8 +51,7 @@ def get_input_columns(self, column_name: str) -> Dict[str, str]: class LLMEval(BaseLLMEval): - class Config: - type_alias = "evidently:descriptor:LLMEval" + __type_alias__: ClassVar[Optional[str]] = "evidently:descriptor:LLMEval" name: ClassVar[str] = "LLMEval" @@ -67,8 +66,7 @@ def get_subcolumn(self) -> Optional[str]: class BinaryClassificationLLMEval(BaseLLMEval): - class Config: - type_alias = "evidently:descriptor:BinaryClassificationLLMEval" + __type_alias__: ClassVar[Optional[str]] = "evidently:descriptor:BinaryClassificationLLMEval" template: ClassVar[BinaryClassificationPromptTemplate] include_category: Optional[bool] = None @@ -90,8 +88,7 @@ def get_subcolumn(self) -> Optional[str]: class NegativityLLMEval(BinaryClassificationLLMEval): - class Config: - type_alias = "evidently:descriptor:NegativityLLMEval" + __type_alias__: ClassVar[Optional[str]] = "evidently:descriptor:NegativityLLMEval" name: ClassVar[str] = "Negativity" template: ClassVar[BinaryClassificationPromptTemplate] = BinaryClassificationPromptTemplate( @@ -114,8 +111,7 @@ class Config: class PIILLMEval(BinaryClassificationLLMEval): - class Config: - type_alias = "evidently:descriptor:PIILLMEval" + __type_alias__: ClassVar[Optional[str]] = "evidently:descriptor:PIILLMEval" name: ClassVar[str] = "PII" template: ClassVar[BinaryClassificationPromptTemplate] = BinaryClassificationPromptTemplate( @@ -139,8 +135,7 @@ class Config: class DeclineLLMEval(BinaryClassificationLLMEval): - class Config: - type_alias = "evidently:descriptor:DeclineLLMEval" + __type_alias__: ClassVar[Optional[str]] = "evidently:descriptor:DeclineLLMEval" name: ClassVar[str] = "Decline" template: ClassVar[BinaryClassificationPromptTemplate] = BinaryClassificationPromptTemplate( @@ -161,8 +156,7 @@ class Config: class ContextQualityLLMEval(BinaryClassificationLLMEval): - class Config: - type_alias = "evidently:descriptor:ContextQualityLLMEval" + __type_alias__: ClassVar[Optional[str]] = "evidently:descriptor:ContextQualityLLMEval" name: ClassVar[str] = "ContextQuality" @@ -200,8 +194,7 @@ def get_input_columns(self, column_name: str) -> Dict[str, str]: class BiasLLMEval(BinaryClassificationLLMEval): - class Config: - type_alias = "evidently:descriptor:BiasLLMEval" + __type_alias__: ClassVar[Optional[str]] = "evidently:descriptor:BiasLLMEval" name: ClassVar[str] = "Bias" template: ClassVar[BinaryClassificationPromptTemplate] = BinaryClassificationPromptTemplate( @@ -227,8 +220,7 @@ class Config: class ToxicityLLMEval(BinaryClassificationLLMEval): - class Config: - type_alias = "evidently:descriptor:ToxicityLLMEval" + __type_alias__: ClassVar[Optional[str]] = "evidently:descriptor:ToxicityLLMEval" name: ClassVar[str] = "Toxicity" template: ClassVar[BinaryClassificationPromptTemplate] = BinaryClassificationPromptTemplate( @@ -254,8 +246,7 @@ class Config: class CorrectnessLLMEval(BinaryClassificationLLMEval): - class Config: - type_alias = "evidently:descriptor:CorrectnessLLMEval" + __type_alias__: ClassVar[Optional[str]] = "evidently:descriptor:CorrectnessLLMEval" name: ClassVar[str] = "Correctness" target_output: str @@ -303,8 +294,7 @@ def get_input_columns(self, column_name: str) -> Dict[str, str]: class FaithfulnessLLMEval(BinaryClassificationLLMEval): - class Config: - type_alias = "evidently:descriptor:FaithfulnessLLMEval" + __type_alias__: ClassVar[Optional[str]] = "evidently:descriptor:FaithfulnessLLMEval" name: ClassVar[str] = "Faithfulness" context: str @@ -352,8 +342,7 @@ def get_input_columns(self, column_name: str) -> Dict[str, str]: class CompletenessLLMEval(BinaryClassificationLLMEval): - class Config: - type_alias = "evidently:descriptor:CompletenessLLMEval" + __type_alias__: ClassVar[Optional[str]] = "evidently:descriptor:CompletenessLLMEval" name: ClassVar[str] = "Completeness" context: str @@ -401,8 +390,7 @@ def get_input_columns(self, column_name: str) -> Dict[str, str]: class MulticlassClassificationLLMEval(BaseLLMEval): - class Config: - type_alias = "evidently:descriptor:MulticlassClassificationLLMEval" + __type_alias__: ClassVar[Optional[str]] = "evidently:descriptor:MulticlassClassificationLLMEval" template: ClassVar[MulticlassClassificationPromptTemplate] include_category: Optional[bool] = None diff --git a/src/evidently/legacy/descriptors/non_letter_character_percentage_descriptor.py b/src/evidently/legacy/descriptors/non_letter_character_percentage_descriptor.py index c04797737b..659be4506b 100644 --- a/src/evidently/legacy/descriptors/non_letter_character_percentage_descriptor.py +++ b/src/evidently/legacy/descriptors/non_letter_character_percentage_descriptor.py @@ -1,11 +1,13 @@ +from typing import ClassVar +from typing import Optional + from evidently.legacy.features import non_letter_character_percentage_feature from evidently.legacy.features.generated_features import FeatureDescriptor from evidently.legacy.features.generated_features import GeneratedFeature class NonLetterCharacterPercentage(FeatureDescriptor): - class Config: - type_alias = "evidently:descriptor:NonLetterCharacterPercentage" + __type_alias__: ClassVar[Optional[str]] = "evidently:descriptor:NonLetterCharacterPercentage" def feature(self, column_name: str) -> GeneratedFeature: return non_letter_character_percentage_feature.NonLetterCharacterPercentage(column_name, self.display_name) diff --git a/src/evidently/legacy/descriptors/oov_words_percentage_descriptor.py b/src/evidently/legacy/descriptors/oov_words_percentage_descriptor.py index 22cf63ae64..a11da194a4 100644 --- a/src/evidently/legacy/descriptors/oov_words_percentage_descriptor.py +++ b/src/evidently/legacy/descriptors/oov_words_percentage_descriptor.py @@ -1,3 +1,5 @@ +from typing import ClassVar +from typing import Optional from typing import Tuple from evidently.legacy.features.generated_features import FeatureDescriptor @@ -6,8 +8,7 @@ class OOV(FeatureDescriptor): - class Config: - type_alias = "evidently:descriptor:OOV" + __type_alias__: ClassVar[Optional[str]] = "evidently:descriptor:OOV" ignore_words: Tuple = () diff --git a/src/evidently/legacy/descriptors/openai_descriptor.py b/src/evidently/legacy/descriptors/openai_descriptor.py index 315c7cfb0b..c581c81788 100644 --- a/src/evidently/legacy/descriptors/openai_descriptor.py +++ b/src/evidently/legacy/descriptors/openai_descriptor.py @@ -1,3 +1,4 @@ +from typing import ClassVar from typing import List from typing import Optional @@ -7,8 +8,7 @@ class OpenAIPrompting(FeatureDescriptor): - class Config: - type_alias = "evidently:descriptor:OpenAIPrompting" + __type_alias__: ClassVar[Optional[str]] = "evidently:descriptor:OpenAIPrompting" prompt: str prompt_replace_string: str diff --git a/src/evidently/legacy/descriptors/regexp_descriptor.py b/src/evidently/legacy/descriptors/regexp_descriptor.py index 7d5a65f748..31acead16e 100644 --- a/src/evidently/legacy/descriptors/regexp_descriptor.py +++ b/src/evidently/legacy/descriptors/regexp_descriptor.py @@ -1,11 +1,13 @@ +from typing import ClassVar +from typing import Optional + from evidently.legacy.features import regexp_feature from evidently.legacy.features.generated_features import FeatureDescriptor from evidently.legacy.features.generated_features import GeneratedFeature class RegExp(FeatureDescriptor): - class Config: - type_alias = "evidently:descriptor:RegExp" + __type_alias__: ClassVar[Optional[str]] = "evidently:descriptor:RegExp" reg_exp: str diff --git a/src/evidently/legacy/descriptors/semantic_similarity.py b/src/evidently/legacy/descriptors/semantic_similarity.py index 3253d5683e..3f58013d2f 100644 --- a/src/evidently/legacy/descriptors/semantic_similarity.py +++ b/src/evidently/legacy/descriptors/semantic_similarity.py @@ -1,11 +1,13 @@ +from typing import ClassVar +from typing import Optional + from evidently.legacy.features.generated_features import FeatureDescriptor from evidently.legacy.features.generated_features import GeneratedFeatures from evidently.legacy.features.semantic_similarity_feature import SemanticSimilarityFeature class SemanticSimilarity(FeatureDescriptor): - class Config: - type_alias = "evidently:descriptor:SemanticSimilarity" + __type_alias__: ClassVar[Optional[str]] = "evidently:descriptor:SemanticSimilarity" with_column: str diff --git a/src/evidently/legacy/descriptors/sentence_count_descriptor.py b/src/evidently/legacy/descriptors/sentence_count_descriptor.py index 24b638dbd6..1db0acfde7 100644 --- a/src/evidently/legacy/descriptors/sentence_count_descriptor.py +++ b/src/evidently/legacy/descriptors/sentence_count_descriptor.py @@ -1,11 +1,13 @@ +from typing import ClassVar +from typing import Optional + from evidently.legacy.features import sentence_count_feature from evidently.legacy.features.generated_features import FeatureDescriptor from evidently.legacy.features.generated_features import GeneratedFeature class SentenceCount(FeatureDescriptor): - class Config: - type_alias = "evidently:descriptor:SentenceCount" + __type_alias__: ClassVar[Optional[str]] = "evidently:descriptor:SentenceCount" def feature(self, column_name: str) -> GeneratedFeature: return sentence_count_feature.SentenceCount(column_name, self.display_name) diff --git a/src/evidently/legacy/descriptors/sentiment_descriptor.py b/src/evidently/legacy/descriptors/sentiment_descriptor.py index cdccdc6e9d..37b50a448b 100644 --- a/src/evidently/legacy/descriptors/sentiment_descriptor.py +++ b/src/evidently/legacy/descriptors/sentiment_descriptor.py @@ -1,11 +1,13 @@ +from typing import ClassVar +from typing import Optional + from evidently.legacy.features import sentiment_feature from evidently.legacy.features.generated_features import FeatureDescriptor from evidently.legacy.features.generated_features import GeneratedFeature class Sentiment(FeatureDescriptor): - class Config: - type_alias = "evidently:descriptor:Sentiment" + __type_alias__: ClassVar[Optional[str]] = "evidently:descriptor:Sentiment" def feature(self, column_name: str) -> GeneratedFeature: return sentiment_feature.Sentiment(column_name, self.display_name) diff --git a/src/evidently/legacy/descriptors/text_contains_descriptor.py b/src/evidently/legacy/descriptors/text_contains_descriptor.py index cd503e2515..f007ee1c0e 100644 --- a/src/evidently/legacy/descriptors/text_contains_descriptor.py +++ b/src/evidently/legacy/descriptors/text_contains_descriptor.py @@ -1,4 +1,6 @@ +from typing import ClassVar from typing import List +from typing import Optional from evidently.legacy.features import text_contains_feature from evidently.legacy.features.generated_features import FeatureDescriptor @@ -6,8 +8,7 @@ class Contains(FeatureDescriptor): - class Config: - type_alias = "evidently:descriptor:Contains" + __type_alias__: ClassVar[Optional[str]] = "evidently:descriptor:Contains" items: List[str] mode: str = "any" @@ -24,8 +25,7 @@ def feature(self, column_name: str) -> GeneratedFeature: class DoesNotContain(FeatureDescriptor): - class Config: - type_alias = "evidently:descriptor:DoesNotContain" + __type_alias__: ClassVar[Optional[str]] = "evidently:descriptor:DoesNotContain" items: List[str] mode: str = "all" @@ -42,8 +42,7 @@ def feature(self, column_name: str) -> GeneratedFeature: class ItemMatch(FeatureDescriptor): - class Config: - type_alias = "evidently:descriptor:ItemMatch" + __type_alias__: ClassVar[Optional[str]] = "evidently:descriptor:ItemMatch" with_column: str mode: str = "any" @@ -59,8 +58,7 @@ def feature(self, column_name: str) -> GeneratedFeature: class ItemNoMatch(FeatureDescriptor): - class Config: - type_alias = "evidently:descriptor:ItemNoMatch" + __type_alias__: ClassVar[Optional[str]] = "evidently:descriptor:ItemNoMatch" with_column: str mode: str = "any" diff --git a/src/evidently/legacy/descriptors/text_length_descriptor.py b/src/evidently/legacy/descriptors/text_length_descriptor.py index 03ad3b948c..45ecdc149e 100644 --- a/src/evidently/legacy/descriptors/text_length_descriptor.py +++ b/src/evidently/legacy/descriptors/text_length_descriptor.py @@ -1,11 +1,13 @@ +from typing import ClassVar +from typing import Optional + from evidently.legacy.features import text_length_feature from evidently.legacy.features.generated_features import FeatureDescriptor from evidently.legacy.features.generated_features import GeneratedFeature class TextLength(FeatureDescriptor): - class Config: - type_alias = "evidently:descriptor:TextLength" + __type_alias__: ClassVar[Optional[str]] = "evidently:descriptor:TextLength" def feature(self, column_name: str) -> GeneratedFeature: return text_length_feature.TextLength(column_name, self.display_name) diff --git a/src/evidently/legacy/descriptors/text_part_descriptor.py b/src/evidently/legacy/descriptors/text_part_descriptor.py index 3916487159..2ac226b3ab 100644 --- a/src/evidently/legacy/descriptors/text_part_descriptor.py +++ b/src/evidently/legacy/descriptors/text_part_descriptor.py @@ -1,11 +1,13 @@ +from typing import ClassVar +from typing import Optional + from evidently.legacy.features import text_part_feature from evidently.legacy.features.generated_features import FeatureDescriptor from evidently.legacy.features.generated_features import GeneratedFeature class BeginsWith(FeatureDescriptor): - class Config: - type_alias = "evidently:descriptor:BeginsWith" + __type_alias__: ClassVar[Optional[str]] = "evidently:descriptor:BeginsWith" prefix: str case_sensitive: bool = True @@ -20,8 +22,7 @@ def feature(self, column_name: str) -> GeneratedFeature: class EndsWith(FeatureDescriptor): - class Config: - type_alias = "evidently:descriptor:EndsWith" + __type_alias__: ClassVar[Optional[str]] = "evidently:descriptor:EndsWith" suffix: str case_sensitive: bool = True diff --git a/src/evidently/legacy/descriptors/trigger_words_presence_descriptor.py b/src/evidently/legacy/descriptors/trigger_words_presence_descriptor.py index 4d81dea060..bda19b437f 100644 --- a/src/evidently/legacy/descriptors/trigger_words_presence_descriptor.py +++ b/src/evidently/legacy/descriptors/trigger_words_presence_descriptor.py @@ -1,4 +1,6 @@ +from typing import ClassVar from typing import List +from typing import Optional from evidently.legacy.features import trigger_words_presence_feature from evidently.legacy.features.generated_features import FeatureDescriptor @@ -6,8 +8,7 @@ class TriggerWordsPresence(FeatureDescriptor): - class Config: - type_alias = "evidently:descriptor:TriggerWordsPresence" + __type_alias__: ClassVar[Optional[str]] = "evidently:descriptor:TriggerWordsPresence" words_list: List[str] lemmatize: bool = True diff --git a/src/evidently/legacy/descriptors/word_count_descriptor.py b/src/evidently/legacy/descriptors/word_count_descriptor.py index 2f2bc0c0ef..9de193946b 100644 --- a/src/evidently/legacy/descriptors/word_count_descriptor.py +++ b/src/evidently/legacy/descriptors/word_count_descriptor.py @@ -1,11 +1,13 @@ +from typing import ClassVar +from typing import Optional + from evidently.legacy.features import word_count_feature from evidently.legacy.features.generated_features import FeatureDescriptor from evidently.legacy.features.generated_features import GeneratedFeature class WordCount(FeatureDescriptor): - class Config: - type_alias = "evidently:descriptor:WordCount" + __type_alias__: ClassVar[Optional[str]] = "evidently:descriptor:WordCount" def feature(self, column_name: str) -> GeneratedFeature: return word_count_feature.WordCount(column_name, self.display_name) diff --git a/src/evidently/legacy/descriptors/words_descriptor.py b/src/evidently/legacy/descriptors/words_descriptor.py index ca709a891b..d9a1e412da 100644 --- a/src/evidently/legacy/descriptors/words_descriptor.py +++ b/src/evidently/legacy/descriptors/words_descriptor.py @@ -1,4 +1,6 @@ +from typing import ClassVar from typing import List +from typing import Optional from evidently.legacy.features import words_feature from evidently.legacy.features.generated_features import FeatureDescriptor @@ -6,8 +8,7 @@ class ExcludesWords(FeatureDescriptor): - class Config: - type_alias = "evidently:descriptor:ExcludesWords" + __type_alias__: ClassVar[Optional[str]] = "evidently:descriptor:ExcludesWords" words_list: List[str] mode: str = "all" @@ -24,8 +25,7 @@ def feature(self, column_name: str) -> GeneratedFeature: class IncludesWords(FeatureDescriptor): - class Config: - type_alias = "evidently:descriptor:IncludesWords" + __type_alias__: ClassVar[Optional[str]] = "evidently:descriptor:IncludesWords" words_list: List[str] mode: str = "any" @@ -42,8 +42,7 @@ def feature(self, column_name: str) -> GeneratedFeature: class WordMatch(FeatureDescriptor): - class Config: - type_alias = "evidently:descriptor:WordMatch" + __type_alias__: ClassVar[Optional[str]] = "evidently:descriptor:WordMatch" with_column: str mode: str = "any" @@ -59,8 +58,7 @@ def feature(self, column_name: str) -> GeneratedFeature: class WordNoMatch(FeatureDescriptor): - class Config: - type_alias = "evidently:descriptor:WordNoMatch" + __type_alias__: ClassVar[Optional[str]] = "evidently:descriptor:WordNoMatch" with_column: str mode: str = "any" diff --git a/src/evidently/legacy/experimental/report_set.py b/src/evidently/legacy/experimental/report_set.py index 867e779e2f..c8074bf4ca 100644 --- a/src/evidently/legacy/experimental/report_set.py +++ b/src/evidently/legacy/experimental/report_set.py @@ -3,7 +3,8 @@ from typing import Dict from typing import Optional -from evidently._pydantic_compat import ValidationError +from pydantic import ValidationError + from evidently.legacy.suite.base_suite import Snapshot from evidently.legacy.ui.type_aliases import SnapshotID diff --git a/src/evidently/legacy/features/BERTScore_feature.py b/src/evidently/legacy/features/BERTScore_feature.py index a5227b2000..944331e80d 100644 --- a/src/evidently/legacy/features/BERTScore_feature.py +++ b/src/evidently/legacy/features/BERTScore_feature.py @@ -2,6 +2,7 @@ from typing import ClassVar from typing import Dict from typing import List +from typing import Optional import numpy as np import pandas as pd @@ -13,8 +14,7 @@ class BERTScoreFeature(GeneratedFeature): - class Config: - type_alias = "evidently:feature:BERTScoreFeature" + __type_alias__: ClassVar[Optional[str]] = "evidently:feature:BERTScoreFeature" __feature_type__: ClassVar = ColumnType.Numerical columns: List[str] diff --git a/src/evidently/legacy/features/OOV_words_percentage_feature.py b/src/evidently/legacy/features/OOV_words_percentage_feature.py index d4ba4a60c7..35e053b94d 100644 --- a/src/evidently/legacy/features/OOV_words_percentage_feature.py +++ b/src/evidently/legacy/features/OOV_words_percentage_feature.py @@ -16,8 +16,7 @@ class OOVWordsPercentage(ApplyColumnGeneratedFeature): - class Config: - type_alias = "evidently:feature:OOVWordsPercentage" + __type_alias__: ClassVar[Optional[str]] = "evidently:feature:OOVWordsPercentage" __feature_type__: ClassVar = ColumnType.Numerical display_name_template: ClassVar = "OOV Words % for {column_name}" diff --git a/src/evidently/legacy/features/contains_link_feature.py b/src/evidently/legacy/features/contains_link_feature.py index d8a7e13d86..69ea7a3c50 100644 --- a/src/evidently/legacy/features/contains_link_feature.py +++ b/src/evidently/legacy/features/contains_link_feature.py @@ -10,8 +10,7 @@ class ContainsLink(ApplyColumnGeneratedFeature): - class Config: - type_alias = "evidently:feature:ContainsLink" + __type_alias__: ClassVar[Optional[str]] = "evidently:feature:ContainsLink" __feature_type__: ClassVar = ColumnType.Categorical display_name_template: ClassVar = "{column_name} contains link" diff --git a/src/evidently/legacy/features/custom_feature.py b/src/evidently/legacy/features/custom_feature.py index 7b113b3500..398dd13124 100644 --- a/src/evidently/legacy/features/custom_feature.py +++ b/src/evidently/legacy/features/custom_feature.py @@ -1,9 +1,11 @@ from typing import Callable +from typing import ClassVar +from typing import Optional from typing import Tuple import pandas as pd +from pydantic import Field -from evidently._pydantic_compat import Field from evidently.legacy.base_metric import ColumnName from evidently.legacy.core import ColumnType from evidently.legacy.core import new_id @@ -14,8 +16,7 @@ class CustomFeature(FeatureTypeFieldMixin, GeneratedFeature): - class Config: - type_alias = "evidently:feature:CustomFeature" + __type_alias__: ClassVar[Optional[str]] = "evidently:feature:CustomFeature" display_name: str name: str = Field(default_factory=lambda: str(new_id())) @@ -31,8 +32,7 @@ def _as_column(self) -> "ColumnName": class CustomSingleColumnFeature(FeatureTypeFieldMixin, GeneratedFeature): - class Config: - type_alias = "evidently:feature:CustomSingleColumnFeature" + __type_alias__: ClassVar[Optional[str]] = "evidently:feature:CustomSingleColumnFeature" display_name: str func: Callable[[pd.Series], pd.Series] @@ -50,14 +50,13 @@ def _as_column(self) -> "ColumnName": def get_fingerprint_parts(self) -> Tuple[FingerprintPart, ...]: return tuple( (name, self.get_field_fingerprint(name)) - for name, field in sorted(self.__fields__.items()) - if (field.required or getattr(self, name) != field.get_default()) and field.name != "func" + for name, field in sorted(self.model_fields.items()) + if (field.is_required() or getattr(self, name) != field.default) and name != "func" ) class CustomPairColumnFeature(FeatureTypeFieldMixin, GeneratedFeature): - class Config: - type_alias = "evidently:feature:CustomPairColumnFeature" + __type_alias__: ClassVar[Optional[str]] = "evidently:feature:CustomPairColumnFeature" display_name: str func: Callable[[pd.Series, pd.Series], pd.Series] diff --git a/src/evidently/legacy/features/exact_match_feature.py b/src/evidently/legacy/features/exact_match_feature.py index 7a251bc747..fdf4b33ce2 100644 --- a/src/evidently/legacy/features/exact_match_feature.py +++ b/src/evidently/legacy/features/exact_match_feature.py @@ -1,5 +1,6 @@ from typing import ClassVar from typing import List +from typing import Optional import pandas as pd @@ -10,8 +11,7 @@ class ExactMatchFeature(GeneratedFeature): - class Config: - type_alias = "evidently:feature:ExactMatchFeature" + __type_alias__: ClassVar[Optional[str]] = "evidently:feature:ExactMatchFeature" __feature_type__: ClassVar = ColumnType.Categorical columns: List[str] diff --git a/src/evidently/legacy/features/generated_features.py b/src/evidently/legacy/features/generated_features.py index 6dfdb0acd7..8159660f0c 100644 --- a/src/evidently/legacy/features/generated_features.py +++ b/src/evidently/legacy/features/generated_features.py @@ -9,9 +9,9 @@ import deprecation import pandas as pd import uuid6 +from pydantic import BaseModel +from pydantic import Field -from evidently._pydantic_compat import BaseModel -from evidently._pydantic_compat import Field from evidently.legacy.base_metric import ColumnName from evidently.legacy.base_metric import DatasetType from evidently.legacy.base_metric import TEngineDataType @@ -28,8 +28,7 @@ class FeatureResult(Generic[TEngineDataType]): class GeneratedFeatures(EvidentlyBaseModel): - class Config: - is_base_type = True + __is_base_type__: ClassVar[bool] = True display_name: Optional[str] = None """ @@ -190,16 +189,14 @@ def _as_column(self) -> "ColumnName": class BaseDescriptor(EvidentlyBaseModel): - class Config: - type_alias = "evidently:descriptor:BaseDescriptor" - is_base_type = True + __type_alias__: ClassVar[Optional[str]] = "evidently:descriptor:BaseDescriptor" + __is_base_type__: ClassVar[bool] = True display_name: Optional[str] = None class GeneralDescriptor(BaseDescriptor): - class Config: - is_base_type = True + __is_base_type__: ClassVar[bool] = True @abc.abstractmethod def feature(self) -> GeneratedFeatures: @@ -210,9 +207,8 @@ def as_column(self) -> "ColumnName": class MultiColumnFeatureDescriptor(BaseDescriptor): - class Config: - type_alias = "evidently:descriptor:MultiColumnFeatureDescriptor" - is_base_type = True + __type_alias__: ClassVar[Optional[str]] = "evidently:descriptor:MultiColumnFeatureDescriptor" + __is_base_type__: ClassVar[bool] = True def feature(self, columns: List[str]) -> GeneratedFeature: raise NotImplementedError() @@ -225,8 +221,7 @@ def on(self, columns: List[str]) -> "ColumnName": class FeatureDescriptor(BaseDescriptor): - class Config: - is_base_type = True + __is_base_type__: ClassVar[bool] = True @abc.abstractmethod def feature(self, column_name: str) -> GeneratedFeatures: diff --git a/src/evidently/legacy/features/hf_feature.py b/src/evidently/legacy/features/hf_feature.py index 3e03a9fa80..a769fb54af 100644 --- a/src/evidently/legacy/features/hf_feature.py +++ b/src/evidently/legacy/features/hf_feature.py @@ -16,8 +16,7 @@ class HuggingFaceFeature(FeatureTypeFieldMixin, DataFeature): - class Config: - type_alias = "evidently:feature:HuggingFaceFeature" + __type_alias__: ClassVar[Optional[str]] = "evidently:feature:HuggingFaceFeature" column_name: str model: str @@ -43,8 +42,7 @@ def __hash__(self): class HuggingFaceToxicityFeature(DataFeature): - class Config: - type_alias = "evidently:feature:HuggingFaceToxicityFeature" + __type_alias__: ClassVar[Optional[str]] = "evidently:feature:HuggingFaceToxicityFeature" __feature_type__: ClassVar = ColumnType.Numerical column_name: str diff --git a/src/evidently/legacy/features/is_valid_json_feature.py b/src/evidently/legacy/features/is_valid_json_feature.py index 9ad41dd16f..7363e084a9 100644 --- a/src/evidently/legacy/features/is_valid_json_feature.py +++ b/src/evidently/legacy/features/is_valid_json_feature.py @@ -8,8 +8,7 @@ class IsValidJSON(ApplyColumnGeneratedFeature): - class Config: - type_alias = "evidently:feature:IsValidJSON" + __type_alias__: ClassVar[Optional[str]] = "evidently:feature:IsValidJSON" __feature_type__: ClassVar = ColumnType.Categorical display_name_template: ClassVar = "JSON valid for {column_name}" diff --git a/src/evidently/legacy/features/is_valid_python_feature.py b/src/evidently/legacy/features/is_valid_python_feature.py index d0bf4000f1..215835b426 100644 --- a/src/evidently/legacy/features/is_valid_python_feature.py +++ b/src/evidently/legacy/features/is_valid_python_feature.py @@ -8,8 +8,7 @@ class IsValidPython(ApplyColumnGeneratedFeature): - class Config: - type_alias = "evidently:feature:IsValidPython" + __type_alias__: ClassVar[Optional[str]] = "evidently:feature:IsValidPython" __feature_type__: ClassVar = ColumnType.Categorical display_name_template: ClassVar = "Valid Python for {column_name}" diff --git a/src/evidently/legacy/features/is_valid_sql_feature.py b/src/evidently/legacy/features/is_valid_sql_feature.py index a211f9369d..1cd91bf768 100644 --- a/src/evidently/legacy/features/is_valid_sql_feature.py +++ b/src/evidently/legacy/features/is_valid_sql_feature.py @@ -7,8 +7,7 @@ class IsValidSQL(ApplyColumnGeneratedFeature): - class Config: - type_alias = "evidently:feature:IsValidSQL" + __type_alias__: ClassVar[Optional[str]] = "evidently:feature:IsValidSQL" __feature_type__: ClassVar = ColumnType.Categorical display_name_template: ClassVar = "SQL Validity Check for {column_name}" diff --git a/src/evidently/legacy/features/json_match_feature.py b/src/evidently/legacy/features/json_match_feature.py index 542d0669cc..cebd26aa1f 100644 --- a/src/evidently/legacy/features/json_match_feature.py +++ b/src/evidently/legacy/features/json_match_feature.py @@ -1,4 +1,6 @@ import json +from typing import ClassVar +from typing import Optional import pandas as pd @@ -10,8 +12,7 @@ class JSONMatch(FeatureTypeFieldMixin, GeneratedFeature): - class Config: - type_alias = "evidently:feature:JSONMatch" + __type_alias__: ClassVar[Optional[str]] = "evidently:feature:JSONMatch" first_column: str second_column: str diff --git a/src/evidently/legacy/features/json_schema_match_feature.py b/src/evidently/legacy/features/json_schema_match_feature.py index 4ff5c203b2..db204a93d6 100644 --- a/src/evidently/legacy/features/json_schema_match_feature.py +++ b/src/evidently/legacy/features/json_schema_match_feature.py @@ -13,8 +13,7 @@ class JSONSchemaMatch(GeneratedFeature): - class Config: - type_alias = "evidently:feature:JSONSchemaMatch" + __type_alias__: ClassVar[Optional[str]] = "evidently:feature:JSONSchemaMatch" __feature_type__: ClassVar = ColumnType.Categorical column_name: str diff --git a/src/evidently/legacy/features/llm_judge.py b/src/evidently/legacy/features/llm_judge.py index 4dc9bc0e15..c0cca6f4a8 100644 --- a/src/evidently/legacy/features/llm_judge.py +++ b/src/evidently/legacy/features/llm_judge.py @@ -5,8 +5,8 @@ from typing import Union import pandas as pd +from pydantic import PrivateAttr -from evidently._pydantic_compat import PrivateAttr from evidently.legacy.base_metric import ColumnName from evidently.legacy.core import ColumnType from evidently.legacy.features.generated_features import GeneratedFeatures @@ -22,8 +22,7 @@ class LLMJudge(GeneratedFeatures): - class Config: - type_alias = "evidently:feature:LLMJudge" + __type_alias__: ClassVar[Optional[str]] = "evidently:feature:LLMJudge" """Generic LLM judge generated features""" diff --git a/src/evidently/legacy/features/non_letter_character_percentage_feature.py b/src/evidently/legacy/features/non_letter_character_percentage_feature.py index 9417842c02..6a4160912e 100644 --- a/src/evidently/legacy/features/non_letter_character_percentage_feature.py +++ b/src/evidently/legacy/features/non_letter_character_percentage_feature.py @@ -9,8 +9,7 @@ class NonLetterCharacterPercentage(ApplyColumnGeneratedFeature): - class Config: - type_alias = "evidently:feature:NonLetterCharacterPercentage" + __type_alias__: ClassVar[Optional[str]] = "evidently:feature:NonLetterCharacterPercentage" __feature_type__: ClassVar = ColumnType.Numerical display_name_template: ClassVar = "Non Letter Character % for {column_name}" diff --git a/src/evidently/legacy/features/openai_feature.py b/src/evidently/legacy/features/openai_feature.py index 7fe6a3260e..c5855be51f 100644 --- a/src/evidently/legacy/features/openai_feature.py +++ b/src/evidently/legacy/features/openai_feature.py @@ -1,5 +1,6 @@ from itertools import repeat from typing import Any +from typing import ClassVar from typing import List from typing import Optional from typing import Union @@ -17,8 +18,7 @@ class OpenAIFeature(FeatureTypeFieldMixin, GeneratedFeature): - class Config: - type_alias = "evidently:feature:OpenAIFeature" + __type_alias__: ClassVar[Optional[str]] = "evidently:feature:OpenAIFeature" column_name: str feature_id: str diff --git a/src/evidently/legacy/features/regexp_feature.py b/src/evidently/legacy/features/regexp_feature.py index faf50be425..b4a3691bad 100644 --- a/src/evidently/legacy/features/regexp_feature.py +++ b/src/evidently/legacy/features/regexp_feature.py @@ -10,8 +10,7 @@ class RegExp(GeneratedFeature): - class Config: - type_alias = "evidently:feature:RegExp" + __type_alias__: ClassVar[Optional[str]] = "evidently:feature:RegExp" __feature_type__: ClassVar = ColumnType.Categorical column_name: str diff --git a/src/evidently/legacy/features/semantic_similarity_feature.py b/src/evidently/legacy/features/semantic_similarity_feature.py index a7ca14b094..62f4cab56d 100644 --- a/src/evidently/legacy/features/semantic_similarity_feature.py +++ b/src/evidently/legacy/features/semantic_similarity_feature.py @@ -1,5 +1,6 @@ from typing import ClassVar from typing import List +from typing import Optional import numpy as np import pandas as pd @@ -11,8 +12,7 @@ class SemanticSimilarityFeature(GeneratedFeature): - class Config: - type_alias = "evidently:feature:SemanticSimilarityFeature" + __type_alias__: ClassVar[Optional[str]] = "evidently:feature:SemanticSimilarityFeature" __feature_type__: ClassVar = ColumnType.Numerical columns: List[str] diff --git a/src/evidently/legacy/features/sentence_count_feature.py b/src/evidently/legacy/features/sentence_count_feature.py index 57c6edeeac..ad0407ab39 100644 --- a/src/evidently/legacy/features/sentence_count_feature.py +++ b/src/evidently/legacy/features/sentence_count_feature.py @@ -10,8 +10,7 @@ class SentenceCount(ApplyColumnGeneratedFeature): - class Config: - type_alias = "evidently:feature:SentenceCount" + __type_alias__: ClassVar[Optional[str]] = "evidently:feature:SentenceCount" __feature_type__: ClassVar = ColumnType.Numerical _reg: ClassVar[re.Pattern] = re.compile(r"(? ColumnName: class EndsWith(GeneratedFeature): - class Config: - type_alias = "evidently:feature:EndsWith" + __type_alias__: ClassVar[Optional[str]] = "evidently:feature:EndsWith" __feature_type__: ClassVar = ColumnType.Categorical column_name: str diff --git a/src/evidently/legacy/features/trigger_words_presence_feature.py b/src/evidently/legacy/features/trigger_words_presence_feature.py index 3e14e38a9c..2c34d2fc97 100644 --- a/src/evidently/legacy/features/trigger_words_presence_feature.py +++ b/src/evidently/legacy/features/trigger_words_presence_feature.py @@ -6,15 +6,14 @@ import numpy as np from nltk.stem.wordnet import WordNetLemmatizer +from pydantic import PrivateAttr -from evidently._pydantic_compat import PrivateAttr from evidently.legacy.core import ColumnType from evidently.legacy.features.generated_features import ApplyColumnGeneratedFeature class TriggerWordsPresent(ApplyColumnGeneratedFeature): - class Config: - type_alias = "evidently:feature:TriggerWordsPresent" + __type_alias__: ClassVar[Optional[str]] = "evidently:feature:TriggerWordsPresent" __feature_type__: ClassVar = ColumnType.Categorical column_name: str diff --git a/src/evidently/legacy/features/word_count_feature.py b/src/evidently/legacy/features/word_count_feature.py index 9a3f1d08f0..3971d079c6 100644 --- a/src/evidently/legacy/features/word_count_feature.py +++ b/src/evidently/legacy/features/word_count_feature.py @@ -10,8 +10,7 @@ class WordCount(ApplyColumnGeneratedFeature): - class Config: - type_alias = "evidently:feature:WordCount" + __type_alias__: ClassVar[Optional[str]] = "evidently:feature:WordCount" __feature_type__: ClassVar = ColumnType.Numerical _reg: ClassVar[re.Pattern] = re.compile(r"[^a-zA-Z ]+") diff --git a/src/evidently/legacy/features/words_feature.py b/src/evidently/legacy/features/words_feature.py index 82edb62d56..824ccbfbf0 100644 --- a/src/evidently/legacy/features/words_feature.py +++ b/src/evidently/legacy/features/words_feature.py @@ -7,8 +7,8 @@ import numpy as np import pandas as pd from nltk.stem.wordnet import WordNetLemmatizer +from pydantic import PrivateAttr -from evidently._pydantic_compat import PrivateAttr from evidently.legacy.base_metric import ColumnName from evidently.legacy.core import ColumnType from evidently.legacy.features.generated_features import ApplyColumnGeneratedFeature @@ -45,8 +45,7 @@ def _listed_words_present( class WordsPresence(ApplyColumnGeneratedFeature): - class Config: - type_alias = "evidently:feature:WordsPresence" + __type_alias__: ClassVar[Optional[str]] = "evidently:feature:WordsPresence" __feature_type__: ClassVar = ColumnType.Categorical display_name_template: ClassVar = "{column_name} words presence" @@ -86,8 +85,7 @@ def apply(self, value: Any): class IncludesWords(WordsPresence): - class Config: - type_alias = "evidently:feature:IncludesWords" + __type_alias__: ClassVar[Optional[str]] = "evidently:feature:IncludesWords" def __init__( self, @@ -112,8 +110,7 @@ def _feature_display_name(self): class ExcludesWords(WordsPresence): - class Config: - type_alias = "evidently:feature:ExcludesWords" + __type_alias__: ClassVar[Optional[str]] = "evidently:feature:ExcludesWords" def __init__( self, @@ -138,8 +135,7 @@ def _feature_display_name(self): class RowWordPresence(GeneratedFeature): - class Config: - type_alias = "evidently:feature:RowWordPresence" + __type_alias__: ClassVar[Optional[str]] = "evidently:feature:RowWordPresence" __feature_type__: ClassVar = ColumnType.Categorical columns: List[str] @@ -190,8 +186,7 @@ def _feature_name(self): class WordMatch(RowWordPresence): - class Config: - type_alias = "evidently:feature:WordMatch" + __type_alias__: ClassVar[Optional[str]] = "evidently:feature:WordMatch" def __init__(self, columns: List[str], mode: str, lemmatize: bool, display_name: Optional[str] = None): super().__init__(columns=columns, mode="includes_" + mode, lemmatize=lemmatize, display_name=display_name) @@ -204,8 +199,7 @@ def _as_column(self) -> "ColumnName": class WordNoMatch(RowWordPresence): - class Config: - type_alias = "evidently:feature:WordNoMatch" + __type_alias__: ClassVar[Optional[str]] = "evidently:feature:WordNoMatch" def __init__(self, columns: List[str], mode: str, lemmatize: bool, display_name: Optional[str] = None): super().__init__(columns=columns, mode="excludes_" + mode, lemmatize=lemmatize, display_name=display_name) diff --git a/src/evidently/legacy/metric_preset/classification_performance.py b/src/evidently/legacy/metric_preset/classification_performance.py index 3f393c7864..6cde0512f8 100644 --- a/src/evidently/legacy/metric_preset/classification_performance.py +++ b/src/evidently/legacy/metric_preset/classification_performance.py @@ -1,4 +1,5 @@ from typing import Any +from typing import ClassVar from typing import Dict from typing import List from typing import Optional @@ -19,8 +20,7 @@ class ClassificationPreset(MetricPreset): - class Config: - type_alias = "evidently:metric_preset:ClassificationPreset" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_preset:ClassificationPreset" """ Metrics preset for classification performance. diff --git a/src/evidently/legacy/metric_preset/data_drift.py b/src/evidently/legacy/metric_preset/data_drift.py index 02a9d16c76..fb7751955b 100644 --- a/src/evidently/legacy/metric_preset/data_drift.py +++ b/src/evidently/legacy/metric_preset/data_drift.py @@ -1,4 +1,5 @@ from typing import Any +from typing import ClassVar from typing import Dict from typing import List from typing import Optional @@ -15,8 +16,7 @@ class DataDriftPreset(MetricPreset): - class Config: - type_alias = "evidently:metric_preset:DataDriftPreset" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_preset:DataDriftPreset" """Metric Preset for Data Drift analysis. diff --git a/src/evidently/legacy/metric_preset/data_quality.py b/src/evidently/legacy/metric_preset/data_quality.py index 8889734a73..04864026cd 100644 --- a/src/evidently/legacy/metric_preset/data_quality.py +++ b/src/evidently/legacy/metric_preset/data_quality.py @@ -1,4 +1,5 @@ from typing import Any +from typing import ClassVar from typing import Dict from typing import List from typing import Optional @@ -13,8 +14,7 @@ class DataQualityPreset(MetricPreset): - class Config: - type_alias = "evidently:metric_preset:DataQualityPreset" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_preset:DataQualityPreset" """Metric preset for Data Quality analysis. diff --git a/src/evidently/legacy/metric_preset/metric_preset.py b/src/evidently/legacy/metric_preset/metric_preset.py index a667abc577..70f54ab365 100644 --- a/src/evidently/legacy/metric_preset/metric_preset.py +++ b/src/evidently/legacy/metric_preset/metric_preset.py @@ -1,5 +1,6 @@ import abc from typing import Any +from typing import ClassVar from typing import Dict from typing import List from typing import Optional @@ -16,8 +17,7 @@ class MetricPreset(BasePreset): """Base class for metric presets""" - class Config: - is_base_type = True + __is_base_type__: ClassVar[bool] = True @abc.abstractmethod def generate_metrics( diff --git a/src/evidently/legacy/metric_preset/recsys.py b/src/evidently/legacy/metric_preset/recsys.py index 04df77fa4e..6822df01e1 100644 --- a/src/evidently/legacy/metric_preset/recsys.py +++ b/src/evidently/legacy/metric_preset/recsys.py @@ -1,4 +1,5 @@ from typing import Any +from typing import ClassVar from typing import Dict from typing import List from typing import Optional @@ -27,8 +28,7 @@ class RecsysPreset(MetricPreset): - class Config: - type_alias = "evidently:metric_preset:RecsysPreset" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_preset:RecsysPreset" """Metric preset for recsys performance analysis. diff --git a/src/evidently/legacy/metric_preset/regression_performance.py b/src/evidently/legacy/metric_preset/regression_performance.py index 2a2348db43..592b9a8b92 100644 --- a/src/evidently/legacy/metric_preset/regression_performance.py +++ b/src/evidently/legacy/metric_preset/regression_performance.py @@ -1,4 +1,5 @@ from typing import Any +from typing import ClassVar from typing import Dict from typing import List from typing import Optional @@ -18,8 +19,7 @@ class RegressionPreset(MetricPreset): - class Config: - type_alias = "evidently:metric_preset:RegressionPreset" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_preset:RegressionPreset" """Metric preset for Regression performance analysis. diff --git a/src/evidently/legacy/metric_preset/target_drift.py b/src/evidently/legacy/metric_preset/target_drift.py index a5aae3b964..5d2f2b4376 100644 --- a/src/evidently/legacy/metric_preset/target_drift.py +++ b/src/evidently/legacy/metric_preset/target_drift.py @@ -1,4 +1,5 @@ from typing import Any +from typing import ClassVar from typing import Dict from typing import List from typing import Optional @@ -16,8 +17,7 @@ class TargetDriftPreset(MetricPreset): - class Config: - type_alias = "evidently:metric_preset:TargetDriftPreset" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_preset:TargetDriftPreset" """Metric preset for Target Drift analysis. diff --git a/src/evidently/legacy/metric_preset/text_evals.py b/src/evidently/legacy/metric_preset/text_evals.py index 0544c97711..c0fdda9a80 100644 --- a/src/evidently/legacy/metric_preset/text_evals.py +++ b/src/evidently/legacy/metric_preset/text_evals.py @@ -1,4 +1,5 @@ from typing import Any +from typing import ClassVar from typing import Dict from typing import List from typing import Optional @@ -16,8 +17,7 @@ class TextEvals(MetricPreset): - class Config: - type_alias = "evidently:metric_preset:TextEvals" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_preset:TextEvals" column_name: str descriptors: Optional[List[FeatureDescriptor]] = None diff --git a/src/evidently/legacy/metric_results.py b/src/evidently/legacy/metric_results.py index 5ba523879e..7c9921fb3b 100644 --- a/src/evidently/legacy/metric_results.py +++ b/src/evidently/legacy/metric_results.py @@ -1,3 +1,4 @@ +from typing import ClassVar from typing import Dict from typing import List from typing import Optional @@ -9,15 +10,15 @@ from typing import Union from typing import overload -import numpy as np import pandas as pd +from pydantic import TypeAdapter +from pydantic.v1 import validator from typing_extensions import Literal -from evidently._pydantic_compat import parse_obj_as -from evidently._pydantic_compat import validator from evidently.legacy.base_metric import MetricResult from evidently.legacy.core import IncludeTags from evidently.legacy.core import Label +from evidently.legacy.core import PydanticNPArray from evidently.legacy.core import pydantic_type_validator from evidently.legacy.pipeline.column_mapping import TargetNames @@ -47,7 +48,7 @@ def label_key_valudator(value): ScatterData = Union[pd.Series] -ContourData = Tuple[np.ndarray, List[float], List[float]] +ContourData = Tuple[PydanticNPArray, List[float], List[float]] ColumnScatter = Dict[LabelKey, ScatterData] ScatterAggData = Union[pd.DataFrame] @@ -65,44 +66,37 @@ class _ColumnScatterOrAggType: def column_scatter_valudator(value): if any(isinstance(o, dict) for o in value.values()): # dict -> dataframe -> agg - return parse_obj_as(ColumnAggScatter, value) + return TypeAdapter(ColumnAggScatter).validate_python(value) if any(isinstance(o, (pd.DataFrame, pd.Series)) for o in value.values()): return value - return parse_obj_as(ColumnScatter, value) + return TypeAdapter(ColumnScatter).validate_python(value) class Distribution(MetricResult): - class Config: - type_alias = "evidently:metric_result:Distribution" - pd_include = False - tags = {IncludeTags.Render} - smart_union = True - extract_as_obj = True + __pd_include__: ClassVar[bool] = False + __tags__: ClassVar[Set[IncludeTags]] = {IncludeTags.Render} + __extract_as_obj__: ClassVar[bool] = True + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:Distribution" - x: Union[np.ndarray, list, pd.Categorical, pd.Series] - y: Union[np.ndarray, list, pd.Categorical, pd.Series] + x: Union[PydanticNPArray, list, pd.Categorical, pd.Series] + y: Union[PydanticNPArray, list, pd.Categorical, pd.Series] class ConfusionMatrix(MetricResult): - class Config: - type_alias = "evidently:metric_result:ConfusionMatrix" - smart_union = True - - field_tags = {"labels": {IncludeTags.Parameter}} + __field_tags__: ClassVar[Dict[str, set]] = {"labels": {IncludeTags.Parameter}} + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:ConfusionMatrix" labels: Sequence[Label] values: list # todo better typing class PredictionData(MetricResult): - class Config: - type_alias = "evidently:metric_result:PredictionData" - dict_include = False - smart_union = True + __dict_include__: ClassVar[bool] = False + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:PredictionData" predictions: pd.Series labels: LabelList - prediction_probas: Optional[pd.DataFrame] + prediction_probas: Optional[pd.DataFrame] = None @validator("prediction_probas") def validate_prediction_probas(cls, value: pd.DataFrame, values): @@ -125,42 +119,38 @@ def validate_prediction_probas(cls, value: pd.DataFrame, values): class StatsByFeature(MetricResult): - class Config: - type_alias = "evidently:metric_result:StatsByFeature" - dict_include = False - pd_include = False - tags = {IncludeTags.Render} + __dict_include__: ClassVar[bool] = False + __pd_include__: ClassVar[bool] = False + __tags__: ClassVar[Set[IncludeTags]] = {IncludeTags.Render} + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:StatsByFeature" plot_data: pd.DataFrame # todo what type of plot? - predictions: Optional[PredictionData] + predictions: Optional[PredictionData] = None class DatasetUtilityColumns(MetricResult): - class Config: - type_alias = "evidently:metric_result:DatasetUtilityColumns" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:DatasetUtilityColumns" - date: Optional[str] - id: Optional[str] - target: Optional[str] - prediction: Optional[Union[str, Sequence[str]]] + date: Optional[str] = None + id: Optional[str] = None + target: Optional[str] = None + prediction: Optional[Union[str, Sequence[str]]] = None class DatasetColumns(MetricResult): - class Config: - type_alias = "evidently:metric_result:DatasetColumns" - dict_exclude_fields = {"task", "target_type"} - pd_include = False - tags = {IncludeTags.Parameter} - smart_union = True + __dict_exclude_fields__: ClassVar[set] = {"task", "target_type"} + __pd_include__: ClassVar[bool] = False + __tags__: ClassVar[Set[IncludeTags]] = {IncludeTags.Parameter} + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:DatasetColumns" utility_columns: DatasetUtilityColumns - target_type: Optional[str] + target_type: Optional[str] = None num_feature_names: List[str] cat_feature_names: List[str] text_feature_names: List[str] datetime_feature_names: List[str] - target_names: Optional[TargetNames] - task: Optional[str] + target_names: Optional[TargetNames] = None + task: Optional[str] = None @property def target_names_list(self) -> Optional[List]: @@ -244,13 +234,10 @@ def column_scatter_from_df(df: Optional[pd.DataFrame], with_index: bool) -> Opti class ScatterAggField(MetricResult): - class Config: - type_alias = "evidently:metric_result:ScatterAggField" - smart_union = True - dict_include = False - pd_include = False - - tags = {IncludeTags.Render} + __dict_include__: ClassVar[bool] = False + __pd_include__: ClassVar[bool] = False + __tags__: ClassVar[Set[IncludeTags]] = {IncludeTags.Render} + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:ScatterAggField" scatter: ColumnAggScatter x_name: str @@ -258,13 +245,10 @@ class Config: class ScatterField(MetricResult): - class Config: - type_alias = "evidently:metric_result:ScatterField" - smart_union = True - dict_include = False - pd_include = False - - tags = {IncludeTags.Render} + __dict_include__: ClassVar[bool] = False + __pd_include__: ClassVar[bool] = False + __tags__: ClassVar[Set[IncludeTags]] = {IncludeTags.Render} + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:ScatterField" scatter: ColumnScatter x_name: str @@ -272,39 +256,34 @@ class Config: class ColumnScatterResult(MetricResult): - class Config: - type_alias = "evidently:metric_result:ColumnScatterResult" - smart_union = True - dict_include = False - pd_include = False - - tags = {IncludeTags.Render} - field_tags = {"current": {IncludeTags.Current}, "reference": {IncludeTags.Reference}} + __dict_include__: ClassVar[bool] = False + __pd_include__: ClassVar[bool] = False + __tags__: ClassVar[Set[IncludeTags]] = {IncludeTags.Render} + __field_tags__: ClassVar[Dict[str, set]] = {"current": {IncludeTags.Current}, "reference": {IncludeTags.Reference}} + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:ColumnScatterResult" current: ColumnScatter - reference: Optional[ColumnScatter] + reference: Optional[ColumnScatter] = None x_name: str x_name_ref: Optional[str] = None class ColumnAggScatterResult(ColumnScatterResult): - class Config: - type_alias = "evidently:metric_result:ColumnAggScatterResult" - field_tags = {"current": {IncludeTags.Current}, "reference": {IncludeTags.Reference}} + __field_tags__: ClassVar[Dict[str, set]] = {"current": {IncludeTags.Current}, "reference": {IncludeTags.Reference}} + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:ColumnAggScatterResult" # TODO: fix type collision with super type current: ColumnAggScatter # type: ignore[assignment] - reference: Optional[ColumnAggScatter] # type: ignore[assignment] + reference: Optional[ColumnAggScatter] = None # type: ignore[assignment] PlotData = List[float] class Boxes(MetricResult): - class Config: - type_alias = "evidently:metric_result:Boxes" - dict_include = False - tags = {IncludeTags.Render} + __dict_include__: ClassVar[bool] = False + __tags__: ClassVar[Set[IncludeTags]] = {IncludeTags.Render} + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:Boxes" mins: PlotData lowers: PlotData @@ -314,10 +293,9 @@ class Config: class RatesPlotData(MetricResult): - class Config: - type_alias = "evidently:metric_result:RatesPlotData" - dict_include = False - tags = {IncludeTags.Render} + __dict_include__: ClassVar[bool] = False + __tags__: ClassVar[Set[IncludeTags]] = {IncludeTags.Render} + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:RatesPlotData" thrs: PlotData tpr: PlotData @@ -327,10 +305,9 @@ class Config: class PRCurveData(MetricResult): - class Config: - type_alias = "evidently:metric_result:PRCurveData" - dict_include = False - tags = {IncludeTags.Render} + __dict_include__: ClassVar[bool] = False + __tags__: ClassVar[Set[IncludeTags]] = {IncludeTags.Render} + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:PRCurveData" pr: PlotData rcl: PlotData @@ -341,10 +318,9 @@ class Config: class ROCCurveData(MetricResult): - class Config: - type_alias = "evidently:metric_result:ROCCurveData" - dict_include = False - tags = {IncludeTags.Render} + __dict_include__: ClassVar[bool] = False + __tags__: ClassVar[Set[IncludeTags]] = {IncludeTags.Render} + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:ROCCurveData" fpr: PlotData tpr: PlotData @@ -355,10 +331,9 @@ class Config: class LiftCurveData(MetricResult): - class Config: - type_alias = "evidently:metric_result:LiftCurveData" - dict_include = False - tags = {IncludeTags.Render} + __dict_include__: ClassVar[bool] = False + __tags__: ClassVar[Set[IncludeTags]] = {IncludeTags.Render} + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:LiftCurveData" lift: PlotData top: PlotData @@ -378,11 +353,10 @@ class Config: class HistogramData(MetricResult): - class Config: - type_alias = "evidently:metric_result:HistogramData" - dict_include = False - tags = {IncludeTags.Render} - extract_as_obj = True + __dict_include__: ClassVar[bool] = False + __tags__: ClassVar[Set[IncludeTags]] = {IncludeTags.Render} + __extract_as_obj__: ClassVar[bool] = True + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:HistogramData" x: pd.Series count: pd.Series @@ -421,23 +395,22 @@ def from_any( raise NotImplementedError(f"Cannot create {cls.__name__} from {value.__class__.__name__}") def to_df(self): - return pd.DataFrame.from_dict(self.dict(include={"x", "count"})) + return pd.DataFrame.from_dict(self.model_dump(include={"x", "count"})) class Histogram(MetricResult): - class Config: - type_alias = "evidently:metric_result:Histogram" - dict_include = False - tags = {IncludeTags.Render} - field_tags = { - "current": {IncludeTags.Current}, - "reference": {IncludeTags.Reference}, - "current_log": {IncludeTags.Current}, - "reference_log": {IncludeTags.Reference}, - } + __dict_include__: ClassVar[bool] = False + __tags__: ClassVar[Set[IncludeTags]] = {IncludeTags.Render} + __field_tags__: ClassVar[Dict[str, set]] = { + "current": {IncludeTags.Current}, + "reference": {IncludeTags.Reference}, + "current_log": {IncludeTags.Current}, + "reference_log": {IncludeTags.Reference}, + } + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:Histogram" current: HistogramData - reference: Optional[HistogramData] + reference: Optional[HistogramData] = None current_log: Optional[HistogramData] = None reference_log: Optional[HistogramData] = None @@ -445,17 +418,15 @@ class Config: # todo need better config overriding logic in metricresult class DistributionIncluded(Distribution): - class Config: - type_alias = "evidently:metric_result:DistributionIncluded" - tags: Set[IncludeTags] = set() - dict_include = True - field_tags = {"x": {IncludeTags.Extra}} + __tags__: ClassVar[Set[IncludeTags]] = set() + __dict_include__: ClassVar[bool] = True + __field_tags__: ClassVar[Dict[str, set]] = {"x": {IncludeTags.Extra}} + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:DistributionIncluded" class ColumnCorrelations(MetricResult): - class Config: - type_alias = "evidently:metric_result:ColumnCorrelations" - field_tags = {"column_name": {IncludeTags.Parameter}, "kind": {IncludeTags.Parameter}} + __field_tags__: ClassVar[Dict[str, set]] = {"column_name": {IncludeTags.Parameter}, "kind": {IncludeTags.Parameter}} + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:ColumnCorrelations" column_name: str kind: str @@ -471,8 +442,7 @@ def get_pandas(self) -> pd.DataFrame: class DatasetClassificationQuality(MetricResult): - class Config: - type_alias = "evidently:metric_result:DatasetClassificationQuality" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:DatasetClassificationQuality" accuracy: float precision: float diff --git a/src/evidently/legacy/metrics/classification_performance/base_classification_metric.py b/src/evidently/legacy/metrics/classification_performance/base_classification_metric.py index efaf3a6f2a..d0ccb19c83 100644 --- a/src/evidently/legacy/metrics/classification_performance/base_classification_metric.py +++ b/src/evidently/legacy/metrics/classification_performance/base_classification_metric.py @@ -33,8 +33,8 @@ def _cleanup_data(data: pd.DataFrame, dataset_columns: DatasetColumns) -> pd.Dat class ThresholdClassificationMetric(Metric[TResult], Generic[TResult], ABC): - probas_threshold: Optional[float] - k: Optional[int] + probas_threshold: Optional[float] = None + k: Optional[int] = None def __init__(self, probas_threshold: Optional[float], k: Optional[int], options: AnyOptions = None): if probas_threshold is not None and k is not None: diff --git a/src/evidently/legacy/metrics/classification_performance/class_balance_metric.py b/src/evidently/legacy/metrics/classification_performance/class_balance_metric.py index f96de2488b..3951918429 100644 --- a/src/evidently/legacy/metrics/classification_performance/class_balance_metric.py +++ b/src/evidently/legacy/metrics/classification_performance/class_balance_metric.py @@ -1,3 +1,4 @@ +from typing import ClassVar from typing import Dict from typing import List from typing import Optional @@ -17,17 +18,15 @@ class ClassificationClassBalanceResult(MetricResult): - class Config: - type_alias = "evidently:metric_result:ClassificationClassBalanceResult" - dict_exclude_fields = {"plot_data"} - pd_exclude_fields = {"plot_data"} + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:ClassificationClassBalanceResult" + __dict_exclude_fields__: ClassVar[set] = {"plot_data"} + __pd_exclude_fields__: ClassVar[set] = {"plot_data"} plot_data: Histogram class ClassificationClassBalance(Metric[ClassificationClassBalanceResult]): - class Config: - type_alias = "evidently:metric:ClassificationClassBalance" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:ClassificationClassBalance" def calculate(self, data: InputData) -> ClassificationClassBalanceResult: dataset_columns = process_columns(data.current_data, data.column_mapping) diff --git a/src/evidently/legacy/metrics/classification_performance/class_separation_metric.py b/src/evidently/legacy/metrics/classification_performance/class_separation_metric.py index ae2969ae11..e134504f72 100644 --- a/src/evidently/legacy/metrics/classification_performance/class_separation_metric.py +++ b/src/evidently/legacy/metrics/classification_performance/class_separation_metric.py @@ -1,3 +1,5 @@ +from typing import ClassVar +from typing import Dict from typing import List from typing import Optional @@ -29,15 +31,14 @@ class ClassificationClassSeparationPlotResults(MetricResult): - class Config: - type_alias = "evidently:metric_result:ClassificationClassSeparationPlotResults" - dict_exclude_fields = {"current", "reference"} - pd_exclude_fields = {"current", "reference"} - field_tags = { - "current": {IncludeTags.Current, IncludeTags.Extra}, - "reference": {IncludeTags.Reference, IncludeTags.Extra}, - "target_name": {IncludeTags.Parameter}, - } + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:ClassificationClassSeparationPlotResults" + __dict_exclude_fields__: ClassVar[set] = {"current", "reference"} + __pd_exclude_fields__: ClassVar[set] = {"current", "reference"} + __field_tags__: ClassVar[Dict[str, set]] = { + "current": {IncludeTags.Current, IncludeTags.Extra}, + "reference": {IncludeTags.Reference, IncludeTags.Extra}, + "target_name": {IncludeTags.Parameter}, + } target_name: str @@ -73,8 +74,7 @@ def _quantiles(qdf, value): class ClassificationClassSeparationPlot(UsesRawDataMixin, Metric[ClassificationClassSeparationPlotResults]): - class Config: - type_alias = "evidently:metric:ClassificationClassSeparationPlot" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:ClassificationClassSeparationPlot" def __init__(self, options: AnyOptions = None): super().__init__(options=options) diff --git a/src/evidently/legacy/metrics/classification_performance/classification_dummy_metric.py b/src/evidently/legacy/metrics/classification_performance/classification_dummy_metric.py index a6d642dd75..f2ea382239 100644 --- a/src/evidently/legacy/metrics/classification_performance/classification_dummy_metric.py +++ b/src/evidently/legacy/metrics/classification_performance/classification_dummy_metric.py @@ -1,3 +1,5 @@ +from typing import ClassVar +from typing import Dict from typing import List from typing import Optional @@ -32,12 +34,13 @@ class ClassificationDummyMetricResults(MetricResult): - class Config: - type_alias = "evidently:metric_result:ClassificationDummyMetricResults" - dict_exclude_fields = {"metrics_matrix"} - pd_exclude_fields = {"metrics_matrix"} - - field_tags = {"by_reference_dummy": {IncludeTags.Reference}, "metrics_matrix": {IncludeTags.Extra}} + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:ClassificationDummyMetricResults" + __dict_exclude_fields__: ClassVar[set] = {"metrics_matrix"} + __pd_exclude_fields__: ClassVar[set] = {"metrics_matrix"} + __field_tags__: ClassVar[Dict[str, set]] = { + "by_reference_dummy": {IncludeTags.Reference}, + "metrics_matrix": {IncludeTags.Extra}, + } dummy: DatasetClassificationQuality by_reference_dummy: Optional[DatasetClassificationQuality] @@ -46,8 +49,7 @@ class Config: class ClassificationDummyMetric(ThresholdClassificationMetric[ClassificationDummyMetricResults]): - class Config: - type_alias = "evidently:metric:ClassificationDummyMetric" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:ClassificationDummyMetric" _quality_metric: ClassificationQualityMetric diff --git a/src/evidently/legacy/metrics/classification_performance/classification_quality_metric.py b/src/evidently/legacy/metrics/classification_performance/classification_quality_metric.py index b48ef714d2..5242fa95f2 100644 --- a/src/evidently/legacy/metrics/classification_performance/classification_quality_metric.py +++ b/src/evidently/legacy/metrics/classification_performance/classification_quality_metric.py @@ -1,3 +1,5 @@ +from typing import ClassVar +from typing import Dict from typing import List from typing import Optional @@ -19,22 +21,20 @@ class ClassificationQualityMetricResult(MetricResult): - class Config: - type_alias = "evidently:metric_result:ClassificationQualityMetricResult" - field_tags = { - "current": {IncludeTags.Current}, - "reference": {IncludeTags.Reference}, - "target_name": {IncludeTags.Parameter}, - } + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:ClassificationQualityMetricResult" + __field_tags__: ClassVar[Dict[str, set]] = { + "current": {IncludeTags.Current}, + "reference": {IncludeTags.Reference}, + "target_name": {IncludeTags.Parameter}, + } current: DatasetClassificationQuality - reference: Optional[DatasetClassificationQuality] + reference: Optional[DatasetClassificationQuality] = None target_name: str class ClassificationQualityMetric(ThresholdClassificationMetric[ClassificationQualityMetricResult]): - class Config: - type_alias = "evidently:metric:ClassificationQualityMetric" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:ClassificationQualityMetric" _confusion_matrix_metric: ClassificationConfusionMatrix @@ -44,8 +44,8 @@ def __init__( k: Optional[int] = None, options: AnyOptions = None, ): - self._confusion_matrix_metric = ClassificationConfusionMatrix(probas_threshold=probas_threshold, k=k) super().__init__(probas_threshold=probas_threshold, k=k, options=options) + self._confusion_matrix_metric = ClassificationConfusionMatrix(probas_threshold=probas_threshold, k=k) def calculate(self, data: InputData) -> ClassificationQualityMetricResult: dataset_columns = process_columns(data.current_data, data.column_mapping) diff --git a/src/evidently/legacy/metrics/classification_performance/confusion_matrix_metric.py b/src/evidently/legacy/metrics/classification_performance/confusion_matrix_metric.py index d954918db0..ebd6c0aa3c 100644 --- a/src/evidently/legacy/metrics/classification_performance/confusion_matrix_metric.py +++ b/src/evidently/legacy/metrics/classification_performance/confusion_matrix_metric.py @@ -1,8 +1,10 @@ +from typing import ClassVar from typing import Dict from typing import List from typing import Optional -from evidently._pydantic_compat import BaseModel +from pydantic import BaseModel + from evidently.legacy.base_metric import InputData from evidently.legacy.base_metric import MetricResult from evidently.legacy.calculations.classification_performance import calculate_matrix @@ -23,17 +25,15 @@ class ClassificationConfusionMatrixResult(MetricResult): - class Config: - type_alias = "evidently:metric_result:ClassificationConfusionMatrixResult" - field_tags = { - "current_matrix": {IncludeTags.Current}, - "reference_matrix": {IncludeTags.Reference}, - "target_names": {IncludeTags.Parameter}, - } - smart_union = True + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:ClassificationConfusionMatrixResult" + __field_tags__: ClassVar[Dict[str, set]] = { + "current_matrix": {IncludeTags.Current}, + "reference_matrix": {IncludeTags.Reference}, + "target_names": {IncludeTags.Parameter}, + } current_matrix: ConfusionMatrix - reference_matrix: Optional[ConfusionMatrix] + reference_matrix: Optional[ConfusionMatrix] = None target_names: Optional[TargetNames] = None @@ -48,8 +48,7 @@ def confusion_matric_metric(self): class ClassificationConfusionMatrix( ThresholdClassificationMetric[ClassificationConfusionMatrixResult], ClassificationConfusionMatrixParameters ): - class Config: - type_alias = "evidently:metric:ClassificationConfusionMatrix" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:ClassificationConfusionMatrix" def __init__( self, diff --git a/src/evidently/legacy/metrics/classification_performance/lift_curve_metric.py b/src/evidently/legacy/metrics/classification_performance/lift_curve_metric.py index ab15c1bb77..d7408dabdd 100644 --- a/src/evidently/legacy/metrics/classification_performance/lift_curve_metric.py +++ b/src/evidently/legacy/metrics/classification_performance/lift_curve_metric.py @@ -1,4 +1,5 @@ from typing import Any +from typing import ClassVar from typing import Dict from typing import List from typing import Optional @@ -26,19 +27,19 @@ class ClassificationLiftCurveResults(MetricResult): - class Config: - type_alias = "evidently:metric_result:ClassificationLiftCurveResults" - pd_include = False - - field_tags = {"current_lift_curve": {IncludeTags.Current}, "reference_lift_curve": {IncludeTags.Reference}} + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:ClassificationLiftCurveResults" + __pd_include__: ClassVar[bool] = False + __field_tags__: ClassVar[Dict[str, set]] = { + "current_lift_curve": {IncludeTags.Current}, + "reference_lift_curve": {IncludeTags.Reference}, + } current_lift_curve: Optional[LiftCurve] = None reference_lift_curve: Optional[LiftCurve] = None class ClassificationLiftCurve(Metric[ClassificationLiftCurveResults]): - class Config: - type_alias = "evidently:metric:ClassificationLiftCurve" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:ClassificationLiftCurve" def calculate(self, data: InputData) -> ClassificationLiftCurveResults: dataset_columns = process_columns(data.current_data, data.column_mapping) diff --git a/src/evidently/legacy/metrics/classification_performance/lift_table_metric.py b/src/evidently/legacy/metrics/classification_performance/lift_table_metric.py index 4f8aa70c0a..4343cca1a4 100644 --- a/src/evidently/legacy/metrics/classification_performance/lift_table_metric.py +++ b/src/evidently/legacy/metrics/classification_performance/lift_table_metric.py @@ -1,21 +1,18 @@ -from typing import TYPE_CHECKING -from typing import Any +from typing import ClassVar from typing import Dict from typing import List from typing import Optional -from typing import Type from typing import Union import pandas as pd -from evidently._pydantic_compat import BaseModel from evidently.legacy.base_metric import InputData from evidently.legacy.base_metric import Metric from evidently.legacy.base_metric import MetricResult from evidently.legacy.calculations.classification_performance import calculate_lift_table from evidently.legacy.calculations.classification_performance import get_prediction_data from evidently.legacy.core import IncludeTags -from evidently.legacy.metric_results import Label +from evidently.legacy.core import LabelIntStr from evidently.legacy.metric_results import PredictionData from evidently.legacy.model.widget import BaseWidgetInfo from evidently.legacy.options.base import AnyOptions @@ -27,32 +24,17 @@ from evidently.legacy.renderers.html_widgets import widget_tabs from evidently.legacy.utils.data_operations import process_columns -if TYPE_CHECKING: - from evidently._pydantic_compat import Model - - -class LabelModel(BaseModel): - __root__: Union[int, str] - - def validate(cls: Type["Model"], value: Any): # type: ignore[override, misc] - try: - return int(value) - except TypeError: - return value - - -LiftTable = Dict[Union[LabelModel, Label], List[List[Union[float, int]]]] +LiftTable = Dict[LabelIntStr, List[List[Union[float, int]]]] class ClassificationLiftTableResults(MetricResult): - class Config: - type_alias = "evidently:metric_result:ClassificationLiftTableResults" - pd_include = False - field_tags = { - "current_lift_table": {IncludeTags.Current}, - "reference_lift_table": {IncludeTags.Reference}, - "top": {IncludeTags.Parameter}, - } + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:ClassificationLiftTableResults" + __pd_include__: ClassVar[bool] = False + __field_tags__: ClassVar[Dict[str, set]] = { + "current_lift_table": {IncludeTags.Current}, + "reference_lift_table": {IncludeTags.Reference}, + "top": {IncludeTags.Parameter}, + } current_lift_table: Optional[LiftTable] = None reference_lift_table: Optional[LiftTable] = None @@ -60,8 +42,7 @@ class Config: class ClassificationLiftTable(Metric[ClassificationLiftTableResults]): - class Config: - type_alias = "evidently:metric:ClassificationLiftTable" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:ClassificationLiftTable" """ Evidently metric with inherited behaviour, provides data for lift analysis diff --git a/src/evidently/legacy/metrics/classification_performance/objects.py b/src/evidently/legacy/metrics/classification_performance/objects.py index 1639dca3ad..f229a68ed4 100644 --- a/src/evidently/legacy/metrics/classification_performance/objects.py +++ b/src/evidently/legacy/metrics/classification_performance/objects.py @@ -1,17 +1,17 @@ +from typing import ClassVar from typing import Dict from typing import Optional +from pydantic import Field +from pydantic import TypeAdapter from sklearn.metrics import classification_report -from evidently._pydantic_compat import Field -from evidently._pydantic_compat import parse_obj_as from evidently.legacy.base_metric import MetricResult from evidently.legacy.metric_results import Label class ClassMetric(MetricResult): - class Config: - type_alias = "evidently:metric_result:ClassMetric" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:ClassMetric" precision: float recall: float @@ -24,9 +24,7 @@ class Config: class ClassificationReport(MetricResult): - class Config: - type_alias = "evidently:metric_result:ClassificationReport" - smart_union = True + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:ClassificationReport" classes: ClassesMetrics accuracy: float @@ -60,6 +58,6 @@ def create( if not isinstance(v, dict): continue v["f1"] = v.pop("f1-score") - class_metrics = {str(k): parse_obj_as(ClassMetric, report[str(k)]) for k in classes} + class_metrics = {str(k): TypeAdapter(ClassMetric).validate_python(report[str(k)]) for k in classes} other = {str(k): v for k, v in report.items() if k not in [str(cl) for cl in classes]} - return parse_obj_as(cls, {"classes": class_metrics, **other}) + return TypeAdapter(cls).validate_python({"classes": class_metrics, **other}) diff --git a/src/evidently/legacy/metrics/classification_performance/pr_curve_metric.py b/src/evidently/legacy/metrics/classification_performance/pr_curve_metric.py index 9f4320e08e..cfcc7b10ea 100644 --- a/src/evidently/legacy/metrics/classification_performance/pr_curve_metric.py +++ b/src/evidently/legacy/metrics/classification_performance/pr_curve_metric.py @@ -1,3 +1,5 @@ +from typing import ClassVar +from typing import Dict from typing import List from typing import Optional @@ -26,19 +28,19 @@ class ClassificationPRCurveResults(MetricResult): - class Config: - type_alias = "evidently:metric_result:ClassificationPRCurveResults" - pd_include = False - - field_tags = {"current_pr_curve": {IncludeTags.Current}, "reference_pr_curve": {IncludeTags.Reference}} + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:ClassificationPRCurveResults" + __pd_include__: ClassVar[bool] = False + __field_tags__: ClassVar[Dict[str, set]] = { + "current_pr_curve": {IncludeTags.Current}, + "reference_pr_curve": {IncludeTags.Reference}, + } current_pr_curve: Optional[PRCurve] = None reference_pr_curve: Optional[PRCurve] = None class ClassificationPRCurve(Metric[ClassificationPRCurveResults]): - class Config: - type_alias = "evidently:metric:ClassificationPRCurve" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:ClassificationPRCurve" def calculate(self, data: InputData) -> ClassificationPRCurveResults: dataset_columns = process_columns(data.current_data, data.column_mapping) diff --git a/src/evidently/legacy/metrics/classification_performance/pr_table_metric.py b/src/evidently/legacy/metrics/classification_performance/pr_table_metric.py index 9f56665440..f0bd3c90d4 100644 --- a/src/evidently/legacy/metrics/classification_performance/pr_table_metric.py +++ b/src/evidently/legacy/metrics/classification_performance/pr_table_metric.py @@ -1,21 +1,18 @@ -from typing import TYPE_CHECKING -from typing import Any +from typing import ClassVar from typing import Dict from typing import List from typing import Optional -from typing import Type from typing import Union import pandas as pd -from evidently._pydantic_compat import BaseModel from evidently.legacy.base_metric import InputData from evidently.legacy.base_metric import Metric from evidently.legacy.base_metric import MetricResult from evidently.legacy.calculations.classification_performance import calculate_pr_table from evidently.legacy.calculations.classification_performance import get_prediction_data from evidently.legacy.core import IncludeTags -from evidently.legacy.metric_results import Label +from evidently.legacy.core import LabelIntStr from evidently.legacy.metric_results import PredictionData from evidently.legacy.model.widget import BaseWidgetInfo from evidently.legacy.renderers.base_renderer import MetricRenderer @@ -26,36 +23,20 @@ from evidently.legacy.renderers.html_widgets import widget_tabs from evidently.legacy.utils.data_operations import process_columns -if TYPE_CHECKING: - from evidently._pydantic_compat import Model - - -class LabelModel(BaseModel): - __root__: Union[int, str] - - def validate(cls: Type["Model"], value: Any): # type: ignore[override, misc] - try: - return int(value) - except TypeError: - return value - - -PRTable = Dict[Union[LabelModel, Label], List[List[Union[float, int]]]] +PRTable = Dict[LabelIntStr, List[List[Union[float, int]]]] class ClassificationPRTableResults(MetricResult): - class Config: - type_alias = "evidently:metric_result:ClassificationPRTableResults" - pd_include = False - field_tags = {"current": {IncludeTags.Current}, "reference": {IncludeTags.Reference}} + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:ClassificationPRTableResults" + __pd_include__: ClassVar[bool] = False + __field_tags__: ClassVar[Dict[str, set]] = {"current": {IncludeTags.Current}, "reference": {IncludeTags.Reference}} current: Optional[PRTable] = None reference: Optional[PRTable] = None class ClassificationPRTable(Metric[ClassificationPRTableResults]): - class Config: - type_alias = "evidently:metric:ClassificationPRTable" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:ClassificationPRTable" def calculate(self, data: InputData) -> ClassificationPRTableResults: dataset_columns = process_columns(data.current_data, data.column_mapping) diff --git a/src/evidently/legacy/metrics/classification_performance/probability_distribution_metric.py b/src/evidently/legacy/metrics/classification_performance/probability_distribution_metric.py index e60c17ec92..05226206c8 100644 --- a/src/evidently/legacy/metrics/classification_performance/probability_distribution_metric.py +++ b/src/evidently/legacy/metrics/classification_performance/probability_distribution_metric.py @@ -1,7 +1,10 @@ +from typing import Annotated +from typing import ClassVar from typing import Dict from typing import Iterable from typing import List from typing import Optional +from typing import Set import numpy as np import pandas as pd @@ -19,24 +22,25 @@ from evidently.legacy.renderers.html_widgets import WidgetSize from evidently.legacy.renderers.html_widgets import plotly_graph_tabs from evidently.legacy.utils.data_operations import process_columns +from evidently.pydantic_utils import StrKeyValidator class ClassificationProbDistributionResults(MetricResult): - class Config: - type_alias = "evidently:metric_result:ClassificationProbDistributionResults" - dict_include = False - pd_include = False - tags = {IncludeTags.Render} + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:ClassificationProbDistributionResults" + __dict_include__: ClassVar[bool] = False + __pd_include__: ClassVar[bool] = False + __tags__: ClassVar[Set[IncludeTags]] = {IncludeTags.Render} + __field_tags__: ClassVar[Dict[str, set]] = { + "current_distribution": {IncludeTags.Current}, + "reference_distribution": {IncludeTags.Reference}, + } - field_tags = {"current_distribution": {IncludeTags.Current}, "reference_distribution": {IncludeTags.Reference}} - - current_distribution: Optional[Dict[str, list]] # todo use DistributionField? - reference_distribution: Optional[Dict[str, list]] + current_distribution: Annotated[Optional[Dict[str, list]], StrKeyValidator] # todo use DistributionField? + reference_distribution: Annotated[Optional[Dict[str, list]], StrKeyValidator] class ClassificationProbDistribution(Metric[ClassificationProbDistributionResults]): - class Config: - type_alias = "evidently:metric:ClassificationProbDistribution" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:ClassificationProbDistribution" @staticmethod def get_distribution(dataset: pd.DataFrame, target_name: str, prediction_labels: Iterable) -> Dict[str, list]: diff --git a/src/evidently/legacy/metrics/classification_performance/quality_by_class_metric.py b/src/evidently/legacy/metrics/classification_performance/quality_by_class_metric.py index 34c55b1c4b..2f18db3ac0 100644 --- a/src/evidently/legacy/metrics/classification_performance/quality_by_class_metric.py +++ b/src/evidently/legacy/metrics/classification_performance/quality_by_class_metric.py @@ -1,4 +1,6 @@ from typing import Any +from typing import ClassVar +from typing import Dict from typing import List from typing import Optional @@ -11,7 +13,6 @@ from evidently.legacy.base_metric import InputData from evidently.legacy.base_metric import MetricResult -from evidently.legacy.core import AllDict from evidently.legacy.core import IncludeTags from evidently.legacy.metric_results import DatasetColumns from evidently.legacy.metrics.classification_performance.base_classification_metric import ThresholdClassificationMetric @@ -28,27 +29,24 @@ class ClassificationQuality(MetricResult): - class Config: - type_alias = "evidently:metric_result:ClassificationQuality" - smart_union = True + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:ClassificationQuality" metrics: ClassesMetrics roc_aucs: Optional[List[float]] @property def metrics_dict(self): - return self.dict(include={"metrics"}, exclude={"metrics": AllDict({"type"})})["metrics"] + return self.model_dump(include={"metrics"}, exclude={"metrics": {"__all__": {"type"}}})["metrics"] + # return self.dict(include={"metrics"}, exclude={"metrics": AllDict({"type"})})["metrics"] class ClassificationQualityByClassResult(MetricResult): - class Config: - type_alias = "evidently:metric_result:ClassificationQualityByClassResult" - field_tags = { - "current": {IncludeTags.Current}, - "reference": {IncludeTags.Reference}, - "columns": {IncludeTags.Parameter}, - } - smart_union = True + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:ClassificationQualityByClassResult" + __field_tags__: ClassVar[Dict[str, set]] = { + "current": {IncludeTags.Current}, + "reference": {IncludeTags.Reference}, + "columns": {IncludeTags.Parameter}, + } columns: DatasetColumns current: ClassificationQuality @@ -69,8 +67,7 @@ def get_pandas(self) -> pd.DataFrame: class ClassificationQualityByClass(ThresholdClassificationMetric[ClassificationQualityByClassResult]): - class Config: - type_alias = "evidently:metric:ClassificationQualityByClass" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:ClassificationQualityByClass" def __init__( self, diff --git a/src/evidently/legacy/metrics/classification_performance/quality_by_feature_table.py b/src/evidently/legacy/metrics/classification_performance/quality_by_feature_table.py index a57a63ac67..0ded6910f3 100644 --- a/src/evidently/legacy/metrics/classification_performance/quality_by_feature_table.py +++ b/src/evidently/legacy/metrics/classification_performance/quality_by_feature_table.py @@ -1,4 +1,5 @@ import json +from typing import ClassVar from typing import Dict from typing import List from typing import Optional @@ -33,14 +34,13 @@ class ClassificationQualityByFeatureTableResults(MetricResult): - class Config: - type_alias = "evidently:metric_result:ClassificationQualityByFeatureTableResults" - field_tags = { - "current": {IncludeTags.Current}, - "reference": {IncludeTags.Reference}, - "target_name": {IncludeTags.Parameter}, - "columns": {IncludeTags.Parameter}, - } + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:ClassificationQualityByFeatureTableResults" + __field_tags__: ClassVar[Dict[str, set]] = { + "current": {IncludeTags.Current}, + "reference": {IncludeTags.Reference}, + "target_name": {IncludeTags.Parameter}, + "columns": {IncludeTags.Parameter}, + } current: StatsByFeature reference: Optional[StatsByFeature] @@ -50,11 +50,10 @@ class Config: class ClassificationQualityByFeatureTable(UsesRawDataMixin, Metric[ClassificationQualityByFeatureTableResults]): - class Config: - type_alias = "evidently:metric:ClassificationQualityByFeatureTable" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:ClassificationQualityByFeatureTable" - columns: Optional[List[str]] - descriptors: Optional[Dict[str, Dict[str, FeatureDescriptor]]] + columns: Optional[List[str]] = None + descriptors: Optional[Dict[str, Dict[str, FeatureDescriptor]]] = None _text_features_gen: Optional[Dict[str, Dict[str, GeneratedFeature]]] def __init__( @@ -64,9 +63,9 @@ def __init__( options: AnyOptions = None, ): self.columns = columns - self._text_features_gen = None self.descriptors = descriptors super().__init__(options=options) + self._text_features_gen = None def required_features(self, data_definition: DataDefinition): if len(data_definition.get_columns(ColumnType.Text, features_only=True)) > 0: diff --git a/src/evidently/legacy/metrics/classification_performance/roc_curve_metric.py b/src/evidently/legacy/metrics/classification_performance/roc_curve_metric.py index 17014f3985..5185471815 100644 --- a/src/evidently/legacy/metrics/classification_performance/roc_curve_metric.py +++ b/src/evidently/legacy/metrics/classification_performance/roc_curve_metric.py @@ -1,3 +1,4 @@ +from typing import ClassVar from typing import Dict from typing import List from typing import Optional @@ -29,19 +30,19 @@ class ClassificationRocCurveResults(MetricResult): - class Config: - type_alias = "evidently:metric_result:ClassificationRocCurveResults" - pd_include = False - - field_tags = {"current_roc_curve": {IncludeTags.Current}, "reference_roc_curve": {IncludeTags.Reference}} + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:ClassificationRocCurveResults" + __pd_include__: ClassVar[bool] = False + __field_tags__: ClassVar[Dict[str, set]] = { + "current_roc_curve": {IncludeTags.Current}, + "reference_roc_curve": {IncludeTags.Reference}, + } current_roc_curve: Optional[ROCCurve] = None reference_roc_curve: Optional[ROCCurve] = None class ClassificationRocCurve(Metric[ClassificationRocCurveResults]): - class Config: - type_alias = "evidently:metric:ClassificationRocCurve" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:ClassificationRocCurve" def calculate(self, data: InputData) -> ClassificationRocCurveResults: dataset_columns = process_columns(data.current_data, data.column_mapping) diff --git a/src/evidently/legacy/metrics/custom_metric.py b/src/evidently/legacy/metrics/custom_metric.py index a0400fcaea..a072f1911c 100644 --- a/src/evidently/legacy/metrics/custom_metric.py +++ b/src/evidently/legacy/metrics/custom_metric.py @@ -1,9 +1,11 @@ from typing import Callable +from typing import ClassVar from typing import List from typing import Optional from typing import Union -from evidently._pydantic_compat import PrivateAttr +from pydantic import PrivateAttr + from evidently.legacy.base_metric import InputData from evidently.legacy.base_metric import Metric from evidently.legacy.base_metric import MetricResult @@ -17,8 +19,7 @@ class CustomCallableMetricResult(MetricResult): - class Config: - type_alias = "evidently:metric_result:CustomCallableMetricResult" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:CustomCallableMetricResult" value: float @@ -27,8 +28,7 @@ class Config: class CustomValueMetric(Metric[CustomCallableMetricResult]): - class Config: - type_alias = "evidently:metric:CustomValueMetric" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:CustomValueMetric" func: str title: Optional[str] = None @@ -45,14 +45,15 @@ def __init__( **data, ): if callable(func): - self._func = func + _func = func self.func = f"{func.__module__}.{func.__name__}" else: - self._func = None + _func = None self.func = func self.title = title self.size = size super().__init__(options, **data) + self._func = _func def calculate(self, data: InputData) -> CustomCallableMetricResult: if self._func is None: diff --git a/src/evidently/legacy/metrics/data_drift/column_drift_metric.py b/src/evidently/legacy/metrics/data_drift/column_drift_metric.py index be7cabbbea..06a92f2d1c 100644 --- a/src/evidently/legacy/metrics/data_drift/column_drift_metric.py +++ b/src/evidently/legacy/metrics/data_drift/column_drift_metric.py @@ -1,3 +1,4 @@ +from typing import ClassVar from typing import List from typing import Optional from typing import Union @@ -247,13 +248,12 @@ def get_one_column_drift( class ColumnDriftMetric(UsesRawDataMixin, ColumnMetric[ColumnDataDriftMetrics]): - class Config: - type_alias = "evidently:metric:ColumnDriftMetric" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:ColumnDriftMetric" """Calculate drift metric for a column""" - stattest: Optional[PossibleStatTestType] - stattest_threshold: Optional[float] + stattest: Optional[PossibleStatTestType] = None + stattest_threshold: Optional[float] = None def __init__( self, diff --git a/src/evidently/legacy/metrics/data_drift/column_interaction_plot.py b/src/evidently/legacy/metrics/data_drift/column_interaction_plot.py index 2a349f9480..5bce067de2 100644 --- a/src/evidently/legacy/metrics/data_drift/column_interaction_plot.py +++ b/src/evidently/legacy/metrics/data_drift/column_interaction_plot.py @@ -1,5 +1,6 @@ import json from typing import Any +from typing import ClassVar from typing import Dict from typing import List from typing import Optional @@ -36,38 +37,36 @@ class ColumnInteractionPlotResults(MetricResult): - class Config: - type_alias = "evidently:metric_result:ColumnInteractionPlotResults" - dict_include = False - pd_include = False - tags = {IncludeTags.Render} - field_tags = { - "current": {IncludeTags.Current}, - "reference": {IncludeTags.Reference}, - "current_scatter": {IncludeTags.Current}, - "current_contour": {IncludeTags.Current}, - "current_boxes": {IncludeTags.Current}, - "reference_scatter": {IncludeTags.Reference}, - "reference_contour": {IncludeTags.Reference}, - "reference_boxes": {IncludeTags.Reference}, - } + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:ColumnInteractionPlotResults" + __dict_include__: ClassVar[bool] = False + __pd_include__: ClassVar[bool] = False + __tags__: ClassVar[set] = {IncludeTags.Render} + __field_tags__: ClassVar[Dict[str, set]] = { + "current": {IncludeTags.Current}, + "reference": {IncludeTags.Reference}, + "current_scatter": {IncludeTags.Current}, + "current_contour": {IncludeTags.Current}, + "current_boxes": {IncludeTags.Current}, + "reference_scatter": {IncludeTags.Reference}, + "reference_contour": {IncludeTags.Reference}, + "reference_boxes": {IncludeTags.Reference}, + } y_type: ColumnType x_type: ColumnType - current_scatter: Optional[ColumnScatter] - current_contour: Optional[ContourData] - current_boxes: Optional[Dict[str, Union[list, np.ndarray]]] - current: Optional[pd.DataFrame] - reference_scatter: Optional[ColumnScatter] - reference_contour: Optional[ContourData] - reference_boxes: Optional[Dict[str, Union[list, np.ndarray]]] - reference: Optional[pd.DataFrame] + current_scatter: Optional[ColumnScatter] = None + current_contour: Optional[ContourData] = None + current_boxes: Optional[Dict[str, Union[list, np.ndarray]]] = None + current: Optional[pd.DataFrame] = None + reference_scatter: Optional[ColumnScatter] = None + reference_contour: Optional[ContourData] = None + reference_boxes: Optional[Dict[str, Union[list, np.ndarray]]] = None + reference: Optional[pd.DataFrame] = None prefix: Optional[str] = None class ColumnInteractionPlot(UsesRawDataMixin, Metric[ColumnInteractionPlotResults]): - class Config: - type_alias = "evidently:metric:ColumnInteractionPlot" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:ColumnInteractionPlot" x_column: str y_column: str diff --git a/src/evidently/legacy/metrics/data_drift/column_value_plot.py b/src/evidently/legacy/metrics/data_drift/column_value_plot.py index e97a21f360..7f949848e0 100644 --- a/src/evidently/legacy/metrics/data_drift/column_value_plot.py +++ b/src/evidently/legacy/metrics/data_drift/column_value_plot.py @@ -1,3 +1,5 @@ +from typing import ClassVar +from typing import Dict from typing import List from typing import Optional from typing import Union @@ -31,12 +33,11 @@ class ColumnValuePlotResults(MetricResult): - class Config: - type_alias = "evidently:metric_result:ColumnValuePlotResults" - dict_include = False - pd_include = False - tags = {IncludeTags.Render} - field_tags = {"current": {IncludeTags.Current}, "reference": {IncludeTags.Reference}} + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:ColumnValuePlotResults" + __dict_include__: ClassVar[bool] = False + __pd_include__: ClassVar[bool] = False + __tags__: ClassVar[set] = {IncludeTags.Render} + __field_tags__: ClassVar[Dict[str, set]] = {"current": {IncludeTags.Current}, "reference": {IncludeTags.Reference}} column_name: str datetime_column_name: Optional[str] @@ -49,8 +50,7 @@ class Config: class ColumnValuePlot(UsesRawDataMixin, Metric[ColumnValuePlotResults]): - class Config: - type_alias = "evidently:metric:ColumnValuePlot" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:ColumnValuePlot" column_name: str diff --git a/src/evidently/legacy/metrics/data_drift/data_drift_table.py b/src/evidently/legacy/metrics/data_drift/data_drift_table.py index f4c7975b8e..656e56a169 100644 --- a/src/evidently/legacy/metrics/data_drift/data_drift_table.py +++ b/src/evidently/legacy/metrics/data_drift/data_drift_table.py @@ -1,3 +1,4 @@ +from typing import ClassVar from typing import Dict from typing import List from typing import Optional @@ -36,13 +37,12 @@ class DataDriftTableResults(MetricResult): - class Config: - type_alias = "evidently:metric_result:DataDriftTableResults" - dict_exclude_fields = {"dataset_columns"} - field_tags = { - "current_fi": {IncludeTags.Extra, IncludeTags.Current}, - "reference_fi": {IncludeTags.Extra, IncludeTags.Reference}, - } + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:DataDriftTableResults" + __dict_exclude_fields__: ClassVar[set] = {"dataset_columns"} + __field_tags__: ClassVar[Dict[str, set]] = { + "current_fi": {IncludeTags.Extra, IncludeTags.Current}, + "reference_fi": {IncludeTags.Extra, IncludeTags.Reference}, + } number_of_columns: int number_of_drifted_columns: int @@ -55,11 +55,10 @@ class Config: class DataDriftTable(UsesRawDataMixin, WithDriftOptions[DataDriftTableResults]): - class Config: - type_alias = "evidently:metric:DataDriftTable" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:DataDriftTable" - columns: Optional[List[str]] - feature_importance: Optional[bool] + columns: Optional[List[str]] = None + feature_importance: Optional[bool] = None _feature_importance_metric: Optional[FeatureImportanceMetric] def __init__( diff --git a/src/evidently/legacy/metrics/data_drift/dataset_drift_metric.py b/src/evidently/legacy/metrics/data_drift/dataset_drift_metric.py index a85eff6d41..95ceecb5e9 100644 --- a/src/evidently/legacy/metrics/data_drift/dataset_drift_metric.py +++ b/src/evidently/legacy/metrics/data_drift/dataset_drift_metric.py @@ -1,3 +1,4 @@ +from typing import ClassVar from typing import Dict from typing import List from typing import Optional @@ -18,8 +19,7 @@ class DatasetDriftMetricResults(MetricResult): - class Config: - type_alias = "evidently:metric_result:DatasetDriftMetricResults" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:DatasetDriftMetricResults" drift_share: float number_of_columns: int @@ -31,10 +31,9 @@ class Config: class DatasetDriftMetric( WithDriftOptions[DatasetDriftMetricResults], ): - class Config: - type_alias = "evidently:metric:DatasetDriftMetric" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:DatasetDriftMetric" - columns: Optional[List[str]] + columns: Optional[List[str]] = None drift_share: float def __init__( diff --git a/src/evidently/legacy/metrics/data_drift/embedding_drift_methods.py b/src/evidently/legacy/metrics/data_drift/embedding_drift_methods.py index b6b6c1775e..aa71bce3fc 100644 --- a/src/evidently/legacy/metrics/data_drift/embedding_drift_methods.py +++ b/src/evidently/legacy/metrics/data_drift/embedding_drift_methods.py @@ -1,5 +1,6 @@ import abc from typing import Callable +from typing import ClassVar from typing import Optional from typing import Tuple @@ -47,8 +48,7 @@ def get_pca_df( class DriftMethod(EvidentlyBaseModel): - class Config: - is_base_type = True + __is_base_type__: ClassVar[bool] = True @abc.abstractmethod def __call__(self, current_emb: pd.DataFrame, reference_emb: pd.DataFrame) -> Tuple[float, bool, str]: @@ -57,8 +57,7 @@ def __call__(self, current_emb: pd.DataFrame, reference_emb: pd.DataFrame) -> Tu @autoregister class DistanceDriftMethod(DriftMethod): - class Config: - type_alias = "evidently:drift_method:DistanceDriftMethod" + __type_alias__: ClassVar[Optional[str]] = "evidently:drift_method:DistanceDriftMethod" dist: str = "euclidean" threshold: float = 0.2 @@ -129,8 +128,7 @@ def calc_roc_auc_random(y_test, i): @autoregister class ModelDriftMethod(DriftMethod): - class Config: - type_alias = "evidently:drift_method:ModelDriftMethod" + __type_alias__: ClassVar[Optional[str]] = "evidently:drift_method:ModelDriftMethod" threshold: float = 0.55 bootstrap: Optional[bool] = None @@ -188,8 +186,7 @@ def model( @autoregister class RatioDriftMethod(DriftMethod): - class Config: - type_alias = "evidently:drift_method:RatioDriftMethod" + __type_alias__: ClassVar[Optional[str]] = "evidently:drift_method:RatioDriftMethod" component_stattest: str = "wasserstein" component_stattest_threshold: float = 0.1 @@ -267,8 +264,7 @@ def MMD2u_bstrp(K, m, n, x_idx, y_idx): @autoregister class MMDDriftMethod(DriftMethod): - class Config: - type_alias = "evidently:drift_method:MMDDriftMethod" + __type_alias__: ClassVar[Optional[str]] = "evidently:drift_method:MMDDriftMethod" threshold: float = 0.015 bootstrap: Optional[bool] = None diff --git a/src/evidently/legacy/metrics/data_drift/embeddings_drift.py b/src/evidently/legacy/metrics/data_drift/embeddings_drift.py index b5c4262e21..5b43cef919 100644 --- a/src/evidently/legacy/metrics/data_drift/embeddings_drift.py +++ b/src/evidently/legacy/metrics/data_drift/embeddings_drift.py @@ -1,3 +1,5 @@ +from typing import ClassVar +from typing import Dict from typing import List from typing import Optional @@ -26,19 +28,14 @@ class EmbeddingsDriftMetricResults(MetricResult): - class Config: - type_alias = "evidently:metric_result:EmbeddingsDriftMetricResults" - dict_exclude_fields = { - "reference", - "current", - } - - field_tags = { - "current": {IncludeTags.Current, IncludeTags.Render}, - "reference": {IncludeTags.Reference, IncludeTags.Render}, - "embeddings_name": {IncludeTags.Parameter}, - "method_name": {IncludeTags.Parameter}, - } + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:EmbeddingsDriftMetricResults" + __dict_exclude_fields__: ClassVar[set] = {"reference", "current"} + __field_tags__: ClassVar[Dict[str, set]] = { + "current": {IncludeTags.Current, IncludeTags.Render}, + "reference": {IncludeTags.Reference, IncludeTags.Render}, + "embeddings_name": {IncludeTags.Parameter}, + "method_name": {IncludeTags.Parameter}, + } embeddings_name: str drift_score: float @@ -49,8 +46,7 @@ class Config: class EmbeddingsDriftMetric(Metric[EmbeddingsDriftMetricResults]): - class Config: - type_alias = "evidently:metric:EmbeddingsDriftMetric" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:EmbeddingsDriftMetric" embeddings_name: str drift_method: Optional[DriftMethod] diff --git a/src/evidently/legacy/metrics/data_drift/feature_importance.py b/src/evidently/legacy/metrics/data_drift/feature_importance.py index 5f341a1caa..3cf0261e2f 100644 --- a/src/evidently/legacy/metrics/data_drift/feature_importance.py +++ b/src/evidently/legacy/metrics/data_drift/feature_importance.py @@ -1,3 +1,4 @@ +from typing import ClassVar from typing import Dict from typing import List from typing import Optional @@ -22,9 +23,8 @@ class FeatureImportanceMetricResult(MetricResult): - class Config: - type_alias = "evidently:metric_result:FeatureImportanceMetricResult" - field_tags = {"current": {IncludeTags.Current}, "reference": {IncludeTags.Reference}} + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:FeatureImportanceMetricResult" + __field_tags__: ClassVar[Dict[str, set]] = {"current": {IncludeTags.Current}, "reference": {IncludeTags.Reference}} current: Optional[Dict[str, float]] = None reference: Optional[Dict[str, float]] = None @@ -42,8 +42,7 @@ def get_pandas(self) -> pd.DataFrame: class FeatureImportanceMetric(Metric[FeatureImportanceMetricResult]): - class Config: - type_alias = "evidently:metric:FeatureImportanceMetric" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:FeatureImportanceMetric" def calculate(self, data: InputData) -> FeatureImportanceMetricResult: if data.additional_data.get("current_feature_importance") is not None: diff --git a/src/evidently/legacy/metrics/data_drift/target_by_features_table.py b/src/evidently/legacy/metrics/data_drift/target_by_features_table.py index 48640787c6..c0db284683 100644 --- a/src/evidently/legacy/metrics/data_drift/target_by_features_table.py +++ b/src/evidently/legacy/metrics/data_drift/target_by_features_table.py @@ -1,4 +1,5 @@ import json +from typing import ClassVar from typing import Dict from typing import List from typing import Optional @@ -33,29 +34,27 @@ class TargetByFeaturesTableResults(MetricResult): - class Config: - type_alias = "evidently:metric_result:TargetByFeaturesTableResults" - dict_include = False - field_tags = { - "current": {IncludeTags.Current}, - "reference": {IncludeTags.Reference}, - "target_name": {IncludeTags.Parameter}, - "columns": {IncludeTags.Parameter}, - "task": {IncludeTags.Parameter}, - } + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:TargetByFeaturesTableResults" + __dict_include__: ClassVar[bool] = False + __field_tags__: ClassVar[Dict[str, set]] = { + "current": {IncludeTags.Current}, + "reference": {IncludeTags.Reference}, + "target_name": {IncludeTags.Parameter}, + "columns": {IncludeTags.Parameter}, + "task": {IncludeTags.Parameter}, + } current: StatsByFeature - reference: Optional[StatsByFeature] - target_name: Optional[str] + reference: Optional[StatsByFeature] = None + target_name: Optional[str] = None columns: List[str] task: str class TargetByFeaturesTable(UsesRawDataMixin, Metric[TargetByFeaturesTableResults]): - class Config: - type_alias = "evidently:metric:TargetByFeaturesTable" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:TargetByFeaturesTable" - columns: Optional[List[str]] + columns: Optional[List[str]] = None _text_features_gen: Optional[ Dict[ str, diff --git a/src/evidently/legacy/metrics/data_drift/text_descriptors_drift_metric.py b/src/evidently/legacy/metrics/data_drift/text_descriptors_drift_metric.py index 4baf907ca2..a556341ea6 100644 --- a/src/evidently/legacy/metrics/data_drift/text_descriptors_drift_metric.py +++ b/src/evidently/legacy/metrics/data_drift/text_descriptors_drift_metric.py @@ -1,3 +1,4 @@ +from typing import ClassVar from typing import Dict from typing import List from typing import Optional @@ -43,8 +44,7 @@ class TextDescriptorsDriftMetricResults(MetricResult): - class Config: - type_alias = "evidently:metric_result:TextDescriptorsDriftMetricResults" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:TextDescriptorsDriftMetricResults" number_of_columns: int number_of_drifted_columns: int @@ -55,8 +55,7 @@ class Config: class TextDescriptorsDriftMetric(UsesRawDataMixin, Metric[TextDescriptorsDriftMetricResults]): - class Config: - type_alias = "evidently:metric:TextDescriptorsDriftMetric" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:TextDescriptorsDriftMetric" column_name: str stattest: Optional[PossibleStatTestType] = None diff --git a/src/evidently/legacy/metrics/data_drift/text_domain_classifier_drift_metric.py b/src/evidently/legacy/metrics/data_drift/text_domain_classifier_drift_metric.py index 8efec5f40a..1373297523 100644 --- a/src/evidently/legacy/metrics/data_drift/text_domain_classifier_drift_metric.py +++ b/src/evidently/legacy/metrics/data_drift/text_domain_classifier_drift_metric.py @@ -1,3 +1,5 @@ +from typing import ClassVar +from typing import Dict from typing import List from typing import Optional from typing import Tuple @@ -26,21 +28,19 @@ class TextDomainField(MetricResult): - class Config: - type_alias = "evidently:metric_result:TextDomainField" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:TextDomainField" characteristic_examples: Optional[List[str]] characteristic_words: Optional[List[str]] class TextDomainClassifierDriftResult(MetricResult): - class Config: - type_alias = "evidently:metric_result:TextDomainClassifierDriftResult" - field_tags = { - "current": {IncludeTags.Current, IncludeTags.Extra}, - "reference": {IncludeTags.Reference, IncludeTags.Extra}, - "text_column_name": {IncludeTags.Parameter}, - } + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:TextDomainClassifierDriftResult" + __field_tags__: ClassVar[Dict[str, set]] = { + "current": {IncludeTags.Current, IncludeTags.Extra}, + "reference": {IncludeTags.Reference, IncludeTags.Extra}, + "text_column_name": {IncludeTags.Parameter}, + } text_column_name: str domain_classifier_roc_auc: float @@ -51,8 +51,7 @@ class Config: class TextDomainClassifierDriftMetric(Metric[TextDomainClassifierDriftResult]): - class Config: - type_alias = "evidently:metric:TextDomainClassifierDriftMetric" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:TextDomainClassifierDriftMetric" text_column_name: str diff --git a/src/evidently/legacy/metrics/data_drift/text_metric.py b/src/evidently/legacy/metrics/data_drift/text_metric.py index cd713170aa..004248f952 100644 --- a/src/evidently/legacy/metrics/data_drift/text_metric.py +++ b/src/evidently/legacy/metrics/data_drift/text_metric.py @@ -1,4 +1,7 @@ +from typing import ClassVar from typing import List +from typing import Optional +from typing import Set from evidently.legacy.base_metric import InputData from evidently.legacy.base_metric import Metric @@ -12,17 +15,15 @@ class CommentResults(MetricResult): - class Config: - type_alias = "evidently:metric_result:CommentResults" - dict_include = False - tags = {IncludeTags.Render} + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:CommentResults" + __dict_include__: ClassVar[bool] = False + __tags__: ClassVar[Set[IncludeTags]] = {IncludeTags.Render} text: str class Comment(Metric[CommentResults]): - class Config: - type_alias = "evidently:metric:Comment" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:Comment" text: str diff --git a/src/evidently/legacy/metrics/data_integrity/column_missing_values_metric.py b/src/evidently/legacy/metrics/data_integrity/column_missing_values_metric.py index c54e3c784f..dc4f35dbee 100644 --- a/src/evidently/legacy/metrics/data_integrity/column_missing_values_metric.py +++ b/src/evidently/legacy/metrics/data_integrity/column_missing_values_metric.py @@ -10,7 +10,7 @@ from evidently.legacy.base_metric import Metric from evidently.legacy.base_metric import MetricResult from evidently.legacy.core import IncludeTags -from evidently.legacy.metrics.data_integrity.dataset_missing_values_metric import MissingValue +from evidently.legacy.metrics.data_integrity.dataset_missing_values_metric import MissingValueType from evidently.legacy.model.widget import BaseWidgetInfo from evidently.legacy.options.base import AnyOptions from evidently.legacy.renderers.base_renderer import MetricRenderer @@ -26,15 +26,17 @@ class ColumnMissingValues(MetricResult): """Statistics about missing values in a column""" - class Config: - type_alias = "evidently:metric_result:ColumnMissingValues" - pd_exclude_fields = {"different_missing_values"} - field_tags = {"number_of_rows": {IncludeTags.Extra}, "different_missing_values": {IncludeTags.Extra}} + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:ColumnMissingValues" + __pd_exclude_fields__: ClassVar[set] = {"different_missing_values"} + __field_tags__: ClassVar[Dict[str, set]] = { + "number_of_rows": {IncludeTags.Extra}, + "different_missing_values": {IncludeTags.Extra}, + } # count of rows in the column number_of_rows: int # set of different missed values in the column - different_missing_values: Dict[MissingValue, int] + different_missing_values: Dict[MissingValueType, int] # number of different missed values in the column number_of_different_missing_values: int # count of missed values in the column @@ -44,13 +46,12 @@ class Config: class ColumnMissingValuesMetricResult(MetricResult): - class Config: - type_alias = "evidently:metric_result:ColumnMissingValuesMetricResult" - field_tags = { - "current": {IncludeTags.Current}, - "reference": {IncludeTags.Reference}, - "column_name": {IncludeTags.Parameter}, - } + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:ColumnMissingValuesMetricResult" + __field_tags__: ClassVar[Dict[str, set]] = { + "current": {IncludeTags.Current}, + "reference": {IncludeTags.Reference}, + "column_name": {IncludeTags.Parameter}, + } column_name: str current: ColumnMissingValues @@ -58,8 +59,7 @@ class Config: class ColumnMissingValuesMetric(Metric[ColumnMissingValuesMetricResult]): - class Config: - type_alias = "evidently:metric:ColumnMissingValuesMetric" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:ColumnMissingValuesMetric" """Count missing values in a column. diff --git a/src/evidently/legacy/metrics/data_integrity/column_regexp_metric.py b/src/evidently/legacy/metrics/data_integrity/column_regexp_metric.py index 1517be3b31..f3f8588075 100644 --- a/src/evidently/legacy/metrics/data_integrity/column_regexp_metric.py +++ b/src/evidently/legacy/metrics/data_integrity/column_regexp_metric.py @@ -1,5 +1,6 @@ import collections import re +from typing import ClassVar from typing import Dict from typing import List from typing import Optional @@ -27,15 +28,13 @@ class DataIntegrityValueByRegexpStat(MetricResult): """Statistics about matched by a regular expression values in a column for one dataset""" - class Config: - type_alias = "evidently:metric_result:DataIntegrityValueByRegexpStat" - pd_exclude_fields = {"table_of_matched", "table_of_not_matched"} - - field_tags = { - "number_of_rows": {IncludeTags.Extra}, - "table_of_matched": {IncludeTags.Extra}, - "table_of_not_matched": {IncludeTags.Extra}, - } + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:DataIntegrityValueByRegexpStat" + __pd_exclude_fields__: ClassVar[set] = {"table_of_matched", "table_of_not_matched"} + __field_tags__: ClassVar[Dict[str, set]] = { + "number_of_rows": {IncludeTags.Extra}, + "table_of_matched": {IncludeTags.Extra}, + "table_of_not_matched": {IncludeTags.Extra}, + } # count of matched values in the column, without NaNs number_of_matched: int @@ -50,15 +49,14 @@ class Config: class DataIntegrityValueByRegexpMetricResult(MetricResult): - class Config: - type_alias = "evidently:metric_result:DataIntegrityValueByRegexpMetricResult" - field_tags = { - "current": {IncludeTags.Current}, - "reference": {IncludeTags.Reference}, - "column_name": {IncludeTags.Parameter}, - "reg_exp": {IncludeTags.Parameter}, - "top": {IncludeTags.Parameter}, - } + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:DataIntegrityValueByRegexpMetricResult" + __field_tags__: ClassVar[Dict[str, set]] = { + "current": {IncludeTags.Current}, + "reference": {IncludeTags.Reference}, + "column_name": {IncludeTags.Parameter}, + "reg_exp": {IncludeTags.Parameter}, + "top": {IncludeTags.Parameter}, + } # name of the column that we check by the regular expression column_name: str @@ -72,8 +70,7 @@ class Config: class ColumnRegExpMetric(Metric[DataIntegrityValueByRegexpMetricResult]): - class Config: - type_alias = "evidently:metric:ColumnRegExpMetric" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:ColumnRegExpMetric" """Count number of values in a column matched or not by a regular expression (regexp)""" @@ -89,8 +86,8 @@ def __init__(self, column_name: str, reg_exp: str, top: int = 10, options: AnyOp self.top = top self.reg_exp = reg_exp self.column_name = column_name - self._reg_exp_compiled = re.compile(reg_exp) super().__init__(options=options) + self._reg_exp_compiled = re.compile(reg_exp) def _calculate_stats_by_regexp(self, column: pd.Series) -> DataIntegrityValueByRegexpStat: number_of_matched = 0 diff --git a/src/evidently/legacy/metrics/data_integrity/column_summary_metric.py b/src/evidently/legacy/metrics/data_integrity/column_summary_metric.py index b470aed9f1..7268936cc9 100644 --- a/src/evidently/legacy/metrics/data_integrity/column_summary_metric.py +++ b/src/evidently/legacy/metrics/data_integrity/column_summary_metric.py @@ -1,8 +1,10 @@ import json import warnings +from typing import ClassVar from typing import Dict from typing import List from typing import Optional +from typing import Set from typing import Tuple from typing import Union @@ -54,8 +56,7 @@ class ColumnCharacteristics(MetricResult): - class Config: - type_alias = "evidently:metric_result:ColumnCharacteristics" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:ColumnCharacteristics" number_of_rows: int count: int @@ -64,8 +65,7 @@ class Config: class NumericCharacteristics(ColumnCharacteristics): - class Config: - type_alias = "evidently:metric_result:NumericCharacteristics" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:NumericCharacteristics" mean: Optional[Numeric] std: Optional[Numeric] @@ -83,8 +83,7 @@ class Config: class CategoricalCharacteristics(ColumnCharacteristics): - class Config: - type_alias = "evidently:metric_result:CategoricalCharacteristics" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:CategoricalCharacteristics" unique: Optional[int] unique_percentage: Optional[float] @@ -95,8 +94,7 @@ class Config: class DatetimeCharacteristics(ColumnCharacteristics): - class Config: - type_alias = "evidently:metric_result:DatetimeCharacteristics" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:DatetimeCharacteristics" unique: Optional[int] unique_percentage: Optional[float] @@ -107,8 +105,7 @@ class Config: class TextCharacteristics(ColumnCharacteristics): - class Config: - type_alias = "evidently:metric_result:TextCharacteristics" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:TextCharacteristics" text_length_min: Optional[float] text_length_mean: Optional[float] @@ -122,17 +119,15 @@ class Config: class DataInTimePlots(MetricResult): - class Config: - type_alias = "evidently:metric_result:DataInTimePlots" - field_tags = {"current": {IncludeTags.Current}, "reference": {IncludeTags.Reference}} + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:DataInTimePlots" + __field_tags__: ClassVar[Dict[str, set]] = {"current": {IncludeTags.Current}, "reference": {IncludeTags.Reference}} current: pd.DataFrame - reference: Optional[pd.DataFrame] + reference: Optional[pd.DataFrame] = None class DataInTime(MetricResult): - class Config: - type_alias = "evidently:metric_result:DataInTime" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:DataInTime" data_for_plots: DataInTimePlots freq: str @@ -140,28 +135,25 @@ class Config: class DataByTarget(MetricResult): - class Config: - type_alias = "evidently:metric_result:DataByTarget" - smart_union = True - - box_data: Optional[Dict[str, dict]] - scatter_data: Optional[Dict[str, Dict[str, list]]] - contour_data: Optional[Dict[str, ContourData]] - count_data: Optional[Dict[str, pd.DataFrame]] + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:DataByTarget" + + box_data: Optional[Dict[str, dict]] = None + scatter_data: Optional[Dict[str, Dict[str, list]]] = None + contour_data: Optional[Dict[str, ContourData]] = None + count_data: Optional[Dict[str, pd.DataFrame]] = None target_name: str target_type: str class DataQualityPlot(MetricResult): - class Config: - type_alias = "evidently:metric_result:DataQualityPlot" - dict_include = False - pd_include = False - tags = {IncludeTags.Render} + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:DataQualityPlot" + __dict_include__: ClassVar[bool] = False + __pd_include__: ClassVar[bool] = False + __tags__: ClassVar[Set[IncludeTags]] = {IncludeTags.Render} bins_for_hist: Optional[Histogram] data_in_time: Optional[DataInTime] - data_by_target: Optional[DataByTarget] + data_by_target: Optional[DataByTarget] = None counts_of_values: Optional[Dict[str, pd.DataFrame]] @@ -327,26 +319,23 @@ def _split_periods(curr_data: pd.DataFrame, ref_data: pd.DataFrame, feature_name class ColumnSummaryResult(ColumnMetricResult): - class Config: - type_alias = "evidently:metric_result:ColumnSummaryResult" - pd_name_mapping = { - "reference_characteristics": "reference", - "current_characteristics": "current", - } - - field_tags = { - "current_characteristics": {IncludeTags.Current}, - "reference_characteristics": {IncludeTags.Reference}, - } - - reference_characteristics: Optional[ColumnCharacteristics] + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:ColumnSummaryResult" + __pd_name_mapping__: ClassVar[Dict[str, str]] = { + "reference_characteristics": "reference", + "current_characteristics": "current", + } + __field_tags__: ClassVar[Dict[str, set]] = { + "current_characteristics": {IncludeTags.Current}, + "reference_characteristics": {IncludeTags.Reference}, + } + + reference_characteristics: Optional[ColumnCharacteristics] = None current_characteristics: ColumnCharacteristics plot_data: DataQualityPlot class ColumnSummaryMetric(UsesRawDataMixin, ColumnMetric[ColumnSummaryResult]): - class Config: - type_alias = "evidently:metric:ColumnSummaryMetric" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:ColumnSummaryMetric" _generated_text_features: Optional[Dict[str, Union[TextLength, NonLetterCharacterPercentage, OOVWordsPercentage]]] diff --git a/src/evidently/legacy/metrics/data_integrity/dataset_missing_values_metric.py b/src/evidently/legacy/metrics/data_integrity/dataset_missing_values_metric.py index 5beff23bc5..a8076a4172 100644 --- a/src/evidently/legacy/metrics/data_integrity/dataset_missing_values_metric.py +++ b/src/evidently/legacy/metrics/data_integrity/dataset_missing_values_metric.py @@ -1,20 +1,20 @@ +from typing import Annotated from typing import Any from typing import ClassVar from typing import Dict from typing import FrozenSet from typing import List from typing import Optional -from typing import Union import numpy as np import pandas as pd +from pydantic import BeforeValidator from evidently.legacy.base_metric import InputData from evidently.legacy.base_metric import Metric from evidently.legacy.base_metric import MetricResult from evidently.legacy.calculations.data_quality import get_rows_count from evidently.legacy.core import IncludeTags -from evidently.legacy.core import pydantic_type_validator from evidently.legacy.model.widget import BaseWidgetInfo from evidently.legacy.options.base import AnyOptions from evidently.legacy.renderers.base_renderer import MetricRenderer @@ -28,50 +28,63 @@ from evidently.legacy.renderers.html_widgets import table_data from evidently.legacy.renderers.html_widgets import widget_tabs -NoneKey = type("NoneKey", tuple(), {}) +# NoneKey = type("NoneKey", tuple(), {}) -@pydantic_type_validator(NoneKey) -def null_valudator(value): +# @pydantic_type_validator(NoneKey) +# def null_valudator(value): +# if value is None or value == "null": +# return None +# raise ValueError("not a None") +# +# +# MissingValue = Union[PydanticNPDouble, NoneKey, Any] # type: ignore[valid-type] + + +def missing_value_validator(value): if value is None or value == "null": return None - raise ValueError("not a None") + if value == "": + return "" + try: + return float(value) + except ValueError: + pass + return value -MissingValue = Union[np.double, NoneKey, Any] # type: ignore[valid-type] +MissingValueType = Annotated[Any, BeforeValidator(missing_value_validator)] class DatasetMissingValues(MetricResult): """Statistics about missed values in a dataset""" - class Config: - type_alias = "evidently:metric_result:DatasetMissingValues" - pd_exclude_fields = { - "different_missing_values_by_column", - "different_missing_values", - "number_of_different_missing_values_by_column", - "number_of_missing_values_by_column", - "share_of_missing_values_by_column", - "columns_with_missing_values", - } - - field_tags = { - "different_missing_values": {IncludeTags.Extra}, - "different_missing_values_by_column": {IncludeTags.Extra}, - "number_of_different_missing_values_by_column": {IncludeTags.Extra}, - "number_of_missing_values_by_column": {IncludeTags.Extra}, - "share_of_missing_values_by_column": {IncludeTags.Extra}, - "number_of_rows": {IncludeTags.Extra}, - "number_of_columns": {IncludeTags.Extra}, - "columns_with_missing_values": {IncludeTags.Extra}, - } + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:DatasetMissingValues" + __pd_exclude_fields__: ClassVar[set] = { + "different_missing_values_by_column", + "different_missing_values", + "number_of_different_missing_values_by_column", + "number_of_missing_values_by_column", + "share_of_missing_values_by_column", + "columns_with_missing_values", + } + __field_tags__: ClassVar[Dict[str, set]] = { + "different_missing_values": {IncludeTags.Extra}, + "different_missing_values_by_column": {IncludeTags.Extra}, + "number_of_different_missing_values_by_column": {IncludeTags.Extra}, + "number_of_missing_values_by_column": {IncludeTags.Extra}, + "share_of_missing_values_by_column": {IncludeTags.Extra}, + "number_of_rows": {IncludeTags.Extra}, + "number_of_columns": {IncludeTags.Extra}, + "columns_with_missing_values": {IncludeTags.Extra}, + } # set of different missing values in the dataset - different_missing_values: Dict[MissingValue, int] + different_missing_values: Dict[MissingValueType, int] # number of different missing values in the dataset number_of_different_missing_values: int # set of different missing values for each column - different_missing_values_by_column: Dict[str, Dict[MissingValue, int]] + different_missing_values_by_column: Dict[str, Dict[MissingValueType, int]] # count of different missing values for each column number_of_different_missing_values_by_column: Dict[str, int] # count of missing values in all dataset @@ -99,17 +112,15 @@ class Config: class DatasetMissingValuesMetricResult(MetricResult): - class Config: - type_alias = "evidently:metric_result:DatasetMissingValuesMetricResult" - field_tags = {"current": {IncludeTags.Current}, "reference": {IncludeTags.Reference}} + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:DatasetMissingValuesMetricResult" + __field_tags__: ClassVar[Dict[str, set]] = {"current": {IncludeTags.Current}, "reference": {IncludeTags.Reference}} current: DatasetMissingValues reference: Optional[DatasetMissingValues] = None class DatasetMissingValuesMetric(Metric[DatasetMissingValuesMetricResult]): - class Config: - type_alias = "evidently:metric:DatasetMissingValuesMetric" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:DatasetMissingValuesMetric" """Count missing values in a dataset. @@ -127,7 +138,7 @@ class Config: # default missing values list DEFAULT_MISSING_VALUES: ClassVar = ["", np.inf, -np.inf, None] - missing_values: FrozenSet[MissingValue] + missing_values: FrozenSet[MissingValueType] def __init__(self, missing_values: Optional[list] = None, replace: bool = True, options: AnyOptions = None) -> None: _missing_values: list diff --git a/src/evidently/legacy/metrics/data_integrity/dataset_summary_metric.py b/src/evidently/legacy/metrics/data_integrity/dataset_summary_metric.py index 841db40d55..99d424e935 100644 --- a/src/evidently/legacy/metrics/data_integrity/dataset_summary_metric.py +++ b/src/evidently/legacy/metrics/data_integrity/dataset_summary_metric.py @@ -1,3 +1,4 @@ +from typing import ClassVar from typing import Dict from typing import List from typing import Optional @@ -53,20 +54,18 @@ def from_dtype(cls, dtype: Union[np.dtype, ExtensionDtype]): class DatasetSummary(MetricResult): """Columns information in a dataset""" - class Config: - type_alias = "evidently:metric_result:DatasetSummary" - dict_exclude_fields = {"columns_type_data"} - pd_exclude_fields = {"columns_type_data", "nans_by_columns", "number_uniques_by_columns"} - - field_tags = { - "target": {IncludeTags.Parameter}, - "prediction": {IncludeTags.Parameter}, - "date_column": {IncludeTags.Parameter}, - "id_column": {IncludeTags.Parameter}, - "columns_type_data": {IncludeTags.Extra}, - "nans_by_columns": {IncludeTags.Extra}, - "number_uniques_by_columns": {IncludeTags.Extra}, - } + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:DatasetSummary" + __dict_exclude_fields__: ClassVar[set] = {"columns_type_data"} + __pd_exclude_fields__: ClassVar[set] = {"columns_type_data", "nans_by_columns", "number_uniques_by_columns"} + __field_tags__: ClassVar[Dict[str, set]] = { + "target": {IncludeTags.Parameter}, + "prediction": {IncludeTags.Parameter}, + "date_column": {IncludeTags.Parameter}, + "id_column": {IncludeTags.Parameter}, + "columns_type_data": {IncludeTags.Extra}, + "nans_by_columns": {IncludeTags.Extra}, + "number_uniques_by_columns": {IncludeTags.Extra}, + } target: Optional[str] prediction: Optional[Union[str, Sequence[str]]] @@ -96,13 +95,12 @@ def columns_type(self) -> Dict[Label, np.dtype]: class DatasetSummaryMetricResult(MetricResult): - class Config: - type_alias = "evidently:metric_result:DatasetSummaryMetricResult" - field_tags = { - "almost_duplicated_threshold": {IncludeTags.Parameter}, - "current": {IncludeTags.Current}, - "reference": {IncludeTags.Reference}, - } + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:DatasetSummaryMetricResult" + __field_tags__: ClassVar[Dict[str, set]] = { + "almost_duplicated_threshold": {IncludeTags.Parameter}, + "current": {IncludeTags.Current}, + "reference": {IncludeTags.Reference}, + } almost_duplicated_threshold: float current: DatasetSummary @@ -110,8 +108,7 @@ class Config: class DatasetSummaryMetric(Metric[DatasetSummaryMetricResult]): - class Config: - type_alias = "evidently:metric:DatasetSummaryMetric" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:DatasetSummaryMetric" """Common dataset(s) columns/features characteristics""" diff --git a/src/evidently/legacy/metrics/data_quality/column_category_metric.py b/src/evidently/legacy/metrics/data_quality/column_category_metric.py index 5caaa11ac3..ff11cf31af 100644 --- a/src/evidently/legacy/metrics/data_quality/column_category_metric.py +++ b/src/evidently/legacy/metrics/data_quality/column_category_metric.py @@ -1,3 +1,4 @@ +from typing import ClassVar from typing import Dict from typing import List from typing import Optional @@ -21,9 +22,8 @@ class CategoryStat(MetricResult): - class Config: - type_alias = "evidently:metric_result:CategoryStat" - field_tags = {"all_num": {IncludeTags.Extra}} + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:CategoryStat" + __field_tags__: ClassVar[Dict[str, set]] = {"all_num": {IncludeTags.Extra}} all_num: int category_num: int @@ -31,24 +31,21 @@ class Config: class CountOfValues(MetricResult): - class Config: - type_alias = "evidently:metric_result:CountOfValues" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:CountOfValues" current: HistogramData reference: Optional[HistogramData] = None class ColumnCategoryMetricResult(MetricResult): - class Config: - type_alias = "evidently:metric_result:ColumnCategoryMetricResult" - pd_exclude_fields = {"counts"} - field_tags = { - "current": {IncludeTags.Current}, - "reference": {IncludeTags.Reference}, - "column_name": {IncludeTags.Parameter}, - "counts": {IncludeTags.Extra}, - } - smart_union = True + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:ColumnCategoryMetricResult" + __pd_exclude_fields__: ClassVar[set] = {"counts"} + __field_tags__: ClassVar[Dict[str, set]] = { + "current": {IncludeTags.Current}, + "reference": {IncludeTags.Reference}, + "column_name": {IncludeTags.Parameter}, + "counts": {IncludeTags.Extra}, + } def __init__(self, **data) -> None: """for backward compatibility""" @@ -82,9 +79,7 @@ def counts_of_values(self) -> Dict[str, pd.DataFrame]: class ColumnCategoryMetric(Metric[ColumnCategoryMetricResult]): """Calculates count and shares of values in the predefined values list""" - class Config: - type_alias = "evidently:metric:ColumnCategoryMetric" - smart_union = True + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:ColumnCategoryMetric" column_name: ColumnName category: Union[bool, int, float, str] diff --git a/src/evidently/legacy/metrics/data_quality/column_correlations_metric.py b/src/evidently/legacy/metrics/data_quality/column_correlations_metric.py index e404e1619c..77812785e3 100644 --- a/src/evidently/legacy/metrics/data_quality/column_correlations_metric.py +++ b/src/evidently/legacy/metrics/data_quality/column_correlations_metric.py @@ -1,3 +1,4 @@ +from typing import ClassVar from typing import Dict from typing import List from typing import Optional @@ -26,13 +27,12 @@ class ColumnCorrelationsMetricResult(MetricResult): - class Config: - type_alias = "evidently:metric_result:ColumnCorrelationsMetricResult" - field_tags = { - "current": {IncludeTags.Current}, - "reference": {IncludeTags.Reference}, - "column_name": {IncludeTags.Parameter}, - } + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:ColumnCorrelationsMetricResult" + __field_tags__: ClassVar[Dict[str, set]] = { + "current": {IncludeTags.Current}, + "reference": {IncludeTags.Reference}, + "column_name": {IncludeTags.Parameter}, + } column_name: str current: Dict[str, ColumnCorrelations] @@ -55,8 +55,7 @@ def get_pandas(self) -> pd.DataFrame: class ColumnCorrelationsMetric(Metric[ColumnCorrelationsMetricResult]): - class Config: - type_alias = "evidently:metric:ColumnCorrelationsMetric" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:ColumnCorrelationsMetric" """Calculates correlations between the selected column and all the other columns. In the current and reference (if presented) datasets""" diff --git a/src/evidently/legacy/metrics/data_quality/column_distribution_metric.py b/src/evidently/legacy/metrics/data_quality/column_distribution_metric.py index b6bcd8a110..7195638900 100644 --- a/src/evidently/legacy/metrics/data_quality/column_distribution_metric.py +++ b/src/evidently/legacy/metrics/data_quality/column_distribution_metric.py @@ -1,3 +1,5 @@ +from typing import ClassVar +from typing import Dict from typing import List from typing import Optional from typing import Union @@ -24,13 +26,12 @@ class ColumnDistributionMetricResult(MetricResult): - class Config: - type_alias = "evidently:metric_result:ColumnDistributionMetricResult" - field_tags = { - "current": {IncludeTags.Current}, - "reference": {IncludeTags.Reference}, - "column_name": {IncludeTags.Parameter}, - } + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:ColumnDistributionMetricResult" + __field_tags__: ClassVar[Dict[str, set]] = { + "current": {IncludeTags.Current}, + "reference": {IncludeTags.Reference}, + "column_name": {IncludeTags.Parameter}, + } column_name: str current: Distribution @@ -38,8 +39,7 @@ class Config: class ColumnDistributionMetric(Metric[ColumnDistributionMetricResult]): - class Config: - type_alias = "evidently:metric:ColumnDistributionMetric" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:ColumnDistributionMetric" """Calculates distribution for the column""" diff --git a/src/evidently/legacy/metrics/data_quality/column_quantile_metric.py b/src/evidently/legacy/metrics/data_quality/column_quantile_metric.py index 8ac34e5499..d12baaf2d6 100644 --- a/src/evidently/legacy/metrics/data_quality/column_quantile_metric.py +++ b/src/evidently/legacy/metrics/data_quality/column_quantile_metric.py @@ -1,3 +1,5 @@ +from typing import ClassVar +from typing import Dict from typing import List from typing import Optional from typing import Union @@ -27,8 +29,7 @@ class QuantileStats(MetricResult): - class Config: - type_alias = "evidently:metric_result:QuantileStats" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:QuantileStats" value: float # calculated value of the quantile @@ -37,13 +38,12 @@ class Config: class ColumnQuantileMetricResult(ColumnMetricResult): - class Config: - type_alias = "evidently:metric_result:ColumnQuantileMetricResult" - field_tags = { - "current": {IncludeTags.Current}, - "reference": {IncludeTags.Reference}, - "quantile": {IncludeTags.Parameter}, - } + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:ColumnQuantileMetricResult" + __field_tags__: ClassVar[Dict[str, set]] = { + "current": {IncludeTags.Current}, + "reference": {IncludeTags.Reference}, + "quantile": {IncludeTags.Parameter}, + } # range of the quantile (from 0 to 1) quantile: float @@ -52,8 +52,7 @@ class Config: class ColumnQuantileMetric(Metric[ColumnQuantileMetricResult]): - class Config: - type_alias = "evidently:metric:ColumnQuantileMetric" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:ColumnQuantileMetric" """Calculates quantile with specified range""" diff --git a/src/evidently/legacy/metrics/data_quality/column_value_list_metric.py b/src/evidently/legacy/metrics/data_quality/column_value_list_metric.py index e72b0b4e16..30720e2893 100644 --- a/src/evidently/legacy/metrics/data_quality/column_value_list_metric.py +++ b/src/evidently/legacy/metrics/data_quality/column_value_list_metric.py @@ -1,4 +1,6 @@ from typing import Any +from typing import ClassVar +from typing import Dict from typing import List from typing import Optional from typing import Tuple @@ -24,13 +26,12 @@ class ValueListStat(MetricResult): - class Config: - type_alias = "evidently:metric_result:ValueListStat" - field_tags = { - "values_in_list_dist": {IncludeTags.Extra}, - "values_not_in_list_dist": {IncludeTags.Extra}, - "rows_count": {IncludeTags.Extra}, - } + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:ValueListStat" + __field_tags__: ClassVar[Dict[str, set]] = { + "values_in_list_dist": {IncludeTags.Extra}, + "values_not_in_list_dist": {IncludeTags.Extra}, + "rows_count": {IncludeTags.Extra}, + } def __init__(self, **data: Any): if "values_in_list" in data: @@ -64,14 +65,13 @@ def values_not_in_list(self) -> List[Tuple[Any, int]]: class ColumnValueListMetricResult(MetricResult): - class Config: - type_alias = "evidently:metric_result:ColumnValueListMetricResult" - field_tags = { - "current": {IncludeTags.Current}, - "reference": {IncludeTags.Reference}, - "column_name": {IncludeTags.Parameter}, - "values": {IncludeTags.Parameter}, - } + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:ColumnValueListMetricResult" + __field_tags__: ClassVar[Dict[str, set]] = { + "current": {IncludeTags.Current}, + "reference": {IncludeTags.Reference}, + "column_name": {IncludeTags.Parameter}, + "values": {IncludeTags.Parameter}, + } column_name: str values: List[Any] @@ -80,8 +80,7 @@ class Config: class ColumnValueListMetric(Metric[ColumnValueListMetricResult]): - class Config: - type_alias = "evidently:metric:ColumnValueListMetric" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:ColumnValueListMetric" """Calculates count and shares of values in the predefined values list""" diff --git a/src/evidently/legacy/metrics/data_quality/column_value_range_metric.py b/src/evidently/legacy/metrics/data_quality/column_value_range_metric.py index 3ab5e7f1a8..5afc9e7050 100644 --- a/src/evidently/legacy/metrics/data_quality/column_value_range_metric.py +++ b/src/evidently/legacy/metrics/data_quality/column_value_range_metric.py @@ -1,3 +1,5 @@ +from typing import ClassVar +from typing import Dict from typing import List from typing import Optional from typing import Union @@ -31,9 +33,8 @@ class ValuesInRangeStat(MetricResult): - class Config: - type_alias = "evidently:metric_result:ValuesInRangeStat" - field_tags = {"number_of_values": {IncludeTags.Extra}} + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:ValuesInRangeStat" + __field_tags__: ClassVar[Dict[str, set]] = {"number_of_values": {IncludeTags.Extra}} number_in_range: int number_not_in_range: int @@ -45,15 +46,14 @@ class Config: class ColumnValueRangeMetricResult(MetricResult): - class Config: - type_alias = "evidently:metric_result:ColumnValueRangeMetricResult" - field_tags = { - "current": {IncludeTags.Current}, - "reference": {IncludeTags.Reference}, - "column_name": {IncludeTags.Parameter}, - "left": {IncludeTags.Parameter}, - "right": {IncludeTags.Parameter}, - } + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:ColumnValueRangeMetricResult" + __field_tags__: ClassVar[Dict[str, set]] = { + "current": {IncludeTags.Current}, + "reference": {IncludeTags.Reference}, + "column_name": {IncludeTags.Parameter}, + "left": {IncludeTags.Parameter}, + "right": {IncludeTags.Parameter}, + } column_name: str left: Numeric @@ -63,14 +63,13 @@ class Config: class ColumnValueRangeMetric(Metric[ColumnValueRangeMetricResult]): - class Config: - type_alias = "evidently:metric:ColumnValueRangeMetric" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:ColumnValueRangeMetric" """Calculates count and shares of values in the predefined values range""" column_name: ColumnName - left: Optional[Numeric] - right: Optional[Numeric] + left: Optional[float] = None + right: Optional[float] = None def __init__( self, diff --git a/src/evidently/legacy/metrics/data_quality/conflict_prediction_metric.py b/src/evidently/legacy/metrics/data_quality/conflict_prediction_metric.py index 37c0f34d63..91e98180d0 100644 --- a/src/evidently/legacy/metrics/data_quality/conflict_prediction_metric.py +++ b/src/evidently/legacy/metrics/data_quality/conflict_prediction_metric.py @@ -1,3 +1,5 @@ +from typing import ClassVar +from typing import Dict from typing import List from typing import Optional @@ -15,25 +17,22 @@ class ConflictPredictionData(MetricResult): - class Config: - type_alias = "evidently:metric_result:ConflictPredictionData" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:ConflictPredictionData" number_not_stable_prediction: int share_not_stable_prediction: float class ConflictPredictionMetricResults(MetricResult): - class Config: - type_alias = "evidently:metric_result:ConflictPredictionMetricResults" - field_tags = {"current": {IncludeTags.Current}, "reference": {IncludeTags.Reference}} + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:ConflictPredictionMetricResults" + __field_tags__: ClassVar[Dict[str, set]] = {"current": {IncludeTags.Current}, "reference": {IncludeTags.Reference}} current: ConflictPredictionData reference: Optional[ConflictPredictionData] class ConflictPredictionMetric(Metric[ConflictPredictionMetricResults]): - class Config: - type_alias = "evidently:metric:ConflictPredictionMetric" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:ConflictPredictionMetric" def calculate(self, data: InputData) -> ConflictPredictionMetricResults: dataset_columns = process_columns(data.current_data, data.column_mapping) diff --git a/src/evidently/legacy/metrics/data_quality/conflict_target_metric.py b/src/evidently/legacy/metrics/data_quality/conflict_target_metric.py index 5751eda5c2..309c1e0b5b 100644 --- a/src/evidently/legacy/metrics/data_quality/conflict_target_metric.py +++ b/src/evidently/legacy/metrics/data_quality/conflict_target_metric.py @@ -1,3 +1,5 @@ +from typing import ClassVar +from typing import Dict from typing import List from typing import Optional @@ -15,14 +17,13 @@ class ConflictTargetMetricResults(MetricResult): - class Config: - type_alias = "evidently:metric_result:ConflictTargetMetricResults" - field_tags = { - "number_not_stable_target": {IncludeTags.Current}, - "share_not_stable_target": {IncludeTags.Current}, - "number_not_stable_target_ref": {IncludeTags.Reference}, - "share_not_stable_target_ref": {IncludeTags.Reference}, - } + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:ConflictTargetMetricResults" + __field_tags__: ClassVar[Dict[str, set]] = { + "number_not_stable_target": {IncludeTags.Current}, + "share_not_stable_target": {IncludeTags.Current}, + "number_not_stable_target_ref": {IncludeTags.Reference}, + "share_not_stable_target_ref": {IncludeTags.Reference}, + } number_not_stable_target: int share_not_stable_target: float @@ -31,8 +32,7 @@ class Config: class ConflictTargetMetric(Metric[ConflictTargetMetricResults]): - class Config: - type_alias = "evidently:metric:ConflictTargetMetric" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:ConflictTargetMetric" def calculate(self, data: InputData) -> ConflictTargetMetricResults: dataset_columns = process_columns(data.current_data, data.column_mapping) diff --git a/src/evidently/legacy/metrics/data_quality/dataset_correlations_metric.py b/src/evidently/legacy/metrics/data_quality/dataset_correlations_metric.py index c863a4655b..b0d84cfecf 100644 --- a/src/evidently/legacy/metrics/data_quality/dataset_correlations_metric.py +++ b/src/evidently/legacy/metrics/data_quality/dataset_correlations_metric.py @@ -1,4 +1,5 @@ import copy +from typing import ClassVar from typing import Dict from typing import List from typing import Optional @@ -33,14 +34,13 @@ class CorrelationStats(MetricResult): - class Config: - type_alias = "evidently:metric_result:CorrelationStats" - field_tags = { - "abs_max_target_features_correlation": {IncludeTags.Extra}, - "abs_max_prediction_features_correlation": {IncludeTags.Extra}, - "abs_max_correlation": {IncludeTags.Extra}, - "abs_max_features_correlation": {IncludeTags.Extra}, - } + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:CorrelationStats" + __field_tags__: ClassVar[Dict[str, set]] = { + "abs_max_target_features_correlation": {IncludeTags.Extra}, + "abs_max_prediction_features_correlation": {IncludeTags.Extra}, + "abs_max_correlation": {IncludeTags.Extra}, + "abs_max_features_correlation": {IncludeTags.Extra}, + } target_prediction_correlation: Optional[float] = None abs_max_target_features_correlation: Optional[float] = None @@ -50,13 +50,11 @@ class Config: class DatasetCorrelation(MetricResult): - class Config: - type_alias = "evidently:metric_result:DatasetCorrelation" - dict_exclude_fields = {"correlation", "correlations_calculate"} - pd_include = False - pd_exclude_fields = {"correlation", "correlations_calculate"} - - field_tags = {"correlations_calculate": {IncludeTags.Extra}} + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:DatasetCorrelation" + __dict_exclude_fields__: ClassVar[set] = {"correlation", "correlations_calculate"} + __pd_include__: ClassVar[bool] = False + __pd_exclude_fields__: ClassVar[set] = {"correlation", "correlations_calculate"} + __field_tags__: ClassVar[Dict[str, set]] = {"correlations_calculate": {IncludeTags.Extra}} correlation: Dict[str, pd.DataFrame] stats: Dict[str, CorrelationStats] @@ -64,15 +62,14 @@ class Config: class DatasetCorrelationsMetricResult(MetricResult): - class Config: - type_alias = "evidently:metric_result:DatasetCorrelationsMetricResult" - dict_exclude_fields = {"target_correlation"} - pd_exclude_fields = {"target_correlation"} - field_tags = { - "current": {IncludeTags.Current}, - "reference": {IncludeTags.Reference}, - "target_correlation": {IncludeTags.Parameter}, - } + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:DatasetCorrelationsMetricResult" + __dict_exclude_fields__: ClassVar[set] = {"target_correlation"} + __pd_exclude_fields__: ClassVar[set] = {"target_correlation"} + __field_tags__: ClassVar[Dict[str, set]] = { + "current": {IncludeTags.Current}, + "reference": {IncludeTags.Reference}, + "target_correlation": {IncludeTags.Parameter}, + } current: DatasetCorrelation reference: Optional[DatasetCorrelation] @@ -80,8 +77,7 @@ class Config: class DatasetCorrelationsMetric(Metric[DatasetCorrelationsMetricResult]): - class Config: - type_alias = "evidently:metric:DatasetCorrelationsMetric" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:DatasetCorrelationsMetric" """Calculate different correlations with target, predictions and features""" @@ -93,8 +89,8 @@ class Config: ] def __init__(self, options: AnyOptions = None): - self._text_features_gen = None super().__init__(options=options) + self._text_features_gen = None def required_features(self, data_definition: DataDefinition): if len(data_definition.get_columns(ColumnType.Text, features_only=True)) > 0: diff --git a/src/evidently/legacy/metrics/data_quality/stability_metric.py b/src/evidently/legacy/metrics/data_quality/stability_metric.py index fe583a8f24..a3784dedfe 100644 --- a/src/evidently/legacy/metrics/data_quality/stability_metric.py +++ b/src/evidently/legacy/metrics/data_quality/stability_metric.py @@ -1,3 +1,4 @@ +from typing import ClassVar from typing import List from typing import Optional @@ -13,16 +14,14 @@ class DataQualityStabilityMetricResult(MetricResult): - class Config: - type_alias = "evidently:metric_result:DataQualityStabilityMetricResult" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:DataQualityStabilityMetricResult" number_not_stable_target: Optional[int] = None number_not_stable_prediction: Optional[int] = None class DataQualityStabilityMetric(Metric[DataQualityStabilityMetricResult]): - class Config: - type_alias = "evidently:metric:DataQualityStabilityMetric" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:DataQualityStabilityMetric" """Calculates stability by target and prediction""" diff --git a/src/evidently/legacy/metrics/data_quality/text_descriptors_correlation_metric.py b/src/evidently/legacy/metrics/data_quality/text_descriptors_correlation_metric.py index b23a0c9c42..8723ae475e 100644 --- a/src/evidently/legacy/metrics/data_quality/text_descriptors_correlation_metric.py +++ b/src/evidently/legacy/metrics/data_quality/text_descriptors_correlation_metric.py @@ -1,3 +1,4 @@ +from typing import ClassVar from typing import Dict from typing import List from typing import Optional @@ -29,14 +30,13 @@ class TextDescriptorsCorrelationMetricResult(MetricResult): - class Config: - type_alias = "evidently:metric_result:TextDescriptorsCorrelationMetricResult" - pd_include = False - field_tags = { - "current": {IncludeTags.Current}, - "reference": {IncludeTags.Reference}, - "column_name": {IncludeTags.Parameter}, - } + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:TextDescriptorsCorrelationMetricResult" + __pd_include__: ClassVar[bool] = False + __field_tags__: ClassVar[Dict[str, set]] = { + "current": {IncludeTags.Current}, + "reference": {IncludeTags.Reference}, + "column_name": {IncludeTags.Parameter}, + } column_name: str current: Dict[str, Dict[str, ColumnCorrelations]] @@ -44,8 +44,7 @@ class Config: class TextDescriptorsCorrelationMetric(Metric[TextDescriptorsCorrelationMetricResult]): - class Config: - type_alias = "evidently:metric:TextDescriptorsCorrelationMetric" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:TextDescriptorsCorrelationMetric" """Calculates correlations between each auto-generated text feature for column_name and other dataset columns""" diff --git a/src/evidently/legacy/metrics/data_quality/text_descriptors_distribution.py b/src/evidently/legacy/metrics/data_quality/text_descriptors_distribution.py index 53bac1c2dd..4539fc40c7 100644 --- a/src/evidently/legacy/metrics/data_quality/text_descriptors_distribution.py +++ b/src/evidently/legacy/metrics/data_quality/text_descriptors_distribution.py @@ -1,3 +1,4 @@ +from typing import ClassVar from typing import Dict from typing import List from typing import Optional @@ -29,14 +30,13 @@ class TextDescriptorsDistributionResult(MetricResult): - class Config: - type_alias = "evidently:metric_result:TextDescriptorsDistributionResult" - pd_include = False - field_tags = { - "current": {IncludeTags.Current}, - "reference": {IncludeTags.Reference}, - "column_name": {IncludeTags.Parameter}, - } + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:TextDescriptorsDistributionResult" + __pd_include__: ClassVar[bool] = False + __field_tags__: ClassVar[Dict[str, set]] = { + "current": {IncludeTags.Current}, + "reference": {IncludeTags.Reference}, + "column_name": {IncludeTags.Parameter}, + } column_name: str current: Dict[str, Distribution] @@ -44,8 +44,7 @@ class Config: class TextDescriptorsDistribution(Metric[TextDescriptorsDistributionResult]): - class Config: - type_alias = "evidently:metric:TextDescriptorsDistribution" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:TextDescriptorsDistribution" """Calculates distribution for the column""" diff --git a/src/evidently/legacy/metrics/recsys/base_top_k.py b/src/evidently/legacy/metrics/recsys/base_top_k.py index 0f0fe2c511..ca07cde7bb 100644 --- a/src/evidently/legacy/metrics/recsys/base_top_k.py +++ b/src/evidently/legacy/metrics/recsys/base_top_k.py @@ -1,4 +1,6 @@ import abc +from typing import ClassVar +from typing import Dict from typing import List from typing import Optional @@ -21,13 +23,12 @@ class TopKMetricResult(MetricResult): - class Config: - type_alias = "evidently:metric_result:TopKMetricResult" - field_tags = { - "current": {IncludeTags.Current}, - "reference": {IncludeTags.Reference}, - "k": {IncludeTags.Parameter}, - } + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:TopKMetricResult" + __field_tags__: ClassVar[Dict[str, set]] = { + "current": {IncludeTags.Current}, + "reference": {IncludeTags.Reference}, + "k": {IncludeTags.Parameter}, + } k: int current: pd.Series @@ -56,7 +57,7 @@ def __init__( class TopKMetric(Metric[TopKMetricResult], abc.ABC): _precision_recall_calculation: PrecisionRecallCalculation k: int - min_rel_score: Optional[int] + min_rel_score: Optional[int] = None no_feedback_users: bool def __init__( @@ -65,8 +66,8 @@ def __init__( self.k = k self.min_rel_score = min_rel_score self.no_feedback_users = no_feedback_users - self._precision_recall_calculation = PrecisionRecallCalculation(max(k, 10), min_rel_score) super().__init__(options=options) + self._precision_recall_calculation = PrecisionRecallCalculation(max(k, 10), min_rel_score) def calculate(self, data: InputData) -> TopKMetricResult: result = self._precision_recall_calculation.get_result() diff --git a/src/evidently/legacy/metrics/recsys/diversity.py b/src/evidently/legacy/metrics/recsys/diversity.py index 31e6ffa320..3d4a844776 100644 --- a/src/evidently/legacy/metrics/recsys/diversity.py +++ b/src/evidently/legacy/metrics/recsys/diversity.py @@ -1,4 +1,5 @@ from itertools import combinations +from typing import ClassVar from typing import Dict from typing import List from typing import Optional @@ -28,15 +29,14 @@ class DiversityMetricResult(MetricResult): - class Config: - type_alias = "evidently:metric_result:DiversityMetricResult" - field_tags = { - "k": {IncludeTags.Parameter}, - "current_value": {IncludeTags.Current}, - "current_distr": {IncludeTags.Current}, - "reference_value": {IncludeTags.Reference}, - "reference_distr": {IncludeTags.Reference}, - } + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:DiversityMetricResult" + __field_tags__: ClassVar[Dict[str, set]] = { + "k": {IncludeTags.Parameter}, + "current_value": {IncludeTags.Current}, + "current_distr": {IncludeTags.Current}, + "reference_value": {IncludeTags.Reference}, + "reference_distr": {IncludeTags.Reference}, + } k: int current_value: float @@ -46,8 +46,7 @@ class Config: class DiversityMetric(Metric[DiversityMetricResult]): - class Config: - type_alias = "evidently:metric:DiversityMetric" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:DiversityMetric" """Intra list diversity""" @@ -58,8 +57,8 @@ class Config: def __init__(self, k: int, item_features: List[str], options: AnyOptions = None) -> None: self.k = k self.item_features = item_features - self._pairwise_distance = PairwiseDistance(k=k, item_features=item_features) super().__init__(options=options) + self._pairwise_distance = PairwiseDistance(k=k, item_features=item_features) def get_ild( self, diff --git a/src/evidently/legacy/metrics/recsys/f_beta_top_k.py b/src/evidently/legacy/metrics/recsys/f_beta_top_k.py index a14101bb0f..1241a2dac5 100644 --- a/src/evidently/legacy/metrics/recsys/f_beta_top_k.py +++ b/src/evidently/legacy/metrics/recsys/f_beta_top_k.py @@ -1,3 +1,4 @@ +from typing import ClassVar from typing import Optional import numpy as np @@ -13,12 +14,11 @@ class FBetaTopKMetric(TopKMetric): - class Config: - type_alias = "evidently:metric:FBetaTopKMetric" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:FBetaTopKMetric" k: int - beta: Optional[float] - min_rel_score: Optional[int] + beta: Optional[float] = None + min_rel_score: Optional[int] = None no_feedback_users: bool _precision_recall_calculation: PrecisionRecallCalculation @@ -34,13 +34,13 @@ def __init__( self.beta = beta self.min_rel_score = min_rel_score self.no_feedback_users = no_feedback_users - self._precision_recall_calculation = PrecisionRecallCalculation(max(k, 10), min_rel_score) super().__init__( options=options, k=k, min_rel_score=min_rel_score, no_feedback_users=no_feedback_users, ) + self._precision_recall_calculation = PrecisionRecallCalculation(max(k, 10), min_rel_score) def calculate(self, data: InputData) -> TopKMetricResult: if self.no_feedback_users: diff --git a/src/evidently/legacy/metrics/recsys/hit_rate_k.py b/src/evidently/legacy/metrics/recsys/hit_rate_k.py index 5b08001bbf..b2a82c013a 100644 --- a/src/evidently/legacy/metrics/recsys/hit_rate_k.py +++ b/src/evidently/legacy/metrics/recsys/hit_rate_k.py @@ -1,3 +1,5 @@ +from typing import ClassVar +from typing import Dict from typing import Optional import pandas as pd @@ -13,13 +15,12 @@ class HitRateKMetricResult(MetricResult): - class Config: - type_alias = "evidently:metric_result:HitRateKMetricResult" - field_tags = { - "current": {IncludeTags.Current}, - "reference": {IncludeTags.Reference}, - "k": {IncludeTags.Parameter}, - } + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:HitRateKMetricResult" + __field_tags__: ClassVar[Dict[str, set]] = { + "current": {IncludeTags.Current}, + "reference": {IncludeTags.Reference}, + "k": {IncludeTags.Parameter}, + } k: int current: pd.Series @@ -27,11 +28,10 @@ class Config: class HitRateKMetric(Metric[HitRateKMetricResult]): - class Config: - type_alias = "evidently:metric:HitRateKMetric" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:HitRateKMetric" k: int - min_rel_score: Optional[int] + min_rel_score: Optional[int] = None no_feedback_users: bool def __init__( diff --git a/src/evidently/legacy/metrics/recsys/item_bias.py b/src/evidently/legacy/metrics/recsys/item_bias.py index cfee4e053d..b22b3d9c9d 100644 --- a/src/evidently/legacy/metrics/recsys/item_bias.py +++ b/src/evidently/legacy/metrics/recsys/item_bias.py @@ -1,3 +1,5 @@ +from typing import ClassVar +from typing import Dict from typing import List from typing import Optional @@ -21,16 +23,15 @@ class ItemBiasMetricResult(MetricResult): - class Config: - type_alias = "evidently:metric_result:ItemBiasMetricResult" - field_tags = { - "k": {IncludeTags.Parameter}, - "column_name": {IncludeTags.Parameter}, - "current_train_distr": {IncludeTags.Current}, - "current_distr": {IncludeTags.Current}, - "reference_train_distr": {IncludeTags.Reference}, - "reference_distr": {IncludeTags.Reference}, - } + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:ItemBiasMetricResult" + __field_tags__: ClassVar[Dict[str, set]] = { + "k": {IncludeTags.Parameter}, + "column_name": {IncludeTags.Parameter}, + "current_train_distr": {IncludeTags.Current}, + "current_distr": {IncludeTags.Current}, + "reference_train_distr": {IncludeTags.Reference}, + "reference_distr": {IncludeTags.Reference}, + } k: int column_name: str @@ -41,8 +42,7 @@ class Config: class ItemBiasMetric(Metric[ItemBiasMetricResult]): - class Config: - type_alias = "evidently:metric:ItemBiasMetric" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:ItemBiasMetric" k: int column_name: str diff --git a/src/evidently/legacy/metrics/recsys/map_k.py b/src/evidently/legacy/metrics/recsys/map_k.py index 2419929100..240991a93a 100644 --- a/src/evidently/legacy/metrics/recsys/map_k.py +++ b/src/evidently/legacy/metrics/recsys/map_k.py @@ -1,11 +1,13 @@ +from typing import ClassVar +from typing import Optional + from evidently.legacy.metrics.recsys.base_top_k import TopKMetric from evidently.legacy.metrics.recsys.base_top_k import TopKMetricRenderer from evidently.legacy.renderers.base_renderer import default_renderer class MAPKMetric(TopKMetric): - class Config: - type_alias = "evidently:metric:MAPKMetric" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:MAPKMetric" def key(self): return "map" diff --git a/src/evidently/legacy/metrics/recsys/mar_k.py b/src/evidently/legacy/metrics/recsys/mar_k.py index 2d24803497..a128ee85b8 100644 --- a/src/evidently/legacy/metrics/recsys/mar_k.py +++ b/src/evidently/legacy/metrics/recsys/mar_k.py @@ -1,11 +1,13 @@ +from typing import ClassVar +from typing import Optional + from evidently.legacy.metrics.recsys.base_top_k import TopKMetric from evidently.legacy.metrics.recsys.base_top_k import TopKMetricRenderer from evidently.legacy.renderers.base_renderer import default_renderer class MARKMetric(TopKMetric): - class Config: - type_alias = "evidently:metric:MARKMetric" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:MARKMetric" def key(self): return "mar" diff --git a/src/evidently/legacy/metrics/recsys/mrr.py b/src/evidently/legacy/metrics/recsys/mrr.py index b3f7e69e4a..263154f65e 100644 --- a/src/evidently/legacy/metrics/recsys/mrr.py +++ b/src/evidently/legacy/metrics/recsys/mrr.py @@ -1,3 +1,5 @@ +from typing import ClassVar +from typing import Dict from typing import Optional import pandas as pd @@ -13,13 +15,12 @@ class MRRKMetricResult(MetricResult): - class Config: - type_alias = "evidently:metric_result:MRRKMetricResult" - field_tags = { - "k": {IncludeTags.Parameter}, - "current": {IncludeTags.Current}, - "reference": {IncludeTags.Reference}, - } + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:MRRKMetricResult" + __field_tags__: ClassVar[Dict[str, set]] = { + "k": {IncludeTags.Parameter}, + "current": {IncludeTags.Current}, + "reference": {IncludeTags.Reference}, + } k: int current: pd.Series @@ -27,11 +28,10 @@ class Config: class MRRKMetric(Metric[MRRKMetricResult]): - class Config: - type_alias = "evidently:metric:MRRKMetric" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:MRRKMetric" k: int - min_rel_score: Optional[int] + min_rel_score: Optional[int] = None no_feedback_users: bool def __init__( diff --git a/src/evidently/legacy/metrics/recsys/ndcg_k.py b/src/evidently/legacy/metrics/recsys/ndcg_k.py index 1675db0af3..ced2443c79 100644 --- a/src/evidently/legacy/metrics/recsys/ndcg_k.py +++ b/src/evidently/legacy/metrics/recsys/ndcg_k.py @@ -1,3 +1,4 @@ +from typing import ClassVar from typing import Optional import numpy as np @@ -13,11 +14,10 @@ class NDCGKMetric(Metric[TopKMetricResult]): - class Config: - type_alias = "evidently:metric:NDCGKMetric" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:NDCGKMetric" k: int - min_rel_score: Optional[int] + min_rel_score: Optional[int] = None no_feedback_users: bool def __init__( diff --git a/src/evidently/legacy/metrics/recsys/novelty.py b/src/evidently/legacy/metrics/recsys/novelty.py index b971918498..b9afb7d150 100644 --- a/src/evidently/legacy/metrics/recsys/novelty.py +++ b/src/evidently/legacy/metrics/recsys/novelty.py @@ -1,3 +1,5 @@ +from typing import ClassVar +from typing import Dict from typing import List from typing import Optional @@ -26,15 +28,14 @@ class NoveltyMetricResult(MetricResult): - class Config: - type_alias = "evidently:metric_result:NoveltyMetricResult" - field_tags = { - "k": {IncludeTags.Parameter}, - "current_value": {IncludeTags.Current}, - "current_distr": {IncludeTags.Current}, - "reference_value": {IncludeTags.Reference}, - "reference_distr": {IncludeTags.Reference}, - } + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:NoveltyMetricResult" + __field_tags__: ClassVar[Dict[str, set]] = { + "k": {IncludeTags.Parameter}, + "current_value": {IncludeTags.Current}, + "current_distr": {IncludeTags.Current}, + "reference_value": {IncludeTags.Reference}, + "reference_distr": {IncludeTags.Reference}, + } k: int current_value: float @@ -44,8 +45,7 @@ class Config: class NoveltyMetric(Metric[NoveltyMetricResult]): - class Config: - type_alias = "evidently:metric:NoveltyMetric" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:NoveltyMetric" """Mean Inverse User Frequency""" @@ -54,8 +54,8 @@ class Config: def __init__(self, k: int, options: AnyOptions = None) -> None: self.k = k - self._train_stats = TrainStats() super().__init__(options=options) + self._train_stats = TrainStats() def get_miuf( self, df, k, recommendations_type: Optional[RecomType], user_name, item_name, prediction_name, interactions diff --git a/src/evidently/legacy/metrics/recsys/pairwise_distance.py b/src/evidently/legacy/metrics/recsys/pairwise_distance.py index 8702ff10ab..238e177021 100644 --- a/src/evidently/legacy/metrics/recsys/pairwise_distance.py +++ b/src/evidently/legacy/metrics/recsys/pairwise_distance.py @@ -1,9 +1,13 @@ +from typing import Annotated +from typing import ClassVar from typing import Dict from typing import List +from typing import Optional from typing import Union import numpy as np import pandas as pd +from pydantic import BeforeValidator from sklearn.metrics import pairwise_distances from evidently.legacy.base_metric import InputData @@ -11,6 +15,8 @@ from evidently.legacy.base_metric import MetricResult from evidently.legacy.calculations.recommender_systems import get_prediciton_name from evidently.legacy.core import IncludeTags +from evidently.legacy.core import PydanticNPArray +from evidently.legacy.core import try_int_validator from evidently.legacy.model.widget import BaseWidgetInfo from evidently.legacy.options.base import AnyOptions from evidently.legacy.pipeline.column_mapping import RecomType @@ -19,18 +25,16 @@ class PairwiseDistanceResult(MetricResult): - class Config: - type_alias = "evidently:metric_result:PairwiseDistanceResult" - pd_include = False - field_tags = {"dist_matrix": {IncludeTags.Extra}} + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:PairwiseDistanceResult" + __pd_include__: ClassVar[bool] = False + __field_tags__: ClassVar[Dict[str, set]] = {"dist_matrix": {IncludeTags.Extra}} - dist_matrix: np.ndarray - name_dict: Dict[Union[int, str], int] + dist_matrix: PydanticNPArray + name_dict: Dict[Annotated[Union[int, str], BeforeValidator(try_int_validator)], int] class PairwiseDistance(Metric[PairwiseDistanceResult]): - class Config: - type_alias = "evidently:metric:PairwiseDistance" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:PairwiseDistance" k: int item_features: List[str] diff --git a/src/evidently/legacy/metrics/recsys/personalisation.py b/src/evidently/legacy/metrics/recsys/personalisation.py index 54cda0447a..22f2a5a5f1 100644 --- a/src/evidently/legacy/metrics/recsys/personalisation.py +++ b/src/evidently/legacy/metrics/recsys/personalisation.py @@ -1,4 +1,6 @@ +from typing import Annotated from typing import Any +from typing import ClassVar from typing import Dict from typing import List from typing import Optional @@ -22,31 +24,29 @@ from evidently.legacy.renderers.html_widgets import header_text from evidently.legacy.renderers.html_widgets import table_data from evidently.legacy.renderers.html_widgets import widget_tabs +from evidently.pydantic_utils import StrKeyValidator class PersonalizationMetricResult(MetricResult): - class Config: - type_alias = "evidently:metric_result:PersonalizationMetricResult" - pd_include = False - - field_tags = { - "k": {IncludeTags.Parameter}, - "current_value": {IncludeTags.Current}, - "current_table": {IncludeTags.Current, IncludeTags.Extra}, - "reference_value": {IncludeTags.Reference}, - "reference_table": {IncludeTags.Reference, IncludeTags.Extra}, - } + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:PersonalizationMetricResult" + __pd_include__: ClassVar[bool] = False + __field_tags__: ClassVar[Dict[str, set]] = { + "k": {IncludeTags.Parameter}, + "current_value": {IncludeTags.Current}, + "current_table": {IncludeTags.Current, IncludeTags.Extra}, + "reference_value": {IncludeTags.Reference}, + "reference_table": {IncludeTags.Reference, IncludeTags.Extra}, + } k: int current_value: float - current_table: Dict[str, int] + current_table: Annotated[Dict[str, int], StrKeyValidator] reference_value: Optional[float] = None - reference_table: Optional[Dict[str, int]] = None + reference_table: Annotated[Optional[Dict[str, int]], StrKeyValidator] = None class PersonalizationMetric(Metric[PersonalizationMetricResult]): - class Config: - type_alias = "evidently:metric:PersonalizationMetric" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:PersonalizationMetric" """Mean Inter List""" diff --git a/src/evidently/legacy/metrics/recsys/popularity_bias.py b/src/evidently/legacy/metrics/recsys/popularity_bias.py index d00f35a399..cd113a70f6 100644 --- a/src/evidently/legacy/metrics/recsys/popularity_bias.py +++ b/src/evidently/legacy/metrics/recsys/popularity_bias.py @@ -1,3 +1,5 @@ +from typing import ClassVar +from typing import Dict from typing import List from typing import Optional @@ -26,20 +28,19 @@ class PopularityBiasResult(MetricResult): - class Config: - type_alias = "evidently:metric_result:PopularityBiasResult" - field_tags = { - "k": {IncludeTags.Parameter}, - "normalize_arp": {IncludeTags.Parameter}, - "current_apr": {IncludeTags.Current}, - "current_coverage": {IncludeTags.Current}, - "current_gini": {IncludeTags.Current}, - "current_distr": {IncludeTags.Current}, - "reference_apr": {IncludeTags.Reference}, - "reference_coverage": {IncludeTags.Reference}, - "reference_gini": {IncludeTags.Reference}, - "reference_distr": {IncludeTags.Reference}, - } + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:PopularityBiasResult" + __field_tags__: ClassVar[Dict[str, set]] = { + "k": {IncludeTags.Parameter}, + "normalize_arp": {IncludeTags.Parameter}, + "current_apr": {IncludeTags.Current}, + "current_coverage": {IncludeTags.Current}, + "current_gini": {IncludeTags.Current}, + "current_distr": {IncludeTags.Current}, + "reference_apr": {IncludeTags.Reference}, + "reference_coverage": {IncludeTags.Reference}, + "reference_gini": {IncludeTags.Reference}, + "reference_distr": {IncludeTags.Reference}, + } k: int normalize_arp: bool @@ -54,8 +55,7 @@ class Config: class PopularityBias(Metric[PopularityBiasResult]): - class Config: - type_alias = "evidently:metric:PopularityBias" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:PopularityBias" """ Average Recommendation Popularity @@ -70,8 +70,8 @@ class Config: def __init__(self, k: int, normalize_arp: bool = False, options: AnyOptions = None) -> None: self.k = k self.normalize_arp = normalize_arp - self._train_stats = TrainStats() super().__init__(options=options) + self._train_stats = TrainStats() def get_apr( self, diff --git a/src/evidently/legacy/metrics/recsys/precision_recall_k.py b/src/evidently/legacy/metrics/recsys/precision_recall_k.py index f84bef541d..de95c419ad 100644 --- a/src/evidently/legacy/metrics/recsys/precision_recall_k.py +++ b/src/evidently/legacy/metrics/recsys/precision_recall_k.py @@ -1,4 +1,5 @@ import warnings +from typing import ClassVar from typing import Dict from typing import List from typing import Optional @@ -29,21 +30,19 @@ def safe_pd_column_expanding_and_sum(df: pd.DataFrame): class PrecisionRecallCalculationResult(MetricResult): - class Config: - type_alias = "evidently:metric_result:PrecisionRecallCalculationResult" - pd_include = False - field_tags = {"current": {IncludeTags.Current}, "reference": {IncludeTags.Reference}} + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:PrecisionRecallCalculationResult" + __pd_include__: ClassVar[bool] = False + __field_tags__: ClassVar[Dict[str, set]] = {"current": {IncludeTags.Current}, "reference": {IncludeTags.Reference}} current: Dict[str, list] reference: Optional[Dict[str, list]] = None class PrecisionRecallCalculation(Metric[PrecisionRecallCalculationResult]): - class Config: - type_alias = "evidently:metric:PrecisionRecallCalculation" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:PrecisionRecallCalculation" max_k: int - min_rel_score: Optional[int] + min_rel_score: Optional[int] = None def __init__(self, max_k: int, min_rel_score: Optional[int] = None, options: AnyOptions = None) -> None: self.max_k = max_k diff --git a/src/evidently/legacy/metrics/recsys/precision_top_k.py b/src/evidently/legacy/metrics/recsys/precision_top_k.py index bd7a6de318..f8a2cc823c 100644 --- a/src/evidently/legacy/metrics/recsys/precision_top_k.py +++ b/src/evidently/legacy/metrics/recsys/precision_top_k.py @@ -1,11 +1,13 @@ +from typing import ClassVar +from typing import Optional + from evidently.legacy.metrics.recsys.base_top_k import TopKMetric from evidently.legacy.metrics.recsys.base_top_k import TopKMetricRenderer from evidently.legacy.renderers.base_renderer import default_renderer class PrecisionTopKMetric(TopKMetric): - class Config: - type_alias = "evidently:metric:PrecisionTopKMetric" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:PrecisionTopKMetric" def key(self): return "precision" diff --git a/src/evidently/legacy/metrics/recsys/rec_examples.py b/src/evidently/legacy/metrics/recsys/rec_examples.py index 234df3ec64..74d68cd7c0 100644 --- a/src/evidently/legacy/metrics/recsys/rec_examples.py +++ b/src/evidently/legacy/metrics/recsys/rec_examples.py @@ -1,4 +1,5 @@ from functools import reduce +from typing import ClassVar from typing import Dict from typing import List from typing import Optional @@ -26,16 +27,15 @@ class RecCasesTableResults(MetricResult): - class Config: - type_alias = "evidently:metric_result:RecCasesTableResults" - pd_include = False - field_tags = { - "user_ids": {IncludeTags.Extra}, - "current": {IncludeTags.Current, IncludeTags.Extra}, - "reference": {IncludeTags.Reference}, - "current_train": {IncludeTags.Current, IncludeTags.Extra}, - "reference_train": {IncludeTags.Reference}, - } + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:RecCasesTableResults" + __pd_include__: ClassVar[bool] = False + __field_tags__: ClassVar[Dict[str, set]] = { + "user_ids": {IncludeTags.Extra}, + "current": {IncludeTags.Current, IncludeTags.Extra}, + "reference": {IncludeTags.Reference}, + "current_train": {IncludeTags.Current, IncludeTags.Extra}, + "reference_train": {IncludeTags.Reference}, + } user_ids: List[str] current: Dict[str, pd.DataFrame] @@ -45,12 +45,11 @@ class Config: class RecCasesTable(Metric[RecCasesTableResults]): - class Config: - type_alias = "evidently:metric:RecCasesTable" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:RecCasesTable" - user_ids: Optional[List[Union[int, str]]] - display_features: Optional[List[str]] - item_num: Optional[int] + user_ids: Optional[List[Union[int, str]]] = None + display_features: Optional[List[str]] = None + item_num: Optional[int] = None train_item_num: int def __init__( diff --git a/src/evidently/legacy/metrics/recsys/recall_top_k.py b/src/evidently/legacy/metrics/recsys/recall_top_k.py index 45eabb368f..d5cf2c2a19 100644 --- a/src/evidently/legacy/metrics/recsys/recall_top_k.py +++ b/src/evidently/legacy/metrics/recsys/recall_top_k.py @@ -1,11 +1,13 @@ +from typing import ClassVar +from typing import Optional + from evidently.legacy.metrics.recsys.base_top_k import TopKMetric from evidently.legacy.metrics.recsys.base_top_k import TopKMetricRenderer from evidently.legacy.renderers.base_renderer import default_renderer class RecallTopKMetric(TopKMetric): - class Config: - type_alias = "evidently:metric:RecallTopKMetric" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:RecallTopKMetric" def key(self): return "recall" diff --git a/src/evidently/legacy/metrics/recsys/scores_distribution.py b/src/evidently/legacy/metrics/recsys/scores_distribution.py index 00d8af8f30..d6d2629016 100644 --- a/src/evidently/legacy/metrics/recsys/scores_distribution.py +++ b/src/evidently/legacy/metrics/recsys/scores_distribution.py @@ -1,3 +1,5 @@ +from typing import ClassVar +from typing import Dict from typing import List from typing import Optional from typing import Tuple @@ -27,17 +29,16 @@ class ScoreDistributionResult(MetricResult): - class Config: - type_alias = "evidently:metric_result:ScoreDistributionResult" - field_tags = { - "k": {IncludeTags.Parameter}, - "current_top_k_distr": {IncludeTags.Current}, - "current_other_distr": {IncludeTags.Current}, - "current_entropy": {IncludeTags.Current}, - "reference_top_k_distr": {IncludeTags.Reference}, - "reference_other_distr": {IncludeTags.Reference}, - "reference_entropy": {IncludeTags.Reference}, - } + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:ScoreDistributionResult" + __field_tags__: ClassVar[Dict[str, set]] = { + "k": {IncludeTags.Parameter}, + "current_top_k_distr": {IncludeTags.Current}, + "current_other_distr": {IncludeTags.Current}, + "current_entropy": {IncludeTags.Current}, + "reference_top_k_distr": {IncludeTags.Reference}, + "reference_other_distr": {IncludeTags.Reference}, + "reference_entropy": {IncludeTags.Reference}, + } k: int current_top_k_distr: Distribution @@ -49,8 +50,7 @@ class Config: class ScoreDistribution(Metric[ScoreDistributionResult]): - class Config: - type_alias = "evidently:metric:ScoreDistribution" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:ScoreDistribution" k: int diff --git a/src/evidently/legacy/metrics/recsys/serendipity.py b/src/evidently/legacy/metrics/recsys/serendipity.py index 64f5e32722..d396b0e775 100644 --- a/src/evidently/legacy/metrics/recsys/serendipity.py +++ b/src/evidently/legacy/metrics/recsys/serendipity.py @@ -1,5 +1,6 @@ from itertools import product from typing import Any +from typing import ClassVar from typing import Dict from typing import List from typing import Optional @@ -29,15 +30,14 @@ class SerendipityMetricResult(MetricResult): - class Config: - type_alias = "evidently:metric_result:SerendipityMetricResult" - field_tags = { - "k": {IncludeTags.Parameter}, - "current_value": {IncludeTags.Current}, - "current_distr": {IncludeTags.Current}, - "reference_value": {IncludeTags.Reference}, - "reference_distr": {IncludeTags.Reference}, - } + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:SerendipityMetricResult" + __field_tags__: ClassVar[Dict[str, set]] = { + "k": {IncludeTags.Parameter}, + "current_value": {IncludeTags.Current}, + "current_distr": {IncludeTags.Current}, + "reference_value": {IncludeTags.Reference}, + "reference_distr": {IncludeTags.Reference}, + } k: int current_value: float @@ -47,15 +47,14 @@ class Config: class SerendipityMetric(Metric[SerendipityMetricResult]): - class Config: - type_alias = "evidently:metric:SerendipityMetric" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:SerendipityMetric" """unusualness * relevance""" _pairwise_distance: PairwiseDistance k: int item_features: List[str] - min_rel_score: Optional[int] + min_rel_score: Optional[int] = None def __init__( self, k: int, item_features: List[str], min_rel_score: Optional[int] = None, options: AnyOptions = None @@ -63,8 +62,8 @@ def __init__( self.k = k self.item_features = item_features self.min_rel_score = min_rel_score - self._pairwise_distance = PairwiseDistance(k=k, item_features=item_features) super().__init__(options=options) + self._pairwise_distance = PairwiseDistance(k=k, item_features=item_features) def get_serendipity( self, diff --git a/src/evidently/legacy/metrics/recsys/train_stats.py b/src/evidently/legacy/metrics/recsys/train_stats.py index 6981d5298a..c67d1e6200 100644 --- a/src/evidently/legacy/metrics/recsys/train_stats.py +++ b/src/evidently/legacy/metrics/recsys/train_stats.py @@ -1,3 +1,5 @@ +from typing import ClassVar +from typing import Dict from typing import List from typing import Optional @@ -14,14 +16,13 @@ class TrainStatsResult(MetricResult): - class Config: - type_alias = "evidently:metric_result:TrainStatsResult" - field_tags = { - "current": {IncludeTags.Current}, - "current_n_users": {IncludeTags.Current}, - "reference": {IncludeTags.Reference}, - "reference_n_users": {IncludeTags.Reference}, - } + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:TrainStatsResult" + __field_tags__: ClassVar[Dict[str, set]] = { + "current": {IncludeTags.Current}, + "current_n_users": {IncludeTags.Current}, + "reference": {IncludeTags.Reference}, + "reference_n_users": {IncludeTags.Reference}, + } current: pd.Series current_n_users: int @@ -30,8 +31,7 @@ class Config: class TrainStats(Metric[TrainStatsResult]): - class Config: - type_alias = "evidently:metric:TrainStats" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:TrainStats" """Calculates the number of times each item has been rated in the training set""" diff --git a/src/evidently/legacy/metrics/recsys/user_bias.py b/src/evidently/legacy/metrics/recsys/user_bias.py index 74a6335f13..f23cecfd17 100644 --- a/src/evidently/legacy/metrics/recsys/user_bias.py +++ b/src/evidently/legacy/metrics/recsys/user_bias.py @@ -1,3 +1,5 @@ +from typing import ClassVar +from typing import Dict from typing import List from typing import Optional @@ -19,15 +21,14 @@ class UserBiasMetricResult(MetricResult): - class Config: - type_alias = "evidently:metric_result:UserBiasMetricResult" - field_tags = { - "column_name": {IncludeTags.Parameter}, - "current_train_distr": {IncludeTags.Current}, - "current_distr": {IncludeTags.Current}, - "reference_train_distr": {IncludeTags.Reference}, - "reference_distr": {IncludeTags.Reference}, - } + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:UserBiasMetricResult" + __field_tags__: ClassVar[Dict[str, set]] = { + "column_name": {IncludeTags.Parameter}, + "current_train_distr": {IncludeTags.Current}, + "current_distr": {IncludeTags.Current}, + "reference_train_distr": {IncludeTags.Reference}, + "reference_distr": {IncludeTags.Reference}, + } column_name: str current_train_distr: Distribution @@ -37,8 +38,7 @@ class Config: class UserBiasMetric(Metric[UserBiasMetricResult]): - class Config: - type_alias = "evidently:metric:UserBiasMetric" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:UserBiasMetric" column_name: str diff --git a/src/evidently/legacy/metrics/regression_performance/abs_perc_error_in_time.py b/src/evidently/legacy/metrics/regression_performance/abs_perc_error_in_time.py index b01a17c112..08112f075e 100644 --- a/src/evidently/legacy/metrics/regression_performance/abs_perc_error_in_time.py +++ b/src/evidently/legacy/metrics/regression_performance/abs_perc_error_in_time.py @@ -1,3 +1,4 @@ +from typing import ClassVar from typing import List from typing import Optional from typing import Union @@ -24,8 +25,7 @@ class RegressionAbsPercentageErrorPlot(UsesRawDataMixin, Metric[ColumnScatterResult]): - class Config: - type_alias = "evidently:metric:RegressionAbsPercentageErrorPlot" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:RegressionAbsPercentageErrorPlot" def __init__(self, options: AnyOptions = None): super().__init__(options=options) diff --git a/src/evidently/legacy/metrics/regression_performance/error_bias_table.py b/src/evidently/legacy/metrics/regression_performance/error_bias_table.py index 0e88288ff1..8968a6e391 100644 --- a/src/evidently/legacy/metrics/regression_performance/error_bias_table.py +++ b/src/evidently/legacy/metrics/regression_performance/error_bias_table.py @@ -35,28 +35,26 @@ class RegressionErrorBiasTableResults(MetricResult): - class Config: - type_alias = "evidently:metric_result:RegressionErrorBiasTableResults" - dict_exclude_fields = {"current_plot_data", "reference_plot_data"} - pd_exclude_fields = { - "current_plot_data", - "reference_plot_data", - "num_feature_names", - "cat_feature_names", - "error_bias", - "columns", - } - - field_tags = { - "current_plot_data": {IncludeTags.Current, IncludeTags.Render}, - "reference_plot_data": {IncludeTags.Reference, IncludeTags.Render}, - "target_name": {IncludeTags.Parameter}, - "prediction_name": {IncludeTags.Parameter}, - "num_feature_names": {IncludeTags.Parameter}, - "cat_feature_names": {IncludeTags.Parameter}, - "columns": {IncludeTags.Parameter}, - "error_bias": {IncludeTags.Extra}, - } + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:RegressionErrorBiasTableResults" + __dict_exclude_fields__: ClassVar[set] = {"current_plot_data", "reference_plot_data"} + __pd_exclude_fields__: ClassVar[set] = { + "current_plot_data", + "reference_plot_data", + "num_feature_names", + "cat_feature_names", + "error_bias", + "columns", + } + __field_tags__: ClassVar[Dict[str, set]] = { + "current_plot_data": {IncludeTags.Current, IncludeTags.Render}, + "reference_plot_data": {IncludeTags.Reference, IncludeTags.Render}, + "target_name": {IncludeTags.Parameter}, + "prediction_name": {IncludeTags.Parameter}, + "num_feature_names": {IncludeTags.Parameter}, + "cat_feature_names": {IncludeTags.Parameter}, + "columns": {IncludeTags.Parameter}, + "error_bias": {IncludeTags.Extra}, + } top_error: float current_plot_data: pd.DataFrame @@ -71,15 +69,14 @@ class Config: class RegressionErrorBiasTable(UsesRawDataMixin, Metric[RegressionErrorBiasTableResults]): # by default, we get 5% values for the error bias calculations - class Config: - type_alias = "evidently:metric:RegressionErrorBiasTable" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:RegressionErrorBiasTable" TOP_ERROR_DEFAULT: ClassVar[float] = 0.05 TOP_ERROR_MIN: ClassVar[float] = 0 TOP_ERROR_MAX: ClassVar[float] = 0.5 top_error: float - columns: Optional[List[str]] - descriptors: Optional[Dict[str, Dict[str, FeatureDescriptor]]] + columns: Optional[List[str]] = None + descriptors: Optional[Dict[str, Dict[str, FeatureDescriptor]]] = None _text_features_gen: Optional[Dict[str, Dict[str, GeneratedFeature]]] def __init__( @@ -96,9 +93,9 @@ def __init__( self.top_error = top_error self.columns = columns - self._text_features_gen = None self.descriptors = descriptors super().__init__(options=options) + self._text_features_gen = None def required_features(self, data_definition: DataDefinition): if len(data_definition.get_columns(ColumnType.Text, features_only=True)) > 0: diff --git a/src/evidently/legacy/metrics/regression_performance/error_distribution.py b/src/evidently/legacy/metrics/regression_performance/error_distribution.py index 7b23db7571..8fc239a19a 100644 --- a/src/evidently/legacy/metrics/regression_performance/error_distribution.py +++ b/src/evidently/legacy/metrics/regression_performance/error_distribution.py @@ -1,3 +1,5 @@ +from typing import ClassVar +from typing import Dict from typing import List from typing import Optional @@ -19,20 +21,20 @@ class RegressionErrorDistributionResults(MetricResult): - class Config: - type_alias = "evidently:metric_result:RegressionErrorDistributionResults" - dict_exclude_fields = {"current_bins", "reference_bins"} - pd_exclude_fields = {"current_bins", "reference_bins"} - - field_tags = {"current_bins": {IncludeTags.Current}, "reference_bins": {IncludeTags.Reference}} + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:RegressionErrorDistributionResults" + __dict_exclude_fields__: ClassVar[set] = {"current_bins", "reference_bins"} + __pd_exclude_fields__: ClassVar[set] = {"current_bins", "reference_bins"} + __field_tags__: ClassVar[Dict[str, set]] = { + "current_bins": {IncludeTags.Current}, + "reference_bins": {IncludeTags.Reference}, + } current_bins: HistogramData reference_bins: Optional[HistogramData] class RegressionErrorDistribution(Metric[RegressionErrorDistributionResults]): - class Config: - type_alias = "evidently:metric:RegressionErrorDistribution" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:RegressionErrorDistribution" def calculate(self, data: InputData) -> RegressionErrorDistributionResults: dataset_columns = process_columns(data.current_data, data.column_mapping) diff --git a/src/evidently/legacy/metrics/regression_performance/error_in_time.py b/src/evidently/legacy/metrics/regression_performance/error_in_time.py index 87935cd473..fb9cbd0444 100644 --- a/src/evidently/legacy/metrics/regression_performance/error_in_time.py +++ b/src/evidently/legacy/metrics/regression_performance/error_in_time.py @@ -1,3 +1,4 @@ +from typing import ClassVar from typing import Dict from typing import List from typing import Optional @@ -23,8 +24,7 @@ class RegressionErrorPlot(UsesRawDataMixin, Metric[ColumnScatterResult]): - class Config: - type_alias = "evidently:metric:RegressionErrorPlot" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:RegressionErrorPlot" def __init__(self, options: AnyOptions = None): super().__init__(options=options) diff --git a/src/evidently/legacy/metrics/regression_performance/error_normality.py b/src/evidently/legacy/metrics/regression_performance/error_normality.py index c594f19d03..869399e9bb 100644 --- a/src/evidently/legacy/metrics/regression_performance/error_normality.py +++ b/src/evidently/legacy/metrics/regression_performance/error_normality.py @@ -1,5 +1,7 @@ import json from typing import Any +from typing import ClassVar +from typing import Dict from typing import List from typing import Optional from typing import Union @@ -25,17 +27,25 @@ class RegressionErrorNormalityResults(MetricResult): - class Config: - type_alias = "evidently:metric_result:RegressionErrorNormalityResults" - dict_exclude_fields = {"current_plot", "current_theoretical", "reference_plot", "reference_theoretical"} - pd_exclude_fields = {"current_plot", "current_theoretical", "reference_plot", "reference_theoretical"} - - field_tags = { - "current_plot": {IncludeTags.Render, IncludeTags.Current}, - "current_theoretical": {IncludeTags.Extra, IncludeTags.Current}, - "reference_plot": {IncludeTags.Render, IncludeTags.Reference}, - "reference_theoretical": {IncludeTags.Extra, IncludeTags.Reference}, - } + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:RegressionErrorNormalityResults" + __dict_exclude_fields__: ClassVar[set] = { + "current_plot", + "current_theoretical", + "reference_plot", + "reference_theoretical", + } + __pd_exclude_fields__: ClassVar[set] = { + "current_plot", + "current_theoretical", + "reference_plot", + "reference_theoretical", + } + __field_tags__: ClassVar[Dict[str, set]] = { + "current_plot": {IncludeTags.Render, IncludeTags.Current}, + "current_theoretical": {IncludeTags.Extra, IncludeTags.Current}, + "reference_plot": {IncludeTags.Render, IncludeTags.Reference}, + "reference_theoretical": {IncludeTags.Extra, IncludeTags.Reference}, + } current_plot: pd.DataFrame current_theoretical: pd.DataFrame @@ -44,8 +54,7 @@ class Config: class RegressionErrorNormality(UsesRawDataMixin, Metric[RegressionErrorNormalityResults]): - class Config: - type_alias = "evidently:metric:RegressionErrorNormality" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:RegressionErrorNormality" def __init__(self, options: AnyOptions = None): super().__init__(options=options) diff --git a/src/evidently/legacy/metrics/regression_performance/objects.py b/src/evidently/legacy/metrics/regression_performance/objects.py index 838d005eb5..99ab641f4a 100644 --- a/src/evidently/legacy/metrics/regression_performance/objects.py +++ b/src/evidently/legacy/metrics/regression_performance/objects.py @@ -1,3 +1,4 @@ +from typing import ClassVar from typing import Dict from typing import List from typing import Optional @@ -12,8 +13,7 @@ class PredActualScatter(MetricResult): - class Config: - type_alias = "evidently:metric_result:PredActualScatter" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:PredActualScatter" predicted: ScatterData actual: ScatterData @@ -30,12 +30,11 @@ def scatter_as_dict(scatter: Optional[PredActualScatter]) -> Optional[Dict[str, def scatter_as_dict(scatter: Optional[PredActualScatter]) -> Optional[Dict[str, ScatterData]]: if scatter is None: return None - return scatter.dict() + return scatter.model_dump() class RegressionScatter(MetricResult): - class Config: - type_alias = "evidently:metric_result:RegressionScatter" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:RegressionScatter" underestimation: PredActualScatter majority: PredActualScatter @@ -43,9 +42,7 @@ class Config: class IntervalSeries(MetricResult): - class Config: - type_alias = "evidently:metric_result:IntervalSeries" - underscore_attrs_are_private = True + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:IntervalSeries" bins: List[float] values: List[float] @@ -75,10 +72,8 @@ def __mul__(self, other: float): class RegressionMetricScatter(MetricResult): - class Config: - type_alias = "evidently:metric_result:RegressionMetricScatter" - smart_union = True - field_tags = {"current": {IncludeTags.Current}, "reference": {IncludeTags.Reference}} + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:RegressionMetricScatter" + __field_tags__: ClassVar[Dict[str, set]] = {"current": {IncludeTags.Current}, "reference": {IncludeTags.Reference}} current: IntervalSeries reference: Optional[IntervalSeries] = None @@ -90,8 +85,7 @@ def __mul__(self, other: float): class RegressionMetricsScatter(MetricResult): - class Config: - type_alias = "evidently:metric_result:RegressionMetricsScatter" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:RegressionMetricsScatter" r2_score: RegressionMetricScatter rmse: RegressionMetricScatter diff --git a/src/evidently/legacy/metrics/regression_performance/predicted_and_actual_in_time.py b/src/evidently/legacy/metrics/regression_performance/predicted_and_actual_in_time.py index 91056e64eb..c0a61978dd 100644 --- a/src/evidently/legacy/metrics/regression_performance/predicted_and_actual_in_time.py +++ b/src/evidently/legacy/metrics/regression_performance/predicted_and_actual_in_time.py @@ -1,3 +1,4 @@ +from typing import ClassVar from typing import List from typing import Optional from typing import Union @@ -22,8 +23,7 @@ class RegressionPredictedVsActualPlot(UsesRawDataMixin, Metric[ColumnScatterResult]): - class Config: - type_alias = "evidently:metric:RegressionPredictedVsActualPlot" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:RegressionPredictedVsActualPlot" def __init__(self, options: AnyOptions = None): super().__init__(options=options) diff --git a/src/evidently/legacy/metrics/regression_performance/predicted_vs_actual.py b/src/evidently/legacy/metrics/regression_performance/predicted_vs_actual.py index de7074060c..879c4b5fc3 100644 --- a/src/evidently/legacy/metrics/regression_performance/predicted_vs_actual.py +++ b/src/evidently/legacy/metrics/regression_performance/predicted_vs_actual.py @@ -1,3 +1,5 @@ +from typing import ClassVar +from typing import Dict from typing import List from typing import Optional from typing import Union @@ -28,20 +30,18 @@ class AggPredActualScatter(MetricResult): - class Config: - type_alias = "evidently:metric_result:AggPredActualScatter" - dict_include = False - tags = {IncludeTags.Render} + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:AggPredActualScatter" + __dict_include__: ClassVar[bool] = False + __tags__: ClassVar[set] = {IncludeTags.Render} data: Optional[ContourData] class RegressionPredictedVsActualScatterResults(MetricResult): - class Config: - type_alias = "evidently:metric_result:RegressionPredictedVsActualScatterResults" - dict_include = False - tags = {IncludeTags.Render} - field_tags = {"current": {IncludeTags.Current}, "reference": {IncludeTags.Reference}} + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:RegressionPredictedVsActualScatterResults" + __dict_include__: ClassVar[bool] = False + __tags__: ClassVar[set] = {IncludeTags.Render} + __field_tags__: ClassVar[Dict[str, set]] = {"current": {IncludeTags.Current}, "reference": {IncludeTags.Reference}} current: Union[PredActualScatter, AggPredActualScatter] reference: Optional[Union[PredActualScatter, AggPredActualScatter]] @@ -52,8 +52,7 @@ class Config: class RegressionPredictedVsActualScatter(UsesRawDataMixin, Metric[RegressionPredictedVsActualScatterResults]): - class Config: - type_alias = "evidently:metric:RegressionPredictedVsActualScatter" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:RegressionPredictedVsActualScatter" def __init__(self, options: AnyOptions = None): super().__init__(options=options) diff --git a/src/evidently/legacy/metrics/regression_performance/regression_dummy_metric.py b/src/evidently/legacy/metrics/regression_performance/regression_dummy_metric.py index 0b044078f5..a6a3c32f55 100644 --- a/src/evidently/legacy/metrics/regression_performance/regression_dummy_metric.py +++ b/src/evidently/legacy/metrics/regression_performance/regression_dummy_metric.py @@ -1,3 +1,4 @@ +from typing import ClassVar from typing import List from typing import Optional @@ -21,8 +22,7 @@ class RegressionDummyMetricResults(MetricResult): - class Config: - type_alias = "evidently:metric_result:RegressionDummyMetricResults" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:RegressionDummyMetricResults" rmse_default: float mean_abs_error_default: float @@ -39,8 +39,7 @@ class Config: class RegressionDummyMetric(Metric[RegressionDummyMetricResults]): - class Config: - type_alias = "evidently:metric:RegressionDummyMetric" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:RegressionDummyMetric" _quality_metric: RegressionQualityMetric diff --git a/src/evidently/legacy/metrics/regression_performance/regression_performance_metrics.py b/src/evidently/legacy/metrics/regression_performance/regression_performance_metrics.py index 30d175fda4..58dfe50ddf 100644 --- a/src/evidently/legacy/metrics/regression_performance/regression_performance_metrics.py +++ b/src/evidently/legacy/metrics/regression_performance/regression_performance_metrics.py @@ -1,3 +1,4 @@ +from typing import ClassVar from typing import Dict from typing import List from typing import Optional @@ -31,10 +32,9 @@ class RegressionMetrics(MetricResult): - class Config: - type_alias = "evidently:metric_result:RegressionMetrics" - pd_exclude_fields = {"underperformance"} - field_tags = {"underperformance": {IncludeTags.Extra}} + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:RegressionMetrics" + __pd_exclude_fields__: ClassVar[set] = {"underperformance"} + __field_tags__: ClassVar[Dict[str, set]] = {"underperformance": {IncludeTags.Extra}} r2_score: float rmse: float @@ -46,25 +46,30 @@ class Config: class RegressionPerformanceMetricsResults(MetricResult): - class Config: - type_alias = "evidently:metric_result:RegressionPerformanceMetricsResults" - dict_exclude_fields = {"hist_for_plot", "vals_for_plots", "me_hist_for_plot"} - pd_exclude_fields = {"hist_for_plot", "vals_for_plots", "me_hist_for_plot", "error_bias", "error_normality"} - field_tags = { - "current": {IncludeTags.Current}, - "reference": {IncludeTags.Reference}, - "rmse_default": {IncludeTags.Extra}, - "me_default_sigma": {IncludeTags.Extra}, - "mean_abs_error_default": {IncludeTags.Extra}, - "mean_abs_perc_error_default": {IncludeTags.Extra}, - "abs_error_max_default": {IncludeTags.Extra}, - "error_std": {IncludeTags.Extra}, - "abs_error_std": {IncludeTags.Extra}, - "abs_perc_error_std": {IncludeTags.Extra}, - "error_normality": {IncludeTags.Extra}, - "vals_for_plots": {IncludeTags.Render}, - "error_bias": {IncludeTags.Extra}, - } + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:RegressionPerformanceMetricsResults" + __dict_exclude_fields__: ClassVar[set] = {"hist_for_plot", "vals_for_plots", "me_hist_for_plot"} + __pd_exclude_fields__: ClassVar[set] = { + "hist_for_plot", + "vals_for_plots", + "me_hist_for_plot", + "error_bias", + "error_normality", + } + __field_tags__: ClassVar[Dict[str, set]] = { + "current": {IncludeTags.Current}, + "reference": {IncludeTags.Reference}, + "rmse_default": {IncludeTags.Extra}, + "me_default_sigma": {IncludeTags.Extra}, + "mean_abs_error_default": {IncludeTags.Extra}, + "mean_abs_perc_error_default": {IncludeTags.Extra}, + "abs_error_max_default": {IncludeTags.Extra}, + "error_std": {IncludeTags.Extra}, + "abs_error_std": {IncludeTags.Extra}, + "abs_perc_error_std": {IncludeTags.Extra}, + "error_normality": {IncludeTags.Extra}, + "vals_for_plots": {IncludeTags.Render}, + "error_bias": {IncludeTags.Extra}, + } columns: DatasetColumns @@ -88,8 +93,7 @@ class Config: class RegressionPerformanceMetrics(Metric[RegressionPerformanceMetricsResults]): - class Config: - type_alias = "evidently:metric:RegressionPerformanceMetrics" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:RegressionPerformanceMetrics" def get_parameters(self) -> tuple: return () diff --git a/src/evidently/legacy/metrics/regression_performance/regression_quality.py b/src/evidently/legacy/metrics/regression_performance/regression_quality.py index d775ec5202..9c00157b31 100644 --- a/src/evidently/legacy/metrics/regression_performance/regression_quality.py +++ b/src/evidently/legacy/metrics/regression_performance/regression_quality.py @@ -1,3 +1,4 @@ +from typing import ClassVar from typing import Dict from typing import List from typing import Optional @@ -33,9 +34,8 @@ class MoreRegressionMetrics(RegressionMetrics): - class Config: - type_alias = "evidently:metric_result:MoreRegressionMetrics" - field_tags: Dict[str, Set[IncludeTags]] = {"underperformance": set()} + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:MoreRegressionMetrics" + __field_tags__: ClassVar[Dict[str, Set[IncludeTags]]] = {"underperformance": set()} error_std: float abs_error_std: float @@ -43,22 +43,27 @@ class Config: class RegressionQualityMetricResults(MetricResult): - class Config: - type_alias = "evidently:metric_result:RegressionQualityMetricResults" - dict_exclude_fields = {"hist_for_plot", "vals_for_plots", "me_hist_for_plot"} - pd_exclude_fields = {"hist_for_plot", "vals_for_plots", "me_hist_for_plot", "error_normality", "error_bias"} - field_tags = { - "current": {IncludeTags.Current}, - "reference": {IncludeTags.Reference}, - "rmse_default": {IncludeTags.Extra}, - "me_default_sigma": {IncludeTags.Extra}, - "mean_abs_error_default": {IncludeTags.Extra}, - "mean_abs_perc_error_default": {IncludeTags.Extra}, - "abs_error_max_default": {IncludeTags.Extra}, - "error_normality": {IncludeTags.Extra}, - "vals_for_plots": {IncludeTags.Render}, - "error_bias": {IncludeTags.Extra}, - } + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:RegressionQualityMetricResults" + __dict_exclude_fields__: ClassVar[set] = {"hist_for_plot", "vals_for_plots", "me_hist_for_plot"} + __pd_exclude_fields__: ClassVar[set] = { + "hist_for_plot", + "vals_for_plots", + "me_hist_for_plot", + "error_normality", + "error_bias", + } + __field_tags__: ClassVar[Dict[str, set]] = { + "current": {IncludeTags.Current}, + "reference": {IncludeTags.Reference}, + "rmse_default": {IncludeTags.Extra}, + "me_default_sigma": {IncludeTags.Extra}, + "mean_abs_error_default": {IncludeTags.Extra}, + "mean_abs_perc_error_default": {IncludeTags.Extra}, + "abs_error_max_default": {IncludeTags.Extra}, + "error_normality": {IncludeTags.Extra}, + "vals_for_plots": {IncludeTags.Render}, + "error_bias": {IncludeTags.Extra}, + } columns: DatasetColumns current: MoreRegressionMetrics @@ -76,8 +81,7 @@ class Config: class RegressionQualityMetric(Metric[RegressionQualityMetricResults]): - class Config: - type_alias = "evidently:metric:RegressionQualityMetric" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:RegressionQualityMetric" def calculate(self, data: InputData) -> RegressionQualityMetricResults: dataset_columns = process_columns(data.current_data, data.column_mapping) diff --git a/src/evidently/legacy/metrics/regression_performance/top_error.py b/src/evidently/legacy/metrics/regression_performance/top_error.py index 222885d2ac..42c5d6f4eb 100644 --- a/src/evidently/legacy/metrics/regression_performance/top_error.py +++ b/src/evidently/legacy/metrics/regression_performance/top_error.py @@ -1,4 +1,5 @@ import json +from typing import ClassVar from typing import Dict from typing import List from typing import Optional @@ -30,28 +31,25 @@ class TopData(MetricResult): - class Config: - type_alias = "evidently:metric_result:TopData" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:TopData" mean_err_per_group: Dict[str, Dict[str, float]] scatter: RegressionScatter class AggTopData(MetricResult): - class Config: - type_alias = "evidently:metric_result:AggTopData" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:AggTopData" mean_err_per_group: Dict[str, Dict[str, float]] contour: Dict[str, ContourData] class RegressionTopErrorMetricResults(MetricResult): - class Config: - type_alias = "evidently:metric_result:RegressionTopErrorMetricResults" - dict_include = False - pd_include = False - tags = {IncludeTags.Render} - field_tags = {"current": {IncludeTags.Current}, "reference": {IncludeTags.Reference}} + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:RegressionTopErrorMetricResults" + __dict_include__: ClassVar[bool] = False + __pd_include__: ClassVar[bool] = False + __tags__: ClassVar[set] = {IncludeTags.Render} + __field_tags__: ClassVar[Dict[str, set]] = {"current": {IncludeTags.Current}, "reference": {IncludeTags.Reference}} current: Union[TopData, AggTopData] reference: Optional[Union[TopData, AggTopData]] @@ -62,8 +60,7 @@ class Config: class RegressionTopErrorMetric(UsesRawDataMixin, Metric[RegressionTopErrorMetricResults]): - class Config: - type_alias = "evidently:metric:RegressionTopErrorMetric" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:RegressionTopErrorMetric" def calculate(self, data: InputData) -> RegressionTopErrorMetricResults: dataset_columns = process_columns(data.current_data, data.column_mapping) diff --git a/src/evidently/legacy/model/dashboard.py b/src/evidently/legacy/model/dashboard.py index 5379e24498..fa69a60bc5 100644 --- a/src/evidently/legacy/model/dashboard.py +++ b/src/evidently/legacy/model/dashboard.py @@ -3,7 +3,8 @@ from typing import List -from evidently._pydantic_compat import BaseModel +from pydantic import BaseModel + from evidently.legacy.model.widget import BaseWidgetInfo diff --git a/src/evidently/legacy/model/widget.py b/src/evidently/legacy/model/widget.py index 9f19177c82..c676843c1b 100644 --- a/src/evidently/legacy/model/widget.py +++ b/src/evidently/legacy/model/widget.py @@ -9,9 +9,9 @@ from typing import Union import uuid6 +from pydantic import BaseModel +from pydantic import Field -from evidently._pydantic_compat import BaseModel -from evidently._pydantic_compat import Field from evidently.pydantic_utils import EvidentlyBaseModel from evidently.pydantic_utils import Fingerprint @@ -53,8 +53,7 @@ class Alert(BaseModel): class AdditionalGraphInfo(BaseModel): - class Config: - extra = "forbid" + model_config = {"extra": "forbid"} id: str params: dict @@ -79,9 +78,6 @@ class WidgetType(Enum): # @dataclass class BaseWidgetInfo(BaseModel): - class Config: - smart_union = True - type: str title: str size: int @@ -138,4 +134,4 @@ class TabInfo(BaseModel): widget: BaseWidgetInfo -BaseWidgetInfo.update_forward_refs() +BaseWidgetInfo.model_rebuild() diff --git a/src/evidently/legacy/options/base.py b/src/evidently/legacy/options/base.py index 3ac7dfc0b2..7bab5d4fb2 100644 --- a/src/evidently/legacy/options/base.py +++ b/src/evidently/legacy/options/base.py @@ -6,16 +6,20 @@ from typing import TypeVar from typing import Union -from evidently._pydantic_compat import BaseModel +from pydantic import BaseModel + from evidently.legacy.options import ColorOptions from evidently.legacy.options.agg_data import DataDefinitionOptions from evidently.legacy.options.agg_data import RenderOptions from evidently.legacy.options.option import Option +from evidently.pydantic_utils import get_field_inner_type if TYPE_CHECKING: - from evidently._pydantic_compat import AbstractSetIntStr - from evidently._pydantic_compat import DictStrAny - from evidently._pydantic_compat import MappingIntStrAny + from typing import AbstractSet + from typing import Dict as DictStrAny + from typing import Mapping as MappingIntStrAny + + AbstractSetIntStr = AbstractSet[Union[int, str]] TypeParam = TypeVar("TypeParam", bound=Option) @@ -81,7 +85,7 @@ def override(self, other: "Options") -> "Options": res.custom = self.custom.copy() for key, value in other.custom.items(): res.custom[key] = value - for name in self.__fields__: + for name in self.model_fields: if name == "custom": continue override = getattr(other, name) @@ -92,7 +96,7 @@ def override(self, other: "Options") -> "Options": return res def __hash__(self): - value_pairs = [(f, getattr(self, f)) for f in self.__fields__ if f != "custom"] + value_pairs = [(f, getattr(self, f)) for f in self.model_fields if f != "custom"] value_pairs.extend(sorted(list(self.custom.items()))) return hash((type(self),) + tuple(value_pairs)) @@ -129,6 +133,6 @@ def dict( ) -_option_cls_mapping = {field.type_: name for name, field in Options.__fields__.items()} +_option_cls_mapping = {get_field_inner_type(field): name for name, field in Options.model_fields.items()} AnyOptions = Union[Options, Option, dict, List[Option], None] diff --git a/src/evidently/legacy/options/data_drift.py b/src/evidently/legacy/options/data_drift.py index b6e89761a7..0de2fc6e57 100644 --- a/src/evidently/legacy/options/data_drift.py +++ b/src/evidently/legacy/options/data_drift.py @@ -5,7 +5,8 @@ from typing import Optional from typing import Union -from evidently._pydantic_compat import BaseModel +from pydantic import BaseModel + from evidently.legacy.calculations.stattests import PossibleStatTestType from evidently.legacy.calculations.stattests import StatTest from evidently.legacy.utils.data_drift_utils import resolve_stattest_threshold diff --git a/src/evidently/legacy/renderers/base_renderer.py b/src/evidently/legacy/renderers/base_renderer.py index 35760f9a86..9a4fb8931d 100644 --- a/src/evidently/legacy/renderers/base_renderer.py +++ b/src/evidently/legacy/renderers/base_renderer.py @@ -11,9 +11,9 @@ import pandas as pd import uuid6 +from pydantic import BaseModel +from pydantic import Field -from evidently._pydantic_compat import BaseModel -from evidently._pydantic_compat import Field from evidently.legacy.model.widget import AdditionalGraphInfo from evidently.legacy.model.widget import BaseWidgetInfo from evidently.legacy.model.widget import PlotlyGraphInfo @@ -44,7 +44,7 @@ def __init__(self, color_options: Optional[ColorOptions] = None) -> None: class MetricRenderer(Generic[TMetric], BaseRenderer): def render_pandas(self, obj: TMetric) -> pd.DataFrame: result = obj.get_result() - if not result.__config__.pd_include: + if not result.__pd_include__: warnings.warn( f"{obj.get_id()} metric does not support as_dataframe yet. Please submit an issue to https://github.com/evidentlyai/evidently/issues" ) diff --git a/src/evidently/legacy/suite/base_suite.py b/src/evidently/legacy/suite/base_suite.py index c2e74162a5..9f006c5a7b 100644 --- a/src/evidently/legacy/suite/base_suite.py +++ b/src/evidently/legacy/suite/base_suite.py @@ -16,10 +16,10 @@ from typing import Union import ujson +from pydantic import BaseModel +from pydantic import TypeAdapter import evidently -from evidently._pydantic_compat import BaseModel -from evidently._pydantic_compat import parse_obj_as from evidently.legacy.base_metric import ErrorResult from evidently.legacy.base_metric import GenericInputData from evidently.legacy.base_metric import Metric @@ -168,7 +168,7 @@ class ContextPayload(BaseModel): tests: List[Test] test_results: List[TestResult] options: Options = Options() - data_definition: Optional[DataDefinition] + data_definition: Optional[DataDefinition] = None run_metadata: RunMetadata = RunMetadata() @classmethod @@ -525,7 +525,7 @@ def save(self, filename): @classmethod def load(cls, filename): with open(filename, "r") as f: - return parse_obj_as(Snapshot, json.load(f)) + return TypeAdapter(Snapshot).validate_python(json.load(f)) @property def is_report(self): diff --git a/src/evidently/legacy/test_preset/classification_binary.py b/src/evidently/legacy/test_preset/classification_binary.py index 0fd11d7526..a18ad21a82 100644 --- a/src/evidently/legacy/test_preset/classification_binary.py +++ b/src/evidently/legacy/test_preset/classification_binary.py @@ -1,4 +1,5 @@ from typing import Any +from typing import ClassVar from typing import Dict from typing import List from typing import Optional @@ -16,8 +17,7 @@ class BinaryClassificationTestPreset(TestPreset): - class Config: - type_alias = "evidently:test_preset:BinaryClassificationTestPreset" + __type_alias__: ClassVar[Optional[str]] = "evidently:test_preset:BinaryClassificationTestPreset" """ Binary Classification Tests. diff --git a/src/evidently/legacy/test_preset/classification_binary_topk.py b/src/evidently/legacy/test_preset/classification_binary_topk.py index 57b3ea66f6..5e6dad47f1 100644 --- a/src/evidently/legacy/test_preset/classification_binary_topk.py +++ b/src/evidently/legacy/test_preset/classification_binary_topk.py @@ -1,4 +1,5 @@ from typing import Any +from typing import ClassVar from typing import Dict from typing import List from typing import Optional @@ -17,8 +18,7 @@ class BinaryClassificationTopKTestPreset(TestPreset): - class Config: - type_alias = "evidently:test_preset:BinaryClassificationTopKTestPreset" + __type_alias__: ClassVar[Optional[str]] = "evidently:test_preset:BinaryClassificationTopKTestPreset" """ Binary Classification Tests for Top K threshold. diff --git a/src/evidently/legacy/test_preset/classification_multiclass.py b/src/evidently/legacy/test_preset/classification_multiclass.py index 7ae9a4c807..7adddf5a8d 100644 --- a/src/evidently/legacy/test_preset/classification_multiclass.py +++ b/src/evidently/legacy/test_preset/classification_multiclass.py @@ -1,4 +1,5 @@ from typing import Any +from typing import ClassVar from typing import Dict from typing import List from typing import Optional @@ -18,8 +19,7 @@ class MulticlassClassificationTestPreset(TestPreset): - class Config: - type_alias = "evidently:test_preset:MulticlassClassificationTestPreset" + __type_alias__: ClassVar[Optional[str]] = "evidently:test_preset:MulticlassClassificationTestPreset" """ Multiclass Classification tests. diff --git a/src/evidently/legacy/test_preset/data_drift.py b/src/evidently/legacy/test_preset/data_drift.py index 1d32983b2b..2b8f2a3da3 100644 --- a/src/evidently/legacy/test_preset/data_drift.py +++ b/src/evidently/legacy/test_preset/data_drift.py @@ -1,4 +1,5 @@ from typing import Any +from typing import ClassVar from typing import Dict from typing import List from typing import Optional @@ -20,8 +21,7 @@ class DataDriftTestPreset(TestPreset): - class Config: - type_alias = "evidently:test_preset:DataDriftTestPreset" + __type_alias__: ClassVar[Optional[str]] = "evidently:test_preset:DataDriftTestPreset" """ Data Drift tests. diff --git a/src/evidently/legacy/test_preset/data_quality.py b/src/evidently/legacy/test_preset/data_quality.py index 3717518cd3..cfc99a8bfb 100644 --- a/src/evidently/legacy/test_preset/data_quality.py +++ b/src/evidently/legacy/test_preset/data_quality.py @@ -1,4 +1,5 @@ from typing import Any +from typing import ClassVar from typing import Dict from typing import List from typing import Optional @@ -14,8 +15,7 @@ class DataQualityTestPreset(TestPreset): - class Config: - type_alias = "evidently:test_preset:DataQualityTestPreset" + __type_alias__: ClassVar[Optional[str]] = "evidently:test_preset:DataQualityTestPreset" """ Data Quality tests. diff --git a/src/evidently/legacy/test_preset/data_stability.py b/src/evidently/legacy/test_preset/data_stability.py index aa544a1e81..4b98681b63 100644 --- a/src/evidently/legacy/test_preset/data_stability.py +++ b/src/evidently/legacy/test_preset/data_stability.py @@ -1,4 +1,5 @@ from typing import Any +from typing import ClassVar from typing import Dict from typing import List from typing import Optional @@ -16,8 +17,7 @@ class DataStabilityTestPreset(TestPreset): - class Config: - type_alias = "evidently:test_preset:DataStabilityTestPreset" + __type_alias__: ClassVar[Optional[str]] = "evidently:test_preset:DataStabilityTestPreset" """ Data Stability tests. diff --git a/src/evidently/legacy/test_preset/no_target_performance.py b/src/evidently/legacy/test_preset/no_target_performance.py index 83f64c6030..f9843c5e42 100644 --- a/src/evidently/legacy/test_preset/no_target_performance.py +++ b/src/evidently/legacy/test_preset/no_target_performance.py @@ -1,4 +1,5 @@ from typing import Any +from typing import ClassVar from typing import Dict from typing import List from typing import Optional @@ -24,8 +25,7 @@ class NoTargetPerformanceTestPreset(TestPreset): - class Config: - type_alias = "evidently:test_preset:NoTargetPerformanceTestPreset" + __type_alias__: ClassVar[Optional[str]] = "evidently:test_preset:NoTargetPerformanceTestPreset" """ No Target Performance tests. diff --git a/src/evidently/legacy/test_preset/recsys.py b/src/evidently/legacy/test_preset/recsys.py index 0ea07669f8..7683204887 100644 --- a/src/evidently/legacy/test_preset/recsys.py +++ b/src/evidently/legacy/test_preset/recsys.py @@ -1,4 +1,5 @@ from typing import Any +from typing import ClassVar from typing import Dict from typing import List from typing import Optional @@ -14,8 +15,7 @@ class RecsysTestPreset(TestPreset): - class Config: - type_alias = "evidently:test_preset:RecsysTestPreset" + __type_alias__: ClassVar[Optional[str]] = "evidently:test_preset:RecsysTestPreset" """ Recsys performance tests. diff --git a/src/evidently/legacy/test_preset/regression.py b/src/evidently/legacy/test_preset/regression.py index 1ead58045a..3418c149f6 100644 --- a/src/evidently/legacy/test_preset/regression.py +++ b/src/evidently/legacy/test_preset/regression.py @@ -1,4 +1,5 @@ from typing import Any +from typing import ClassVar from typing import Dict from typing import List from typing import Optional @@ -13,8 +14,7 @@ class RegressionTestPreset(TestPreset): - class Config: - type_alias = "evidently:test_preset:RegressionTestPreset" + __type_alias__: ClassVar[Optional[str]] = "evidently:test_preset:RegressionTestPreset" """ Regression performance tests. diff --git a/src/evidently/legacy/test_preset/test_preset.py b/src/evidently/legacy/test_preset/test_preset.py index faa5356b15..494311201f 100644 --- a/src/evidently/legacy/test_preset/test_preset.py +++ b/src/evidently/legacy/test_preset/test_preset.py @@ -1,5 +1,6 @@ import abc from typing import Any +from typing import ClassVar from typing import Dict from typing import List from typing import Optional @@ -14,8 +15,7 @@ class TestPreset(BasePreset): - class Config: - is_base_type = True + __is_base_type__: ClassVar[bool] = True @abc.abstractmethod def generate_tests( diff --git a/src/evidently/legacy/tests/base_test.py b/src/evidently/legacy/tests/base_test.py index 30080184f7..7903e3618e 100644 --- a/src/evidently/legacy/tests/base_test.py +++ b/src/evidently/legacy/tests/base_test.py @@ -12,8 +12,10 @@ from typing import TypeVar from typing import Union -from evidently._pydantic_compat import BaseModel -from evidently._pydantic_compat import Field +from pydantic import BaseModel +from pydantic import ConfigDict +from pydantic import Field + from evidently.legacy.base_metric import BaseResult from evidently.legacy.base_metric import Metric from evidently.legacy.base_metric import MetricResult @@ -94,17 +96,14 @@ class TestStatus(str, Enum): SKIPPED = "SKIPPED" # the test was skipped -class TestParameters(EvidentlyBaseModel, BaseResult): # type: ignore[misc] # pydantic Config - class Config: - type_alias = "evidently:test_parameters:TestParameters" - field_tags = {"type": {IncludeTags.TypeField}} - is_base_type = True +class TestParameters(EvidentlyBaseModel, BaseResult): # type: ignore[misc] + __type_alias__: ClassVar[Optional[str]] = "evidently:test_parameters:TestParameters" + __is_base_type__: ClassVar[bool] = True + __field_tags__: ClassVar[Dict[str, set]] = {"type": {IncludeTags.TypeField}} class TestResult(EnumValueMixin, MetricResult): # todo: create common base class - # short name/title from the test class - class Config: - type_alias = "evidently:metric_result:TestResult" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:TestResult" name: str # what was checked, what threshold (current value 13 is not ok with condition less than 5) @@ -113,7 +112,7 @@ class Config: status: TestStatus # grouping parameters group: str - parameters: Optional[TestParameters] + parameters: Optional[TestParameters] = None _exception: Optional[BaseException] = None @property @@ -143,8 +142,7 @@ def is_passed(self): class Test(WithTestAndMetricDependencies): - class Config: - is_base_type = True + __is_base_type__: ClassVar[bool] = True """ all fields in test class with type that is subclass of Metric would be used as dependencies of test. @@ -195,10 +193,7 @@ class TestValueCondition(ExcludeNoneMixin): An object of the class stores specified conditions and can be used for checking a value by them. """ - class Config: - arbitrary_types_allowed = True - use_enum_values = True - smart_union = True + model_config = ConfigDict(arbitrary_types_allowed=True) eq: Optional[NumericApprox] = None gt: Optional[NumericApprox] = None @@ -278,8 +273,7 @@ def __str__(self) -> str: class ConditionTestParameters(TestParameters): - class Config: - type_alias = "evidently:test_parameters:ConditionTestParameters" + __type_alias__: ClassVar[Optional[str]] = "evidently:test_parameters:ConditionTestParameters" condition: TestValueCondition @@ -289,11 +283,7 @@ class BaseConditionsTest(Test, TestValueCondition, ABC): Base class for all tests with a condition """ - class Config: - arbitrary_types_allowed = True - use_enum_values = True - smart_union = True - underscore_attrs_are_private = True + model_config = ConfigDict(arbitrary_types_allowed=True) # condition: TestValueCondition @@ -312,15 +302,13 @@ def condition(self) -> TestValueCondition: class CheckValueParameters(ConditionTestParameters): - class Config: - type_alias = "evidently:test_parameters:CheckValueParameters" + __type_alias__: ClassVar[Optional[str]] = "evidently:test_parameters:CheckValueParameters" value: Optional[Numeric] class ColumnCheckValueParameters(CheckValueParameters): - class Config: - type_alias = "evidently:test_parameters:ColumnCheckValueParameters" + __type_alias__: ClassVar[Optional[str]] = "evidently:test_parameters:ColumnCheckValueParameters" column_name: str diff --git a/src/evidently/legacy/tests/classification_performance_tests.py b/src/evidently/legacy/tests/classification_performance_tests.py index de778b9218..642b7e7b86 100644 --- a/src/evidently/legacy/tests/classification_performance_tests.py +++ b/src/evidently/legacy/tests/classification_performance_tests.py @@ -156,8 +156,7 @@ def conf_matrix(self): class TestAccuracyScore(SimpleClassificationTestTopK): - class Config: - type_alias = "evidently:test:TestAccuracyScore" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestAccuracyScore" name = "Accuracy Score" @@ -180,8 +179,7 @@ def render_html(self, obj: TestAccuracyScore) -> TestHtmlInfo: class TestPrecisionScore(SimpleClassificationTestTopK): - class Config: - type_alias = "evidently:test:TestPrecisionScore" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestPrecisionScore" name = "Precision Score" @@ -204,8 +202,7 @@ def render_html(self, obj: TestPrecisionScore) -> TestHtmlInfo: class TestF1Score(SimpleClassificationTestTopK): - class Config: - type_alias = "evidently:test:TestF1Score" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestF1Score" name: ClassVar = "F1 Score" @@ -228,8 +225,7 @@ def render_html(self, obj: TestF1Score) -> TestHtmlInfo: class TestRecallScore(SimpleClassificationTestTopK): - class Config: - type_alias = "evidently:test:TestRecallScore" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestRecallScore" name = "Recall Score" @@ -252,8 +248,7 @@ def render_html(self, obj: TestRecallScore) -> TestHtmlInfo: class TestRocAuc(SimpleClassificationTest): - class Config: - type_alias = "evidently:test:TestRocAuc" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestRocAuc" name: ClassVar = "ROC AUC Score" _roc_curve: ClassificationRocCurve @@ -270,7 +265,6 @@ def __init__( not_in: Optional[List[Union[Numeric, str, bool]]] = None, is_critical: bool = True, ): - self._roc_curve = ClassificationRocCurve() super().__init__( eq=eq, gt=gt, @@ -282,6 +276,7 @@ def __init__( not_in=not_in, is_critical=is_critical, ) + self._roc_curve = ClassificationRocCurve() def get_value(self, result: DatasetClassificationQuality): return result.roc_auc @@ -314,8 +309,7 @@ def render_html(self, obj: TestRocAuc) -> TestHtmlInfo: class TestLogLoss(SimpleClassificationTest): - class Config: - type_alias = "evidently:test:TestLogLoss" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestLogLoss" condition_arg = "lt" name = "Logarithmic Loss" @@ -352,8 +346,7 @@ def render_html(self, obj: TestLogLoss) -> TestHtmlInfo: class TestTPR(SimpleClassificationTestTopK): - class Config: - type_alias = "evidently:test:TestTPR" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestTPR" name = "True Positive Rate" @@ -391,8 +384,7 @@ def render_html(self, obj: TestF1Score) -> TestHtmlInfo: class TestTNR(SimpleClassificationTestTopK): - class Config: - type_alias = "evidently:test:TestTNR" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestTNR" name = "True Negative Rate" @@ -430,8 +422,7 @@ def render_html(self, obj: TestF1Score) -> TestHtmlInfo: class TestFPR(SimpleClassificationTestTopK): - class Config: - type_alias = "evidently:test:TestFPR" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestFPR" condition_arg: ClassVar = "lt" name = "False Positive Rate" @@ -470,8 +461,7 @@ def render_html(self, obj: TestF1Score) -> TestHtmlInfo: class TestFNR(SimpleClassificationTestTopK): - class Config: - type_alias = "evidently:test:TestFNR" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestFNR" condition_arg: ClassVar = "lt" name = "False Negative Rate" @@ -510,8 +500,7 @@ def render_html(self, obj: TestF1Score) -> TestHtmlInfo: class ByClassParameters(CheckValueParameters): - class Config: - type_alias = "evidently:test_parameters:ByClassParameters" + __type_alias__: ClassVar[Optional[str]] = "evidently:test_parameters:ByClassParameters" label: Label @@ -609,8 +598,7 @@ def get_parameters(self) -> ByClassParameters: class TestPrecisionByClass(ByClassClassificationTest): - class Config: - type_alias = "evidently:test:TestPrecisionByClass" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestPrecisionByClass" name: ClassVar[str] = "Precision Score by Class" @@ -636,8 +624,7 @@ def render_html(self, obj: TestPrecisionByClass) -> TestHtmlInfo: class TestRecallByClass(ByClassClassificationTest): - class Config: - type_alias = "evidently:test:TestRecallByClass" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestRecallByClass" name: ClassVar[str] = "Recall Score by Class" @@ -663,8 +650,7 @@ def render_html(self, obj: TestRecallByClass) -> TestHtmlInfo: class TestF1ByClass(ByClassClassificationTest): - class Config: - type_alias = "evidently:test:TestF1ByClass" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestF1ByClass" name: ClassVar[str] = "F1 Score by Class" diff --git a/src/evidently/legacy/tests/custom_test.py b/src/evidently/legacy/tests/custom_test.py index d127d4d25b..2be9a5894d 100644 --- a/src/evidently/legacy/tests/custom_test.py +++ b/src/evidently/legacy/tests/custom_test.py @@ -17,8 +17,7 @@ class CustomValueTest(BaseCheckValueTest): - class Config: - type_alias = "evidently:test:CustomValueTest" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:CustomValueTest" name: ClassVar = "Custom Value test" group = CUSTOM_GROUP.id @@ -39,7 +38,6 @@ def __init__( not_in: Optional[List[Union[Numeric, str, bool]]] = None, is_critical: bool = True, ): - self._metric = CustomValueMetric(func=func, title=title) super().__init__( eq=eq, gt=gt, @@ -51,6 +49,7 @@ def __init__( not_in=not_in, is_critical=is_critical, ) + self._metric = CustomValueMetric(func=func, title=title) if not self.has_condition(): raise ValueError("Specify at least one condition") diff --git a/src/evidently/legacy/tests/data_drift_tests.py b/src/evidently/legacy/tests/data_drift_tests.py index cb958821a4..54b6ed4cd5 100644 --- a/src/evidently/legacy/tests/data_drift_tests.py +++ b/src/evidently/legacy/tests/data_drift_tests.py @@ -48,8 +48,7 @@ class ColumnDriftParameter(ExcludeNoneMixin, TestParameters): # type: ignore[misc] # pydantic Config - class Config: - type_alias = "evidently:test_parameters:ColumnDriftParameter" + __type_alias__: ClassVar[Optional[str]] = "evidently:test_parameters:ColumnDriftParameter" stattest: str score: float @@ -70,8 +69,7 @@ def from_metric(cls, data: ColumnDataDriftMetrics, column_name: str = None): class ColumnsDriftParameters(ConditionTestParameters): # todo: rename to columns? - class Config: - type_alias = "evidently:test_parameters:ColumnsDriftParameters" + __type_alias__: ClassVar[Optional[str]] = "evidently:test_parameters:ColumnsDriftParameters" features: Dict[str, ColumnDriftParameter] @@ -185,8 +183,7 @@ def check(self): class TestNumberOfDriftedColumns(BaseDataDriftMetricsTest): - class Config: - type_alias = "evidently:test:TestNumberOfDriftedColumns" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestNumberOfDriftedColumns" name: ClassVar = "Number of Drifted Features" @@ -208,8 +205,7 @@ def get_description(self, value: Numeric) -> str: class TestShareOfDriftedColumns(BaseDataDriftMetricsTest): - class Config: - type_alias = "evidently:test:TestShareOfDriftedColumns" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestShareOfDriftedColumns" name: ClassVar = "Share of Drifted Columns" @@ -232,8 +228,7 @@ def get_description(self, value: Numeric) -> str: class TestColumnDrift(Test): - class Config: - type_alias = "evidently:test:TestColumnDrift" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestColumnDrift" name: ClassVar = "Drift per Column" group: ClassVar = DATA_DRIFT_GROUP.id @@ -619,8 +614,7 @@ def render_html(self, obj: TestColumnDrift) -> TestHtmlInfo: class TestEmbeddingsDrift(Test): - class Config: - type_alias = "evidently:test:TestEmbeddingsDrift" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestEmbeddingsDrift" name: ClassVar = "Drift for embeddings" group: ClassVar = DATA_DRIFT_GROUP.id diff --git a/src/evidently/legacy/tests/data_integrity_tests.py b/src/evidently/legacy/tests/data_integrity_tests.py index 0b101e47f5..12e45c2781 100644 --- a/src/evidently/legacy/tests/data_integrity_tests.py +++ b/src/evidently/legacy/tests/data_integrity_tests.py @@ -77,8 +77,7 @@ def __init__( class TestNumberOfColumns(BaseIntegrityValueTest): - class Config: - type_alias = "evidently:test:TestNumberOfColumns" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestNumberOfColumns" """Number of all columns in the data, including utility columns (id/index, datetime, target, predictions)""" @@ -115,8 +114,7 @@ def render_html(self, obj: TestNumberOfColumns) -> TestHtmlInfo: class TestNumberOfRows(BaseIntegrityValueTest): - class Config: - type_alias = "evidently:test:TestNumberOfRows" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestNumberOfRows" """Number of rows in the data""" @@ -247,8 +245,7 @@ def get_table_with_number_of_missing_values_by_one_missing_value( class TestNumberOfDifferentMissingValues(BaseIntegrityMissingValuesValuesTest): - class Config: - type_alias = "evidently:test:TestNumberOfDifferentMissingValues" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestNumberOfDifferentMissingValues" """Check a number of different encoded missing values.""" @@ -293,8 +290,7 @@ def render_html(self, obj: TestNumberOfDifferentMissingValues) -> TestHtmlInfo: class TestNumberOfMissingValues(BaseIntegrityMissingValuesValuesTest): - class Config: - type_alias = "evidently:test:TestNumberOfMissingValues" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestNumberOfMissingValues" """Check a number of missing values.""" @@ -332,8 +328,7 @@ def render_html(self, obj: TestNumberOfMissingValues) -> TestHtmlInfo: class TestShareOfMissingValues(BaseIntegrityMissingValuesValuesTest): - class Config: - type_alias = "evidently:test:TestShareOfMissingValues" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestShareOfMissingValues" """Check a share of missing values.""" @@ -361,8 +356,7 @@ def render_html(self, obj: TestNumberOfMissingValues) -> TestHtmlInfo: class TestNumberOfColumnsWithMissingValues(BaseIntegrityMissingValuesValuesTest): - class Config: - type_alias = "evidently:test:TestNumberOfColumnsWithMissingValues" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestNumberOfColumnsWithMissingValues" """Check a number of columns with a missing value.""" @@ -394,8 +388,7 @@ def render_html(self, obj: TestNumberOfMissingValues) -> TestHtmlInfo: class TestShareOfColumnsWithMissingValues(BaseIntegrityMissingValuesValuesTest): - class Config: - type_alias = "evidently:test:TestShareOfColumnsWithMissingValues" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestShareOfColumnsWithMissingValues" """Check a share of columns with a missing value.""" @@ -428,8 +421,7 @@ def render_html(self, obj: TestNumberOfMissingValues) -> TestHtmlInfo: class TestNumberOfRowsWithMissingValues(BaseIntegrityMissingValuesValuesTest): - class Config: - type_alias = "evidently:test:TestNumberOfRowsWithMissingValues" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestNumberOfRowsWithMissingValues" """Check a number of rows with a missing value.""" @@ -454,8 +446,7 @@ def get_description(self, value: Numeric) -> str: class TestShareOfRowsWithMissingValues(BaseIntegrityMissingValuesValuesTest): - class Config: - type_alias = "evidently:test:TestShareOfRowsWithMissingValues" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestShareOfRowsWithMissingValues" """Check a share of rows with a missing value.""" @@ -516,8 +507,7 @@ def __init__( class TestColumnNumberOfDifferentMissingValues(BaseIntegrityColumnMissingValuesTest): - class Config: - type_alias = "evidently:test:TestColumnNumberOfDifferentMissingValues" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestColumnNumberOfDifferentMissingValues" """Check a number of differently encoded missing values in one column.""" @@ -569,8 +559,7 @@ def render_html(self, obj: TestColumnNumberOfDifferentMissingValues) -> TestHtml class TestColumnNumberOfMissingValues(BaseIntegrityColumnMissingValuesTest): - class Config: - type_alias = "evidently:test:TestColumnNumberOfMissingValues" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestColumnNumberOfMissingValues" """Check a number of missing values in one column.""" @@ -597,8 +586,7 @@ def get_description(self, value: Numeric) -> str: class TestColumnShareOfMissingValues(BaseIntegrityColumnMissingValuesTest): - class Config: - type_alias = "evidently:test:TestColumnShareOfMissingValues" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestColumnShareOfMissingValues" """Check a share of missing values in one column.""" @@ -650,8 +638,7 @@ def generate(self, data_definition: DataDefinition) -> List[TestColumnShareOfMis class TestNumberOfConstantColumns(BaseIntegrityValueTest): - class Config: - type_alias = "evidently:test:TestNumberOfConstantColumns" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestNumberOfConstantColumns" """Number of columns contained only one unique value""" @@ -690,8 +677,7 @@ def render_html(self, obj: TestNumberOfConstantColumns) -> TestHtmlInfo: class TestNumberOfEmptyRows(BaseIntegrityValueTest): - class Config: - type_alias = "evidently:test:TestNumberOfEmptyRows" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestNumberOfEmptyRows" """Number of rows contained all NAN values""" @@ -715,8 +701,7 @@ def get_description(self, value: Numeric) -> str: class TestNumberOfEmptyColumns(BaseIntegrityValueTest): - class Config: - type_alias = "evidently:test:TestNumberOfEmptyColumns" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestNumberOfEmptyColumns" """Number of columns contained all NAN values""" @@ -754,8 +739,7 @@ def render_html(self, obj: TestNumberOfEmptyColumns) -> TestHtmlInfo: class TestNumberOfDuplicatedRows(BaseIntegrityValueTest): - class Config: - type_alias = "evidently:test:TestNumberOfDuplicatedRows" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestNumberOfDuplicatedRows" """How many rows have duplicates in the dataset""" @@ -779,8 +763,7 @@ def get_description(self, value: Numeric) -> str: class TestNumberOfDuplicatedColumns(BaseIntegrityValueTest): - class Config: - type_alias = "evidently:test:TestNumberOfDuplicatedColumns" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestNumberOfDuplicatedColumns" """How many columns have duplicates in the dataset""" @@ -857,8 +840,7 @@ def groups(self) -> Dict[str, str]: class TestColumnAllConstantValues(BaseIntegrityOneColumnTest): - class Config: - type_alias = "evidently:test:TestColumnAllConstantValues" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestColumnAllConstantValues" """Test that there is only one unique value in a column""" @@ -903,8 +885,7 @@ def render_html(self, obj: TestColumnAllConstantValues) -> TestHtmlInfo: class TestColumnAllUniqueValues(BaseIntegrityOneColumnTest): - class Config: - type_alias = "evidently:test:TestColumnAllUniqueValues" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestColumnAllUniqueValues" """Test that there is only uniques values in a column""" @@ -949,8 +930,7 @@ def render_html(self, obj: TestColumnAllUniqueValues) -> TestHtmlInfo: class ColumnTypeParameter(TestParameters): - class Config: - type_alias = "evidently:test_parameters:ColumnTypeParameter" + __type_alias__: ClassVar[Optional[str]] = "evidently:test_parameters:ColumnTypeParameter" actual_type: str column_name: str @@ -958,15 +938,13 @@ class Config: class ColumnTypesParameter(TestParameters): - class Config: - type_alias = "evidently:test_parameters:ColumnTypesParameter" + __type_alias__: ClassVar[Optional[str]] = "evidently:test_parameters:ColumnTypesParameter" columns: List[ColumnTypeParameter] class TestColumnsType(Test): - class Config: - type_alias = "evidently:test:TestColumnsType" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestColumnsType" """This test compares columns type against the specified ones or a reference dataframe""" @@ -977,8 +955,8 @@ class Config: def __init__(self, columns_type: Optional[dict] = None, is_critical: bool = True): self.columns_type = columns_type - self._metric = DatasetSummaryMetric() super().__init__(is_critical=is_critical) + self._metric = DatasetSummaryMetric() @property def metric(self): diff --git a/src/evidently/legacy/tests/data_quality_tests.py b/src/evidently/legacy/tests/data_quality_tests.py index 3b515dcc1b..3380058e40 100644 --- a/src/evidently/legacy/tests/data_quality_tests.py +++ b/src/evidently/legacy/tests/data_quality_tests.py @@ -94,16 +94,15 @@ def __init__( class TestConflictTarget(Test): - class Config: - type_alias = "evidently:test:TestConflictTarget" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestConflictTarget" group: ClassVar = DATA_QUALITY_GROUP.id name: ClassVar = "Test number of conflicts in target" _metric: ConflictTargetMetric def __init__(self, is_critical: bool = True): - self._metric = ConflictTargetMetric() super().__init__(is_critical=is_critical) + self._metric = ConflictTargetMetric() @property def metric(self): @@ -131,16 +130,15 @@ def groups(self) -> Dict[str, str]: class TestConflictPrediction(Test): - class Config: - type_alias = "evidently:test:TestConflictPrediction" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestConflictPrediction" group: ClassVar = DATA_QUALITY_GROUP.id name: ClassVar = "Test number of conflicts in prediction" _metric: ConflictPredictionMetric def __init__(self, is_critical: bool = True): - self._metric = ConflictPredictionMetric() super().__init__(is_critical=is_critical) + self._metric = ConflictPredictionMetric() @property def metric(self): @@ -201,8 +199,7 @@ def __init__( class TestTargetPredictionCorrelation(BaseDataQualityCorrelationsMetricsValueTest): - class Config: - type_alias = "evidently:test:TestTargetPredictionCorrelation" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestTargetPredictionCorrelation" name: ClassVar = "Correlation between Target and Prediction" @@ -230,8 +227,7 @@ def get_description(self, value: Numeric) -> str: class TestHighlyCorrelatedColumns(BaseDataQualityCorrelationsMetricsValueTest): - class Config: - type_alias = "evidently:test:TestHighlyCorrelatedColumns" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestHighlyCorrelatedColumns" name: ClassVar = "Highly Correlated Columns" @@ -271,8 +267,7 @@ def render_html(self, obj: TestHighlyCorrelatedColumns) -> TestHtmlInfo: class TestTargetFeaturesCorrelations(BaseDataQualityCorrelationsMetricsValueTest): - class Config: - type_alias = "evidently:test:TestTargetFeaturesCorrelations" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestTargetFeaturesCorrelations" name: ClassVar = "Correlation between Target and Features" @@ -299,8 +294,7 @@ def get_description(self, value: Numeric) -> str: class TestPredictionFeaturesCorrelations(BaseDataQualityCorrelationsMetricsValueTest): - class Config: - type_alias = "evidently:test:TestPredictionFeaturesCorrelations" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestPredictionFeaturesCorrelations" name: ClassVar = "Correlation between Prediction and Features" @@ -347,8 +341,7 @@ def render_html(self, obj: TestTargetFeaturesCorrelations) -> TestHtmlInfo: class TestCorrelationChanges(BaseDataQualityCorrelationsMetricsValueTest): - class Config: - type_alias = "evidently:test:TestCorrelationChanges" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestCorrelationChanges" group: ClassVar = DATA_QUALITY_GROUP.id name: ClassVar = "Change in Correlation" @@ -488,8 +481,7 @@ def get_stat(self, current: NumericCharacteristics): class TestColumnValueMin(BaseFeatureDataQualityMetricsTest): - class Config: - type_alias = "evidently:test:TestColumnValueMin" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestColumnValueMin" name: ClassVar = "Min Value" @@ -516,8 +508,7 @@ def get_description(self, value: Numeric) -> str: class TestColumnValueMax(BaseFeatureDataQualityMetricsTest): - class Config: - type_alias = "evidently:test:TestColumnValueMax" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestColumnValueMax" name: ClassVar = "Max Value" @@ -546,8 +537,7 @@ def get_description(self, value: Numeric) -> str: class TestColumnValueMean(BaseFeatureDataQualityMetricsTest): - class Config: - type_alias = "evidently:test:TestColumnValueMean" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestColumnValueMean" name: ClassVar = "Mean Value" @@ -572,8 +562,7 @@ def get_description(self, value: Numeric) -> str: class TestColumnValueMedian(BaseFeatureDataQualityMetricsTest): - class Config: - type_alias = "evidently:test:TestColumnValueMedian" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestColumnValueMedian" name: ClassVar = "Median Value" @@ -645,8 +634,7 @@ def _feature_render_html(self, obj: BaseFeatureDataQualityMetricsTest): class TestColumnValueStd(BaseFeatureDataQualityMetricsTest): - class Config: - type_alias = "evidently:test:TestColumnValueStd" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestColumnValueStd" name: ClassVar = "Standard Deviation (SD)" @@ -692,8 +680,7 @@ def render_html(self, obj: BaseFeatureDataQualityMetricsTest) -> TestHtmlInfo: class TestNumberOfUniqueValues(BaseFeatureDataQualityMetricsTest): - class Config: - type_alias = "evidently:test:TestNumberOfUniqueValues" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestNumberOfUniqueValues" name: ClassVar = "Number of Unique Values" @@ -739,8 +726,7 @@ def render_html(self, obj: TestNumberOfUniqueValues) -> TestHtmlInfo: class TestUniqueValuesShare(BaseFeatureDataQualityMetricsTest): - class Config: - type_alias = "evidently:test:TestUniqueValuesShare" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestUniqueValuesShare" name: ClassVar = "Share of Unique Values" @@ -793,8 +779,7 @@ def render_html(self, obj: TestUniqueValuesShare) -> TestHtmlInfo: class TestMostCommonValueShare(BaseFeatureDataQualityMetricsTest): - class Config: - type_alias = "evidently:test:TestMostCommonValueShare" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestMostCommonValueShare" name: ClassVar = "Share of the Most Common Value" @@ -888,8 +873,7 @@ def generate(self, data_definition: DataDefinition) -> List[TestMostCommonValueS class MeanInNSigmasParameter(TestParameters): - class Config: - type_alias = "evidently:test_parameters:MeanInNSigmasParameter" + __type_alias__: ClassVar[Optional[str]] = "evidently:test_parameters:MeanInNSigmasParameter" column_name: str current_mean: float @@ -899,8 +883,7 @@ class Config: class TestMeanInNSigmas(Test): - class Config: - type_alias = "evidently:test:TestMeanInNSigmas" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestMeanInNSigmas" group: ClassVar = DATA_QUALITY_GROUP.id name: ClassVar = "Mean Value Stability" @@ -1053,8 +1036,7 @@ def generate(self, data_definition: DataDefinition) -> List[TestMeanInNSigmas]: class TestValueRange(Test): - class Config: - type_alias = "evidently:test:TestValueRange" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestValueRange" group: ClassVar = DATA_QUALITY_GROUP.id name: ClassVar = "Value Range" @@ -1184,8 +1166,7 @@ def metric(self): class TestNumberOfOutRangeValues(BaseDataQualityValueRangeMetricsTest): - class Config: - type_alias = "evidently:test:TestNumberOfOutRangeValues" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestNumberOfOutRangeValues" name: ClassVar = "Number of Out-of-Range Values " @@ -1200,16 +1181,14 @@ def get_description(self, value: Numeric) -> str: class ShareOfOutRangeParameters(CheckValueParameters): - class Config: - type_alias = "evidently:test_parameters:ShareOfOutRangeParameters" + __type_alias__: ClassVar[Optional[str]] = "evidently:test_parameters:ShareOfOutRangeParameters" left: Optional[float] right: Optional[float] class TestShareOfOutRangeValues(BaseDataQualityValueRangeMetricsTest): - class Config: - type_alias = "evidently:test:TestShareOfOutRangeValues" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestShareOfOutRangeValues" name: ClassVar = "Share of Out-of-Range Values" @@ -1292,8 +1271,7 @@ def generate(self, data_definition: DataDefinition) -> List[TestShareOfOutRangeV class ColumnValueListParameters(TestParameters): - class Config: - type_alias = "evidently:test_parameters:ColumnValueListParameters" + __type_alias__: ClassVar[Optional[str]] = "evidently:test_parameters:ColumnValueListParameters" value: Numeric column_name: str @@ -1301,8 +1279,7 @@ class Config: class TestValueList(Test): - class Config: - type_alias = "evidently:test:TestValueList" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestValueList" group: ClassVar = DATA_QUALITY_GROUP.id name: ClassVar = "Out-of-List Values" @@ -1396,8 +1373,7 @@ def get_condition(self) -> TestValueCondition: class TestNumberOfOutListValues(BaseDataQualityValueListMetricsTest): - class Config: - type_alias = "evidently:test:TestNumberOfOutListValues" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestNumberOfOutListValues" name: ClassVar = "Number Out-of-List Values" alias: ClassVar = "number_value_list" @@ -1414,15 +1390,13 @@ def get_description(self, value: Numeric) -> str: class ValueListParameters(CheckValueParameters): # todo: typing - class Config: - type_alias = "evidently:test_parameters:ValueListParameters" + __type_alias__: ClassVar[Optional[str]] = "evidently:test_parameters:ValueListParameters" values: Optional[List[Any]] = None class TestShareOfOutListValues(BaseDataQualityValueListMetricsTest): - class Config: - type_alias = "evidently:test:TestShareOfOutListValues" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestShareOfOutListValues" name: ClassVar = "Share of Out-of-List Values" alias: ClassVar = "share_value_list" @@ -1470,8 +1444,7 @@ def generate(self, data_definition: DataDefinition) -> List[TestShareOfOutListVa class TestColumnQuantile(BaseCheckValueTest): - class Config: - type_alias = "evidently:test:TestColumnQuantile" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestColumnQuantile" group: ClassVar = DATA_QUALITY_GROUP.id name: ClassVar = "Quantile Value" @@ -1598,7 +1571,7 @@ class BaseDataQualityCategoryMetricsTest(BaseCheckValueTest, ABC): group: ClassVar = DATA_QUALITY_GROUP.id _metric: ColumnCategoryMetric column_name: ColumnName - category: Union[str, int, float] + category: Union[bool, str, int, float] def __init__( self, @@ -1652,8 +1625,7 @@ def get_condition(self) -> TestValueCondition: class TestCategoryShare(BaseDataQualityCategoryMetricsTest): - class Config: - type_alias = "evidently:test:TestCategoryShare" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestCategoryShare" name: ClassVar = "Share of category" alias: ClassVar = "share_category" @@ -1679,8 +1651,7 @@ def get_parameters(self) -> CheckValueParameters: class TestCategoryCount(BaseDataQualityCategoryMetricsTest): - class Config: - type_alias = "evidently:test:TestCategoryCount" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestCategoryCount" name: ClassVar = "Count of category" alias: ClassVar = "count_category" diff --git a/src/evidently/legacy/tests/recsys_tests.py b/src/evidently/legacy/tests/recsys_tests.py index 8cb2112af2..0d6ba51471 100644 --- a/src/evidently/legacy/tests/recsys_tests.py +++ b/src/evidently/legacy/tests/recsys_tests.py @@ -134,8 +134,7 @@ def render_html(self, obj: BaseTopkRecsysTest) -> TestHtmlInfo: class TestPrecisionTopK(BaseTopkRecsysTest): - class Config: - type_alias = "evidently:test:TestPrecisionTopK" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestPrecisionTopK" name: ClassVar = "Precision (top-k)" header: str = "Precision" @@ -150,8 +149,7 @@ class TestPrecisionTopKRenderer(BaseTopkRecsysRenderer): class TestRecallTopK(BaseTopkRecsysTest): - class Config: - type_alias = "evidently:test:TestRecallTopK" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestRecallTopK" name: ClassVar = "Recall (top-k)" header: str = "Recall" @@ -166,8 +164,7 @@ class TestRecallTopKRenderer(BaseTopkRecsysRenderer): class TestFBetaTopK(BaseTopkRecsysTest): - class Config: - type_alias = "evidently:test:TestFBetaTopK" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestFBetaTopK" name: ClassVar = "F_beta (top-k)" header: str = "F_beta" @@ -182,8 +179,7 @@ class TestFBetaTopKRenderer(BaseTopkRecsysRenderer): class TestMAPK(BaseTopkRecsysTest): - class Config: - type_alias = "evidently:test:TestMAPK" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestMAPK" name: ClassVar = "MAP (top-k)" header: str = "MAP" @@ -198,8 +194,7 @@ class TestMAPKRenderer(BaseTopkRecsysRenderer): class TestMARK(BaseTopkRecsysTest): - class Config: - type_alias = "evidently:test:TestMARK" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestMARK" name: ClassVar = "MAR (top-k)" header: str = "MAR" @@ -214,8 +209,7 @@ class TestMARKRenderer(BaseTopkRecsysRenderer): class TestNDCGK(BaseTopkRecsysTest): - class Config: - type_alias = "evidently:test:TestNDCGK" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestNDCGK" name: ClassVar = "NDCG (top-k)" header: str = "NDCG" @@ -230,8 +224,7 @@ class TestNDCGKRenderer(BaseTopkRecsysRenderer): class TestHitRateK(BaseTopkRecsysTest): - class Config: - type_alias = "evidently:test:TestHitRateK" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestHitRateK" name: ClassVar = "Hit Rate (top-k)" header: str = "Hit Rate" @@ -246,8 +239,7 @@ class TestHitRateKRenderer(BaseTopkRecsysRenderer): class TestMRRK(BaseTopkRecsysTest): - class Config: - type_alias = "evidently:test:TestMRRK" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestMRRK" name: ClassVar = "MRR (top-k)" header: str = "MRR" @@ -359,8 +351,7 @@ def render_html(self, obj: BaseNotRankRecsysTest) -> TestHtmlInfo: class TestNovelty(BaseNotRankRecsysTest[NoveltyMetric]): - class Config: - type_alias = "evidently:test:TestNovelty" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestNovelty" name: ClassVar = "Novelty (top-k)" header: str = "Novelty" @@ -375,8 +366,7 @@ class TestNoveltyRenderer(BaseNotRankRecsysTestRenderer): class TestDiversity(BaseNotRankRecsysTest[DiversityMetric]): - class Config: - type_alias = "evidently:test:TestDiversity" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestDiversity" name: ClassVar = "Diversity (top-k)" header: str = "Diversity" @@ -391,8 +381,7 @@ class TestDiversityRenderer(BaseNotRankRecsysTestRenderer): class TestSerendipity(BaseNotRankRecsysTest[SerendipityMetric]): - class Config: - type_alias = "evidently:test:TestSerendipity" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestSerendipity" name: ClassVar = "Serendipity (top-k)" header: str = "Serendipity" @@ -407,8 +396,7 @@ class TestSerendipityRenderer(BaseNotRankRecsysTestRenderer): class TestPersonalization(BaseNotRankRecsysTest[PersonalizationMetric]): - class Config: - type_alias = "evidently:test:TestPersonalization" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestPersonalization" name: ClassVar = "Personalization (top-k)" header: str = "Personalization" @@ -453,8 +441,7 @@ def render_html(self, obj: BaseNotRankRecsysTest) -> TestHtmlInfo: class TestARP(BaseCheckValueTest): - class Config: - type_alias = "evidently:test:TestARP" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestARP" group: ClassVar = RECSYS_GROUP.id name: ClassVar = "ARP (top-k)" @@ -512,8 +499,7 @@ def metric(self): class TestGiniIndex(BaseCheckValueTest): - class Config: - type_alias = "evidently:test:TestGiniIndex" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestGiniIndex" group: ClassVar = RECSYS_GROUP.id name: ClassVar = "Gini Index (top-k)" @@ -568,8 +554,7 @@ def metric(self): class TestCoverage(BaseCheckValueTest): - class Config: - type_alias = "evidently:test:TestCoverage" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestCoverage" group: ClassVar = RECSYS_GROUP.id name: ClassVar = "Coverage (top-k)" @@ -649,8 +634,7 @@ def render_html(self, obj: Union[TestARP, TestGiniIndex, TestCoverage]) -> TestH class TestScoreEntropy(BaseCheckValueTest): - class Config: - type_alias = "evidently:test:TestScoreEntropy" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestScoreEntropy" group: ClassVar = RECSYS_GROUP.id name: ClassVar = "Score Entropy (top-k)" diff --git a/src/evidently/legacy/tests/regression_performance_tests.py b/src/evidently/legacy/tests/regression_performance_tests.py index c59a9d8ca8..531e3d7841 100644 --- a/src/evidently/legacy/tests/regression_performance_tests.py +++ b/src/evidently/legacy/tests/regression_performance_tests.py @@ -65,8 +65,7 @@ def dummy_metric(self): class TestValueMAE(BaseRegressionPerformanceMetricsTest): - class Config: - type_alias = "evidently:test:TestValueMAE" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestValueMAE" name: ClassVar = "Mean Absolute Error (MAE)" @@ -104,8 +103,7 @@ def render_html(self, obj: TestValueMAE) -> TestHtmlInfo: class TestValueMAPE(BaseRegressionPerformanceMetricsTest): - class Config: - type_alias = "evidently:test:TestValueMAPE" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestValueMAPE" name: ClassVar = "Mean Absolute Percentage Error (MAPE)" @@ -145,8 +143,7 @@ def render_html(self, obj: TestValueMAPE) -> TestHtmlInfo: class TestValueRMSE(BaseRegressionPerformanceMetricsTest): - class Config: - type_alias = "evidently:test:TestValueRMSE" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestValueRMSE" name: ClassVar = "Root Mean Square Error (RMSE)" @@ -184,8 +181,7 @@ def render_html(self, obj: TestValueRMSE) -> TestHtmlInfo: class TestValueMeanError(BaseRegressionPerformanceMetricsTest): - class Config: - type_alias = "evidently:test:TestValueMeanError" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestValueMeanError" name: ClassVar = "Mean Error (ME)" @@ -230,8 +226,7 @@ def render_html(self, obj: TestValueMeanError) -> TestHtmlInfo: class TestValueAbsMaxError(BaseRegressionPerformanceMetricsTest): - class Config: - type_alias = "evidently:test:TestValueAbsMaxError" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestValueAbsMaxError" name: ClassVar = "Max Absolute Error" @@ -265,8 +260,7 @@ def render_html(self, obj: TestValueAbsMaxError) -> TestHtmlInfo: class TestValueR2Score(BaseRegressionPerformanceMetricsTest): - class Config: - type_alias = "evidently:test:TestValueR2Score" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestValueR2Score" name: ClassVar = "R2 Score" diff --git a/src/evidently/legacy/tests/utils.py b/src/evidently/legacy/tests/utils.py index 697826f566..75f5631783 100644 --- a/src/evidently/legacy/tests/utils.py +++ b/src/evidently/legacy/tests/utils.py @@ -178,6 +178,9 @@ class ApproxValueNoDict(ApproxValue): def dict(self, *args, **kwargs): return self + def model_dump(self, *args, **kwargs): + return self + # some monkeing for np asserts to work with ApproxValue np.core.numeric.ScalarType = np.core.numeric.ScalarType + (ApproxValue, ApproxValueNoDict) # type: ignore[attr-defined] diff --git a/src/evidently/legacy/ui/api/models.py b/src/evidently/legacy/ui/api/models.py index 1fa0aa66e0..8fe2ae8398 100644 --- a/src/evidently/legacy/ui/api/models.py +++ b/src/evidently/legacy/ui/api/models.py @@ -4,8 +4,9 @@ from typing import Optional from typing import TypeVar -from evidently._pydantic_compat import BaseModel -from evidently._pydantic_compat import Extra +from pydantic import BaseModel +from pydantic import ConfigDict + from evidently.legacy.base_metric import Metric from evidently.legacy.model.dashboard import DashboardInfo from evidently.legacy.model.widget import BaseWidgetInfo @@ -30,8 +31,7 @@ class EvidentlyAPIModel(BaseModel): # todo: migrate all models to this base - class Config: - extra = Extra.forbid + model_config = ConfigDict(extra="forbid") class MetricModel(BaseModel): @@ -180,7 +180,7 @@ def from_user(cls, user: User): return UserModel(id=user.id, name=user.name, email=user.email) def merge(self: UT, other: "UserModel") -> UT: - kwargs = {f: getattr(other, f, None) or getattr(self, f) for f in self.__fields__} + kwargs = {f: getattr(other, f, None) or getattr(self, f) for f in self.model_fields} return self.__class__(**kwargs) diff --git a/src/evidently/legacy/ui/app.py b/src/evidently/legacy/ui/app.py index 973f38810b..fa10f366f8 100644 --- a/src/evidently/legacy/ui/app.py +++ b/src/evidently/legacy/ui/app.py @@ -2,8 +2,8 @@ from typing import Optional import uvicorn +from pydantic import SecretStr -from evidently._pydantic_compat import SecretStr from evidently.legacy.ui.components.base import AppBuilder from evidently.legacy.ui.components.storage import LocalStorageComponent from evidently.legacy.ui.config import AppConfig diff --git a/src/evidently/legacy/ui/base.py b/src/evidently/legacy/ui/base.py index 052bbd3f21..a4a88daaed 100644 --- a/src/evidently/legacy/ui/base.py +++ b/src/evidently/legacy/ui/base.py @@ -16,11 +16,11 @@ from typing import Union import uuid6 +from pydantic import BaseModel +from pydantic import Field +from pydantic import PrivateAttr +from pydantic import TypeAdapter -from evidently._pydantic_compat import BaseModel -from evidently._pydantic_compat import Field -from evidently._pydantic_compat import PrivateAttr -from evidently._pydantic_compat import parse_obj_as from evidently.core.report import Snapshot as SnapshotV2 from evidently.legacy.core import new_id from evidently.legacy.model.dashboard import DashboardInfo @@ -121,9 +121,6 @@ async def get_additional_graphs(self): class Project(Entity): entity_type: ClassVar[EntityType] = EntityType.Project - class Config: - underscore_attrs_are_private = True - id: ProjectID = Field(default_factory=new_id) name: str description: Optional[str] = None @@ -320,7 +317,7 @@ def parse_value(cls: Type[PointType], value: Any) -> PointType: return value if isinstance(value, str): value = json.loads(value) - return parse_obj_as(cls, value) + return TypeAdapter(cls).validate_python(value) @abstractmethod async def load_test_results( diff --git a/src/evidently/legacy/ui/components/base.py b/src/evidently/legacy/ui/components/base.py index 8332dd710c..bdf796e164 100644 --- a/src/evidently/legacy/ui/components/base.py +++ b/src/evidently/legacy/ui/components/base.py @@ -16,7 +16,6 @@ from litestar.types import ExceptionHandlersMap from litestar.types import Middleware -from evidently._pydantic_compat import Extra from evidently.legacy.ui.utils import parse_json from evidently.pydantic_utils import PolymorphicModel @@ -69,10 +68,9 @@ def get_priority(self) -> int: def get_requirements(self) -> List[Type["Component"]]: return self.__require__ - class Config: - extra = Extra.forbid - alias_required = False - is_base_type = True + model_config = {"extra": "forbid"} + __alias_required__: ClassVar[bool] = False + __is_base_type__: ClassVar[bool] = True def __init_subclass__(cls): super().__init_subclass__() diff --git a/src/evidently/legacy/ui/components/local_storage.py b/src/evidently/legacy/ui/components/local_storage.py index 1d913fa5f7..18e29763c5 100644 --- a/src/evidently/legacy/ui/components/local_storage.py +++ b/src/evidently/legacy/ui/components/local_storage.py @@ -16,8 +16,7 @@ class FSSpecBlobComponent(BlobStorageComponent): - class Config: - type_alias = "fsspec" + __type_alias__: ClassVar[Optional[str]] = "fsspec" path: str @@ -26,8 +25,7 @@ def dependency_factory(self) -> Callable[..., BlobStorage]: class JsonMetadataComponent(MetadataStorageComponent): - class Config: - type_alias = "json_file" + __type_alias__: ClassVar[Optional[str]] = "json_file" path: str @@ -39,8 +37,7 @@ async def json_meta(local_state: Optional[LocalState] = None): class InmemoryDataComponent(DataStorageComponent): - class Config: - type_alias = "inmemory" + __type_alias__: ClassVar[Optional[str]] = "inmemory" path: str diff --git a/src/evidently/legacy/ui/components/security.py b/src/evidently/legacy/ui/components/security.py index 67893c5b83..8306b994c5 100644 --- a/src/evidently/legacy/ui/components/security.py +++ b/src/evidently/legacy/ui/components/security.py @@ -1,6 +1,7 @@ from abc import ABC from typing import ClassVar from typing import Dict +from typing import Optional import uuid6 from litestar import Request @@ -11,8 +12,8 @@ from litestar.types import Receive from litestar.types import Scope from litestar.types import Send +from pydantic import SecretStr -from evidently._pydantic_compat import SecretStr from evidently.legacy.ui.components.base import Component from evidently.legacy.ui.components.base import ComponentContext from evidently.legacy.ui.errors import NotEnoughPermissions @@ -27,8 +28,7 @@ class SecurityComponent(Component, ABC): add_security_middleware: ClassVar[bool] = True - class Config: - is_base_type = True + __is_base_type__: ClassVar[bool] = True def get_security(self) -> SecurityService: raise NotImplementedError @@ -87,8 +87,7 @@ def get_dependencies(self, ctx: ComponentContext) -> Dict[str, Provide]: class NoSecurityComponent(SimpleSecurity): - class Config: - type_alias = "none" + __type_alias__: ClassVar[Optional[str]] = "none" dummy_user_id: UserID = uuid6.UUID(int=1, version=7) dummy_org_id: OrgID = uuid6.UUID(int=2, version=7) @@ -100,8 +99,7 @@ def get_security(self) -> SecurityService: class TokenSecurityComponent(SimpleSecurity): - class Config: - type_alias = "token" + __type_alias__: ClassVar[Optional[str]] = "token" token: SecretStr diff --git a/src/evidently/legacy/ui/components/storage.py b/src/evidently/legacy/ui/components/storage.py index 0fa03aab57..6180db52a3 100644 --- a/src/evidently/legacy/ui/components/storage.py +++ b/src/evidently/legacy/ui/components/storage.py @@ -14,8 +14,7 @@ class StorageComponent(FactoryComponent[ProjectManager], ABC): - class Config: - is_base_type = True + __is_base_type__: ClassVar[bool] = True dependency_name: ClassVar = "project_manager" use_cache: ClassVar[bool] = True @@ -31,8 +30,7 @@ def dependency_factory(self) -> Callable[..., ProjectManager]: class MetadataStorageComponent(FactoryComponent[ProjectMetadataStorage], ABC): - class Config: - is_base_type = True + __is_base_type__: ClassVar[bool] = True __section__: ClassVar = "metadata" dependency_name: ClassVar = "project_metadata" @@ -41,8 +39,7 @@ class Config: class DataStorageComponent(FactoryComponent[DataStorage], ABC): - class Config: - is_base_type = True + __is_base_type__: ClassVar[bool] = True __section__: ClassVar = "data" @@ -52,8 +49,7 @@ class Config: class BlobStorageComponent(FactoryComponent[BlobStorage], ABC): - class Config: - is_base_type = True + __is_base_type__: ClassVar[bool] = True __section__: ClassVar = "blob" diff --git a/src/evidently/legacy/ui/config.py b/src/evidently/legacy/ui/config.py index bd82c8a53c..c76336d497 100644 --- a/src/evidently/legacy/ui/config.py +++ b/src/evidently/legacy/ui/config.py @@ -10,10 +10,10 @@ from dynaconf.utils.boxing import DynaBox from litestar import Litestar from litestar.di import Provide +from pydantic import BaseModel +from pydantic import PrivateAttr +from pydantic import TypeAdapter -from evidently._pydantic_compat import BaseModel -from evidently._pydantic_compat import PrivateAttr -from evidently._pydantic_compat import parse_obj_as from evidently.legacy.ui.components.base import SECTION_COMPONENT_TYPE_MAPPING from evidently.legacy.ui.components.base import AppBuilder from evidently.legacy.ui.components.base import Component @@ -119,14 +119,16 @@ def load_config(config_type: Type[TConfig], box: dict) -> TConfig: continue if section == "additional_components": for subsection, compoennt_subdict in component_dict.items(): - component = parse_obj_as(SECTION_COMPONENT_TYPE_MAPPING.get(subsection, Component), compoennt_subdict) + component = TypeAdapter( + SECTION_COMPONENT_TYPE_MAPPING.get(subsection).validate_python(Component), compoennt_subdict + ) components[subsection] = component elif section in config_type.__fields__: type_ = config_type.__fields__[section].type_ - component = parse_obj_as(type_, component_dict) + component = TypeAdapter(type_).validate_python(component_dict) named_components[section] = component elif section in SECTION_COMPONENT_TYPE_MAPPING: - component = parse_obj_as(SECTION_COMPONENT_TYPE_MAPPING[section], component_dict) + component = TypeAdapter(SECTION_COMPONENT_TYPE_MAPPING[section]).validate_python(component_dict) components[section] = component else: raise ValueError(f"unknown config section {section}") diff --git a/src/evidently/legacy/ui/dashboards/base.py b/src/evidently/legacy/ui/dashboards/base.py index b5a13d16f9..286e9afedc 100644 --- a/src/evidently/legacy/ui/dashboards/base.py +++ b/src/evidently/legacy/ui/dashboards/base.py @@ -4,16 +4,18 @@ from functools import wraps from typing import TYPE_CHECKING from typing import Any +from typing import ClassVar from typing import Dict from typing import List from typing import Optional from typing import Union import uuid6 +from pydantic import BaseModel +from pydantic import ConfigDict +from pydantic import Field +from pydantic.v1 import validator -from evidently._pydantic_compat import BaseModel -from evidently._pydantic_compat import Field -from evidently._pydantic_compat import validator from evidently.legacy.base_metric import Metric from evidently.legacy.core import new_id from evidently.legacy.model.dashboard import DashboardInfo @@ -53,6 +55,8 @@ def filter(self, report: ReportBase): class PanelValue(BaseModel): + model_config = ConfigDict(arbitrary_types_allowed=True) + field_path: Union[str, FieldPath] metric_id: Optional[str] = None metric_fingerprint: Optional[str] = None @@ -133,9 +137,8 @@ async def inner(self: "DashboardPanel", *args, **kwargs) -> BaseWidgetInfo: class DashboardPanel(EnumValueMixin, PolymorphicModel): - class Config: - type_alias = "evidently:dashboard_panel:DashboardPanel" - is_base_type = True + __type_alias__: ClassVar[Optional[str]] = "evidently:dashboard_panel:DashboardPanel" + __is_base_type__: ClassVar[bool] = True id: PanelID = Field(default_factory=new_id) title: str diff --git a/src/evidently/legacy/ui/dashboards/reports.py b/src/evidently/legacy/ui/dashboards/reports.py index 153e0d06c8..1fbe534bca 100644 --- a/src/evidently/legacy/ui/dashboards/reports.py +++ b/src/evidently/legacy/ui/dashboards/reports.py @@ -2,6 +2,7 @@ from collections import defaultdict from typing import TYPE_CHECKING from typing import Any +from typing import ClassVar from typing import Dict from typing import List from typing import Optional @@ -40,8 +41,7 @@ @autoregister class DashboardPanelPlot(DashboardPanel): - class Config: - type_alias = "evidently:dashboard_panel:DashboardPanelPlot" + __type_alias__: ClassVar[Optional[str]] = "evidently:dashboard_panel:DashboardPanelPlot" values: List[PanelValue] plot_type: PlotType @@ -102,8 +102,7 @@ def plot_type_cls(self): @autoregister class DashboardPanelCounter(DashboardPanel): - class Config: - type_alias = "evidently:dashboard_panel:DashboardPanelCounter" + __type_alias__: ClassVar[Optional[str]] = "evidently:dashboard_panel:DashboardPanelCounter" agg: CounterAgg value: Optional[PanelValue] = None @@ -148,8 +147,7 @@ def _get_counter_value(self, points: Dict[Metric, List[PointInfo]]) -> float: @autoregister class DashboardPanelDistribution(DashboardPanel): - class Config: - type_alias = "evidently:dashboard_panel:DashboardPanelDistribution" + __type_alias__: ClassVar[Optional[str]] = "evidently:dashboard_panel:DashboardPanelDistribution" value: PanelValue barmode: HistBarMode = HistBarMode.STACK diff --git a/src/evidently/legacy/ui/dashboards/test_suites.py b/src/evidently/legacy/ui/dashboards/test_suites.py index 50b0e52623..4e38464fa5 100644 --- a/src/evidently/legacy/ui/dashboards/test_suites.py +++ b/src/evidently/legacy/ui/dashboards/test_suites.py @@ -4,6 +4,7 @@ import warnings from collections import Counter from typing import Any +from typing import ClassVar from typing import Dict from typing import List from typing import Optional @@ -12,8 +13,8 @@ import pandas as pd from plotly import graph_objs as go +from pydantic import BaseModel -from evidently._pydantic_compat import BaseModel from evidently.legacy.model.widget import BaseWidgetInfo from evidently.legacy.renderers.html_widgets import CounterData from evidently.legacy.renderers.html_widgets import counter @@ -89,8 +90,7 @@ def get(self, test_suite: TestSuite) -> Dict[Test, TestInfo]: @autoregister class DashboardPanelTestSuite(DashboardPanel): - class Config: - type_alias = "evidently:dashboard_panel:DashboardPanelTestSuite" + __type_alias__: ClassVar[Optional[str]] = "evidently:dashboard_panel:DashboardPanelTestSuite" test_filters: List[TestFilter] = [] filter: ReportFilter = ReportFilter(metadata_values={}, tag_values=[], include_test_suites=True) @@ -200,8 +200,7 @@ def to_period(time_agg: Optional[str], timestamp: datetime.datetime) -> datetime @autoregister class DashboardPanelTestSuiteCounter(DashboardPanel): - class Config: - type_alias = "evidently:dashboard_panel:DashboardPanelTestSuiteCounter" + __type_alias__: ClassVar[Optional[str]] = "evidently:dashboard_panel:DashboardPanelTestSuiteCounter" agg: CounterAgg = CounterAgg.NONE filter: ReportFilter = ReportFilter(metadata_values={}, tag_values=[], include_test_suites=True) diff --git a/src/evidently/legacy/ui/dashboards/utils.py b/src/evidently/legacy/ui/dashboards/utils.py index 3a089bd431..ee7f4ea484 100644 --- a/src/evidently/legacy/ui/dashboards/utils.py +++ b/src/evidently/legacy/ui/dashboards/utils.py @@ -11,8 +11,8 @@ from typing import Union import plotly.io as pio +from pydantic import BaseModel -from evidently._pydantic_compat import BaseModel from evidently.core.metric_types import BoundTest from evidently.core.metric_types import MetricTest from evidently.legacy.base_metric import ColumnName diff --git a/src/evidently/legacy/ui/managers/auth.py b/src/evidently/legacy/ui/managers/auth.py index 791b407436..d8d423799f 100644 --- a/src/evidently/legacy/ui/managers/auth.py +++ b/src/evidently/legacy/ui/managers/auth.py @@ -8,7 +8,8 @@ from typing import Set from typing import Tuple -from evidently._pydantic_compat import BaseModel +from pydantic import BaseModel + from evidently.legacy.ui.base import EntityType from evidently.legacy.ui.base import Org from evidently.legacy.ui.base import Team diff --git a/src/evidently/legacy/ui/managers/projects.py b/src/evidently/legacy/ui/managers/projects.py index 0159a3f868..c0da6acc95 100644 --- a/src/evidently/legacy/ui/managers/projects.py +++ b/src/evidently/legacy/ui/managers/projects.py @@ -5,9 +5,9 @@ from typing import Union from litestar.params import Dependency +from pydantic import TypeAdapter from typing_extensions import Annotated -from evidently._pydantic_compat import parse_obj_as from evidently.legacy.suite.base_suite import Snapshot from evidently.legacy.ui.base import BlobStorage from evidently.legacy.ui.base import DataStorage @@ -197,7 +197,7 @@ async def load_snapshot( if isinstance(snapshot, SnapshotID): snapshot = await self.get_snapshot_metadata(user_id, project_id, snapshot) with self.blob_storage.open_blob(snapshot.blob.id) as f: - return parse_obj_as(Snapshot, json.load(f)) + return TypeAdapter(Snapshot).validate_python(json.load(f)) async def get_snapshot_metadata( self, user_id: UserID, project_id: ProjectID, snapshot_id: SnapshotID diff --git a/src/evidently/legacy/ui/storage/local/base.py b/src/evidently/legacy/ui/storage/local/base.py index b5edf2747d..c8fdb5ca1f 100644 --- a/src/evidently/legacy/ui/storage/local/base.py +++ b/src/evidently/legacy/ui/storage/local/base.py @@ -12,10 +12,10 @@ import uuid6 from fsspec import AbstractFileSystem from fsspec import get_fs_token_paths +from pydantic import PrivateAttr +from pydantic import TypeAdapter +from pydantic import ValidationError -from evidently._pydantic_compat import PrivateAttr -from evidently._pydantic_compat import ValidationError -from evidently._pydantic_compat import parse_obj_as from evidently.legacy.suite.base_suite import Snapshot from evidently.legacy.test_suite import TestSuite from evidently.legacy.tests.base_test import Test @@ -127,7 +127,7 @@ async def get_blob_metadata(self, blob_id: BlobID) -> BlobMetadata: def load_project(location: FSLocation, path: str) -> Optional[Project]: try: with location.open(posixpath.join(path, METADATA_PATH)) as f: - return parse_obj_as(Project, json.load(f)) + return TypeAdapter(Project).validate_python(json.load(f)) except FileNotFoundError: return None @@ -177,7 +177,7 @@ def reload_snapshot(self, project: Project, snapshot_id: SnapshotID, skip_errors try: snapshot_path = posixpath.join(str(project.id), SNAPSHOTS, str(snapshot_id) + ".json") with self.location.open(snapshot_path) as f: - suite = parse_obj_as(Snapshot, json.load(f)) + suite = TypeAdapter(Snapshot).validate_python(json.load(f)) snapshot = SnapshotMetadata.from_snapshot( suite, BlobMetadata(id=snapshot_path, size=self.location.size(snapshot_path)) ).bind(project) diff --git a/src/evidently/legacy/ui/storage/utils.py b/src/evidently/legacy/ui/storage/utils.py index 7211b2678b..758c27602f 100644 --- a/src/evidently/legacy/ui/storage/utils.py +++ b/src/evidently/legacy/ui/storage/utils.py @@ -6,7 +6,8 @@ from typing import Optional from typing import Tuple -from evidently._pydantic_compat import BaseModel +from pydantic import BaseModel + from evidently.legacy.base_metric import MetricResult from evidently.legacy.core import BaseResult from evidently.legacy.utils import NumpyEncoder @@ -43,7 +44,7 @@ def iterate_obj_fields( if isinstance(obj, dict): yield from (r for key, value in obj.items() for r in iterate_obj_fields(value, paths + [str(key)], early_stop)) return - if isinstance(obj, BaseResult) and obj.__config__.extract_as_obj: + if isinstance(obj, BaseResult) and obj.__extract_as_obj__: yield ".".join(paths), obj return if isinstance(obj, BaseModel): @@ -61,7 +62,7 @@ def iterate_obj_float_fields(obj: Any, paths: List[str]) -> Iterator[Tuple[str, if isinstance(value, dict): yield path, json.dumps(value, cls=NumpyEncoder) continue - if isinstance(value, BaseResult) and value.__config__.extract_as_obj: + if isinstance(value, BaseResult) and value.__extract_as_obj__: yield path, json.dumps(value.dict(), cls=NumpyEncoder) continue try: diff --git a/src/evidently/legacy/ui/utils.py b/src/evidently/legacy/ui/utils.py index 3876c93313..912767b6ee 100644 --- a/src/evidently/legacy/ui/utils.py +++ b/src/evidently/legacy/ui/utils.py @@ -7,9 +7,9 @@ from typing import Union import requests +from pydantic import BaseModel +from pydantic import TypeAdapter -from evidently._pydantic_compat import BaseModel -from evidently._pydantic_compat import parse_obj_as from evidently.legacy.ui.storage.common import SECRET_HEADER_NAME from evidently.legacy.utils import NumpyEncoder @@ -42,7 +42,7 @@ def _request( ) response.raise_for_status() if response_model is not None: - return parse_obj_as(response_model, response.json()) + return TypeAdapter(response_model).validate_python(response.json()) return response diff --git a/src/evidently/legacy/ui/workspace/cloud.py b/src/evidently/legacy/ui/workspace/cloud.py index 965c53a0f9..9b654638c1 100644 --- a/src/evidently/legacy/ui/workspace/cloud.py +++ b/src/evidently/legacy/ui/workspace/cloud.py @@ -13,10 +13,10 @@ from typing import overload import pandas as pd +from pydantic import TypeAdapter from requests import HTTPError from requests import Response -from evidently._pydantic_compat import parse_obj_as from evidently.core.datasets import DataDefinition from evidently.core.datasets import Dataset from evidently.legacy.pipeline.column_mapping import ColumnMapping @@ -245,7 +245,7 @@ def load_dataset_v2(self, dataset_id: DatasetID) -> Dataset: metadata, file_content = read_multipart_response(response) df = pd.read_parquet(BytesIO(file_content)) - data_def = parse_obj_as(DataDefinition, metadata["data_definition"]) + data_def = TypeAdapter(DataDefinition).validate_python(metadata["data_definition"]) return Dataset.from_pandas(df, data_definition=data_def) diff --git a/src/evidently/legacy/ui/workspace/remote.py b/src/evidently/legacy/ui/workspace/remote.py index 07076f3e9f..c03b2ba69f 100644 --- a/src/evidently/legacy/ui/workspace/remote.py +++ b/src/evidently/legacy/ui/workspace/remote.py @@ -15,11 +15,11 @@ from typing import overload from urllib.error import HTTPError +from pydantic import TypeAdapter from requests import Request from requests import Response from requests import Session -from evidently._pydantic_compat import parse_obj_as from evidently.errors import EvidentlyError from evidently.legacy.suite.base_suite import Snapshot from evidently.legacy.ui.api.service import EVIDENTLY_APPLICATION_NAME @@ -139,7 +139,7 @@ def _request( pass response.raise_for_status() if response_model is not None: - return parse_obj_as(response_model, response.json()) + return TypeAdapter(response_model).validate_python(response.json()) return response diff --git a/src/evidently/legacy/utils/data_preprocessing.py b/src/evidently/legacy/utils/data_preprocessing.py index 91d8161a6a..cbf42f3afb 100644 --- a/src/evidently/legacy/utils/data_preprocessing.py +++ b/src/evidently/legacy/utils/data_preprocessing.py @@ -11,8 +11,8 @@ import pandas as pd import pandas.api.types +from pydantic import BaseModel -from evidently._pydantic_compat import BaseModel from evidently.legacy.core import ColumnType from evidently.legacy.pipeline.column_mapping import ColumnMapping from evidently.legacy.pipeline.column_mapping import RecomType @@ -73,18 +73,18 @@ def _check_filter( class DataDefinition(EnumValueMixin): columns: Dict[str, ColumnDefinition] - target: Optional[ColumnDefinition] - prediction_columns: Optional[PredictionColumns] - id_column: Optional[ColumnDefinition] - datetime_column: Optional[ColumnDefinition] - embeddings: Optional[Dict[str, List[str]]] - user_id: Optional[ColumnDefinition] - item_id: Optional[ColumnDefinition] - - task: Optional[str] - classification_labels: Optional[TargetNames] + target: Optional[ColumnDefinition] = None + prediction_columns: Optional[PredictionColumns] = None + id_column: Optional[ColumnDefinition] = None + datetime_column: Optional[ColumnDefinition] = None + embeddings: Optional[Dict[str, List[str]]] = None + user_id: Optional[ColumnDefinition] = None + item_id: Optional[ColumnDefinition] = None + + task: Optional[str] = None + classification_labels: Optional[TargetNames] = None reference_present: bool - recommendations_type: Optional[RecomType] + recommendations_type: Optional[RecomType] = None def get_column(self, column_name: str) -> ColumnDefinition: return self.columns[column_name] diff --git a/src/evidently/legacy/utils/numpy_encoder.py b/src/evidently/legacy/utils/numpy_encoder.py index 20af3955ec..d6078c8212 100644 --- a/src/evidently/legacy/utils/numpy_encoder.py +++ b/src/evidently/legacy/utils/numpy_encoder.py @@ -2,6 +2,7 @@ import json import typing import uuid +from enum import Enum from functools import partial from typing import Callable from typing import Tuple @@ -32,6 +33,7 @@ ((uuid.UUID,), lambda obj: str(obj)), ((ColumnType,), lambda obj: obj.value), ((pd.Period,), lambda obj: str(obj)), + ((Enum,), lambda obj: obj.value), ) diff --git a/src/evidently/legacy/utils/types.py b/src/evidently/legacy/utils/types.py index d49cceedcb..c87d1c0955 100644 --- a/src/evidently/legacy/utils/types.py +++ b/src/evidently/legacy/utils/types.py @@ -18,9 +18,6 @@ class ApproxValue(FrozenBaseModel, ExcludeNoneMixin): """Class for approximate scalar value calculations""" - class Config: - smart_union = True - DEFAULT_RELATIVE: ClassVar = 1e-6 DEFAULT_ABSOLUTE: ClassVar = 1e-12 @@ -28,7 +25,11 @@ class Config: relative: Numeric absolute: Numeric - def __init__(self, value: Numeric, relative: Optional[Numeric] = None, absolute: Optional[Numeric] = None): + def __init__( + self, value: Numeric, relative: Optional[Numeric] = None, absolute: Optional[Numeric] = None, **kwargs + ): + # if kwargs: + # print(kwargs) if relative is not None and relative <= 0: raise ValueError("Relative value for approx should be greater than 0") @@ -67,5 +68,8 @@ def __gt__(self, other): def __ge__(self, other): return self.value + self.tolerance >= other + def __hash__(self): + return hash(f"{self.value} {self.absolute} {self.relative}") + NumericApprox = Union[int, float, ApproxValue] diff --git a/src/evidently/llm/datagen/base.py b/src/evidently/llm/datagen/base.py index e9b5aa8905..b64f30792f 100644 --- a/src/evidently/llm/datagen/base.py +++ b/src/evidently/llm/datagen/base.py @@ -4,9 +4,10 @@ from typing import Optional import pandas as pd +from pydantic import ConfigDict +from pydantic import PrivateAttr from typing_extensions import TypeAlias -from evidently._pydantic_compat import PrivateAttr from evidently.legacy.options.base import Options from evidently.legacy.utils.sync import async_to_sync from evidently.llm.utils.blocks import SimpleBlock @@ -29,9 +30,8 @@ class BaseDatasetGenerator(AutoAliasMixin, EvidentlyBaseModel, ABC): __alias_type__: ClassVar = "dataset_generator" - class Config: - is_base_type = True - extra = "forbid" + __is_base_type__: ClassVar[bool] = True + model_config = ConfigDict(extra="forbid") options: Options """Processing options.""" diff --git a/src/evidently/llm/optimization/optimizer.py b/src/evidently/llm/optimization/optimizer.py index fa2e44ee4e..b5883e08f3 100644 --- a/src/evidently/llm/optimization/optimizer.py +++ b/src/evidently/llm/optimization/optimizer.py @@ -17,11 +17,12 @@ import numpy as np import pandas as pd +from pydantic import BaseModel +from pydantic import ConfigDict +from pydantic import Field +from pydantic import PrivateAttr from sklearn.model_selection import train_test_split -from evidently._pydantic_compat import BaseModel -from evidently._pydantic_compat import Field -from evidently._pydantic_compat import PrivateAttr from evidently.legacy.core import new_id from evidently.legacy.options.base import Options from evidently.legacy.utils.llm.wrapper import LLMWrapper @@ -164,6 +165,8 @@ class LLMDataset(BaseModel): for train/val/test splits. """ + model_config = ConfigDict(arbitrary_types_allowed=True) + input_values: pd.Series """Input values (e.g., prompts).""" target: Optional[pd.Series] = None @@ -256,6 +259,8 @@ class LLMResultDataset(BaseModel): Stores predictions, reasoning, and scores from optimization runs. """ + model_config = ConfigDict(arbitrary_types_allowed=True) + predictions: Optional[pd.Series] = None """Optional model predictions.""" reasoning: Optional[pd.Series] = None @@ -361,8 +366,7 @@ class OptimizerConfig(AutoAliasMixin, EvidentlyBaseModel): __alias_type__: ClassVar = "optimizer_config" - class Config: - is_base_type = True + __is_base_type__: ClassVar[bool] = True provider: str = "openai" """LLM provider name.""" @@ -387,8 +391,7 @@ class OptimizerLog(AutoAliasMixin, EvidentlyBaseModel, ABC): __alias_type__: ClassVar = "optimizer_log" __is_step__: ClassVar[bool] = False - class Config: - is_base_type = True + __is_base_type__: ClassVar[bool] = True id: LogID = Field(default_factory=new_id) """Unique log identifier.""" diff --git a/src/evidently/llm/optimization/prompts.py b/src/evidently/llm/optimization/prompts.py index 3d4c8da307..98c961fa40 100644 --- a/src/evidently/llm/optimization/prompts.py +++ b/src/evidently/llm/optimization/prompts.py @@ -14,11 +14,11 @@ from typing import Union import pandas as pd +from pydantic import BaseModel +from pydantic import PrivateAttr from evidently import ColumnType from evidently import Dataset -from evidently._pydantic_compat import BaseModel -from evidently._pydantic_compat import PrivateAttr from evidently.core.datasets import FeatureDescriptor from evidently.core.datasets import LLMClassification from evidently.legacy.features.llm_judge import BinaryClassificationPromptTemplate @@ -97,8 +97,7 @@ class PromptOptimizerStrategy(BaseArgTypeRegistry, AutoAliasMixin, EvidentlyBase __alias_type__: ClassVar = "prompt_optimizer_strategy" - class Config: - is_base_type = True + __is_base_type__: ClassVar[bool] = True @abstractmethod def get_default_scorer(self) -> "OptimizationScorer": @@ -216,8 +215,7 @@ class PromptExecutor(AutoAliasMixin, EvidentlyBaseModel, InitContextMixin, ABC): __alias_type__: ClassVar = "prompt_executor" - class Config: - is_base_type = True + __is_base_type__: ClassVar[bool] = True @abstractmethod def execute(self, prompt: str, run: OptimizerRun) -> PromptExecutionLog: diff --git a/src/evidently/llm/optimization/scorers.py b/src/evidently/llm/optimization/scorers.py index ed54a96786..afd7749d51 100644 --- a/src/evidently/llm/optimization/scorers.py +++ b/src/evidently/llm/optimization/scorers.py @@ -42,8 +42,7 @@ class OptimizationScorer(BaseArgTypeRegistry, AutoAliasMixin, EvidentlyBaseModel __alias_type__: ClassVar = "optimizer_scorer" - class Config: - is_base_type = True + __is_base_type__: ClassVar[bool] = True def get_name(self) -> str: """Get the name of this scorer. diff --git a/src/evidently/llm/prompts/content.py b/src/evidently/llm/prompts/content.py index 2d9f27be75..8b199c08fb 100644 --- a/src/evidently/llm/prompts/content.py +++ b/src/evidently/llm/prompts/content.py @@ -25,8 +25,7 @@ class PromptContent(AutoAliasMixin, EvidentlyBaseModel): __alias_type__: ClassVar = "prompt_content" __parse_priority__: ClassVar[float] = 0 - class Config: - is_base_type = True + __is_base_type__: ClassVar[bool] = True @abstractmethod def as_text(self) -> str: diff --git a/src/evidently/llm/rag/index.py b/src/evidently/llm/rag/index.py index 4a2ed7568b..8c62938e53 100644 --- a/src/evidently/llm/rag/index.py +++ b/src/evidently/llm/rag/index.py @@ -10,8 +10,8 @@ from typing import Optional import numpy as np +from pydantic import PrivateAttr -from evidently._pydantic_compat import PrivateAttr from evidently.llm.rag.splitter import AnySplitter from evidently.llm.rag.splitter import Chunk from evidently.llm.rag.splitter import Splitter @@ -30,8 +30,7 @@ class DataCollectionProvider(AutoAliasMixin, EvidentlyBaseModel, ABC): __alias_type__: ClassVar = "data_collection_provider" - class Config: - is_base_type = True + __is_base_type__: ClassVar[bool] = True chunk_size: int = DEFAULT_CHUNK_SIZE chunk_overlap: int = DEFAULT_CHUNK_OVERLAP @@ -75,8 +74,7 @@ def _get_data_collection(self): class FileDataCollectionProvider(DataCollectionProvider): - class Config: - type_alias = "evidently:data_collection_provider:FileDataCollectionProvider" + __type_alias__: ClassVar[Optional[str]] = "evidently:data_collection_provider:FileDataCollectionProvider" path: str recursive: bool = False diff --git a/src/evidently/llm/rag/splitter.py b/src/evidently/llm/rag/splitter.py index 974579d766..2fd4767ff0 100644 --- a/src/evidently/llm/rag/splitter.py +++ b/src/evidently/llm/rag/splitter.py @@ -8,7 +8,8 @@ from typing import Optional from typing import Union -from evidently._pydantic_compat import PrivateAttr +from pydantic import PrivateAttr + from evidently.pydantic_utils import AutoAliasMixin from evidently.pydantic_utils import EvidentlyBaseModel @@ -53,8 +54,7 @@ class Splitters(str, Enum): class Splitter(AutoAliasMixin, EvidentlyBaseModel, ABC): __alias_type__: ClassVar[str] = "splitter" - class Config: - is_base_type = True + __is_base_type__: ClassVar[bool] = True chunk_size: int chunk_overlap: int diff --git a/src/evidently/llm/templates.py b/src/evidently/llm/templates.py index d09d070c97..7aa3c1d5a0 100644 --- a/src/evidently/llm/templates.py +++ b/src/evidently/llm/templates.py @@ -1,5 +1,6 @@ from abc import abstractmethod from enum import Enum +from typing import ClassVar from typing import Dict from typing import Iterator from typing import List @@ -10,8 +11,8 @@ from typing import Union import pandas as pd +from pydantic import Field -from evidently._pydantic_compat import Field from evidently.core.datasets import ColumnType from evidently.llm.models import LLMMessage from evidently.llm.utils.blocks import PromptBlock @@ -28,8 +29,7 @@ class BaseLLMPromptTemplate(BlockPromptTemplate): or generation tasks. """ - class Config: - is_base_type = True + __is_base_type__: ClassVar[bool] = True def iterate_messages(self, data: pd.DataFrame, input_columns: Dict[str, str]) -> Iterator[LLMRequest[dict]]: """Generate LLM requests for each row in the data. diff --git a/src/evidently/llm/utils/blocks.py b/src/evidently/llm/utils/blocks.py index b394731cd0..46e56346eb 100644 --- a/src/evidently/llm/utils/blocks.py +++ b/src/evidently/llm/utils/blocks.py @@ -12,6 +12,8 @@ from typing import TypeVar from typing import Union +from pydantic import ConfigDict + from evidently.llm.utils.errors import LLMResponseParseError from evidently.llm.utils.parsing import get_tags from evidently.pydantic_utils import AutoAliasMixin @@ -21,9 +23,8 @@ class PromptBlock(AutoAliasMixin, EvidentlyBaseModel): __alias_type__: ClassVar[str] = "prompt_block" - class Config: - is_base_type = True - extra = "forbid" + __is_base_type__: ClassVar[bool] = True + model_config = ConfigDict(extra="forbid") def _get_class_doc(self): doc = inspect.getdoc(self.__class__) diff --git a/src/evidently/llm/utils/templates.py b/src/evidently/llm/utils/templates.py index 0277b8ace6..d2937ce22a 100644 --- a/src/evidently/llm/utils/templates.py +++ b/src/evidently/llm/utils/templates.py @@ -19,9 +19,9 @@ from typing import get_args import typing_inspect +from pydantic import PrivateAttr from typing_inspect import is_classvar -from evidently._pydantic_compat import PrivateAttr from evidently.llm.models import LLMMessage from evidently.llm.utils.blocks import OutputFormatBlock from evidently.llm.utils.blocks import PromptBlock @@ -75,8 +75,7 @@ class PromptTemplate(AutoAliasMixin, EvidentlyBaseModel): __alias_type__: ClassVar = "prompt_template" _prepared_template: PreparedTemplate = PrivateAttr() - class Config: - is_base_type = True + __is_base_type__: ClassVar[bool] = True def render(self, values: dict) -> str: return self.prepared_template.render(values) diff --git a/src/evidently/llm/utils/wrapper.py b/src/evidently/llm/utils/wrapper.py index 05cbd2a72e..9393b51629 100644 --- a/src/evidently/llm/utils/wrapper.py +++ b/src/evidently/llm/utils/wrapper.py @@ -21,8 +21,9 @@ from typing import Type from typing import TypeVar -from evidently._pydantic_compat import BaseModel -from evidently._pydantic_compat import SecretStr +from pydantic import BaseModel +from pydantic import SecretStr + from evidently.legacy.options.base import Options from evidently.legacy.options.option import Option from evidently.legacy.utils.sync import sync_api @@ -450,8 +451,7 @@ class LLMOptions(Option): __provider_name__: ClassVar[str] - class Config: - extra = "forbid" + model_config = {"extra": "forbid"} api_key: Optional[SecretStr] = None """Optional API key for the provider.""" diff --git a/src/evidently/metrics/column_statistics.py b/src/evidently/metrics/column_statistics.py index de609b9bef..00f1fb2b97 100644 --- a/src/evidently/metrics/column_statistics.py +++ b/src/evidently/metrics/column_statistics.py @@ -275,9 +275,6 @@ class CategoryCount(ColumnMetric, CountMetric): ``` """ - class Config: - smart_union = True - category: Optional[Label] = None """Single category value to count.""" categories: List[Label] = [] diff --git a/src/evidently/presets/classification.py b/src/evidently/presets/classification.py index f1e3ff2c2e..2551cf667d 100644 --- a/src/evidently/presets/classification.py +++ b/src/evidently/presets/classification.py @@ -3,7 +3,8 @@ from typing import Sequence from typing import Tuple -from evidently._pydantic_compat import PrivateAttr +from pydantic import PrivateAttr + from evidently.core.container import MetricContainer from evidently.core.container import MetricOrContainer from evidently.core.datasets import BinaryClassification diff --git a/src/evidently/presets/dataset_stats.py b/src/evidently/presets/dataset_stats.py index 4c12939cea..2bb0e27f28 100644 --- a/src/evidently/presets/dataset_stats.py +++ b/src/evidently/presets/dataset_stats.py @@ -7,8 +7,9 @@ from typing import Sequence from typing import Tuple -from evidently._pydantic_compat import PrivateAttr -from evidently._pydantic_compat import validator +from pydantic import PrivateAttr +from pydantic.v1 import validator + from evidently.core.base_types import Label from evidently.core.container import ColumnMetricContainer from evidently.core.container import MetricContainer diff --git a/src/evidently/presets/regression.py b/src/evidently/presets/regression.py index a29c45b257..88125a40ae 100644 --- a/src/evidently/presets/regression.py +++ b/src/evidently/presets/regression.py @@ -3,7 +3,8 @@ from typing import Sequence from typing import Tuple -from evidently._pydantic_compat import PrivateAttr +from pydantic import PrivateAttr + from evidently.core.container import MetricContainer from evidently.core.container import MetricOrContainer from evidently.core.metric_types import GenericSingleValueMetricTests diff --git a/src/evidently/pydantic_utils.py b/src/evidently/pydantic_utils.py index d6ab50d937..bd25e57211 100644 --- a/src/evidently/pydantic_utils.py +++ b/src/evidently/pydantic_utils.py @@ -9,6 +9,7 @@ from enum import Enum from functools import lru_cache from typing import TYPE_CHECKING +from typing import Annotated from typing import Any from typing import Callable from typing import ClassVar @@ -18,27 +19,55 @@ from typing import List from typing import Literal from typing import Optional +from typing import Self from typing import Set from typing import Tuple from typing import Type from typing import TypeVar from typing import Union from typing import get_args +from typing import get_origin import numpy as np import yaml +from pydantic import BaseModel +from pydantic import BeforeValidator +from pydantic import ConfigDict +from pydantic import Field +from pydantic import GetCoreSchemaHandler +from pydantic import TypeAdapter +from pydantic import model_serializer +from pydantic import model_validator +from pydantic._internal._model_construction import ModelMetaclass +from pydantic._internal._validators import import_string +from pydantic.fields import FieldInfo as PydanticFieldInfo +from pydantic_core import PydanticCustomError +from pydantic_core import core_schema +from pydantic_core.core_schema import SerializationInfo +from pydantic_core.core_schema import SerializerFunctionWrapHandler from typing_inspect import is_union_type -from evidently._pydantic_compat import SHAPE_DICT -from evidently._pydantic_compat import BaseConfig -from evidently._pydantic_compat import BaseModel -from evidently._pydantic_compat import Field -from evidently._pydantic_compat import ModelMetaclass -from evidently._pydantic_compat import import_string -from evidently._pydantic_compat import parse_obj_as - if TYPE_CHECKING: - from evidently._pydantic_compat import DictStrAny + from pydantic import DictStrAny + + +def _is_dict_annotation(annotation: Any) -> bool: + """True if the field annotation is dict-like (Dict, dict, Mapping).""" + origin = get_origin(annotation) + if origin is None: + return annotation is dict + return origin is dict or (hasattr(origin, "__origin__") and origin.__name__ == "Mapping") + + +def _get_dict_value_type(annotation: Any) -> Any: + """Get the value type from a Dict[K, V] annotation, unwrapping Annotated if needed.""" + while get_origin(annotation) is Annotated: + annotation = get_args(annotation)[0] + args = get_args(annotation) + if len(args) >= 2: + return args[1] + return None + md5_kwargs = {"usedforsecurity": False} @@ -46,31 +75,39 @@ T = TypeVar("T") -def pydantic_type_validator(type_: Type[Any], prioritize: bool = False): - def decorator(f): - from evidently._pydantic_compat import _VALIDATORS +def pydantic_type_validator(type_: Type[Any]): + """ + Decorator to register a validator function for a type in Pydantic v2. + + Args: + type_: The type to register the validator for (e.g., pd.Series) + + Example: + @pydantic_type_validator(pd.Series) + def validate_series(v: Any) -> pd.Series: + if isinstance(v, pd.Series): + return v + if isinstance(v, list): + return pd.Series(v) + raise ValueError(f"Cannot convert {type(v)} to pd.Series") + """ - for cls, validators in _VALIDATORS: - if cls is type_: - if prioritize: - validators.insert(0, f) - else: - validators.append(f) - return - if prioritize: - _VALIDATORS.insert(0, (type_, [f])) - else: - _VALIDATORS.append( - (type_, [f]), - ) + def decorator(validator_func: Callable[[Any], Any]): + def _get_pydantic_core_schema(cls, source_type: Any, handler: GetCoreSchemaHandler) -> core_schema.CoreSchema: + return core_schema.no_info_plain_validator_function(validator_func) + + # Monkey-patch the type + type_.__get_pydantic_core_schema__ = classmethod(_get_pydantic_core_schema) + + return validator_func return decorator class FrozenBaseMeta(ModelMetaclass): def __new__(mcs, name, bases, namespace, **kwargs): - res = super().__new__(mcs, name, bases, namespace, **kwargs) - res.__config__.frozen = True + res: Type[BaseModel] = super().__new__(mcs, name, bases, namespace, **kwargs) + res.model_config["frozen"] = True return res @@ -79,8 +116,7 @@ def __new__(mcs, name, bases, namespace, **kwargs): class FrozenBaseModel(BaseModel, metaclass=FrozenBaseMeta): - class Config: - underscore_attrs_are_private = True + model_config = ConfigDict() _init_values: Optional[Dict] @@ -99,7 +135,7 @@ def __init_values__(self): def __setattr__(self, key, value): if self.__init_values__ is not None: - if key not in self.__fields__ and key not in self.__private_attributes__: + if key not in self.model_fields and key not in self.__private_attributes__: raise AttributeError(f"{self.__class__.__name__} has no attribute {key}") self.__init_values__[key] = value return @@ -144,7 +180,7 @@ def register_type_alias(base_class: Type["PolymorphicModel"], classpath: str, al if base_class is PolymorphicModel: break base_class = get_base_class(base_class, ensure_parent=True) # type: ignore[arg-type] - if not base_class.__config__.transitive_aliases: + if not getattr(base_class, "__transitive_aliases__", False): break @@ -174,8 +210,7 @@ def get_base_class(cls: Type["PolymorphicModel"], ensure_parent: bool = False) - continue if not issubclass(cls_, PolymorphicModel): continue - config = cls_.__dict__.get("Config") - if config is not None and config.__dict__.get("is_base_type", False): + if cls_.__dict__.get("__is_base_type__", False): return cls_ return PolymorphicModel @@ -195,25 +230,21 @@ def is_not_abstract(cls): class PolymorphicModel(BaseModel): - class Config(BaseConfig): - # value to put into "type" field - type_alias: ClassVar[Optional[str]] = None - # flag to mark alias required. If not required, classpath is used by default - alias_required: ClassVar[bool] = True - # flag to register aliaes for grand-parent base type - # eg PolymorphicModel -> A -> B -> C, where A and B are base types. only if A has this flag, C can be parsed as both A and B. - transitive_aliases: ClassVar[bool] = False - # flag to mark type as base. This means it will be possible to parse all subclasses of it as this type - is_base_type: ClassVar[bool] = False - - __config__: ClassVar[Type[Config]] = Config + model_config = ConfigDict(arbitrary_types_allowed=True) + + # Configuration values + __type_alias__: ClassVar[Optional[str]] = None + __alias_required__: ClassVar[bool] = True + # __transitive_aliases__: ClassVar[bool] = False + __is_base_type__: ClassVar[bool] = False + + type: str = Field(default="") @classmethod def __get_type__(cls) -> str: - config = cls.__dict__.get("Config") - if config is not None and config.__dict__.get("type_alias") is not None: - return config.type_alias - if cls.__config__.alias_required and is_not_abstract(cls): + if cls.__dict__.get("__type_alias__") is not None: + return cls.__type_alias__ + if cls.__alias_required__ and is_not_abstract(cls): raise ValueError(f"Alias is required for {cls.__name__}") return cls.__get_classpath__() @@ -221,58 +252,105 @@ def __get_type__(cls) -> str: def __get_classpath__(cls): return get_classpath(cls) - type: str = Field("") + @classmethod + def __subtypes__(cls) -> Tuple[Type["PolymorphicModel"], ...]: + return tuple(all_subclasses(cls)) + + @classmethod + def __get_is_base_type__(cls) -> bool: + return cls.__dict__.get("__is_base_type__", False) def __init_subclass__(cls): super().__init_subclass__() - if cls == PolymorphicModel: + if cls == PolymorphicModel or cls.__get_is_base_type__(): return typename = cls.__get_type__() literal_typename = Literal[typename] - type_field = cls.__fields__["type"] - type_field.default = typename - type_field.field_info.default = typename - type_field.type_ = type_field.outer_type_ = literal_typename + cls.__annotations__["type"] = literal_typename + if "type" not in cls.__dict__: + cls.type = typename base_class = get_base_class(cls) if (base_class, typename) not in LOADED_TYPE_ALIASES: register_loaded_alias(base_class, cls, typename) if base_class != cls: - base_typefield = base_class.__fields__["type"] - base_typefield_type = base_typefield.type_ - if is_union_type(base_typefield_type): - subclass_literals = get_args(base_typefield_type) + (literal_typename,) - else: - subclass_literals = (base_typefield_type, literal_typename) - base_typefield.type_ = base_typefield.outer_type_ = Union[subclass_literals] - - @classmethod - def __subtypes__(cls: Type[TPM]) -> Tuple[Type["TPM"], ...]: - return tuple(all_subclasses(cls)) + base_typefield = base_class.model_fields.get("type") + if base_typefield is not None: + base_typefield_type = base_typefield.annotation + if is_union_type(base_typefield_type): + subclass_literals = get_args(base_typefield_type) + (literal_typename,) + else: + subclass_literals = (base_typefield_type, literal_typename) + base_class.__annotations__["type"] = Union[subclass_literals] - @classmethod - def __is_base_type__(cls) -> bool: - config = cls.__dict__.get("Config") - if config is not None and config.__dict__.get("is_base_type") is not None: - return config.is_base_type - return False + def __str__(self): + return f"{self.__class__.__name__}[{self.__get_type__()}]({super().__str__()})" @classmethod - def validate(cls: Type[TPM], value: Any) -> TPM: + def model_validate( + cls, + value: Any, + *, + strict: bool | None = None, + from_attributes: bool | None = None, + context: Any | None = None, + ) -> Self: if isinstance(value, dict) and "type" in value: typename = value.pop("type") try: subcls = cls.load_alias(typename) - return subcls.validate(value) # type: ignore[return-value] + return subcls.model_validate( + value, strict=strict, from_attributes=from_attributes, context=context + ) # Pydantic v2 uses model_validate finally: value["type"] = typename - return super().validate(value) # type: ignore[misc] + return super().model_validate(value, strict=strict, from_attributes=from_attributes, context=context) + + @model_validator(mode="wrap") + def _delegate_validation(cls, data: dict, handler) -> Any: + # if isinstance(data, cls): + # return data + # if not isinstance(data, dict): + # raise ValueError(f"{data!r} is not a dict") + if "type" in data and data["type"] != cls.__get_type__(): + return cls.model_validate(data) + typename = data.pop("type", None) if isinstance(data, dict) else None + try: + return handler(data) + finally: + if typename is not None: + data["type"] = typename + + # if "type" in data and data["type"] != cls.__get_type__(): + # return cls.model_validate(data) + # try: + # return handler(data) + # except TypeError as e: + # raise + + @model_serializer(mode="wrap") + def _delegate_serialization(self, nxt: SerializerFunctionWrapHandler, info: SerializationInfo): + if f"serializer={self.__class__.__name__}" not in str(nxt): + return self.model_dump( + mode=info.mode, + include=info.include, + exclude=info.exclude, + context=info.context, + by_alias=info.by_alias, + exclude_unset=info.exclude_unset, + exclude_none=info.exclude_none, + exclude_defaults=info.exclude_defaults, + round_trip=info.round_trip, + serialize_as_any=info.serialize_as_any, + ) + return nxt(self) @classmethod - def load_alias(cls, typename): - key = (get_base_class(cls), typename) # type: ignore[arg-type] + def load_alias(cls, typename: str): + key = (get_base_class(cls), typename) + if key in LOADED_TYPE_ALIASES: subcls = LOADED_TYPE_ALIASES[key] else: @@ -280,14 +358,14 @@ def load_alias(cls, typename): classpath = TYPE_ALIASES[key] else: if "." not in typename: - raise ValueError(f'Unknown alias "{typename}"') + raise PydanticCustomError("unknown_alias", f'Unknown alias "{typename}"') classpath = typename if not any(classpath.startswith(p) for p in ALLOWED_TYPE_PREFIXES): - raise ValueError(f"{classpath} does not match any allowed prefixes") + raise PydanticCustomError("invalid_prefix", f"{classpath} does not match any allowed prefixes") try: subcls = import_string(classpath) except ImportError as e: - raise ValueError(f"Error importing subclass from '{classpath}' {e.args[0]}") from e + raise PydanticCustomError("import_error", f"Error importing subclass from '{classpath}'") from e return subcls @@ -297,7 +375,7 @@ def get_value_fingerprint(value: Any) -> FingerprintPart: if isinstance(value, np.int64): return int(value) if isinstance(value, BaseModel): - return get_value_fingerprint(value.dict()) + return get_value_fingerprint(value.model_dump()) if dataclasses.is_dataclass(value): return get_value_fingerprint(dataclasses.asdict(value)) if isinstance(value, Enum): @@ -329,10 +407,9 @@ def _is_yaml_fmt(path: str, fmt: Literal["yaml", "json", None]) -> bool: class EvidentlyBaseModel(FrozenBaseModel, PolymorphicModel): - class Config: - type_alias = "evidently:base:EvidentlyBaseModel" - alias_required = True - is_base_type = True + __type_alias__: ClassVar[Optional[str]] = "evidently:base:EvidentlyBaseModel" + __alias_required__: ClassVar[bool] = True + __is_base_type__: ClassVar[bool] = True def get_fingerprint(self) -> Fingerprint: classpath = self.__get_classpath__() @@ -343,8 +420,8 @@ def get_fingerprint(self) -> Fingerprint: def get_fingerprint_parts(self) -> Tuple[FingerprintPart, ...]: return tuple( (name, self.get_field_fingerprint(name)) - for name, field in sorted(self.__fields__.items()) - if field.required or getattr(self, name) != field.get_default() + for name, field in sorted(self.model_fields.items()) + if field.is_required() or getattr(self, name) != field.default ) def get_field_fingerprint(self, field: str) -> FingerprintPart: @@ -352,7 +429,7 @@ def get_field_fingerprint(self, field: str) -> FingerprintPart: return get_value_fingerprint(value) def update(self: EBM, **kwargs) -> EBM: - data = self.dict() + data = self.model_dump() data.update(kwargs) return self.__class__(**data) @@ -363,20 +440,19 @@ def load(cls: Type[EBM], path: str, fmt: Literal["json", "yaml", None] = None) - data = yaml.safe_load(f) else: data = json.load(f) - return parse_obj_as(cls, data) + return TypeAdapter(cls).validate_python(data) def dump(self, path: str, fmt: Literal["json", "yaml", None] = None): with open(path, "w") as f: if _is_yaml_fmt(path, fmt): - yaml.safe_dump(json.loads(self.json()), f) + yaml.safe_dump(json.loads(self.model_dump_json()), f) else: - f.write(self.json(indent=2, ensure_ascii=False)) + f.write(self.model_dump_json(indent=2)) @autoregister class WithTestAndMetricDependencies(EvidentlyBaseModel): - class Config: - type_alias = "evidently:test:WithTestAndMetricDependencies" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:WithTestAndMetricDependencies" def __evidently_dependencies__(self): from evidently.legacy.base_metric import Metric @@ -391,8 +467,9 @@ def __evidently_dependencies__(self): class EnumValueMixin(BaseModel): def _to_enum_value(self, key, value): - field = self.__fields__[key] - if isinstance(field.type_, type) and not issubclass(field.type_, Enum): + field = self.model_fields[key] + ann = field.annotation + if isinstance(ann, type) and not issubclass(ann, Enum): return value if isinstance(value, list): @@ -405,15 +482,16 @@ def _to_enum_value(self, key, value): return {v.value if isinstance(v, Enum) else v for v in value} return value.value if isinstance(value, Enum) else value - def dict(self, *args, **kwargs) -> "DictStrAny": - res = super().dict(*args, **kwargs) + def model_dump(self, *args, **kwargs) -> "DictStrAny": + res = super().model_dump(*args, **kwargs) return {k: self._to_enum_value(k, v) for k, v in res.items()} class ExcludeNoneMixin(BaseModel): - def dict(self, *args, **kwargs) -> "DictStrAny": - kwargs["exclude_none"] = True - return super().dict(*args, **kwargs) + @model_serializer(mode="wrap") + def exclude_none(self, nxt: SerializerFunctionWrapHandler): + res = nxt(self) + return {k: v for k, v in res.items() if v is not None} class FieldTags(Enum): @@ -429,8 +507,7 @@ class FieldTags(Enum): class FieldInfo(EnumValueMixin): - class Config: - frozen = True + model_config = ConfigDict(frozen=True) path: str tags: FrozenSet[FieldTags] @@ -467,7 +544,7 @@ def list_fields(self) -> List[str]: if self.has_instance and self._is_mapping and isinstance(self._instance, dict): return list(self._instance.keys()) if isinstance(self._cls, type) and issubclass(self._cls, BaseModel): - return list(self._cls.__fields__) + return list(self._cls.model_fields) return [] def __getattr__(self, item) -> "FieldPath": @@ -477,14 +554,18 @@ def child(self, item: str) -> "FieldPath": if self._is_mapping: if self.has_instance and isinstance(self._instance, dict): return FieldPath(self._path + [item], self._instance[item]) + if not self.has_instance and _is_dict_annotation(self._cls): + value_type = _get_dict_value_type(self._cls) + if value_type is not None: + return FieldPath(self._path + [item], value_type) return FieldPath(self._path + [item], self._cls) if not issubclass(self._cls, BaseModel): raise AttributeError(f"{self._cls} does not have fields") - if item not in self._cls.__fields__: + if item not in self._cls.model_fields: raise AttributeError(f"{self._cls} type does not have '{item}' field") - field = self._cls.__fields__[item] - field_value = field.type_ - is_mapping = field.shape == SHAPE_DICT + field = self._cls.model_fields[item] + field_value = get_field_inner_type(field) + is_mapping = _is_dict_annotation(field.annotation) if self.has_instance: field_value = getattr(self._instance, item) if is_mapping: @@ -495,15 +576,15 @@ def list_nested_fields(self, exclude: Set["IncludeTags"] = None) -> List[str]: if not isinstance(self._cls, type) or not issubclass(self._cls, BaseModel): return [repr(self)] res = [] - for name, field in self._cls.__fields__.items(): - field_value = field.type_ + for name, field in self._cls.model_fields.items(): + field_value = field.annotation # todo: do something with recursive imports from evidently.legacy.core import get_field_tags field_tags = get_field_tags(self._cls, name) if field_tags is not None and (exclude is not None and any(t in exclude for t in field_tags)): continue - is_mapping = field.shape == SHAPE_DICT + is_mapping = _is_dict_annotation(field.annotation) if self.has_instance: field_value = getattr(self._instance, name) if is_mapping and isinstance(field_value, dict): @@ -512,7 +593,14 @@ def list_nested_fields(self, exclude: Set["IncludeTags"] = None) -> List[str]: continue else: if is_mapping: - name = f"{name}.*" + value_type = _get_dict_value_type(field.annotation) + if value_type is not None: + res.extend( + FieldPath(self._path + [f"{name}.*"], value_type).list_nested_fields(exclude=exclude) + ) + else: + res.append(_to_path(self._path + [f"{name}.*"])) + continue res.extend(FieldPath(self._path + [name], field_value).list_nested_fields(exclude=exclude)) return res @@ -521,7 +609,7 @@ def _list_with_tags(self, current_tags: Set["IncludeTags"]) -> List[Tuple[List[A return [(self._path, current_tags)] from evidently.legacy.core import BaseResult - if issubclass(self._cls, BaseResult) and self._cls.__config__.extract_as_obj: + if issubclass(self._cls, BaseResult) and self._cls.model_config.get("extract_as_obj", False): return [(self._path, current_tags)] res = [] from evidently.ui.backport import ByLabelCountValueV1 @@ -532,15 +620,15 @@ def _list_with_tags(self, current_tags: Set["IncludeTags"]) -> List[Tuple[List[A if issubclass(self._cls, ByLabelCountValueV1): res.append((self._path + ["counts"], current_tags.union({IncludeTags.Render}))) res.append((self._path + ["shares"], current_tags.union({IncludeTags.Render}))) - for name, field in self._cls.__fields__.items(): - field_value = field.type_ + for name, field in self._cls.model_fields.items(): + field_value = field.annotation # todo: do something with recursive imports from evidently.legacy.core import get_field_tags field_tags = get_field_tags(self._cls, name) - is_mapping = field.shape == SHAPE_DICT + is_mapping = _is_dict_annotation(field.annotation) if self.has_instance: field_value = getattr(self._instance, name) if is_mapping and isinstance(field_value, dict): @@ -569,7 +657,7 @@ def _get_field_type(self, path: List[str]) -> Type: raise ValueError("Empty path provided") if len(path) == 1: if isinstance(self._cls, type) and issubclass(self._cls, BaseModel): - return self._cls.__fields__[path[0]].outer_type_ + return self._cls.model_fields[path[0]].annotation if self.has_instance: # fixme: tmp fix # in case of field like f: Dict[str, A] we wont know that value was type annotated with A when we get to it @@ -596,7 +684,7 @@ def get_field_tags(self, path: List[str]) -> Optional[Set["IncludeTags"]]: if not isinstance(self._cls, type) or not issubclass(self._cls, BaseResult): return None - self_tags = self._cls.__config__.tags + self_tags = self._cls.__tags__ or set() if len(path) == 0: return self_tags field_name, *path = path @@ -608,7 +696,7 @@ def get_field_tags(self, path: List[str]) -> Optional[Set["IncludeTags"]]: @pydantic_type_validator(FieldPath) -def series_validator(value): +def field_path_validator(value): return value.get_path() @@ -616,16 +704,55 @@ def get_object_hash_deprecated(obj: Union[BaseModel, dict]): from evidently.legacy.utils import NumpyEncoder if isinstance(obj, BaseModel): - obj = obj.dict() + obj = obj.model_dump() return hashlib.md5(json.dumps(obj, cls=NumpyEncoder).encode("utf8"), **md5_kwargs).hexdigest() # nosec: B324 class AutoAliasMixin: __alias_type__: ClassVar[str] + __type_alias__: ClassVar[Optional[str]] = None @classmethod def __get_type__(cls) -> str: - config = cls.__dict__.get("Config") - if config is not None and config.__dict__.get("type_alias") is not None: - return config.type_alias + if cls.__dict__.get("__type_alias__") is not None: + return cls.__type_alias__ + if not hasattr(cls, "__alias_type__"): + raise TypeError(f"{cls.__name__} `__alias_type__` is not defined") return f"evidently:{cls.__alias_type__}:{cls.__name__}" + + +def get_field_inner_type(field: PydanticFieldInfo) -> Type: + """Return the inner type of a Pydantic field, unwrapping containers like Optional, List, etc.""" + typ = field.annotation + while True: + origin = get_origin(typ) + args = get_args(typ) + # Unwrap Annotated + if origin is Annotated: + typ = args[0] + continue + # Unwrap Optional/Union[T, None] + if origin is Union: + non_none = [t for t in args if t is not type(None)] + if len(non_none) == 1: + typ = non_none[0] + continue + # Unwrap list, set, tuple, etc. + if origin in (list, set, tuple): + if args: + typ = args[0] + continue + if origin is dict: + typ = args[1] + break + break + return typ + + +def force_keys_to_str(value: Optional[dict[Any, Any]]) -> Optional[dict[str, Any]]: + if value is None: + return None + return {str(k): v for k, v in value.items()} + + +StrKeyValidator = BeforeValidator(force_keys_to_str) diff --git a/src/evidently/sdk/artifacts.py b/src/evidently/sdk/artifacts.py index 28a477df41..bc4f9a11e0 100644 --- a/src/evidently/sdk/artifacts.py +++ b/src/evidently/sdk/artifacts.py @@ -16,10 +16,11 @@ from typing import TypeVar from typing import Union -from evidently._pydantic_compat import BaseModel -from evidently._pydantic_compat import Field -from evidently._pydantic_compat import PrivateAttr -from evidently._pydantic_compat import parse_obj_as +from pydantic import BaseModel +from pydantic import Field +from pydantic import PrivateAttr +from pydantic import TypeAdapter + from evidently.errors import EvidentlyError from evidently.pydantic_utils import AutoAliasMixin from evidently.pydantic_utils import EvidentlyBaseModel @@ -68,9 +69,7 @@ class ArtifactContent(AutoAliasMixin, EvidentlyBaseModel, Generic[TArtifactValue __alias_type__: ClassVar = "artifact_content" __value_class__: ClassVar[Type[TArtifactValue]] __value_type__: ClassVar[ArtifactContentType] - - class Config: - is_base_type = True + __is_base_type__: ClassVar[bool] = True data: Any """Raw data stored in the artifact.""" @@ -81,7 +80,7 @@ def get_value(self) -> TArtifactValue: Returns: * Typed value parsed from the stored data. """ - return parse_obj_as(self.__value_class__, self.data) + return TypeAdapter(self.__value_class__).validate_python(self.data) def get_type(self) -> ArtifactContentType: """Get the content type. @@ -105,7 +104,8 @@ def from_value(cls, value: TArtifactValue) -> "ArtifactContent": raise NotImplementedError() def __init_subclass__(cls): - _CONTENT_TYPE_MAPPING[cls.__value_class__] = cls + if "__value_class__" in cls.__dict__: + _CONTENT_TYPE_MAPPING[cls.__value_class__] = cls super().__init_subclass__() @@ -201,7 +201,7 @@ def __init__( ): if not isinstance(content, ArtifactContent): try: - content = parse_obj_as(ArtifactContent, content) # type: ignore[type-abstract] + content = TypeAdapter(ArtifactContent).validate_python(content) # type: ignore[type-abstract] except ValueError: content = _parse_any_to_content(content) diff --git a/src/evidently/sdk/configs.py b/src/evidently/sdk/configs.py index 95ee2c521f..672bfb33af 100644 --- a/src/evidently/sdk/configs.py +++ b/src/evidently/sdk/configs.py @@ -15,8 +15,9 @@ from typing import Type from typing import TypeVar -from evidently._pydantic_compat import Field -from evidently._pydantic_compat import PrivateAttr +from pydantic import Field +from pydantic import PrivateAttr + from evidently.core.datasets import Descriptor from evidently.errors import EvidentlyError from evidently.sdk.artifacts import Artifact as GenericConfig diff --git a/src/evidently/sdk/datasets.py b/src/evidently/sdk/datasets.py index 999436e961..bb1a5f56ad 100644 --- a/src/evidently/sdk/datasets.py +++ b/src/evidently/sdk/datasets.py @@ -7,10 +7,10 @@ from typing import Optional import pandas as pd +from pydantic import BaseModel +from pydantic import TypeAdapter from requests import Response -from evidently._pydantic_compat import BaseModel -from evidently._pydantic_compat import parse_obj_as from evidently.core.datasets import DataDefinition from evidently.core.datasets import Dataset from evidently.legacy.suite.base_suite import MetadataValueType @@ -132,7 +132,7 @@ def load(self, dataset_id: DatasetID) -> Dataset: metadata, file_content = read_multipart_response(response) df = pd.read_parquet(io.BytesIO(file_content)) - data_def = parse_obj_as(DataDefinition, metadata["data_definition"]) + data_def = TypeAdapter(DataDefinition).validate_python(metadata["data_definition"]) return Dataset.from_pandas(df, data_definition=data_def) def add( diff --git a/src/evidently/sdk/models.py b/src/evidently/sdk/models.py index f00d2998d6..27c66cbb23 100644 --- a/src/evidently/sdk/models.py +++ b/src/evidently/sdk/models.py @@ -5,10 +5,10 @@ from typing import Optional import uuid6 +from pydantic import BaseModel +from pydantic import Field +from pydantic.v1 import validator -from evidently._pydantic_compat import BaseModel -from evidently._pydantic_compat import Field -from evidently._pydantic_compat import validator from evidently.legacy.core import new_id from evidently.legacy.suite.base_suite import SnapshotLinks from evidently.legacy.ui.type_aliases import OrgID diff --git a/src/evidently/sdk/prompts.py b/src/evidently/sdk/prompts.py index 7a8c245d60..b3a67c9f11 100644 --- a/src/evidently/sdk/prompts.py +++ b/src/evidently/sdk/prompts.py @@ -10,11 +10,12 @@ from typing import Optional from typing import Union -from evidently._pydantic_compat import BaseModel -from evidently._pydantic_compat import Field -from evidently._pydantic_compat import PrivateAttr -from evidently._pydantic_compat import ValidationError -from evidently._pydantic_compat import parse_obj_as +from pydantic import BaseModel +from pydantic import Field +from pydantic import PrivateAttr +from pydantic import TypeAdapter +from pydantic import ValidationError + from evidently.errors import EvidentlyError from evidently.llm.prompts.content import PromptContent from evidently.llm.prompts.content import PromptContentType @@ -111,7 +112,7 @@ def __init__( ): if not isinstance(content, PromptContent): try: - content = parse_obj_as(PromptContent, content) # type: ignore[type-abstract] + content = TypeAdapter(PromptContent).validate_python(content) # type: ignore[type-abstract] except ValidationError: content = PromptContent.parse(content) if content_type is None: diff --git a/src/evidently/tests/aliases.py b/src/evidently/tests/aliases.py index fcc16dbed1..3f3e5a6152 100644 --- a/src/evidently/tests/aliases.py +++ b/src/evidently/tests/aliases.py @@ -30,7 +30,7 @@ AnyTest = Union[GenericTest, MetricTest, DescriptorTest] -GenericTest.update_forward_refs(MetricTest=MetricTest, DescriptorTest=DescriptorTest) +GenericTest.model_rebuild() @overload diff --git a/src/evidently/tests/categorical_tests.py b/src/evidently/tests/categorical_tests.py index 88367ebe2a..02584136f0 100644 --- a/src/evidently/tests/categorical_tests.py +++ b/src/evidently/tests/categorical_tests.py @@ -14,9 +14,6 @@ class IsInMetricTest(MetricTest): - class Config: - smart_union = True - values: List[InValueType] def to_test(self) -> SingleValueTest: @@ -35,9 +32,6 @@ def func(context: Context, metric: MetricCalculationBase, value: SingleValue) -> class NotInMetricTest(MetricTest): - class Config: - smart_union = True - values: List[InValueType] def to_test(self) -> SingleValueTest: diff --git a/src/evidently/ui/backport.py b/src/evidently/ui/backport.py index e19e7da269..42016be306 100644 --- a/src/evidently/ui/backport.py +++ b/src/evidently/ui/backport.py @@ -61,52 +61,45 @@ class MetricResultV2Adapter(MetricResultV1): - class Config: - type_alias = "evidently:metric_result:MetricResultV2Adapter" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:MetricResultV2Adapter" widget: List[dict] class PresetMetricValueV1(MetricResultV2Adapter): - class Config: - type_alias = "evidently:metric_result:PresetMetricValueV1" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:PresetMetricValueV1" class SingleValueV1(MetricResultV2Adapter): - class Config: - type_alias = "evidently:metric_result:SingleValueV1" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:SingleValueV1" value: Union[float, int, str] class ByLabelValueV1(MetricResultV2Adapter): - class Config: - type_alias = "evidently:metric_result:ByLabelValueV1" - field_tags = {"values": {IncludeTags.Render}} + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:ByLabelValueV1" + __field_tags__: ClassVar[Dict[str, set]] = {"values": {IncludeTags.Render}} values: Dict[Label, Union[float, int, bool, str]] class ByLabelCountValueV1(MetricResultV2Adapter): - class Config: - type_alias = "evidently:metric_result:ByLabelCountValueV1" - field_tags = {"values": {IncludeTags.Render}} + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:ByLabelCountValueV1" + __field_tags__: ClassVar[Dict[str, set]] = {"values": {IncludeTags.Render}} counts: Dict[Label, int] shares: Dict[Label, float] class CountValueV1(MetricResultV2Adapter): - class Config: - type_alias = "evidently:metric_result:CountValueV1" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:CountValueV1" count: int share: float class MeanStdValueV1(MetricResultV2Adapter): - class Config: - type_alias = "evidently:metric_result:MeanStdValueV1" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:MeanStdValueV1" mean: float std: float @@ -152,8 +145,7 @@ def metric_result_v2_to_v1(metric_result: MetricResultV2, ignore_widget: bool = class MetricV2Adapter(MetricV1[MetricResultV2Adapter]): - class Config: - type_alias = "evidently:metric:MetricV2Adapter" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:MetricV2Adapter" metric: Union[MetricV2, dict] fingerprint: Fingerprint = "" @@ -169,8 +161,7 @@ def get_fingerprint(self) -> Fingerprint: class MetricV2PresetAdapter(MetricV1[MetricResultV2Adapter]): - class Config: - type_alias = "evidently:metric:MetricV2PresetAdapter" + __type_alias__: ClassVar[Optional[str]] = "evidently:metric:MetricV2PresetAdapter" id: str @@ -341,13 +332,11 @@ def snapshot_v2_to_v1(snapshot: SnapshotV2) -> SnapshotV1: class DashboardPanelV2(DashboardPanel): - class Config: - type_alias = "evidently:dashboard_panel:DashboardPanelV2" + __type_alias__: ClassVar[Optional[str]] = "evidently:dashboard_panel:DashboardPanelV2" class SingleValueDashboardPanel(DashboardPanelV2): - class Config: - type_alias = "evidently:dashboard_panel:SingleValueDashboardPanel" + __type_alias__: ClassVar[Optional[str]] = "evidently:dashboard_panel:SingleValueDashboardPanel" title: str = "" filter: ReportFilter = ReportFilter(metadata_values={}, tag_values=[], include_test_suites=True) @@ -366,8 +355,7 @@ async def build( class TestV2Adapter(TestV1): - class Config: - type_alias = "evidently:test:TestV2Adapter" + __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestV2Adapter" name: ClassVar[str] = "TestV2Adapter" group: ClassVar[str] = "TestV2Adapter" @@ -382,8 +370,7 @@ def groups(self) -> Dict[str, str]: class TestV2Parameters(TestParameters): - class Config: - type_alias = "evidently:test_parameters:TestV2Parameters" + __type_alias__: ClassVar[Optional[str]] = "evidently:test_parameters:TestV2Parameters" class TestsConfig(MetricV2): diff --git a/src/evidently/ui/runner/core.py b/src/evidently/ui/runner/core.py index c0a50506a5..f4fad394b4 100644 --- a/src/evidently/ui/runner/core.py +++ b/src/evidently/ui/runner/core.py @@ -8,8 +8,9 @@ from typing import Optional from typing import Union -from evidently._pydantic_compat import BaseModel -from evidently._pydantic_compat import PrivateAttr +from pydantic import BaseModel +from pydantic import PrivateAttr + from evidently.ui.runner.utils import get_url_to_service_by_port from evidently.ui.runner.utils import is_service_running from evidently.ui.runner.utils import terminate_process @@ -118,10 +119,7 @@ class Success(RunServiceInfo): process: subprocess.Popen """Subprocess handle for the running service.""" - class Config: - """@private""" - - arbitrary_types_allowed = True + model_config = {"arbitrary_types_allowed": True} def __str__(self) -> str: return f"Running on {get_url_to_service_by_port(port=self.port)}" diff --git a/src/evidently/ui/service/api/datasets.py b/src/evidently/ui/service/api/datasets.py index 782f0a2b99..36b2d61e95 100644 --- a/src/evidently/ui/service/api/datasets.py +++ b/src/evidently/ui/service/api/datasets.py @@ -21,12 +21,12 @@ from litestar.params import Dependency from litestar.params import Parameter from litestar.response.base import ASGIResponse +from pydantic import BaseModel +from pydantic import ConfigDict +from pydantic import TypeAdapter +from pydantic import ValidationError from typing_extensions import Annotated -from evidently._pydantic_compat import BaseModel -from evidently._pydantic_compat import Extra -from evidently._pydantic_compat import ValidationError -from evidently._pydantic_compat import parse_obj_as from evidently.core.datasets import DataDefinition from evidently.core.datasets import Dataset from evidently.legacy.suite.base_suite import MetadataValueType @@ -49,9 +49,7 @@ class UploadDatasetRequest(BaseModel): """Request for uploading a dataset.""" - class Config: - extra = Extra.forbid - arbitrary_types_allowed = True + model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True) name: str file: UploadFile @@ -65,20 +63,20 @@ def metadata(self) -> Dict[str, MetadataValueType]: """Parse metadata from string.""" if not self.metadata_str: return {} - return parse_obj_as(Dict[str, MetadataValueType], json.loads(self.metadata_str)) + return TypeAdapter(Dict[str, MetadataValueType]).validate_python(json.loads(self.metadata_str)) @property def tags(self) -> List[str]: """Parse tags from string.""" if not self.tags_str: return [] - return parse_obj_as(List[str], json.loads(self.tags_str)) + return TypeAdapter(List[str]).validate_python(json.loads(self.tags_str)) @property def data_definition(self) -> Optional[DataDefinition]: """Parse data definition from string.""" if self.data_definition_str: - return parse_obj_as(DataDefinition, json.loads(self.data_definition_str)) + return TypeAdapter(DataDefinition).validate_python(json.loads(self.data_definition_str)) return None @@ -193,7 +191,9 @@ async def get_dataset( """Get a dataset with pagination.""" try: - filter_queries = [parse_obj_as(FilterBy, json.loads(_filter)) for _filter in filters] if filters else None + filter_queries = ( + [TypeAdapter(FilterBy).validate_python(json.loads(_filter)) for _filter in filters] if filters else None + ) except (ValidationError, JSONDecodeError) as e: raise ValidationException(str(e)) from e @@ -272,7 +272,7 @@ class DatasetMetadataResponse(EvidentlyAPIModel): @classmethod def from_dataset_metadata(cls, dataset: DatasetMetadataFull): """Create from DatasetMetadataFull.""" - return cls(**{k: v for k, v in dataset.__dict__.items() if k in cls.__fields__}) + return cls(**{k: v for k, v in dataset.__dict__.items() if k in cls.model_fields}) class ListDatasetResponse(EvidentlyAPIModel): @@ -344,9 +344,7 @@ class AddTracingDatasetRequest(BaseModel): """Request for creating a tracing dataset.""" name: str - - class Config: - extra = "forbid" + model_config = ConfigDict(extra="forbid") class AddTracingDatasetResponse(EvidentlyAPIModel): diff --git a/src/evidently/ui/service/api/llm_judges.py b/src/evidently/ui/service/api/llm_judges.py index ad0b7e6344..c260abd0f0 100644 --- a/src/evidently/ui/service/api/llm_judges.py +++ b/src/evidently/ui/service/api/llm_judges.py @@ -3,8 +3,8 @@ from litestar import Router from litestar import get from litestar import post +from pydantic import BaseModel -from evidently._pydantic_compat import BaseModel from evidently.legacy.descriptors import BiasLLMEval from evidently.legacy.descriptors import CompletenessLLMEval from evidently.legacy.descriptors import ContextQualityLLMEval diff --git a/src/evidently/ui/service/api/models.py b/src/evidently/ui/service/api/models.py index ace566abba..3f866c1291 100644 --- a/src/evidently/ui/service/api/models.py +++ b/src/evidently/ui/service/api/models.py @@ -3,7 +3,8 @@ from typing import List from typing import Optional -from evidently._pydantic_compat import BaseModel +from pydantic import BaseModel + from evidently.legacy.report import Report from evidently.legacy.suite.base_suite import MetadataValueType from evidently.legacy.suite.base_suite import SnapshotLinks diff --git a/src/evidently/ui/service/api/projects.py b/src/evidently/ui/service/api/projects.py index 3a9b133e41..6b19d31269 100644 --- a/src/evidently/ui/service/api/projects.py +++ b/src/evidently/ui/service/api/projects.py @@ -18,10 +18,10 @@ from litestar.exceptions import HTTPException from litestar.params import Dependency from litestar.params import Parameter +from pydantic import BaseModel +from pydantic import TypeAdapter from typing_extensions import Annotated -from evidently._pydantic_compat import BaseModel -from evidently._pydantic_compat import parse_obj_as from evidently.core.report import Snapshot from evidently.core.serialization import SnapshotModel from evidently.legacy.model.dashboard import DashboardInfo @@ -327,7 +327,7 @@ async def add_snapshot( log_event: Annotated[Callable, Dependency()], user_id: UserID, ) -> AddSnapshotResponse: - model = parse_obj_as(SnapshotModel, json.loads(body)) + model = TypeAdapter(SnapshotModel).validate_python(json.loads(body)) snapshot_id = await project_manager.add_snapshot(user_id, project.id, model) log_event("add_snapshot") return AddSnapshotResponse(snapshot_id=snapshot_id) diff --git a/src/evidently/ui/service/app.py b/src/evidently/ui/service/app.py index e6a959bcaf..2009c71c20 100644 --- a/src/evidently/ui/service/app.py +++ b/src/evidently/ui/service/app.py @@ -2,8 +2,8 @@ from typing import Optional import uvicorn +from pydantic import SecretStr -from evidently._pydantic_compat import SecretStr from evidently.ui.service.components.base import AppBuilder from evidently.ui.service.components.storage import LocalStorageComponent from evidently.ui.service.config import AppConfig diff --git a/src/evidently/ui/service/base.py b/src/evidently/ui/service/base.py index 38324f25c8..40bfbb4b3e 100644 --- a/src/evidently/ui/service/base.py +++ b/src/evidently/ui/service/base.py @@ -16,10 +16,10 @@ from typing import Union import uuid6 +from pydantic import BaseModel +from pydantic import Field +from pydantic import PrivateAttr -from evidently._pydantic_compat import BaseModel -from evidently._pydantic_compat import Field -from evidently._pydantic_compat import PrivateAttr from evidently.core.report import Snapshot as SnapshotV2 from evidently.core.serialization import SnapshotModel from evidently.legacy.core import new_id @@ -130,9 +130,6 @@ def _default_dashboard(): class Project(Entity): entity_type: ClassVar[EntityType] = EntityType.Project - class Config: - underscore_attrs_are_private = True - id: ProjectID = Field(default_factory=new_id) name: str description: Optional[str] = None diff --git a/src/evidently/ui/service/components/base.py b/src/evidently/ui/service/components/base.py index 5973bc7615..f59e32897c 100644 --- a/src/evidently/ui/service/components/base.py +++ b/src/evidently/ui/service/components/base.py @@ -17,8 +17,8 @@ from litestar.types import ControllerRouterHandler from litestar.types import ExceptionHandlersMap from litestar.types import Middleware +from pydantic import ConfigDict -from evidently._pydantic_compat import Extra from evidently.legacy.ui.utils import parse_json from evidently.pydantic_utils import PolymorphicModel @@ -79,10 +79,9 @@ def get_priority(self) -> int: def get_requirements(self) -> List[Type["Component"]]: return self.__require__ - class Config: - extra = Extra.forbid - alias_required = False - is_base_type = True + model_config = ConfigDict(extra="forbid") + __alias_required__: ClassVar[bool] = False + __is_base_type__: ClassVar[bool] = True def __init_subclass__(cls): super().__init_subclass__() diff --git a/src/evidently/ui/service/components/local_storage.py b/src/evidently/ui/service/components/local_storage.py index d4d7910713..ba121ed56a 100644 --- a/src/evidently/ui/service/components/local_storage.py +++ b/src/evidently/ui/service/components/local_storage.py @@ -23,8 +23,7 @@ class FSSpecBlobComponent(BlobStorageComponent): - class Config: - type_alias = "fsspec" + __type_alias__: ClassVar[Optional[str]] = "fsspec" path: str @@ -36,8 +35,7 @@ def blob_storage_factory() -> BlobStorage: class JsonMetadataComponent(MetadataStorageComponent): - class Config: - type_alias = "json_file" + __type_alias__: ClassVar[Optional[str]] = "json_file" path: str @@ -49,8 +47,7 @@ async def json_meta(local_state: Optional[LocalState] = None): class InmemoryDataComponent(DataStorageComponent): - class Config: - type_alias = "inmemory" + __type_alias__: ClassVar[Optional[str]] = "inmemory" path: str @@ -74,8 +71,7 @@ def dependency_factory(self) -> Callable[..., LocalState]: class JsonDatasetMetadataComponent(DatasetMetadataComponent): """JSON file-based dataset metadata storage component.""" - class Config: - type_alias = "json_file" + __type_alias__: ClassVar[Optional[str]] = "json_file" path: str = "workspace" @@ -89,8 +85,7 @@ def dataset_metadata_factory() -> DatasetMetadataStorage: class FSSpecDatasetFileStorageComponent(DatasetFileStorageComponent): """FSSpec-based dataset file storage component.""" - class Config: - type_alias = "fsspec" + __type_alias__: ClassVar[Optional[str]] = "fsspec" path: str = "workspace" @@ -104,8 +99,7 @@ def blob_storage_factory() -> BlobStorage: class FileSnapshotDatasetLinksComponent(SnapshotDatasetLinksComponent): """File-based snapshot dataset links component.""" - class Config: - type_alias = "file" + __type_alias__: ClassVar[Optional[str]] = "file" path: str = "workspace" diff --git a/src/evidently/ui/service/components/security.py b/src/evidently/ui/service/components/security.py index 374cf0abb5..09431699af 100644 --- a/src/evidently/ui/service/components/security.py +++ b/src/evidently/ui/service/components/security.py @@ -1,6 +1,7 @@ from abc import ABC from typing import ClassVar from typing import Dict +from typing import Optional import uuid6 from litestar import Request @@ -11,8 +12,8 @@ from litestar.types import Receive from litestar.types import Scope from litestar.types import Send +from pydantic import SecretStr -from evidently._pydantic_compat import SecretStr from evidently.pydantic_utils import register_type_alias from evidently.ui.service.components.base import Component from evidently.ui.service.components.base import ComponentContext @@ -27,8 +28,7 @@ class SecurityComponent(Component, ABC): add_security_middleware: ClassVar[bool] = True - class Config: - is_base_type = True + __is_base_type__: ClassVar[bool] = True def get_security(self) -> SecurityService: raise NotImplementedError @@ -87,8 +87,7 @@ def get_dependencies(self, ctx: ComponentContext) -> Dict[str, Provide]: class NoSecurityComponent(SimpleSecurity): - class Config: - type_alias = "none" + __type_alias__: ClassVar[Optional[str]] = "none" dummy_user_id: UserID = uuid6.UUID(int=1, version=7) dummy_org_id: OrgID = uuid6.UUID(int=2, version=7) @@ -100,8 +99,7 @@ def get_security(self) -> SecurityService: class TokenSecurityComponent(SimpleSecurity): - class Config: - type_alias = "token" + __type_alias__: ClassVar[Optional[str]] = "token" token: SecretStr diff --git a/src/evidently/ui/service/components/snapshot_links.py b/src/evidently/ui/service/components/snapshot_links.py index 34a8c10584..0066425865 100644 --- a/src/evidently/ui/service/components/snapshot_links.py +++ b/src/evidently/ui/service/components/snapshot_links.py @@ -9,8 +9,7 @@ class SnapshotDatasetLinksComponent(FactoryComponent[SnapshotDatasetLinksManager], Component): """Component for snapshot dataset links manager.""" - class Config: - is_base_type = True + __is_base_type__: ClassVar[bool] = True __section__: ClassVar[str] = "snapshot_dataset_links" dependency_name: ClassVar[str] = "snapshot_dataset_links" diff --git a/src/evidently/ui/service/components/storage.py b/src/evidently/ui/service/components/storage.py index ca53e4aaf7..f356eb1418 100644 --- a/src/evidently/ui/service/components/storage.py +++ b/src/evidently/ui/service/components/storage.py @@ -28,8 +28,7 @@ class DatasetMetadataComponent(FactoryComponent[DatasetMetadataStorage], ABC): - class Config: - is_base_type = True + __is_base_type__: ClassVar[bool] = True __section__: ClassVar[str] = "dataset_metadata" dependency_name: ClassVar[str] = "dataset_metadata" @@ -40,8 +39,7 @@ def dependency_factory(self) -> Callable[..., DatasetMetadataStorage]: class DatasetFileStorageComponent(FactoryComponent[BlobStorage], ABC): - class Config: - is_base_type = True + __is_base_type__: ClassVar[bool] = True __section__: ClassVar[str] = "dataset_storage" dependency_name: ClassVar[str] = "dataset_blob_storage" @@ -53,8 +51,7 @@ def dependency_factory(self) -> Callable[..., BlobStorage]: class StorageComponent(Component, ABC): - class Config: - is_base_type = True + __is_base_type__: ClassVar[bool] = True dependency_name: ClassVar = "project_manager" use_cache: ClassVar[bool] = True @@ -100,8 +97,7 @@ def artifact_storage_provider(self) -> Callable[..., Awaitable["ArtifactStorage" class LocalStorageComponent(StorageComponent): - class Config: - type_alias = "local" + __type_alias__: ClassVar[Optional[str]] = "local" path: str = "workspace" autorefresh: bool = True @@ -148,8 +144,7 @@ async def artifact_storage_factory(): class MetadataStorageComponent(FactoryComponent[ProjectMetadataStorage], ABC): - class Config: - is_base_type = True + __is_base_type__: ClassVar[bool] = True __section__: ClassVar = "metadata" dependency_name: ClassVar = "project_metadata" @@ -158,8 +153,7 @@ class Config: class DataStorageComponent(FactoryComponent[DataStorage], ABC): - class Config: - is_base_type = True + __is_base_type__: ClassVar[bool] = True __section__: ClassVar = "data" @@ -169,8 +163,7 @@ class Config: class BlobStorageComponent(FactoryComponent[BlobStorage], ABC): - class Config: - is_base_type = True + __is_base_type__: ClassVar[bool] = True __section__: ClassVar = "blob" diff --git a/src/evidently/ui/service/components/tracing.py b/src/evidently/ui/service/components/tracing.py index b765605cd0..f6aa4b0c5e 100644 --- a/src/evidently/ui/service/components/tracing.py +++ b/src/evidently/ui/service/components/tracing.py @@ -11,8 +11,7 @@ class TracingStorageComponent(FactoryComponent[TracingStorage], ABC): """Base component for tracing storage.""" - class Config: - is_base_type = True + __is_base_type__: ClassVar[bool] = True __section__: ClassVar[str] = "tracing_storage" dependency_name: ClassVar[str] = "tracing_storage" diff --git a/src/evidently/ui/service/config.py b/src/evidently/ui/service/config.py index 1ad753af3c..d676a02fdb 100644 --- a/src/evidently/ui/service/config.py +++ b/src/evidently/ui/service/config.py @@ -13,10 +13,10 @@ from dynaconf.utils.boxing import DynaBox from litestar import Litestar from litestar.di import Provide +from pydantic import BaseModel +from pydantic import PrivateAttr +from pydantic import TypeAdapter -from evidently._pydantic_compat import BaseModel -from evidently._pydantic_compat import PrivateAttr -from evidently._pydantic_compat import parse_obj_as from evidently.ui.service.components.base import SECTION_COMPONENT_TYPE_MAPPING from evidently.ui.service.components.base import AppBuilder from evidently.ui.service.components.base import Component @@ -130,14 +130,16 @@ def load_config(config_type: Type[TConfig], box: dict) -> TConfig: continue if section == "additional_components": for subsection, compoennt_subdict in component_dict.items(): - component = parse_obj_as(SECTION_COMPONENT_TYPE_MAPPING.get(subsection, Component), compoennt_subdict) + component = TypeAdapter(SECTION_COMPONENT_TYPE_MAPPING.get(subsection, Component)).validate_python( + compoennt_subdict + ) components[subsection] = component elif section in config_type.__fields__: type_ = config_type.__fields__[section].type_ - component = parse_obj_as(type_, component_dict) + component = TypeAdapter(type_).validate_python(component_dict) named_components[section] = component elif section in SECTION_COMPONENT_TYPE_MAPPING: - component = parse_obj_as(SECTION_COMPONENT_TYPE_MAPPING[section], component_dict) + component = TypeAdapter(SECTION_COMPONENT_TYPE_MAPPING[section]).validate_python(component_dict) components[section] = component else: raise ValueError(f"unknown config section {section}") diff --git a/src/evidently/ui/service/datasets/data_source.py b/src/evidently/ui/service/datasets/data_source.py index 2c8dfd3dd6..9a8e153d16 100644 --- a/src/evidently/ui/service/datasets/data_source.py +++ b/src/evidently/ui/service/datasets/data_source.py @@ -12,9 +12,9 @@ import pandas as pd from litestar import Response +from pydantic import BaseModel from typing_extensions import TypeAlias -from evidently._pydantic_compat import BaseModel from evidently.core.datasets import DataDefinition from evidently.core.metric_types import AutoAliasMixin from evidently.pydantic_utils import PolymorphicModel @@ -56,9 +56,8 @@ class DataSource(AutoAliasMixin, PolymorphicModel, ABC): __alias_namespace__: ClassVar = "evidently" __alias_type__: ClassVar = "data_source" - class Config: - is_base_type = True - alias_required = True + __is_base_type__: ClassVar[bool] = True + __alias_required__: ClassVar[bool] = True async def materialize(self, dataset_manager: "DatasetManager") -> MaterializedDataset: """Materialize the data source into a DataFrame.""" @@ -141,15 +140,14 @@ def get_original_dataset_id(self) -> Optional[DatasetID]: class DataSourceDTO(AutoAliasMixin, PolymorphicModel, ABC): """DTO for data source serialization.""" - class Config: - is_base_type = True + __is_base_type__: ClassVar[bool] = True __data_source_type__: ClassVar[Type[DataSource]] __alias_type__: ClassVar = "data_source_dto" def to_data_source(self, **kwargs) -> DataSource: """Convert DTO to data source.""" - kwargs = {k: v for k, v in kwargs.items() if k in self.__data_source_type__.__fields__} + kwargs = {k: v for k, v in kwargs.items() if k in self.__data_source_type__.model_fields} return self.__data_source_type__(**self.__dict__, **kwargs) @staticmethod @@ -158,8 +156,12 @@ def for_type( ) -> Type["DataSourceDTO"]: """Create a DTO type for a data source type.""" namespace = { - "__annotations__": {n: f.outer_type_ for n, f in data_source_type.__fields__.items() if n not in exclude}, - **{n: f.default for n, f in data_source_type.__fields__.items() if n not in exclude and not f.required}, + "__annotations__": {n: f.annotation for n, f in data_source_type.model_fields.items() if n not in exclude}, + **{ + n: f.default + for n, f in data_source_type.model_fields.items() + if n not in exclude and not f.is_required() + }, } new_dto_type: Type[DataSourceDTO] = type(f"{data_source_type.__name__}DTO", (DataSourceDTO,), namespace) diff --git a/src/evidently/ui/service/datasets/filters.py b/src/evidently/ui/service/datasets/filters.py index 882feb0593..f6f1ec0160 100644 --- a/src/evidently/ui/service/datasets/filters.py +++ b/src/evidently/ui/service/datasets/filters.py @@ -3,106 +3,92 @@ from typing import Optional import pandas as pd +from pydantic import ConfigDict -from evidently._pydantic_compat import Extra from evidently.pydantic_utils import PolymorphicModel class FilterBy(PolymorphicModel): - column: str + model_config = ConfigDict(extra="forbid") + __type_alias__: str = "filter_by" + __is_base_type__: bool = True - class Config: - is_base_type = True - type_alias = "filter_by" - extra = Extra.forbid + column: str def condition(self, df: pd.DataFrame) -> pd.Series: raise NotImplementedError class FilterByString(FilterBy): + __type_alias__: str = "filter_by_string" + __is_base_type__: bool = True value: str - class Config: - is_base_type = True - type_alias = "filter_by_string" - class FilterByNumber(FilterBy): + __type_alias__: str = "filter_by_number" + __is_base_type__: bool = True value: float - class Config: - is_base_type = True - type_alias = "filter_by_number" - class ContainsStrFilter(FilterByString): - class Config: - type_alias = "contains" + __type_alias__: str = "contains" def condition(self, df: pd.DataFrame) -> pd.Series: return df[self.column].str.contains(self.value, na=False) class StartsWithFilter(FilterByString): - class Config: - type_alias = "starts_with" + __type_alias__: str = "starts_with" def condition(self, df: pd.DataFrame) -> pd.Series: return df[self.column].str.startswith(self.value, na=False) class EndsWithFilter(FilterByString): - class Config: - type_alias = "ends_with" + __type_alias__: str = "ends_with" def condition(self, df: pd.DataFrame) -> pd.Series: return df[self.column].str.endswith(self.value, na=False) class EqualFilter(FilterByNumber): - class Config: - type_alias = "eq" + __type_alias__: str = "eq" def condition(self, df: pd.DataFrame) -> pd.Series: return df[self.column] == self.value class NotEqualFilter(FilterByNumber): - class Config: - type_alias = "not_eq" + __type_alias__: str = "not_eq" def condition(self, df: pd.DataFrame) -> pd.Series: return df[self.column] != self.value class GTFilter(FilterByNumber): - class Config: - type_alias = "gt" + __type_alias__: str = "gt" def condition(self, df: pd.DataFrame) -> pd.Series: return df[self.column] > self.value class GTEFilter(FilterByNumber): - class Config: - type_alias = "gte" + __type_alias__: str = "gte" def condition(self, df: pd.DataFrame) -> pd.Series: return df[self.column] >= self.value class LTFilter(FilterByNumber): - class Config: - type_alias = "lt" + __type_alias__: str = "lt" def condition(self, df: pd.DataFrame) -> pd.Series: return df[self.column] < self.value class LTEFilter(FilterByNumber): - class Config: - type_alias = "lte" + __type_alias__: str = "lte" def condition(self, df: pd.DataFrame) -> pd.Series: return df[self.column] <= self.value diff --git a/src/evidently/ui/service/datasets/metadata.py b/src/evidently/ui/service/datasets/metadata.py index ff59d0af11..4443361687 100644 --- a/src/evidently/ui/service/datasets/metadata.py +++ b/src/evidently/ui/service/datasets/metadata.py @@ -9,8 +9,9 @@ from typing import List from typing import Optional -from evidently._pydantic_compat import BaseModel -from evidently._pydantic_compat import parse_obj_as +from pydantic import BaseModel +from pydantic import TypeAdapter + from evidently.core.datasets import EVIDENTLY_DATASET_EXT from evidently.core.datasets import DataDefinition from evidently.legacy.suite.base_suite import MetadataValueType @@ -159,7 +160,7 @@ async def add_dataset_metadata(self, user_id: UserID, project_id: ProjectID, dat dataset = DatasetMetadataFull(**dataset.dict(), author_name="", created_at=now, updated_at=now) metadata_path = self._metadata_path(project_id, dataset.id) with self.location.open(metadata_path, "w") as f: - f.write(dataset.json(indent=2)) + f.write(dataset.model_dump_json(indent=2)) return dataset.id @@ -237,7 +238,7 @@ async def get_dataset_metadata(self, dataset_id: DatasetID) -> Optional[DatasetM metadata_dict = json.load(f) # Convert to DatasetMetadataFull - return parse_obj_as(DatasetMetadataFull, metadata_dict) + return TypeAdapter(DatasetMetadataFull).validate_python(metadata_dict) return None @@ -313,7 +314,7 @@ async def list_datasets_metadata( if is_draft != draft: continue - dataset_metadata = parse_obj_as(DatasetMetadataFull, metadata_dict) + dataset_metadata = TypeAdapter(DatasetMetadataFull).validate_python(metadata_dict) datasets.append(dataset_metadata) except Exception: # Skip malformed datasets diff --git a/src/evidently/ui/service/datasets/models.py b/src/evidently/ui/service/datasets/models.py index fecc603c21..1e6377e11f 100644 --- a/src/evidently/ui/service/datasets/models.py +++ b/src/evidently/ui/service/datasets/models.py @@ -1,7 +1,8 @@ from enum import Enum from typing import List -from evidently._pydantic_compat import BaseModel +from pydantic import BaseModel + from evidently.core.datasets import DataDefinition diff --git a/src/evidently/ui/service/managers/auth.py b/src/evidently/ui/service/managers/auth.py index 79c7e05145..34a1fe151d 100644 --- a/src/evidently/ui/service/managers/auth.py +++ b/src/evidently/ui/service/managers/auth.py @@ -8,7 +8,8 @@ from typing import Set from typing import Tuple -from evidently._pydantic_compat import BaseModel +from pydantic import BaseModel + from evidently.ui.service.base import EntityType from evidently.ui.service.base import Org from evidently.ui.service.base import Team diff --git a/src/evidently/ui/service/managers/projects.py b/src/evidently/ui/service/managers/projects.py index 963b306c70..2a1eef42e8 100644 --- a/src/evidently/ui/service/managers/projects.py +++ b/src/evidently/ui/service/managers/projects.py @@ -6,8 +6,8 @@ from typing import Optional from litestar.exceptions import HTTPException +from pydantic import TypeAdapter -from evidently._pydantic_compat import parse_obj_as from evidently.core.serialization import SnapshotModel from evidently.legacy.utils import NumpyEncoder from evidently.sdk.models import DashboardModel @@ -182,7 +182,7 @@ async def load_snapshot( ): raise ProjectNotFound() with self.blob_storage.open_blob(self._create_path_for_snapshot(project_id, snapshot)) as f: - return parse_obj_as(SnapshotModel, json.load(f)) + return TypeAdapter(SnapshotModel).validate_python(json.load(f)) async def get_snapshot_metadata( self, user_id: UserID, project_id: ProjectID, snapshot_id: SnapshotID diff --git a/src/evidently/ui/service/storage/local/artifacts.py b/src/evidently/ui/service/storage/local/artifacts.py index b93ef905c4..a4dbb8f4ec 100644 --- a/src/evidently/ui/service/storage/local/artifacts.py +++ b/src/evidently/ui/service/storage/local/artifacts.py @@ -4,7 +4,8 @@ from typing import List from typing import Optional -from evidently._pydantic_compat import BaseModel +from pydantic import BaseModel + from evidently.legacy.core import new_id from evidently.sdk.artifacts import Artifact from evidently.sdk.artifacts import ArtifactID diff --git a/src/evidently/ui/service/storage/local/base.py b/src/evidently/ui/service/storage/local/base.py index f2d8682553..d554a52e25 100644 --- a/src/evidently/ui/service/storage/local/base.py +++ b/src/evidently/ui/service/storage/local/base.py @@ -12,11 +12,11 @@ from typing import Union import uuid6 +from pydantic import BaseModel +from pydantic import PrivateAttr +from pydantic import TypeAdapter +from pydantic import ValidationError -from evidently._pydantic_compat import BaseModel -from evidently._pydantic_compat import PrivateAttr -from evidently._pydantic_compat import ValidationError -from evidently._pydantic_compat import parse_obj_as from evidently.core.metric_types import ByLabelCountValue from evidently.core.metric_types import ByLabelValue from evidently.core.metric_types import CountValue @@ -98,7 +98,7 @@ async def delete_blob(self, blob_id: BlobID): def load_project(location: FSLocation, path: str) -> Optional[Project]: try: with location.open(posixpath.join(path, METADATA_PATH)) as f: - return parse_obj_as(Project, json.load(f)) + return TypeAdapter(Project).validate_python(json.load(f)) except FileNotFoundError: return None @@ -154,7 +154,7 @@ def reload_snapshot(self, project: Project, snapshot_id: SnapshotID, skip_errors try: snapshot_path = posixpath.join(str(project.id), SNAPSHOTS, str(snapshot_id) + ".json") with self.location.open(snapshot_path) as f: - model = parse_obj_as(SnapshotModel, json.load(f)) + model = TypeAdapter(SnapshotModel).validate_python(json.load(f)) self.snapshots[project.id][snapshot_id] = model for callback in self.callbacks: callback(project.id, snapshot_id, model) diff --git a/src/evidently/ui/service/storage/local/snapshot_links.py b/src/evidently/ui/service/storage/local/snapshot_links.py index 883c5fa422..507ed321da 100644 --- a/src/evidently/ui/service/storage/local/snapshot_links.py +++ b/src/evidently/ui/service/storage/local/snapshot_links.py @@ -1,7 +1,8 @@ import json import posixpath -from evidently._pydantic_compat import parse_obj_as +from pydantic import TypeAdapter + from evidently.legacy.suite.base_suite import DatasetInputOutputLinks from evidently.legacy.suite.base_suite import SnapshotLinks from evidently.legacy.utils.numpy_encoder import numpy_dumps @@ -32,7 +33,7 @@ async def get_links(self, project_id: ProjectID, snapshot_id: SnapshotID) -> Sna with self.location.open(links_path, "r") as f: links_data = json.load(f) - input_output_links = parse_obj_as(DatasetInputOutputLinks, links_data) + input_output_links = TypeAdapter(DatasetInputOutputLinks).validate_python(links_data) return SnapshotLinks(datasets=input_output_links) @@ -54,7 +55,7 @@ async def link_dataset_snapshot( if self.location.exists(links_path): with self.location.open(links_path, "r") as f: links_data = json.load(f) - input_output_links = parse_obj_as(DatasetInputOutputLinks, links_data) + input_output_links = TypeAdapter(DatasetInputOutputLinks).validate_python(links_data) else: input_output_links = DatasetInputOutputLinks() diff --git a/src/evidently/ui/service/storage/sql/components.py b/src/evidently/ui/service/storage/sql/components.py index 5735af7210..417ef7adc1 100644 --- a/src/evidently/ui/service/storage/sql/components.py +++ b/src/evidently/ui/service/storage/sql/components.py @@ -7,12 +7,12 @@ from typing import Optional from litestar.di import Provide +from pydantic import BaseModel +from pydantic import Field +from pydantic import SecretStr from sqlalchemy import Engine from sqlalchemy import create_engine -from evidently._pydantic_compat import BaseModel -from evidently._pydantic_compat import Field -from evidently._pydantic_compat import SecretStr from evidently.legacy.utils.numpy_encoder import numpy_dumps from evidently.ui.service.base import BlobStorage from evidently.ui.service.base import DataStorage @@ -41,8 +41,7 @@ class SQLMetadataComponent(MetadataStorageComponent): """SQL metadata storage component.""" - class Config: - type_alias = "sql" + __type_alias__: ClassVar[Optional[str]] = "sql" def dependency_factory(self) -> Callable[..., ProjectMetadataStorage]: """Create SQL metadata storage factory.""" @@ -52,8 +51,7 @@ def dependency_factory(self) -> Callable[..., ProjectMetadataStorage]: class SQLDataComponent(DataStorageComponent): """SQL data storage component.""" - class Config: - type_alias = "sql" + __type_alias__: ClassVar[Optional[str]] = "sql" def dependency_factory(self) -> Callable[..., DataStorage]: """Create SQL data storage factory.""" @@ -67,8 +65,7 @@ def sql_data_storage(engine: Engine) -> SQLDataStorage: class SQLBlobComponent(BlobStorageComponent): """SQL blob storage component.""" - class Config: - type_alias = "sql" + __type_alias__: ClassVar[Optional[str]] = "sql" def dependency_factory(self) -> Callable[..., BlobStorage]: """Create SQL blob storage factory.""" @@ -127,8 +124,7 @@ def get_dependencies(self, ctx: ComponentContext) -> Dict[str, Provide]: class SQLStorageComponent(StorageComponent): __require__: ClassVar = [DatabaseComponent] - class Config: - type_alias = "sql" + __type_alias__: ClassVar[Optional[str]] = "sql" def project_manager_provider(self) -> Callable[..., Awaitable[ProjectManager]]: async def project_manager_factory(engine: Engine) -> ProjectManager: @@ -177,8 +173,7 @@ class SQLDatasetMetadataComponent(DatasetMetadataComponent): __require__: ClassVar = [DatabaseComponent] - class Config: - type_alias = "sql" + __type_alias__: ClassVar[Optional[str]] = "sql" def dependency_factory(self) -> Callable[..., DatasetMetadataStorage]: def sql_dataset_metadata(engine: Engine) -> DatasetMetadataStorage: @@ -192,8 +187,7 @@ class SQLDatasetFileStorageComponent(DatasetFileStorageComponent): __require__: ClassVar = [DatabaseComponent] - class Config: - type_alias = "sql" + __type_alias__: ClassVar[Optional[str]] = "sql" def dependency_factory(self) -> Callable[..., BlobStorage]: def sql_dataset_file_storage(engine: Engine) -> BlobStorage: @@ -207,8 +201,7 @@ class SQLSnapshotDatasetLinksComponent(SnapshotDatasetLinksComponent): __require__: ClassVar = [DatabaseComponent] - class Config: - type_alias = "sql" + __type_alias__: ClassVar[Optional[str]] = "sql" def dependency_factory(self) -> Callable[..., SnapshotDatasetLinksManager]: def sql_snapshot_dataset_links(engine: Engine) -> SnapshotDatasetLinksManager: diff --git a/src/evidently/ui/service/storage/sql/dashboard.py b/src/evidently/ui/service/storage/sql/dashboard.py index 37f8b3f343..6f8ad53794 100644 --- a/src/evidently/ui/service/storage/sql/dashboard.py +++ b/src/evidently/ui/service/storage/sql/dashboard.py @@ -37,9 +37,9 @@ async def get_dashboard(self, project_id: ProjectID) -> DashboardModel: return DashboardModel(tabs=[], panels=[]) # Convert dashboard_json to DashboardModel - from evidently._pydantic_compat import parse_obj_as + from pydantic import TypeAdapter - return parse_obj_as(DashboardModel, project.dashboard_json) + return TypeAdapter(DashboardModel).validate_python(project.dashboard_json) async def save_dashboard(self, project_id: ProjectID, dashboard: DashboardModel) -> None: """Save dashboard for a project.""" diff --git a/src/evidently/ui/service/storage/sql/models.py b/src/evidently/ui/service/storage/sql/models.py index 4edc2347d4..6ba90b8846 100644 --- a/src/evidently/ui/service/storage/sql/models.py +++ b/src/evidently/ui/service/storage/sql/models.py @@ -7,6 +7,7 @@ from typing import List from typing import Optional +from pydantic import TypeAdapter from sqlalchemy import JSON from sqlalchemy import ForeignKey from sqlalchemy import Index @@ -17,7 +18,6 @@ from sqlalchemy.orm import mapped_column from sqlalchemy.orm import relationship -from evidently._pydantic_compat import parse_obj_as from evidently.core.datasets import DataDefinition from evidently.core.metric_types import Metric from evidently.core.serialization import SnapshotModel @@ -127,7 +127,7 @@ def from_snapshot(cls, snapshot: SnapshotModel, project_id: ProjectID, blob: Blo def load(self, blob_storage) -> SnapshotModel: """Load snapshot from blob storage.""" with blob_storage.open_blob(self.blob_path) as f: - return parse_obj_as(SnapshotModel, json.load(f)) + return TypeAdapter(SnapshotModel).validate_python(json.load(f)) def to_snapshot_metadata(self, project: Optional[Project]) -> SnapshotMetadataModel: """Convert model to SnapshotMetadataModel object.""" @@ -152,7 +152,7 @@ class MetricsSQLModel(Base): @property def metric(self) -> Metric: """Get metric object from JSON.""" - return parse_obj_as(Metric, json.loads(self.metric_json)) # type: ignore[type-abstract,return-value] + return TypeAdapter(Metric).validate_python(json.loads(self.metric_json)) # type: ignore[type-abstract,return-value] class PointSQLModel(Base): @@ -240,8 +240,8 @@ def to_dataset_metadata(self): project_id=self.project_id, author_id=self.author_id, all_columns=self.all_columns, - data_definition=parse_obj_as(DataDefinition, self.data_definition), - source=parse_obj_as(DataSource, self.source), + data_definition=TypeAdapter(DataDefinition).validate_python(self.data_definition), + source=TypeAdapter(DataSource).validate_python(self.source), created_at=self.created_at, updated_at=self.updated_at, author_name=self.author.name if self.author else "Unknown User", @@ -250,7 +250,9 @@ def to_dataset_metadata(self): origin=DatasetOrigin(self.origin), metadata=self.metadata_json, tags=self.tags, - tracing_params=parse_obj_as(DatasetTracingParams, self.tracing_params) if self.tracing_params else None, + tracing_params=TypeAdapter(DatasetTracingParams).validate_python(self.tracing_params) + if self.tracing_params + else None, human_feedback_custom_shortcut_labels=self.human_feedback_custom_shortcut_labels, ) diff --git a/src/evidently/ui/service/storage/utils.py b/src/evidently/ui/service/storage/utils.py index 7211b2678b..758c27602f 100644 --- a/src/evidently/ui/service/storage/utils.py +++ b/src/evidently/ui/service/storage/utils.py @@ -6,7 +6,8 @@ from typing import Optional from typing import Tuple -from evidently._pydantic_compat import BaseModel +from pydantic import BaseModel + from evidently.legacy.base_metric import MetricResult from evidently.legacy.core import BaseResult from evidently.legacy.utils import NumpyEncoder @@ -43,7 +44,7 @@ def iterate_obj_fields( if isinstance(obj, dict): yield from (r for key, value in obj.items() for r in iterate_obj_fields(value, paths + [str(key)], early_stop)) return - if isinstance(obj, BaseResult) and obj.__config__.extract_as_obj: + if isinstance(obj, BaseResult) and obj.__extract_as_obj__: yield ".".join(paths), obj return if isinstance(obj, BaseModel): @@ -61,7 +62,7 @@ def iterate_obj_float_fields(obj: Any, paths: List[str]) -> Iterator[Tuple[str, if isinstance(value, dict): yield path, json.dumps(value, cls=NumpyEncoder) continue - if isinstance(value, BaseResult) and value.__config__.extract_as_obj: + if isinstance(value, BaseResult) and value.__extract_as_obj__: yield path, json.dumps(value.dict(), cls=NumpyEncoder) continue try: diff --git a/src/evidently/ui/service/tracing/api.py b/src/evidently/ui/service/tracing/api.py index 8d2d7a271b..84d41ddb88 100644 --- a/src/evidently/ui/service/tracing/api.py +++ b/src/evidently/ui/service/tracing/api.py @@ -14,8 +14,8 @@ from litestar.exceptions import NotFoundException from litestar.params import Dependency from opentelemetry.proto.collector.trace.v1 import trace_service_pb2 +from pydantic import BaseModel -from evidently._pydantic_compat import BaseModel from evidently.core.datasets import ServiceColumns from evidently.legacy.ui.type_aliases import UserID from evidently.ui.service.datasets.metadata import DatasetTracingParams diff --git a/src/evidently/ui/service/tracing/storage/base.py b/src/evidently/ui/service/tracing/storage/base.py index b8f09a5225..a5a4419fd2 100644 --- a/src/evidently/ui/service/tracing/storage/base.py +++ b/src/evidently/ui/service/tracing/storage/base.py @@ -9,9 +9,9 @@ import pandas as pd from opentelemetry.proto.collector.trace.v1 import trace_service_pb2 +from pydantic import BaseModel from typing_extensions import TypeAlias -from evidently._pydantic_compat import BaseModel from evidently.core.datasets import DataDefinition diff --git a/src/evidently/ui/service/utils.py b/src/evidently/ui/service/utils.py index 3876c93313..912767b6ee 100644 --- a/src/evidently/ui/service/utils.py +++ b/src/evidently/ui/service/utils.py @@ -7,9 +7,9 @@ from typing import Union import requests +from pydantic import BaseModel +from pydantic import TypeAdapter -from evidently._pydantic_compat import BaseModel -from evidently._pydantic_compat import parse_obj_as from evidently.legacy.ui.storage.common import SECRET_HEADER_NAME from evidently.legacy.utils import NumpyEncoder @@ -42,7 +42,7 @@ def _request( ) response.raise_for_status() if response_model is not None: - return parse_obj_as(response_model, response.json()) + return TypeAdapter(response_model).validate_python(response.json()) return response diff --git a/src/evidently/ui/storage/local/base.py b/src/evidently/ui/storage/local/base.py index 07701f9f7c..b29a03b7c9 100644 --- a/src/evidently/ui/storage/local/base.py +++ b/src/evidently/ui/storage/local/base.py @@ -3,8 +3,9 @@ import re from typing import List -from evidently._pydantic_compat import BaseModel -from evidently._pydantic_compat import parse_obj_as +from pydantic import BaseModel +from pydantic import TypeAdapter + from evidently.core.serialization import SnapshotModel from evidently.legacy.utils import NumpyEncoder from evidently.sdk.models import DashboardModel @@ -51,7 +52,7 @@ def _snapshot_path(self, project_id: STR_UUID, snapshot_id: STR_UUID) -> str: def read_project(self, project_id: STR_UUID) -> ProjectModel: with self.location.open(self._project_path(project_id)) as f: - return parse_obj_as(ProjectModel, json.load(f)) + return TypeAdapter(ProjectModel).validate_python(json.load(f)) def write_project(self, project: ProjectModel) -> ProjectModel: self.location.makedirs(str(project.id)) @@ -66,7 +67,7 @@ def write_snapshot(self, project_id: STR_UUID, snapshot_id: STR_UUID, snapshot: def read_snapshot(self, project_id: STR_UUID, snapshot_id: STR_UUID) -> SnapshotModel: with self.location.open(self._snapshot_path(project_id, snapshot_id)) as f: - return parse_obj_as(SnapshotModel, json.load(f)) + return TypeAdapter(SnapshotModel).validate_python(json.load(f)) def list_projects(self) -> List[ProjectID]: projects = [] @@ -99,4 +100,4 @@ def read_dashboard(self, project_id: STR_UUID) -> DashboardModel: if not self.location.exists(self._dashboard_path(project_id)): return DashboardModel(tabs=[], panels=[]) with self.location.open(self._dashboard_path(project_id)) as f: - return parse_obj_as(DashboardModel, json.load(f)) + return TypeAdapter(DashboardModel).validate_python(json.load(f)) diff --git a/src/evidently/ui/workspace.py b/src/evidently/ui/workspace.py index ca7b1dadae..5f3f39ceed 100644 --- a/src/evidently/ui/workspace.py +++ b/src/evidently/ui/workspace.py @@ -15,10 +15,10 @@ from typing import overload from urllib.parse import urljoin +from pydantic import BaseModel from requests import HTTPError from requests import Response -from evidently._pydantic_compat import BaseModel from evidently.core.datasets import Dataset from evidently.core.report import Snapshot from evidently.core.serialization import SnapshotModel diff --git a/src/evidently/utils/schema.py b/src/evidently/utils/schema.py index 395da4f320..b260c51bde 100644 --- a/src/evidently/utils/schema.py +++ b/src/evidently/utils/schema.py @@ -6,6 +6,8 @@ from typing import Tuple from typing import Type from typing import Union +from typing import get_args +from typing import get_origin from typing import no_type_check import litestar @@ -18,14 +20,11 @@ from litestar.openapi.spec import Schema from litestar.serialization import get_serializer from litestar.typing import FieldDefinition +from pydantic import create_model +from pydantic._internal._validators import import_string +from pydantic.fields import FieldInfo from typing_inspect import is_generic_type -from evidently._pydantic_compat import SHAPE_DICT -from evidently._pydantic_compat import SHAPE_LIST -from evidently._pydantic_compat import SHAPE_SINGLETON -from evidently._pydantic_compat import ModelField -from evidently._pydantic_compat import create_model -from evidently._pydantic_compat import import_string from evidently.pydantic_utils import TYPE_ALIASES from evidently.pydantic_utils import PolymorphicModel from evidently.pydantic_utils import is_not_abstract @@ -38,14 +37,17 @@ @no_type_check -def _with_shape(cls: Type, model_field: ModelField): - if model_field.shape == SHAPE_SINGLETON: +def _with_shape(cls: Type, field_info: FieldInfo): + ann = field_info.annotation + origin = get_origin(ann) + if origin is None: return cls - if model_field.shape == SHAPE_LIST: + if origin is list: return List[cls] - if model_field.shape == SHAPE_DICT: - return Dict[model_field.key_field.type_, cls] - raise NotImplementedError(f"Not implemented for shape {model_field.shape}") + if origin is dict: + args = get_args(ann) + return Dict[args[0], cls] if args else Dict[str, cls] + raise NotImplementedError(f"Not implemented for annotation {ann}") def nonabstract_subtypes(cls: Type[PolymorphicModel]) -> Tuple[Type[PolymorphicModel], ...]: @@ -61,7 +63,7 @@ def to_openapi_schema(self, field_definition: FieldDefinition, schema_creator: S if is_generic_type(ann): ann = typing_inspect.get_origin(ann) or ann - if isinstance(ann, type) and issubclass(ann, PolymorphicModel) and ann.__is_base_type__(): + if isinstance(ann, type) and issubclass(ann, PolymorphicModel) and ann.__get_is_base_type__(): subtypes = nonabstract_subtypes(ann) if len(subtypes) > 0: return schema_creator.for_field_definition(FieldDefinition.from_annotation(Union[subtypes])) diff --git a/tests/calculation_engine/test_python_engine.py b/tests/calculation_engine/test_python_engine.py index e141cc6555..1b83099c1e 100644 --- a/tests/calculation_engine/test_python_engine.py +++ b/tests/calculation_engine/test_python_engine.py @@ -1,3 +1,5 @@ +from typing import ClassVar + import pandas as pd from evidently.legacy.base_metric import GenericInputData @@ -13,8 +15,7 @@ class OldTypeSimpleMetric(Metric[int]): - class Config: - alias_required = False + __alias_required__: ClassVar[bool] = False value: int @@ -27,8 +28,7 @@ def calculate(self, data: InputData) -> int: class SimpleMetric(Metric[int]): - class Config: - alias_required = False + __alias_required__: ClassVar[bool] = False value: int diff --git a/tests/calculations/test_data_quality.py b/tests/calculations/test_data_quality.py index caec9b6d6a..c715cc0b6f 100644 --- a/tests/calculations/test_data_quality.py +++ b/tests/calculations/test_data_quality.py @@ -6,7 +6,7 @@ from evidently.legacy.calculations.data_quality import calculate_cramer_v_correlation from evidently.legacy.calculations.data_quality import get_rows_count from evidently.legacy.metric_results import ColumnCorrelations -from evidently.legacy.metric_results import Distribution +from evidently.legacy.metric_results import DistributionIncluded @pytest.mark.parametrize( @@ -47,7 +47,7 @@ def test_calculate_cramer_v_correlations(): assert calculate_cramer_v_correlation("test1", data, ["test2", "test3", "test4"]) == ColumnCorrelations( column_name="test1", kind="cramer_v", - values=Distribution( + values=DistributionIncluded( x=["test2", "test3", "test4"], y=[1.0, 1.0, 1.0], ), diff --git a/tests/collector/conftest.py b/tests/collector/conftest.py index 7965519e4a..f0669f3660 100644 --- a/tests/collector/conftest.py +++ b/tests/collector/conftest.py @@ -102,6 +102,9 @@ class ReportConfigMock(ReportConfig): def to_report_base(self): return ReportBaseMock() + def __eq__(self, other): + return isinstance(other, ReportConfig) and self.model_dump() == other.model_dump() + @pytest.fixture() def mock_collector_config() -> CollectorConfig: diff --git a/tests/collector/test_app.py b/tests/collector/test_app.py index 4d253be5ba..3c4873929b 100644 --- a/tests/collector/test_app.py +++ b/tests/collector/test_app.py @@ -5,8 +5,8 @@ import pandas as pd import pytest from litestar.testing import TestClient +from pydantic import parse_obj_as -from evidently._pydantic_compat import parse_obj_as from evidently.legacy.collector.app import check_snapshots_factory from evidently.legacy.collector.config import CollectorConfig from evidently.legacy.collector.config import CollectorServiceConfig diff --git a/tests/collector/test_config.py b/tests/collector/test_config.py index cdd9671704..4ce879f968 100644 --- a/tests/collector/test_config.py +++ b/tests/collector/test_config.py @@ -3,8 +3,8 @@ from unittest.mock import Mock import pytest +from pydantic import ValidationError -from evidently._pydantic_compat import ValidationError from evidently.legacy.collector.config import IntervalTrigger from evidently.legacy.collector.config import RowsCountOrIntervalTrigger from evidently.legacy.collector.config import RowsCountTrigger diff --git a/tests/conftest.py b/tests/conftest.py index cf527aeea9..5effca85e8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,9 +1,9 @@ import numpy as np import pandas as pd import pytest +from pydantic import BaseModel +from pydantic._internal._validators import import_string -from evidently._pydantic_compat import BaseModel -from evidently._pydantic_compat import import_string from evidently.legacy.utils.types import ApproxValue from evidently.pydantic_utils import TYPE_ALIASES from evidently.pydantic_utils import PolymorphicModel @@ -26,10 +26,10 @@ def smart_assert_equal(actual, expected, path=""): ) ): ignore_not_set = hasattr(expected, "__ignore_not_set__") and expected.__ignore_not_set__ - for field in actual.__fields__.values(): - if ignore_not_set and getattr(expected, field.name) is None: + for name in actual.model_fields: + if ignore_not_set and getattr(expected, name) is None: continue - smart_assert_equal(getattr(actual, field.name), getattr(expected, field.name), path=f"{path}.{field.name}") + smart_assert_equal(getattr(actual, name), getattr(expected, name), path=f"{path}.{name}") return if isinstance(actual, pd.Series): try: @@ -60,6 +60,10 @@ def smart_assert_equal(actual, expected, path=""): slow = pytest.mark.slow +def pydantic_v2_not_supported(reason): + return pytest.mark.skipif(True, reason=reason) + + def load_all_subtypes(base_class): classpaths = [ cp for (base, _), cp in TYPE_ALIASES.items() if isinstance(base, type) and issubclass(base, base_class) diff --git a/tests/descriptors/test_serialization.py b/tests/descriptors/test_serialization.py index d6d9a61d98..d8c1125fff 100644 --- a/tests/descriptors/test_serialization.py +++ b/tests/descriptors/test_serialization.py @@ -1,6 +1,6 @@ import pytest +from pydantic import parse_obj_as -from evidently._pydantic_compat import parse_obj_as from evidently.core.datasets import FeatureDescriptor from evidently.descriptors import BeginsWith from evidently.descriptors import BERTScore diff --git a/tests/features/test_multicolumn.py b/tests/features/test_multicolumn.py index a4d69fccb4..4b2729a23a 100644 --- a/tests/features/test_multicolumn.py +++ b/tests/features/test_multicolumn.py @@ -1,9 +1,10 @@ +from typing import ClassVar from typing import List from typing import Optional import pandas as pd +from pydantic import PrivateAttr -from evidently._pydantic_compat import PrivateAttr from evidently.legacy.base_metric import ColumnName from evidently.legacy.core import ColumnType from evidently.legacy.features.feature_generator import FeatureGenerator @@ -15,8 +16,7 @@ class MultiColumnFeature(GeneratedFeatures): - class Config: - alias_required = False + __alias_required__: ClassVar[bool] = False source_column: str _called_count: int = PrivateAttr(0) diff --git a/tests/future/descriptors/test_conditions.py b/tests/future/descriptors/test_conditions.py index 6da089f65f..e105982c81 100644 --- a/tests/future/descriptors/test_conditions.py +++ b/tests/future/descriptors/test_conditions.py @@ -5,8 +5,8 @@ import pandas as pd import pytest +from pydantic import parse_obj_as -from evidently._pydantic_compat import parse_obj_as from evidently.core.datasets import ColumnCondition from evidently.core.datasets import ColumnTest from evidently.core.datasets import Dataset @@ -22,37 +22,37 @@ all_conditions: List[Tuple[ColumnCondition, pd.Series, str, pd.Series]] = [ ( - GreaterEqualColumnCondition(threshold=2), + GreaterEqualColumnCondition(threshold=2.0), pd.Series([1, 2, 3], name="input"), "input: greater or equal to 2.0", pd.Series([False, True, True]), ), ( - GreaterColumnCondition(threshold=2), + GreaterColumnCondition(threshold=2.0), pd.Series([1, 2, 3], name="input"), "input greater than 2.0", pd.Series([False, False, True]), ), ( - LessEqualColumnCondition(threshold=2), + LessEqualColumnCondition(threshold=2.0), pd.Series([1, 2, 3], name="input"), "input: less or equal to 2.0", pd.Series([True, True, False]), ), ( - LessColumnCondition(threshold=2), + LessColumnCondition(threshold=2.0), pd.Series([1, 2, 3], name="input"), "input: less than 2.0", pd.Series([True, False, False]), ), ( - EqualsColumnCondition(expected=2), + EqualsColumnCondition(expected=2.0), pd.Series([1, 2, 3], name="input"), "input: equals 2", pd.Series([False, True, False]), ), ( - NotEqualsColumnCondition(expected=2), + NotEqualsColumnCondition(expected=2.0), pd.Series([1, 2, 3], name="input"), "input not equals 2", pd.Series([True, False, True]), diff --git a/tests/future/descriptors/test_descriptors.py b/tests/future/descriptors/test_descriptors.py index cd2e8e54b6..886882f081 100644 --- a/tests/future/descriptors/test_descriptors.py +++ b/tests/future/descriptors/test_descriptors.py @@ -9,9 +9,9 @@ import pandas as pd import pytest +from pydantic import parse_obj_as from evidently import ColumnType -from evidently._pydantic_compat import parse_obj_as from evidently.core.datasets import ColumnTest from evidently.core.datasets import Dataset from evidently.core.datasets import DatasetColumn diff --git a/tests/future/descriptors/test_feature_descriptors.py b/tests/future/descriptors/test_feature_descriptors.py index 8628902fbb..f404245705 100644 --- a/tests/future/descriptors/test_feature_descriptors.py +++ b/tests/future/descriptors/test_feature_descriptors.py @@ -1,10 +1,11 @@ import json +from typing import ClassVar from typing import List from typing import Optional import pandas as pd +from pydantic import parse_obj_as -from evidently._pydantic_compat import parse_obj_as from evidently.core.datasets import Dataset from evidently.core.datasets import Descriptor from evidently.core.datasets import FeatureDescriptor @@ -16,8 +17,7 @@ class MockGeneratedFeature(GeneratedFeatures): - class Config: - type_alias = "mock_generated_feature" + __type_alias__: ClassVar[Optional[str]] = "mock_generated_feature" column: str field: str diff --git a/tests/future/descriptors/test_text_match.py b/tests/future/descriptors/test_text_match.py index 761f511ae8..0a30fe0305 100644 --- a/tests/future/descriptors/test_text_match.py +++ b/tests/future/descriptors/test_text_match.py @@ -259,7 +259,7 @@ def test_regex_multiple_patterns_error(sample_dataset): def test_invalid_match_type(): - with pytest.raises(ValueError, match=".*match_type\n unexpected value.*"): + with pytest.raises(ValueError, match=".*match_type\n Input should be.*"): TextMatch( column_name="description", match_items=["urgent"], diff --git a/tests/future/metrics/test_test_fields.py b/tests/future/metrics/test_test_fields.py index ca3a3a96c7..10f284b853 100644 --- a/tests/future/metrics/test_test_fields.py +++ b/tests/future/metrics/test_test_fields.py @@ -11,11 +11,11 @@ from typing import Union import pytest +from pydantic.fields import FieldInfo from typing_inspect import get_origin from evidently import Dataset from evidently import Report -from evidently._pydantic_compat import ModelField from evidently.core.metric_types import ColumnMetric from evidently.core.metric_types import DataframeMetric from evidently.core.metric_types import Metric @@ -67,9 +67,9 @@ load_all_subtypes(MetricTest) -def iter_type_test_fields(metric_type: Type[Metric]) -> Iterable[Tuple[str, ModelField]]: +def iter_type_test_fields(metric_type: Type[Metric]) -> Iterable[Tuple[str, FieldInfo]]: for field_name, field in metric_type.__fields__.items(): - if not _is_test_field(field): + if not _is_test_field(field_name, field): continue yield field_name, field @@ -223,10 +223,10 @@ def _fmt(tp) -> str: assert len(missing_tests) == 0, "Missing tests for metric fields: {}".format(format_missing) -def _is_test_field(field: ModelField) -> bool: - if field.outer_type_ is bool: +def _is_test_field(field_name: str, field: FieldInfo) -> bool: + if field.annotation is bool: return False - return "tests" in field.name + return "tests" in field_name def _make_id(tp): diff --git a/tests/future/presets/test_serialization.py b/tests/future/presets/test_serialization.py index b47837fd42..ffcc744921 100644 --- a/tests/future/presets/test_serialization.py +++ b/tests/future/presets/test_serialization.py @@ -3,8 +3,8 @@ from typing import List import pytest +from pydantic import parse_obj_as -from evidently._pydantic_compat import parse_obj_as from evidently.core.container import MetricContainer from evidently.core.datasets import TestSummaryInfo from evidently.generators import ColumnMetricGenerator diff --git a/tests/future/presets/test_test_fields.py b/tests/future/presets/test_test_fields.py index e59bf820ed..b8b386aeb4 100644 --- a/tests/future/presets/test_test_fields.py +++ b/tests/future/presets/test_test_fields.py @@ -12,11 +12,11 @@ import pandas as pd import pytest +from pydantic.fields import FieldInfo from evidently import BinaryClassification from evidently import DataDefinition from evidently import Dataset -from evidently._pydantic_compat import ModelField from evidently.core.container import MetricContainer from evidently.core.metric_types import MeanStdMetric from evidently.core.metric_types import MeanStdMetricTests @@ -245,17 +245,17 @@ def _fmt(fields): assert len(missing_test_fields) == 0, "Missing tests for preset fields: {}".format(format_missing) -def _is_test_field(field: ModelField) -> bool: - if field.outer_type_ is bool: +def _is_test_field(field_name: str, field: FieldInfo) -> bool: + if field.annotation is bool: return False - return "tests" in field.name + return "tests" in field_name def _get_test_field_instance( - field: ModelField, check: Union[GenericTest, MetricTest], preset_type: Type[MetricContainer] + field: FieldInfo, check: Union[GenericTest, MetricTest], preset_type: Type[MetricContainer] ): - if get_origin(field.outer_type_) == dict: - if field.type_ is ValueStatsTests: + if get_origin(field.annotation) == dict: + if field.annotation is ValueStatsTests: col = "text_length" return { col: ValueStatsTests( @@ -268,16 +268,16 @@ def _get_test_field_instance( ) } return {"a": [check]} - if get_origin(field.outer_type_) == list: + if get_origin(field.annotation) == list: return [check] - if field.outer_type_ is MeanStdMetricTests: + if field.annotation is MeanStdMetricTests: return MeanStdMetricTests(mean=[check], std=[check]) - return NotImplementedError(f"Not implemented for {field.outer_type_}") + return NotImplementedError(f"Not implemented for {field.annotation}") -def iter_type_test_fields(preset_type: Type[MetricContainer]) -> Iterable[Tuple[str, ModelField]]: +def iter_type_test_fields(preset_type: Type[MetricContainer]) -> Iterable[Tuple[str, FieldInfo]]: for field_name, field in preset_type.__fields__.items(): - if not _is_test_field(field): + if not _is_test_field(field_name, field): continue yield field_name, field diff --git a/tests/future/test_data_definition.py b/tests/future/test_data_definition.py index 05f1335910..a74d3c48e2 100644 --- a/tests/future/test_data_definition.py +++ b/tests/future/test_data_definition.py @@ -4,8 +4,8 @@ import pandas as pd import pytest +from pydantic import parse_obj_as -from evidently._pydantic_compat import parse_obj_as from evidently.core.datasets import DEFAULT_TRACE_LINK_COLUMN from evidently.core.datasets import DataDefinition from evidently.core.datasets import Dataset diff --git a/tests/future/test_ui/conftest.py b/tests/future/test_ui/conftest.py index 16ed61c258..5e972a0ff7 100644 --- a/tests/future/test_ui/conftest.py +++ b/tests/future/test_ui/conftest.py @@ -3,10 +3,10 @@ import pytest from litestar.testing import TestClient +from pydantic import BaseModel from sqlalchemy import create_engine from sqlalchemy.pool import NullPool -from evidently._pydantic_compat import BaseModel from evidently.legacy.core import new_id from evidently.legacy.utils import NumpyEncoder from evidently.ui.service.app import create_app diff --git a/tests/metrics/data_quality/test_column_value_list_metric.py b/tests/metrics/data_quality/test_column_value_list_metric.py index 57ddecab90..76a3d22921 100644 --- a/tests/metrics/data_quality/test_column_value_list_metric.py +++ b/tests/metrics/data_quality/test_column_value_list_metric.py @@ -4,8 +4,8 @@ import numpy as np import pandas as pd import pytest +from pydantic import parse_obj_as -from evidently._pydantic_compat import parse_obj_as from evidently.legacy.metrics import ColumnValueListMetric from evidently.legacy.metrics.data_quality.column_value_list_metric import ColumnValueListMetricResult from evidently.legacy.metrics.data_quality.column_value_list_metric import ValueListStat diff --git a/tests/metrics/recsys/test_precision_top_k.py b/tests/metrics/recsys/test_precision_top_k.py index 76fe00948a..517c673406 100644 --- a/tests/metrics/recsys/test_precision_top_k.py +++ b/tests/metrics/recsys/test_precision_top_k.py @@ -2,8 +2,8 @@ import numpy as np import pandas as pd +from pydantic import parse_obj_as -from evidently._pydantic_compat import parse_obj_as from evidently.legacy.base_metric import MetricResult from evidently.legacy.metrics import PrecisionTopKMetric from evidently.legacy.metrics.recsys.base_top_k import TopKMetricResult diff --git a/tests/metrics/test_base_metric.py b/tests/metrics/test_base_metric.py index 7725b3e9d7..1772af033a 100644 --- a/tests/metrics/test_base_metric.py +++ b/tests/metrics/test_base_metric.py @@ -49,8 +49,7 @@ def test_metric_generator(): class SimpleMetric(Metric[int]): - class Config: - alias_required = False + __alias_required__: ClassVar[bool] = False column_name: ColumnName @@ -63,8 +62,7 @@ def calculate(self, data: InputData) -> int: class SimpleMetric2(Metric[int]): - class Config: - alias_required = False + __alias_required__: ClassVar[bool] = False column_name: ColumnName @@ -77,8 +75,7 @@ def calculate(self, data: InputData) -> int: class SimpleMetricWithFeatures(Metric[int]): - class Config: - alias_required = False + __alias_required__: ClassVar[bool] = False column_name: str _feature: Optional[GeneratedFeature] @@ -102,8 +99,7 @@ def required_features(self, data_definition: DataDefinition): class MetricWithAllTextFeatures(Metric[Dict[str, int]]): - class Config: - alias_required = False + __alias_required__: ClassVar[bool] = False _features: Dict[str, "LengthFeature"] @@ -119,8 +115,7 @@ def required_features(self, data_definition: DataDefinition): class SimpleGeneratedFeature(GeneratedFeature): - class Config: - alias_required = False + __alias_required__: ClassVar[bool] = False __feature_type__: ClassVar = ColumnType.Numerical column_name: str @@ -134,12 +129,11 @@ def generate_feature(self, data: pd.DataFrame, data_definition: DataDefinition) return pd.DataFrame(dict([(self.column_name, data[self.column_name] * 2)])) def _as_column(self) -> ColumnName: - return self._create_column(subcolumn=self.column_name, default_display_name="SGF: {self.column_name}") + return self._create_column(subcolumn=self.column_name, default_display_name=f"SGF: {self.column_name}") class LengthFeature(GeneratedFeature): - class Config: - alias_required = False + __alias_required__: ClassVar[bool] = False __feature_type__: ClassVar = ColumnType.Numerical column_name: str @@ -233,8 +227,7 @@ class MyOption(Option): field: str class MockMetric(Metric[MetricResult]): - class Config: - alias_required = False + __alias_required__: ClassVar[bool] = False def calculate(self, data: InputData): return MetricResult() @@ -256,8 +249,7 @@ def get_options_fingerprint(self) -> FingerprintPart: return get_value_fingerprint(self.options.get(MyOption).field) class MockMetricWithOption(UsesMyOptionMixin, Metric[MetricResult]): - class Config: - alias_required = False + __alias_required__: ClassVar[bool] = False def calculate(self, data: InputData): return MetricResult() diff --git a/tests/multitest/conftest.py b/tests/multitest/conftest.py index 5e0fc4d72c..4407ec83d4 100644 --- a/tests/multitest/conftest.py +++ b/tests/multitest/conftest.py @@ -6,19 +6,25 @@ from inspect import isabstract from typing import Any from typing import Callable +from typing import ClassVar from typing import Dict from typing import Optional from typing import Type from typing import TypeVar from typing import Union +from typing import get_args +from typing import get_origin + +from pydantic import BaseModel +from pydantic import model_validator import evidently -from evidently._pydantic_compat import BaseModel from evidently.legacy.base_metric import Metric from evidently.legacy.base_metric import MetricResult from evidently.legacy.report import Report from evidently.legacy.utils.types import ApproxValue from evidently.pydantic_utils import PolymorphicModel +from evidently.pydantic_utils import get_field_inner_type from tests.conftest import smart_assert_equal @@ -106,19 +112,25 @@ def find_all_subclasses( T = TypeVar("T", bound=BaseModel) -def make_approx_type(cls: Type[T], ignore_not_set: bool = False) -> Type[T]: - class ApproxFields(cls): - class Config: - alias_required = False +def make_approx_type(original_cls: Type[T], ignore_not_set: bool = False) -> Type[T]: + optional = False + if get_origin(original_cls) is Optional: + original_cls = get_args(original_cls)[0] + optional = True + + class ApproxFields(original_cls): + __alias_required__: ClassVar[bool] = False + if isinstance(original_cls, type) and issubclass(original_cls, PolymorphicModel): + __type_alias__: ClassVar[str] = original_cls.__get_type__() __ignore_not_set__ = ignore_not_set __annotations__ = { - k: Union[ApproxValue, f.type_] - if not isinstance(f.type_, type) or not issubclass(f.type_, BaseModel) - else make_approx_type(f.type_) - for k, f in cls.__fields__.items() + k: Union[ApproxValue, f.annotation] + if not isinstance(get_field_inner_type(f), type) or not issubclass(get_field_inner_type(f), BaseModel) + else make_approx_type(get_field_inner_type(f)) + for k, f in original_cls.__fields__.items() } - locals().update({k: f.default for k, f in cls.__fields__.items()}) + locals().update({k: f.default for k, f in original_cls.__fields__.items()}) def __eq__(self, other): if ignore_not_set: @@ -127,8 +139,14 @@ def __eq__(self, other): return d == d2 return super().__eq__(other) - if issubclass(cls, PolymorphicModel): - ApproxFields.__fields__["type"].default = cls.__fields__["type"].default + @model_validator(mode="before") + def allow_parent(cls, value): + if isinstance(value, original_cls): + value = value.model_dump() + return value + + if issubclass(original_cls, PolymorphicModel): + ApproxFields.__fields__["type"].default = original_cls.__fields__["type"].default - ApproxFields.__name__ = f"Approx{cls.__name__}" - return ApproxFields + ApproxFields.__name__ = f"Approx{original_cls.__name__}" + return ApproxFields if not optional else Optional[ApproxFields] diff --git a/tests/multitest/metrics/test_all.py b/tests/multitest/metrics/test_all.py index 7f75850a02..9aec917232 100644 --- a/tests/multitest/metrics/test_all.py +++ b/tests/multitest/metrics/test_all.py @@ -22,7 +22,7 @@ def _check_dataframe(report: Report, metric: Metric): - if not metric.__class__.result_type().__config__.pd_include: + if not getattr(metric.__class__.result_type(), "__pd_include__", True): # skipping not supported return df = report.as_dataframe() diff --git a/tests/report/test_report.py b/tests/report/test_report.py index 1200431f8e..23f53b88ff 100644 --- a/tests/report/test_report.py +++ b/tests/report/test_report.py @@ -1,4 +1,5 @@ import json +from typing import ClassVar from typing import List import pandas as pd @@ -14,9 +15,8 @@ class MockMetricResult(MetricResult): - class Config: - alias_required = False - dict_exclude_fields = {"series"} + __alias_required__: ClassVar[bool] = False + __dict_exclude_fields__: ClassVar[set] = {"series"} value: str series: pd.Series @@ -24,8 +24,7 @@ class Config: class MockMetric(Metric[MockMetricResult]): - class Config: - alias_required = False + __alias_required__: ClassVar[bool] = False def calculate(self, data: InputData) -> MockMetricResult: return MockMetricResult(value="a", series=pd.Series([0]), distribution=Distribution(x=[1, 1], y=[0, 0])) diff --git a/tests/report/test_report_profile.py b/tests/report/test_report_profile.py index 2b517ddce6..c70a3c65df 100644 --- a/tests/report/test_report_profile.py +++ b/tests/report/test_report_profile.py @@ -27,6 +27,7 @@ def test_report_loading(data): report = Report(metrics=[ClassificationPreset(probas_threshold=0.7)]) report.run(reference_data=ref, current_data=curr) + report._inner_suite.raise_for_error() report.save("profile.json") diff --git a/tests/test_core.py b/tests/test_core.py index eebbe2d77d..3060f4db7a 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,4 +1,7 @@ +from typing import ClassVar +from typing import Dict from typing import List +from typing import Set from typing import Union from evidently.legacy.core import BaseResult @@ -7,9 +10,8 @@ class A(BaseResult): - class Config: - field_tags = {"f1": {IncludeTags.Render}} - tags = {IncludeTags.Current} + __field_tags__: ClassVar[Dict[str, Set[IncludeTags]]] = {"f1": {IncludeTags.Render}} + __tags__: ClassVar[Set[IncludeTags]] = {IncludeTags.Current} f1: str @@ -20,17 +22,15 @@ def test_get_fields_tags(): def test_get_field_tags_subclass(): class B(A): - class Config: - field_tags = {"f1": {IncludeTags.Reference}} - tags = {IncludeTags.Extra} + __field_tags__: ClassVar[Dict[str, Set[IncludeTags]]] = {"f1": {IncludeTags.Reference}} + __tags__: ClassVar[Set[IncludeTags]] = {IncludeTags.Extra} assert get_all_fields_tags(B) == {"f1": {IncludeTags.Reference, IncludeTags.Extra}} def test_get_field_tags_field_add_tag(): class C(BaseResult): - class Config: - field_tags = {"a2": {IncludeTags.Render}} + __field_tags__: ClassVar[Dict[str, Set[IncludeTags]]] = {"a2": {IncludeTags.Render}} a1: A a2: A @@ -40,8 +40,7 @@ class Config: def test_get_field_tags_list_field(): class D(BaseResult): - class Config: - field_tags = {"a2": {IncludeTags.Render}} + __field_tags__: ClassVar[Dict[str, Set[IncludeTags]]] = {"a2": {IncludeTags.Render}} a1: List[A] a2: List[A] @@ -54,8 +53,7 @@ class C(BaseResult): pass class E(BaseResult): - class Config: - field_tags = {"ac2": {IncludeTags.Render}} + __field_tags__: ClassVar[Dict[str, Set[IncludeTags]]] = {"ac2": {IncludeTags.Render}} ac1: Union[A, C] ac2: Union[A, C] @@ -65,8 +63,7 @@ class Config: def test_get_field_tags_remove_tags(): class F(A): - class Config: - field_tags = {"f1": set()} - tags = set() + __field_tags__: ClassVar[Dict[str, Set[IncludeTags]]] = {"f1": set()} + __tags__: ClassVar[Set[IncludeTags]] = set() assert get_all_fields_tags(F) == {"f1": set()} diff --git a/tests/test_metric_results.py b/tests/test_metric_results.py index 28fb5d0e5d..aad29119fa 100644 --- a/tests/test_metric_results.py +++ b/tests/test_metric_results.py @@ -2,19 +2,21 @@ import os from enum import Enum from importlib import import_module +from typing import ClassVar from typing import Dict from typing import List from typing import Set from typing import Type import pytest +from pydantic import BaseModel +from pydantic import Field +from pydantic import TypeAdapter import evidently -from evidently._pydantic_compat import BaseModel -from evidently._pydantic_compat import Field -from evidently._pydantic_compat import parse_obj_as from evidently.legacy.base_metric import MetricResult from evidently.legacy.tests.base_test import EnumValueMixin +from tests.conftest import pydantic_v2_not_supported @pytest.fixture @@ -37,7 +39,7 @@ def all_metric_results(): def test_metric_result_fields_config(all_metric_results: Set[Type[MetricResult]]): errors = [] for cls in all_metric_results: - field_names = set(cls.__fields__) + field_names = set(cls.model_fields) for config_field in ( "pd_name_mapping", "dict_include_fields", @@ -45,7 +47,7 @@ def test_metric_result_fields_config(all_metric_results: Set[Type[MetricResult]] "pd_include_fields", "pd_exclude_fields", ): - field_value = getattr(cls.__config__, config_field) + field_value = getattr(cls, f"__{config_field}__", None) if field_value is None: continue for field_name in field_value: @@ -56,16 +58,14 @@ def test_metric_result_fields_config(all_metric_results: Set[Type[MetricResult]] class SimpleField(MetricResult): - class Config: - alias_required = False + __alias_required__: ClassVar[bool] = False f1: str class ExcludeModel(MetricResult): - class Config: - alias_required = False - dict_exclude_fields = {"simple"} + __dict_exclude_fields__: ClassVar[set] = {"simple"} + __alias_required__: ClassVar[bool] = False simple: SimpleField @@ -76,43 +76,38 @@ def test_default_json(obj: MetricResult, expected): class FieldExclude(MetricResult): - class Config: - alias_required = False - dict_exclude_fields = {"f2"} + __dict_exclude_fields__: ClassVar[set] = {"f2"} + __alias_required__: ClassVar[bool] = False f1: str f2: List[int] class FieldInclude(MetricResult): - class Config: - alias_required = False - dict_include_fields = {"f1"} + __dict_include_fields__: ClassVar[set] = {"f1"} + __alias_required__: ClassVar[bool] = False f1: str f2: List[int] class DictExclude(MetricResult): - class Config: - alias_required = False - dict_include = False + __dict_include__: ClassVar[bool] = False + __alias_required__: ClassVar[bool] = False f1: List[int] f2: List[int] class NestedExclude(MetricResult): - class Config: - alias_required = False + __alias_required__: ClassVar[bool] = False f: str nested: DictExclude class Model(MetricResult): - class Config: - alias_required = False + __alias_required__: ClassVar[bool] = False no: NestedExclude = Field(..., include={"nested": {"f1"}}) fe: FieldExclude @@ -138,18 +133,19 @@ def model(): @pytest.mark.parametrize( "include,exclude,expected", [ - ( - None, - None, - { - "fe": {"f1": "a"}, - "feo": {"f1": "a", "f2": []}, - "fi": {"f1": "a"}, - "deo": {"f1": [], "f2": []}, - "n": {"f": "a"}, - "no": {"nested": {"f1": []}}, - }, - ), + # skip - pydantic v2 does not support Field(include=...) + # ( + # None, + # None, + # { + # "fe": {"f1": "a"}, + # "feo": {"f1": "a", "f2": []}, + # "fi": {"f1": "a"}, + # "deo": {"f1": [], "f2": []}, + # "n": {"f": "a"}, + # "no": {"nested": {"f1": []}}, + # }, + # ), ({"n": {"f"}}, None, {"n": {"f": "a"}}), (None, {"n", "no", "deo", "feo"}, {"fe": {"f1": "a"}, "fi": {"f1": "a"}}), ], @@ -159,8 +155,7 @@ def test_include_exclude(model: Model, include, exclude, expected): class DictModel(MetricResult): - class Config: - alias_required = False + __alias_required__: ClassVar[bool] = False de: Dict[str, DictExclude] deo: Dict[str, DictExclude] = Field(..., include=True) @@ -179,6 +174,7 @@ def dict_model(): return DictModel(de=de, deo=de) +@pydantic_v2_not_supported("pydantic v2 does not support Field(include=...)") @pytest.mark.parametrize( "include,exclude,expected", [ @@ -197,34 +193,29 @@ def test_include_exclude_dict(dict_model: DictModel, include, exclude, expected) def test_polymorphic(): class Parent(MetricResult): - class Config: - alias_required = False + __alias_required__: ClassVar[bool] = False class A(Parent): - class Config: - dict_include_fields = {"f1"} - alias_required = False + __dict_include_fields__: ClassVar[set] = {"f1"} + __alias_required__: ClassVar[bool] = False f1: str f2: str class B(Parent): - class Config: - dict_exclude_fields = {"b"} - alias_required = False + __dict_exclude_fields__: ClassVar[set] = {"b"} + __alias_required__: ClassVar[bool] = False a: str b: str class PModel(MetricResult): - class Config: - alias_required = False + __alias_required__: ClassVar[bool] = False vals: Dict[str, Parent] - assert PModel(vals={"a": A(f1="a", f2="b"), "b": B(a="a", b="b")}).get_dict() == { - "vals": {"a": {"f1": "a"}, "b": {"a": "a"}} - } + p_model = PModel(vals={"a": A(f1="a", f2="b"), "b": B(a="a", b="b")}) + assert p_model.get_dict() == {"vals": {"a": {"f1": "a"}, "b": {"a": "a"}}} def test_model_enum(): @@ -237,24 +228,31 @@ class Container(EnumValueMixin, BaseModel): obj = Container(value=MyEnum.A) assert obj.value == MyEnum.A - d = obj.dict() + d = obj.model_dump() assert d == {"value": "a"} - obj2 = parse_obj_as(Container, d) + obj2 = TypeAdapter(Container).validate_python(d) assert obj2.value == MyEnum.A assert obj2 == obj +def test_skip_type(): + class A(MetricResult): + __alias_required__: ClassVar[bool] = False + field: str + + res = A(field="aaa").get_dict() + assert "type" not in res + + def test_model_list(): class SimpleField(MetricResult): - class Config: - alias_required = False + __alias_required__: ClassVar[bool] = False field: str field2: str class Container(MetricResult): - class Config: - alias_required = False + __alias_required__: ClassVar[bool] = False field: List[SimpleField] diff --git a/tests/test_pydantic_aliases.py b/tests/test_pydantic_aliases.py index e88e524cbe..a5142f3767 100644 --- a/tests/test_pydantic_aliases.py +++ b/tests/test_pydantic_aliases.py @@ -9,9 +9,9 @@ from typing import TypeVar import pytest +from pydantic.v1.utils import import_string import evidently -from evidently._pydantic_compat import import_string from evidently.core import registries from evidently.core.container import MetricContainer from evidently.core.datasets import ColumnCondition @@ -88,6 +88,9 @@ def find_all_subclasses( continue module = import_module(mod_name) for key, value in module.__dict__.items(): + if "[" in key: + # pydantic auto-generated generic subclasses + continue if isinstance(value, type) and value is not base and issubclass(value, base): if not isabstract(value) or include_abstract: classes.add(value) @@ -145,7 +148,7 @@ def test_all_aliases_registered(): not_registered = [] for cls in find_all_subclasses(PolymorphicModel, include_abstract=True): - if cls.__is_base_type__(): + if cls.__get_is_base_type__(): continue classpath = cls.__get_classpath__() typename = cls.__get_type__() @@ -235,7 +238,7 @@ def test_all_aliases_correct(): continue for base_class, base_type in base_class_type_mapping.items(): if issubclass(cls, base_class): - # alias = getattr(cls.__config__, "type_alias") + # alias = getattr(cls, "__type_alias__", None) alias = cls.__get_type__() assert alias is not None, f"{cls.__name__} has no alias ({alias})" assert alias == f"evidently:{base_type}:{cls.__name__}", f"wrong alias for {cls.__name__}" diff --git a/tests/test_pydantic_compat.py b/tests/test_pydantic_compat.py deleted file mode 100644 index 72a417db9f..0000000000 --- a/tests/test_pydantic_compat.py +++ /dev/null @@ -1,40 +0,0 @@ -import ast -import glob -import os.path -from _ast import Import -from _ast import ImportFrom -from typing import Any - -import pytest - -import evidently - -IMPORT_EXCEPTIONS = ["evidently.ui.config"] -IMPORT_EXCEPTIONS = [os.path.join(*i.split(".")) + ".py" for i in IMPORT_EXCEPTIONS] - - -@pytest.mark.parametrize( - "sourcefile", - glob.glob( - os.path.join(os.path.dirname(os.path.dirname(evidently.__file__)), "**", "*.py"), - recursive=True, - ), -) -def test_all_imports_from_compat(sourcefile): - if sourcefile.endswith("_pydantic_compat.py"): - return - with open(sourcefile, encoding="utf8") as f: - tree = ast.parse(f.read(), filename=sourcefile) - - class Visitor(ast.NodeVisitor): - def visit_ImportFrom(self, node: ImportFrom) -> Any: - if node.module is not None and node.module.startswith("pydantic"): - raise Exception(f"{sourcefile}:{node.lineno}") - - def visit_Import(self, node: Import) -> Any: - if any(sourcefile.endswith(p) for p in IMPORT_EXCEPTIONS): - return - if any("pydantic" == name.name for name in node.names): - raise Exception(f"{sourcefile}:{node.lineno}") - - Visitor().visit(tree) diff --git a/tests/test_suite/test_test_suite.py b/tests/test_suite/test_test_suite.py index b9e407e1a9..537ee67596 100644 --- a/tests/test_suite/test_test_suite.py +++ b/tests/test_suite/test_test_suite.py @@ -1,4 +1,5 @@ import json +from typing import ClassVar from typing import Dict import numpy as np @@ -47,8 +48,7 @@ class ErrorTest(Test): - class Config: - alias_required = False + __alias_required__: ClassVar[bool] = False name = "Error Test" group = "example" diff --git a/tests/ui/conftest.py b/tests/ui/conftest.py index 4c2251407d..5a40142c62 100644 --- a/tests/ui/conftest.py +++ b/tests/ui/conftest.py @@ -7,8 +7,9 @@ from litestar import get from litestar.datastructures import State from litestar.testing import TestClient +from pydantic import BaseModel +from pydantic import ConfigDict -from evidently._pydantic_compat import BaseModel from evidently.legacy.ui.app import create_app from evidently.legacy.ui.base import Project from evidently.legacy.ui.components.base import Component @@ -26,8 +27,7 @@ class TestsSetupComponent(Component): - class Config: - arbitrary_types_allowed = True + model_config = ConfigDict(arbitrary_types_allowed=True) app: Optional[Litestar] = None diff --git a/tests/ui/test_app.py b/tests/ui/test_app.py index bd09e794aa..93d8c2e935 100644 --- a/tests/ui/test_app.py +++ b/tests/ui/test_app.py @@ -3,13 +3,14 @@ import os import time from copy import deepcopy +from typing import ClassVar from typing import List import pytest from litestar.testing import TestClient +from pydantic import parse_obj_as import evidently -from evidently._pydantic_compat import parse_obj_as from evidently.legacy.base_metric import InputData from evidently.legacy.base_metric import Metric from evidently.legacy.base_metric import MetricResult @@ -123,8 +124,7 @@ async def test_delete_project(test_client: TestClient, project_manager: ProjectM class MockMetricResult(MetricResult): - class Config: - alias_required = False + __alias_required__: ClassVar[bool] = False value: float @@ -134,8 +134,7 @@ def create(cls, value: float): class MockMetric(Metric[MockMetricResult]): - class Config: - alias_required = False + __alias_required__: ClassVar[bool] = False def calculate(self, data: InputData) -> MockMetricResult: return MockMetricResult.create(1) diff --git a/tests/ui/test_dashboards.py b/tests/ui/test_dashboards.py index e520eb7fa9..41203d4250 100644 --- a/tests/ui/test_dashboards.py +++ b/tests/ui/test_dashboards.py @@ -1,9 +1,10 @@ import json +from typing import ClassVar from typing import Dict import pytest +from pydantic import TypeAdapter -from evidently._pydantic_compat import parse_obj_as from evidently.legacy.base_metric import ColumnName from evidently.legacy.base_metric import InputData from evidently.legacy.base_metric import Metric @@ -17,15 +18,13 @@ class A(MetricResult): - class Config: - alias_required = False + __alias_required__: ClassVar[bool] = False f: str class B(MetricResult): - class Config: - alias_required = False + __alias_required__: ClassVar[bool] = False f: Dict[str, A] f1: A @@ -42,16 +41,15 @@ def test_getattr_nested(obj, path: str, value): def test_panel_value_metric_args_ser(): pv = PanelValue(field_path="", metric_args={"col": OOV(display_name="OOV").for_column("Review_Text")}) - pl = json.dumps(pv.dict()) - pv2 = parse_obj_as(PanelValue, json.loads(pl)) + pl = json.dumps(pv.model_dump()) + pv2 = TypeAdapter(PanelValue).validate_python(json.loads(pl)) assert pv2 == pv def test_panel_value_methic_hash_filter(): class MyMetric(Metric[A]): - class Config: - alias_required = False + __alias_required__: ClassVar[bool] = False arg: str @@ -68,14 +66,12 @@ def calculate(self, data: InputData) -> TResult: def test_metric_hover_template(): class Nested(EvidentlyBaseModel): - class Config: - alias_required = False + __alias_required__: ClassVar[bool] = False f: str class MyMetric(Metric[A]): - class Config: - alias_required = False + __alias_required__: ClassVar[bool] = False arg: str n: Nested @@ -108,8 +104,7 @@ def calculate(self, data: InputData) -> TResult: def test_metric_hover_template_column_name(): class MyMetric(Metric[A]): - class Config: - alias_required = False + __alias_required__: ClassVar[bool] = False column_name: ColumnName diff --git a/tests/utils/test_pydantic_utils.py b/tests/utils/test_pydantic_utils.py index 3c26b1bb52..6e69ddc28f 100644 --- a/tests/utils/test_pydantic_utils.py +++ b/tests/utils/test_pydantic_utils.py @@ -1,12 +1,14 @@ from abc import ABC +from typing import ClassVar from typing import Dict from typing import Optional +from typing import Set from typing import Union import pytest +from pydantic import TypeAdapter +from pydantic import ValidationError -from evidently._pydantic_compat import ValidationError -from evidently._pydantic_compat import parse_obj_as from evidently.legacy.base_metric import Metric from evidently.legacy.base_metric import MetricResult from evidently.legacy.core import IncludeTags @@ -18,22 +20,19 @@ class MockMetricResultField(MetricResult): - class Config: - alias_required = False + __alias_required__: ClassVar[bool] = False nested_field: str class ExtendedMockMetricResultField(MockMetricResultField): - class Config: - alias_required = False + __alias_required__: ClassVar[bool] = False additional_field: str class MockMetricResult(MetricResult): - class Config: - alias_required = False + __alias_required__: ClassVar[bool] = False field1: MockMetricResultField field2: int @@ -41,8 +40,7 @@ class Config: def _metric_with_result(result: MetricResult): class MockMetric(Metric): - class Config: - alias_required = False + __alias_required__: ClassVar[bool] = False def get_result(self): return result @@ -85,8 +83,7 @@ def test_field_path(): class MockMetricResultWithDict(MetricResult): - class Config: - alias_required = False + __alias_required__: ClassVar[bool] = False d: Dict[str, MockMetricResultField] @@ -128,41 +125,37 @@ def test_field_path_with_dict(): def test_not_allowed_prefix(): class SomeModel(PolymorphicModel): - class Config: - alias_required = False + __alias_required__: ClassVar[bool] = False with pytest.raises(ValueError): - parse_obj_as(SomeModel, {"type": "external.Class"}) + TypeAdapter(SomeModel).validate_python({"type": "external.Class"}) def test_type_alias(): class SomeModel(PolymorphicModel): - class Config: - type_alias = "somemodel" - alias_required = False + __type_alias__: ClassVar[Optional[str]] = "somemodel" + __alias_required__: ClassVar[bool] = False class SomeModelSubclass(SomeModel): pass class SomeOtherSubclass(SomeModel): - class Config: - type_alias = "othersubclass" + __type_alias__: ClassVar[Optional[str]] = "othersubclass" - obj = parse_obj_as(SomeModel, {"type": "somemodel"}) + obj = TypeAdapter(SomeModel).validate_python({"type": "somemodel"}) assert obj.__class__ == SomeModel - obj = parse_obj_as(SomeModel, {"type": SomeModelSubclass.__get_type__()}) + obj = TypeAdapter(SomeModel).validate_python({"type": SomeModelSubclass.__get_type__()}) assert obj.__class__ == SomeModelSubclass - obj = parse_obj_as(SomeModel, {"type": "othersubclass"}) + obj = TypeAdapter(SomeModel).validate_python({"type": "othersubclass"}) assert obj.__class__ == SomeOtherSubclass def test_include_exclude(): class SomeModel(MetricResult): - class Config: - field_tags = {"f1": {IncludeTags.Render}} - alias_required = False + __field_tags__: ClassVar[Dict[str, Set[IncludeTags]]] = {"f1": {IncludeTags.Render}} + __alias_required__: ClassVar[bool] = False f1: str f2: str @@ -172,15 +165,13 @@ class Config: # assert SomeModel.fields.list_nested_fields(include={IncludeTags.Render}) == ["f1"] class SomeNestedModel(MetricResult): - class Config: - tags = {IncludeTags.Render} - alias_required = False + __tags__: ClassVar[Set[IncludeTags]] = {IncludeTags.Render} + __alias_required__: ClassVar[bool] = False f1: str class SomeOtherModel(MetricResult): - class Config: - alias_required = False + __alias_required__: ClassVar[bool] = False f1: str f2: SomeNestedModel @@ -195,9 +186,8 @@ class Config: def test_get_field_tags(): class SomeModel(MetricResult): - class Config: - field_tags = {"f1": {IncludeTags.Render}} - alias_required = False + __field_tags__: ClassVar[Dict[str, Set[IncludeTags]]] = {"f1": {IncludeTags.Render}} + __alias_required__: ClassVar[bool] = False f1: str f2: str @@ -207,15 +197,13 @@ class Config: assert SomeModel.fields.get_field_tags(["f2"]) == set() class SomeNestedModel(MetricResult): - class Config: - tags = {IncludeTags.Render} - alias_required = False + __tags__: ClassVar[Set[IncludeTags]] = {IncludeTags.Render} + __alias_required__: ClassVar[bool] = False f1: str class SomeOtherModel(MetricResult): - class Config: - alias_required = False + __alias_required__: ClassVar[bool] = False f1: str f2: SomeNestedModel @@ -232,9 +220,8 @@ class Config: def test_list_with_tags(): class SomeModel(MetricResult): - class Config: - field_tags = {"f1": {IncludeTags.Render}} - alias_required = False + __field_tags__: ClassVar[Dict[str, Set[IncludeTags]]] = {"f1": {IncludeTags.Render}} + __alias_required__: ClassVar[bool] = False f1: str f2: str @@ -246,15 +233,13 @@ class Config: ] class SomeNestedModel(MetricResult): - class Config: - tags = {IncludeTags.Render} - alias_required = False + __tags__: ClassVar[Set[IncludeTags]] = {IncludeTags.Render} + __alias_required__: ClassVar[bool] = False f1: str class SomeOtherModel(MetricResult): - class Config: - alias_required = False + __alias_required__: ClassVar[bool] = False f1: str f2: SomeNestedModel @@ -273,16 +258,14 @@ class Config: def test_list_with_tags_with_union(): class A(MetricResult): - class Config: - tags = {IncludeTags.Render} - alias_required = False + __tags__: ClassVar[Set[IncludeTags]] = {IncludeTags.Render} + __alias_required__: ClassVar[bool] = False f1: str class B(MetricResult): - class Config: - tags = {IncludeTags.Render} - alias_required = False + __tags__: ClassVar[Set[IncludeTags]] = {IncludeTags.Render} + __alias_required__: ClassVar[bool] = False f1: str @@ -291,8 +274,7 @@ class Config: assert fp._cls == A class SomeModel(MetricResult): - class Config: - alias_required = False + __alias_required__: ClassVar[bool] = False f2: Union[A, B] f1: str @@ -307,20 +289,17 @@ class Config: def test_get_field_tags_no_overwrite(): class A(MetricResult): - class Config: - field_tags = {"f": {IncludeTags.Current}} - alias_required = False + __field_tags__: ClassVar[Dict[str, Set[IncludeTags]]] = {"f": {IncludeTags.Current}} + __alias_required__: ClassVar[bool] = False f: str class B(A): - class Config: - tags = {IncludeTags.Reference} + __tags__: ClassVar[Set[IncludeTags]] = {IncludeTags.Reference} class C(MetricResult): - class Config: - field_tags = {"f": {IncludeTags.Reference}} - alias_required = False + __field_tags__: ClassVar[Dict[str, Set[IncludeTags]]] = {"f": {IncludeTags.Reference}} + __alias_required__: ClassVar[bool] = False f: A @@ -336,16 +315,14 @@ class Config: def test_fingerprint_add_new_default_field(): class A(EvidentlyBaseModel): - class Config: - alias_required = False + __alias_required__: ClassVar[bool] = False field1: str f1 = A(field1="123").get_fingerprint() class A(EvidentlyBaseModel): - class Config: - alias_required = False + __alias_required__: ClassVar[bool] = False field1: str field2: str = "321" @@ -358,8 +335,7 @@ class Config: def test_fingerprint_reorder_fields(): class A(EvidentlyBaseModel): - class Config: - alias_required = False + __alias_required__: ClassVar[bool] = False field1: str field2: str @@ -367,8 +343,7 @@ class Config: f1 = A(field1="123", field2="321").get_fingerprint() class A(EvidentlyBaseModel): - class Config: - alias_required = False + __alias_required__: ClassVar[bool] = False field2: str field1: str @@ -381,8 +356,7 @@ class Config: def test_fingerprint_default_collision(): class A(EvidentlyBaseModel): - class Config: - alias_required = False + __alias_required__: ClassVar[bool] = False field1: Optional[str] = None field2: Optional[str] = None @@ -392,24 +366,22 @@ class Config: def test_wrong_classpath(): class WrongClassPath(EvidentlyBaseModel): - class Config: - alias_required = False + __alias_required__: ClassVar[bool] = False f: str ALLOWED_TYPE_PREFIXES.append("tests.") a = WrongClassPath(f="asd") - assert parse_obj_as(WrongClassPath, a.dict()) == a - d = a.dict() + assert TypeAdapter(WrongClassPath).validate_python(a.model_dump()) == a + d = a.model_dump() d["type"] += "_" with pytest.raises(ValidationError): - parse_obj_as(WrongClassPath, d) + TypeAdapter(WrongClassPath).validate_python(d) def test_alias_requied(): class RequiredAlias(PolymorphicModel, ABC): - class Config: - alias_required = True + __alias_required__: ClassVar[bool] = True with pytest.raises(ValueError): @@ -417,5 +389,4 @@ class NoAlias(RequiredAlias): pass class Alias(RequiredAlias): - class Config: - type_alias = "alias" + __type_alias__: ClassVar[Optional[str]] = "alias" From d82689818add097b59dbd96ea51614501799b61d Mon Sep 17 00:00:00 2001 From: mike0sv Date: Wed, 18 Feb 2026 01:29:48 +0000 Subject: [PATCH 02/11] fix tests hopefully --- src/evidently/core/metric_types.py | 7 ++ .../descriptors/_custom_descriptors.py | 18 +++-- src/evidently/descriptors/llm_judges.py | 9 ++- src/evidently/generators/column.py | 22 +++++-- src/evidently/legacy/base_metric.py | 27 ++++---- src/evidently/legacy/collector/config.py | 20 +++--- src/evidently/legacy/collector/storage.py | 6 +- src/evidently/legacy/features/llm_judge.py | 9 ++- .../legacy/features/sentiment_feature.py | 9 ++- .../trigger_words_presence_feature.py | 9 ++- .../legacy/features/words_feature.py | 17 +++-- src/evidently/legacy/metric_results.py | 18 ++++- .../classification_dummy_metric.py | 6 +- .../quality_by_feature_table.py | 10 +-- src/evidently/legacy/metrics/custom_metric.py | 10 ++- .../metrics/data_drift/data_drift_table.py | 10 +-- .../data_integrity/column_regexp_metric.py | 6 +- .../dataset_correlations_metric.py | 23 ++++--- .../legacy/metrics/recsys/f_beta_top_k.py | 6 +- .../legacy/metrics/recsys/novelty.py | 6 +- .../legacy/metrics/recsys/serendipity.py | 6 +- .../error_bias_table.py | 10 +-- .../metrics/regression_performance/objects.py | 14 ++-- .../regression_dummy_metric.py | 6 +- src/evidently/legacy/tests/base_test.py | 12 ++-- src/evidently/legacy/ui/base.py | 55 ++++++++-------- src/evidently/legacy/ui/config.py | 12 ++-- src/evidently/legacy/ui/storage/local/base.py | 31 +++++---- src/evidently/llm/datagen/base.py | 9 ++- src/evidently/llm/optimization/optimizer.py | 9 ++- src/evidently/llm/optimization/prompts.py | 25 ++++--- src/evidently/llm/rag/index.py | 11 ++-- src/evidently/llm/rag/splitter.py | 11 ++-- src/evidently/llm/utils/templates.py | 23 ++++--- src/evidently/presets/classification.py | 26 ++++---- src/evidently/presets/dataset_stats.py | 15 ++--- src/evidently/presets/regression.py | 16 ++--- src/evidently/pydantic_utils.py | 13 +++- src/evidently/sdk/artifacts.py | 17 +++-- src/evidently/sdk/configs.py | 17 +++-- src/evidently/sdk/prompts.py | 17 +++-- src/evidently/ui/runner/core.py | 65 +++++++++---------- src/evidently/ui/service/base.py | 29 ++++----- src/evidently/ui/service/config.py | 11 ++-- .../ui/service/storage/local/artifacts.py | 8 +-- .../ui/service/storage/local/base.py | 53 ++++++++------- tests/future/descriptors/test_conditions.py | 4 +- tests/future/descriptors/test_descriptors.py | 14 ++-- tests/future/generators/test_generator.py | 4 +- tests/future/metrics/all_metrics_tests.py | 8 +-- tests/future/presets/test_serialization.py | 1 + tests/future/presets/test_test_fields.py | 16 +++-- 52 files changed, 420 insertions(+), 396 deletions(-) diff --git a/src/evidently/core/metric_types.py b/src/evidently/core/metric_types.py index a700a7a43a..a55ddb7a1e 100644 --- a/src/evidently/core/metric_types.py +++ b/src/evidently/core/metric_types.py @@ -30,6 +30,7 @@ from pydantic import Field from pydantic import TypeAdapter from pydantic import field_validator +from pydantic import model_validator from evidently.core.base_types import Label from evidently.legacy.model.dashboard import DashboardInfo @@ -1015,6 +1016,12 @@ class MetricTest(AutoAliasMixin, EvidentlyBaseModel): label_filters: Optional[Dict[str, str]] = None """Optional filters for DataFrame metrics to select specific rows by label values.""" + @model_validator(mode="wrap") + def from_generic_test(self, nxt, info): + if isinstance(self, GenericTest): + return nxt(self.metric) + return nxt(self) + @abstractmethod def to_test(self) -> MetricTestProto: """Convert to a test protocol function. diff --git a/src/evidently/descriptors/_custom_descriptors.py b/src/evidently/descriptors/_custom_descriptors.py index ea6df23aa9..0cc9cf26de 100644 --- a/src/evidently/descriptors/_custom_descriptors.py +++ b/src/evidently/descriptors/_custom_descriptors.py @@ -4,8 +4,6 @@ from typing import Optional from typing import Union -from pydantic import PrivateAttr - from evidently.core.datasets import AnyDescriptorTest from evidently.core.datasets import Dataset from evidently.core.datasets import DatasetColumn @@ -22,7 +20,7 @@ class CustomColumnDescriptor(Descriptor): """Name of the column to process.""" func: str """Function name or callable to apply to column data.""" - _func: Optional[CustomColumnCallable] = PrivateAttr(None) + __func__: Optional[CustomColumnCallable] = None """Internal cached callable.""" def __init__( @@ -40,14 +38,14 @@ def __init__( _func = None self.func = func super().__init__(alias=alias or f"custom_column_descriptor:{func}", tests=tests) - self._func = _func + self.__func__ = _func def generate_data(self, dataset: Dataset, options: Options) -> Union[DatasetColumn, Dict[str, DatasetColumn]]: """Apply custom function to column data.""" - if self._func is None: + if self.__func__ is None: raise ValueError("CustomColumnDescriptor is not configured with callable func") column_data = dataset.column(self.column_name) - return self._func(column_data) + return self.__func__(column_data) def list_input_columns(self) -> Optional[List[str]]: """Return list of required input column names.""" @@ -62,7 +60,7 @@ class CustomDescriptor(Descriptor): func: str """Function name or callable to apply to dataset.""" - _func: Optional[CustomDescriptorCallable] = PrivateAttr(None) + __func__: Optional[CustomDescriptorCallable] = None """Internal cached callable.""" def __init__( @@ -78,10 +76,10 @@ def __init__( _func = None self.func = func super().__init__(alias=alias or f"custom_descriptor:{func}", tests=tests) - self._func = _func + self.__func__ = _func def generate_data(self, dataset: "Dataset", options: Options) -> Union[DatasetColumn, Dict[str, DatasetColumn]]: """Apply custom function to dataset.""" - if self._func is None: + if self.__func__ is None: raise ValueError("CustomDescriptor is not configured with callable func") - return self._func(dataset) + return self.__func__(dataset) diff --git a/src/evidently/descriptors/llm_judges.py b/src/evidently/descriptors/llm_judges.py index 310c223972..c4147435d0 100644 --- a/src/evidently/descriptors/llm_judges.py +++ b/src/evidently/descriptors/llm_judges.py @@ -7,7 +7,6 @@ from typing import cast import pandas as pd -from pydantic import PrivateAttr from evidently import ColumnType from evidently import Dataset @@ -42,7 +41,7 @@ class GenericLLMDescriptor(Descriptor): prompt: PromptContent """Prompt template or messages to send to LLM.""" - _llm_wrapper: Optional[LLMWrapper] = PrivateAttr(None) + __llm_wrapper__: Optional[LLMWrapper] = None """Internal cached LLM wrapper.""" def __init__( @@ -63,9 +62,9 @@ def __init__( def get_llm_wrapper(self, options: Options) -> LLMWrapper: """Get or create LLM wrapper instance.""" - if self._llm_wrapper is None: - self._llm_wrapper = get_llm_wrapper(self.provider, self.model, options) - return self._llm_wrapper + if self.__llm_wrapper__ is None: + self.__llm_wrapper__ = get_llm_wrapper(self.provider, self.model, options) + return self.__llm_wrapper__ def _fmt_messages(self, values: Dict[str, Any]) -> List[LegacyLLMMessage]: """Format prompt messages with column values.""" diff --git a/src/evidently/generators/column.py b/src/evidently/generators/column.py index 7d089ca810..32e5ceb926 100644 --- a/src/evidently/generators/column.py +++ b/src/evidently/generators/column.py @@ -9,7 +9,7 @@ from typing import Type from typing import Union -from pydantic import PrivateAttr +from pydantic import TypeAdapter from pydantic import ValidationError from evidently.core.container import ColumnMetricContainer @@ -44,7 +44,7 @@ class ColumnMetricGenerator(MetricContainer): metric_type_alias: str """Alias string identifying the metric type.""" - _metric_type: Union[Type[ColumnMetric], Type[ColumnMetricContainer]] = PrivateAttr() + __metric_type__: Optional[Union[Type[ColumnMetric], Type[ColumnMetricContainer]]] = None """The metric class to instantiate for each column.""" columns: Optional[List[str]] = None """Optional list of specific column names to generate metrics for.""" @@ -84,9 +84,21 @@ def __init__( self.column_types = column_types if metric_kwargs and kwargs: raise ValueError("only one of metric_kwargs or **kwargs may be specified") - self.metric_kwargs = metric_kwargs or kwargs or {} + self.metric_kwargs = self.validate_metric_kwargs(_metric_type, metric_kwargs or kwargs or {}) super().__init__(include_tests=include_tests) - self._metric_type = _metric_type + self.__metric_type__ = _metric_type + + @classmethod + def validate_metric_kwargs( + cls, metric_type: Union[Type[ColumnMetric], Type[ColumnMetricContainer]], metric_kwargs: Dict[str, Any] + ) -> Dict[str, Any]: + validated = {} + for field_name, field_value in metric_kwargs.items(): + field_info = metric_type.model_fields.get(field_name, None) + if field_info is None: + raise ValueError(f"Metric {metric_type.__name__} does not have field {field_name}") + validated[field_name] = TypeAdapter(field_info.annotation).validate_python(field_value) + return validated def _instantiate_metric(self, column: str) -> MetricOrContainer: """Create a metric instance for a specific column. @@ -97,7 +109,7 @@ def _instantiate_metric(self, column: str) -> MetricOrContainer: Returns: * `Metric` or `MetricContainer` instance. """ - return self._metric_type(column=column, **self.metric_kwargs) + return self.__metric_type__(column=column, **self.metric_kwargs) def generate_metrics(self, context: "Context") -> Sequence[MetricOrContainer]: """Generate metric instances for matching columns. diff --git a/src/evidently/legacy/base_metric.py b/src/evidently/legacy/base_metric.py index a5b821becf..51c33abcef 100644 --- a/src/evidently/legacy/base_metric.py +++ b/src/evidently/legacy/base_metric.py @@ -18,7 +18,6 @@ import pandas as pd from pydantic import ConfigDict from pydantic import Field -from pydantic import PrivateAttr from pydantic._internal._model_construction import ModelMetaclass from evidently.legacy.core import BaseResult @@ -59,15 +58,15 @@ class MetricResult(PolymorphicModel, BaseResult, metaclass=WithFieldsPathMetacla class ErrorResult(BaseResult): model_config = ConfigDict() - _exception: Optional[BaseException] = None # todo: fix serialization of exceptions + __exception__: Optional[BaseException] = None # todo: fix serialization of exceptions def __init__(self, exception: Optional[BaseException]): super().__init__() - self._exception = exception + self.__exception__ = exception @property def exception(self): - return self._exception + return self.__exception__ class DatasetType(Enum): @@ -85,13 +84,13 @@ class ColumnName(EnumValueMixin, EvidentlyBaseModel): name: str display_name: DisplayName dataset: DatasetType - _feature_class: Optional["GeneratedFeatures"] = PrivateAttr(None) + __feature_class__: Optional["GeneratedFeatures"] = None def __init__( self, name: str, display_name: str, dataset: DatasetType, feature_class: Optional["GeneratedFeatures"] = None ): super().__init__(name=name, display_name=display_name, dataset=dataset) - self._feature_class = feature_class + self.__feature_class__ = feature_class def is_main_dataset(self): return self.dataset == DatasetType.MAIN @@ -109,7 +108,7 @@ def from_any(cls, column_name: Union[str, "ColumnName"]): @property def feature_class(self) -> Optional["GeneratedFeatures"]: - return self._feature_class + return self.__feature_class__ def get_fingerprint_parts(self) -> Tuple[FingerprintPart, ...]: return tuple( @@ -246,7 +245,7 @@ class BasePreset(EvidentlyBaseModel): class Metric(WithTestAndMetricDependencies, Generic[TResult], metaclass=WithResultFieldPathMetaclass): __is_base_type__: ClassVar[bool] = True - _context: Optional["Context"] = None + __context__: Optional["Context"] = None options: Optional[Options] = Field(default=None) @@ -273,12 +272,12 @@ def calculate(self, data: InputData) -> TResult: raise NotImplementedError() def set_context(self, context): - self._context = context + self.__context__ = context def get_result(self) -> TResult: - if not hasattr(self, "_context") or self._context is None: + if not hasattr(self, "__context__") or self.__context__ is None: raise ValueError("No context is set") - result = self._context.metric_results.get(self, None) + result = self.__context__.metric_results.get(self, None) if isinstance(result, ErrorResult): raise result.exception if result is None: @@ -288,7 +287,7 @@ def get_result(self) -> TResult: def get_parameters(self) -> Optional[tuple]: attributes = [] for field, value in sorted(self.__dict__.items(), key=lambda x: x[0]): - if field in ["_context"]: + if field in ["__context__"]: continue if isinstance(value, list): attributes.append(tuple(value)) @@ -313,8 +312,8 @@ def required_features(self, data_definition: DataDefinition) -> List["GeneratedF def get_options(self): options = self.options if hasattr(self, "options") else Options() - if self._context is not None: - options = self._context.options.override(options) + if self.__context__ is not None: + options = self.__context__.options.override(options) return options def get_field_fingerprint(self, field: str) -> FingerprintPart: diff --git a/src/evidently/legacy/collector/config.py b/src/evidently/legacy/collector/config.py index ec3149562d..d916857551 100644 --- a/src/evidently/legacy/collector/config.py +++ b/src/evidently/legacy/collector/config.py @@ -144,8 +144,8 @@ class CollectorConfig(Config): is_cloud: Optional[bool] = None # None means autodetect save_datasets: bool = False - _reference: Any = None - _workspace: Optional[WorkspaceView] = None + __reference__: Any = None + __workspace__: Optional[WorkspaceView] = None @property def is_cloud_resolved(self) -> bool: @@ -153,16 +153,16 @@ def is_cloud_resolved(self) -> bool: @property def workspace(self) -> WorkspaceView: - if self._workspace is None: + if self.__workspace__ is None: if self.is_cloud_resolved: if self.api_secret is None: raise ValueError("Please provide token and org_id for CloudWorkspace") - self._workspace = CloudWorkspace(token=self.api_secret, url=self.api_url) + self.__workspace__ = CloudWorkspace(token=self.api_secret, url=self.api_url) else: if self.save_datasets: warnings.warn("'save_datasets' is not supported for self-hosted Evidently UI") - self._workspace = RemoteWorkspace(base_url=self.api_url, secret=self.api_secret) - return self._workspace + self.__workspace__ = RemoteWorkspace(base_url=self.api_url, secret=self.api_secret) + return self.__workspace__ def _read_reference(self): return pd.read_parquet(self.reference_path) @@ -171,12 +171,12 @@ def _read_reference(self): def reference(self): if self.reference_path is None: return None - if self._reference is not None: - return self._reference + if self.__reference__ is not None: + return self.__reference__ if not self.cache_reference: return self._read_reference() - self._reference = self._read_reference() - return self._reference + self.__reference__ = self._read_reference() + return self.__reference__ class CollectorServiceConfig(Config): diff --git a/src/evidently/legacy/collector/storage.py b/src/evidently/legacy/collector/storage.py index 93d259f182..b1461e6eb0 100644 --- a/src/evidently/legacy/collector/storage.py +++ b/src/evidently/legacy/collector/storage.py @@ -46,13 +46,13 @@ def __exit__(self, exc_type, exc_val, exc_tb): class CollectorStorage(PolymorphicModel): __is_base_type__: ClassVar[bool] = True - _locks: Dict[str, Lock] = {} + __locks__: Dict[str, Lock] = {} def lock(self, id: str): - return self._locks[id] + return self.__locks__[id] def init(self, id: str): - self._locks[id] = Lock() + self.__locks__[id] = Lock() def init_all(self, config): for id in config.collectors: diff --git a/src/evidently/legacy/features/llm_judge.py b/src/evidently/legacy/features/llm_judge.py index c0cca6f4a8..f463a80d31 100644 --- a/src/evidently/legacy/features/llm_judge.py +++ b/src/evidently/legacy/features/llm_judge.py @@ -5,7 +5,6 @@ from typing import Union import pandas as pd -from pydantic import PrivateAttr from evidently.legacy.base_metric import ColumnName from evidently.legacy.core import ColumnType @@ -35,12 +34,12 @@ class LLMJudge(GeneratedFeatures): input_columns: Optional[Dict[str, str]] = None template: BaseLLMPromptTemplate - _llm_wrapper: Optional[LLMWrapper] = PrivateAttr(None) + __llm_wrapper__: Optional[LLMWrapper] = None def get_llm_wrapper(self, options: Options) -> LLMWrapper: - if self._llm_wrapper is None: - self._llm_wrapper = get_llm_wrapper(self.provider, self.model, options) - return self._llm_wrapper + if self.__llm_wrapper__ is None: + self.__llm_wrapper__ = get_llm_wrapper(self.provider, self.model, options) + return self.__llm_wrapper__ def get_input_columns(self): if self.input_column is None: diff --git a/src/evidently/legacy/features/sentiment_feature.py b/src/evidently/legacy/features/sentiment_feature.py index 5c2b1d9db2..f340bb792b 100644 --- a/src/evidently/legacy/features/sentiment_feature.py +++ b/src/evidently/legacy/features/sentiment_feature.py @@ -4,7 +4,6 @@ import numpy as np from nltk.sentiment.vader import SentimentIntensityAnalyzer -from pydantic import PrivateAttr from evidently.legacy.core import ColumnType from evidently.legacy.features.generated_features import ApplyColumnGeneratedFeature @@ -17,7 +16,7 @@ class Sentiment(ApplyColumnGeneratedFeature): display_name_template: ClassVar = "Sentiment for {column_name}" column_name: str - _sid: Optional[SentimentIntensityAnalyzer] = PrivateAttr(None) + __sid__: Optional[SentimentIntensityAnalyzer] = None def __init__(self, column_name: str, display_name: Optional[str] = None): self.display_name = display_name @@ -25,12 +24,12 @@ def __init__(self, column_name: str, display_name: Optional[str] = None): @property def sid(self): - if self._sid is None: + if self.__sid__ is None: import nltk nltk.download("vader_lexicon", quiet=True) - self._sid = SentimentIntensityAnalyzer() - return self._sid + self.__sid__ = SentimentIntensityAnalyzer() + return self.__sid__ def apply(self, value: Any): if value is None or (isinstance(value, float) and np.isnan(value)): diff --git a/src/evidently/legacy/features/trigger_words_presence_feature.py b/src/evidently/legacy/features/trigger_words_presence_feature.py index 2c34d2fc97..0992bbe5b5 100644 --- a/src/evidently/legacy/features/trigger_words_presence_feature.py +++ b/src/evidently/legacy/features/trigger_words_presence_feature.py @@ -6,7 +6,6 @@ import numpy as np from nltk.stem.wordnet import WordNetLemmatizer -from pydantic import PrivateAttr from evidently.legacy.core import ColumnType from evidently.legacy.features.generated_features import ApplyColumnGeneratedFeature @@ -19,7 +18,7 @@ class TriggerWordsPresent(ApplyColumnGeneratedFeature): column_name: str words_list: List[str] lemmatize: bool = True - _lem: Optional[WordNetLemmatizer] = PrivateAttr(None) + __lem__: Optional[WordNetLemmatizer] = None def __init__( self, @@ -35,12 +34,12 @@ def __init__( @property def lem(self): - if self._lem is None: + if self.__lem__ is None: import nltk nltk.download("wordnet", quiet=True) - self._lem = WordNetLemmatizer() - return self._lem + self.__lem__ = WordNetLemmatizer() + return self.__lem__ def apply(self, value: Any): if value is None or (isinstance(value, float) and np.isnan(value)): diff --git a/src/evidently/legacy/features/words_feature.py b/src/evidently/legacy/features/words_feature.py index 824ccbfbf0..765b265f31 100644 --- a/src/evidently/legacy/features/words_feature.py +++ b/src/evidently/legacy/features/words_feature.py @@ -7,7 +7,6 @@ import numpy as np import pandas as pd from nltk.stem.wordnet import WordNetLemmatizer -from pydantic import PrivateAttr from evidently.legacy.base_metric import ColumnName from evidently.legacy.core import ColumnType @@ -53,7 +52,7 @@ class WordsPresence(ApplyColumnGeneratedFeature): words_list: List[str] mode: str lemmatize: bool = True - _lem: Optional[WordNetLemmatizer] = PrivateAttr(None) + __lem__: Optional[WordNetLemmatizer] = None def __init__( self, @@ -73,12 +72,12 @@ def __init__( @property def lem(self): - if self._lem is None: + if self.__lem__ is None: import nltk nltk.download("wordnet", quiet=True) - self._lem = WordNetLemmatizer() - return self._lem + self.__lem__ = WordNetLemmatizer() + return self.__lem__ def apply(self, value: Any): return _listed_words_present(value, self.mode, self.lem, self.words_list, self.lemmatize) @@ -141,7 +140,7 @@ class RowWordPresence(GeneratedFeature): columns: List[str] mode: str = "any" lemmatize: bool = True - _lem: Optional[WordNetLemmatizer] = PrivateAttr(None) + __lem__: Optional[WordNetLemmatizer] = None def __init__(self, columns: List[str], mode: str, lemmatize: bool, display_name: Optional[str] = None): self.columns = columns @@ -174,12 +173,12 @@ def generate_feature(self, data: pd.DataFrame, data_definition: DataDefinition) @property def lem(self): - if self._lem is None: + if self.__lem__ is None: import nltk nltk.download("wordnet", quiet=True) - self._lem = WordNetLemmatizer() - return self._lem + self.__lem__ = WordNetLemmatizer() + return self.__lem__ def _feature_name(self): return "_".join(["RowWordPresence", self.columns[0], self.columns[1], str(self.lemmatize), str(self.mode)]) diff --git a/src/evidently/legacy/metric_results.py b/src/evidently/legacy/metric_results.py index 7c9921fb3b..906cbaa26e 100644 --- a/src/evidently/legacy/metric_results.py +++ b/src/evidently/legacy/metric_results.py @@ -1,3 +1,4 @@ +from typing import Annotated from typing import ClassVar from typing import Dict from typing import List @@ -10,7 +11,9 @@ from typing import Union from typing import overload +import numpy as np import pandas as pd +from pydantic import BeforeValidator from pydantic import TypeAdapter from pydantic.v1 import validator from typing_extensions import Literal @@ -72,14 +75,25 @@ def column_scatter_valudator(value): return TypeAdapter(ColumnScatter).validate_python(value) +def distribution_field_validator(value): + if isinstance(value, (np.ndarray, list, pd.Categorical, pd.Series)): + return value + return np.array(value) + + class Distribution(MetricResult): __pd_include__: ClassVar[bool] = False __tags__: ClassVar[Set[IncludeTags]] = {IncludeTags.Render} __extract_as_obj__: ClassVar[bool] = True __type_alias__: ClassVar[Optional[str]] = "evidently:metric_result:Distribution" - x: Union[PydanticNPArray, list, pd.Categorical, pd.Series] - y: Union[PydanticNPArray, list, pd.Categorical, pd.Series] + x: Annotated[Union[PydanticNPArray, list, pd.Categorical, pd.Series], BeforeValidator(distribution_field_validator)] + y: Annotated[Union[PydanticNPArray, list, pd.Categorical, pd.Series], BeforeValidator(distribution_field_validator)] + + def __eq__(self, other): + if not isinstance(other, Distribution): + return False + return np.array_equal(self.x, other.x) and np.array_equal(self.y, other.y) class ConfusionMatrix(MetricResult): diff --git a/src/evidently/legacy/metrics/classification_performance/classification_dummy_metric.py b/src/evidently/legacy/metrics/classification_performance/classification_dummy_metric.py index f2ea382239..fb4c927593 100644 --- a/src/evidently/legacy/metrics/classification_performance/classification_dummy_metric.py +++ b/src/evidently/legacy/metrics/classification_performance/classification_dummy_metric.py @@ -51,7 +51,7 @@ class ClassificationDummyMetricResults(MetricResult): class ClassificationDummyMetric(ThresholdClassificationMetric[ClassificationDummyMetricResults]): __type_alias__: ClassVar[Optional[str]] = "evidently:metric:ClassificationDummyMetric" - _quality_metric: ClassificationQualityMetric + __quality_metric__: ClassificationQualityMetric = None # type: ignore[assignment] def __init__( self, @@ -62,7 +62,7 @@ def __init__( self.probas_threshold = probas_threshold self.k = k super().__init__(probas_threshold, k, options) - self._quality_metric = ClassificationQualityMetric() + self.__quality_metric__ = ClassificationQualityMetric() def calculate(self, data: InputData) -> ClassificationDummyMetricResults: quality_metric: Optional[ClassificationQualityMetric] @@ -75,7 +75,7 @@ def calculate(self, data: InputData) -> ClassificationDummyMetricResults: if prediction_name is None: quality_metric = None else: - quality_metric = self._quality_metric + quality_metric = self.__quality_metric__ # dummy by current labels_ratio = data.current_data[target_name].value_counts(normalize=True) diff --git a/src/evidently/legacy/metrics/classification_performance/quality_by_feature_table.py b/src/evidently/legacy/metrics/classification_performance/quality_by_feature_table.py index 0ded6910f3..f22800bf4f 100644 --- a/src/evidently/legacy/metrics/classification_performance/quality_by_feature_table.py +++ b/src/evidently/legacy/metrics/classification_performance/quality_by_feature_table.py @@ -54,7 +54,7 @@ class ClassificationQualityByFeatureTable(UsesRawDataMixin, Metric[Classificatio columns: Optional[List[str]] = None descriptors: Optional[Dict[str, Dict[str, FeatureDescriptor]]] = None - _text_features_gen: Optional[Dict[str, Dict[str, GeneratedFeature]]] + __text_features_gen__: Optional[Dict[str, Dict[str, GeneratedFeature]]] = None def __init__( self, @@ -65,7 +65,7 @@ def __init__( self.columns = columns self.descriptors = descriptors super().__init__(options=options) - self._text_features_gen = None + self.__text_features_gen__ = None def required_features(self, data_definition: DataDefinition): if len(data_definition.get_columns(ColumnType.Text, features_only=True)) > 0: @@ -87,7 +87,7 @@ def required_features(self, data_definition: DataDefinition): text_features_gen_result += list(col_dict.values()) text_features_gen[col] = col_dict - self._text_features_gen = text_features_gen + self.__text_features_gen__ = text_features_gen return text_features_gen_result else: @@ -137,8 +137,8 @@ def calculate(self, data: InputData) -> ClassificationQualityByFeatureTableResul # process text columns - if self._text_features_gen is not None: - for column, features in self._text_features_gen.items(): + if self.__text_features_gen__ is not None: + for column, features in self.__text_features_gen__.items(): columns.remove(column) columns += list(features.keys()) curr_text_df = pd.concat([data.get_current_column(x.as_column()) for x in features.values()], axis=1) diff --git a/src/evidently/legacy/metrics/custom_metric.py b/src/evidently/legacy/metrics/custom_metric.py index a072f1911c..06c7e534fe 100644 --- a/src/evidently/legacy/metrics/custom_metric.py +++ b/src/evidently/legacy/metrics/custom_metric.py @@ -4,8 +4,6 @@ from typing import Optional from typing import Union -from pydantic import PrivateAttr - from evidently.legacy.base_metric import InputData from evidently.legacy.base_metric import Metric from evidently.legacy.base_metric import MetricResult @@ -34,7 +32,7 @@ class CustomValueMetric(Metric[CustomCallableMetricResult]): title: Optional[str] = None size: Optional[WidgetSize] = None - _func: Optional[CustomCallableType] = PrivateAttr(None) + __func__: Optional[CustomCallableType] = None def __init__( self, @@ -53,12 +51,12 @@ def __init__( self.title = title self.size = size super().__init__(options, **data) - self._func = _func + self.__func__ = _func def calculate(self, data: InputData) -> CustomCallableMetricResult: - if self._func is None: + if self.__func__ is None: raise ValueError("CustomCallableMetric is not configured with callable func") - return CustomCallableMetricResult(value=self._func(data)) + return CustomCallableMetricResult(value=self.__func__(data)) @default_renderer(wrap_type=CustomValueMetric) diff --git a/src/evidently/legacy/metrics/data_drift/data_drift_table.py b/src/evidently/legacy/metrics/data_drift/data_drift_table.py index 656e56a169..89ca70fdac 100644 --- a/src/evidently/legacy/metrics/data_drift/data_drift_table.py +++ b/src/evidently/legacy/metrics/data_drift/data_drift_table.py @@ -59,7 +59,7 @@ class DataDriftTable(UsesRawDataMixin, WithDriftOptions[DataDriftTableResults]): columns: Optional[List[str]] = None feature_importance: Optional[bool] = None - _feature_importance_metric: Optional[FeatureImportanceMetric] + __feature_importance_metric__: Optional[FeatureImportanceMetric] = None def __init__( self, @@ -105,9 +105,9 @@ def __init__( per_feature_threshold=per_column_stattest_threshold, ) if feature_importance: - self._feature_importance_metric = FeatureImportanceMetric() + self.__feature_importance_metric__ = FeatureImportanceMetric() else: - self._feature_importance_metric = None + self.__feature_importance_metric__ = None def get_parameters(self) -> tuple: return (None if self.columns is None else tuple(self.columns), self.feature_importance, self.drift_options) @@ -133,8 +133,8 @@ def calculate(self, data: InputData) -> DataDriftTableResults: current_fi: Optional[Dict[str, float]] = None reference_fi: Optional[Dict[str, float]] = None - if self._feature_importance_metric is not None: - res = self._feature_importance_metric.get_result() + if self.__feature_importance_metric__ is not None: + res = self.__feature_importance_metric__.get_result() current_fi = res.current reference_fi = res.reference diff --git a/src/evidently/legacy/metrics/data_integrity/column_regexp_metric.py b/src/evidently/legacy/metrics/data_integrity/column_regexp_metric.py index f3f8588075..a8e8fe13f2 100644 --- a/src/evidently/legacy/metrics/data_integrity/column_regexp_metric.py +++ b/src/evidently/legacy/metrics/data_integrity/column_regexp_metric.py @@ -80,14 +80,14 @@ class ColumnRegExpMetric(Metric[DataIntegrityValueByRegexpMetricResult]): reg_exp: str top: int # compiled regular expression for speed optimization - _reg_exp_compiled: Pattern + __reg_exp_compiled__: Optional[Pattern] = None def __init__(self, column_name: str, reg_exp: str, top: int = 10, options: AnyOptions = None): self.top = top self.reg_exp = reg_exp self.column_name = column_name super().__init__(options=options) - self._reg_exp_compiled = re.compile(reg_exp) + self.__reg_exp_compiled__ = re.compile(reg_exp) def _calculate_stats_by_regexp(self, column: pd.Series) -> DataIntegrityValueByRegexpStat: number_of_matched = 0 @@ -103,7 +103,7 @@ def _calculate_stats_by_regexp(self, column: pd.Series) -> DataIntegrityValueByR item = str(item) - if bool(self._reg_exp_compiled.match(str(item))): + if bool(self.__reg_exp_compiled__.match(str(item))): number_of_matched += 1 table_of_matched[item] += 1 diff --git a/src/evidently/legacy/metrics/data_quality/dataset_correlations_metric.py b/src/evidently/legacy/metrics/data_quality/dataset_correlations_metric.py index b0d84cfecf..3bc1a99829 100644 --- a/src/evidently/legacy/metrics/data_quality/dataset_correlations_metric.py +++ b/src/evidently/legacy/metrics/data_quality/dataset_correlations_metric.py @@ -81,16 +81,16 @@ class DatasetCorrelationsMetric(Metric[DatasetCorrelationsMetricResult]): """Calculate different correlations with target, predictions and features""" - _text_features_gen: Optional[ + __text_features_gen__: Optional[ Dict[ str, Dict[str, Union[TextLength, NonLetterCharacterPercentage, OOVWordsPercentage]], ] - ] + ] = None def __init__(self, options: AnyOptions = None): super().__init__(options=options) - self._text_features_gen = None + self.__text_features_gen__ = None def required_features(self, data_definition: DataDefinition): if len(data_definition.get_columns(ColumnType.Text, features_only=True)) > 0: @@ -112,7 +112,7 @@ def required_features(self, data_definition: DataDefinition): col_dict[f"{col}: OOV %"], ] text_features_gen[col] = col_dict - self._text_features_gen = text_features_gen + self.__text_features_gen__ = text_features_gen return text_features_gen_result else: @@ -261,13 +261,13 @@ def calculate(self, data: InputData) -> DatasetCorrelationsMetricResult: # process text columns text_columns = [] - if self._text_features_gen is not None: - for col in list(self._text_features_gen.keys()): + if self.__text_features_gen__ is not None: + for col in list(self.__text_features_gen__.keys()): curr_text_df = pd.concat( - [data.get_current_column(x.as_column()) for x in list(self._text_features_gen[col].values())], + [data.get_current_column(x.as_column()) for x in list(self.__text_features_gen__[col].values())], axis=1, ) - curr_text_df.columns = pd.Index(list(self._text_features_gen[col].keys())) + curr_text_df.columns = pd.Index(list(self.__text_features_gen__[col].keys())) text_columns.append(list(curr_text_df.columns)) curr_df = pd.concat( [ @@ -279,10 +279,13 @@ def calculate(self, data: InputData) -> DatasetCorrelationsMetricResult: if ref_df is not None: ref_text_df = pd.concat( - [data.get_reference_column(x.as_column()) for x in list(self._text_features_gen[col].values())], + [ + data.get_reference_column(x.as_column()) + for x in list(self.__text_features_gen__[col].values()) + ], axis=1, ) - ref_text_df.columns = pd.Index(list(self._text_features_gen[col].keys())) + ref_text_df.columns = pd.Index(list(self.__text_features_gen__[col].keys())) ref_df = pd.concat( [ ref_df.copy().reset_index(drop=True), diff --git a/src/evidently/legacy/metrics/recsys/f_beta_top_k.py b/src/evidently/legacy/metrics/recsys/f_beta_top_k.py index 1241a2dac5..65f09ab3b9 100644 --- a/src/evidently/legacy/metrics/recsys/f_beta_top_k.py +++ b/src/evidently/legacy/metrics/recsys/f_beta_top_k.py @@ -20,7 +20,7 @@ class FBetaTopKMetric(TopKMetric): beta: Optional[float] = None min_rel_score: Optional[int] = None no_feedback_users: bool - _precision_recall_calculation: PrecisionRecallCalculation + __precision_recall_calculation__: PrecisionRecallCalculation = None # type: ignore[assignment] def __init__( self, @@ -40,7 +40,7 @@ def __init__( min_rel_score=min_rel_score, no_feedback_users=no_feedback_users, ) - self._precision_recall_calculation = PrecisionRecallCalculation(max(k, 10), min_rel_score) + self.__precision_recall_calculation__ = PrecisionRecallCalculation(max(k, 10), min_rel_score) def calculate(self, data: InputData) -> TopKMetricResult: if self.no_feedback_users: @@ -49,7 +49,7 @@ def calculate(self, data: InputData) -> TopKMetricResult: else: pr_key = "precision" rc_key = "recall" - result = self._precision_recall_calculation.get_result() + result = self.__precision_recall_calculation__.get_result() current = pd.Series(data=self.fbeta(result.current[pr_key], result.current[rc_key])) ref_data = result.reference reference: Optional[pd.Series] = None diff --git a/src/evidently/legacy/metrics/recsys/novelty.py b/src/evidently/legacy/metrics/recsys/novelty.py index b9afb7d150..d43edfde3b 100644 --- a/src/evidently/legacy/metrics/recsys/novelty.py +++ b/src/evidently/legacy/metrics/recsys/novelty.py @@ -50,12 +50,12 @@ class NoveltyMetric(Metric[NoveltyMetricResult]): """Mean Inverse User Frequency""" k: int - _train_stats: TrainStats + __train_stats__: TrainStats = None # type: ignore[assignment] def __init__(self, k: int, options: AnyOptions = None) -> None: self.k = k super().__init__(options=options) - self._train_stats = TrainStats() + self.__train_stats__ = TrainStats() def get_miuf( self, df, k, recommendations_type: Optional[RecomType], user_name, item_name, prediction_name, interactions @@ -71,7 +71,7 @@ def get_miuf( return distr, value def calculate(self, data: InputData) -> NoveltyMetricResult: - train_result = self._train_stats.get_result() + train_result = self.__train_stats__.get_result() curr_user_interacted = train_result.current ref_user_interacted = train_result.reference current_n_users = train_result.current_n_users diff --git a/src/evidently/legacy/metrics/recsys/serendipity.py b/src/evidently/legacy/metrics/recsys/serendipity.py index d396b0e775..c308fcc3c8 100644 --- a/src/evidently/legacy/metrics/recsys/serendipity.py +++ b/src/evidently/legacy/metrics/recsys/serendipity.py @@ -51,7 +51,7 @@ class SerendipityMetric(Metric[SerendipityMetricResult]): """unusualness * relevance""" - _pairwise_distance: PairwiseDistance + __pairwise_distance__: PairwiseDistance = None # type: ignore[assignment] k: int item_features: List[str] min_rel_score: Optional[int] = None @@ -63,7 +63,7 @@ def __init__( self.item_features = item_features self.min_rel_score = min_rel_score super().__init__(options=options) - self._pairwise_distance = PairwiseDistance(k=k, item_features=item_features) + self.__pairwise_distance__ = PairwiseDistance(k=k, item_features=item_features) def get_serendipity( self, @@ -101,7 +101,7 @@ def get_serendipity( return distr_data, value def calculate(self, data: InputData) -> SerendipityMetricResult: - result = self._pairwise_distance.get_result() + result = self.__pairwise_distance__.get_result() dist_matrix = result.dist_matrix name_dict = result.name_dict target = data.data_definition.get_target_column() diff --git a/src/evidently/legacy/metrics/regression_performance/error_bias_table.py b/src/evidently/legacy/metrics/regression_performance/error_bias_table.py index 8968a6e391..ca821dbb41 100644 --- a/src/evidently/legacy/metrics/regression_performance/error_bias_table.py +++ b/src/evidently/legacy/metrics/regression_performance/error_bias_table.py @@ -77,7 +77,7 @@ class RegressionErrorBiasTable(UsesRawDataMixin, Metric[RegressionErrorBiasTable top_error: float columns: Optional[List[str]] = None descriptors: Optional[Dict[str, Dict[str, FeatureDescriptor]]] = None - _text_features_gen: Optional[Dict[str, Dict[str, GeneratedFeature]]] + __text_features_gen__: Optional[Dict[str, Dict[str, GeneratedFeature]]] = None def __init__( self, @@ -95,7 +95,7 @@ def __init__( self.columns = columns self.descriptors = descriptors super().__init__(options=options) - self._text_features_gen = None + self.__text_features_gen__ = None def required_features(self, data_definition: DataDefinition): if len(data_definition.get_columns(ColumnType.Text, features_only=True)) > 0: @@ -117,7 +117,7 @@ def required_features(self, data_definition: DataDefinition): text_features_gen_result += list(col_dict.values()) text_features_gen[col] = col_dict - self._text_features_gen = text_features_gen + self.__text_features_gen__ = text_features_gen return text_features_gen_result else: @@ -173,8 +173,8 @@ def calculate(self, data: InputData) -> RegressionErrorBiasTableResults: num_feature_names = list(np.intersect1d(dataset_columns.num_feature_names, columns)) cat_feature_names = list(np.intersect1d(dataset_columns.cat_feature_names, columns)) # process text columns - if self._text_features_gen is not None: - for column, features in self._text_features_gen.items(): + if self.__text_features_gen__ is not None: + for column, features in self.__text_features_gen__.items(): columns.remove(column) num_feature_names += list(features.keys()) columns += list(features.keys()) diff --git a/src/evidently/legacy/metrics/regression_performance/objects.py b/src/evidently/legacy/metrics/regression_performance/objects.py index 99ab641f4a..1c508f71ba 100644 --- a/src/evidently/legacy/metrics/regression_performance/objects.py +++ b/src/evidently/legacy/metrics/regression_performance/objects.py @@ -47,27 +47,27 @@ class IntervalSeries(MetricResult): bins: List[float] values: List[float] - _data: pd.Series + __data__: Optional[pd.Series] = None @property def data(self): - if not hasattr(self, "_data"): - self._data = pd.Series( + if not hasattr(self, "__data__") or self.__data__ is None: + self.__data__ = pd.Series( self.values, index=[Interval(a, b, closed="right") for a, b in zip(self.bins, self.bins[1:])] ) - return self._data + return self.__data__ @classmethod def from_data(cls, data: pd.Series): index: List[Interval] = list(data.index) interval_series = cls(values=list(data), bins=[i.left for i in index] + [index[-1].right]) - interval_series._data = data + interval_series.__data__ = data return interval_series def __mul__(self, other: float): series = IntervalSeries(bins=self.bins, values=[v * other for v in self.values]) - if hasattr(self, "_data"): - series._data = self._data * other + if hasattr(self, "__data__") and self.__data__ is not None: + series.__data__ = self.__data__ * other return series diff --git a/src/evidently/legacy/metrics/regression_performance/regression_dummy_metric.py b/src/evidently/legacy/metrics/regression_performance/regression_dummy_metric.py index a6a3c32f55..ae42f3b816 100644 --- a/src/evidently/legacy/metrics/regression_performance/regression_dummy_metric.py +++ b/src/evidently/legacy/metrics/regression_performance/regression_dummy_metric.py @@ -41,15 +41,15 @@ class RegressionDummyMetricResults(MetricResult): class RegressionDummyMetric(Metric[RegressionDummyMetricResults]): __type_alias__: ClassVar[Optional[str]] = "evidently:metric:RegressionDummyMetric" - _quality_metric: RegressionQualityMetric + __quality_metric__: RegressionQualityMetric = None # type: ignore[assignment] def __init__(self, options: AnyOptions = None): super().__init__(options=options) - self._quality_metric = RegressionQualityMetric() + self.__quality_metric__ = RegressionQualityMetric() @property def quality_metric(self): - return self._quality_metric + return self.__quality_metric__ def calculate(self, data: InputData) -> RegressionDummyMetricResults: quality_metric: Optional[RegressionQualityMetric] diff --git a/src/evidently/legacy/tests/base_test.py b/src/evidently/legacy/tests/base_test.py index 7903e3618e..6e759731ae 100644 --- a/src/evidently/legacy/tests/base_test.py +++ b/src/evidently/legacy/tests/base_test.py @@ -113,11 +113,11 @@ class TestResult(EnumValueMixin, MetricResult): # todo: create common base clas # grouping parameters group: str parameters: Optional[TestParameters] = None - _exception: Optional[BaseException] = None + __exception__: Optional[BaseException] = None @property def exception(self): - return self._exception + return self.__exception__ def set_status(self, status: TestStatus, description: Optional[str] = None) -> None: self.status = status @@ -151,19 +151,19 @@ class Test(WithTestAndMetricDependencies): name: ClassVar[str] group: ClassVar[str] is_critical: bool = True - _context: Optional["Context"] = None + __context__: Optional["Context"] = None @abc.abstractmethod def check(self) -> TestResult: raise NotImplementedError def set_context(self, context: "Context"): - self._context = context + self.__context__ = context def get_result(self) -> TestResult: - if self._context is None: + if self.__context__ is None: raise ValueError("No context is set") - result = self._context.test_results.get(self, None) + result = self.__context__.test_results.get(self, None) if result is None: raise ValueError(f"No result found for metric {self} of type {type(self).__name__}") return result # type: ignore[return-value] diff --git a/src/evidently/legacy/ui/base.py b/src/evidently/legacy/ui/base.py index a4a88daaed..e5c4f78526 100644 --- a/src/evidently/legacy/ui/base.py +++ b/src/evidently/legacy/ui/base.py @@ -18,7 +18,6 @@ import uuid6 from pydantic import BaseModel from pydantic import Field -from pydantic import PrivateAttr from pydantic import TypeAdapter from evidently.core.report import Snapshot as SnapshotV2 @@ -74,23 +73,23 @@ class SnapshotMetadata(BaseModel): blob: "BlobMetadata" links: SnapshotLinks = SnapshotLinks() # links to datasets and stuff - _project: "Project" = PrivateAttr(None) - _dashboard_info: "DashboardInfo" = PrivateAttr(None) - _additional_graphs: Dict[str, dict] = PrivateAttr(None) + __project__: Optional["Project"] = None + __dashboard_info__: Optional["DashboardInfo"] = None + __additional_graphs__: Optional[Dict[str, dict]] = None @property def project(self): - return self._project + return self.__project__ async def load(self) -> Snapshot: - return await self.project.project_manager.load_snapshot(self.project._user_id, self.project.id, self.id) + return await self.project.project_manager.load_snapshot(self.project.__user_id__, self.project.id, self.id) async def as_report_base(self) -> ReportBase: value = await self.load() return value.as_report() if value.is_report else value.as_test_suite() def bind(self, project: "Project"): - self._project = project + self.__project__ = project return self @classmethod @@ -106,16 +105,16 @@ def from_snapshot(cls, snapshot: Snapshot, blob: "BlobMetadata") -> "SnapshotMet ) async def get_dashboard_info(self): - if self._dashboard_info is None: + if self.__dashboard_info__ is None: report = await self.as_report_base() - _, self._dashboard_info, self._additional_graphs = report._build_dashboard_info() - return self._dashboard_info + _, self.__dashboard_info__, self.__additional_graphs__ = report._build_dashboard_info() + return self.__dashboard_info__ async def get_additional_graphs(self): - if self._additional_graphs is None: + if self.__additional_graphs__ is None: report = await self.as_report_base() - _, self._dashboard_info, self._additional_graphs = report._build_dashboard_info() - return self._additional_graphs + _, self.__dashboard_info__, self.__additional_graphs__ = report._build_dashboard_info() + return self.__additional_graphs__ class Project(Entity): @@ -135,46 +134,48 @@ class Project(Entity): version: str = "1" # Field(default=datetime.datetime.fromisoformat("1900-01-01T00:00:00")) - _project_manager: Optional["ProjectManager"] = PrivateAttr(default=None) - _user_id: Optional[UserID] = PrivateAttr(default=None) + __project_manager__: Optional["ProjectManager"] = None + __user_id__: Optional[UserID] = None def bind(self, project_manager: Optional["ProjectManager"], user_id: Optional[UserID]): - self._project_manager = project_manager - self._user_id = user_id + self.__project_manager__ = project_manager + self.__user_id__ = user_id return self @property def project_manager(self) -> "ProjectManager": - if self._project_manager is None: + if self.__project_manager__ is None: raise ValueError("Project is not binded") - return self._project_manager + return self.__project_manager__ async def save_async(self): - await self.project_manager.update_project(self._user_id, self) # type: ignore[arg-type] + await self.project_manager.update_project(self.__user_id__, self) # type: ignore[arg-type] return self async def load_snapshot_async(self, snapshot_id: SnapshotID) -> Snapshot: - return await self.project_manager.load_snapshot(self._user_id, self.id, snapshot_id) # type: ignore[arg-type] + return await self.project_manager.load_snapshot(self.__user_id__, self.id, snapshot_id) # type: ignore[arg-type] async def add_snapshot_async(self, snapshot: AnySnapshot): if not isinstance(snapshot, Snapshot): from evidently.ui.backport import snapshot_v2_to_v1 snapshot = snapshot_v2_to_v1(snapshot) - await self.project_manager.add_snapshot(self._user_id, self.id, snapshot) # type: ignore[arg-type] + await self.project_manager.add_snapshot(self.__user_id__, self.id, snapshot) # type: ignore[arg-type] async def delete_snapshot_async(self, snapshot_id: Union[str, SnapshotID]): if isinstance(snapshot_id, str): snapshot_id = uuid6.UUID(snapshot_id) - await self.project_manager.delete_snapshot(self._user_id, self.id, snapshot_id) # type: ignore[arg-type] + await self.project_manager.delete_snapshot(self.__user_id__, self.id, snapshot_id) # type: ignore[arg-type] async def list_snapshots_async( self, include_reports: bool = True, include_test_suites: bool = True ) -> List[SnapshotMetadata]: - return await self.project_manager.list_snapshots(self._user_id, self.id, include_reports, include_test_suites) # type: ignore[arg-type] + return await self.project_manager.list_snapshots( + self.__user_id__, self.id, include_reports, include_test_suites + ) # type: ignore[arg-type] async def get_snapshot_metadata_async(self, id: SnapshotID) -> SnapshotMetadata: - return await self.project_manager.get_snapshot_metadata(self._user_id, self.id, id) # type: ignore[arg-type] + return await self.project_manager.get_snapshot_metadata(self.__user_id__, self.id, id) # type: ignore[arg-type] async def build_dashboard_info_async( self, @@ -204,11 +205,11 @@ async def show_dashboard_async( async def reload_async(self, reload_snapshots: bool = False): # fixme: reload snapshots - project = await self.project_manager.get_project(self._user_id, self.id) # type: ignore[arg-type] + project = await self.project_manager.get_project(self.__user_id__, self.id) # type: ignore[arg-type] self.__dict__.update(project.__dict__) if reload_snapshots: - await self.project_manager.reload_snapshots(self._user_id, self.id) # type: ignore[arg-type] + await self.project_manager.reload_snapshots(self.__user_id__, self.id) # type: ignore[arg-type] save = sync_api(save_async) load_snapshot = sync_api(load_snapshot_async) diff --git a/src/evidently/legacy/ui/config.py b/src/evidently/legacy/ui/config.py index c76336d497..6e5573f735 100644 --- a/src/evidently/legacy/ui/config.py +++ b/src/evidently/legacy/ui/config.py @@ -2,6 +2,7 @@ from typing import Dict from typing import Iterator from typing import List +from typing import Optional from typing import Type from typing import TypeVar @@ -11,7 +12,6 @@ from litestar import Litestar from litestar.di import Provide from pydantic import BaseModel -from pydantic import PrivateAttr from pydantic import TypeAdapter from evidently.legacy.ui.components.base import SECTION_COMPONENT_TYPE_MAPPING @@ -79,12 +79,12 @@ def validate(self): class Config(BaseModel): additional_components: Dict[str, Component] = {} - _components: List[Component] = PrivateAttr(default_factory=list) - _ctx: ComponentContext = PrivateAttr() + __components__: Optional[List[Component]] = None # not a model field, internal only + __ctx__: Optional[ComponentContext] = None @property def components(self) -> List[Component]: - return [getattr(self, name) for name in self.__fields__ if isinstance(getattr(self, name), Component)] + list( + return [getattr(self, name) for name in self.model_fields if isinstance(getattr(self, name), Component)] + list( self.additional_components.values() ) @@ -92,9 +92,9 @@ def components(self) -> List[Component]: def context(self) -> Iterator[ConfigContext]: ctx = ConfigContext(self, {type(c): c for c in self.components}) ctx.validate() - self._ctx = ctx + self.__ctx__ = ctx yield ctx - del self._ctx + del self.__ctx__ class AppConfig(Config): diff --git a/src/evidently/legacy/ui/storage/local/base.py b/src/evidently/legacy/ui/storage/local/base.py index c8fdb5ca1f..b603f76561 100644 --- a/src/evidently/legacy/ui/storage/local/base.py +++ b/src/evidently/legacy/ui/storage/local/base.py @@ -12,7 +12,6 @@ import uuid6 from fsspec import AbstractFileSystem from fsspec import get_fs_token_paths -from pydantic import PrivateAttr from pydantic import TypeAdapter from pydantic import ValidationError @@ -94,17 +93,17 @@ def size(self, path): class FSSpecBlobStorage(BlobStorage): base_path: str - _location: FSLocation = PrivateAttr(None) + __location__: Optional[FSLocation] = None def __init__(self, base_path: str): self.base_path = base_path - self._location = FSLocation(self.base_path) + self.__location__ = FSLocation(self.base_path) @property def location(self) -> FSLocation: - if self._location is None: - self._location = FSLocation(self.base_path) - return self._location + if self.__location__ is None: + self.__location__ = FSLocation(self.base_path) + return self.__location__ def get_snapshot_blob_id(self, project_id: ProjectID, snapshot: Snapshot) -> BlobID: return posixpath.join(str(project_id), SNAPSHOTS, str(snapshot.id)) + ".json" @@ -191,17 +190,17 @@ def reload_snapshot(self, project: Project, snapshot_id: SnapshotID, skip_errors class JsonFileProjectMetadataStorage(ProjectMetadataStorage): path: str - _state: LocalState = PrivateAttr(None) + __state__: Optional[LocalState] = None def __init__(self, path: str, local_state: Optional[LocalState] = None): self.path = path - self._state = local_state or LocalState.load(self.path, None) + self.__state__ = local_state or LocalState.load(self.path, None) @property def state(self): - if self._state is None: - self._state = LocalState.load(self.path, None) - return self._state + if self.__state__ is None: + self.__state__ = LocalState.load(self.path, None) + return self.__state__ async def add_project( self, project: Project, user: User, team: Optional[Team], org_id: Optional[OrgID] = None @@ -275,17 +274,17 @@ async def reload_snapshots(self, project_id: ProjectID): class InMemoryDataStorage(DataStorage): path: str - _state: LocalState = PrivateAttr(None) + __state__: Optional[LocalState] = None def __init__(self, path: str, local_state: Optional[LocalState] = None): self.path = path - self._state = local_state or LocalState.load(self.path, None) + self.__state__ = local_state or LocalState.load(self.path, None) @property def state(self): - if self._state is None: - self._state = LocalState.load(self.path, None) - return self._state + if self.__state__ is None: + self.__state__ = LocalState.load(self.path, None) + return self.__state__ async def extract_points(self, project_id: ProjectID, snapshot: Snapshot): pass diff --git a/src/evidently/llm/datagen/base.py b/src/evidently/llm/datagen/base.py index b64f30792f..95ef472139 100644 --- a/src/evidently/llm/datagen/base.py +++ b/src/evidently/llm/datagen/base.py @@ -5,7 +5,6 @@ import pandas as pd from pydantic import ConfigDict -from pydantic import PrivateAttr from typing_extensions import TypeAlias from evidently.legacy.options.base import Options @@ -67,7 +66,7 @@ class BaseLLMDatasetGenerator(BaseDatasetGenerator, ABC): """LLM provider name.""" model: str """LLM model name.""" - _llm_wrapper: Optional[LLMWrapper] = PrivateAttr(None) + __llm_wrapper__: Optional[LLMWrapper] = None def get_llm_wrapper(self, options: Options) -> LLMWrapper: """Get or create the LLM wrapper for this generator. @@ -78,9 +77,9 @@ def get_llm_wrapper(self, options: Options) -> LLMWrapper: Returns: * `LLMWrapper` instance for the configured provider/model. """ - if self._llm_wrapper is None: - self._llm_wrapper = get_llm_wrapper(self.provider, self.model, options) - return self._llm_wrapper + if self.__llm_wrapper__ is None: + self.__llm_wrapper__ = get_llm_wrapper(self.provider, self.model, options) + return self.__llm_wrapper__ @property def wrapper(self): diff --git a/src/evidently/llm/optimization/optimizer.py b/src/evidently/llm/optimization/optimizer.py index b5883e08f3..faf2941094 100644 --- a/src/evidently/llm/optimization/optimizer.py +++ b/src/evidently/llm/optimization/optimizer.py @@ -20,7 +20,6 @@ from pydantic import BaseModel from pydantic import ConfigDict from pydantic import Field -from pydantic import PrivateAttr from sklearn.model_selection import train_test_split from evidently.legacy.core import new_id @@ -447,7 +446,7 @@ class OptimizerRun(BaseModel): """Random seed used for this run.""" start_time: datetime.datetime = Field(default_factory=datetime.datetime.now) """Timestamp when the run started.""" - _context: "OptimizerContext" = PrivateAttr() + __context__: Optional["OptimizerContext"] = None def bind(self, context: "OptimizerContext") -> "OptimizerRun": """Bind this run to an optimizer context. @@ -458,7 +457,7 @@ def bind(self, context: "OptimizerContext") -> "OptimizerRun": Returns: * Self for method chaining. """ - self._context = context + self.__context__ = context return self @property @@ -468,7 +467,7 @@ def context(self) -> "OptimizerContext": Returns: * `OptimizerContext` associated with this run. """ - return self._context + return self.__context__ def add_log(self, log: OptimizerLog): """Add a log entry to the context and log its message. @@ -476,7 +475,7 @@ def add_log(self, log: OptimizerLog): Args: * `log`: `OptimizerLog` to add. """ - if self._context.config.verbose: + if self.__context__.config.verbose: print(f"[{self.run_id}]", log.message()) self.logs[log.id] = log diff --git a/src/evidently/llm/optimization/prompts.py b/src/evidently/llm/optimization/prompts.py index 98c961fa40..ccbe04c8e9 100644 --- a/src/evidently/llm/optimization/prompts.py +++ b/src/evidently/llm/optimization/prompts.py @@ -15,7 +15,6 @@ import pandas as pd from pydantic import BaseModel -from pydantic import PrivateAttr from evidently import ColumnType from evidently import Dataset @@ -375,7 +374,7 @@ class CallablePromptExecutor(PromptExecutor): """Prompt executor that wraps a callable function.""" func: str - _func: Optional[CustomExecutorCallable] = PrivateAttr(None) + __func__: Optional[CustomExecutorCallable] = None task: Optional[str] = None def __init__( @@ -383,17 +382,17 @@ def __init__( func: Union[str, CustomExecutorCallable], ): if callable(func): - self._func = func + self.__func__ = func func = f"{func.__module__}.{func.__name__}" else: - self._func = None + self.__func__ = None self.func = func super().__init__() def execute(self, prompt: str, run: OptimizerRun) -> PromptExecutionLog: - if self._func is None: + if self.__func__ is None: raise OptimizationConfigurationError(f"No function set for CallablePromptExecutor: {self.func}") - result = self._func(prompt, run.context) + result = self.__func__(prompt, run.context) if not isinstance(result, PromptExecutionLog): result = PromptExecutionLog( prompt=prompt, @@ -409,14 +408,14 @@ def get_base_prompt(self) -> Optional[str]: def get_task(self) -> str: if self.task is not None: return self.task - doc = self._func.__doc__ + doc = self.__func__.__doc__ if doc is None: warnings.warn(f"Please add docstring to {self.func} describing task you are trying to optimize.") return doc or "" def get_best_result(self, prompt: str, context: OptimizerContext): def best_result(): - return self._func(prompt, context) + return self.__func__(prompt, context) return best_result @@ -505,7 +504,7 @@ def get_best_result(self, prompt: str, context: OptimizerContext): """ return self.sub_executor.get_best_result(prompt, context) - _sub_executor: Optional[LLMJudgePromptExecutor] = PrivateAttr(default=None) + __sub_executor__: Optional[LLMJudgePromptExecutor] = None @property def sub_executor(self) -> LLMJudgePromptExecutor: @@ -517,9 +516,9 @@ def sub_executor(self) -> LLMJudgePromptExecutor: Raises: * `OptimizationRuntimeError`: If sub-executor hasn't been initialized. """ - if self._sub_executor is None: + if self.__sub_executor__ is None: raise OptimizationRuntimeError("Sub executor is not set for BlankLLMJudge") - return self._sub_executor + return self.__sub_executor__ async def start(self, run: OptimizerRun): """Initialize the sub-executor by building the judge. @@ -535,9 +534,9 @@ async def _ensure_sub_executor(self, run: OptimizerRun): Args: * `run`: `OptimizerRun` to use for building the judge. """ - if self._sub_executor is None: + if self.__sub_executor__ is None: judge = await self._build_judge(run) - self._sub_executor = LLMJudgePromptExecutor(judge=judge) + self.__sub_executor__ = LLMJudgePromptExecutor(judge=judge) async def _build_judge(self, run: OptimizerRun) -> LLMJudge: """Build an LLM judge based on the dataset labels. diff --git a/src/evidently/llm/rag/index.py b/src/evidently/llm/rag/index.py index 8c62938e53..37f2b8971a 100644 --- a/src/evidently/llm/rag/index.py +++ b/src/evidently/llm/rag/index.py @@ -10,7 +10,6 @@ from typing import Optional import numpy as np -from pydantic import PrivateAttr from evidently.llm.rag.splitter import AnySplitter from evidently.llm.rag.splitter import Chunk @@ -35,13 +34,13 @@ class DataCollectionProvider(AutoAliasMixin, EvidentlyBaseModel, ABC): chunk_size: int = DEFAULT_CHUNK_SIZE chunk_overlap: int = DEFAULT_CHUNK_OVERLAP splitter: AnySplitter = "llama_index" - _data_collection_cache: "DataCollection" = PrivateAttr() + __data_collection_cache__: Optional["DataCollection"] = None def get_data_collection(self, use_cache: bool = True) -> "DataCollection": - if use_cache and hasattr(self, "_data_collection_cache"): - return self._data_collection_cache - self._data_collection_cache = self._get_data_collection() - return self._data_collection_cache + if use_cache and self.__data_collection_cache__ is not None: + return self.__data_collection_cache__ + self.__data_collection_cache__ = self._get_data_collection() + return self.__data_collection_cache__ @abstractmethod def _get_data_collection(self) -> "DataCollection": diff --git a/src/evidently/llm/rag/splitter.py b/src/evidently/llm/rag/splitter.py index 2fd4767ff0..2de75ac558 100644 --- a/src/evidently/llm/rag/splitter.py +++ b/src/evidently/llm/rag/splitter.py @@ -2,14 +2,13 @@ from abc import ABC from abc import abstractmethod from enum import Enum +from typing import Any from typing import ClassVar from typing import Iterator from typing import List from typing import Optional from typing import Union -from pydantic import PrivateAttr - from evidently.pydantic_utils import AutoAliasMixin from evidently.pydantic_utils import EvidentlyBaseModel @@ -108,21 +107,21 @@ def split_text(self, text: TextSource) -> Iterator[Chunk]: class LlamaIndexSplitter(Splitter): separator: str = " " paragraph_separator: Optional[str] = None - _splitter = PrivateAttr(None) + __splitter__: Optional[Any] = None @property def splitter(self): - if self._splitter is None: + if self.__splitter__ is None: from llama_index.core.node_parser import SentenceSplitter from llama_index.core.node_parser.text.sentence import DEFAULT_PARAGRAPH_SEP - self._splitter = SentenceSplitter( + self.__splitter__ = SentenceSplitter( chunk_size=self.chunk_size, chunk_overlap=self.chunk_overlap, separator=self.separator, paragraph_separator=self.paragraph_separator or DEFAULT_PARAGRAPH_SEP, ) - return self._splitter + return self.__splitter__ def split_text(self, text: TextSource) -> Iterator[Chunk]: yield from self.splitter.split_text(text.get_text()) diff --git a/src/evidently/llm/utils/templates.py b/src/evidently/llm/utils/templates.py index d2937ce22a..d6e50fedf7 100644 --- a/src/evidently/llm/utils/templates.py +++ b/src/evidently/llm/utils/templates.py @@ -19,7 +19,6 @@ from typing import get_args import typing_inspect -from pydantic import PrivateAttr from typing_inspect import is_classvar from evidently.llm.models import LLMMessage @@ -73,7 +72,7 @@ def _get_genric_arg(cls: Type): class PromptTemplate(AutoAliasMixin, EvidentlyBaseModel): __alias_type__: ClassVar = "prompt_template" - _prepared_template: PreparedTemplate = PrivateAttr() + __prepared_template__: PreparedTemplate __is_base_type__: ClassVar[bool] = True @@ -90,13 +89,13 @@ def prepare(self) -> PreparedTemplate: @property def prepared_template(self) -> PreparedTemplate: - if not hasattr(self, "_prepared_template"): - self._prepared_template = self.prepare() - return self._prepared_template + if not hasattr(self, "__prepared_template__"): + self.__prepared_template__ = self.prepare() + return self.__prepared_template__ def clear_prepared_template(self): try: - delattr(self, "_prepared_template") + delattr(self, "__prepared_template__") except AttributeError: pass @@ -182,7 +181,7 @@ def smart_isinstance(value, type_) -> bool: class StrPromptTemplate(PromptTemplate): prompt_template: Optional[str] = None _contract: ClassVar[Callable] - _context_vars: Dict[str, Any] = PrivateAttr() + __context_vars__: Optional[Dict[str, Any]] = None def __init_subclass__(cls): super().__init_subclass__() @@ -205,18 +204,18 @@ def list_context_variables(self) -> Dict[str, Type]: def _set_context_variable(self, name: str, value: Any): context_vars = self._get_context_variables() context_vars[name] = value - self._context_vars = context_vars + self.__context_vars__ = context_vars def _set_context_variables(self, variables: Dict[str, Any]): - self._context_vars = variables + self.__context_vars__ = variables def _get_context_variables(self) -> Dict[str, Any]: - if not hasattr(self, "_context_vars"): + if not hasattr(self, "__context_vars__") or self.__context_vars__ is None: return {} - return self._context_vars + return self.__context_vars__ def _delete_context_variables(self): - delattr(self, "_context_vars") + delattr(self, "__context_vars__") @contextlib.contextmanager def with_context(self, **variables: Any): diff --git a/src/evidently/presets/classification.py b/src/evidently/presets/classification.py index 2551cf667d..668cfed82b 100644 --- a/src/evidently/presets/classification.py +++ b/src/evidently/presets/classification.py @@ -3,8 +3,6 @@ from typing import Sequence from typing import Tuple -from pydantic import PrivateAttr - from evidently.core.container import MetricContainer from evidently.core.container import MetricOrContainer from evidently.core.datasets import BinaryClassification @@ -414,11 +412,11 @@ class ClassificationPreset(MetricContainer): classification_name: str = "default" """Name of the classification task.""" - _quality: ClassificationQuality = PrivateAttr() + __quality__: Optional[ClassificationQuality] = None """Internal classification quality preset.""" - _quality_by_label: ClassificationQualityByLabel = PrivateAttr() + __quality_by_label__: Optional[ClassificationQualityByLabel] = None """Internal classification quality by label preset.""" - _roc_auc: Optional[RocAuc] = PrivateAttr() + __roc_auc__: Optional[RocAuc] = None """Internal ROC AUC metric.""" def __init__( @@ -460,7 +458,7 @@ def __init__( rocauc_by_label_tests=convert_tests(rocauc_by_label_tests), classification_name=classification_name, ) - self._quality = ClassificationQuality( + self.__quality__ = ClassificationQuality( probas_threshold=probas_threshold, conf_matrix=True, pr_curve=True, @@ -478,7 +476,7 @@ def __init__( include_tests=include_tests, classification_name=classification_name, ) - self._quality_by_label = ClassificationQualityByLabel( + self.__quality_by_label__ = ClassificationQualityByLabel( probas_threshold=probas_threshold, f1score_tests=f1score_by_label_tests, precision_tests=precision_by_label_tests, @@ -487,15 +485,15 @@ def __init__( include_tests=include_tests, classification_name=classification_name, ) - self._roc_auc = None + self.__roc_auc__ = None def generate_metrics(self, context: "Context") -> Sequence[MetricOrContainer]: classification = context.data_definition.get_classification(self.classification_name) if classification is None: raise ValueError("Cannot use ClassificationPreset without a classification configration") - quality_metrics = self._quality.metrics(context) - self._roc_auc = next((m for m in quality_metrics if isinstance(m, RocAuc)), None) - return quality_metrics + self._quality_by_label.metrics(context) + quality_metrics = self.__quality__.metrics(context) + self.__roc_auc__ = next((m for m in quality_metrics if isinstance(m, RocAuc)), None) + return quality_metrics + self.__quality_by_label__.metrics(context) def render( self, @@ -503,7 +501,7 @@ def render( child_widgets: Optional[List[Tuple[Optional[MetricId], List[BaseWidgetInfo]]]] = None, ) -> List[BaseWidgetInfo]: return ( - self._quality.render(context) - + self._quality_by_label.render(context) - + ([] if self._roc_auc is None else context.get_metric_result(self._roc_auc).get_widgets()) + self.__quality__.render(context) + + self.__quality_by_label__.render(context) + + ([] if self.__roc_auc__ is None else context.get_metric_result(self.__roc_auc__).get_widgets()) ) diff --git a/src/evidently/presets/dataset_stats.py b/src/evidently/presets/dataset_stats.py index 2bb0e27f28..9a026dbd44 100644 --- a/src/evidently/presets/dataset_stats.py +++ b/src/evidently/presets/dataset_stats.py @@ -7,7 +7,6 @@ from typing import Sequence from typing import Tuple -from pydantic import PrivateAttr from pydantic.v1 import validator from evidently.core.base_types import Label @@ -600,9 +599,9 @@ class DataSummaryPreset(MetricContainer): column_tests: Optional[Dict[str, ValueStatsTests]] = None """Optional dictionary mapping column names to ValueStatsTests configurations.""" - _dataset_stats: Optional[DatasetStats] = PrivateAttr(None) + __dataset_stats__: Optional[DatasetStats] = None """Internal dataset stats preset.""" - _text_evals: Optional[TextEvals] = PrivateAttr(None) + __text_evals__: Optional[TextEvals] = None """Internal text evals preset.""" def __init__( @@ -637,7 +636,7 @@ def __init__( def generate_metrics(self, context: Context) -> Sequence[MetricOrContainer]: columns_ = context.data_definition.get_categorical_columns() + context.data_definition.get_numerical_columns() - self._dataset_stats = DatasetStats( + self.__dataset_stats__ = DatasetStats( row_count_tests=self.row_count_tests, column_count_tests=self.column_count_tests, duplicated_row_count_tests=self.duplicated_row_count_tests, @@ -650,16 +649,16 @@ def generate_metrics(self, context: Context) -> Sequence[MetricOrContainer]: dataset_missing_value_count_tests=self.dataset_missing_value_count_tests, include_tests=self.include_tests, ) - self._text_evals = TextEvals( + self.__text_evals__ = TextEvals( self.columns or columns_, column_tests=self.column_tests, include_tests=self.include_tests ) - return self._dataset_stats.metrics(context) + self._text_evals.metrics(context) + return self.__dataset_stats__.metrics(context) + self.__text_evals__.metrics(context) def render( self, context: "Context", child_widgets: Optional[List[Tuple[Optional[MetricId], List[BaseWidgetInfo]]]] = None, ) -> List[BaseWidgetInfo]: - if self._dataset_stats is None or self._text_evals is None: + if self.__dataset_stats__ is None or self.__text_evals__ is None: raise ValueError("Inconsistent internal state for DataSummaryPreset") - return self._dataset_stats.render(context) + self._text_evals.render(context) + return self.__dataset_stats__.render(context) + self.__text_evals__.render(context) diff --git a/src/evidently/presets/regression.py b/src/evidently/presets/regression.py index 88125a40ae..860d05cc0d 100644 --- a/src/evidently/presets/regression.py +++ b/src/evidently/presets/regression.py @@ -3,8 +3,6 @@ from typing import Sequence from typing import Tuple -from pydantic import PrivateAttr - from evidently.core.container import MetricContainer from evidently.core.container import MetricOrContainer from evidently.core.metric_types import GenericSingleValueMetricTests @@ -220,7 +218,7 @@ class RegressionPreset(MetricContainer): abs_max_error_tests: SingleValueMetricTests = None """Optional test conditions for absolute max error.""" - _quality: Optional[RegressionQuality] = PrivateAttr(None) + __quality__: Optional[RegressionQuality] = None """Internal regression quality preset.""" regression_name: str = "default" """Name of the regression task.""" @@ -236,7 +234,6 @@ def __init__( regression_name: str = "default", include_tests: bool = True, ): - self._quality = None self.mean_error_tests = convert_to_mean_tests(mean_error_tests) or MeanStdMetricTests() self.mape_tests = convert_to_mean_tests(mape_tests) or MeanStdMetricTests() self.rmse_tests = convert_tests(rmse_tests) @@ -245,9 +242,10 @@ def __init__( self.abs_max_error_tests = convert_tests(abs_max_error_tests) self.regression_name = regression_name super().__init__(include_tests=include_tests) + self.__quality__ = None def generate_metrics(self, context: Context) -> Sequence[MetricOrContainer]: - self._quality = RegressionQuality( + self.__quality__ = RegressionQuality( True, True, True, @@ -261,7 +259,7 @@ def generate_metrics(self, context: Context) -> Sequence[MetricOrContainer]: regression_name=self.regression_name, ) return ( - self._quality.metrics(context) + self.__quality__.metrics(context) + [ # MAPE(mean_tests=self._get_tests(self.mape_tests.mean), std_tests=self._get_tests(self.mape_tests.std)), # AbsMaxError(tests=self._get_tests(self.abs_max_error_tests)), @@ -274,10 +272,10 @@ def render( context: "Context", child_widgets: Optional[List[Tuple[Optional[MetricId], List[BaseWidgetInfo]]]] = None, ) -> List[BaseWidgetInfo]: - if self._quality is None: - raise ValueError("No _quality set in preset, something went wrong.") + if self.__quality__ is None: + raise ValueError("No __quality__ set in preset, something went wrong.") return ( - self._quality.render(context) + self.__quality__.render(context) + context.get_metric_result( MAPE( regression_name=self.regression_name, diff --git a/src/evidently/pydantic_utils.py b/src/evidently/pydantic_utils.py index bd25e57211..2cac166117 100644 --- a/src/evidently/pydantic_utils.py +++ b/src/evidently/pydantic_utils.py @@ -143,7 +143,9 @@ def __setattr__(self, key, value): def __hash__(self): try: - return hash(self.__class__) + hash(tuple(self._field_hash(v) for v in self.__dict__.values())) + return hash(self.__class__) + hash( + tuple(self._field_hash(v) for k, v in self.__dict__.items() if k in self.model_fields) + ) except TypeError: raise @@ -749,6 +751,15 @@ def get_field_inner_type(field: PydanticFieldInfo) -> Type: return typ +def get_field_outer_type(field: PydanticFieldInfo) -> Type: + typ = field.annotation + if get_origin(typ) is Union: + args = get_args(typ) + if len(args) == 2 and type(None) in args: + typ = [a for a in args if a is not type(None)][0] + return typ + + def force_keys_to_str(value: Optional[dict[Any, Any]]) -> Optional[dict[str, Any]]: if value is None: return None diff --git a/src/evidently/sdk/artifacts.py b/src/evidently/sdk/artifacts.py index bc4f9a11e0..90998cb728 100644 --- a/src/evidently/sdk/artifacts.py +++ b/src/evidently/sdk/artifacts.py @@ -18,7 +18,6 @@ from pydantic import BaseModel from pydantic import Field -from pydantic import PrivateAttr from pydantic import TypeAdapter from evidently.errors import EvidentlyError @@ -233,7 +232,7 @@ class RemoteArtifact(Artifact): and manage remote artifacts. """ - _api: "ArtifactAPI" = PrivateAttr() + __api__: Optional["ArtifactAPI"] = None id: ArtifactID = ZERO_UUID """Unique artifact identifier.""" @@ -253,7 +252,7 @@ def bind(self, api: "ArtifactAPI") -> "RemoteArtifact": Returns: * Self for method chaining. """ - self._api = api + self.__api__ = api return self def list_versions(self) -> List[ArtifactVersion]: @@ -262,7 +261,7 @@ def list_versions(self) -> List[ArtifactVersion]: Returns: * List of `ArtifactVersion` objects. """ - return self._api.list_versions(self.id) + return self.__api__.list_versions(self.id) def get_version(self, version: VersionOrLatest = "latest") -> ArtifactVersion: """Get a specific version of this artifact. @@ -273,7 +272,7 @@ def get_version(self, version: VersionOrLatest = "latest") -> ArtifactVersion: Returns: * `ArtifactVersion` for the specified version. """ - return self._api.get_version(self.id, version) + return self.__api__.get_version(self.id, version) def bump_version(self, content: Any): """Create a new version with the given content. @@ -284,7 +283,7 @@ def bump_version(self, content: Any): Returns: * New `ArtifactVersion` with incremented version number. """ - return self._api.bump_artifact_version(self.id, content) + return self.__api__.bump_artifact_version(self.id, content) def delete(self): """Delete this artifact and all its versions. @@ -292,7 +291,7 @@ def delete(self): This operation is irreversible and will permanently remove the artifact and all associated versions from the workspace. """ - return self._api.delete_artifact(self.id) + return self.__api__.delete_artifact(self.id) def delete_version(self, version_id: ArtifactVersionID): """Delete a specific version. @@ -302,14 +301,14 @@ def delete_version(self, version_id: ArtifactVersionID): Args: * `version_id`: ID of the version to delete. """ - return self._api.delete_version(version_id) + return self.__api__.delete_version(version_id) def save(self): """Save changes to this artifact's metadata to the remote workspace. Updates the artifact's name and metadata fields. Does not affect versions. """ - self._api.update_artifact(self) + self.__api__.update_artifact(self) class ArtifactAPI(ABC): diff --git a/src/evidently/sdk/configs.py b/src/evidently/sdk/configs.py index 672bfb33af..ca3b532401 100644 --- a/src/evidently/sdk/configs.py +++ b/src/evidently/sdk/configs.py @@ -16,7 +16,6 @@ from typing import TypeVar from pydantic import Field -from pydantic import PrivateAttr from evidently.core.datasets import Descriptor from evidently.errors import EvidentlyError @@ -49,7 +48,7 @@ class RemoteGenericConfig(GenericConfig): and manage remote configs. """ - _api: "ConfigAPI" = PrivateAttr() + __api__: "ConfigAPI" id: ArtifactID = ZERO_UUID """Unique config identifier.""" @@ -69,7 +68,7 @@ def bind(self, api: "ConfigAPI") -> "RemoteGenericConfig": Returns: * Self for method chaining. """ - self._api = api + self.__api__ = api return self def list_versions(self) -> List[ConfigVersion]: @@ -78,7 +77,7 @@ def list_versions(self) -> List[ConfigVersion]: Returns: * List of `ConfigVersion` objects. """ - return self._api.list_versions(self.id) + return self.__api__.list_versions(self.id) def get_version(self, version: VersionOrLatest = "latest") -> ConfigVersion: """Get a specific version of this config. @@ -89,7 +88,7 @@ def get_version(self, version: VersionOrLatest = "latest") -> ConfigVersion: Returns: * `ConfigVersion` for the specified version. """ - return self._api.get_version(self.id, version) + return self.__api__.get_version(self.id, version) def bump_version(self, content: Any): """Create a new version with the given content. @@ -100,7 +99,7 @@ def bump_version(self, content: Any): Returns: * New `ConfigVersion` with incremented version number. """ - return self._api.bump_config_version(self.id, content) + return self.__api__.bump_config_version(self.id, content) def delete(self): """Delete this config and all its versions. @@ -108,7 +107,7 @@ def delete(self): This operation is irreversible and will permanently remove the config and all associated versions from the workspace. """ - return self._api.delete_config(self.id) + return self.__api__.delete_config(self.id) def delete_version(self, version_id: ConfigVersionID): """Delete a specific version. @@ -118,14 +117,14 @@ def delete_version(self, version_id: ConfigVersionID): Args: * `version_id`: ID of the version to delete. """ - return self._api.delete_version(version_id) + return self.__api__.delete_version(version_id) def save(self): """Save changes to this config's metadata to the remote workspace. Updates the config's name and metadata fields. Does not affect versions. """ - self._api.update_config(self) + self.__api__.update_config(self) class DescriptorContent(ConfigContent[Descriptor]): diff --git a/src/evidently/sdk/prompts.py b/src/evidently/sdk/prompts.py index b3a67c9f11..41acf5d117 100644 --- a/src/evidently/sdk/prompts.py +++ b/src/evidently/sdk/prompts.py @@ -12,7 +12,6 @@ from pydantic import BaseModel from pydantic import Field -from pydantic import PrivateAttr from pydantic import TypeAdapter from pydantic import ValidationError @@ -142,7 +141,7 @@ class RemotePrompt(Prompt): and manage remote prompts. """ - _api: "PromptAPI" = PrivateAttr() + __api__: Optional["PromptAPI"] = None id: PromptID = ZERO_UUID """Unique prompt identifier.""" @@ -162,7 +161,7 @@ def bind(self, api: "PromptAPI") -> "RemotePrompt": Returns: * Self for method chaining. """ - self._api = api + self.__api__ = api return self def list_versions(self) -> List[PromptVersion]: @@ -171,7 +170,7 @@ def list_versions(self) -> List[PromptVersion]: Returns: * List of `PromptVersion` objects. """ - return self._api.list_versions(self.id) + return self.__api__.list_versions(self.id) def get_version(self, version: VersionOrLatest = "latest") -> PromptVersion: """Get a specific version of this prompt. @@ -182,7 +181,7 @@ def get_version(self, version: VersionOrLatest = "latest") -> PromptVersion: Returns: * `PromptVersion` for the specified version. """ - return self._api.get_version(self.id, version) + return self.__api__.get_version(self.id, version) def bump_version(self, content: Any): """Create a new version with the given content. @@ -193,7 +192,7 @@ def bump_version(self, content: Any): Returns: * New `PromptVersion` with incremented version number. """ - return self._api.bump_prompt_version(self.id, content) + return self.__api__.bump_prompt_version(self.id, content) def delete(self): """Delete this prompt and all its versions. @@ -201,7 +200,7 @@ def delete(self): This operation is irreversible and will permanently remove the prompt and all associated versions from the workspace. """ - return self._api.delete_prompt(self.id) + return self.__api__.delete_prompt(self.id) def delete_version(self, version_id: PromptVersionID): """Delete a specific version. @@ -211,14 +210,14 @@ def delete_version(self, version_id: PromptVersionID): Args: * `version_id`: ID of the version to delete. """ - return self._api.delete_version(version_id) + return self.__api__.delete_version(version_id) def save(self): """Save changes to this prompt's metadata to the remote workspace. Updates the prompt's name and metadata fields. Does not affect versions. """ - self._api.update_prompt(self) + self.__api__.update_prompt(self) class PromptAPI(ABC): diff --git a/src/evidently/ui/runner/core.py b/src/evidently/ui/runner/core.py index f4fad394b4..8971da9599 100644 --- a/src/evidently/ui/runner/core.py +++ b/src/evidently/ui/runner/core.py @@ -9,7 +9,6 @@ from typing import Union from pydantic import BaseModel -from pydantic import PrivateAttr from evidently.ui.runner.utils import get_url_to_service_by_port from evidently.ui.runner.utils import is_service_running @@ -185,33 +184,33 @@ class _EvidentlyUIRunnerImpl(BaseModel): port: Optional[int] workspace: Optional[str] demo_projects: Optional[List[DemoProjectNamesForCliType]] - # Internal state: caches the service execution result after run() is called - _run_info: RunServiceInfo = PrivateAttr(default_factory=lambda: RunServiceInfoVariants.NotRunning()) - _atexit_handler: Optional[Callable[[], object]] = PrivateAttr(default=None) + # Internal state: caches the service execution result after run() is called (not a model field) + __run_info__: RunServiceInfo = RunServiceInfoVariants.NotRunning() + __atexit_handler__: Optional[Callable[[], object]] = None def run(self, *, force: bool) -> RunServiceInfo: if force: self._stop_service() - if isinstance(self._run_info, RunServiceInfoVariants.Success): - return self._run_info + if isinstance(self.__run_info__, RunServiceInfoVariants.Success): + return self.__run_info__ if self.port: - self._run_info = self._run_evidently_service_on_port(port=self.port) - logger.info(self._run_info) - return self._run_info + self.__run_info__ = self._run_evidently_service_on_port(port=self.port) + logger.info(self.__run_info__) + return self.__run_info__ # by default try to run on ports 8000-8005 - self._run_info = self._run_evidently_service_try_on_ports_range(ports=list(range(8000, 8006))) - logger.info(self._run_info) - return self._run_info + self.__run_info__ = self._run_evidently_service_try_on_ports_range(ports=list(range(8000, 8006))) + logger.info(self.__run_info__) + return self.__run_info__ def show_service(self, *, page: ServicePage = None): - if not isinstance(self._run_info, RunServiceInfoVariants.Success): - self._run_info.raise_on_error() - raise ServiceRunnerError(f"Unexpected status: {self._run_info}") + if not isinstance(self.__run_info__, RunServiceInfoVariants.Success): + self.__run_info__.raise_on_error() + raise ServiceRunnerError(f"Unexpected status: {self.__run_info__}") - port = self._run_info.port + port = self.__run_info__.port service_url = get_url_to_service_by_port(port=port) page = page or ServicePage() @@ -220,32 +219,32 @@ def show_service(self, *, page: ServicePage = None): return _ServiceIframeHandler(service_url) def get_service_url(self) -> str: - if not isinstance(self._run_info, RunServiceInfoVariants.Success): - self._run_info.raise_on_error() - raise ServiceRunnerError(f"Unexpected status: {self._run_info}") + if not isinstance(self.__run_info__, RunServiceInfoVariants.Success): + self.__run_info__.raise_on_error() + raise ServiceRunnerError(f"Unexpected status: {self.__run_info__}") - return get_url_to_service_by_port(port=self._run_info.port) + return get_url_to_service_by_port(port=self.__run_info__.port) def terminate(self): - self._run_info.raise_on_error() + self.__run_info__.raise_on_error() self._stop_service() def _stop_service(self): - if isinstance(self._run_info, RunServiceInfoVariants.Success): - terminate_process(self._run_info.process) + if isinstance(self.__run_info__, RunServiceInfoVariants.Success): + terminate_process(self.__run_info__.process) - self._run_info = RunServiceInfoVariants.NotRunning() + self.__run_info__ = RunServiceInfoVariants.NotRunning() - if self._atexit_handler: - atexit.unregister(self._atexit_handler) - self._atexit_handler = None + if self.__atexit_handler__: + atexit.unregister(self.__atexit_handler__) + self.__atexit_handler__ = None def get_workspace(self) -> RemoteWorkspace: - if not isinstance(self._run_info, RunServiceInfoVariants.Success): - self._run_info.raise_on_error() - raise ServiceRunnerError(f"Unexpected status: {self._run_info}") + if not isinstance(self.__run_info__, RunServiceInfoVariants.Success): + self.__run_info__.raise_on_error() + raise ServiceRunnerError(f"Unexpected status: {self.__run_info__}") - return RemoteWorkspace(base_url=get_url_to_service_by_port(port=self._run_info.port)) + return RemoteWorkspace(base_url=get_url_to_service_by_port(port=self.__run_info__.port)) def _run_evidently_service_on_port( self, *, port: int, max_wait_time_in_seconds: int = 10, time_step: float = 0.5 @@ -265,9 +264,9 @@ def _run_evidently_service_on_port( def _terminate_process(): terminate_process(process) - self._run_info = RunServiceInfoVariants.NotRunning() + self.__run_info__ = RunServiceInfoVariants.NotRunning() - self._atexit_handler = _terminate_process + self.__atexit_handler__ = _terminate_process atexit.register(_terminate_process) # wait for service to start diff --git a/src/evidently/ui/service/base.py b/src/evidently/ui/service/base.py index 40bfbb4b3e..e809055b64 100644 --- a/src/evidently/ui/service/base.py +++ b/src/evidently/ui/service/base.py @@ -18,7 +18,6 @@ import uuid6 from pydantic import BaseModel from pydantic import Field -from pydantic import PrivateAttr from evidently.core.report import Snapshot as SnapshotV2 from evidently.core.serialization import SnapshotModel @@ -143,48 +142,48 @@ class Project(Entity): version: str = "1" # Field(default=datetime.datetime.fromisoformat("1900-01-01T00:00:00")) - _project_manager: Optional["ProjectManager"] = PrivateAttr(default=None) - _user_id: Optional[UserID] = PrivateAttr(default=None) + __project_manager__: Optional["ProjectManager"] = None + __user_id__: Optional[UserID] = None def bind(self, project_manager: Optional["ProjectManager"], user_id: Optional[UserID]): - self._project_manager = project_manager - self._user_id = user_id + self.__project_manager__ = project_manager + self.__user_id__ = user_id return self @property def project_manager(self) -> "ProjectManager": - if self._project_manager is None: + if self.__project_manager__ is None: raise ValueError("Project is not binded") - return self._project_manager + return self.__project_manager__ async def save_async(self): - await self.project_manager.update_project(self._user_id, self) # type: ignore[arg-type] + await self.project_manager.update_project(self.__user_id__, self) # type: ignore[arg-type] return self async def load_snapshot_async(self, snapshot_id: SnapshotID) -> SnapshotModel: - return await self.project_manager.load_snapshot(self._user_id, self.id, snapshot_id) # type: ignore[arg-type] + return await self.project_manager.load_snapshot(self.__user_id__, self.id, snapshot_id) # type: ignore[arg-type] async def add_snapshot_async(self, snapshot: SnapshotModel): - await self.project_manager.add_snapshot(self._user_id, self.id, snapshot) # type: ignore[arg-type] + await self.project_manager.add_snapshot(self.__user_id__, self.id, snapshot) # type: ignore[arg-type] async def delete_snapshot_async(self, snapshot_id: Union[str, SnapshotID]): if isinstance(snapshot_id, str): snapshot_id = uuid6.UUID(snapshot_id) - await self.project_manager.delete_snapshot(self._user_id, self.id, snapshot_id) # type: ignore[arg-type] + await self.project_manager.delete_snapshot(self.__user_id__, self.id, snapshot_id) # type: ignore[arg-type] async def list_snapshots_async(self) -> List[SnapshotMetadataModel]: - return await self.project_manager.list_snapshots(self._user_id, self.id) # type: ignore[arg-type] + return await self.project_manager.list_snapshots(self.__user_id__, self.id) # type: ignore[arg-type] async def get_snapshot_metadata_async(self, id: SnapshotID) -> SnapshotMetadataModel: - return await self.project_manager.get_snapshot_metadata(self._user_id, self.id, id) # type: ignore[arg-type] + return await self.project_manager.get_snapshot_metadata(self.__user_id__, self.id, id) # type: ignore[arg-type] async def reload_async(self, reload_snapshots: bool = False): # fixme: reload snapshots - project = await self.project_manager.get_project(self._user_id, self.id) # type: ignore[arg-type] + project = await self.project_manager.get_project(self.__user_id__, self.id) # type: ignore[arg-type] self.__dict__.update(project.__dict__) if reload_snapshots: - await self.project_manager.reload_snapshots(self._user_id, self.id) # type: ignore[arg-type] + await self.project_manager.reload_snapshots(self.__user_id__, self.id) # type: ignore[arg-type] save = sync_api(save_async) load_snapshot = sync_api(load_snapshot_async) diff --git a/src/evidently/ui/service/config.py b/src/evidently/ui/service/config.py index d676a02fdb..57c2abd1bf 100644 --- a/src/evidently/ui/service/config.py +++ b/src/evidently/ui/service/config.py @@ -14,7 +14,6 @@ from litestar import Litestar from litestar.di import Provide from pydantic import BaseModel -from pydantic import PrivateAttr from pydantic import TypeAdapter from evidently.ui.service.components.base import SECTION_COMPONENT_TYPE_MAPPING @@ -90,12 +89,12 @@ def validate(self): class Config(BaseModel): additional_components: Dict[str, Component] = {} - _components: List[Component] = PrivateAttr(default_factory=list) - _ctx: ComponentContext = PrivateAttr() + __components__: Optional[List[Component]] = None # not a model field, internal only + __ctx__: Optional[ComponentContext] = None @property def components(self) -> List[Component]: - return [getattr(self, name) for name in self.__fields__ if isinstance(getattr(self, name), Component)] + list( + return [getattr(self, name) for name in self.model_fields if isinstance(getattr(self, name), Component)] + list( self.additional_components.values() ) @@ -103,9 +102,9 @@ def components(self) -> List[Component]: def context(self) -> Iterator[ConfigContext]: ctx = ConfigContext(self, {type(c): c for c in self.components}) ctx.validate() - self._ctx = ctx + self.__ctx__ = ctx yield ctx - del self._ctx + del self.__ctx__ class AppConfig(Config): diff --git a/src/evidently/ui/service/storage/local/artifacts.py b/src/evidently/ui/service/storage/local/artifacts.py index a4dbb8f4ec..c84f449a17 100644 --- a/src/evidently/ui/service/storage/local/artifacts.py +++ b/src/evidently/ui/service/storage/local/artifacts.py @@ -28,7 +28,7 @@ class ArtifactStorageState(BaseModel): class FileArtifactStorage(ArtifactStorage): base_path: str - _location: Optional[FSLocation] = None + __location__: Optional[FSLocation] = None def __init__(self, base_path: str): self.base_path = base_path @@ -36,9 +36,9 @@ def __init__(self, base_path: str): @property def location(self) -> FSLocation: - if self._location is None: - self._location = FSLocation(self.base_path) - return self._location + if self.__location__ is None: + self.__location__ = FSLocation(self.base_path) + return self.__location__ def _get_artifact_metadata_path(self, project_id: ProjectID, artifact_id: ArtifactID) -> str: return posixpath.join(str(project_id), ARTIFACTS_DIR, str(artifact_id), "metadata.json") diff --git a/src/evidently/ui/service/storage/local/base.py b/src/evidently/ui/service/storage/local/base.py index d554a52e25..adde388092 100644 --- a/src/evidently/ui/service/storage/local/base.py +++ b/src/evidently/ui/service/storage/local/base.py @@ -13,7 +13,6 @@ import uuid6 from pydantic import BaseModel -from pydantic import PrivateAttr from pydantic import TypeAdapter from pydantic import ValidationError @@ -59,17 +58,17 @@ class FSSpecBlobStorage(BlobStorage): base_path: str - _location: Optional[FSLocation] = PrivateAttr(default=None) + __location__: Optional[FSLocation] = None def __init__(self, base_path: str): self.base_path = base_path - self._location = FSLocation(self.base_path) + self.__location__ = FSLocation(self.base_path) @property def location(self) -> FSLocation: - if self._location is None: - self._location = FSLocation(self.base_path) - return self._location + if self.__location__ is None: + self.__location__ = FSLocation(self.base_path) + return self.__location__ def get_snapshot_blob_id(self, project_id: ProjectID, snapshot: Snapshot) -> BlobID: return posixpath.join(str(project_id), SNAPSHOTS, str(snapshot.id)) + ".json" @@ -166,17 +165,17 @@ def reload_snapshot(self, project: Project, snapshot_id: SnapshotID, skip_errors class JsonFileProjectMetadataStorage(ProjectMetadataStorage): path: str - _state: Optional[LocalState] = PrivateAttr(default=None) + __state__: Optional[LocalState] = None def __init__(self, path: str, local_state: Optional[LocalState] = None): self.path = path - self._state = local_state or LocalState.load(self.path, None) + self.__state__ = local_state or LocalState.load(self.path, None) @property def state(self) -> LocalState: - if self._state is None: - self._state = LocalState.load(self.path, None) - return self._state + if self.__state__ is None: + self.__state__ = LocalState.load(self.path, None) + return self.__state__ async def add_project(self, project: Project, user: User, org_id: Optional[OrgID] = None) -> Project: project_id = str(project.id) @@ -274,31 +273,31 @@ class MetricItem(BaseModel): class InMemoryDataStorage(DataStorage): path: str - _state: Optional[LocalState] = PrivateAttr(default=None) - _metrics_points: Optional[Dict[uuid.UUID, Dict[uuid.UUID, List[MetricItem]]]] = PrivateAttr(default=None) + __state__: Optional[LocalState] = None + __metrics_points__: Optional[Dict[uuid.UUID, Dict[uuid.UUID, List[MetricItem]]]] = None def __init__(self, path: str, local_state: Optional[LocalState] = None): self.path = path - self._state = local_state or LocalState.load(self.path, None) - self._metrics_points = {} - for project_id, snapshots in self._state.snapshots.items(): + self.__state__ = local_state or LocalState.load(self.path, None) + self.__metrics_points__ = {} + for project_id, snapshots in self.__state__.snapshots.items(): for snapshot_id, snapshot in snapshots.items(): self._add_snapshot_points_sync(project_id, snapshot_id, snapshot) - self._state.register_new_snapshot_callback(self._add_snapshot_points_sync) + self.__state__.register_new_snapshot_callback(self._add_snapshot_points_sync) @property def state(self): - if self._state is None: - self._state = LocalState.load(self.path, None) - return self._state + if self.__state__ is None: + self.__state__ = LocalState.load(self.path, None) + return self.__state__ async def add_snapshot_points(self, project_id: ProjectID, snapshot_id: SnapshotID, snapshot: SnapshotModel): return self._add_snapshot_points_sync(project_id, snapshot_id, snapshot) def _add_snapshot_points_sync(self, project_id: ProjectID, snapshot_id: SnapshotID, snapshot: SnapshotModel): - if self._metrics_points is None: - self._metrics_points = {} - points = self._metrics_points + if self.__metrics_points__ is None: + self.__metrics_points__ = {} + points = self.__metrics_points__ if project_id in points and snapshot_id in points[project_id]: points[project_id][snapshot_id] = [] for result in snapshot.metric_results.values(): @@ -342,9 +341,9 @@ def _add_value( if k in ["type", "tests", "count_tests", "share_tests", "mean_tests", "std_tests"]: continue params[k] = str(v) - if self._metrics_points is None: - self._metrics_points = {} - points = self._metrics_points + if self.__metrics_points__ is None: + self.__metrics_points__ = {} + points = self.__metrics_points__ if project_id not in points: points[project_id] = {} if snapshot_id not in points[project_id]: @@ -427,7 +426,7 @@ async def get_data_series( last_snapshot = None series_filters_map: Dict[tuple, int] = {} index = 0 - points = self._metrics_points if self._metrics_points is not None else {} + points = self.__metrics_points__ if self.__metrics_points__ is not None else {} for snapshot_id, timestamp, snapshot in matching_snapshots: for item in points.get(project_id, {}).get(snapshot_id, []): metric_type = item.metric_type diff --git a/tests/future/descriptors/test_conditions.py b/tests/future/descriptors/test_conditions.py index e105982c81..dd2b56541a 100644 --- a/tests/future/descriptors/test_conditions.py +++ b/tests/future/descriptors/test_conditions.py @@ -46,13 +46,13 @@ pd.Series([True, False, False]), ), ( - EqualsColumnCondition(expected=2.0), + EqualsColumnCondition(expected=2), pd.Series([1, 2, 3], name="input"), "input: equals 2", pd.Series([False, True, False]), ), ( - NotEqualsColumnCondition(expected=2.0), + NotEqualsColumnCondition(expected=2), pd.Series([1, 2, 3], name="input"), "input not equals 2", pd.Series([True, False, True]), diff --git a/tests/future/descriptors/test_descriptors.py b/tests/future/descriptors/test_descriptors.py index 886882f081..24d96a429c 100644 --- a/tests/future/descriptors/test_descriptors.py +++ b/tests/future/descriptors/test_descriptors.py @@ -16,7 +16,6 @@ from evidently.core.datasets import Dataset from evidently.core.datasets import DatasetColumn from evidently.core.datasets import Descriptor -from evidently.core.datasets import FeatureDescriptor from evidently.core.datasets import TestSummary from evidently.descriptors import ContextRelevance from evidently.descriptors import CustomColumnDescriptor @@ -37,8 +36,6 @@ from evidently.tests import eq from tests.conftest import load_all_subtypes -from .test_feature_descriptors import MockGeneratedFeature - int_data = pd.Series([1, 2, 3], name="int") str_data = pd.Series(["a", "b", "c"], name="str") @@ -104,11 +101,11 @@ def semantic_scoring_mock(question: DatasetColumn, context: DatasetColumn, optio all_descriptors: List[Tuple[Descriptor, Union[pd.Series, pd.DataFrame], Dict[str, pd.Series]]] = [ - ( - FeatureDescriptor(feature=MockGeneratedFeature(column="str", field="a"), alias="res"), - str_data, - {"a1702de9f83a993ea3cb4701ca9d17f7.str": pd.Series(["aa", "ba", "ca"])}, - ), + # ( + # FeatureDescriptor(feature=MockGeneratedFeature(column="str", field="a"), alias="res"), + # str_data, + # {"a1702de9f83a993ea3cb4701ca9d17f7.str": pd.Series(["aa", "ba", "ca"])}, + # ), ( LLMJudge(provider="mock_d", model="", template=MockTemplate(), input_columns={"aaa": "data"}, alias="res"), pd.DataFrame({"aaa": ["x", "y"]}), @@ -205,4 +202,5 @@ def test_descriptors(descriptor: Descriptor, data: Union[pd.Series, pd.DataFrame payload = json.loads(descriptor.json()) descriptor2 = parse_obj_as(Descriptor, payload) + assert json.loads(descriptor2.json()) == payload assert descriptor2 == descriptor diff --git a/tests/future/generators/test_generator.py b/tests/future/generators/test_generator.py index 19ee2f8015..30276ccb0a 100644 --- a/tests/future/generators/test_generator.py +++ b/tests/future/generators/test_generator.py @@ -14,7 +14,7 @@ def test_generator_renders(): def test_generator_kwargs(): - generator = ColumnMetricGenerator(ValueStats, columns=["a", "b"], metric_kwargs={"tests": [gt(0)]}) - generator2 = ColumnMetricGenerator(ValueStats, columns=["a", "b"], tests=[gt(0)]) + generator = ColumnMetricGenerator(ValueStats, columns=["a", "b"], metric_kwargs={"min_tests": [gt(0)]}) + generator2 = ColumnMetricGenerator(ValueStats, columns=["a", "b"], min_tests=[gt(0)]) assert generator.dict() == generator2.dict() diff --git a/tests/future/metrics/all_metrics_tests.py b/tests/future/metrics/all_metrics_tests.py index 150118ff8b..a2f8be386f 100644 --- a/tests/future/metrics/all_metrics_tests.py +++ b/tests/future/metrics/all_metrics_tests.py @@ -657,14 +657,14 @@ (simple_dataset, InListValueCount(column="a", values=[1], share_tests=[gt(555)]), TestStatus.FAIL), (simple_dataset, InListValueCount(column="a", values=[1], share_tests=[gt(0)]), TestStatus.SUCCESS), (simple_dataset, InListValueCount(column="a", values=[1], share_tests=[is_in([0])]), TestStatus.FAIL), - (simple_dataset, InListValueCount(column="a", values=[0.3333333333333333], share_tests=[is_in([0.3333333333333333])]), TestStatus.SUCCESS), + (simple_dataset, InListValueCount(column="a", values=[0.3333333333333333], share_tests=[is_in([0])]), TestStatus.SUCCESS), (simple_dataset, InListValueCount(column="a", values=[1], share_tests=[lte(0)]), TestStatus.FAIL), (simple_dataset, InListValueCount(column="a", values=[1], share_tests=[lte(1)]), TestStatus.SUCCESS), (simple_dataset, InListValueCount(column="a", values=[1], share_tests=[lt(0)]), TestStatus.FAIL), (simple_dataset, InListValueCount(column="a", values=[1], share_tests=[lt(555)]), TestStatus.SUCCESS), (simple_dataset, InListValueCount(column="a", values=[0], share_tests=[not_eq(ApproxValue(0.3333333333333333, absolute=0.01))]), TestStatus.FAIL), (simple_dataset, InListValueCount(column="a", values=[1], share_tests=[not_eq(0)]), TestStatus.SUCCESS), - (simple_dataset, InListValueCount(column="a", values=[0.3333333333333333], share_tests=[not_in([0.3333333333333333])]), TestStatus.FAIL), + (simple_dataset, InListValueCount(column="a", values=[0.3333333333333333], share_tests=[not_in([0])]), TestStatus.FAIL), (simple_dataset, InListValueCount(column="a", values=[1], share_tests=[not_in([0])]), TestStatus.SUCCESS), (simple_dataset, InListValueCount(column="a", values=[1], tests=[eq(0)]), TestStatus.FAIL), (simple_dataset, InListValueCount(column="a", values=[1], tests=[eq(ApproxValue(1.000, absolute=0.01))]), TestStatus.SUCCESS), @@ -945,14 +945,14 @@ (simple_dataset, OutListValueCount(column="a", values=[1], share_tests=[gt(555)]), TestStatus.FAIL), (simple_dataset, OutListValueCount(column="a", values=[1], share_tests=[gt(0)]), TestStatus.SUCCESS), (simple_dataset, OutListValueCount(column="a", values=[1], share_tests=[is_in([0])]), TestStatus.FAIL), - (simple_dataset, OutListValueCount(column="a", values=[0.6666666666666666], share_tests=[is_in([0.6666666666666666])]), TestStatus.SUCCESS,), + (simple_dataset, OutListValueCount(column="a", values=[0.6666666666666666], share_tests=[is_in([1])]), TestStatus.SUCCESS,), (simple_dataset, OutListValueCount(column="a", values=[1], share_tests=[lte(0)]), TestStatus.FAIL), (simple_dataset, OutListValueCount(column="a", values=[1], share_tests=[lte(1)]), TestStatus.SUCCESS), (simple_dataset, OutListValueCount(column="a", values=[1], share_tests=[lt(0)]), TestStatus.FAIL), (simple_dataset, OutListValueCount(column="a", values=[1], share_tests=[lt(555)]), TestStatus.SUCCESS), (simple_dataset, OutListValueCount(column="a", values=[0], share_tests=[not_eq(ApproxValue(0.6666666666666666, absolute=0.01))]), TestStatus.FAIL,), (simple_dataset, OutListValueCount(column="a", values=[1], share_tests=[not_eq(0)]), TestStatus.SUCCESS), - (simple_dataset, OutListValueCount(column="a", values=[0.6666666666666666], share_tests=[not_in([0.6666666666666666])]),TestStatus.FAIL,), + (simple_dataset, OutListValueCount(column="a", values=[0.6666666666666666], share_tests=[not_in([1])]),TestStatus.FAIL,), (simple_dataset, OutListValueCount(column="a", values=[1], share_tests=[not_in([0])]), TestStatus.SUCCESS), (simple_dataset, OutListValueCount(column="a", values=[1], tests=[eq(0)]), TestStatus.FAIL), (simple_dataset, OutListValueCount(column="a", values=[1], tests=[eq(ApproxValue(2.000, absolute=0.01))]), TestStatus.SUCCESS), diff --git a/tests/future/presets/test_serialization.py b/tests/future/presets/test_serialization.py index ffcc744921..88181bb1ea 100644 --- a/tests/future/presets/test_serialization.py +++ b/tests/future/presets/test_serialization.py @@ -66,4 +66,5 @@ def test_all_presets_tested(): def test_all_presets_json_serialization(preset): payload = json.loads(preset.json()) preset2 = parse_obj_as(MetricContainer, payload) + assert json.loads(preset2.json()) == payload assert preset2 == preset diff --git a/tests/future/presets/test_test_fields.py b/tests/future/presets/test_test_fields.py index b8b386aeb4..d947be6c6b 100644 --- a/tests/future/presets/test_test_fields.py +++ b/tests/future/presets/test_test_fields.py @@ -1,4 +1,5 @@ import dataclasses +import traceback from inspect import isabstract from typing import Any from typing import Callable @@ -85,6 +86,8 @@ from evidently.presets import TextEvals from evidently.presets.dataset_stats import ValueStatsTests from evidently.presets.special import TestSummaryInfoPreset +from evidently.pydantic_utils import get_field_inner_type +from evidently.pydantic_utils import get_field_outer_type from evidently.tests import eq from tests.conftest import load_all_subtypes @@ -254,8 +257,10 @@ def _is_test_field(field_name: str, field: FieldInfo) -> bool: def _get_test_field_instance( field: FieldInfo, check: Union[GenericTest, MetricTest], preset_type: Type[MetricContainer] ): - if get_origin(field.annotation) == dict: - if field.annotation is ValueStatsTests: + outer_type = get_field_outer_type(field) + inner_type = get_field_inner_type(field) + if get_origin(outer_type) == dict: + if inner_type is ValueStatsTests: col = "text_length" return { col: ValueStatsTests( @@ -268,11 +273,11 @@ def _get_test_field_instance( ) } return {"a": [check]} - if get_origin(field.annotation) == list: + if get_origin(outer_type) == list: return [check] - if field.annotation is MeanStdMetricTests: + if inner_type is MeanStdMetricTests: return MeanStdMetricTests(mean=[check], std=[check]) - return NotImplementedError(f"Not implemented for {field.annotation}") + raise NotImplementedError(f"Not implemented for {field.annotation}") def iter_type_test_fields(preset_type: Type[MetricContainer]) -> Iterable[Tuple[str, FieldInfo]]: @@ -292,6 +297,7 @@ def test_preset_type_test_fields(preset_type: Type[MetricContainer], check: Unio try: instance = preset_type(**{field_name: field_instance}) except Exception as e: + traceback.print_exc() errors[field_name] = e continue dataset = Dataset.from_pandas( From ca80717548ac86afbd2f8186f26596f245d7edad Mon Sep 17 00:00:00 2001 From: mike0sv Date: Wed, 18 Feb 2026 01:55:43 +0000 Subject: [PATCH 03/11] bump pydantic --- pyproject.toml | 2 +- requirements.min.txt | 2 +- src/evidently/pydantic_utils.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index eec74b2ca7..223c3b3702 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ dependencies = [ "scipy>=1.10.0", "requests>=2.32.0", "PyYAML>=6.0.1", - "pydantic>=1.10.16", + "pydantic>=2", "litestar>=2.19.0", "typing-inspect>=0.9.0", "uvicorn[standard]>=0.22.0", diff --git a/requirements.min.txt b/requirements.min.txt index 6b91ee6004..c3d3c09f92 100644 --- a/requirements.min.txt +++ b/requirements.min.txt @@ -8,7 +8,7 @@ nltk==3.6.7 scipy==1.10.0 requests==2.32.0 PyYAML==6.0.1 -pydantic==1.10.16 +pydantic==2 litestar==2.19.0 typing-inspect==0.9.0 uvicorn==0.22.0 diff --git a/src/evidently/pydantic_utils.py b/src/evidently/pydantic_utils.py index 2cac166117..151bc82d44 100644 --- a/src/evidently/pydantic_utils.py +++ b/src/evidently/pydantic_utils.py @@ -19,7 +19,6 @@ from typing import List from typing import Literal from typing import Optional -from typing import Self from typing import Set from typing import Tuple from typing import Type @@ -45,6 +44,7 @@ from pydantic_core import core_schema from pydantic_core.core_schema import SerializationInfo from pydantic_core.core_schema import SerializerFunctionWrapHandler +from typing_extensions import Self from typing_inspect import is_union_type if TYPE_CHECKING: From bb89a95906872c91c775ffd3c064753f9c3bb843 Mon Sep 17 00:00:00 2001 From: mike0sv Date: Wed, 18 Feb 2026 11:49:10 +0000 Subject: [PATCH 04/11] bump pydantic fix some mypy --- pyproject.toml | 2 +- .../descriptors/_generate_descriptors.py | 9 ++++++--- .../calculations/classification_performance.py | 4 ++-- src/evidently/legacy/core.py | 13 +++++++++++-- .../roc_curve_metric.py | 4 ++-- .../data_integrity/column_regexp_metric.py | 2 +- src/evidently/legacy/ui/config.py | 12 ++++++------ src/evidently/llm/optimization/optimizer.py | 7 +++++-- src/evidently/sdk/artifacts.py | 18 ++++++++++++------ src/evidently/ui/service/api/datasets.py | 2 +- src/evidently/ui/service/api/projects.py | 2 +- src/evidently/ui/service/config.py | 10 +++++----- 12 files changed, 53 insertions(+), 32 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 223c3b3702..2579111206 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ dependencies = [ "scipy>=1.10.0", "requests>=2.32.0", "PyYAML>=6.0.1", - "pydantic>=2", + "pydantic>=2.10", "litestar>=2.19.0", "typing-inspect>=0.9.0", "uvicorn[standard]>=0.22.0", diff --git a/src/evidently/descriptors/_generate_descriptors.py b/src/evidently/descriptors/_generate_descriptors.py index d00df87ead..594f4583d7 100644 --- a/src/evidently/descriptors/_generate_descriptors.py +++ b/src/evidently/descriptors/_generate_descriptors.py @@ -45,10 +45,13 @@ SKIP_CLASSES = {CustomFeature, CustomPairColumnFeature, CustomSingleColumnFeature, TextLength} -def _get_type_name(tp: Type): - if tp.__module__.startswith("typing"): +def _get_type_name(tp: Optional[Type[Any]]) -> str: + if tp is None: + return "Any" + module = getattr(tp, "__module__", "") + if module and str(module).startswith("typing"): return str(tp).replace("typing.", "") - return tp.__name__ + return getattr(tp, "__name__", str(tp)) # return str(tp) diff --git a/src/evidently/legacy/calculations/classification_performance.py b/src/evidently/legacy/calculations/classification_performance.py index 0c748aedd4..3d448d4f7e 100644 --- a/src/evidently/legacy/calculations/classification_performance.py +++ b/src/evidently/legacy/calculations/classification_performance.py @@ -28,7 +28,7 @@ def calculate_confusion_by_classes( - confusion_matrix: np.ndarray, class_names: Sequence[Union[str, int, None]] + confusion_matrix: np.ndarray, class_names: Sequence[Label] ) -> Dict[Label, Dict[str, int]]: """Calculate metrics: - TP (true positive) @@ -59,7 +59,7 @@ def calculate_confusion_by_classes( false_positive = confusion_matrix.sum(axis=0) - np.diag(confusion_matrix) false_negative = confusion_matrix.sum(axis=1) - np.diag(confusion_matrix) true_negative = confusion_matrix.sum() - (false_positive + false_negative + true_positive) - confusion_by_classes = {} + confusion_by_classes: Dict[Label, Dict[str, int]] = {} for idx, class_name in enumerate(class_names): confusion_by_classes[class_name] = { diff --git a/src/evidently/legacy/core.py b/src/evidently/legacy/core.py index f6779e0871..636fd16367 100644 --- a/src/evidently/legacy/core.py +++ b/src/evidently/legacy/core.py @@ -11,6 +11,7 @@ from typing import Type from typing import TypeVar from typing import Union +from typing import cast from typing import get_args from typing import get_origin @@ -153,7 +154,12 @@ def get_dict( exclude_tags = {IncludeTags.TypeField} if not include_render: exclude_tags.add(IncludeTags.Render) - return self.model_dump(include=include or self._build_include(exclude_tags=exclude_tags), exclude=exclude) + dump_include = include or self._build_include(exclude_tags=exclude_tags) + dump_exclude = exclude + return self.model_dump( + include=cast(Optional[Union[Set[int], Set[str], Dict[str, Any], Dict[int, Any]]], dump_include), + exclude=cast(Optional[Union[Set[int], Set[str], Dict[str, Any], Dict[int, Any]]], dump_exclude), + ) def _build_include( self, @@ -290,7 +296,10 @@ def get_field_tags(cls: Type[BaseModel], field_name: str) -> Set[IncludeTags]: break field = cls.model_fields[field_name] - field_type = _get_actual_type(field.annotation) + field_annotation = field.annotation + if field_annotation is None: + field_annotation = type(None) + field_type = _get_actual_type(field_annotation) self_tags = set() if not issubclass(cls, BaseResult) else (cls.__tags__ or set()) cls_tags = get_cls_tags(field_type) return self_tags.union(field_tags).union(cls_tags) diff --git a/src/evidently/legacy/metrics/classification_performance/roc_curve_metric.py b/src/evidently/legacy/metrics/classification_performance/roc_curve_metric.py index 5185471815..84681c8e97 100644 --- a/src/evidently/legacy/metrics/classification_performance/roc_curve_metric.py +++ b/src/evidently/legacy/metrics/classification_performance/roc_curve_metric.py @@ -2,7 +2,6 @@ from typing import Dict from typing import List from typing import Optional -from typing import Union import numpy as np import pandas as pd @@ -13,6 +12,7 @@ from evidently.legacy.base_metric import MetricResult from evidently.legacy.calculations.classification_performance import get_prediction_data from evidently.legacy.core import IncludeTags +from evidently.legacy.core import Label from evidently.legacy.metric_results import PredictionData from evidently.legacy.metric_results import ROCCurve from evidently.legacy.metric_results import ROCCurveData @@ -74,7 +74,7 @@ def calculate_metrics( target_names: Optional[TargetNames], ) -> ROCCurve: labels = prediction.labels - tn: Dict[Union[int, str, None], str] = {} + tn: Dict[Label, str] = {} if target_names is None: tn = {} elif isinstance(target_names, list): diff --git a/src/evidently/legacy/metrics/data_integrity/column_regexp_metric.py b/src/evidently/legacy/metrics/data_integrity/column_regexp_metric.py index a8e8fe13f2..becd5d6708 100644 --- a/src/evidently/legacy/metrics/data_integrity/column_regexp_metric.py +++ b/src/evidently/legacy/metrics/data_integrity/column_regexp_metric.py @@ -80,7 +80,7 @@ class ColumnRegExpMetric(Metric[DataIntegrityValueByRegexpMetricResult]): reg_exp: str top: int # compiled regular expression for speed optimization - __reg_exp_compiled__: Optional[Pattern] = None + __reg_exp_compiled__: Pattern def __init__(self, column_name: str, reg_exp: str, top: int = 10, options: AnyOptions = None): self.top = top diff --git a/src/evidently/legacy/ui/config.py b/src/evidently/legacy/ui/config.py index 6e5573f735..b7c9d4e722 100644 --- a/src/evidently/legacy/ui/config.py +++ b/src/evidently/legacy/ui/config.py @@ -1,4 +1,5 @@ import contextlib +from typing import Any from typing import Dict from typing import Iterator from typing import List @@ -107,8 +108,8 @@ class AppConfig(Config): def load_config(config_type: Type[TConfig], box: dict) -> TConfig: new_box = _convert_keys(box) - components = {} - named_components = {} + components: Dict[str, Component] = {} + named_components: Dict[str, Any] = {} for section, component_dict in new_box.items(): # todo if not isinstance(component_dict, dict): @@ -118,10 +119,9 @@ def load_config(config_type: Type[TConfig], box: dict) -> TConfig: if section in ("renamed_vars", "dict_itemiterator"): continue if section == "additional_components": - for subsection, compoennt_subdict in component_dict.items(): - component = TypeAdapter( - SECTION_COMPONENT_TYPE_MAPPING.get(subsection).validate_python(Component), compoennt_subdict - ) + for subsection, component_subdict in component_dict.items(): + subsection_type = SECTION_COMPONENT_TYPE_MAPPING.get(subsection, Component) + component: Component = TypeAdapter(subsection_type).validate_python(component_subdict) components[subsection] = component elif section in config_type.__fields__: type_ = config_type.__fields__[section].type_ diff --git a/src/evidently/llm/optimization/optimizer.py b/src/evidently/llm/optimization/optimizer.py index faf2941094..337769be37 100644 --- a/src/evidently/llm/optimization/optimizer.py +++ b/src/evidently/llm/optimization/optimizer.py @@ -446,7 +446,7 @@ class OptimizerRun(BaseModel): """Random seed used for this run.""" start_time: datetime.datetime = Field(default_factory=datetime.datetime.now) """Timestamp when the run started.""" - __context__: Optional["OptimizerContext"] = None + __context__: "OptimizerContext" def bind(self, context: "OptimizerContext") -> "OptimizerRun": """Bind this run to an optimizer context. @@ -467,6 +467,8 @@ def context(self) -> "OptimizerContext": Returns: * `OptimizerContext` associated with this run. """ + if not hasattr(self, "__context__"): + raise RuntimeError("Optimizer context not initialized; call bind() first") return self.__context__ def add_log(self, log: OptimizerLog): @@ -475,7 +477,8 @@ def add_log(self, log: OptimizerLog): Args: * `log`: `OptimizerLog` to add. """ - if self.__context__.config.verbose: + ctx = self.context + if ctx.config.verbose: print(f"[{self.run_id}]", log.message()) self.logs[log.id] = log diff --git a/src/evidently/sdk/artifacts.py b/src/evidently/sdk/artifacts.py index 90998cb728..0a7527b3f0 100644 --- a/src/evidently/sdk/artifacts.py +++ b/src/evidently/sdk/artifacts.py @@ -255,13 +255,19 @@ def bind(self, api: "ArtifactAPI") -> "RemoteArtifact": self.__api__ = api return self + @property + def _api(self) -> "ArtifactAPI": + if self.__api__ is None: + raise RuntimeError("RemoteArtifact not bound to API; call bind() first") + return self.__api__ + def list_versions(self) -> List[ArtifactVersion]: """List all versions of this artifact. Returns: * List of `ArtifactVersion` objects. """ - return self.__api__.list_versions(self.id) + return self._api.list_versions(self.id) def get_version(self, version: VersionOrLatest = "latest") -> ArtifactVersion: """Get a specific version of this artifact. @@ -272,7 +278,7 @@ def get_version(self, version: VersionOrLatest = "latest") -> ArtifactVersion: Returns: * `ArtifactVersion` for the specified version. """ - return self.__api__.get_version(self.id, version) + return self._api.get_version(self.id, version) def bump_version(self, content: Any): """Create a new version with the given content. @@ -283,7 +289,7 @@ def bump_version(self, content: Any): Returns: * New `ArtifactVersion` with incremented version number. """ - return self.__api__.bump_artifact_version(self.id, content) + return self._api.bump_artifact_version(self.id, content) def delete(self): """Delete this artifact and all its versions. @@ -291,7 +297,7 @@ def delete(self): This operation is irreversible and will permanently remove the artifact and all associated versions from the workspace. """ - return self.__api__.delete_artifact(self.id) + return self._api.delete_artifact(self.id) def delete_version(self, version_id: ArtifactVersionID): """Delete a specific version. @@ -301,14 +307,14 @@ def delete_version(self, version_id: ArtifactVersionID): Args: * `version_id`: ID of the version to delete. """ - return self.__api__.delete_version(version_id) + return self._api.delete_version(version_id) def save(self): """Save changes to this artifact's metadata to the remote workspace. Updates the artifact's name and metadata fields. Does not affect versions. """ - self.__api__.update_artifact(self) + self._api.update_artifact(self) class ArtifactAPI(ABC): diff --git a/src/evidently/ui/service/api/datasets.py b/src/evidently/ui/service/api/datasets.py index 36b2d61e95..6b81399c59 100644 --- a/src/evidently/ui/service/api/datasets.py +++ b/src/evidently/ui/service/api/datasets.py @@ -191,7 +191,7 @@ async def get_dataset( """Get a dataset with pagination.""" try: - filter_queries = ( + filter_queries: Optional[List[FilterBy]] = ( [TypeAdapter(FilterBy).validate_python(json.loads(_filter)) for _filter in filters] if filters else None ) except (ValidationError, JSONDecodeError) as e: diff --git a/src/evidently/ui/service/api/projects.py b/src/evidently/ui/service/api/projects.py index 6b19d31269..4383a711b6 100644 --- a/src/evidently/ui/service/api/projects.py +++ b/src/evidently/ui/service/api/projects.py @@ -327,7 +327,7 @@ async def add_snapshot( log_event: Annotated[Callable, Dependency()], user_id: UserID, ) -> AddSnapshotResponse: - model = TypeAdapter(SnapshotModel).validate_python(json.loads(body)) + model: SnapshotModel = TypeAdapter(SnapshotModel).validate_python(json.loads(body)) snapshot_id = await project_manager.add_snapshot(user_id, project.id, model) log_event("add_snapshot") return AddSnapshotResponse(snapshot_id=snapshot_id) diff --git a/src/evidently/ui/service/config.py b/src/evidently/ui/service/config.py index 57c2abd1bf..60c922ffea 100644 --- a/src/evidently/ui/service/config.py +++ b/src/evidently/ui/service/config.py @@ -129,16 +129,16 @@ def load_config(config_type: Type[TConfig], box: dict) -> TConfig: continue if section == "additional_components": for subsection, compoennt_subdict in component_dict.items(): - component = TypeAdapter(SECTION_COMPONENT_TYPE_MAPPING.get(subsection, Component)).validate_python( - compoennt_subdict - ) + component: Component = TypeAdapter( + SECTION_COMPONENT_TYPE_MAPPING.get(subsection, Component) + ).validate_python(compoennt_subdict) components[subsection] = component elif section in config_type.__fields__: type_ = config_type.__fields__[section].type_ - component = TypeAdapter(type_).validate_python(component_dict) + component: Component = TypeAdapter(type_).validate_python(component_dict) named_components[section] = component elif section in SECTION_COMPONENT_TYPE_MAPPING: - component = TypeAdapter(SECTION_COMPONENT_TYPE_MAPPING[section]).validate_python(component_dict) + component: Component = TypeAdapter(SECTION_COMPONENT_TYPE_MAPPING[section]).validate_python(component_dict) components[section] = component else: raise ValueError(f"unknown config section {section}") From 20cc635493722c60dd7cff585740bd31163c8450 Mon Sep 17 00:00:00 2001 From: mike0sv Date: Wed, 18 Feb 2026 13:23:27 +0000 Subject: [PATCH 05/11] smore mypy --- requirements.min.txt | 2 +- .../tests/classification_performance_tests.py | 16 +++++++-------- src/evidently/legacy/ui/base.py | 18 ++++++++--------- src/evidently/pydantic_utils.py | 20 ++++++++++--------- src/evidently/sdk/prompts.py | 18 +++++++++++------ src/evidently/ui/service/base.py | 14 ++++++------- .../service/storage/local/snapshot_links.py | 8 ++++++-- 7 files changed, 54 insertions(+), 42 deletions(-) diff --git a/requirements.min.txt b/requirements.min.txt index c3d3c09f92..424ae4cdd0 100644 --- a/requirements.min.txt +++ b/requirements.min.txt @@ -8,7 +8,7 @@ nltk==3.6.7 scipy==1.10.0 requests==2.32.0 PyYAML==6.0.1 -pydantic==2 +pydantic==2.10 litestar==2.19.0 typing-inspect==0.9.0 uvicorn==0.22.0 diff --git a/src/evidently/legacy/tests/classification_performance_tests.py b/src/evidently/legacy/tests/classification_performance_tests.py index 642b7e7b86..6e64c0ac6d 100644 --- a/src/evidently/legacy/tests/classification_performance_tests.py +++ b/src/evidently/legacy/tests/classification_performance_tests.py @@ -158,7 +158,7 @@ def conf_matrix(self): class TestAccuracyScore(SimpleClassificationTestTopK): __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestAccuracyScore" - name = "Accuracy Score" + name: ClassVar[str] = "Accuracy Score" def get_value(self, result: DatasetClassificationQuality): return result.accuracy @@ -181,7 +181,7 @@ def render_html(self, obj: TestAccuracyScore) -> TestHtmlInfo: class TestPrecisionScore(SimpleClassificationTestTopK): __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestPrecisionScore" - name = "Precision Score" + name: ClassVar[str] = "Precision Score" def get_value(self, result: DatasetClassificationQuality): return result.precision @@ -227,7 +227,7 @@ def render_html(self, obj: TestF1Score) -> TestHtmlInfo: class TestRecallScore(SimpleClassificationTestTopK): __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestRecallScore" - name = "Recall Score" + name: ClassVar[str] = "Recall Score" def get_value(self, result: DatasetClassificationQuality): return result.recall @@ -312,7 +312,7 @@ class TestLogLoss(SimpleClassificationTest): __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestLogLoss" condition_arg = "lt" - name = "Logarithmic Loss" + name: ClassVar[str] = "Logarithmic Loss" def get_value(self, result: DatasetClassificationQuality): return result.log_loss @@ -348,7 +348,7 @@ def render_html(self, obj: TestLogLoss) -> TestHtmlInfo: class TestTPR(SimpleClassificationTestTopK): __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestTPR" - name = "True Positive Rate" + name: ClassVar[str] = "True Positive Rate" def get_value(self, result: DatasetClassificationQuality): return result.tpr @@ -386,7 +386,7 @@ def render_html(self, obj: TestF1Score) -> TestHtmlInfo: class TestTNR(SimpleClassificationTestTopK): __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestTNR" - name = "True Negative Rate" + name: ClassVar[str] = "True Negative Rate" def get_value(self, result: DatasetClassificationQuality): return result.tnr @@ -425,7 +425,7 @@ class TestFPR(SimpleClassificationTestTopK): __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestFPR" condition_arg: ClassVar = "lt" - name = "False Positive Rate" + name: ClassVar[str] = "False Positive Rate" def get_value(self, result: DatasetClassificationQuality): return result.fpr @@ -464,7 +464,7 @@ class TestFNR(SimpleClassificationTestTopK): __type_alias__: ClassVar[Optional[str]] = "evidently:test:TestFNR" condition_arg: ClassVar = "lt" - name = "False Negative Rate" + name: ClassVar[str] = "False Negative Rate" def get_value(self, result: DatasetClassificationQuality): return result.fnr diff --git a/src/evidently/legacy/ui/base.py b/src/evidently/legacy/ui/base.py index e5c4f78526..8244a3507f 100644 --- a/src/evidently/legacy/ui/base.py +++ b/src/evidently/legacy/ui/base.py @@ -211,15 +211,15 @@ async def reload_async(self, reload_snapshots: bool = False): if reload_snapshots: await self.project_manager.reload_snapshots(self.__user_id__, self.id) # type: ignore[arg-type] - save = sync_api(save_async) - load_snapshot = sync_api(load_snapshot_async) - delete_snapshot = sync_api(delete_snapshot_async) - list_snapshots = sync_api(list_snapshots_async) - show_dashboard = sync_api(show_dashboard_async) - build_dashboard_info = sync_api(build_dashboard_info_async) - get_snapshot_metadata = sync_api(get_snapshot_metadata_async) - add_snapshot = sync_api(add_snapshot_async) - reload = sync_api(reload_async) + save = sync_api(save_async) # type: ignore[pydantic-field] + load_snapshot = sync_api(load_snapshot_async) # type: ignore[pydantic-field] + delete_snapshot = sync_api(delete_snapshot_async) # type: ignore[pydantic-field] + list_snapshots = sync_api(list_snapshots_async) # type: ignore[pydantic-field] + show_dashboard = sync_api(show_dashboard_async) # type: ignore[pydantic-field] + build_dashboard_info = sync_api(build_dashboard_info_async) # type: ignore[pydantic-field] + get_snapshot_metadata = sync_api(get_snapshot_metadata_async) # type: ignore[pydantic-field] + add_snapshot = sync_api(add_snapshot_async) # type: ignore[pydantic-field] + reload = sync_api(reload_async) # type: ignore[pydantic-field] class ProjectMetadataStorage(ABC): diff --git a/src/evidently/pydantic_utils.py b/src/evidently/pydantic_utils.py index 151bc82d44..4b983fd42c 100644 --- a/src/evidently/pydantic_utils.py +++ b/src/evidently/pydantic_utils.py @@ -244,8 +244,9 @@ class PolymorphicModel(BaseModel): @classmethod def __get_type__(cls) -> str: - if cls.__dict__.get("__type_alias__") is not None: - return cls.__type_alias__ + type_alias = cls.__dict__.get("__type_alias__") + if type_alias is not None: + return str(type_alias) if cls.__alias_required__ and is_not_abstract(cls): raise ValueError(f"Alias is required for {cls.__name__}") return cls.__get_classpath__() @@ -311,11 +312,7 @@ def model_validate( return super().model_validate(value, strict=strict, from_attributes=from_attributes, context=context) @model_validator(mode="wrap") - def _delegate_validation(cls, data: dict, handler) -> Any: - # if isinstance(data, cls): - # return data - # if not isinstance(data, dict): - # raise ValueError(f"{data!r} is not a dict") + def _delegate_validation(cls, data, handler) -> Any: if "type" in data and data["type"] != cls.__get_type__(): return cls.model_validate(data) typename = data.pop("type", None) if isinstance(data, dict) else None @@ -716,8 +713,9 @@ class AutoAliasMixin: @classmethod def __get_type__(cls) -> str: - if cls.__dict__.get("__type_alias__") is not None: - return cls.__type_alias__ + type_alias = cls.__dict__.get("__type_alias__") + if type_alias is not None: + return str(type_alias) if not hasattr(cls, "__alias_type__"): raise TypeError(f"{cls.__name__} `__alias_type__` is not defined") return f"evidently:{cls.__alias_type__}:{cls.__name__}" @@ -748,6 +746,8 @@ def get_field_inner_type(field: PydanticFieldInfo) -> Type: typ = args[1] break break + if typ is None: + raise TypeError(f"{field.__name__} does not have correct type annotation") return typ @@ -757,6 +757,8 @@ def get_field_outer_type(field: PydanticFieldInfo) -> Type: args = get_args(typ) if len(args) == 2 and type(None) in args: typ = [a for a in args if a is not type(None)][0] + if typ is None: + raise TypeError(f"{field.__name__} does not have correct type annotation") return typ diff --git a/src/evidently/sdk/prompts.py b/src/evidently/sdk/prompts.py index 41acf5d117..2504a82b53 100644 --- a/src/evidently/sdk/prompts.py +++ b/src/evidently/sdk/prompts.py @@ -164,13 +164,19 @@ def bind(self, api: "PromptAPI") -> "RemotePrompt": self.__api__ = api return self + @property + def _api(self) -> "PromptAPI": + if self.__api__ is None: + raise ValueError("api was not initialized") + return self.__api__ + def list_versions(self) -> List[PromptVersion]: """List all versions of this prompt. Returns: * List of `PromptVersion` objects. """ - return self.__api__.list_versions(self.id) + return self._api.list_versions(self.id) def get_version(self, version: VersionOrLatest = "latest") -> PromptVersion: """Get a specific version of this prompt. @@ -181,7 +187,7 @@ def get_version(self, version: VersionOrLatest = "latest") -> PromptVersion: Returns: * `PromptVersion` for the specified version. """ - return self.__api__.get_version(self.id, version) + return self._api.get_version(self.id, version) def bump_version(self, content: Any): """Create a new version with the given content. @@ -192,7 +198,7 @@ def bump_version(self, content: Any): Returns: * New `PromptVersion` with incremented version number. """ - return self.__api__.bump_prompt_version(self.id, content) + return self._api.bump_prompt_version(self.id, content) def delete(self): """Delete this prompt and all its versions. @@ -200,7 +206,7 @@ def delete(self): This operation is irreversible and will permanently remove the prompt and all associated versions from the workspace. """ - return self.__api__.delete_prompt(self.id) + return self._api.delete_prompt(self.id) def delete_version(self, version_id: PromptVersionID): """Delete a specific version. @@ -210,14 +216,14 @@ def delete_version(self, version_id: PromptVersionID): Args: * `version_id`: ID of the version to delete. """ - return self.__api__.delete_version(version_id) + return self._api.delete_version(version_id) def save(self): """Save changes to this prompt's metadata to the remote workspace. Updates the prompt's name and metadata fields. Does not affect versions. """ - self.__api__.update_prompt(self) + self._api.update_prompt(self) class PromptAPI(ABC): diff --git a/src/evidently/ui/service/base.py b/src/evidently/ui/service/base.py index e809055b64..0d5ae4ac79 100644 --- a/src/evidently/ui/service/base.py +++ b/src/evidently/ui/service/base.py @@ -185,13 +185,13 @@ async def reload_async(self, reload_snapshots: bool = False): if reload_snapshots: await self.project_manager.reload_snapshots(self.__user_id__, self.id) # type: ignore[arg-type] - save = sync_api(save_async) - load_snapshot = sync_api(load_snapshot_async) - delete_snapshot = sync_api(delete_snapshot_async) - list_snapshots = sync_api(list_snapshots_async) - get_snapshot_metadata = sync_api(get_snapshot_metadata_async) - add_snapshot = sync_api(add_snapshot_async) - reload = sync_api(reload_async) + save = sync_api(save_async) # type: ignore[pydantic-field] + load_snapshot = sync_api(load_snapshot_async) # type: ignore[pydantic-field] + delete_snapshot = sync_api(delete_snapshot_async) # type: ignore[pydantic-field] + list_snapshots = sync_api(list_snapshots_async) # type: ignore[pydantic-field] + get_snapshot_metadata = sync_api(get_snapshot_metadata_async) # type: ignore[pydantic-field] + add_snapshot = sync_api(add_snapshot_async) # type: ignore[pydantic-field] + reload = sync_api(reload_async) # type: ignore[pydantic-field] class ProjectMetadataStorage(ABC): diff --git a/src/evidently/ui/service/storage/local/snapshot_links.py b/src/evidently/ui/service/storage/local/snapshot_links.py index 507ed321da..1e4eb772ac 100644 --- a/src/evidently/ui/service/storage/local/snapshot_links.py +++ b/src/evidently/ui/service/storage/local/snapshot_links.py @@ -33,7 +33,9 @@ async def get_links(self, project_id: ProjectID, snapshot_id: SnapshotID) -> Sna with self.location.open(links_path, "r") as f: links_data = json.load(f) - input_output_links = TypeAdapter(DatasetInputOutputLinks).validate_python(links_data) + input_output_links: DatasetInputOutputLinks = TypeAdapter(DatasetInputOutputLinks).validate_python( + links_data + ) return SnapshotLinks(datasets=input_output_links) @@ -55,7 +57,9 @@ async def link_dataset_snapshot( if self.location.exists(links_path): with self.location.open(links_path, "r") as f: links_data = json.load(f) - input_output_links = TypeAdapter(DatasetInputOutputLinks).validate_python(links_data) + input_output_links: DatasetInputOutputLinks = TypeAdapter(DatasetInputOutputLinks).validate_python( + links_data + ) else: input_output_links = DatasetInputOutputLinks() From 0f1cde0f44924468db33bba11d6f2ba64bec21cf Mon Sep 17 00:00:00 2001 From: mike0sv Date: Thu, 19 Feb 2026 21:33:56 +0000 Subject: [PATCH 06/11] smore mypy --- .../descriptors/_generate_descriptors.py | 4 +-- src/evidently/generators/column.py | 2 +- src/evidently/legacy/core.py | 4 +-- .../confusion_matrix_metric.py | 4 +-- src/evidently/legacy/options/base.py | 2 +- .../legacy/pipeline/column_mapping.py | 2 +- src/evidently/legacy/tests/base_test.py | 6 ++--- .../tests/classification_performance_tests.py | 24 ++++++++--------- src/evidently/legacy/tests/custom_test.py | 2 +- .../legacy/tests/data_drift_tests.py | 10 +++---- .../legacy/tests/data_integrity_tests.py | 2 +- .../legacy/tests/data_quality_tests.py | 22 ++++++++-------- src/evidently/legacy/ui/base.py | 24 ++++++++++------- src/evidently/presets/classification.py | 8 +++--- src/evidently/pydantic_utils.py | 26 ++++++++----------- src/evidently/ui/service/api/datasets.py | 2 +- src/evidently/ui/service/config.py | 11 ++++---- .../ui/service/datasets/data_source.py | 6 ++--- 18 files changed, 81 insertions(+), 80 deletions(-) diff --git a/src/evidently/descriptors/_generate_descriptors.py b/src/evidently/descriptors/_generate_descriptors.py index 594f4583d7..e6dc4e873f 100644 --- a/src/evidently/descriptors/_generate_descriptors.py +++ b/src/evidently/descriptors/_generate_descriptors.py @@ -66,12 +66,12 @@ def get_args_kwargs(feature_class: Type[GeneratedFeatures]) -> Tuple[Dict[str, s # get from fields args = { key: _get_type_name(field.annotation) - for key, field in feature_class.model_fields.items() + for key, field in feature_class.model_fields.items() # type: ignore[attr-defined] if field.is_required() } kwargs = { key: (_get_type_name(field.annotation), _get_value_str(field.default)) - for key, field in feature_class.model_fields.items() + for key, field in feature_class.model_fields.items() # type: ignore[attr-defined] if not field.is_required() and key != "type" } return args, kwargs diff --git a/src/evidently/generators/column.py b/src/evidently/generators/column.py index 32e5ceb926..e8ab397e80 100644 --- a/src/evidently/generators/column.py +++ b/src/evidently/generators/column.py @@ -94,7 +94,7 @@ def validate_metric_kwargs( ) -> Dict[str, Any]: validated = {} for field_name, field_value in metric_kwargs.items(): - field_info = metric_type.model_fields.get(field_name, None) + field_info = metric_type.model_fields.get(field_name, None) # type: ignore[attr-defined] if field_info is None: raise ValueError(f"Metric {metric_type.__name__} does not have field {field_name}") validated[field_name] = TypeAdapter(field_info.annotation).validate_python(field_value) diff --git a/src/evidently/legacy/core.py b/src/evidently/legacy/core.py index 636fd16367..724d8d4716 100644 --- a/src/evidently/legacy/core.py +++ b/src/evidently/legacy/core.py @@ -295,7 +295,7 @@ def get_field_tags(cls: Type[BaseModel], field_name: str) -> Set[IncludeTags]: field_tags = ft[field_name] break - field = cls.model_fields[field_name] + field = cls.model_fields[field_name] # type: ignore[index] field_annotation = field.annotation if field_annotation is None: field_annotation = type(None) @@ -306,7 +306,7 @@ def get_field_tags(cls: Type[BaseModel], field_name: str) -> Set[IncludeTags]: def get_all_fields_tags(cls: Type[BaseResult]) -> Dict[str, Set[IncludeTags]]: - return {field_name: get_field_tags(cls, field_name) for field_name in cls.model_fields} + return {field_name: get_field_tags(cls, field_name) for field_name in cls.model_fields} # type: ignore[attr-defined] def new_id() -> uuid.UUID: diff --git a/src/evidently/legacy/metrics/classification_performance/confusion_matrix_metric.py b/src/evidently/legacy/metrics/classification_performance/confusion_matrix_metric.py index ebd6c0aa3c..18fab93fda 100644 --- a/src/evidently/legacy/metrics/classification_performance/confusion_matrix_metric.py +++ b/src/evidently/legacy/metrics/classification_performance/confusion_matrix_metric.py @@ -38,8 +38,8 @@ class ClassificationConfusionMatrixResult(MetricResult): class ClassificationConfusionMatrixParameters(BaseModel): - probas_threshold: Optional[float] - k: Optional[int] + probas_threshold: Optional[float] = None + k: Optional[int] = None def confusion_matric_metric(self): return ClassificationConfusionMatrix(probas_threshold=self.probas_threshold, k=self.k) diff --git a/src/evidently/legacy/options/base.py b/src/evidently/legacy/options/base.py index 7bab5d4fb2..2be018d776 100644 --- a/src/evidently/legacy/options/base.py +++ b/src/evidently/legacy/options/base.py @@ -133,6 +133,6 @@ def dict( ) -_option_cls_mapping = {get_field_inner_type(field): name for name, field in Options.model_fields.items()} +_option_cls_mapping = {get_field_inner_type(field): name for name, field in Options.model_fields.items()} # type: ignore[attr-defined] AnyOptions = Union[Options, Option, dict, List[Option], None] diff --git a/src/evidently/legacy/pipeline/column_mapping.py b/src/evidently/legacy/pipeline/column_mapping.py index 9faedaafbe..25b8407317 100644 --- a/src/evidently/legacy/pipeline/column_mapping.py +++ b/src/evidently/legacy/pipeline/column_mapping.py @@ -35,7 +35,7 @@ class ColumnMapping: datetime_features: Optional[List[str]] = None target_names: Optional[TargetNames] = None task: Optional[str] = None - pos_label: Optional[Union[str, int]] = 1 + pos_label: Optional[Label] = 1 text_features: Optional[List[str]] = None embeddings: Optional[Embeddings] = None user_id: Optional[str] = "user_id" diff --git a/src/evidently/legacy/tests/base_test.py b/src/evidently/legacy/tests/base_test.py index 6e759731ae..1bacb62ae4 100644 --- a/src/evidently/legacy/tests/base_test.py +++ b/src/evidently/legacy/tests/base_test.py @@ -318,7 +318,7 @@ class BaseCheckValueTest(BaseConditionsTest): Base class for all tests with checking a value condition """ - _value: Numeric + __value__: Numeric @abc.abstractmethod def calculate_value_for_test(self) -> Optional[Any]: @@ -342,7 +342,7 @@ def groups(self) -> Dict[str, str]: return {} def get_parameters(self) -> CheckValueParameters: - return CheckValueParameters(condition=self.get_condition(), value=self._value) + return CheckValueParameters(condition=self.get_condition(), value=self.__value__) def check(self): result = TestResult( @@ -353,7 +353,7 @@ def check(self): parameters=None, ) value = self.calculate_value_for_test() - self._value = value + self.__value__ = value result.description = self.get_description(value) result.parameters = self.get_parameters() diff --git a/src/evidently/legacy/tests/classification_performance_tests.py b/src/evidently/legacy/tests/classification_performance_tests.py index 6e64c0ac6d..ab799c5b7a 100644 --- a/src/evidently/legacy/tests/classification_performance_tests.py +++ b/src/evidently/legacy/tests/classification_performance_tests.py @@ -52,8 +52,8 @@ class SimpleClassificationTest(BaseCheckValueTest): group: ClassVar = CLASSIFICATION_GROUP.id name: ClassVar[str] - _metric: ClassificationQualityMetric - _dummy_metric: ClassificationDummyMetric + __metric__: ClassificationQualityMetric + __dummy_metric__: ClassificationDummyMetric def __init__( self, @@ -78,16 +78,16 @@ def __init__( not_in=not_in, is_critical=is_critical, ) - self._metric = ClassificationQualityMetric() - self._dummy_metric = ClassificationDummyMetric() + self.__metric__ = ClassificationQualityMetric() + self.__dummy_metric__ = ClassificationDummyMetric() @property def metric(self): - return self._metric + return self.__metric__ @property def dummy_metric(self): - return self._dummy_metric + return self.__dummy_metric__ def calculate_value_for_test(self) -> Optional[Any]: return self.get_value(self.metric.get_result().current) @@ -112,7 +112,7 @@ def get_value(self, result: DatasetClassificationQuality): class SimpleClassificationTestTopK(SimpleClassificationTest, ClassificationConfusionMatrixParameters, ABC): - _conf_matrix: ClassificationConfusionMatrix + __conf_matrix__: ClassificationConfusionMatrix def __init__( self, @@ -143,16 +143,16 @@ def __init__( not_in=not_in, is_critical=is_critical, ) - self._dummy_metric = ClassificationDummyMetric(probas_threshold=self.probas_threshold, k=self.k) - self._metric = ClassificationQualityMetric(probas_threshold=self.probas_threshold, k=self.k) - self._conf_matrix = self.confusion_matric_metric() + self.__dummy_metric__ = ClassificationDummyMetric(probas_threshold=self.probas_threshold, k=self.k) + self.__metric__ = ClassificationQualityMetric(probas_threshold=self.probas_threshold, k=self.k) + self.__conf_matrix__ = self.confusion_matric_metric() def calculate_value_for_test(self) -> Optional[Any]: return self.get_value(self.metric.get_result().current) @property def conf_matrix(self): - return self._conf_matrix + return self.__conf_matrix__ class TestAccuracyScore(SimpleClassificationTestTopK): @@ -594,7 +594,7 @@ def get_value(self, result: ClassMetric): raise NotImplementedError() def get_parameters(self) -> ByClassParameters: - return ByClassParameters(condition=self.get_condition(), value=self._value, label=self.label) + return ByClassParameters(condition=self.get_condition(), value=self.__value__, label=self.label) class TestPrecisionByClass(ByClassClassificationTest): diff --git a/src/evidently/legacy/tests/custom_test.py b/src/evidently/legacy/tests/custom_test.py index 2be9a5894d..cf5daf4c46 100644 --- a/src/evidently/legacy/tests/custom_test.py +++ b/src/evidently/legacy/tests/custom_test.py @@ -58,6 +58,6 @@ def calculate_value_for_test(self) -> Numeric: def get_description(self, value: Numeric) -> str: return ( - f"Custom function '{self._metric.title or ''}' value is {self._value or None}. " + f"Custom function '{self._metric.title or ''}' value is {self.__value__ or None}. " f"The test threshold is {self.get_condition()}" ) diff --git a/src/evidently/legacy/tests/data_drift_tests.py b/src/evidently/legacy/tests/data_drift_tests.py index 54b6ed4cd5..18bdb705e1 100644 --- a/src/evidently/legacy/tests/data_drift_tests.py +++ b/src/evidently/legacy/tests/data_drift_tests.py @@ -99,9 +99,9 @@ def to_dataframe(self) -> pd.DataFrame: class BaseDataDriftMetricsTest(BaseCheckValueTest, WithDriftOptionsFields, ABC): group: ClassVar = DATA_DRIFT_GROUP.id - _metric: DataDriftTable - columns: Optional[List[str]] - feature_importance: Optional[bool] + __metric__: DataDriftTable + columns: Optional[List[str]] = None + feature_importance: Optional[bool] = None def __init__( self, @@ -150,7 +150,7 @@ def __init__( per_column_stattest_threshold=per_column_stattest_threshold, feature_importance=feature_importance, ) - self._metric = DataDriftTable( + self.__metric__ = DataDriftTable( columns=self.columns, stattest=self.stattest, cat_stattest=self.cat_stattest, @@ -167,7 +167,7 @@ def __init__( @property def metric(self): - return self._metric + return self.__metric__ def check(self): result = super().check() diff --git a/src/evidently/legacy/tests/data_integrity_tests.py b/src/evidently/legacy/tests/data_integrity_tests.py index 12e45c2781..4b107e18d1 100644 --- a/src/evidently/legacy/tests/data_integrity_tests.py +++ b/src/evidently/legacy/tests/data_integrity_tests.py @@ -610,7 +610,7 @@ def get_description(self, value: Numeric) -> str: def get_parameters(self): return ColumnCheckValueParameters( - condition=self.get_condition(), value=self._value, column_name=self.column_name + condition=self.get_condition(), value=self.__value__, column_name=self.column_name ) diff --git a/src/evidently/legacy/tests/data_quality_tests.py b/src/evidently/legacy/tests/data_quality_tests.py index 3380058e40..c59c0424d7 100644 --- a/src/evidently/legacy/tests/data_quality_tests.py +++ b/src/evidently/legacy/tests/data_quality_tests.py @@ -470,7 +470,7 @@ def groups(self) -> Dict[str, str]: def check(self): result = super().check() - if self._value is None: + if self.__value__ is None: result.mark_as_error(f"No value for the feature '{self.column_name}'") return result @@ -821,7 +821,7 @@ def get_description(self, value: Numeric) -> str: def get_parameters(self) -> ColumnCheckValueParameters: return ColumnCheckValueParameters( - column_name=self.column_name.display_name, condition=self.get_condition(), value=self._value + column_name=self.column_name.display_name, condition=self.get_condition(), value=self.__value__ ) @@ -1115,10 +1115,10 @@ def render_html(self, obj: TestValueRange) -> TestHtmlInfo: class BaseDataQualityValueRangeMetricsTest(BaseCheckValueTest, ABC): group: ClassVar = DATA_QUALITY_GROUP.id - _metric: ColumnValueRangeMetric + __metric__: ColumnValueRangeMetric column_name: ColumnName - left: Optional[float] - right: Optional[float] + left: Optional[float] = None + right: Optional[float] = None def __init__( self, @@ -1150,7 +1150,7 @@ def __init__( not_in=not_in, is_critical=is_critical, ) - self._metric = ColumnValueRangeMetric(column_name=self.column_name, left=self.left, right=self.right) + self.__metric__ = ColumnValueRangeMetric(column_name=self.column_name, left=self.left, right=self.right) def groups(self) -> Dict[str, str]: return {GroupingTypes.ByFeature.id: self.column_name.display_name} @@ -1162,7 +1162,7 @@ def get_condition(self) -> TestValueCondition: @property def metric(self): - return self._metric + return self.__metric__ class TestNumberOfOutRangeValues(BaseDataQualityValueRangeMetricsTest): @@ -1205,7 +1205,7 @@ def get_description(self, value: Numeric) -> str: def get_parameters(self) -> ShareOfOutRangeParameters: return ShareOfOutRangeParameters( - condition=self.get_condition(), value=self._value, left=self.left, right=self.right + condition=self.get_condition(), value=self.__value__, left=self.left, right=self.right ) @@ -1415,7 +1415,7 @@ def get_description(self, value: Numeric) -> str: ) def get_parameters(self) -> CheckValueParameters: - return ValueListParameters(condition=self.get_condition(), value=self._value, values=self.values) + return ValueListParameters(condition=self.get_condition(), value=self.__value__, values=self.values) class TestCatColumnsOutOfListValues(BaseGenerator): @@ -1647,7 +1647,7 @@ def get_description(self, value: Numeric) -> str: ) def get_parameters(self) -> CheckValueParameters: - return ValueListParameters(condition=self.get_condition(), value=self._value, category=self.category) + return ValueListParameters(condition=self.get_condition(), value=self.__value__, category=self.category) class TestCategoryCount(BaseDataQualityCategoryMetricsTest): @@ -1673,7 +1673,7 @@ def get_description(self, value: Numeric) -> str: ) def get_parameters(self) -> CheckValueParameters: - return ValueListParameters(condition=self.get_condition(), value=self._value, category=self.category) + return ValueListParameters(condition=self.get_condition(), value=self.__value__, category=self.category) @default_renderer(wrap_type=TestCategoryCount) diff --git a/src/evidently/legacy/ui/base.py b/src/evidently/legacy/ui/base.py index 8244a3507f..f87785e477 100644 --- a/src/evidently/legacy/ui/base.py +++ b/src/evidently/legacy/ui/base.py @@ -148,34 +148,38 @@ def project_manager(self) -> "ProjectManager": raise ValueError("Project is not binded") return self.__project_manager__ + @property + def _user_id(self) -> UserID: + if self.__user_id__ is None: + raise ValueError("Project is not binded") + return self.__user_id__ + async def save_async(self): - await self.project_manager.update_project(self.__user_id__, self) # type: ignore[arg-type] + await self.project_manager.update_project(self._user_id, self) # type: ignore[arg-type] return self async def load_snapshot_async(self, snapshot_id: SnapshotID) -> Snapshot: - return await self.project_manager.load_snapshot(self.__user_id__, self.id, snapshot_id) # type: ignore[arg-type] + return await self.project_manager.load_snapshot(self._user_id, self.id, snapshot_id) # type: ignore[arg-type] async def add_snapshot_async(self, snapshot: AnySnapshot): if not isinstance(snapshot, Snapshot): from evidently.ui.backport import snapshot_v2_to_v1 snapshot = snapshot_v2_to_v1(snapshot) - await self.project_manager.add_snapshot(self.__user_id__, self.id, snapshot) # type: ignore[arg-type] + await self.project_manager.add_snapshot(self._user_id, self.id, snapshot) # type: ignore[arg-type] async def delete_snapshot_async(self, snapshot_id: Union[str, SnapshotID]): if isinstance(snapshot_id, str): snapshot_id = uuid6.UUID(snapshot_id) - await self.project_manager.delete_snapshot(self.__user_id__, self.id, snapshot_id) # type: ignore[arg-type] + await self.project_manager.delete_snapshot(self._user_id, self.id, snapshot_id) # type: ignore[arg-type] async def list_snapshots_async( self, include_reports: bool = True, include_test_suites: bool = True ) -> List[SnapshotMetadata]: - return await self.project_manager.list_snapshots( - self.__user_id__, self.id, include_reports, include_test_suites - ) # type: ignore[arg-type] + return await self.project_manager.list_snapshots(self._user_id, self.id, include_reports, include_test_suites) # type: ignore[arg-type] async def get_snapshot_metadata_async(self, id: SnapshotID) -> SnapshotMetadata: - return await self.project_manager.get_snapshot_metadata(self.__user_id__, self.id, id) # type: ignore[arg-type] + return await self.project_manager.get_snapshot_metadata(self._user_id, self.id, id) # type: ignore[arg-type] async def build_dashboard_info_async( self, @@ -205,11 +209,11 @@ async def show_dashboard_async( async def reload_async(self, reload_snapshots: bool = False): # fixme: reload snapshots - project = await self.project_manager.get_project(self.__user_id__, self.id) # type: ignore[arg-type] + project = await self.project_manager.get_project(self._user_id, self.id) # type: ignore[arg-type] self.__dict__.update(project.__dict__) if reload_snapshots: - await self.project_manager.reload_snapshots(self.__user_id__, self.id) # type: ignore[arg-type] + await self.project_manager.reload_snapshots(self._user_id, self.id) # type: ignore[arg-type] save = sync_api(save_async) # type: ignore[pydantic-field] load_snapshot = sync_api(load_snapshot_async) # type: ignore[pydantic-field] diff --git a/src/evidently/presets/classification.py b/src/evidently/presets/classification.py index 668cfed82b..8ec475d3a3 100644 --- a/src/evidently/presets/classification.py +++ b/src/evidently/presets/classification.py @@ -412,11 +412,11 @@ class ClassificationPreset(MetricContainer): classification_name: str = "default" """Name of the classification task.""" - __quality__: Optional[ClassificationQuality] = None + __quality__: ClassificationQuality """Internal classification quality preset.""" - __quality_by_label__: Optional[ClassificationQualityByLabel] = None + __quality_by_label__: ClassificationQualityByLabel """Internal classification quality by label preset.""" - __roc_auc__: Optional[RocAuc] = None + __roc_auc__: RocAuc """Internal ROC AUC metric.""" def __init__( @@ -485,7 +485,7 @@ def __init__( include_tests=include_tests, classification_name=classification_name, ) - self.__roc_auc__ = None + # self.__roc_auc__ = None def generate_metrics(self, context: "Context") -> Sequence[MetricOrContainer]: classification = context.data_definition.get_classification(self.classification_name) diff --git a/src/evidently/pydantic_utils.py b/src/evidently/pydantic_utils.py index 4b983fd42c..4b6923862a 100644 --- a/src/evidently/pydantic_utils.py +++ b/src/evidently/pydantic_utils.py @@ -8,7 +8,6 @@ from abc import ABC from enum import Enum from functools import lru_cache -from typing import TYPE_CHECKING from typing import Annotated from typing import Any from typing import Callable @@ -47,9 +46,6 @@ from typing_extensions import Self from typing_inspect import is_union_type -if TYPE_CHECKING: - from pydantic import DictStrAny - def _is_dict_annotation(annotation: Any) -> bool: """True if the field annotation is dict-like (Dict, dict, Mapping).""" @@ -332,16 +328,16 @@ def _delegate_validation(cls, data, handler) -> Any: @model_serializer(mode="wrap") def _delegate_serialization(self, nxt: SerializerFunctionWrapHandler, info: SerializationInfo): if f"serializer={self.__class__.__name__}" not in str(nxt): - return self.model_dump( + return self.model_dump( # type: ignore[call-arg] mode=info.mode, - include=info.include, - exclude=info.exclude, + include=info.include, # type: ignore[arg-type] + exclude=info.exclude, # type: ignore[arg-type] context=info.context, by_alias=info.by_alias, exclude_unset=info.exclude_unset, exclude_none=info.exclude_none, exclude_defaults=info.exclude_defaults, - round_trip=info.round_trip, + round_trip=info.round_trip, # type: ignore[arg-type] serialize_as_any=info.serialize_as_any, ) return nxt(self) @@ -481,7 +477,7 @@ def _to_enum_value(self, key, value): return {v.value if isinstance(v, Enum) else v for v in value} return value.value if isinstance(value, Enum) else value - def model_dump(self, *args, **kwargs) -> "DictStrAny": + def model_dump(self, *args, **kwargs) -> dict[str, Any]: res = super().model_dump(*args, **kwargs) return {k: self._to_enum_value(k, v) for k, v in res.items()} @@ -543,7 +539,7 @@ def list_fields(self) -> List[str]: if self.has_instance and self._is_mapping and isinstance(self._instance, dict): return list(self._instance.keys()) if isinstance(self._cls, type) and issubclass(self._cls, BaseModel): - return list(self._cls.model_fields) + return list(self._cls.model_fields) # type: ignore[call-overload] return [] def __getattr__(self, item) -> "FieldPath": @@ -560,9 +556,9 @@ def child(self, item: str) -> "FieldPath": return FieldPath(self._path + [item], self._cls) if not issubclass(self._cls, BaseModel): raise AttributeError(f"{self._cls} does not have fields") - if item not in self._cls.model_fields: + if item not in self._cls.model_fields: # type: ignore[operator] raise AttributeError(f"{self._cls} type does not have '{item}' field") - field = self._cls.model_fields[item] + field = self._cls.model_fields[item] # type: ignore[index] field_value = get_field_inner_type(field) is_mapping = _is_dict_annotation(field.annotation) if self.has_instance: @@ -575,7 +571,7 @@ def list_nested_fields(self, exclude: Set["IncludeTags"] = None) -> List[str]: if not isinstance(self._cls, type) or not issubclass(self._cls, BaseModel): return [repr(self)] res = [] - for name, field in self._cls.model_fields.items(): + for name, field in self._cls.model_fields.items(): # type: ignore[attr-defined] field_value = field.annotation # todo: do something with recursive imports from evidently.legacy.core import get_field_tags @@ -619,7 +615,7 @@ def _list_with_tags(self, current_tags: Set["IncludeTags"]) -> List[Tuple[List[A if issubclass(self._cls, ByLabelCountValueV1): res.append((self._path + ["counts"], current_tags.union({IncludeTags.Render}))) res.append((self._path + ["shares"], current_tags.union({IncludeTags.Render}))) - for name, field in self._cls.model_fields.items(): + for name, field in self._cls.model_fields.items(): # type: ignore[attr-defined] field_value = field.annotation # todo: do something with recursive imports @@ -656,7 +652,7 @@ def _get_field_type(self, path: List[str]) -> Type: raise ValueError("Empty path provided") if len(path) == 1: if isinstance(self._cls, type) and issubclass(self._cls, BaseModel): - return self._cls.model_fields[path[0]].annotation + return self._cls.model_fields[path[0]].annotation # type: ignore[index] if self.has_instance: # fixme: tmp fix # in case of field like f: Dict[str, A] we wont know that value was type annotated with A when we get to it diff --git a/src/evidently/ui/service/api/datasets.py b/src/evidently/ui/service/api/datasets.py index 6b81399c59..1d11ff71da 100644 --- a/src/evidently/ui/service/api/datasets.py +++ b/src/evidently/ui/service/api/datasets.py @@ -272,7 +272,7 @@ class DatasetMetadataResponse(EvidentlyAPIModel): @classmethod def from_dataset_metadata(cls, dataset: DatasetMetadataFull): """Create from DatasetMetadataFull.""" - return cls(**{k: v for k, v in dataset.__dict__.items() if k in cls.model_fields}) + return cls(**{k: v for k, v in dataset.__dict__.items() if k in cls.model_fields}) # type: ignore[operator] class ListDatasetResponse(EvidentlyAPIModel): diff --git a/src/evidently/ui/service/config.py b/src/evidently/ui/service/config.py index 60c922ffea..792180b8e0 100644 --- a/src/evidently/ui/service/config.py +++ b/src/evidently/ui/service/config.py @@ -127,18 +127,19 @@ def load_config(config_type: Type[TConfig], box: dict) -> TConfig: continue if section in ("renamed_vars", "dict_itemiterator"): continue + component: Component if section == "additional_components": for subsection, compoennt_subdict in component_dict.items(): - component: Component = TypeAdapter( - SECTION_COMPONENT_TYPE_MAPPING.get(subsection, Component) - ).validate_python(compoennt_subdict) + component = TypeAdapter(SECTION_COMPONENT_TYPE_MAPPING.get(subsection, Component)).validate_python( + compoennt_subdict + ) components[subsection] = component elif section in config_type.__fields__: type_ = config_type.__fields__[section].type_ - component: Component = TypeAdapter(type_).validate_python(component_dict) + component = TypeAdapter(type_).validate_python(component_dict) named_components[section] = component elif section in SECTION_COMPONENT_TYPE_MAPPING: - component: Component = TypeAdapter(SECTION_COMPONENT_TYPE_MAPPING[section]).validate_python(component_dict) + component = TypeAdapter(SECTION_COMPONENT_TYPE_MAPPING[section]).validate_python(component_dict) components[section] = component else: raise ValueError(f"unknown config section {section}") diff --git a/src/evidently/ui/service/datasets/data_source.py b/src/evidently/ui/service/datasets/data_source.py index 9a8e153d16..14f86fb222 100644 --- a/src/evidently/ui/service/datasets/data_source.py +++ b/src/evidently/ui/service/datasets/data_source.py @@ -147,7 +147,7 @@ class DataSourceDTO(AutoAliasMixin, PolymorphicModel, ABC): def to_data_source(self, **kwargs) -> DataSource: """Convert DTO to data source.""" - kwargs = {k: v for k, v in kwargs.items() if k in self.__data_source_type__.model_fields} + kwargs = {k: v for k, v in kwargs.items() if k in self.__data_source_type__.model_fields} # type: ignore[operator] return self.__data_source_type__(**self.__dict__, **kwargs) @staticmethod @@ -156,10 +156,10 @@ def for_type( ) -> Type["DataSourceDTO"]: """Create a DTO type for a data source type.""" namespace = { - "__annotations__": {n: f.annotation for n, f in data_source_type.model_fields.items() if n not in exclude}, + "__annotations__": {n: f.annotation for n, f in data_source_type.model_fields.items() if n not in exclude}, # type: ignore[attr-defined] **{ n: f.default - for n, f in data_source_type.model_fields.items() + for n, f in data_source_type.model_fields.items() # type: ignore[attr-defined] if n not in exclude and not f.is_required() }, } From 6cc6eaee986d83d457f61abc6afb15e5a7a94e62 Mon Sep 17 00:00:00 2001 From: mike0sv Date: Thu, 19 Feb 2026 22:27:13 +0000 Subject: [PATCH 07/11] smore mypy --- .../legacy/calculations/classification_performance.py | 2 +- src/evidently/presets/classification.py | 5 ++++- src/evidently/pydantic_utils.py | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/evidently/legacy/calculations/classification_performance.py b/src/evidently/legacy/calculations/classification_performance.py index 3d448d4f7e..2021291b16 100644 --- a/src/evidently/legacy/calculations/classification_performance.py +++ b/src/evidently/legacy/calculations/classification_performance.py @@ -88,7 +88,7 @@ def k_probability_threshold( def get_prediction_data( - data: pd.DataFrame, data_columns: DatasetColumns, pos_label: Optional[Union[str, int]], threshold: float = 0.5 + data: pd.DataFrame, data_columns: DatasetColumns, pos_label: Optional[Label], threshold: float = 0.5 ) -> PredictionData: """Get predicted values and optional prediction probabilities from source data. Also take into account a threshold value - if a probability is less than the value, do not take it into account. diff --git a/src/evidently/presets/classification.py b/src/evidently/presets/classification.py index 8ec475d3a3..bd0ed5ad03 100644 --- a/src/evidently/presets/classification.py +++ b/src/evidently/presets/classification.py @@ -492,7 +492,10 @@ def generate_metrics(self, context: "Context") -> Sequence[MetricOrContainer]: if classification is None: raise ValueError("Cannot use ClassificationPreset without a classification configration") quality_metrics = self.__quality__.metrics(context) - self.__roc_auc__ = next((m for m in quality_metrics if isinstance(m, RocAuc)), None) + roc_auc = next((m for m in quality_metrics if isinstance(m, RocAuc)), None) + if roc_auc is None: + raise ValueError("Cannot use ClassificationPreset without a ROC AUC metric") + self.__roc_auc__ = roc_auc return quality_metrics + self.__quality_by_label__.metrics(context) def render( diff --git a/src/evidently/pydantic_utils.py b/src/evidently/pydantic_utils.py index 4b6923862a..b0672f68c1 100644 --- a/src/evidently/pydantic_utils.py +++ b/src/evidently/pydantic_utils.py @@ -743,7 +743,7 @@ def get_field_inner_type(field: PydanticFieldInfo) -> Type: break break if typ is None: - raise TypeError(f"{field.__name__} does not have correct type annotation") + raise TypeError(f"Field {field} does not have correct type annotation ") return typ @@ -754,7 +754,7 @@ def get_field_outer_type(field: PydanticFieldInfo) -> Type: if len(args) == 2 and type(None) in args: typ = [a for a in args if a is not type(None)][0] if typ is None: - raise TypeError(f"{field.__name__} does not have correct type annotation") + raise TypeError(f"Field {field} does not have correct type annotation") return typ From fbfdd9afc6bf522ffeb0b2386c4c1ce65ef5d6f6 Mon Sep 17 00:00:00 2001 From: mike0sv Date: Thu, 19 Feb 2026 23:29:51 +0000 Subject: [PATCH 08/11] smore mypy and test fixes --- .../classification_performance.py | 2 +- src/evidently/legacy/metric_results.py | 30 +++++-------------- .../quality_by_feature_table.py | 2 +- .../metrics/data_drift/embeddings_drift.py | 8 ++--- src/evidently/legacy/suite/base_suite.py | 2 +- src/evidently/legacy/tests/base_test.py | 2 ++ .../legacy/tests/data_quality_tests.py | 2 +- src/evidently/legacy/ui/dashboards/base.py | 2 +- .../legacy/ui/demo_projects/bikes.py | 18 ++++------- 9 files changed, 25 insertions(+), 43 deletions(-) diff --git a/src/evidently/legacy/calculations/classification_performance.py b/src/evidently/legacy/calculations/classification_performance.py index 2021291b16..5f72ea3dd2 100644 --- a/src/evidently/legacy/calculations/classification_performance.py +++ b/src/evidently/legacy/calculations/classification_performance.py @@ -237,7 +237,7 @@ def get_prediction_data( ) -def _check_pos_labels(pos_label: Optional[Union[str, int]], labels: List[str]) -> Union[str, int]: +def _check_pos_labels(pos_label: Optional[Label], labels: List[str]) -> Label: if pos_label is None: raise ValueError("Undefined pos_label.") diff --git a/src/evidently/legacy/metric_results.py b/src/evidently/legacy/metric_results.py index 906cbaa26e..097e3d735c 100644 --- a/src/evidently/legacy/metric_results.py +++ b/src/evidently/legacy/metric_results.py @@ -15,12 +15,13 @@ import pandas as pd from pydantic import BeforeValidator from pydantic import TypeAdapter -from pydantic.v1 import validator +from pydantic import field_validator from typing_extensions import Literal from evidently.legacy.base_metric import MetricResult from evidently.legacy.core import IncludeTags from evidently.legacy.core import Label +from evidently.legacy.core import LabelIntStr from evidently.legacy.core import PydanticNPArray from evidently.legacy.core import pydantic_type_validator from evidently.legacy.pipeline.column_mapping import TargetNames @@ -32,30 +33,15 @@ _caches[List.__getitem__.__wrapped__].cache_clear() # type: ignore[attr-defined] -LabelList = List[Label] - - -class _LabelKeyType(int): - pass - - -LabelKey = Union[_LabelKeyType, Label] # type: ignore[valid-type] - - -@pydantic_type_validator(_LabelKeyType) -def label_key_valudator(value): - try: - return int(value) - except ValueError: - return value +LabelList = List[LabelIntStr] ScatterData = Union[pd.Series] ContourData = Tuple[PydanticNPArray, List[float], List[float]] -ColumnScatter = Dict[LabelKey, ScatterData] +ColumnScatter = Dict[LabelIntStr, ScatterData] ScatterAggData = Union[pd.DataFrame] -ColumnAggScatter = Dict[LabelKey, ScatterAggData] +ColumnAggScatter = Dict[LabelIntStr, ScatterAggData] class _ColumnScatterOrAggType: @@ -112,12 +98,12 @@ class PredictionData(MetricResult): labels: LabelList prediction_probas: Optional[pd.DataFrame] = None - @validator("prediction_probas") - def validate_prediction_probas(cls, value: pd.DataFrame, values): + @field_validator("prediction_probas") + def validate_prediction_probas(cls, value: pd.DataFrame, info): """Align label types""" if value is None: return None - labels = values["labels"] + labels = info.data["labels"] for col in list(value.columns): if col not in labels: if str(col) in labels: diff --git a/src/evidently/legacy/metrics/classification_performance/quality_by_feature_table.py b/src/evidently/legacy/metrics/classification_performance/quality_by_feature_table.py index f22800bf4f..d3f4005598 100644 --- a/src/evidently/legacy/metrics/classification_performance/quality_by_feature_table.py +++ b/src/evidently/legacy/metrics/classification_performance/quality_by_feature_table.py @@ -43,7 +43,7 @@ class ClassificationQualityByFeatureTableResults(MetricResult): } current: StatsByFeature - reference: Optional[StatsByFeature] + reference: Optional[StatsByFeature] = None target_name: str columns: List[str] diff --git a/src/evidently/legacy/metrics/data_drift/embeddings_drift.py b/src/evidently/legacy/metrics/data_drift/embeddings_drift.py index 5b43cef919..39217c6c82 100644 --- a/src/evidently/legacy/metrics/data_drift/embeddings_drift.py +++ b/src/evidently/legacy/metrics/data_drift/embeddings_drift.py @@ -3,7 +3,6 @@ from typing import List from typing import Optional -import numpy as np import pandas as pd from sklearn.manifold import TSNE @@ -11,6 +10,7 @@ from evidently.legacy.base_metric import Metric from evidently.legacy.base_metric import MetricResult from evidently.legacy.core import IncludeTags +from evidently.legacy.core import PydanticNPArray from evidently.legacy.metrics.data_drift.embedding_drift_methods import DriftMethod from evidently.legacy.metrics.data_drift.embedding_drift_methods import model from evidently.legacy.model.widget import BaseWidgetInfo @@ -41,15 +41,15 @@ class EmbeddingsDriftMetricResults(MetricResult): drift_score: float drift_detected: bool method_name: str - reference: np.ndarray - current: np.ndarray + reference: PydanticNPArray + current: PydanticNPArray class EmbeddingsDriftMetric(Metric[EmbeddingsDriftMetricResults]): __type_alias__: ClassVar[Optional[str]] = "evidently:metric:EmbeddingsDriftMetric" embeddings_name: str - drift_method: Optional[DriftMethod] + drift_method: Optional[DriftMethod] = None def __init__(self, embeddings_name: str, drift_method: Optional[DriftMethod] = None, options: AnyOptions = None): self.embeddings_name = embeddings_name diff --git a/src/evidently/legacy/suite/base_suite.py b/src/evidently/legacy/suite/base_suite.py index 9f006c5a7b..777387124a 100644 --- a/src/evidently/legacy/suite/base_suite.py +++ b/src/evidently/legacy/suite/base_suite.py @@ -615,7 +615,7 @@ def _parse_snapshot(cls: Type[T], payload: Snapshot) -> T: def save(self, filename) -> None: """Save state to file (experimental)""" - self._get_snapshot().save(filename) + self.to_snapshot().save(filename) @classmethod def load(cls: Type[T], filename) -> T: diff --git a/src/evidently/legacy/tests/base_test.py b/src/evidently/legacy/tests/base_test.py index 1bacb62ae4..b21ae50999 100644 --- a/src/evidently/legacy/tests/base_test.py +++ b/src/evidently/legacy/tests/base_test.py @@ -7,6 +7,7 @@ from typing import Dict from typing import Generic from typing import List +from typing import Literal from typing import Optional from typing import Type from typing import TypeVar @@ -100,6 +101,7 @@ class TestParameters(EvidentlyBaseModel, BaseResult): # type: ignore[misc] __type_alias__: ClassVar[Optional[str]] = "evidently:test_parameters:TestParameters" __is_base_type__: ClassVar[bool] = True __field_tags__: ClassVar[Dict[str, set]] = {"type": {IncludeTags.TypeField}} + type: Literal["evidently:test_parameters:TestParameters"] = "evidently:test_parameters:TestParameters" class TestResult(EnumValueMixin, MetricResult): # todo: create common base class diff --git a/src/evidently/legacy/tests/data_quality_tests.py b/src/evidently/legacy/tests/data_quality_tests.py index c59c0424d7..73ecda893b 100644 --- a/src/evidently/legacy/tests/data_quality_tests.py +++ b/src/evidently/legacy/tests/data_quality_tests.py @@ -1328,7 +1328,7 @@ class BaseDataQualityValueListMetricsTest(BaseCheckValueTest, ABC): group: ClassVar = DATA_QUALITY_GROUP.id _metric: ColumnValueListMetric column_name: str - values: Optional[list] + values: Optional[list] = None def __init__( self, diff --git a/src/evidently/legacy/ui/dashboards/base.py b/src/evidently/legacy/ui/dashboards/base.py index 286e9afedc..50422f1fff 100644 --- a/src/evidently/legacy/ui/dashboards/base.py +++ b/src/evidently/legacy/ui/dashboards/base.py @@ -60,7 +60,7 @@ class PanelValue(BaseModel): field_path: Union[str, FieldPath] metric_id: Optional[str] = None metric_fingerprint: Optional[str] = None - metric_args: Dict[str, Union[EvidentlyBaseModel, Any]] = {} + metric_args: Dict[str, Any] = {} legend: Optional[str] = None def __init__( diff --git a/src/evidently/legacy/ui/demo_projects/bikes.py b/src/evidently/legacy/ui/demo_projects/bikes.py index fdf38af497..9bb65f1970 100644 --- a/src/evidently/legacy/ui/demo_projects/bikes.py +++ b/src/evidently/legacy/ui/demo_projects/bikes.py @@ -5,9 +5,9 @@ from datetime import time from datetime import timedelta from itertools import cycle +from pathlib import Path import pandas as pd -import requests from dateutil.relativedelta import relativedelta from sklearn import ensemble @@ -26,24 +26,18 @@ from evidently.legacy.ui.demo_projects import DemoProject from evidently.legacy.ui.workspace.base import WorkspaceBase +test_data_path = Path(__file__).parent.parent.parent.parent.parent.parent / "test_data" + def create_data(): if os.path.exists("Bike-Sharing-Dataset.zip"): with open("Bike-Sharing-Dataset.zip", "rb") as f: content = f.read() - elif os.path.exists("../../../../../test_data/bike_sharing_dataset.zip"): - with open("../../../../../test_data/bike_sharing_dataset.zip", "rb") as f: + elif os.path.exists(test_data_path / "bike_sharing_dataset.zip"): + with open(test_data_path / "bike_sharing_dataset.zip", "rb") as f: content = f.read() else: - response = requests.get( - "https://archive.ics.uci.edu/static/public/275/bike+sharing+dataset.zip", - verify=False, - ) - if response.status_code != 200: - raise ValueError(f"Could not download bike sharing dataset. {response.text}") - if response.status_code == 200 and response.headers["content-type"] != "application/zip": - raise ValueError(f"Invalid bike sharing dataset content type: {response.headers['content-type']}.") - content = response.content + raise FileNotFoundError("Bike-Sharing-Dataset.zip not found") with zipfile.ZipFile(io.BytesIO(content)) as arc: raw_data = pd.read_csv( arc.open("hour.csv"), From 6d285478bba2fe5be9e94558511f4bd72c84c778 Mon Sep 17 00:00:00 2001 From: mike0sv Date: Fri, 20 Feb 2026 04:29:57 +0000 Subject: [PATCH 09/11] smore mypy and test fixes --- .../calculations/classification_performance.py | 3 +-- src/evidently/legacy/ui/dashboards/base.py | 8 ++++---- src/evidently/legacy/ui/storage/local/base.py | 3 ++- src/evidently/pydantic_utils.py | 17 +++++++++++++++-- 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/evidently/legacy/calculations/classification_performance.py b/src/evidently/legacy/calculations/classification_performance.py index 5f72ea3dd2..42bbe27672 100644 --- a/src/evidently/legacy/calculations/classification_performance.py +++ b/src/evidently/legacy/calculations/classification_performance.py @@ -3,7 +3,6 @@ from typing import List from typing import Optional from typing import Sequence -from typing import Union import numpy as np import pandas as pd @@ -248,7 +247,7 @@ def _check_pos_labels(pos_label: Optional[Label], labels: List[str]) -> Label: def threshold_probability_labels( - prediction_probas: pd.DataFrame, pos_label: Union[str, int], neg_label: Union[str, int], threshold: float + prediction_probas: pd.DataFrame, pos_label: Label, neg_label: Label, threshold: float ) -> pd.Series: """Get prediction values by probabilities with the threshold apply""" return prediction_probas[pos_label].apply(lambda x: pos_label if x >= threshold else neg_label) diff --git a/src/evidently/legacy/ui/dashboards/base.py b/src/evidently/legacy/ui/dashboards/base.py index 50422f1fff..5fbda52929 100644 --- a/src/evidently/legacy/ui/dashboards/base.py +++ b/src/evidently/legacy/ui/dashboards/base.py @@ -14,7 +14,7 @@ from pydantic import BaseModel from pydantic import ConfigDict from pydantic import Field -from pydantic.v1 import validator +from pydantic import field_validator from evidently.legacy.base_metric import Metric from evidently.legacy.core import new_id @@ -60,7 +60,7 @@ class PanelValue(BaseModel): field_path: Union[str, FieldPath] metric_id: Optional[str] = None metric_fingerprint: Optional[str] = None - metric_args: Dict[str, Any] = {} + metric_args: Dict[str, Union[EvidentlyBaseModel, Any]] = {} legend: Optional[str] = None def __init__( @@ -75,7 +75,7 @@ def __init__( ): # this __init__ is needed to support old-style metric_hash arg if metric_hash is not None: - warnings.warn("metric_hash arg is deperecated, please use metric_fingerprint") + warnings.warn("metric_hash arg is deprecated, please use metric_fingerprint") metric_fingerprint = metric_hash super().__init__( field_path=field_path, @@ -91,7 +91,7 @@ def field_path_str(self): return self.field_path.get_path() return self.field_path - @validator("field_path") + @field_validator("field_path") def validate_field_path(cls, value): if isinstance(value, FieldPath): value = value.get_path() diff --git a/src/evidently/legacy/ui/storage/local/base.py b/src/evidently/legacy/ui/storage/local/base.py index b603f76561..7c3f3a83a5 100644 --- a/src/evidently/legacy/ui/storage/local/base.py +++ b/src/evidently/legacy/ui/storage/local/base.py @@ -126,7 +126,8 @@ async def get_blob_metadata(self, blob_id: BlobID) -> BlobMetadata: def load_project(location: FSLocation, path: str) -> Optional[Project]: try: with location.open(posixpath.join(path, METADATA_PATH)) as f: - return TypeAdapter(Project).validate_python(json.load(f)) + data = json.load(f) + return TypeAdapter(Project).validate_python(data) except FileNotFoundError: return None diff --git a/src/evidently/pydantic_utils.py b/src/evidently/pydantic_utils.py index b0672f68c1..539035870e 100644 --- a/src/evidently/pydantic_utils.py +++ b/src/evidently/pydantic_utils.py @@ -309,6 +309,8 @@ def model_validate( @model_validator(mode="wrap") def _delegate_validation(cls, data, handler) -> Any: + if not isinstance(data, dict): + return handler(data) if "type" in data and data["type"] != cls.__get_type__(): return cls.model_validate(data) typename = data.pop("type", None) if isinstance(data, dict) else None @@ -481,11 +483,22 @@ def model_dump(self, *args, **kwargs) -> dict[str, Any]: res = super().model_dump(*args, **kwargs) return {k: self._to_enum_value(k, v) for k, v in res.items()} + @model_serializer(mode="wrap") + def _delegate_serialization(self, nxt: SerializerFunctionWrapHandler, info: SerializationInfo): + try: + res = super()._delegate_serialization(nxt, info) + except AttributeError: + res = nxt(self) + return {k: self._to_enum_value(k, v) for k, v in res.items()} + class ExcludeNoneMixin(BaseModel): @model_serializer(mode="wrap") - def exclude_none(self, nxt: SerializerFunctionWrapHandler): - res = nxt(self) + def _delegate_serialization(self, nxt: SerializerFunctionWrapHandler, info: SerializationInfo): + try: + res = super()._delegate_serialization(nxt, info) + except AttributeError: + res = nxt(self) return {k: v for k, v in res.items() if v is not None} From fef849f40a2f35b3ff7112c0d94140c385b42272 Mon Sep 17 00:00:00 2001 From: mike0sv Date: Fri, 20 Feb 2026 04:57:17 +0000 Subject: [PATCH 10/11] smore mypy and test fixes --- src/evidently/pydantic_utils.py | 2 +- tests/spark/metrics/test_data_drift.py | 15 +++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/evidently/pydantic_utils.py b/src/evidently/pydantic_utils.py index 539035870e..66a86a2bbc 100644 --- a/src/evidently/pydantic_utils.py +++ b/src/evidently/pydantic_utils.py @@ -489,7 +489,7 @@ def _delegate_serialization(self, nxt: SerializerFunctionWrapHandler, info: Seri res = super()._delegate_serialization(nxt, info) except AttributeError: res = nxt(self) - return {k: self._to_enum_value(k, v) for k, v in res.items()} + return {k: self._to_enum_value(k, v) for k, v in res.items()} # type: ignore[call-arg] class ExcludeNoneMixin(BaseModel): diff --git a/tests/spark/metrics/test_data_drift.py b/tests/spark/metrics/test_data_drift.py index 3b91c6c714..275f77c773 100644 --- a/tests/spark/metrics/test_data_drift.py +++ b/tests/spark/metrics/test_data_drift.py @@ -1,4 +1,3 @@ -import sys from typing import Callable from typing import List @@ -21,10 +20,10 @@ @slow -@pytest.mark.skipif( - sys.platform.startswith("win") or sys.platform == "darwin", - reason="skip spark on Windows and MacOS", -) +# @pytest.mark.skipif( +# sys.platform.startswith("win") or sys.platform == "darwin", +# reason="skip spark on Windows and MacOS", +# ) @pytest.mark.parametrize( "metric,column_mapping,result_adjust", [ @@ -57,7 +56,7 @@ "drift_by_columns.a.reference.correlations": lambda x: None, "drift_by_columns.b.current.correlations": lambda x: None, "drift_by_columns.b.reference.correlations": lambda x: None, - # todo + # # todo "dataset_columns": lambda x: DatasetColumns( utility_columns=DatasetUtilityColumns(), target_type=None, @@ -67,7 +66,7 @@ datetime_feature_names=[], target_names=[], task=None, - ), + ).dict(), }, ), ( @@ -90,7 +89,7 @@ datetime_feature_names=[], target_names=[], task=None, - ), + ).dict(), }, ), ], From 0aca8b7d965abd06aad7283da48bc2773231d462 Mon Sep 17 00:00:00 2001 From: mike0sv Date: Fri, 20 Feb 2026 09:27:28 +0000 Subject: [PATCH 11/11] oops --- tests/spark/metrics/test_data_drift.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/spark/metrics/test_data_drift.py b/tests/spark/metrics/test_data_drift.py index 275f77c773..2c3590875c 100644 --- a/tests/spark/metrics/test_data_drift.py +++ b/tests/spark/metrics/test_data_drift.py @@ -1,3 +1,4 @@ +import sys from typing import Callable from typing import List @@ -20,10 +21,10 @@ @slow -# @pytest.mark.skipif( -# sys.platform.startswith("win") or sys.platform == "darwin", -# reason="skip spark on Windows and MacOS", -# ) +@pytest.mark.skipif( + sys.platform.startswith("win") or sys.platform == "darwin", + reason="skip spark on Windows and MacOS", +) @pytest.mark.parametrize( "metric,column_mapping,result_adjust", [