Skip to content

Commit 475f860

Browse files
authored
Feat/dat 474 constraints (#67)
1 parent f37cb57 commit 475f860

36 files changed

+2137
-3
lines changed

datamimic_ce/constants/attribute_constants.py

+2
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,5 @@
7373
ATTR_STRING: Final = "string"
7474
ATTR_BUCKET: Final = "bucket"
7575
ATTR_MP_PLATFORM: Final = "mpPlatform"
76+
ATTR_IF: Final = "if"
77+
ATTR_THEN: Final = "then"

datamimic_ce/constants/element_constants.py

+2
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,5 @@
2525
EL_CONDITION = "condition"
2626
EL_ELSE_IF = "else-if"
2727
EL_ELSE = "else"
28+
EL_CONSTRAINTS = "constraints"
29+
EL_RULE = "rule"
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# DATAMIMIC
2+
# Copyright (c) 2023-2024 Rapiddweller Asia Co., Ltd.
3+
# This software is licensed under the MIT License.
4+
# See LICENSE file for the full text of the license.
5+
# For questions and support, contact: [email protected]
6+
7+
from pydantic import BaseModel
8+
9+
10+
class ConstraintsModel(BaseModel):
11+
pass

datamimic_ce/model/rule_model.py

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# DATAMIMIC
2+
# Copyright (c) 2023-2024 Rapiddweller Asia Co., Ltd.
3+
# This software is licensed under the MIT License.
4+
# See LICENSE file for the full text of the license.
5+
# For questions and support, contact: [email protected]
6+
from pydantic import BaseModel, Field, field_validator
7+
8+
from datamimic_ce.constants.attribute_constants import ATTR_IF, ATTR_THEN
9+
from datamimic_ce.model.model_util import ModelUtil
10+
11+
12+
class RuleModel(BaseModel):
13+
if_rule: str = Field(alias=ATTR_IF)
14+
then_rule: str = Field(alias=ATTR_THEN)
15+
16+
@field_validator("if_rule", "then_rule")
17+
@classmethod
18+
def validate_name(cls, value):
19+
return ModelUtil.check_not_empty(value=value)
+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# DATAMIMIC
2+
# Copyright (c) 2023-2024 Rapiddweller Asia Co., Ltd.
3+
# This software is licensed under the MIT License.
4+
# See LICENSE file for the full text of the license.
5+
# For questions and support, contact: [email protected]
6+
7+
8+
from pathlib import Path
9+
from xml.etree.ElementTree import Element
10+
11+
from datamimic_ce.constants.element_constants import EL_CONSTRAINTS
12+
from datamimic_ce.parsers.statement_parser import StatementParser
13+
from datamimic_ce.statements.composite_statement import CompositeStatement
14+
from datamimic_ce.statements.constraints_statement import ConstraintsStatement
15+
from datamimic_ce.utils.base_class_factory_util import BaseClassFactoryUtil
16+
17+
18+
class ConstraintsParser(StatementParser):
19+
def __init__(
20+
self,
21+
class_factory_util: BaseClassFactoryUtil,
22+
element: Element,
23+
properties: dict,
24+
):
25+
super().__init__(
26+
element,
27+
properties,
28+
valid_element_tag=EL_CONSTRAINTS,
29+
class_factory_util=class_factory_util,
30+
)
31+
32+
def parse(self, descriptor_dir: Path, parent_stmt: CompositeStatement) -> ConstraintsStatement:
33+
"""
34+
Parse element "constraints" into ConstraintsStatement
35+
:return:
36+
"""
37+
constraints_stmt = ConstraintsStatement(parent_stmt)
38+
sub_stmt_list = self._class_factory_util.get_parser_util_cls()().parse_sub_elements(
39+
class_factory_util=self._class_factory_util,
40+
descriptor_dir=descriptor_dir,
41+
element=self._element,
42+
properties=self._properties,
43+
parent_stmt=constraints_stmt,
44+
)
45+
constraints_stmt.sub_statements = sub_stmt_list
46+
return constraints_stmt

datamimic_ce/parsers/generate_parser.py

+16
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from datamimic_ce.constants.element_constants import EL_GENERATE
1111
from datamimic_ce.model.generate_model import GenerateModel
1212
from datamimic_ce.parsers.statement_parser import StatementParser
13+
from datamimic_ce.statements.constraints_statement import ConstraintsStatement
1314
from datamimic_ce.statements.generate_statement import GenerateStatement
1415
from datamimic_ce.statements.statement import Statement
1516
from datamimic_ce.utils.base_class_factory_util import BaseClassFactoryUtil
@@ -50,5 +51,20 @@ def parse(self, descriptor_dir: Path, parent_stmt: Statement, lazy_parse: bool =
5051
self._properties,
5152
gen_stmt,
5253
)
54+
55+
self._check_only_one_constraints_tag(sub_stmt_list)
56+
5357
gen_stmt.sub_statements = sub_stmt_list
5458
return gen_stmt
59+
60+
@staticmethod
61+
def _check_only_one_constraints_tag(sub_stmt_list: list[Statement]):
62+
"""
63+
Only one 'constraints' tag per generate
64+
"""
65+
count = 0
66+
for stmt in sub_stmt_list:
67+
if isinstance(stmt, ConstraintsStatement):
68+
count += 1
69+
if count > 1:
70+
raise SyntaxError("Only once <constraints> allow in per <generate>")

datamimic_ce/parsers/parser_util.py

+13-2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from datamimic_ce.constants.element_constants import (
1616
EL_ARRAY,
1717
EL_CONDITION,
18+
EL_CONSTRAINTS,
1819
EL_DATABASE,
1920
EL_ECHO,
2021
EL_ELEMENT,
@@ -32,12 +33,14 @@
3233
EL_MONGODB,
3334
EL_NESTED_KEY,
3435
EL_REFERENCE,
36+
EL_RULE,
3537
EL_SETUP,
3638
EL_VARIABLE,
3739
)
3840
from datamimic_ce.logger import logger
3941
from datamimic_ce.parsers.array_parser import ArrayParser
4042
from datamimic_ce.parsers.condition_parser import ConditionParser
43+
from datamimic_ce.parsers.constraints_parser import ConstraintsParser
4144
from datamimic_ce.parsers.database_parser import DatabaseParser
4245
from datamimic_ce.parsers.echo_parser import EchoParser
4346
from datamimic_ce.parsers.element_parser import ElementParser
@@ -54,6 +57,7 @@
5457
from datamimic_ce.parsers.memstore_parser import MemstoreParser
5558
from datamimic_ce.parsers.nested_key_parser import NestedKeyParser
5659
from datamimic_ce.parsers.reference_parser import ReferenceParser
60+
from datamimic_ce.parsers.rule_parser import RuleParser
5761
from datamimic_ce.parsers.variable_parser import VariableParser
5862
from datamimic_ce.statements.array_statement import ArrayStatement
5963
from datamimic_ce.statements.composite_statement import CompositeStatement
@@ -109,6 +113,7 @@ def get_valid_sub_elements_set_by_tag(ele_tag: str) -> set | None:
109113
EL_ELEMENT,
110114
EL_ARRAY,
111115
EL_CONDITION,
116+
EL_CONSTRAINTS,
112117
},
113118
EL_CONDITION: {EL_IF, EL_ELSE_IF, EL_ELSE},
114119
EL_GENERATE: {
@@ -122,6 +127,7 @@ def get_valid_sub_elements_set_by_tag(ele_tag: str) -> set | None:
122127
EL_ECHO,
123128
EL_CONDITION,
124129
EL_INCLUDE,
130+
EL_CONSTRAINTS,
125131
},
126132
EL_INCLUDE: {EL_SETUP},
127133
EL_ITEM: {EL_KEY, EL_NESTED_KEY, EL_LIST, EL_ARRAY, EL_ELEMENT},
@@ -130,6 +136,7 @@ def get_valid_sub_elements_set_by_tag(ele_tag: str) -> set | None:
130136
EL_IF: None,
131137
EL_ELSE_IF: None,
132138
EL_ELSE: None,
139+
EL_CONSTRAINTS: {EL_RULE},
133140
}
134141

