Skip to content

Commit

Permalink
Merge pull request #15 from gabrielguarisa/f/update-pydantic
Browse files Browse the repository at this point in the history
Update pydantic to v2
  • Loading branch information
gabrielguarisa authored Oct 25, 2023
2 parents 3767a12 + 57148cd commit 9c3b284
Show file tree
Hide file tree
Showing 35 changed files with 522 additions and 170 deletions.
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 <gabrielguarisa@gmail.com>", "Nathalia Trotte <nathaliatrotte@gmail.com>"]
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

0 comments on commit 9c3b284

Please sign in to comment.