ApiLogicServer 14.3.0__py3-none-any.whl → 14.3.11__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.3.0.dist-info → ApiLogicServer-14.3.11.dist-info}/METADATA +3 -3
- {ApiLogicServer-14.3.0.dist-info → ApiLogicServer-14.3.11.dist-info}/RECORD +58 -54
- api_logic_server_cli/api_logic_server.py +2 -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__/ont_build.cpython-312.pyc +0 -0
- api_logic_server_cli/create_from_model/ont_build.py +9 -9
- api_logic_server_cli/create_from_model/safrs-react-admin-npm-build/asset-manifest.json +3 -3
- api_logic_server_cli/create_from_model/safrs-react-admin-npm-build/build-0213.txt +1 -0
- api_logic_server_cli/create_from_model/safrs-react-admin-npm-build/index.html +1 -1
- api_logic_server_cli/create_from_model/safrs-react-admin-npm-build/static/js/main.7c8c0e37.js +3 -0
- api_logic_server_cli/create_from_model/safrs-react-admin-npm-build/static/js/{main.bfe80d1d.js.map → main.7c8c0e37.js.map} +1 -1
- api_logic_server_cli/database/nw-gold.sqlite +0 -0
- api_logic_server_cli/genai/genai.py +13 -3
- api_logic_server_cli/genai/genai_svcs.py +2 -0
- api_logic_server_cli/manager.py +20 -16
- api_logic_server_cli/prototypes/base/api/system/expression_parser.py +10 -4
- api_logic_server_cli/prototypes/base/devops/docker-image/env.list +7 -2
- api_logic_server_cli/prototypes/base/integration/kafka/kafka_producer.py +31 -8
- api_logic_server_cli/prototypes/base/integration/system/RowDictMapper.py +33 -16
- api_logic_server_cli/prototypes/base/logic/declare_logic.py +1 -0
- api_logic_server_cli/prototypes/base/logic/load_verify_rules.py +2 -1
- 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 +57 -69
- 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/ApiLogicServer.code-workspace +2 -2
- api_logic_server_cli/prototypes/manager/.vscode/launch.json +21 -21
- api_logic_server_cli/prototypes/manager/README.md +1 -1
- 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 +15 -8
- 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/time_tracking_billing/002_create_db_models.prompt +3 -132
- api_logic_server_cli/prototypes/manager/system/genai/examples/time_tracking_billing/Invoice Made Ready.png +0 -0
- api_logic_server_cli/prototypes/manager/system/genai/examples/time_tracking_billing/readme.md +59 -6
- api_logic_server_cli/prototypes/manager/system/genai/learning_requests/logic_bank_api.prompt +22 -1
- api_logic_server_cli/prototypes/nw/logic/declare_logic.py +1 -1
- 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 +2 -1
- api_logic_server_cli/create_from_model/safrs-react-admin-npm-build/build-0106.txt +0 -1
- api_logic_server_cli/create_from_model/safrs-react-admin-npm-build/static/js/main.bfe80d1d.js +0 -3
- 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.sqlite +0 -0
- 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.3.0.dist-info → ApiLogicServer-14.3.11.dist-info}/LICENSE +0 -0
- {ApiLogicServer-14.3.0.dist-info → ApiLogicServer-14.3.11.dist-info}/WHEEL +0 -0
- {ApiLogicServer-14.3.0.dist-info → ApiLogicServer-14.3.11.dist-info}/entry_points.txt +0 -0
- {ApiLogicServer-14.3.0.dist-info → ApiLogicServer-14.3.11.dist-info}/top_level.txt +0 -0
- /api_logic_server_cli/create_from_model/safrs-react-admin-npm-build/static/js/{main.bfe80d1d.js.LICENSE.txt → main.7c8c0e37.js.LICENSE.txt} +0 -0
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
import datetime
|
|
1
|
+
import datetime, os
|
|
2
2
|
from decimal import Decimal
|
|
3
3
|
from logic_bank.exec_row_logic.logic_row import LogicRow
|
|
4
4
|
from logic_bank.extensions.rule_extensions import RuleExtension
|
|
5
5
|
from logic_bank.logic_bank import Rule
|
|
6
|
-
from
|
|
7
|
-
|
|
6
|
+
from logic_bank.logic_bank import DeclareRule
|
|
7
|
+
import database.models as models
|
|
8
8
|
import api.system.opt_locking.opt_locking as opt_locking
|
|
9
|
-
from security.system.authorization import Grant
|
|
10
|
-
import
|
|
11
|
-
from integration.row_dict_maps.OrderShipping import OrderShipping
|
|
12
|
-
from confluent_kafka import Producer, KafkaException
|
|
9
|
+
from security.system.authorization import Grant, Security
|
|
10
|
+
from logic.load_verify_rules import load_verify_rules
|
|
13
11
|
import integration.kafka.kafka_producer as kafka_producer
|
|
12
|
+
import logging
|
|
14
13
|
|
|
15
14
|
app_logger = logging.getLogger(__name__)
|
|
16
15
|
|
|
@@ -20,104 +19,93 @@ def declare_logic():
|
|
|
20
19
|
''' Declarative multi-table derivations and constraints, extensible with Python.
|
|
21
20
|
|
|
22
21
|
Brief background: see readme_declare_logic.md
|
|
23
|
-
|
|
24
|
-
GenAI: the following prompt was sent to GenAI-Logic, which translated it into the code below:
|
|
25
|
-
|
|
26
|
-
Use case: Check Credit
|
|
27
22
|
|
|
28
|
-
|
|
29
|
-
2. The Customer's balance is the sum of the Order amount_total where date_shipped is null
|
|
30
|
-
3. The Order's amount_total is the sum of the Item amount
|
|
31
|
-
4. The Item amount is the quantity * unit_price
|
|
32
|
-
5. The Item unit_price is copied from the Product unit_price
|
|
23
|
+
Your Code Goes Here - Use code completion (Rule.) to declare rules
|
|
33
24
|
'''
|
|
34
25
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
26
|
+
if os.environ.get("WG_PROJECT"):
|
|
27
|
+
# Inside WG: Load rules from docs/expprt/export.json
|
|
28
|
+
load_verify_rules()
|
|
29
|
+
else:
|
|
30
|
+
# Outside WG: load declare_logic function
|
|
31
|
+
from logic.logic_discovery.auto_discovery import discover_logic
|
|
32
|
+
discover_logic()
|
|
39
33
|
|
|
40
34
|
# Logic from GenAI: (or, use your IDE w/ code completion)
|
|
35
|
+
from database.models import Product, Customer, Item, Order
|
|
41
36
|
|
|
42
|
-
#
|
|
37
|
+
# Ensure that Customer balance does not exceed credit_limit.
|
|
43
38
|
Rule.constraint(validate=Customer,
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
# Computes the customer's balance as the sum of unshipped orders.
|
|
48
|
-
Rule.sum(derive=Customer.Balance, as_sum_of=Order.AmountTotal, where=lambda row: row.ShipDate is None)
|
|
49
|
-
|
|
50
|
-
# Computes the total amount of an order as the sum of item amounts.
|
|
51
|
-
Rule.sum(derive=Order.AmountTotal, as_sum_of=Item.Amount)
|
|
52
|
-
|
|
53
|
-
# Calculates the item amount as the quantity times unit price. (original rule, prior to iteration)
|
|
54
|
-
# Rule.formula(derive=Item.amount, as_expression=lambda row: row.quantity * row.unit_price)
|
|
55
|
-
|
|
56
|
-
# Copies the unit price from the parent product to the item.
|
|
57
|
-
Rule.copy(derive=Item.UnitPrice, from_parent=Product.UnitPrice)
|
|
58
|
-
|
|
59
|
-
# End Logic from GenAI
|
|
39
|
+
as_condition=lambda row: row.balance <= row.credit_limit,
|
|
40
|
+
error_msg="Customer balance ({row.balance}) exceeds credit limit ({row.credit_limit})")
|
|
60
41
|
|
|
42
|
+
# Customer.balance is the sum of Order.amount_total for orders not yet shipped.
|
|
43
|
+
Rule.sum(derive=Customer.balance, as_sum_of=Order.amount_total, where=lambda row: row.date_shipped is None)
|
|
61
44
|
|
|
62
|
-
#
|
|
45
|
+
# Order.amount_total is the sum of Item.amount.
|
|
46
|
+
Rule.sum(derive=Order.amount_total, as_sum_of=Item.amount)
|
|
63
47
|
|
|
64
|
-
|
|
65
|
-
def derive_amount(row: Item, old_row: Item, logic_row: LogicRow):
|
|
48
|
+
def derive_amount(row: models.Item, old_row: models.Item, logic_row: LogicRow):
|
|
66
49
|
amount = row.Quantity * row.UnitPrice
|
|
67
50
|
if row.Product.CarbonNeutral == True and row.Quantity >= 10:
|
|
68
51
|
amount = amount * Decimal(0.9) # breakpoint here
|
|
69
52
|
return amount
|
|
70
|
-
# now register function; note logic ordering is automatic
|
|
71
|
-
Rule.formula(derive=Item.Amount, calling=derive_amount)
|
|
72
53
|
|
|
73
|
-
|
|
74
|
-
|
|
54
|
+
# Items.Amount = Quantity * UnitPrice with discount for CarbonNeutral products.
|
|
55
|
+
Rule.formula(derive=models.Item.Amount,
|
|
56
|
+
calling=derive_amount)
|
|
75
57
|
|
|
76
|
-
|
|
58
|
+
# Item.unit_price is copied from Product.unit_price.
|
|
59
|
+
Rule.copy(derive=Item.unit_price, from_parent=Product.unit_price)
|
|
77
60
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
old_row (Order): n/a
|
|
83
|
-
logic_row (LogicRow): bundles curr/old row, with ins/upd/dlt (etc) state
|
|
84
|
-
"""
|
|
85
|
-
if logic_row.is_inserted():
|
|
86
|
-
kafka_producer.send_kafka_message(logic_row=logic_row,
|
|
87
|
-
row_dict_mapper=OrderShipping,
|
|
88
|
-
kafka_topic="order_shipping",
|
|
89
|
-
kafka_key=str(row.OrderID),
|
|
90
|
-
msg="Sending Order to Shipping")
|
|
91
|
-
|
|
92
|
-
Rule.after_flush_row_event(on_class=Order, calling=send_order_to_shipping) # see above
|
|
61
|
+
# Send the Order to Kafka topic 'order_shipping' if the date_shipped is not None.
|
|
62
|
+
Rule.after_flush_row_event(on_class=Order, calling=kafka_producer.send_row_to_kafka,
|
|
63
|
+
if_condition=lambda row: row.date_shipped is not None,
|
|
64
|
+
with_args={'topic': 'order_shipping'})
|
|
93
65
|
|
|
66
|
+
# End Logic from GenAI
|
|
94
67
|
|
|
95
68
|
|
|
96
|
-
def handle_all(logic_row: LogicRow): #
|
|
69
|
+
def handle_all(logic_row: LogicRow): # #als: TIME / DATE STAMPING, OPTIMISTIC LOCKING
|
|
97
70
|
"""
|
|
98
71
|
This is generic - executed for all classes.
|
|
99
72
|
|
|
100
|
-
Invokes optimistic locking.
|
|
73
|
+
Invokes optimistic locking, and checks Grant permissions.
|
|
101
74
|
|
|
102
|
-
|
|
75
|
+
Also provides user/date stamping.
|
|
103
76
|
|
|
104
77
|
Args:
|
|
105
78
|
logic_row (LogicRow): from LogicBank - old/new row, state
|
|
106
79
|
"""
|
|
107
80
|
|
|
108
|
-
if
|
|
81
|
+
if os.getenv("APILOGICPROJECT_NO_FLASK") is not None:
|
|
82
|
+
print("\ndeclare_logic.py Using TestBase\n")
|
|
109
83
|
return # enables rules to be used outside of Flask, e.g., test data loading
|
|
110
84
|
|
|
111
85
|
if logic_row.is_updated() and logic_row.old_row is not None and logic_row.nest_level == 0:
|
|
112
86
|
opt_locking.opt_lock_patch(logic_row=logic_row)
|
|
113
|
-
|
|
114
|
-
|
|
87
|
+
|
|
88
|
+
Grant.process_updates(logic_row=logic_row)
|
|
89
|
+
|
|
90
|
+
did_stamping = False
|
|
91
|
+
if enable_stamping := False: # #als: DATE / USER STAMPING
|
|
115
92
|
row = logic_row.row
|
|
116
93
|
if logic_row.ins_upd_dlt == "ins" and hasattr(row, "CreatedOn"):
|
|
117
94
|
row.CreatedOn = datetime.datetime.now()
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
95
|
+
did_stamping = True
|
|
96
|
+
if logic_row.ins_upd_dlt == "ins" and hasattr(row, "CreatedBy"):
|
|
97
|
+
row.CreatedBy = Security.current_user().id
|
|
98
|
+
# if Config.SECURITY_ENABLED == True else 'public'
|
|
99
|
+
did_stamping = True
|
|
100
|
+
if logic_row.ins_upd_dlt == "upd" and hasattr(row, "UpdatedOn"):
|
|
101
|
+
row.UpdatedOn = datetime.datetime.now()
|
|
102
|
+
did_stamping = True
|
|
103
|
+
if logic_row.ins_upd_dlt == "upd" and hasattr(row, "UpdatedBy"):
|
|
104
|
+
row.UpdatedBy = Security.current_user().id \
|
|
105
|
+
if Config.SECURITY_ENABLED == True else 'public'
|
|
106
|
+
did_stamping = True
|
|
107
|
+
if did_stamping:
|
|
108
|
+
logic_row.log("early_row_event_all_classes - handle_all did stamping")
|
|
121
109
|
Rule.early_row_event_all_classes(early_row_event_all_classes=handle_all)
|
|
122
110
|
|
|
123
111
|
#als rules report
|
|
@@ -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 |
|