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.
Files changed (69) hide show
  1. {ApiLogicServer-14.3.0.dist-info → ApiLogicServer-14.3.11.dist-info}/METADATA +3 -3
  2. {ApiLogicServer-14.3.0.dist-info → ApiLogicServer-14.3.11.dist-info}/RECORD +58 -54
  3. api_logic_server_cli/api_logic_server.py +2 -1
  4. api_logic_server_cli/api_logic_server_info.yaml +3 -3
  5. api_logic_server_cli/cli.py +5 -2
  6. api_logic_server_cli/create_from_model/__pycache__/ont_build.cpython-312.pyc +0 -0
  7. api_logic_server_cli/create_from_model/ont_build.py +9 -9
  8. api_logic_server_cli/create_from_model/safrs-react-admin-npm-build/asset-manifest.json +3 -3
  9. api_logic_server_cli/create_from_model/safrs-react-admin-npm-build/build-0213.txt +1 -0
  10. api_logic_server_cli/create_from_model/safrs-react-admin-npm-build/index.html +1 -1
  11. api_logic_server_cli/create_from_model/safrs-react-admin-npm-build/static/js/main.7c8c0e37.js +3 -0
  12. api_logic_server_cli/create_from_model/safrs-react-admin-npm-build/static/js/{main.bfe80d1d.js.map → main.7c8c0e37.js.map} +1 -1
  13. api_logic_server_cli/database/nw-gold.sqlite +0 -0
  14. api_logic_server_cli/genai/genai.py +13 -3
  15. api_logic_server_cli/genai/genai_svcs.py +2 -0
  16. api_logic_server_cli/manager.py +20 -16
  17. api_logic_server_cli/prototypes/base/api/system/expression_parser.py +10 -4
  18. api_logic_server_cli/prototypes/base/devops/docker-image/env.list +7 -2
  19. api_logic_server_cli/prototypes/base/integration/kafka/kafka_producer.py +31 -8
  20. api_logic_server_cli/prototypes/base/integration/system/RowDictMapper.py +33 -16
  21. api_logic_server_cli/prototypes/base/logic/declare_logic.py +1 -0
  22. api_logic_server_cli/prototypes/base/logic/load_verify_rules.py +2 -1
  23. api_logic_server_cli/prototypes/genai_demo/api/customize_api.py +9 -11
  24. api_logic_server_cli/prototypes/genai_demo/database/.DS_Store +0 -0
  25. api_logic_server_cli/prototypes/genai_demo/database/db.sqlite +0 -0
  26. api_logic_server_cli/prototypes/genai_demo/database/models.py +52 -42
  27. api_logic_server_cli/prototypes/genai_demo/integration/row_dict_maps/OrderB2B.py +4 -6
  28. api_logic_server_cli/prototypes/genai_demo/integration/row_dict_maps/__pycache__/OrderB2B.cpython-312.pyc +0 -0
  29. api_logic_server_cli/prototypes/genai_demo/integration/row_dict_maps/row_dict_maps_readme.md +3 -0
  30. api_logic_server_cli/prototypes/genai_demo/logic/__pycache__/declare_logic.cpython-312.pyc +0 -0
  31. api_logic_server_cli/prototypes/genai_demo/logic/__pycache__/load_verify_rules.cpython-312.pyc +0 -0
  32. api_logic_server_cli/prototypes/genai_demo/logic/declare_logic.py +57 -69
  33. api_logic_server_cli/prototypes/genai_demo/logic/load_verify_rules.py +216 -0
  34. api_logic_server_cli/prototypes/genai_demo/logic/logic_discovery/__pycache__/__init__.cpython-312.pyc +0 -0
  35. api_logic_server_cli/prototypes/genai_demo/logic/logic_discovery/__pycache__/auto_discovery.cpython-312.pyc +0 -0
  36. api_logic_server_cli/prototypes/genai_demo/logic/logic_discovery/__pycache__/error_testing.cpython-312.pyc +0 -0
  37. api_logic_server_cli/prototypes/genai_demo/logic/logic_discovery/auto_discovery.py +52 -0
  38. api_logic_server_cli/prototypes/genai_demo/logic/readme_declare_logic.md +172 -0
  39. api_logic_server_cli/prototypes/genai_demo/security/__pycache__/declare_security.cpython-312.pyc +0 -0
  40. api_logic_server_cli/prototypes/genai_demo/ui/admin/admin.yaml +86 -53
  41. api_logic_server_cli/prototypes/manager/.vscode/ApiLogicServer.code-workspace +2 -2
  42. api_logic_server_cli/prototypes/manager/.vscode/launch.json +21 -21
  43. api_logic_server_cli/prototypes/manager/README.md +1 -1
  44. api_logic_server_cli/prototypes/manager/system/genai/examples/genai_demo/genai_demo.prompt +4 -1
  45. api_logic_server_cli/prototypes/manager/system/genai/examples/genai_demo/genai_demo.response_example +15 -8
  46. api_logic_server_cli/prototypes/manager/system/genai/examples/genai_demo/genai_demo_informal.prompt +3 -0
  47. api_logic_server_cli/prototypes/manager/system/genai/examples/time_tracking_billing/002_create_db_models.prompt +3 -132
  48. api_logic_server_cli/prototypes/manager/system/genai/examples/time_tracking_billing/Invoice Made Ready.png +0 -0
  49. api_logic_server_cli/prototypes/manager/system/genai/examples/time_tracking_billing/readme.md +59 -6
  50. api_logic_server_cli/prototypes/manager/system/genai/learning_requests/logic_bank_api.prompt +22 -1
  51. api_logic_server_cli/prototypes/nw/logic/declare_logic.py +1 -1
  52. api_logic_server_cli/sqlacodegen_wrapper/sqlacodegen/sqlacodegen/__pycache__/codegen.cpython-312.pyc +0 -0
  53. api_logic_server_cli/sqlacodegen_wrapper/sqlacodegen/sqlacodegen/codegen.py +2 -1
  54. api_logic_server_cli/create_from_model/safrs-react-admin-npm-build/build-0106.txt +0 -1
  55. api_logic_server_cli/create_from_model/safrs-react-admin-npm-build/static/js/main.bfe80d1d.js +0 -3
  56. api_logic_server_cli/prototypes/genai_demo/database/chatgpt/__pycache__/copilot_models.cpython-312.pyc +0 -0
  57. api_logic_server_cli/prototypes/genai_demo/database/chatgpt/__pycache__/sample_ai_models.cpython-312.pyc +0 -0
  58. api_logic_server_cli/prototypes/genai_demo/database/chatgpt/sample_ai.chatgpt +0 -16
  59. api_logic_server_cli/prototypes/genai_demo/database/chatgpt/sample_ai.sql +0 -66
  60. api_logic_server_cli/prototypes/genai_demo/database/chatgpt/sample_ai.sqlite +0 -0
  61. api_logic_server_cli/prototypes/genai_demo/database/chatgpt/sample_ai_items.sqlite +0 -0
  62. api_logic_server_cli/prototypes/genai_demo/database/chatgpt/sample_ai_models.py +0 -156
  63. api_logic_server_cli/prototypes/genai_demo/database/chatgpt/sample_ai_models.sqlite +0 -0
  64. api_logic_server_cli/prototypes/genai_demo/logic/cocktail-napkin.jpg +0 -0
  65. {ApiLogicServer-14.3.0.dist-info → ApiLogicServer-14.3.11.dist-info}/LICENSE +0 -0
  66. {ApiLogicServer-14.3.0.dist-info → ApiLogicServer-14.3.11.dist-info}/WHEEL +0 -0
  67. {ApiLogicServer-14.3.0.dist-info → ApiLogicServer-14.3.11.dist-info}/entry_points.txt +0 -0
  68. {ApiLogicServer-14.3.0.dist-info → ApiLogicServer-14.3.11.dist-info}/top_level.txt +0 -0
  69. /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 database.models import *
7
- from database.models import Customer, Order, Item, Product
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 logging, os
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
- 1. The Customer's balance is less than the credit limit
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
- from logic_bank.logic_bank import Rule
36
-
37
- from logic.logic_discovery.auto_discovery import discover_logic
38
- discover_logic()
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
- # Ensures the customer's balance is less than the credit limit.
37
+ # Ensure that Customer balance does not exceed credit_limit.
43
38
  Rule.constraint(validate=Customer,
44
- as_condition=lambda row: row.Balance <= row.CreditLimit,
45
- error_msg="Customer balance ({row.Balance}) exceeds credit limit ({row.CreditLimit})")
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
- #als: Demonstrate that logic == Rules + Python (for extensibility)
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
- # 4. Items.Amount = Quantity * UnitPrice, altered with IDE for CarbonNeutral discount
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
- def send_order_to_shipping(row: Order, old_row: Order, logic_row: LogicRow):
74
- """ #als: Send Kafka message formatted by OrderShipping RowDictMapper
54
+ # Items.Amount = Quantity * UnitPrice with discount for CarbonNeutral products.
55
+ Rule.formula(derive=models.Item.Amount,
56
+ calling=derive_amount)
75
57
 
76
- Format row per shipping requirements, and send (e.g., a message)
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
- NB: the after_flush event makes Order.Id avaible.
79
-
80
- Args:
81
- row (Order): inserted Order
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): # OPTIMISTIC LOCKING, [TIME / DATE STAMPING]
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
- You can optionally do time and date stamping here, as shown below.
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 not os.getenv("APILOGICPROJECT_NO_FLASK") is not None:
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
- enable_creation_stamping = False # CreatedOn time stamping
114
- if enable_creation_stamping:
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
- logic_row.log("early_row_event_all_classes - handle_all sets 'Created_on"'')
119
- Grant.process_updates(logic_row=logic_row)
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
+
@@ -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
+ &nbsp;
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
+ &nbsp;
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
+ &nbsp;
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
+ &nbsp;
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
+ &nbsp;
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
+ &nbsp;
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
+ &nbsp;
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
+ &nbsp;
137
+
138
+ ### Using the debugger
139
+
140
+ Use the debugger as shown below. Note you can stop in lambda functions.
141
+
142
+ ![Logic Debugger](https://apilogicserver.github.io/Docs/images/logic/logic-debug.png)
143
+
144
+ &nbsp;
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
+ &nbsp;
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 |