rule-engine-core 0.0.2__tar.gz → 0.0.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {rule_engine_core-0.0.2 → rule_engine_core-0.0.3}/PKG-INFO +1 -1
- rule_engine_core-0.0.3/rule_engine_core/metadata_dao.py +89 -0
- {rule_engine_core-0.0.2 → rule_engine_core-0.0.3}/rule_engine_core.egg-info/PKG-INFO +1 -1
- {rule_engine_core-0.0.2 → rule_engine_core-0.0.3}/rule_engine_core.egg-info/SOURCES.txt +2 -0
- {rule_engine_core-0.0.2 → rule_engine_core-0.0.3}/setup.py +1 -1
- rule_engine_core-0.0.3/tests/test_metadata_dao.py +82 -0
- {rule_engine_core-0.0.2 → rule_engine_core-0.0.3}/README.md +0 -0
- {rule_engine_core-0.0.2 → rule_engine_core-0.0.3}/pyproject.toml +0 -0
- {rule_engine_core-0.0.2 → rule_engine_core-0.0.3}/rule_engine_core/__init__.py +0 -0
- {rule_engine_core-0.0.2 → rule_engine_core-0.0.3}/rule_engine_core/entity_base.py +0 -0
- {rule_engine_core-0.0.2 → rule_engine_core-0.0.3}/rule_engine_core/entity_dao.py +0 -0
- {rule_engine_core-0.0.2 → rule_engine_core-0.0.3}/rule_engine_core/entity_types.py +0 -0
- {rule_engine_core-0.0.2 → rule_engine_core-0.0.3}/rule_engine_core/filter.py +0 -0
- {rule_engine_core-0.0.2 → rule_engine_core-0.0.3}/rule_engine_core/metadata_store.py +0 -0
- {rule_engine_core-0.0.2 → rule_engine_core-0.0.3}/rule_engine_core/parser.py +0 -0
- {rule_engine_core-0.0.2 → rule_engine_core-0.0.3}/rule_engine_core/rule.py +0 -0
- {rule_engine_core-0.0.2 → rule_engine_core-0.0.3}/rule_engine_core.egg-info/dependency_links.txt +0 -0
- {rule_engine_core-0.0.2 → rule_engine_core-0.0.3}/rule_engine_core.egg-info/requires.txt +0 -0
- {rule_engine_core-0.0.2 → rule_engine_core-0.0.3}/rule_engine_core.egg-info/top_level.txt +0 -0
- {rule_engine_core-0.0.2 → rule_engine_core-0.0.3}/setup.cfg +0 -0
- {rule_engine_core-0.0.2 → rule_engine_core-0.0.3}/tests/test_rule_creation_storage.py +0 -0
- {rule_engine_core-0.0.2 → rule_engine_core-0.0.3}/tests/test_rule_evaluation.py +0 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DAO and sync helpers for metadata persisted in PostgreSQL.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import logging
|
|
8
|
+
import psycopg2
|
|
9
|
+
from typing import Optional
|
|
10
|
+
from psycopg2.extensions import connection as PgConnection
|
|
11
|
+
from psycopg2.extras import Json
|
|
12
|
+
from rule_engine_core.entity_dao import create_connection
|
|
13
|
+
|
|
14
|
+
_logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MetadataDao:
|
|
18
|
+
def __init__(self, config: Optional[dict] = None):
|
|
19
|
+
self._connection: Optional[PgConnection] = create_connection(config)
|
|
20
|
+
if self._connection is None:
|
|
21
|
+
raise RuntimeError("Failed to create a connection to the database")
|
|
22
|
+
|
|
23
|
+
def get_all_metadata(self) -> dict[str, dict]:
|
|
24
|
+
with self._connection.cursor() as cursor:
|
|
25
|
+
cursor.execute("SELECT column_name, metadata_json FROM metadata")
|
|
26
|
+
rows = cursor.fetchall()
|
|
27
|
+
return {row[0]: row[1] for row in rows}
|
|
28
|
+
|
|
29
|
+
def get_metadata(self, column_name: str) -> Optional[dict]:
|
|
30
|
+
with self._connection.cursor() as cursor:
|
|
31
|
+
cursor.execute(
|
|
32
|
+
"SELECT metadata_json FROM metadata WHERE column_name = %s",
|
|
33
|
+
(column_name,),
|
|
34
|
+
)
|
|
35
|
+
row = cursor.fetchone()
|
|
36
|
+
return row[0] if row is not None else None
|
|
37
|
+
|
|
38
|
+
def upsert_metadata(self, column_name: str, metadata_json: dict) -> bool:
|
|
39
|
+
with self._connection.cursor() as cursor:
|
|
40
|
+
try:
|
|
41
|
+
cursor.execute(
|
|
42
|
+
"""
|
|
43
|
+
INSERT INTO metadata (column_name, metadata_json)
|
|
44
|
+
VALUES (%s, %s)
|
|
45
|
+
ON CONFLICT (column_name)
|
|
46
|
+
DO UPDATE SET metadata_json = EXCLUDED.metadata_json
|
|
47
|
+
""",
|
|
48
|
+
(column_name, Json(metadata_json)),
|
|
49
|
+
)
|
|
50
|
+
self._connection.commit()
|
|
51
|
+
return True
|
|
52
|
+
except psycopg2.Error:
|
|
53
|
+
_logger.exception("Failed to upsert metadata for column: %s", column_name)
|
|
54
|
+
self._connection.rollback()
|
|
55
|
+
return False
|
|
56
|
+
|
|
57
|
+
def delete_metadata(self, column_name: str) -> bool:
|
|
58
|
+
with self._connection.cursor() as cursor:
|
|
59
|
+
try:
|
|
60
|
+
cursor.execute("DELETE FROM metadata WHERE column_name = %s", (column_name,))
|
|
61
|
+
self._connection.commit()
|
|
62
|
+
return True
|
|
63
|
+
except psycopg2.Error:
|
|
64
|
+
_logger.exception("Failed to delete metadata for column: %s", column_name)
|
|
65
|
+
self._connection.rollback()
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
def dump_metadata_to_file(self, metadata_store_file_path: str) -> dict[str, dict]:
|
|
69
|
+
metadata = self.get_all_metadata()
|
|
70
|
+
parent_dir = os.path.dirname(metadata_store_file_path)
|
|
71
|
+
if parent_dir:
|
|
72
|
+
os.makedirs(parent_dir, exist_ok=True)
|
|
73
|
+
with open(metadata_store_file_path, "w", encoding="utf-8") as metadata_file:
|
|
74
|
+
json.dump(metadata, metadata_file, indent=4)
|
|
75
|
+
return metadata
|
|
76
|
+
|
|
77
|
+
def __del__(self):
|
|
78
|
+
self.close()
|
|
79
|
+
|
|
80
|
+
def close(self):
|
|
81
|
+
if self._connection is not None:
|
|
82
|
+
self._connection.close()
|
|
83
|
+
self._connection = None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def sync_metadata_store_file(config: Optional[dict] = None, metadata_store_file_path: Optional[str] = None) -> dict[str, dict]:
|
|
87
|
+
resolved_metadata_store_file_path = metadata_store_file_path or os.getenv("METADATA_STORE_FILE_PATH", "metadata.json")
|
|
88
|
+
metadata_dao = MetadataDao(config)
|
|
89
|
+
return metadata_dao.dump_metadata_to_file(resolved_metadata_store_file_path)
|
|
@@ -6,6 +6,7 @@ rule_engine_core/entity_base.py
|
|
|
6
6
|
rule_engine_core/entity_dao.py
|
|
7
7
|
rule_engine_core/entity_types.py
|
|
8
8
|
rule_engine_core/filter.py
|
|
9
|
+
rule_engine_core/metadata_dao.py
|
|
9
10
|
rule_engine_core/metadata_store.py
|
|
10
11
|
rule_engine_core/parser.py
|
|
11
12
|
rule_engine_core/rule.py
|
|
@@ -14,5 +15,6 @@ rule_engine_core.egg-info/SOURCES.txt
|
|
|
14
15
|
rule_engine_core.egg-info/dependency_links.txt
|
|
15
16
|
rule_engine_core.egg-info/requires.txt
|
|
16
17
|
rule_engine_core.egg-info/top_level.txt
|
|
18
|
+
tests/test_metadata_dao.py
|
|
17
19
|
tests/test_rule_creation_storage.py
|
|
18
20
|
tests/test_rule_evaluation.py
|
|
@@ -14,7 +14,7 @@ def parse_requirements(filename):
|
|
|
14
14
|
path = os.path.join(os.getenv('BUILD_PATH'), "requirements.txt")
|
|
15
15
|
setup(
|
|
16
16
|
name="rule-engine-core",
|
|
17
|
-
version="0.0.
|
|
17
|
+
version="0.0.3",
|
|
18
18
|
author="Mohit Tripathi",
|
|
19
19
|
author_email="tripathimohit051@gmail.com",
|
|
20
20
|
description="A rule engine core library for Python.",
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from rule_engine_core.metadata_dao import MetadataDao, sync_metadata_store_file
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class StubCursor:
|
|
5
|
+
def __init__(self, connection):
|
|
6
|
+
self._connection = connection
|
|
7
|
+
self._rows = []
|
|
8
|
+
self._single_row = None
|
|
9
|
+
|
|
10
|
+
def __enter__(self):
|
|
11
|
+
return self
|
|
12
|
+
|
|
13
|
+
def __exit__(self, exc_type, exc, tb):
|
|
14
|
+
return False
|
|
15
|
+
|
|
16
|
+
def execute(self, query, params=None):
|
|
17
|
+
if "SELECT column_name, metadata_json FROM metadata" in query:
|
|
18
|
+
self._rows = [(k, v) for k, v in self._connection.store.items()]
|
|
19
|
+
elif "SELECT metadata_json FROM metadata WHERE column_name = %s" in query:
|
|
20
|
+
value = self._connection.store.get(params[0])
|
|
21
|
+
self._single_row = (value,) if value is not None else None
|
|
22
|
+
elif "INSERT INTO metadata" in query:
|
|
23
|
+
self._connection.store[params[0]] = getattr(params[1], "adapted", params[1])
|
|
24
|
+
elif "DELETE FROM metadata WHERE column_name = %s" in query:
|
|
25
|
+
self._connection.store.pop(params[0], None)
|
|
26
|
+
|
|
27
|
+
def fetchall(self):
|
|
28
|
+
return self._rows
|
|
29
|
+
|
|
30
|
+
def fetchone(self):
|
|
31
|
+
return self._single_row
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class StubConnection:
|
|
35
|
+
def __init__(self):
|
|
36
|
+
self.store = {"price": {"type": "float"}}
|
|
37
|
+
self.closed = 0
|
|
38
|
+
|
|
39
|
+
def cursor(self):
|
|
40
|
+
return StubCursor(self)
|
|
41
|
+
|
|
42
|
+
def commit(self):
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
def rollback(self):
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
def close(self):
|
|
49
|
+
self.closed = 1
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_metadata_dao_crud_and_dump(monkeypatch, tmp_path):
|
|
53
|
+
stub_connection = StubConnection()
|
|
54
|
+
monkeypatch.setattr("rule_engine_core.metadata_dao.create_connection", lambda _: stub_connection)
|
|
55
|
+
metadata_dao = MetadataDao()
|
|
56
|
+
|
|
57
|
+
assert metadata_dao.get_all_metadata() == {"price": {"type": "float"}}
|
|
58
|
+
assert metadata_dao.get_metadata("price") == {"type": "float"}
|
|
59
|
+
assert metadata_dao.get_metadata("missing") is None
|
|
60
|
+
|
|
61
|
+
assert metadata_dao.upsert_metadata("country", {"type": "string"})
|
|
62
|
+
assert metadata_dao.get_metadata("country") == {"type": "string"}
|
|
63
|
+
|
|
64
|
+
assert metadata_dao.delete_metadata("country")
|
|
65
|
+
assert metadata_dao.get_metadata("country") is None
|
|
66
|
+
|
|
67
|
+
metadata_file_path = tmp_path / "metadata.json"
|
|
68
|
+
metadata = metadata_dao.dump_metadata_to_file(str(metadata_file_path))
|
|
69
|
+
assert metadata == {"price": {"type": "float"}}
|
|
70
|
+
assert metadata_file_path.exists()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_sync_metadata_store_file_uses_env_path(monkeypatch, tmp_path):
|
|
74
|
+
stub_connection = StubConnection()
|
|
75
|
+
monkeypatch.setattr("rule_engine_core.metadata_dao.create_connection", lambda _: stub_connection)
|
|
76
|
+
metadata_file_path = tmp_path / "metadata-from-env.json"
|
|
77
|
+
monkeypatch.setenv("METADATA_STORE_FILE_PATH", str(metadata_file_path))
|
|
78
|
+
|
|
79
|
+
metadata = sync_metadata_store_file()
|
|
80
|
+
|
|
81
|
+
assert metadata == {"price": {"type": "float"}}
|
|
82
|
+
assert metadata_file_path.exists()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rule_engine_core-0.0.2 → rule_engine_core-0.0.3}/rule_engine_core.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|