ApiLogicServer 14.2.20__py3-none-any.whl → 14.3.7__py3-none-any.whl
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.
- {ApiLogicServer-14.2.20.dist-info → ApiLogicServer-14.3.7.dist-info}/METADATA +2 -2
- {ApiLogicServer-14.2.20.dist-info → ApiLogicServer-14.3.7.dist-info}/RECORD +90 -69
- api_logic_server_cli/api_logic_server.py +5 -1
- api_logic_server_cli/api_logic_server_info.yaml +3 -3
- api_logic_server_cli/cli.py +5 -2
- api_logic_server_cli/create_from_model/__pycache__/api_logic_server_utils.cpython-312.pyc +0 -0
- api_logic_server_cli/create_from_model/__pycache__/ont_build.cpython-312.pyc +0 -0
- api_logic_server_cli/create_from_model/__pycache__/ont_create.cpython-312.pyc +0 -0
- api_logic_server_cli/create_from_model/api_logic_server_utils.py +4 -0
- api_logic_server_cli/create_from_model/ont_build.py +53 -19
- api_logic_server_cli/create_from_model/ont_create.py +14 -5
- api_logic_server_cli/fragments/declare_logic.py +72 -0
- api_logic_server_cli/{prototypes/manager/system/genai/create_db_models_inserts/logic_discovery_prefix.py → fragments/declare_logic_begin.py} +2 -1
- api_logic_server_cli/fragments/declare_logic_end.py +52 -0
- api_logic_server_cli/genai/genai.py +25 -8
- api_logic_server_cli/genai/genai_logic_builder.py +14 -11
- api_logic_server_cli/genai/genai_svcs.py +104 -7
- api_logic_server_cli/manager.py +20 -16
- api_logic_server_cli/model_migrator/model_migrator_start.py +1 -1
- api_logic_server_cli/model_migrator/reposreader.py +9 -1
- api_logic_server_cli/model_migrator/rule_obj.py +24 -6
- api_logic_server_cli/prototypes/base/api/api_discovery/ontimize_api.py +4 -1
- api_logic_server_cli/prototypes/base/api/system/expression_parser.py +10 -4
- api_logic_server_cli/prototypes/base/config/activate_logicbank.py +8 -4
- api_logic_server_cli/prototypes/base/database/bind_dbs.py +1 -1
- api_logic_server_cli/prototypes/base/database/test_data/readme.md +5 -5
- api_logic_server_cli/prototypes/base/integration/kafka/kafka_producer.py +32 -8
- api_logic_server_cli/prototypes/base/integration/system/RowDictMapper.py +33 -16
- api_logic_server_cli/prototypes/base/logic/declare_logic.py +9 -3
- api_logic_server_cli/prototypes/base/logic/load_verify_rules.py +217 -0
- api_logic_server_cli/prototypes/base/logic/logic_discovery/auto_discovery.py +22 -13
- api_logic_server_cli/prototypes/genai_demo/api/customize_api.py +9 -11
- api_logic_server_cli/prototypes/genai_demo/database/.DS_Store +0 -0
- api_logic_server_cli/prototypes/genai_demo/database/db.sqlite +0 -0
- api_logic_server_cli/prototypes/genai_demo/database/models.py +52 -42
- api_logic_server_cli/prototypes/genai_demo/integration/row_dict_maps/OrderB2B.py +4 -6
- api_logic_server_cli/prototypes/genai_demo/integration/row_dict_maps/__pycache__/OrderB2B.cpython-312.pyc +0 -0
- api_logic_server_cli/prototypes/genai_demo/integration/row_dict_maps/row_dict_maps_readme.md +3 -0
- api_logic_server_cli/prototypes/genai_demo/logic/__pycache__/declare_logic.cpython-312.pyc +0 -0
- api_logic_server_cli/prototypes/genai_demo/logic/__pycache__/load_verify_rules.cpython-312.pyc +0 -0
- api_logic_server_cli/prototypes/genai_demo/logic/declare_logic.py +58 -62
- api_logic_server_cli/prototypes/genai_demo/logic/load_verify_rules.py +216 -0
- api_logic_server_cli/prototypes/genai_demo/logic/logic_discovery/__pycache__/__init__.cpython-312.pyc +0 -0
- api_logic_server_cli/prototypes/genai_demo/logic/logic_discovery/__pycache__/auto_discovery.cpython-312.pyc +0 -0
- api_logic_server_cli/prototypes/genai_demo/logic/logic_discovery/__pycache__/error_testing.cpython-312.pyc +0 -0
- api_logic_server_cli/prototypes/genai_demo/logic/logic_discovery/auto_discovery.py +52 -0
- api_logic_server_cli/prototypes/genai_demo/logic/readme_declare_logic.md +172 -0
- api_logic_server_cli/prototypes/genai_demo/security/__pycache__/declare_security.cpython-312.pyc +0 -0
- api_logic_server_cli/prototypes/genai_demo/ui/admin/admin.yaml +86 -53
- api_logic_server_cli/prototypes/manager/.vscode/launch.json +1 -1
- api_logic_server_cli/prototypes/manager/README.md +19 -4
- api_logic_server_cli/prototypes/manager/system/genai/examples/genai_demo/genai_demo.prompt +4 -1
- api_logic_server_cli/prototypes/manager/system/genai/examples/genai_demo/genai_demo.response_example +34 -26
- api_logic_server_cli/prototypes/manager/system/genai/examples/genai_demo/genai_demo_informal.prompt +3 -0
- api_logic_server_cli/prototypes/manager/system/genai/examples/genai_demo/genai_demo_iteration_discount/.DS_Store +0 -0
- api_logic_server_cli/prototypes/manager/system/genai/examples/genai_demo/genai_demo_iteration_discount/000_you_are.prompt +1 -0
- api_logic_server_cli/prototypes/manager/system/genai/examples/genai_demo/genai_demo_iteration_discount/001_logic_training.prompt +314 -0
- api_logic_server_cli/prototypes/manager/system/genai/examples/genai_demo/genai_demo_iteration_discount/002_create_db_models.prompt +150 -0
- api_logic_server_cli/prototypes/manager/system/genai/examples/genai_demo/genai_demo_iteration_discount/003_create_db_models.response +134 -0
- api_logic_server_cli/prototypes/manager/system/genai/examples/genai_demo/genai_demo_iteration_discount/004_iteratio_logic.prompt +131 -0
- api_logic_server_cli/prototypes/manager/system/genai/examples/genai_demo/genai_demo_iteration_discount/005_create_db_models.response-example +141 -0
- api_logic_server_cli/prototypes/manager/system/genai/examples/genai_demo/genai_demo_iteration_discount/create_db_models.py +105 -0
- api_logic_server_cli/prototypes/manager/system/genai/examples/genai_demo/genai_demo_iteration_discount/db.dbml +70 -0
- api_logic_server_cli/prototypes/manager/system/genai/examples/genai_demo/genai_demo_iteration_discount/readme.md +6 -0
- api_logic_server_cli/prototypes/manager/system/genai/examples/genai_demo/genai_demo_iteration_discount/response.json +178 -0
- api_logic_server_cli/prototypes/manager/system/genai/examples/genai_demo/wg_dev_merge/base_genai_demo_no_logic/logic/declare_logic.py +0 -1
- api_logic_server_cli/prototypes/manager/system/genai/examples/genai_demo/wg_dev_merge/dev_demo_no_logic_fixed/logic/declare_logic.py +0 -1
- api_logic_server_cli/prototypes/manager/system/genai/examples/genai_demo/wg_dev_merge/wg_demo_no_logic_fixed/genai/examples/genai_demo/wg_dev_merge/base_genai_demo_no_logic/logic/declare_logic.py +0 -1
- api_logic_server_cli/prototypes/manager/system/genai/examples/genai_demo/wg_dev_merge/wg_demo_no_logic_fixed/genai/examples/genai_demo/wg_dev_merge/dev_demo_no_logic_fixed/logic/declare_logic.py +0 -1
- api_logic_server_cli/prototypes/manager/system/genai/examples/genai_demo/wg_dev_merge/wg_demo_no_logic_fixed/genai/examples/genai_demo/wg_dev_merge/wg_genai_demo_no_logic_fixed_from_CLI/logic/declare_logic.py +0 -1
- api_logic_server_cli/prototypes/manager/system/genai/examples/genai_demo/wg_dev_merge/wg_demo_no_logic_fixed/logic/declare_logic.py +0 -1
- api_logic_server_cli/prototypes/manager/system/genai/examples/genai_demo/wg_dev_merge/wg_demo_no_logic_fixed/system/genai/examples/genai_demo/wg_dev_merge/base_genai_demo_no_logic/logic/declare_logic.py +0 -1
- api_logic_server_cli/prototypes/manager/system/genai/examples/genai_demo/wg_dev_merge/wg_demo_no_logic_fixed/system/genai/examples/genai_demo/wg_dev_merge/dev_demo_no_logic_fixed/logic/declare_logic.py +0 -1
- api_logic_server_cli/prototypes/manager/system/genai/examples/genai_demo/wg_dev_merge/wg_demo_no_logic_fixed/system/genai/examples/genai_demo/wg_dev_merge/wg_genai_demo_no_logic_fixed_from_CLI/logic/declare_logic.py +0 -1
- api_logic_server_cli/prototypes/manager/system/genai/examples/time_tracking_billing/002_create_db_models.prompt +194 -0
- api_logic_server_cli/prototypes/manager/system/genai/examples/time_tracking_billing/003_create_db_models.response +298 -0
- api_logic_server_cli/prototypes/{genai_demo/database/chatgpt/sample_ai.sqlite → manager/system/genai/examples/time_tracking_billing/db.sqlite} +0 -0
- api_logic_server_cli/prototypes/manager/system/genai/examples/time_tracking_billing/readme.md +61 -0
- api_logic_server_cli/prototypes/manager/system/genai/learning_requests/logic_bank_api.prompt +29 -11
- api_logic_server_cli/prototypes/manager/system/genai/prompt_inserts/iteration.prompt +2 -1
- api_logic_server_cli/prototypes/nw_no_cust/venv_setup/system_note.txt +1 -1
- api_logic_server_cli/prototypes/ont_app/templates/home_tree_template.html +9 -0
- api_logic_server_cli/prototypes/ont_app/templates/tree_routing.jinja +32 -0
- api_logic_server_cli/sqlacodegen_wrapper/sqlacodegen/sqlacodegen/__pycache__/codegen.cpython-312.pyc +0 -0
- api_logic_server_cli/sqlacodegen_wrapper/sqlacodegen/sqlacodegen/codegen.py +4 -2
- api_logic_server_cli/tools/mini_skel/logic/load_verify_rules.py +1 -1
- api_logic_server_cli/model_migrator/system/custom_endpoint.py +0 -545
- api_logic_server_cli/prototypes/base/database/test_data/z_test_data_rows.py +0 -98
- api_logic_server_cli/prototypes/genai_demo/database/chatgpt/__pycache__/copilot_models.cpython-312.pyc +0 -0
- api_logic_server_cli/prototypes/genai_demo/database/chatgpt/__pycache__/sample_ai_models.cpython-312.pyc +0 -0
- api_logic_server_cli/prototypes/genai_demo/database/chatgpt/sample_ai.chatgpt +0 -16
- api_logic_server_cli/prototypes/genai_demo/database/chatgpt/sample_ai.sql +0 -66
- api_logic_server_cli/prototypes/genai_demo/database/chatgpt/sample_ai_items.sqlite +0 -0
- api_logic_server_cli/prototypes/genai_demo/database/chatgpt/sample_ai_models.py +0 -156
- api_logic_server_cli/prototypes/genai_demo/database/chatgpt/sample_ai_models.sqlite +0 -0
- api_logic_server_cli/prototypes/genai_demo/logic/cocktail-napkin.jpg +0 -0
- {ApiLogicServer-14.2.20.dist-info → ApiLogicServer-14.3.7.dist-info}/LICENSE +0 -0
- {ApiLogicServer-14.2.20.dist-info → ApiLogicServer-14.3.7.dist-info}/WHEEL +0 -0
- {ApiLogicServer-14.2.20.dist-info → ApiLogicServer-14.3.7.dist-info}/entry_points.txt +0 -0
- {ApiLogicServer-14.2.20.dist-info → ApiLogicServer-14.3.7.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
#
|
|
2
|
+
# This code loads and verifies rules from export.json and activates them if they pass verification
|
|
3
|
+
# It is WebGenAI specific, used only when env var WG_PROJECT is set
|
|
4
|
+
#
|
|
5
|
+
import ast
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import safrs
|
|
11
|
+
import subprocess
|
|
12
|
+
from importlib import import_module
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from werkzeug.utils import secure_filename
|
|
15
|
+
from database.models import *
|
|
16
|
+
from logic_bank.logic_bank import DeclareRule, Rule, LogicBank
|
|
17
|
+
from colorama import Fore, Style, init
|
|
18
|
+
from logic_bank.logic_bank import RuleBank
|
|
19
|
+
from logic_bank.rule_bank.rule_bank_setup import find_referenced_attributes
|
|
20
|
+
import tempfile
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
app_logger = logging.getLogger(__name__)
|
|
24
|
+
declare_logic_message = "ALERT: *** No Rules Yet ***" # printed in api_logic_server.py
|
|
25
|
+
|
|
26
|
+
rule_import_template = """
|
|
27
|
+
from logic_bank.logic_bank import Rule
|
|
28
|
+
from database.models import *
|
|
29
|
+
|
|
30
|
+
def init_rule():
|
|
31
|
+
{rule_code}
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
MANAGER_PATH = "/opt/webgenai/database/manager.py"
|
|
35
|
+
EXPORT_JSON_PATH = os.environ.get("EXPORT_JSON_PATH", "./docs/export/export.json")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def set_rule_status(rule_id, status):
|
|
39
|
+
"""
|
|
40
|
+
Call the manager.py script to set the status of a rule
|
|
41
|
+
|
|
42
|
+
(if the status is "active", the manager will remove the rule error)
|
|
43
|
+
"""
|
|
44
|
+
if not Path(MANAGER_PATH).exists():
|
|
45
|
+
app_logger.info(f"No manager, can't set rule {rule_id} status {status}")
|
|
46
|
+
return
|
|
47
|
+
subprocess.run([
|
|
48
|
+
"python", MANAGER_PATH,
|
|
49
|
+
"-R", rule_id,
|
|
50
|
+
"--rule-status", status],
|
|
51
|
+
cwd="/opt/webgenai")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def set_rule_error(rule_id, error):
|
|
55
|
+
"""
|
|
56
|
+
Call the manager.py script to set the error of a rule
|
|
57
|
+
"""
|
|
58
|
+
if not Path(MANAGER_PATH).exists():
|
|
59
|
+
app_logger.warning(f"No manager, can't set rule {rule_id} error {error}")
|
|
60
|
+
return
|
|
61
|
+
subprocess.check_output([
|
|
62
|
+
"python", MANAGER_PATH,
|
|
63
|
+
"-R", rule_id,
|
|
64
|
+
"--rule-error", error],
|
|
65
|
+
cwd="/opt/webgenai")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def check_rule_code_syntax(rule_code):
|
|
69
|
+
"""
|
|
70
|
+
Check the syntax of the rule code
|
|
71
|
+
"""
|
|
72
|
+
try:
|
|
73
|
+
ast.parse(rule_code)
|
|
74
|
+
return rule_code
|
|
75
|
+
except Exception as exc:
|
|
76
|
+
log.warning(f"Syntax error in rule code '{rule_code}': {exc}")
|
|
77
|
+
|
|
78
|
+
rule_code = rule_code.replace("\\\\", "\\")
|
|
79
|
+
try:
|
|
80
|
+
ast.parse(rule_code)
|
|
81
|
+
return rule_code
|
|
82
|
+
except Exception as exc:
|
|
83
|
+
log.warning(f"Syntax error in rule code '{rule_code}': {exc}")
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def get_exported_rules(rule_code_dir):
|
|
88
|
+
"""
|
|
89
|
+
Read the exported rules from export.json and write the code to the
|
|
90
|
+
rule_code_dir
|
|
91
|
+
"""
|
|
92
|
+
export_file = Path(EXPORT_JSON_PATH)
|
|
93
|
+
if not export_file.exists():
|
|
94
|
+
app_logger.info(f"{export_file.resolve()} does not exist")
|
|
95
|
+
return []
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
with open(export_file) as f:
|
|
99
|
+
export = json.load(f)
|
|
100
|
+
rules = export.get("rules", [])
|
|
101
|
+
except Exception as exc:
|
|
102
|
+
app_logger.warning(f"Failed to load rules from {export_file}: {exc}")
|
|
103
|
+
return []
|
|
104
|
+
|
|
105
|
+
for rule in rules:
|
|
106
|
+
if rule["status"] == "rejected":
|
|
107
|
+
continue
|
|
108
|
+
rule_file = rule_code_dir / f"{secure_filename(rule['name']).replace('.','_')}.py"
|
|
109
|
+
try:
|
|
110
|
+
# write current rule to rule_file
|
|
111
|
+
# (we can't use eval, because logicbank uses inspect)
|
|
112
|
+
rule_code_str = check_rule_code_syntax(rule["code"])
|
|
113
|
+
if not rule_code_str:
|
|
114
|
+
continue
|
|
115
|
+
with open(rule_file, "w") as temp_file:
|
|
116
|
+
rule_code = "\n".join([f" {code}" for code in rule_code_str.split("\n")])
|
|
117
|
+
temp_file.write(rule_import_template.format(rule_code=rule_code))
|
|
118
|
+
temp_file_path = temp_file.name
|
|
119
|
+
# module_name used to import current rule
|
|
120
|
+
module_name = Path(temp_file_path).stem
|
|
121
|
+
rule["module_name"] = module_name
|
|
122
|
+
app_logger.info(f"{rule['id']} rule file: {rule_file}")
|
|
123
|
+
except Exception as exc:
|
|
124
|
+
app_logger.exception(exc)
|
|
125
|
+
app_logger.warning(f"Failed to write rule code to {rule_file}: {exc}")
|
|
126
|
+
|
|
127
|
+
return rules
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def verify_rules(rule_code_dir, rule_type="accepted"):
|
|
131
|
+
"""
|
|
132
|
+
Verify the rules from export.json and activate them if they pass verification
|
|
133
|
+
|
|
134
|
+
Write the rule code to a temporary file and import it as a module
|
|
135
|
+
"""
|
|
136
|
+
rules = get_exported_rules(rule_code_dir)
|
|
137
|
+
|
|
138
|
+
for rule in rules:
|
|
139
|
+
if not rule["status"] == rule_type:
|
|
140
|
+
continue
|
|
141
|
+
module_name = rule["module_name"]
|
|
142
|
+
app_logger.info(f"\n{Fore.BLUE}Verifying rule: {module_name} - {rule['id']}{Style.RESET_ALL}")
|
|
143
|
+
try:
|
|
144
|
+
rule_module = import_module(module_name)
|
|
145
|
+
rule_module.init_rule()
|
|
146
|
+
LogicBank.activate(session=safrs.DB.session, activator=rule_module.init_rule)
|
|
147
|
+
if rule["status"] != "active":
|
|
148
|
+
set_rule_status(rule["id"], "active")
|
|
149
|
+
app_logger.info(f"\n{Fore.GREEN}Activated rule {rule['id']}{Style.RESET_ALL}")
|
|
150
|
+
|
|
151
|
+
except Exception as exc:
|
|
152
|
+
app_logger.exception(exc)
|
|
153
|
+
set_rule_error(rule["id"], f"{type(exc).__name__}: {exc}")
|
|
154
|
+
app_logger.warning(f"{Fore.RED}Failed to verify {rule_type} rule code\n{rule['code']}\n{Fore.YELLOW}{type(exc).__name__}: {exc}{Style.RESET_ALL}")
|
|
155
|
+
app_logger.debug(f"{rule}")
|
|
156
|
+
rule["status"] = "accepted"
|
|
157
|
+
rule["error"] = f"{type(exc).__name__}: {exc}"
|
|
158
|
+
|
|
159
|
+
return rules
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def load_active_rules(rule_code_dir, rules=None):
|
|
163
|
+
"""
|
|
164
|
+
Load the active rules from export.json
|
|
165
|
+
"""
|
|
166
|
+
if not rules:
|
|
167
|
+
rules = get_exported_rules(rule_code_dir)
|
|
168
|
+
for rule in rules:
|
|
169
|
+
module_name = rule.get("module_name", None)
|
|
170
|
+
if not rule["status"] == "active" or module_name is None:
|
|
171
|
+
continue
|
|
172
|
+
app_logger.info(f"{Fore.GREEN}Loading Rule Module {module_name} {rule['id']} {Style.RESET_ALL}")
|
|
173
|
+
rule_module = import_module(module_name)
|
|
174
|
+
try:
|
|
175
|
+
rule_module.init_rule()
|
|
176
|
+
except Exception as exc:
|
|
177
|
+
app_logger.exception(exc)
|
|
178
|
+
app_logger.warning(f"{Fore.RED}Failed to load active rule {rule['id']} {rule['code']} {Style.RESET_ALL}")
|
|
179
|
+
set_rule_error(rule["id"], f"{type(exc).__name__}: {exc}")
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def get_project_id():
|
|
183
|
+
if os.environ.get("PROJECT_ID"):
|
|
184
|
+
return os.environ.get("PROJECT_ID")
|
|
185
|
+
|
|
186
|
+
return Path(os.getcwd()).name
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def load_verify_rules():
|
|
190
|
+
|
|
191
|
+
# Add FileHandler to root_logger
|
|
192
|
+
log_file = Path(os.getenv("LOG_DIR",tempfile.mkdtemp())) / "load_verify_rules.log"
|
|
193
|
+
file_handler = logging.FileHandler(log_file)
|
|
194
|
+
root_logger = logging.getLogger()
|
|
195
|
+
root_logger.addHandler(file_handler)
|
|
196
|
+
|
|
197
|
+
rule_code_dir = Path("./logic/wg_rules") # in the project root
|
|
198
|
+
rule_code_dir.mkdir(parents=True, exist_ok=True)
|
|
199
|
+
sys.path.append(f"{rule_code_dir}")
|
|
200
|
+
|
|
201
|
+
app_logger.info(f"Loading rules from {rule_code_dir.resolve()}")
|
|
202
|
+
|
|
203
|
+
rules = []
|
|
204
|
+
|
|
205
|
+
if os.environ.get("VERIFY_RULES") == "True":
|
|
206
|
+
rules = verify_rules(rule_code_dir, rule_type="active")
|
|
207
|
+
verify_rules(rule_code_dir, rule_type="accepted")
|
|
208
|
+
else:
|
|
209
|
+
try:
|
|
210
|
+
load_active_rules(rule_code_dir, rules)
|
|
211
|
+
except Exception as exc:
|
|
212
|
+
app_logger.exception(exc)
|
|
213
|
+
app_logger.warning(f"{Fore.RED}Failed to load active exported rules: {exc}{Style.RESET_ALL}")
|
|
214
|
+
|
|
215
|
+
root_logger.removeHandler(file_handler)
|
|
216
|
+
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
app_logger = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
def discover_logic():
|
|
8
|
+
"""
|
|
9
|
+
Discover additional logic in this directory
|
|
10
|
+
"""
|
|
11
|
+
import os
|
|
12
|
+
logic = []
|
|
13
|
+
logic_path = Path(__file__).parent
|
|
14
|
+
for root, dirs, files in os.walk(logic_path):
|
|
15
|
+
for file in files:
|
|
16
|
+
if file.endswith(".py"):
|
|
17
|
+
spec = importlib.util.spec_from_file_location("module.name", logic_path.joinpath(file))
|
|
18
|
+
if file.endswith("auto_discovery.py"):
|
|
19
|
+
pass
|
|
20
|
+
else:
|
|
21
|
+
logic.append(file)
|
|
22
|
+
each_logic_file = importlib.util.module_from_spec(spec)
|
|
23
|
+
spec.loader.exec_module(each_logic_file) # runs "bare" module code (e.g., initialization)
|
|
24
|
+
each_logic_file.declare_logic() # invoke create function
|
|
25
|
+
|
|
26
|
+
# if False and Path(__file__).parent.parent.parent.joinpath("docs/project_is_genai_demo.txt").exists():
|
|
27
|
+
# return # for genai_demo, logic is in logic/declare_logic.py (so ignore logic_discovery)
|
|
28
|
+
|
|
29
|
+
wg_logic_path = Path(__file__).parent.parent.joinpath("wg_rules")
|
|
30
|
+
if wg_logic_path.exists():
|
|
31
|
+
run_local = os.environ.get("WG_PROJECT") is None # eg, running export locally
|
|
32
|
+
# run_local = False # for debug
|
|
33
|
+
if run_local:
|
|
34
|
+
wg_export_logic_path = Path(__file__).parent.parent.parent.joinpath("logic/wg_rules/active_rules_export.py")
|
|
35
|
+
if wg_export_logic_path.is_file():
|
|
36
|
+
spec = importlib.util.spec_from_file_location("module.name", wg_export_logic_path)
|
|
37
|
+
logic.append(str(wg_export_logic_path))
|
|
38
|
+
wg_export_logic_file = importlib.util.module_from_spec(spec)
|
|
39
|
+
spec.loader.exec_module(wg_export_logic_file) # runs "bare" module code (e.g., initialization)
|
|
40
|
+
wg_export_logic_file.declare_logic() # invoke create function
|
|
41
|
+
else:
|
|
42
|
+
for root, dirs, files in os.walk(wg_logic_path):
|
|
43
|
+
for file in files:
|
|
44
|
+
if file.endswith(".py") and 'active_rules_export.py' != file:
|
|
45
|
+
spec = importlib.util.spec_from_file_location("module.name", wg_logic_path.joinpath(file))
|
|
46
|
+
logic.append(file)
|
|
47
|
+
each_logic_file = importlib.util.module_from_spec(spec)
|
|
48
|
+
spec.loader.exec_module(each_logic_file) # runs "bare" module code (e.g., initialization)
|
|
49
|
+
each_logic_file.init_rule() # invoke create function
|
|
50
|
+
|
|
51
|
+
app_logger.info(f"..discovered logic: {logic}")
|
|
52
|
+
return
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
This describes how to use Logic; for more information, [see here](https://apilogicserver.github.io/Docs/Logic-Why).
|
|
2
|
+
|
|
3
|
+
> Key Takeway - Logic: Multi-table Derivation and Constraint Rules, Extensible with Python
|
|
4
|
+
<br>Rules are:
|
|
5
|
+
<br>1. **Declared** in your IDE - 40X more concise
|
|
6
|
+
<br>2. **Activated** on server start
|
|
7
|
+
<br>3. **Executed** - *automatically* - on updates (using SQLAlchemy events)
|
|
8
|
+
<br>4. **Debugged** in your IDE, and with the console log
|
|
9
|
+
<br>For more on rules, [click here](https://apilogicserver.github.io/Docs/Logic-Why/).
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
## Examples
|
|
14
|
+
Examples from tutorial project:
|
|
15
|
+
* Examples drawn from [tutorial project](https://github.com/ApiLogicServer/demo/blob/main/logic/declare_logic.py)
|
|
16
|
+
* Use Shift + "." to view in project mode
|
|
17
|
+
|
|
18
|
+
You can [find the rules here](https://apilogicserver.github.io/Docs/Logic). Below, we explore the syntax of 3 typical rules.
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
### 1. Multi-Table Derivations
|
|
23
|
+
|
|
24
|
+
This declares the Customer.Balance as the sum of the unshipped Order.AmountTotal:
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
Rule.sum(derive=models.Customer.Balance,
|
|
28
|
+
as_sum_of=models.Order.AmountTotal,
|
|
29
|
+
where=lambda row: row.ShippedDate is None)
|
|
30
|
+
```
|
|
31
|
+
It means the rule engine **watches** for these changes:
|
|
32
|
+
* Order inserted/deleted, or
|
|
33
|
+
* AmountTotal or ShippedDate or CustomerID changes
|
|
34
|
+
|
|
35
|
+
Iff changes are detected, the engine **reacts** by *adjusting* the Customer.Balance. SQLs are [optimized](#declarative-logic-important-notes).
|
|
36
|
+
|
|
37
|
+
This would **chain** to check the Customers' Constraint rule, described below.
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
### 2. Constraints: lambda or function
|
|
42
|
+
|
|
43
|
+
Constraints are multi-field conditions which must be true for transactions to succeed (else an exception is raised). You can express the condition as a lambda or a function:
|
|
44
|
+
|
|
45
|
+
**As a lambda:**
|
|
46
|
+
```python
|
|
47
|
+
Rule.constraint(validate=models.Customer,
|
|
48
|
+
as_condition=lambda row: row.Balance <= row.CreditLimit, # parent references are supported
|
|
49
|
+
error_msg="balance ({row.Balance}) exceeds credit ({row.CreditLimit})")
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**Or, as a function:**
|
|
53
|
+
```python
|
|
54
|
+
def check_balance(row: models.Customer, old_row: models.Customer, logic_row: LogicRow):
|
|
55
|
+
if logic_row.ins_upd_dlt != "dlt": # see also: logic_row.old_row
|
|
56
|
+
return row.Balance <= row.CreditLimit
|
|
57
|
+
else:
|
|
58
|
+
return True
|
|
59
|
+
|
|
60
|
+
Rule.constraint(validate=models.Customer,
|
|
61
|
+
calling=check_balance,
|
|
62
|
+
error_msg=f"balance ({row.Balance}) exceeds credit ({row.CreditLimit})")
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
### 3. Row Events: Extensible with Python
|
|
68
|
+
|
|
69
|
+
Events are procedural Python code, providing extensibility for declarative rules:
|
|
70
|
+
```python
|
|
71
|
+
def congratulate_sales_rep(row: models.Order, old_row: models.Order, logic_row: LogicRow):
|
|
72
|
+
pass # event code here - sending email, messages, etc.
|
|
73
|
+
|
|
74
|
+
Rule.commit_row_event(on_class=models.Order, calling=congratulate_sales_rep)
|
|
75
|
+
```
|
|
76
|
+
Note there are multiple kinds of events, so you can control whether they run before or after rule execution. For more information, [see here](https://apilogicserver.github.io/Docs/Logic-Type-Constraint).
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
## LogicRow: old_row, verb, etc
|
|
81
|
+
|
|
82
|
+
A key argument to functions is `logic_row`:
|
|
83
|
+
|
|
84
|
+
* **Wraps row and old_row,** plus methods for insert, update and delete - rule enforcement
|
|
85
|
+
|
|
86
|
+
* **Additional instance variables:** ins_upd_dlt, nest_level, session, etc.
|
|
87
|
+
|
|
88
|
+
* **Helper Methods:** are_attributes_changed, set_same_named_attributes, get_parent_logic_row(role_name), get_derived_attributes, log, is_inserted, etc
|
|
89
|
+
|
|
90
|
+
Here is an example:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
"""
|
|
94
|
+
STATE TRANSITION LOGIC, using old_row
|
|
95
|
+
"""
|
|
96
|
+
def raise_over_20_percent(row: models.Employee, old_row: models.Employee, logic_row: LogicRow):
|
|
97
|
+
if logic_row.ins_upd_dlt == "upd" and row.Salary > old_row.Salary:
|
|
98
|
+
return row.Salary >= Decimal('1.20') * old_row.Salary
|
|
99
|
+
else:
|
|
100
|
+
return True
|
|
101
|
+
|
|
102
|
+
Rule.constraint(validate=models.Employee,
|
|
103
|
+
calling=raise_over_20_percent,
|
|
104
|
+
error_msg="{row.LastName} needs a more meaningful raise")
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Note the `log` method, which enables you to write row/old_row into the log with a short message:
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
logic_row.log("no manager for this order's salesrep")
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
## Declarative Logic: Important Notes
|
|
116
|
+
|
|
117
|
+
Logic *declarative*, which differs from conventional *procedural* logic:
|
|
118
|
+
|
|
119
|
+
1. **Automatic Invocation:** you don't call the rules; they execute in response to updates (via SQLAlchemy events).
|
|
120
|
+
|
|
121
|
+
2. **Automatic Ordering:** you don't order the rules; execution order is based on system-discovered depencencies.
|
|
122
|
+
|
|
123
|
+
3. **Automatic Optimizations:** logic is optimized to reduce SQLs.
|
|
124
|
+
|
|
125
|
+
* Rule execution is *pruned* if dependent attributes are not altered
|
|
126
|
+
* SQL is optimized, e.g., `sum` rules operate by *adjustment*, not expensive SQL `select sum`
|
|
127
|
+
|
|
128
|
+
These simplify maintenance / iteration: you can be sure new logic is always called, in the correct order.
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
## Debugging
|
|
133
|
+
|
|
134
|
+
Debug rules using **system-generated logic log** and your **IDE debugger**; for more information, [see here](https://apilogicserver.github.io/Docs/Logic-Use).
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
### Using the debugger
|
|
139
|
+
|
|
140
|
+
Use the debugger as shown below. Note you can stop in lambda functions.
|
|
141
|
+
|
|
142
|
+

|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
### Logic Log
|
|
147
|
+
|
|
148
|
+
Logging is performed using standard Python logging, with a logger named `logic_logger`. Use `info` for tracing, and `debug` for additional information (e.g., all declared rules are logged).
|
|
149
|
+
|
|
150
|
+
In addition, the system logs all rules that fire, to aid in debugging. Referring the the screen shot above:
|
|
151
|
+
|
|
152
|
+
* Each line represents a rule execution, showing row state (old/new values), and the _{reason}_ that caused the update (e.g., client, sum adjustment)
|
|
153
|
+
* Log indention shows multi-table chaining
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
## How Logic works
|
|
158
|
+
|
|
159
|
+
*Activation* occurs in `api_logic_server_run.py`:
|
|
160
|
+
```python
|
|
161
|
+
LogicBank.activate(session=session, activator=declare_logic, constraint_event=constraint_handler)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
This installs the rule engine as a SQLAlchemy event listener (`before_flush`). So, Logic *runs* automatically, in response to transaction commits (typically via the API).
|
|
165
|
+
|
|
166
|
+
Rules plug into SQLAlchemy events, and execute as follows:
|
|
167
|
+
|
|
168
|
+
| Logic Phase | Why It Matters |
|
|
169
|
+
|:-----------------------------|:---------------------|
|
|
170
|
+
| **Watch** for changes at the attribute level | Performance - Automatic Attribute-level Pruning |
|
|
171
|
+
| **React** by recomputing value | Ensures Reuse - Invocation is automatic<br>Derivations are optimized (e.g. *adjustment updates* - not aggregate queries) |
|
|
172
|
+
| **Chain** to other referencing data | Simplifies Maintenance - ordering is automatic<br>Multi-table logic automation |
|
api_logic_server_cli/prototypes/genai_demo/security/__pycache__/declare_security.cpython-312.pyc
ADDED
|
Binary file
|
|
@@ -1,109 +1,142 @@
|
|
|
1
1
|
about:
|
|
2
|
-
date:
|
|
3
|
-
merged:
|
|
4
|
-
at: January 28, 2024 13:17:27
|
|
5
|
-
new_attributes: 'Product.CarbonNeutral '
|
|
6
|
-
new_resources: ''
|
|
7
|
-
new_tab_groups: ''
|
|
2
|
+
date: February 02, 2025 11:44:16
|
|
8
3
|
recent_changes: works with modified safrs-react-admin
|
|
9
4
|
version: 0.0.0
|
|
10
5
|
api_root: '{http_type}://{swagger_host}:{port}/{api}'
|
|
11
|
-
authentication:
|
|
12
|
-
endpoint: '{http_type}://{swagger_host}:{port}/api/auth/login'
|
|
6
|
+
authentication: '{system-default}'
|
|
13
7
|
info:
|
|
14
8
|
number_relationships: 3
|
|
15
9
|
number_tables: 4
|
|
10
|
+
info_toggle_checked: true
|
|
16
11
|
resources:
|
|
17
12
|
Customer:
|
|
18
13
|
attributes:
|
|
19
|
-
- label: '
|
|
20
|
-
name:
|
|
21
|
-
required: true
|
|
14
|
+
- label: ' name*'
|
|
15
|
+
name: name
|
|
22
16
|
search: true
|
|
23
17
|
sort: true
|
|
24
|
-
- name:
|
|
25
|
-
|
|
26
|
-
- name:
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
18
|
+
- name: balance
|
|
19
|
+
type: DECIMAL
|
|
20
|
+
- name: credit_limit
|
|
21
|
+
type: DECIMAL
|
|
22
|
+
- name: id
|
|
23
|
+
description: Represents a customer in the system with unique name, balance, and
|
|
24
|
+
credit limit attributes.
|
|
25
|
+
info_list: Represents a customer in the system with unique name, balance, and
|
|
26
|
+
credit limit attributes.
|
|
30
27
|
tab_groups:
|
|
31
28
|
- direction: tomany
|
|
32
29
|
fks:
|
|
33
|
-
-
|
|
30
|
+
- customer_id
|
|
34
31
|
name: OrderList
|
|
35
32
|
resource: Order
|
|
36
33
|
type: Customer
|
|
37
|
-
user_key:
|
|
34
|
+
user_key: name
|
|
38
35
|
Item:
|
|
39
36
|
attributes:
|
|
40
|
-
- label: '
|
|
41
|
-
name:
|
|
37
|
+
- label: ' id*'
|
|
38
|
+
name: id
|
|
42
39
|
search: true
|
|
43
40
|
sort: true
|
|
44
|
-
- name:
|
|
45
|
-
- name:
|
|
46
|
-
- name:
|
|
41
|
+
- name: order_id
|
|
42
|
+
- name: product_id
|
|
43
|
+
- name: quantity
|
|
47
44
|
required: true
|
|
48
|
-
- name:
|
|
49
|
-
|
|
45
|
+
- name: unit_price
|
|
46
|
+
type: DECIMAL
|
|
47
|
+
- name: amount
|
|
48
|
+
type: DECIMAL
|
|
49
|
+
description: Represents an item in an order, including quantity and pricing details.
|
|
50
|
+
info_list: Represents an item in an order, including quantity and pricing details.
|
|
50
51
|
tab_groups:
|
|
51
52
|
- direction: toone
|
|
52
53
|
fks:
|
|
53
|
-
-
|
|
54
|
-
name:
|
|
54
|
+
- order_id
|
|
55
|
+
name: order
|
|
55
56
|
resource: Order
|
|
56
57
|
- direction: toone
|
|
57
58
|
fks:
|
|
58
|
-
-
|
|
59
|
-
name:
|
|
59
|
+
- product_id
|
|
60
|
+
name: product
|
|
60
61
|
resource: Product
|
|
61
62
|
type: Item
|
|
62
|
-
user_key:
|
|
63
|
+
user_key: id
|
|
63
64
|
Order:
|
|
64
65
|
attributes:
|
|
65
|
-
- label: '
|
|
66
|
-
name:
|
|
66
|
+
- label: ' id*'
|
|
67
|
+
name: id
|
|
67
68
|
search: true
|
|
68
69
|
sort: true
|
|
69
|
-
- name:
|
|
70
|
-
- name:
|
|
71
|
-
|
|
72
|
-
- name:
|
|
73
|
-
- name:
|
|
70
|
+
- name: customer_id
|
|
71
|
+
- name: amount_total
|
|
72
|
+
type: DECIMAL
|
|
73
|
+
- name: notes
|
|
74
|
+
- name: date_shipped
|
|
75
|
+
type: DATE
|
|
76
|
+
description: Represents an order made by a customer, including a notes field.
|
|
77
|
+
info_list: Represents an order made by a customer, including a notes field.
|
|
74
78
|
tab_groups:
|
|
75
79
|
- direction: tomany
|
|
76
80
|
fks:
|
|
77
|
-
-
|
|
81
|
+
- order_id
|
|
78
82
|
name: ItemList
|
|
79
83
|
resource: Item
|
|
80
84
|
- direction: toone
|
|
81
85
|
fks:
|
|
82
|
-
-
|
|
83
|
-
name:
|
|
86
|
+
- customer_id
|
|
87
|
+
name: customer
|
|
84
88
|
resource: Customer
|
|
85
89
|
type: Order
|
|
86
|
-
user_key:
|
|
90
|
+
user_key: id
|
|
87
91
|
Product:
|
|
88
92
|
attributes:
|
|
89
|
-
- label: '
|
|
90
|
-
name:
|
|
91
|
-
required: true
|
|
93
|
+
- label: ' name*'
|
|
94
|
+
name: name
|
|
92
95
|
search: true
|
|
93
96
|
sort: true
|
|
94
|
-
- name:
|
|
95
|
-
|
|
96
|
-
- name:
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
- name: unit_price
|
|
98
|
+
type: DECIMAL
|
|
99
|
+
- name: carbon_neutral
|
|
100
|
+
type: BOOLEAN
|
|
101
|
+
- name: id
|
|
102
|
+
description: Represents a product available in the system with a unit price.
|
|
103
|
+
info_list: Represents a product available in the system with a unit price.
|
|
99
104
|
tab_groups:
|
|
100
105
|
- direction: tomany
|
|
101
106
|
fks:
|
|
102
|
-
-
|
|
107
|
+
- product_id
|
|
103
108
|
name: ItemList
|
|
104
109
|
resource: Item
|
|
105
110
|
type: Product
|
|
106
|
-
user_key:
|
|
111
|
+
user_key: name
|
|
107
112
|
settings:
|
|
108
|
-
HomeJS:
|
|
113
|
+
HomeJS: /admin-app/home.js
|
|
109
114
|
max_list_columns: 8
|
|
115
|
+
style_guide:
|
|
116
|
+
applicationLocales:
|
|
117
|
+
- en
|
|
118
|
+
- es
|
|
119
|
+
currency_symbol: $
|
|
120
|
+
currency_symbol_position: left
|
|
121
|
+
date_format: LL
|
|
122
|
+
decimal_max: '1000000000'
|
|
123
|
+
decimal_min: '0'
|
|
124
|
+
decimal_separator: .
|
|
125
|
+
detail_mode: tab
|
|
126
|
+
edit_on_mode: dblclick
|
|
127
|
+
exclude_listpicker: false
|
|
128
|
+
include_translation: 'false'
|
|
129
|
+
keycloak_client_id: alsclient
|
|
130
|
+
keycloak_realm: kcals
|
|
131
|
+
keycloak_url: http://localhost:8080
|
|
132
|
+
locale: en
|
|
133
|
+
max_decimal_digits: '4'
|
|
134
|
+
min_decimal_digits: '2'
|
|
135
|
+
new_mode: dialog
|
|
136
|
+
pick_style: list
|
|
137
|
+
row_height: small,
|
|
138
|
+
serviceType: OntimizeEE
|
|
139
|
+
startSessionPath: /auth/login
|
|
140
|
+
style: light
|
|
141
|
+
thousand_separator: ','
|
|
142
|
+
use_keycloak: 'false'
|