135142
return valid_sub_element_dict.get(ele_tag, set())
@@ -189,6 +196,10 @@ def get_parser_by_element(class_factory_util: BaseClassFactoryUtil, element: Ele
189196
return ElementParser(class_factory_util, element=element, properties=properties)
190197
elif tag == EL_GENERATOR:
191198
return GeneratorParser(class_factory_util, element=element, properties=properties)
199+
elif tag == EL_CONSTRAINTS:
200+
return ConstraintsParser(class_factory_util, element=element, properties=properties)
201+
elif tag == EL_RULE:
202+
return RuleParser(class_factory_util, element=element, properties=properties)
192203
else:
193204
raise ValueError(f"Cannot get parser for element <{tag}>")
194205

@@ -238,9 +249,9 @@ def parse_sub_elements(
238249
| GeneratorParser,
239250
):
240251
stmt = parser.parse()
241-
elif isinstance(parser, KeyParser):
252+
elif isinstance(parser, KeyParser | RuleParser):
242253
stmt = parser.parse(descriptor_dir=descriptor_dir, parent_stmt=parent_stmt)
243-
elif isinstance(parser, ConditionParser):
254+
elif isinstance(parser, ConditionParser | ConstraintsParser):
244255
stmt = parser.parse(
245256
descriptor_dir=descriptor_dir, parent_stmt=cast(CompositeStatement, parent_stmt)
246257
)

datamimic_ce/parsers/rule_parser.py

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# DATAMIMIC
2+
# Copyright (c) 2023-2024 Rapiddweller Asia Co., Ltd.
3+
# This software is licensed under the MIT License.
4+
# See LICENSE file for the full text of the license.
5+
# For questions and support, contact: [email protected]
6+
from pathlib import Path
7+
from xml.etree.ElementTree import Element
8+
9+
from datamimic_ce.constants.element_constants import EL_RULE
10+
from datamimic_ce.model.rule_model import RuleModel
11+
from datamimic_ce.parsers.statement_parser import StatementParser
12+
from datamimic_ce.statements.rule_statement import RuleStatement
13+
from datamimic_ce.statements.statement import Statement
14+
from datamimic_ce.utils.base_class_factory_util import BaseClassFactoryUtil
15+
16+
17+
class RuleParser(StatementParser):
18+
"""
19+
Parse element "rule" to RuleStatement
20+
"""
21+
22+
def __init__(
23+
self,
24+
class_factory_util: BaseClassFactoryUtil,
25+
element: Element,
26+
properties: dict,
27+
):
28+
super().__init__(element, properties, valid_element_tag=EL_RULE, class_factory_util=class_factory_util)
29+
30+
def parse(self, descriptor_dir: Path, parent_stmt: Statement) -> RuleStatement:
31+
"""
32+
Parse element "xml-attribute" to XmlAttributeStatement
33+
:return:
34+
"""
35+
36+
return RuleStatement(self.validate_attributes(RuleModel))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# DATAMIMIC
2+
# Copyright (c) 2023-2024 Rapiddweller Asia Co., Ltd.
3+
# This software is licensed under the MIT License.
4+
# See LICENSE file for the full text of the license.
5+
# For questions and support, contact: [email protected]
6+
from datamimic_ce.statements.composite_statement import CompositeStatement
7+
8+
9+
class ConstraintsStatement(CompositeStatement):
10+
def __init__(self, parent_stmt: CompositeStatement):
11+
super().__init__(None, parent_stmt=parent_stmt)
+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# DATAMIMIC
2+
# Copyright (c) 2023-2024 Rapiddweller Asia Co., Ltd.
3+
# This software is licensed under the MIT License.
4+
# See LICENSE file for the full text of the license.
5+
# For questions and support, contact: [email protected]
6+
from datamimic_ce.model.rule_model import RuleModel
7+
from datamimic_ce.statements.statement import Statement
8+
9+
10+
class RuleStatement(Statement):
11+
def __init__(self, model: RuleModel):
12+
super().__init__(None, None)
13+
self._if_rule = model.if_rule
14+
self._then_rule = model.then_rule
15+
16+
@property
17+
def if_rule(self):
18+
return self._if_rule
19+
20+
@property
21+
def then_rule(self):
22+
return self._then_rule
+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# DATAMIMIC
2+
# Copyright (c) 2023-2024 Rapiddweller Asia Co., Ltd.
3+
# This software is licensed under the MIT License.
4+
# See LICENSE file for the full text of the license.
5+
# For questions and support, contact: [email protected]
6+
import copy
7+
import itertools
8+
9+
from datamimic_ce.contexts.context import SAFE_GLOBALS, DotableDict
10+
from datamimic_ce.data_sources.data_source_pagination import DataSourcePagination
11+
from datamimic_ce.statements.constraints_statement import ConstraintsStatement
12+
from datamimic_ce.statements.rule_statement import RuleStatement
13+
from datamimic_ce.tasks.task import Task
14+
15+
16+
class ConstraintsTask(Task):
17+
def __init__(self, statement: ConstraintsStatement):
18+
self._statement = statement
19+
20+
@property
21+
def statement(self) -> ConstraintsStatement:
22+
return self._statement
23+
24+
def execute(self, source_data, pagination: DataSourcePagination | None = None, cyclic: bool | None = False) -> list:
25+
filter_data = list(source_data)
26+
# If source is empty, return empty list
27+
if len(filter_data) == 0:
28+
return []
29+
30+
for i in range(len(filter_data) - 1, -1, -1): # Iterate from last to first
31+
data_dict = copy.deepcopy(filter_data[i])
32+
33+
for key, value in data_dict.items():
34+
if isinstance(value, dict):
35+
data_dict[key] = DotableDict(value)
36+
37+
for child_stmt in self.statement.sub_statements:
38+
if isinstance(child_stmt, RuleStatement):
39+
if_condition = eval(child_stmt.if_rule, SAFE_GLOBALS, data_dict)
40+
if isinstance(if_condition, bool) and if_condition:
41+
else_condition = eval(child_stmt.then_rule, SAFE_GLOBALS, data_dict)
42+
if isinstance(else_condition, bool) and else_condition is False:
43+
del filter_data[i] # remove data that not meet then_rule
44+
break
45+
# If filtered data is empty, return empty list
46+
if len(filter_data) == 0:
47+
return []
48+
49+
if pagination is None:
50+
start_idx = 0
51+
end_idx = len(filter_data)
52+
else:
53+
start_idx = pagination.skip
54+
end_idx = pagination.skip + pagination.limit
55+
# Get cyclic data from filtered data source
56+
if cyclic:
57+
iterator = itertools.cycle(filter_data)
58+
return [copy.deepcopy(ele) for ele in itertools.islice(iterator, start_idx, end_idx)]
59+
else:
60+
return list(itertools.islice(filter_data, start_idx, end_idx))

datamimic_ce/tasks/nested_key_task.py

+20-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from datamimic_ce.data_sources.data_source_registry import DataSourceRegistry
1616
from datamimic_ce.logger import logger
1717
from datamimic_ce.statements.nested_key_statement import NestedKeyStatement
18+
from datamimic_ce.tasks.constraints_task import ConstraintsTask
1819
from datamimic_ce.tasks.element_task import ElementTask
1920
from datamimic_ce.tasks.task import Task
2021
from datamimic_ce.utils.base_class_factory_util import BaseClassFactoryUtil
@@ -187,6 +188,9 @@ def _try_execute_sub_tasks(self, ctx: GenIterContext) -> dict:
187188
try:
188189
if isinstance(sub_task, ElementTask):
189190
attributes.update(sub_task.generate_xml_attribute(ctx))
191+
elif isinstance(sub_task, ConstraintsTask):
192+
# do not execute ConstraintsTask here, ConstraintsTask is for filter source data
193+
pass
190194
else:
191195
sub_task.execute(ctx)
192196
except StopIteration:
@@ -316,7 +320,8 @@ def _modify_nestedkey_data_list(self, parent_context: GenIterContext, value: lis
316320
:return:
317321
"""
318322
result = []
319-
323+
# filter source data by constraints
324+
value = self._filter_source_by_constraints_task(parent_context=parent_context, source_data=value)
320325
# Determine len of nestedkey
321326
count = self._determine_nestedkey_length(context=parent_context)
322327
value_len = len(value)
@@ -380,3 +385,17 @@ def _post_convert(self, value):
380385
for converter in self._converter_list:
381386
value = converter.convert(value)
382387
return value
388+
389+
def _filter_source_by_constraints_task(self, parent_context: GenIterContext, source_data: list) -> list:
390+
"""
391+
Execute ConstraintsTask to filter source data
392+
"""
393+
result = source_data
394+
if self._sub_tasks:
395+
for sub_task in self._sub_tasks:
396+
if isinstance(sub_task, ConstraintsTask):
397+
nestedkey_len = self._determine_nestedkey_length(context=parent_context)
398+
temp_pagination = DataSourcePagination(skip=0, limit=nestedkey_len) if nestedkey_len else None
399+
result = sub_task.execute(source_data, pagination=temp_pagination, cyclic=self.statement.cyclic)
400+
break
401+
return result

0 commit comments

Comments
 (0)