Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

chore: deprecate Pydantic V1 #39

Merged
merged 1 commit into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -33,7 +33,7 @@ classifiers = [

dependencies = [
"omegaconf>=2.0.0",
"pydantic>=1.0.0",
"pydantic<3.0.0",
]

[project.urls]
Expand Down Expand Up @@ -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]
Expand Down
1 change: 0 additions & 1 deletion src/arta/_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
103 changes: 51 additions & 52 deletions src/arta/models.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -40,75 +44,73 @@ 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")

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
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
Expand All @@ -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."""
Expand All @@ -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"
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading