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

Update pydantic to v2 #15

Merged
merged 13 commits into from
Oct 25, 2023
4 changes: 2 additions & 2 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: [3.7.16, 3.11]
python-version: [3.9, 3.11]
poetry-version: [1.4.2]
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
Expand Down Expand Up @@ -47,7 +47,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: [3.9]
python-version: [3.11]
poetry-version: [1.4.2]
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: [3.7.16]
python-version: [3.11]
poetry-version: [1.4.2]
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -282,3 +282,4 @@ $RECYCLE.BIN/

# Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option)

.vscode/settings.json
6 changes: 2 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,11 @@ init:

.PHONY: formatting
formatting:
poetry run isort --settings-path pyproject.toml ./
poetry run black --config pyproject.toml ./
poetry run ruff format .

.PHONY: check-formatting
check-formatting:
poetry run isort --settings-path pyproject.toml --check-only ./
poetry run black --config pyproject.toml --check ./
poetry run ruff check .

.PHONY: tests
tests:
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,16 @@ import retrack
parser = retrack.Parser(rule)

# Create a runner
runner = retrack.Runner(parser)
runner = retrack.Runner(parser, name="your-rule")

# Run the rule/model passing the data
runner.execute(data)
```

The `Parser` class parses the rule/model and creates a graph of nodes. The `Runner` class runs the rule/model using the data passed to the runner. The `data` is a dictionary or a list of dictionaries containing the data that will be used to evaluate the conditions and execute the actions. To see wich data is required for the given rule/model, check the `runner.request_model` property that is a pydantic model used to validate the data.

Optionally you can name the rule by passing the `name` field to the `retrack.Runner` constructor. This is useful to identify the rule when exceptions are raised.

### Creating a rule/model

A rule is a set of conditions and actions that are executed when the conditions are met. The conditions are evaluated using the data passed to the runner. The actions are executed when the conditions are met.
Expand Down
93 changes: 12 additions & 81 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "retrack"
version = "0.11.1"
version = "1.0.0-alpha.1"
description = "A business rules engine"
authors = ["Gabriel Guarisa <[email protected]>", "Nathalia Trotte <[email protected]>"]
license = "MIT"
Expand All @@ -10,90 +10,21 @@ homepage = "https://github.com/gabrielguarisa/retrack"
keywords = ["rules", "models", "business", "node", "graph"]

[tool.poetry.dependencies]
python = "^3.7.16"
pandas = [
{ version = "1.2.0", python = "<3.8" },
{ version = "^1.2.0", python = ">=3.8" }
]
numpy = [
{ version = "1.19.5", python = "<3.8" },
{ version = "^1.19.5", python = ">=3.8" }
]
pydantic = "^1.10.4"
networkx = [
{ version = "2.6.3", python = "<3.8" },
{ version = "^2.6.3", python = ">=3.8" }
]
python = ">=3.9,<4.0.0"
pandas = "^1.2.0"
numpy = "^1.19.5"
pydantic = "2.4.2"
networkx = "^2.6.3"
pandera = "^0.17.2"

[tool.poetry.dev-dependencies]
pytest = [
{ version = "6.2.2", python = "<3.8" },
{ version = "^6.2.4", python = ">=3.8" }
]
pytest-cov = [
{ version = "2.11.1", python = "<3.8" },
{ version = "^3.0.0", python = ">=3.8" }
]
black = [
{ version = "20.8b1", python = "<3.8" },
{ version = "^22.6.0", python = ">=3.8" }
]
isort = {extras = ["colors"], version = "*"}
pytest-mock = [
{ version = "3.5.1", python = "<3.8" },
{ version = "^3.10.0", python = ">=3.8" }
]

[tool.black]
# https://github.com/psf/black
target-version = ["py37"]
line-length = 88
color = true

exclude = '''
/(
\.git
| \.hg
| \.mypy_cache
| \.tox
| \.venv
| _build
| buck-out
| build
| dist
| env
| venv
)/
'''

[tool.isort]
# https://github.com/timothycrosley/isort/
py_version = 37
line_length = 88

known_typing = [
"typing",
"types",
"typing_extensions",
"mypy",
"mypy_extensions",
]
sections = [
"FUTURE",
"TYPING",
"STDLIB",
"THIRDPARTY",
"FIRSTPARTY",
"LOCALFOLDER",
]
include_trailing_comma = true
profile = "black"
multi_line_output = 3
indent = 4
color_output = true
pytest = "*"
pytest-cov = "*"
ruff = "^0.1.2"
pytest-mock = "*"

[tool.pytest.ini_options]
addopts = "--junitxml=pytest.xml -p no:warnings --cov-report term-missing:skip-covered --cov=retrack"
addopts = "-vv --junitxml=pytest.xml -p no:warnings --cov-report term-missing:skip-covered --cov=retrack"

[build-system]
requires = ["poetry-core>=1.0.0"]
Expand Down
31 changes: 30 additions & 1 deletion retrack/engine/parser.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import typing

import hashlib

from retrack import nodes, validators
from retrack.utils.registry import Registry
import json


class Parser:
Expand All @@ -28,11 +31,16 @@ def __init__(
self._set_indexes_by_kind_map()
self._set_execution_order()
self._set_indexes_by_memory_type_map()
self._set_version()

@property
def graph_data(self) -> dict:
return self.__graph_data

@property
def version(self) -> str:
return self._version

@staticmethod
def _check_input_data(data: dict):
if not isinstance(data, dict):
Expand Down Expand Up @@ -166,7 +174,7 @@ def _set_execution_order(self):
def get_node_connections(
self, node_id: str, is_input: bool = True, filter_by_connector=None
):
node_dict = self.get_by_id(node_id).dict(by_alias=True)
node_dict = self.get_by_id(node_id).model_dump(by_alias=True)

connectors = node_dict.get("inputs" if is_input else "outputs", {})
result = []
Expand Down Expand Up @@ -200,3 +208,24 @@ def _walk(self, actual_id: str, skiped_ids: list):
self._walk(next_id, skiped_ids)

return skiped_ids

def _set_version(self):
self._version = self.graph_data.get("version", None)

graph_json_content = (
json.dumps(self.graph_data["nodes"])
.replace(": ", ":")
.replace(", ", ",")
.encode("utf-8")
)
calculated_hash = hashlib.sha256(graph_json_content).hexdigest()[:10]

if self.version is None:
self._version = f"{calculated_hash}.dynamic"
else:
file_version_hash = self.version.split(".")[0]

if file_version_hash != calculated_hash:
raise ValueError(
f"Invalid version. Graph data has changed and the hash is different: {calculated_hash} != {file_version_hash}"
)
48 changes: 38 additions & 10 deletions retrack/engine/request_manager.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import typing

import pandas as pd
import pandera
import pydantic

from retrack.nodes.base import BaseNode, NodeKind
Expand All @@ -8,6 +10,7 @@
class RequestManager:
def __init__(self, inputs: typing.List[BaseNode]):
self._model = None
self._dataframe_model = None
self.inputs = inputs

@property
Expand Down Expand Up @@ -41,13 +44,19 @@ def inputs(self, inputs: typing.List[BaseNode]):

if len(self.inputs) > 0:
self._model = self.__create_model()
self._dataframe_model = self.__create_dataframe_model()
else:
self._model = None
self._dataframe_model = None

@property
def model(self) -> typing.Type[pydantic.BaseModel]:
return self._model

@property
def dataframe_model(self) -> pandera.DataFrameSchema:
return self._dataframe_model

def __create_model(
self, model_name: str = "RequestModel"
) -> typing.Type[pydantic.BaseModel]:
Expand All @@ -62,26 +71,45 @@ def __create_model(
fields = {}
for input_field in self.inputs:
fields[input_field.data.name] = (
(str, ...)
if input_field.data.default is None
else (str, input_field.data.default)
str,
pydantic.Field(
default=Ellipsis
if input_field.data.default is None
else input_field.data.default,
),
)

return pydantic.create_model(
model_name,
**fields,
)

def __create_dataframe_model(self) -> pandera.DataFrameSchema:
"""Create a pydantic model from the RequestManager's inputs"""
fields = {}
for input_field in self.inputs:
fields[input_field.data.name] = pandera.Column(
str,
nullable=input_field.data.default is not None,
coerce=True,
default=input_field.data.default,
)

return pandera.DataFrameSchema(
fields,
index=pandera.Index(int),
strict=True,
coerce=True,
)

def validate(
self,
payload: typing.Union[
typing.Dict[str, str], typing.List[typing.Dict[str, str]]
],
payload: pd.DataFrame,
) -> typing.List[pydantic.BaseModel]:
"""Validate the payload against the RequestManager's model

Args:
payload (typing.Union[typing.Dict[str, str], typing.List[typing.Dict[str, str]]]): The payload to validate
payload (pandas.DataFrame): The payload to validate

Raises:
ValueError: If the RequestManager has no model
Expand All @@ -92,7 +120,7 @@ def validate(
if self.model is None:
raise ValueError("No inputs found")

if not isinstance(payload, list):
payload = [payload]
if not isinstance(payload, pd.DataFrame):
raise TypeError(f"payload must be a pandas.DataFrame, not {type(payload)}")

return pydantic.parse_obj_as(typing.List[self.model], payload)
return self.dataframe_model.validate(payload)
Loading
Loading