rule-engine-core 0.0.1__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.
Files changed (22) hide show
  1. {rule_engine_core-0.0.1 → rule_engine_core-0.0.3}/PKG-INFO +2 -2
  2. {rule_engine_core-0.0.1 → rule_engine_core-0.0.3}/README.md +1 -1
  3. {rule_engine_core-0.0.1 → rule_engine_core-0.0.3}/rule_engine_core/entity_base.py +4 -4
  4. rule_engine_core-0.0.3/rule_engine_core/metadata_dao.py +89 -0
  5. {rule_engine_core-0.0.1 → rule_engine_core-0.0.3}/rule_engine_core/parser.py +2 -2
  6. {rule_engine_core-0.0.1 → rule_engine_core-0.0.3}/rule_engine_core.egg-info/PKG-INFO +2 -2
  7. {rule_engine_core-0.0.1 → rule_engine_core-0.0.3}/rule_engine_core.egg-info/SOURCES.txt +2 -0
  8. {rule_engine_core-0.0.1 → rule_engine_core-0.0.3}/setup.py +1 -1
  9. rule_engine_core-0.0.3/tests/test_metadata_dao.py +82 -0
  10. {rule_engine_core-0.0.1 → rule_engine_core-0.0.3}/pyproject.toml +0 -0
  11. {rule_engine_core-0.0.1 → rule_engine_core-0.0.3}/rule_engine_core/__init__.py +0 -0
  12. {rule_engine_core-0.0.1 → rule_engine_core-0.0.3}/rule_engine_core/entity_dao.py +0 -0
  13. {rule_engine_core-0.0.1 → rule_engine_core-0.0.3}/rule_engine_core/entity_types.py +0 -0
  14. {rule_engine_core-0.0.1 → rule_engine_core-0.0.3}/rule_engine_core/filter.py +0 -0
  15. {rule_engine_core-0.0.1 → rule_engine_core-0.0.3}/rule_engine_core/metadata_store.py +0 -0
  16. {rule_engine_core-0.0.1 → rule_engine_core-0.0.3}/rule_engine_core/rule.py +0 -0
  17. {rule_engine_core-0.0.1 → rule_engine_core-0.0.3}/rule_engine_core.egg-info/dependency_links.txt +0 -0
  18. {rule_engine_core-0.0.1 → rule_engine_core-0.0.3}/rule_engine_core.egg-info/requires.txt +0 -0
  19. {rule_engine_core-0.0.1 → rule_engine_core-0.0.3}/rule_engine_core.egg-info/top_level.txt +0 -0
  20. {rule_engine_core-0.0.1 → rule_engine_core-0.0.3}/setup.cfg +0 -0
  21. {rule_engine_core-0.0.1 → rule_engine_core-0.0.3}/tests/test_rule_creation_storage.py +0 -0
  22. {rule_engine_core-0.0.1 → rule_engine_core-0.0.3}/tests/test_rule_evaluation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rule-engine-core
3
- Version: 0.0.1
3
+ Version: 0.0.3
4
4
  Summary: A rule engine core library for Python.
5
5
  Home-page: https://github.com/temp-noob/rule-engine
6
6
  Author: Mohit Tripathi
@@ -24,7 +24,7 @@ Dynamic: summary
24
24
  ## Build
25
25
  -> create venv and activate
26
26
  (install meta)
27
- -> pip install steuptools wheel twine
27
+ -> pip install setuptools wheel twine
28
28
  (build package)
29
29
  -> python setup.py sdist bdist_wheel
30
30
  -> pip install dist/rule-engine-core-0.1.0-py3-none-any.whl
@@ -1,7 +1,7 @@
1
1
  ## Build
2
2
  -> create venv and activate
3
3
  (install meta)
4
- -> pip install steuptools wheel twine
4
+ -> pip install setuptools wheel twine
5
5
  (build package)
6
6
  -> python setup.py sdist bdist_wheel
7
7
  -> pip install dist/rule-engine-core-0.1.0-py3-none-any.whl
@@ -18,7 +18,7 @@ class EntityBase(ABC):
18
18
  Initializes the entity with an expression string and parses it into an expression tree.
19
19
  """
20
20
  self.expression = expression
21
- self._expression_tree: Node = self._parse_expression(expression)
21
+ self.expression_tree: Node = self._parse_expression(expression)
22
22
  self.node_type = node_type
23
23
  self.name = name
24
24
 
@@ -27,7 +27,7 @@ class EntityBase(ABC):
27
27
  Validates whether the operands of the entity are valid.
28
28
  _validate_node must be implemented by subclasses
29
29
  """
30
- return self._validate_node(self._expression_tree)
30
+ return self._validate_node(self.expression_tree)
31
31
 
32
32
  @abstractmethod
33
33
  def _validate_node(self, node: Node) -> bool:
@@ -57,7 +57,7 @@ class EntityBase(ABC):
57
57
  entity_eval_cache: Dictionary of cached evaluation results for entities. We do a dfs sort of thing and keep on populating the cache with the results.
58
58
  entity_cache: Dictionary of cached entities. This is used to get entity object by entity id.
59
59
  """
60
- return self._evaluate_node(EntityNode(self._expression_tree, self.node_type), pos_data, entity_eval_cache, entity_cache)
60
+ return self._evaluate_node(EntityNode(self.expression_tree, self.node_type), pos_data, entity_eval_cache, entity_cache)
61
61
 
62
62
  def _parse_expression(self, expression: str) -> Node:
63
63
  """
@@ -87,7 +87,7 @@ class EntityBase(ABC):
87
87
  'expression': self.expression,
88
88
  'node_type': self.node_type,
89
89
  'name': self.name,
90
- 'expression_tree': self._expression_tree,
90
+ 'expression_tree': self.expression_tree,
91
91
  }
92
92
 
93
93
 
@@ -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)
@@ -66,7 +66,7 @@ class Parser:
66
66
  def tokenize(self, text: str):
67
67
  # Define token specifications.
68
68
  token_spec = [
69
- ('NUMBER', r'\d+(\.\d+)?'), # Integer or decimal number
69
+ ('NUMBER', r'-?\d+(\.\d+)?'), # Integer or decimal number
70
70
  ('RELOP', r'(<=|>=|<|>)'), # Relational operators
71
71
  ('IDENT', r'[A-Za-z_]\w*'), # Identifiers (operands)
72
72
  ('OP', r'[+\-*/&|!]'), # Other operators
@@ -219,4 +219,4 @@ class Parser:
219
219
  _logger.error(errMsg)
220
220
  raise RuntimeError(errMsg)
221
221
  return node
222
-
222
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rule-engine-core
3
- Version: 0.0.1
3
+ Version: 0.0.3
4
4
  Summary: A rule engine core library for Python.
5
5
  Home-page: https://github.com/temp-noob/rule-engine
6
6
  Author: Mohit Tripathi
@@ -24,7 +24,7 @@ Dynamic: summary
24
24
  ## Build
25
25
  -> create venv and activate
26
26
  (install meta)
27
- -> pip install steuptools wheel twine
27
+ -> pip install setuptools wheel twine
28
28
  (build package)
29
29
  -> python setup.py sdist bdist_wheel
30
30
  -> pip install dist/rule-engine-core-0.1.0-py3-none-any.whl
@@ -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.1",
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()