From 81e2498cfc067ffcb72624b7d1603eb41970e5f6 Mon Sep 17 00:00:00 2001 From: Charles <43383361+develop-cs@users.noreply.github.com> Date: Mon, 9 Dec 2024 15:29:23 +0100 Subject: [PATCH] chore: deprecate Pydantic V1 (#39) Signed-off-by: develop-cs <43383361+develop-cs@users.noreply.github.com> --- .pre-commit-config.yaml | 17 ++- CHANGELOG.md | 14 +++ pyproject.toml | 6 +- src/arta/_engine.py | 1 - src/arta/models.py | 103 +++++++++--------- .../math/rules_simpl_cond_math.yaml | 8 +- 6 files changed, 84 insertions(+), 65 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0931412..26ff3d3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ default_language_version: python: python3 repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: - id: check-ast - id: check-byte-order-marker @@ -26,20 +26,20 @@ repos: - id: check-added-large-files args: [--maxkb=500] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.6.3 + rev: v0.8.0 hooks: - id: ruff args: [--fix] - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.11.2 + rev: v1.13.0 hooks: - id: mypy args: [--config-file=pyproject.toml] files: src additional_dependencies: [pydantic~=2.0] - repo: https://github.com/gitleaks/gitleaks - rev: v8.18.4 + rev: v8.21.2 hooks: - id: gitleaks - repo: https://github.com/pypa/pip-audit @@ -48,13 +48,20 @@ repos: - id: pip-audit args: [--skip-editable] - repo: https://github.com/compilerla/conventional-pre-commit - rev: v3.4.0 + rev: v3.6.0 hooks: - id: conventional-pre-commit stages: [commit-msg] args: [feat, fix, ci, chore, test, docs] - repo: local hooks: + - id: cov-clean + name: Coverage - Clean + language: system + entry: coverage erase + types: [python] + pass_filenames: false + always_run: true - id: tox-check name: Tests entry: tox diff --git a/CHANGELOG.md b/CHANGELOG.md index 21c91ec..944b29f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,20 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.8.2] - November, 2024 + +### Maintenance + +* Use true Pydantic V2 (or Pydantic V1) models (`DeprecationWarning` added about Pydantic V1). + +### Documentation + +* New page: *"Use your business objects".* + +### Breaking changes + +* Because of using `StringConstraints` (w/ Pydantic V2) rather than `constr()`, we can't use plain `YES` or `NO` (YAML booleans) as rule ids anymore. Use `"YES"` or `"NO"` instead in your YAML file. + ## [0.8.1] - September, 2024 ### Fixes diff --git a/pyproject.toml b/pyproject.toml index ccfa73d..221cd13 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "arta" -version = "0.8.1" +version = "0.8.2" requires-python = ">3.8.0" description = "A Python Rules Engine - Make rule handling simple" readme = "README.md" @@ -33,7 +33,7 @@ classifiers = [ dependencies = [ "omegaconf>=2.0.0", - "pydantic>=1.0.0", + "pydantic<3.0.0", ] [project.urls] @@ -88,7 +88,7 @@ select = [ "D", # pydocstyle "NPY", # NumPy-specific rules ] -ignore = ["E501", "D2", "D3", "D4", "D104", "D100", "D106", "S311"] +ignore = ["E501", "D2", "D3", "D4", "D104", "D100", "D106", "S311", "UP007"] exclude = ["tests/*"] [tool.ruff.format] diff --git a/src/arta/_engine.py b/src/arta/_engine.py index 462e7a9..85ea1a2 100644 --- a/src/arta/_engine.py +++ b/src/arta/_engine.py @@ -288,7 +288,6 @@ def _build_rules( Return a dictionary of Rule instances built from the configuration. Args: - # rule_sets: Sets of rules to be loaded in the Rules Engine (as needed by further uses). std_condition_instances: Dictionary of condition instances (k: condition id, v: StandardCondition instance) action_functions: Dictionary of action functions (k: action name, v: Callable) config: Dictionary of the imported configuration from yaml files. diff --git a/src/arta/models.py b/src/arta/models.py index 0a258a3..05d578d 100644 --- a/src/arta/models.py +++ b/src/arta/models.py @@ -1,37 +1,41 @@ -"""Pydantic model implementations. +"""Pydantic model implementations.""" -Note: Having no "from __future__ import annotations" here is wanted (pydantic compatibility). -""" +from __future__ import annotations -from typing import Annotated, Any, Callable, Dict, List, Optional +from typing import Annotated, Any, Callable, Optional +from warnings import warn import pydantic from pydantic.version import VERSION from arta.utils import ParsingErrorStrategy -if not VERSION.startswith("1."): +PYDANTIC_V1: bool = VERSION.startswith("1.") + +if not PYDANTIC_V1: + # Pydantic V2 + # ---------------------------------- # For instantiation using rules_dict class RuleRaw(pydantic.BaseModel): """Pydantic model for validating a rule.""" - condition: Optional[Callable] - condition_parameters: Optional[Dict[str, Any]] + condition: Optional[Callable] = None + condition_parameters: Optional[dict[str, Any]] = None action: Callable - action_parameters: Optional[Dict[str, Any]] + action_parameters: Optional[dict[str, Any]] = None model_config = pydantic.ConfigDict(extra="forbid") - class RulesGroup(pydantic.RootModel): # noqa + class RulesGroup(pydantic.RootModel): """Pydantic model for validating a rules group.""" - root: Dict[str, RuleRaw] + root: dict[str, RuleRaw] - class RulesDict(pydantic.RootModel): # noqa + class RulesDict(pydantic.RootModel): """Pydantic model for validating rules dict instanciation.""" - root: Dict[str, RulesGroup] + root: dict[str, RulesGroup] # ---------------------------------- # For instantiation using config_path @@ -40,14 +44,14 @@ class Condition(pydantic.BaseModel): description: str validation_function: str - condition_parameters: Optional[Dict[str, Any]] = None + condition_parameters: Optional[dict[str, Any]] = None class RulesConfig(pydantic.BaseModel): """Pydantic model for validating a rule group from config file.""" condition: Optional[str] = None simple_condition: Optional[str] = None - action: Annotated[str, pydantic.StringConstraints(to_lower=True)] # type: ignore + action: Annotated[str, pydantic.StringConstraints(to_lower=True)] action_parameters: Optional[Any] = None model_config = pydantic.ConfigDict(extra="allow") @@ -55,37 +59,35 @@ class RulesConfig(pydantic.BaseModel): class Configuration(pydantic.BaseModel): """Pydantic model for validating configuration files.""" - conditions: Optional[Dict[str, Condition]] = None - conditions_source_modules: Optional[List[str]] = None - actions_source_modules: List[str] - custom_classes_source_modules: Optional[List[str]] = None - condition_factory_mapping: Optional[Dict[str, str]] = None - rules: Dict[str, Dict[str, Dict[str, RulesConfig]]] + conditions: Optional[dict[str, Condition]] = None + conditions_source_modules: Optional[list[str]] = None + actions_source_modules: list[str] + custom_classes_source_modules: Optional[list[str]] = None + condition_factory_mapping: Optional[dict[str, str]] = None + rules: dict[str, dict[str, dict[Annotated[str, pydantic.StringConstraints(to_upper=True)], RulesConfig]]] parsing_error_strategy: Optional[ParsingErrorStrategy] = None - model_config = pydantic.ConfigDict(extra="ignore") - - @pydantic.field_validator("rules", mode="before") # noqa - def upper_key(cls, vl): # noqa - """Validate and uppercase keys for RulesConfig""" - for k, v in vl.items(): - for kk, vv in v.items(): - for key, rules in [*vv.items()]: - if key != str(key).upper(): - del vl[k][kk][key] - vl[k][kk][str(key).upper()] = rules - return vl - else: + # Pydantic V1 + + warn( + ( + "Soon, Pydantic V1 will no longer be compatible with Arta. " + "Please, migrate to Pydantic V2 (https://docs.pydantic.dev/latest/migration/)." + ), + DeprecationWarning, + stacklevel=2, + ) class BaseModelV2(pydantic.BaseModel): """Wrapper to expose missed methods used elsewhere in the code""" - model_dump: Callable = pydantic.BaseModel.dict # noqa + model_dump: Callable = pydantic.BaseModel.dict @classmethod - def model_validate(cls, obj): # noqa - return cls.parse_obj(obj) # noqa + def model_validate(cls, obj): + """Method mapping between V1 to V2.""" + return cls.parse_obj(obj) # ---------------------------------- # For instantiation using rules_dict @@ -93,22 +95,22 @@ class RuleRaw(BaseModelV2): # type: ignore[no-redef] """Pydantic model for validating a rule.""" condition: Optional[Callable] - condition_parameters: Optional[Dict[str, Any]] + condition_parameters: Optional[dict[str, Any]] action: Callable - action_parameters: Optional[Dict[str, Any]] + action_parameters: Optional[dict[str, Any]] class Config: extra = "forbid" - class RulesGroup(pydantic.BaseModel): # type: ignore[no-redef] # noqa + class RulesGroup(pydantic.BaseModel): # type: ignore[no-redef] """Pydantic model for validating a rules group.""" - __root__: Dict[str, RuleRaw] # noqa + __root__: dict[str, RuleRaw] - class RulesDict(BaseModelV2): # type: ignore[no-redef] # noqa + class RulesDict(BaseModelV2): # type: ignore[no-redef] """Pydantic model for validating rules dict instanciation.""" - __root__: Dict[str, RulesGroup] # noqa + __root__: dict[str, RulesGroup] # ---------------------------------- # For instantiation using config_path @@ -117,7 +119,7 @@ class Condition(BaseModelV2): # type: ignore[no-redef] description: str validation_function: str - condition_parameters: Optional[Dict[str, Any]] + condition_parameters: Optional[dict[str, Any]] class RulesConfig(BaseModelV2): # type: ignore[no-redef] """Pydantic model for validating a rule group from config file.""" @@ -133,13 +135,10 @@ class Config: class Configuration(BaseModelV2): # type: ignore[no-redef] """Pydantic model for validating configuration files.""" - conditions: Optional[Dict[str, Condition]] - conditions_source_modules: Optional[List[str]] - actions_source_modules: List[str] - custom_classes_source_modules: Optional[List[str]] - condition_factory_mapping: Optional[Dict[str, str]] - rules: Dict[str, Dict[str, Dict[pydantic.constr(to_upper=True), RulesConfig]]] # type: ignore + conditions: Optional[dict[str, Condition]] + conditions_source_modules: Optional[list[str]] + actions_source_modules: list[str] + custom_classes_source_modules: Optional[list[str]] + condition_factory_mapping: Optional[dict[str, str]] + rules: dict[str, dict[str, dict[pydantic.constr(to_upper=True), RulesConfig]]] # type: ignore parsing_error_strategy: Optional[ParsingErrorStrategy] - - class Config: - extra = "ignore" diff --git a/tests/examples/simple_condition/math/rules_simpl_cond_math.yaml b/tests/examples/simple_condition/math/rules_simpl_cond_math.yaml index da12d32..0167666 100644 --- a/tests/examples/simple_condition/math/rules_simpl_cond_math.yaml +++ b/tests/examples/simple_condition/math/rules_simpl_cond_math.yaml @@ -75,26 +75,26 @@ rules: - " than " - threshold equal_1: - YES: + "YES": simple_condition: input.a==input.b action: concatenate_str action_parameters: list_str: - "yes" - NO: + "NO": simple_condition: input.a!=input.b action: concatenate_str action_parameters: list_str: - "no" equal_2: - YES: + "YES": simple_condition: input.a==1.3 action: concatenate_str action_parameters: list_str: - "yes" - NO: + "NO": simple_condition: input.a!=1.3 action: concatenate_str action_parameters: