ApiLogicServer 14.3.0__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.3.0.dist-info → ApiLogicServer-14.3.7.dist-info}/METADATA +2 -2
- {ApiLogicServer-14.3.0.dist-info → ApiLogicServer-14.3.7.dist-info}/RECORD +46 -43
- 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 +1 -1
- api_logic_server_cli/genai/genai.py +4 -0
- 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/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 +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/launch.json +1 -1
- 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/readme.md +58 -5
- api_logic_server_cli/prototypes/manager/system/genai/learning_requests/logic_bank_api.prompt +15 -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/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.7.dist-info}/LICENSE +0 -0
- {ApiLogicServer-14.3.0.dist-info → ApiLogicServer-14.3.7.dist-info}/WHEEL +0 -0
- {ApiLogicServer-14.3.0.dist-info → ApiLogicServer-14.3.7.dist-info}/entry_points.txt +0 -0
- {ApiLogicServer-14.3.0.dist-info → ApiLogicServer-14.3.7.dist-info}/top_level.txt +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# coding: utf-8
|
|
2
|
-
from sqlalchemy import
|
|
2
|
+
from sqlalchemy import DECIMAL, DateTime # API Logic Server GenAI assist
|
|
3
|
+
from sqlalchemy import Column, Date, ForeignKey, Integer, Numeric, String, Boolean
|
|
3
4
|
from sqlalchemy.orm import relationship
|
|
4
5
|
from sqlalchemy.ext.declarative import declarative_base
|
|
5
6
|
|
|
@@ -9,13 +10,13 @@ from sqlalchemy.ext.declarative import declarative_base
|
|
|
9
10
|
# Alter this file per your database maintenance policy
|
|
10
11
|
# See https://apilogicserver.github.io/Docs/Project-Rebuild/#rebuilding
|
|
11
12
|
#
|
|
12
|
-
# Created:
|
|
13
|
-
# Database: sqlite:////Users/val/dev/ApiLogicServer/ApiLogicServer-dev/
|
|
13
|
+
# Created: January 31, 2025 17:52:29
|
|
14
|
+
# Database: sqlite:////Users/val/dev/ApiLogicServer/ApiLogicServer-dev/build_and_test/ApiLogicServer/genai_demo/database/db.sqlite
|
|
14
15
|
# Dialect: sqlite
|
|
15
16
|
#
|
|
16
17
|
# mypy: ignore-errors
|
|
17
18
|
########################################################################################################################
|
|
18
|
-
|
|
19
|
+
|
|
19
20
|
from database.system.SAFRSBaseX import SAFRSBaseX, TestBase
|
|
20
21
|
from flask_login import UserMixin
|
|
21
22
|
import safrs, flask_sqlalchemy, os
|
|
@@ -42,72 +43,81 @@ else:
|
|
|
42
43
|
print('*** Models.py Using TestBase ***')
|
|
43
44
|
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
|
|
47
|
+
class Customer(Base): # type: ignore
|
|
48
|
+
"""
|
|
49
|
+
description: Customer table with unique name, balance and credit_limit.
|
|
50
|
+
"""
|
|
51
|
+
__tablename__ = 'customer'
|
|
47
52
|
_s_collection_name = 'Customer' # type: ignore
|
|
48
53
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
Balance : DECIMAL = Column(DECIMAL(10, 2))
|
|
54
|
-
CreditLimit : DECIMAL = Column(DECIMAL(10, 2), nullable=False)
|
|
54
|
+
id = Column(Integer, primary_key=True)
|
|
55
|
+
name = Column(String(255), unique=True)
|
|
56
|
+
balance = Column(Numeric)
|
|
57
|
+
credit_limit = Column(Numeric)
|
|
55
58
|
|
|
56
59
|
# parent relationships (access parent)
|
|
57
60
|
|
|
58
61
|
# child relationships (access children)
|
|
59
|
-
OrderList : Mapped[List["Order"]] = relationship(back_populates="
|
|
62
|
+
OrderList : Mapped[List["Order"]] = relationship(back_populates="customer")
|
|
60
63
|
|
|
61
64
|
|
|
62
|
-
class Product(Base):
|
|
63
|
-
__tablename__ = 'Products'
|
|
64
|
-
_s_collection_name = 'Product' # type: ignore
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
class Product(Base): # type: ignore
|
|
67
|
+
"""
|
|
68
|
+
description: Product table with unit_price used for copying to Item.
|
|
69
|
+
"""
|
|
70
|
+
__tablename__ = 'product'
|
|
71
|
+
_s_collection_name = 'Product' # type: ignore
|
|
70
72
|
|
|
73
|
+
id = Column(Integer, primary_key=True)
|
|
74
|
+
name = Column(String(255))
|
|
75
|
+
unit_price = Column(Numeric)
|
|
76
|
+
carbon_neutral = Column(Boolean)
|
|
71
77
|
# parent relationships (access parent)
|
|
72
78
|
|
|
73
79
|
# child relationships (access children)
|
|
74
|
-
ItemList : Mapped[List["Item"]] = relationship(back_populates="
|
|
80
|
+
ItemList : Mapped[List["Item"]] = relationship(back_populates="product")
|
|
75
81
|
|
|
76
82
|
|
|
77
83
|
|
|
78
|
-
class Order(Base):
|
|
79
|
-
|
|
84
|
+
class Order(Base): # type: ignore
|
|
85
|
+
"""
|
|
86
|
+
description: Order table with a foreign key to Customer, a notes field, date_shipped and derived amount_total.
|
|
87
|
+
"""
|
|
88
|
+
__tablename__ = 'order'
|
|
80
89
|
_s_collection_name = 'Order' # type: ignore
|
|
81
90
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
AmountTotal : DECIMAL = Column(DECIMAL(10, 2))
|
|
91
|
+
id = Column(Integer, primary_key=True)
|
|
92
|
+
customer_id = Column(ForeignKey('customer.id'))
|
|
93
|
+
date_shipped = Column(Date)
|
|
94
|
+
notes = Column(String(255))
|
|
95
|
+
amount_total = Column(Numeric)
|
|
88
96
|
|
|
89
97
|
# parent relationships (access parent)
|
|
90
|
-
|
|
98
|
+
customer : Mapped["Customer"] = relationship(back_populates=("OrderList"))
|
|
91
99
|
|
|
92
100
|
# child relationships (access children)
|
|
93
|
-
ItemList : Mapped[List["Item"]] = relationship(back_populates="
|
|
101
|
+
ItemList : Mapped[List["Item"]] = relationship(back_populates="order")
|
|
94
102
|
|
|
95
103
|
|
|
96
104
|
|
|
97
|
-
class Item(Base):
|
|
98
|
-
|
|
105
|
+
class Item(Base): # type: ignore
|
|
106
|
+
"""
|
|
107
|
+
description: Item table with non-null quantity, derived amount and unit_price copied from Product.
|
|
108
|
+
"""
|
|
109
|
+
__tablename__ = 'item'
|
|
99
110
|
_s_collection_name = 'Item' # type: ignore
|
|
100
111
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
112
|
+
id = Column(Integer, primary_key=True)
|
|
113
|
+
order_id = Column(ForeignKey('order.id'))
|
|
114
|
+
product_id = Column(ForeignKey('product.id'))
|
|
115
|
+
quantity = Column(Integer, nullable=False)
|
|
116
|
+
unit_price = Column(Numeric)
|
|
117
|
+
amount = Column(Numeric)
|
|
107
118
|
|
|
108
119
|
# parent relationships (access parent)
|
|
109
|
-
|
|
110
|
-
|
|
120
|
+
order : Mapped["Order"] = relationship(back_populates=("ItemList"))
|
|
121
|
+
product : Mapped["Product"] = relationship(back_populates=("ItemList"))
|
|
111
122
|
|
|
112
123
|
# child relationships (access children)
|
|
113
|
-
|
|
@@ -19,15 +19,13 @@ class OrderB2B(RowDictMapper):
|
|
|
19
19
|
order = super(OrderB2B, self).__init__(
|
|
20
20
|
model_class=models.Order
|
|
21
21
|
, alias = "order"
|
|
22
|
-
, fields = [(models.Order.Notes)]
|
|
23
|
-
, parent_lookups = [( models.Customer,
|
|
24
|
-
[(models.Customer.CustomerName, 'Account')]
|
|
25
|
-
)]
|
|
22
|
+
, fields = [(models.Order.notes, "Notes")]
|
|
23
|
+
, parent_lookups = [( models.Customer, [(models.Customer.name, 'Account')] )]
|
|
26
24
|
, related = [
|
|
27
25
|
(RowDictMapper(model_class=models.Item
|
|
28
26
|
, alias="Items"
|
|
29
|
-
, fields = [(models.Item.
|
|
30
|
-
, parent_lookups = [( models.Product, [models.Product.ProductName] )]
|
|
27
|
+
, fields = [(models.Item.quantity, "QuantityOrdered")]
|
|
28
|
+
, parent_lookups = [( models.Product, [(models.Product.name, 'ProductName')] )]
|
|
31
29
|
)
|
|
32
30
|
)
|
|
33
31
|
]
|
|
Binary file
|
|
Binary file
|
api_logic_server_cli/prototypes/genai_demo/logic/__pycache__/load_verify_rules.cpython-312.pyc
ADDED
|
Binary file
|
|
@@ -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
|