ApiLogicServer 14.4.0__py3-none-any.whl → 14.5.0__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 (107) hide show
  1. api_logic_server_cli/add_cust/add_cust.py +283 -0
  2. api_logic_server_cli/api_logic_server.py +15 -237
  3. api_logic_server_cli/api_logic_server_info.yaml +3 -3
  4. api_logic_server_cli/cli.py +38 -28
  5. api_logic_server_cli/create_from_model/__pycache__/api_logic_server_utils.cpython-312.pyc +0 -0
  6. api_logic_server_cli/create_from_model/__pycache__/dbml.cpython-312.pyc +0 -0
  7. api_logic_server_cli/create_from_model/__pycache__/ont_build.cpython-312.pyc +0 -0
  8. api_logic_server_cli/create_from_model/__pycache__/ont_create.cpython-312.pyc +0 -0
  9. api_logic_server_cli/create_from_model/api_logic_server_utils.py +47 -0
  10. api_logic_server_cli/create_from_model/dbml.py +113 -58
  11. api_logic_server_cli/create_from_model/ont_build.py +83 -60
  12. api_logic_server_cli/create_from_model/ont_create.py +2 -1
  13. api_logic_server_cli/database/basic_demo.sqlite +0 -0
  14. api_logic_server_cli/database/basic_demo.txt +1 -0
  15. api_logic_server_cli/database/basic_demo_wg.sqlite +0 -0
  16. api_logic_server_cli/manager.py +3 -2
  17. api_logic_server_cli/prototypes/base/.vscode/launch.json +3 -2
  18. api_logic_server_cli/prototypes/base/config/config.py +66 -11
  19. api_logic_server_cli/prototypes/base/config/default.env +7 -1
  20. api_logic_server_cli/prototypes/base/database/test_data/readme.md +2 -1
  21. api_logic_server_cli/prototypes/base/integration/kafka/kafka_producer.py +5 -2
  22. api_logic_server_cli/prototypes/base/integration/n8n/n8n_producer.py +68 -21
  23. api_logic_server_cli/prototypes/base/integration/n8n/n8n_readme.md +19 -0
  24. api_logic_server_cli/prototypes/base/test/basic/server_test.py +1 -1
  25. api_logic_server_cli/prototypes/basic_demo/README.md +29 -52
  26. api_logic_server_cli/prototypes/basic_demo/customizations/api/.DS_Store +0 -0
  27. api_logic_server_cli/prototypes/basic_demo/customizations/api/api_discovery/mcp_server_executor.py +138 -0
  28. api_logic_server_cli/prototypes/basic_demo/customizations/api/api_discovery/openapi.py +92 -0
  29. api_logic_server_cli/prototypes/basic_demo/customizations/api/api_discovery/proper_update_def.json +71 -0
  30. api_logic_server_cli/prototypes/basic_demo/customizations/config/default.env +13 -0
  31. api_logic_server_cli/prototypes/basic_demo/customizations/database/db.sqlite +0 -0
  32. api_logic_server_cli/prototypes/basic_demo/customizations/database/models.py +131 -0
  33. api_logic_server_cli/prototypes/basic_demo/customizations/integration/.DS_Store +0 -0
  34. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/.DS_Store +0 -0
  35. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/1_langchain_loader.py +71 -0
  36. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/README_mcp.md +13 -0
  37. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/mcp_client_executor.py +295 -0
  38. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/mcp_schema.txt +47 -0
  39. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/mcp_server_discovery.json +9 -0
  40. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/multi_mcp_flow/multi_mcp_flow.png +0 -0
  41. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/multi_mcp_flow/multi_mcp_orchestration.yaml +49 -0
  42. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/multi_mcp_flow/wny mcp flows.png +0 -0
  43. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/natlang_to_api.py +73 -0
  44. api_logic_server_cli/prototypes/{nw_no_cust → basic_demo/customizations}/integration/mcp/resources/curl.txt +2 -1
  45. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/images/MCP Overview.png +0 -0
  46. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/images/MCP_Arch.png +0 -0
  47. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/images/MCP_Overview_Executor.png +0 -0
  48. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/invoke_llm/1 - prompt_messages_array.json +10 -0
  49. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/invoke_llm/2 - completion_tool_context.json +12 -0
  50. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/llm_schema.txt +38 -0
  51. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/nw_swagger_2.yaml +17393 -0
  52. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/nw_swagger_3_relaxed.yaml +109 -0
  53. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/proxy_server.py +51 -0
  54. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/proxy_serverZ.py +72 -0
  55. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/validate_jsonapi.py +64 -0
  56. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/swagger_converter.py +65 -0
  57. api_logic_server_cli/prototypes/{nw_no_cust/integration/mcp → basic_demo/customizations/integration/mcp/z_old}/3_executor_test_agent.py +20 -6
  58. api_logic_server_cli/prototypes/basic_demo/customizations/integration/openai_function/3_executor_test_agent.py +52 -0
  59. api_logic_server_cli/prototypes/basic_demo/customizations/integration/openai_function/README_functon.md +201 -0
  60. api_logic_server_cli/prototypes/basic_demo/customizations/integration/openai_function/ai_plugin.json +17 -0
  61. api_logic_server_cli/prototypes/basic_demo/customizations/integration/openai_function/nw-swagger_3.json +1731 -0
  62. api_logic_server_cli/prototypes/basic_demo/customizations/integration/openai_function/snippets.txt +5 -0
  63. api_logic_server_cli/prototypes/basic_demo/customizations/integration/openai_function/swagger_3 genai_demo_with_get.json +1731 -0
  64. api_logic_server_cli/prototypes/basic_demo/customizations/integration/openai_function/swagger_3.json +1782 -0
  65. api_logic_server_cli/prototypes/basic_demo/customizations/integration/openai_function/swagger_3_genai_demo.json +264 -0
  66. api_logic_server_cli/prototypes/basic_demo/customizations/integration/openai_function/swagger_3_genai_demo_with_update.json +1782 -0
  67. api_logic_server_cli/prototypes/basic_demo/customizations/logic/declare_logic.py +62 -44
  68. api_logic_server_cli/prototypes/basic_demo/customizations/security/declare_security.py +11 -12
  69. api_logic_server_cli/prototypes/basic_demo/customizations/ui/admin/admin.yaml +166 -0
  70. api_logic_server_cli/prototypes/basic_demo/iteration/api/{customize_api.py → api_discovery/order_b2b.py} +17 -23
  71. api_logic_server_cli/prototypes/basic_demo/iteration/database/db.sqlite +0 -0
  72. api_logic_server_cli/prototypes/basic_demo/iteration/integration/row_dict_maps/OrderB2B.py +6 -5
  73. api_logic_server_cli/prototypes/basic_demo/iteration/integration/row_dict_maps/OrderShipping.py +4 -4
  74. api_logic_server_cli/prototypes/basic_demo/iteration/logic/declare_logic.py +69 -43
  75. api_logic_server_cli/prototypes/basic_demo/iteration/ui/admin/admin.yaml +125 -50
  76. api_logic_server_cli/prototypes/manager/README.md +4 -0
  77. api_logic_server_cli/prototypes/manager/README_X.md +663 -0
  78. api_logic_server_cli/prototypes/nw_no_cust/Tutorial.md +45 -26
  79. api_logic_server_cli/prototypes/nw_no_cust/api/api_discovery/openapi.py +130 -0
  80. api_logic_server_cli/prototypes/nw_no_cust/api/api_discovery/proper_update_def.json +71 -0
  81. api_logic_server_cli/prototypes/nw_no_cust/config/default.env +13 -0
  82. api_logic_server_cli/prototypes/ont_app/ontimize_seed/package-lock.json +9725 -1180
  83. api_logic_server_cli/prototypes/ont_app/ontimize_seed/package.json +3 -6
  84. api_logic_server_cli/prototypes/ont_app/ontimize_seed/src/app/shared/app.services.config.ts +1 -1
  85. api_logic_server_cli/prototypes/ont_app/ontimize_seed/src/assets/css/app.scss +4 -0
  86. api_logic_server_cli/prototypes/ont_app/ontimize_seed/src/assets/i18n/en.json +1 -1
  87. api_logic_server_cli/prototypes/ont_app/ontimize_seed/src/assets/i18n/es.json +14 -12
  88. api_logic_server_cli/prototypes/ont_app/templates/app_config.jinja +1 -1
  89. api_logic_server_cli/prototypes/ont_app/templates/date_template.html +1 -1
  90. api_logic_server_cli/prototypes/ont_app/templates/textarea_template.html +1 -1
  91. api_logic_server_cli/prototypes/ont_app/templates/timestamp_template.html +1 -1
  92. api_logic_server_cli/prototypes/sample_ai/logic/declare_logic.py +30 -13
  93. {apilogicserver-14.4.0.dist-info → apilogicserver-14.5.0.dist-info}/METADATA +2 -2
  94. {apilogicserver-14.4.0.dist-info → apilogicserver-14.5.0.dist-info}/RECORD +101 -60
  95. {apilogicserver-14.4.0.dist-info → apilogicserver-14.5.0.dist-info}/WHEEL +1 -1
  96. api_logic_server_cli/prototypes/basic_demo/apply_customizations.ps1 +0 -17
  97. api_logic_server_cli/prototypes/basic_demo/apply_customizations.sh +0 -14
  98. api_logic_server_cli/prototypes/basic_demo/apply_iteration.ps1 +0 -20
  99. api_logic_server_cli/prototypes/basic_demo/apply_iteration.sh +0 -15
  100. api_logic_server_cli/prototypes/nw_no_cust/integration/mcp/1_langchain_loader.py +0 -19
  101. api_logic_server_cli/prototypes/nw_no_cust/integration/mcp/README.md +0 -17
  102. /api_logic_server_cli/prototypes/{nw_no_cust → basic_demo/customizations}/integration/mcp/2_gpt_mcp_prompt.txt +0 -0
  103. /api_logic_server_cli/prototypes/{nw_no_cust → basic_demo/customizations}/integration/mcp/resources/nw_swagger_3.yaml +0 -0
  104. /api_logic_server_cli/prototypes/{nw_no_cust → basic_demo/customizations}/integration/mcp/run_executor.py +0 -0
  105. {apilogicserver-14.4.0.dist-info → apilogicserver-14.5.0.dist-info}/entry_points.txt +0 -0
  106. {apilogicserver-14.4.0.dist-info → apilogicserver-14.5.0.dist-info}/licenses/LICENSE +0 -0
  107. {apilogicserver-14.4.0.dist-info → apilogicserver-14.5.0.dist-info}/top_level.txt +0 -0
@@ -1,87 +1,105 @@
1
- #############################################
2
- # copy this file over logic/declare_logic.py
3
- #############################################
4
-
5
- import datetime
1
+ import datetime, os
6
2
  from decimal import Decimal
7
3
  from logic_bank.exec_row_logic.logic_row import LogicRow
8
4
  from logic_bank.extensions.rule_extensions import RuleExtension
9
5
  from logic_bank.logic_bank import Rule
10
- from database.models import *
6
+ from logic_bank.logic_bank import DeclareRule
7
+ import database.models as models
11
8
  import api.system.opt_locking.opt_locking as opt_locking
12
- from security.system.authorization import Grant
13
- import logging, os
9
+ from security.system.authorization import Grant, Security
10
+ from logic.load_verify_rules import load_verify_rules
11
+ import integration.kafka.kafka_producer as kafka_producer
12
+ import logging
14
13
 
15
14
  app_logger = logging.getLogger(__name__)
16
15
 
17
- declare_logic_message = "Sample Rules Loaded"
16
+ declare_logic_message = "ALERT: *** No Rules Yet ***" # printed in api_logic_server.py
18
17
 
19
18
  def declare_logic():
20
-
21
- """ Declarative multi-table derivations and constraints, extensible with Python.
22
-
19
+ ''' Declarative multi-table derivations and constraints, extensible with Python.
20
+
23
21
  Brief background: see readme_declare_logic.md
22
+
23
+ Your Code Goes Here - Use code completion (Rule.) to declare rules
24
+ '''
24
25
 
25
- Use code completion (Rule.) to declare rules here
26
-
27
- Check Credit - Logic Design:
28
-
29
- 1. Customer.Balance <= CreditLimit
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()
30
33
 
31
- 2. Customer.Balance = Sum(Order.AmountTotal where unshipped)
34
+ # Logic from GenAI: (or, use your IDE w/ code completion)
35
+ from database.models import Product, Order, Item, Customer
32
36
 
33
- 3. Order.AmountTotal = Sum(Items.Amount)
37
+ # Ensure the customer's balance is less than their credit limit
38
+ Rule.constraint(validate=Customer, as_condition=lambda row: row.balance <= row.credit_limit, error_msg="Customer balance ({row.balance}) exceeds credit limit ({row.credit_limit})")
34
39
 
35
- 4. Items.Amount = Quantity * UnitPrice
40
+ # Derive the customer's balance as the sum of order totals where not yet shipped.
41
+ Rule.sum(derive=Customer.balance, as_sum_of=Order.amount_total, where=lambda row: row.date_shipped is None)
36
42
 
37
- 5. Items.UnitPrice = copy from Product
38
- """
43
+ # Derive the order's total amount from the sum of item amounts.
44
+ Rule.sum(derive=Order.amount_total, as_sum_of=Item.amount)
39
45
 
40
- Rule.constraint(validate=Customer, # logic design translates directly into rules
41
- as_condition=lambda row: row.Balance <= row.CreditLimit,
42
- error_msg="balance ({round(row.Balance, 2)}) exceeds credit ({round(row.CreditLimit, 2)})")
46
+ # Calculate item amount based on quantity and unit price.
47
+ Rule.formula(derive=Item.amount, as_expression=lambda row: row.quantity * row.unit_price)
43
48
 
44
- Rule.sum(derive=Customer.Balance, # adjust iff AmountTotal or ShippedDate or CustomerID changes
45
- as_sum_of=Order.AmountTotal,
46
- where=lambda row: row.ShipDate is None) # adjusts - *not* a sql select sum...
49
+ # Copy unit price from product to item.
50
+ Rule.copy(derive=Item.unit_price, from_parent=Product.unit_price)
47
51
 
48
- Rule.sum(derive=Order.AmountTotal, # adjust iff Amount or OrderID changes
49
- as_sum_of=Item.Amount)
52
+ # Send order details to Kafka if order is shipped.
53
+ Rule.after_flush_row_event(on_class=Order, calling=kafka_producer.send_row_to_kafka, if_condition=lambda row: row.date_shipped is not None, with_args={'topic': 'order_shipping'})
50
54
 
51
- Rule.formula(derive=Item.Amount, # compute price * qty
52
- as_expression=lambda row: row.UnitPrice * row.Quantity)
55
+ # End Logic from GenAI
53
56
 
54
- Rule.copy(derive=Item.UnitPrice, # get Product Price (e,g., on insert, or ProductId change)
55
- from_parent=Product.UnitPrice)
56
57
 
57
-
58
- def handle_all(logic_row: LogicRow): # OPTIMISTIC LOCKING, [TIME / DATE STAMPING]
58
+ def handle_all(logic_row: LogicRow): # #als: TIME / DATE STAMPING, OPTIMISTIC LOCKING
59
59
  """
60
60
  This is generic - executed for all classes.
61
61
 
62
- Invokes optimistic locking.
62
+ Invokes optimistic locking, and checks Grant permissions.
63
63
 
64
- You can optionally do time and date stamping here, as shown below.
64
+ Also provides user/date stamping.
65
65
 
66
66
  Args:
67
67
  logic_row (LogicRow): from LogicBank - old/new row, state
68
68
  """
69
69
 
70
- if not os.getenv("APILOGICPROJECT_NO_FLASK") is not None:
70
+ if os.getenv("APILOGICPROJECT_NO_FLASK") is not None:
71
+ print("\ndeclare_logic.py Using TestBase\n")
71
72
  return # enables rules to be used outside of Flask, e.g., test data loading
72
73
 
73
74
  if logic_row.is_updated() and logic_row.old_row is not None and logic_row.nest_level == 0:
74
75
  opt_locking.opt_lock_patch(logic_row=logic_row)
75
- enable_creation_stamping = False # CreatedOn time stamping
76
- if enable_creation_stamping:
76
+
77
+ Grant.process_updates(logic_row=logic_row)
78
+
79
+ did_stamping = False
80
+ if enable_stamping := False: # #als: DATE / USER STAMPING
77
81
  row = logic_row.row
78
82
  if logic_row.ins_upd_dlt == "ins" and hasattr(row, "CreatedOn"):
79
83
  row.CreatedOn = datetime.datetime.now()
80
- logic_row.log("early_row_event_all_classes - handle_all sets 'Created_on"'')
81
- Grant.process_updates(logic_row=logic_row)
82
-
84
+ did_stamping = True
85
+ if logic_row.ins_upd_dlt == "ins" and hasattr(row, "CreatedBy"):
86
+ row.CreatedBy = Security.current_user().id
87
+ # if Config.SECURITY_ENABLED == True else 'public'
88
+ did_stamping = True
89
+ if logic_row.ins_upd_dlt == "upd" and hasattr(row, "UpdatedOn"):
90
+ row.UpdatedOn = datetime.datetime.now()
91
+ did_stamping = True
92
+ if logic_row.ins_upd_dlt == "upd" and hasattr(row, "UpdatedBy"):
93
+ row.UpdatedBy = Security.current_user().id \
94
+ if Config.SECURITY_ENABLED == True else 'public'
95
+ did_stamping = True
96
+ if did_stamping:
97
+ logic_row.log("early_row_event_all_classes - handle_all did stamping")
83
98
  Rule.early_row_event_all_classes(early_row_event_all_classes=handle_all)
84
99
 
100
+ #als rules report
101
+ from api.system import api_utils
102
+ # api_utils.rules_report()
85
103
 
86
104
  app_logger.debug("..logic/declare_logic.py (logic == rules + code)")
87
105
 
@@ -35,26 +35,25 @@ DefaultRolePermission(to_role = Roles.renter, can_read=True, can_delete=False)
35
35
  DefaultRolePermission(to_role = Roles.manager, can_read=True, can_delete=False)
36
36
  DefaultRolePermission(to_role = Roles.sales, can_read=True, can_delete=False)
37
37
 
38
+
38
39
  #############################################
39
40
  # Observe: Filters are AND'd, Grants are OR'd
40
41
  #############################################
41
- GlobalFilter( global_filter_attribute_name = "Region", # sales see only Customers in British Isles (9 rows)
42
- roles_not_filtered = ["sa", "manager", "tenant", "renter"], # ie, just sales
43
- filter = '{entity_class}.Region == Security.current_user().region')
44
-
42
+
43
+ GlobalFilter( global_filter_attribute_name = "name", # sales does not see Bob (yes, silly - see nw & multi-tenant))
44
+ roles_not_filtered = ["sa", "manager", "tenant", "renter"], # ie, just sales
45
+ filter = '{entity_class}.name != \'Bob\' ') # poor Bob
46
+
47
+
45
48
  Grant( on_entity = models.Customer,
46
49
  to_role = Roles.sales,
47
- filter = lambda : models.Customer.CreditLimit > 300,
48
- filter_debug = "CreditLimit > 0") # this eliminates all rows, but...
50
+ filter = lambda : models.Customer.credit_limit >= 3000,
51
+ filter_debug = "credit_limit > 3000") # this eliminates Charlie, but...
49
52
 
50
53
  Grant( on_entity = models.Customer,
51
54
  to_role = Roles.sales,
52
- filter = lambda : models.Customer.ContactName == "Mike",
53
- filter_debug = "ContactName == Mike (see security/declare_security.py)")
54
-
55
- # so user s1 sees the CTWSR customer row, per the resulting where from 2 global filters and 2 Grants:
56
- # where (Client_id=2 and region="British Isles") and (CreditLimit>300 or ContactName="Mike")
57
- # <---- Filters AND'd -------------------> <--- Grants OR'd --------------------->
55
+ filter = lambda : models.Customer.balance > 0,
56
+ filter_debug = "balance > 0 (see security/declare_security.py)") # this let's us see Charlie (grants OR'd)
58
57
 
59
58
 
60
59
  app_logger.debug("Declare Security complete - security/declare_security.py"
@@ -0,0 +1,166 @@
1
+ about:
2
+ date: May 13, 2025 20:12:31
3
+ merged:
4
+ at: May 13, 2025 20:15:21
5
+ new_attributes: 'Customer.email Order.CreatedOn '
6
+ new_resources: 'Email '
7
+ new_tab_groups: 'Customer.EmailList '
8
+ recent_changes: works with modified safrs-react-admin
9
+ version: 0.0.0
10
+ api_root: '{http_type}://{swagger_host}:{port}/{api}'
11
+ authentication: '{system-default}'
12
+ info:
13
+ number_relationships: 4
14
+ number_tables: 5
15
+ info_toggle_checked: true
16
+ resources:
17
+ Customer:
18
+ attributes:
19
+ - label: ' name*'
20
+ name: name
21
+ search: true
22
+ sort: true
23
+ - name: balance
24
+ type: DECIMAL
25
+ - name: credit_limit
26
+ type: DECIMAL
27
+ - name: id
28
+ - name: email
29
+ - name: email_opt_out
30
+ type: BOOLEAN
31
+ tab_groups:
32
+ - direction: tomany
33
+ fks:
34
+ - customer_id
35
+ name: OrderList
36
+ resource: Order
37
+ - direction: tomany
38
+ fks:
39
+ - customer_id
40
+ name: EmailList
41
+ resource: Email
42
+ type: Customer
43
+ user_key: name
44
+ Email:
45
+ attributes:
46
+ - label: ' id*'
47
+ name: id
48
+ search: true
49
+ sort: true
50
+ - name: customer_id
51
+ required: true
52
+ - name: message
53
+ - name: CreatedOn
54
+ type: DATE
55
+ tab_groups:
56
+ - direction: toone
57
+ fks:
58
+ - customer_id
59
+ name: customer
60
+ resource: Customer
61
+ type: Email
62
+ user_key: id
63
+ Item:
64
+ attributes:
65
+ - label: ' id*'
66
+ name: id
67
+ search: true
68
+ sort: true
69
+ - name: order_id
70
+ - name: product_id
71
+ required: true
72
+ - name: quantity
73
+ required: true
74
+ - name: amount
75
+ type: DECIMAL
76
+ - name: unit_price
77
+ type: DECIMAL
78
+ tab_groups:
79
+ - direction: toone
80
+ fks:
81
+ - order_id
82
+ name: order
83
+ resource: Order
84
+ - direction: toone
85
+ fks:
86
+ - product_id
87
+ name: product
88
+ resource: Product
89
+ type: Item
90
+ user_key: id
91
+ Order:
92
+ attributes:
93
+ - label: ' id*'
94
+ name: id
95
+ search: true
96
+ sort: true
97
+ - name: customer_id
98
+ required: true
99
+ - name: notes
100
+ - name: amount_total
101
+ type: DECIMAL
102
+ - name: date_shipped
103
+ type: DATE
104
+ - name: CreatedOn
105
+ type: DATE
106
+ tab_groups:
107
+ - direction: tomany
108
+ fks:
109
+ - order_id
110
+ name: ItemList
111
+ resource: Item
112
+ - direction: toone
113
+ fks:
114
+ - customer_id
115
+ name: customer
116
+ resource: Customer
117
+ type: Order
118
+ user_key: id
119
+ Product:
120
+ attributes:
121
+ - label: ' name*'
122
+ name: name
123
+ search: true
124
+ sort: true
125
+ - name: unit_price
126
+ type: DECIMAL
127
+ - name: id
128
+ tab_groups:
129
+ - direction: tomany
130
+ fks:
131
+ - product_id
132
+ name: ItemList
133
+ resource: Item
134
+ type: Product
135
+ user_key: name
136
+ settings:
137
+ HomeJS: /admin-app/home.js
138
+ max_list_columns: 8
139
+ style_guide:
140
+ applicationLocales:
141
+ - en
142
+ - es
143
+ currency_symbol: $
144
+ currency_symbol_position: left
145
+ date_format: LL
146
+ decimal_max: '1000000000'
147
+ decimal_min: '2'
148
+ decimal_separator: .
149
+ detail_mode: tab
150
+ edit_on_mode: dblclick
151
+ exclude_listpicker: false
152
+ include_translation: 'false'
153
+ keycloak_client_id: alsclient
154
+ keycloak_realm: kcals
155
+ keycloak_url: http://localhost:8080
156
+ locale: en
157
+ max_decimal_digits: '4'
158
+ min_decimal_digits: '2'
159
+ new_mode: dialog
160
+ pick_style: list
161
+ row_height: small,
162
+ serviceType: JSONAPI
163
+ startSessionPath: /auth/login
164
+ style: light
165
+ thousand_separator: ','
166
+ use_keycloak: 'false'
@@ -1,35 +1,29 @@
1
- import logging
2
- import api.system.api_utils as api_utils
3
- import safrs
4
1
  from flask import request, jsonify
2
+ import logging
3
+
5
4
  from safrs import jsonapi_rpc
6
- from database import models
7
- import integration.system.RowDictMapper as row_dict_mapper
8
- from integration.row_dict_maps.OrderShipping import OrderShipping
5
+ import safrs
6
+
9
7
  from integration.row_dict_maps.OrderB2B import OrderB2B
10
8
 
11
- # called by api_logic_server_run.py, to customize api (new end points, services).
12
- # separate from expose_api_models.py, to simplify merge if project recreated
9
+ app_logger = logging.getLogger("api_logic_server_app")
13
10
 
14
- app_logger = logging.getLogger(__name__)
11
+ def add_service(app, api, project_dir, swagger_host: str, PORT: str, method_decorators = []):
12
+ pass
15
13
 
16
- def expose_services(app, api, project_dir, swagger_host: str, PORT: str):
17
- """ Customize API - new end points for services
18
-
19
- Brief background: see readme_customize_api.md
20
14
 
21
- Your Code Goes Here
22
-
23
15
  """
16
+ Illustrates #als: custom end point with swagger, RowDictMapper
24
17
 
25
- from api.api_discovery.auto_discovery import discover_services
26
- discover_services(app, api, project_dir, swagger_host, PORT)
27
-
28
- app_logger.debug("api/customize_api.py - expose custom services")
18
+ * Custom service - visible in swagger
19
+ * Services *not* requiring authentication (contrast to CategoriesEndPoint, below)
20
+ * Use OrderB2B (extends RowDictMapper) to map json to rows with aliasing, joins and lookups
21
+ * Recall business logic is not in service, but encapsulated for reuse in logic/declare_logic.py
22
+ """
23
+ # class B2BSvcs(safrs.JABase):
29
24
 
30
25
  api.expose_object(ServicesEndPoint) # Swagger-visible services
31
26
 
32
-
33
27
  """
34
28
  Illustrates #als: custom end point with swagger, RowDictMapper
35
29
 
@@ -47,12 +41,12 @@ class ServicesEndPoint(safrs.JABase):
47
41
  """ # yaml creates Swagger description
48
42
  args :
49
43
  order:
50
- Account: "Customer 1"
44
+ Account: "Alice"
51
45
  Notes: "Please Rush"
52
46
  Items :
53
- - Name: "Product A"
47
+ - Name: "Gadget"
54
48
  QuantityOrdered: 1
55
- - Name: "Product B"
49
+ - Name: "Widget"
56
50
  QuantityOrdered: 2
57
51
  - Name: "Green"
58
52
  QuantityOrdered: 3
@@ -19,16 +19,17 @@ 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)]
22
+ , fields = [(models.Order.notes, "Notes")]
23
23
  , parent_lookups = [( models.Customer,
24
- [(models.Customer.Name, 'Account')]
24
+ [(models.Customer.name, 'Account')]
25
25
  )]
26
26
  , related = [
27
27
  (RowDictMapper(model_class=models.Item
28
28
  , alias="Items"
29
- , fields = [(models.Item.Quantity, "QuantityOrdered")]
30
- , parent_lookups = [( models.Product, [models.Product.Name] )]
31
- )
29
+ , fields = [(models.Item.quantity, "QuantityOrdered")]
30
+ , parent_lookups = [( models.Product,
31
+ [(models.Product.name, "Name" )]
32
+ )])
32
33
  )
33
34
  ]
34
35
  )
@@ -17,11 +17,11 @@ class OrderShipping(RowDictMapper):
17
17
  order = super(OrderShipping, self).__init__(
18
18
  model_class=models.Order
19
19
  , alias = "order"
20
- , fields = [models.Order.OrderID, (models.Order.AmountTotal, "Total"), (models.Order.ShipDate, "Order Date"), models.Order.CustomerID]
20
+ , fields = [models.Order.id, (models.Order.amount_total, "Total"), (models.Order.date_shipped, "Order Date"), models.Order.customer_id]
21
21
  , related = RowDictMapper(model_class=models.Item, alias="Items"
22
- , fields = [models.Item.OrderID, models.Item.Quantity, models.Item.Amount]
23
- , related = RowDictMapper(model_class=models.Product, alias="product"
24
- , fields=[models.Product.Name, models.Product.UnitPrice]
22
+ , fields = [models.Item.order_id, models.Item.quantity, models.Item.amount]
23
+ , related = RowDictMapper(model_class=models.Product, alias="product", role_name='product'
24
+ , fields=[models.Product.name, models.Product.unit_price]
25
25
  , isParent=True
26
26
  )
27
27
  )
@@ -1,16 +1,16 @@
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 import models
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
9
  from integration.row_dict_maps.OrderShipping import OrderShipping
12
- from confluent_kafka import Producer, KafkaException
10
+ from security.system.authorization import Grant, Security
11
+ from logic.load_verify_rules import load_verify_rules
13
12
  import integration.kafka.kafka_producer as kafka_producer
13
+ import logging
14
14
 
15
15
  app_logger = logging.getLogger(__name__)
16
16
 
@@ -22,45 +22,55 @@ def declare_logic():
22
22
  Brief background: see readme_declare_logic.md
23
23
 
24
24
  Your Code Goes Here - Use code completion (Rule.) to declare rules
25
-
26
- GenAI: Paste the following into Copilot Chat, and paste the result below.
27
-
28
- Use Logic Bank to enforce these requirements:
29
-
30
- Enforce the Check Credit requirement (do not generate check constraints):
31
- 1. Customer.Balance <= CreditLimit
32
- 2. Customer.Balance = Sum(Order.AmountTotal where date shipped is null)
33
- 3. Order.AmountTotal = Sum(Items.Amount)
34
- 4. Items.Amount = Quantity * UnitPrice
35
- 5. Store the Items.UnitPrice as a copy from Product.UnitPrice
36
25
  '''
37
26
 
38
- from logic_bank.logic_bank import Rule
27
+ if os.environ.get("WG_PROJECT"):
28
+ # Inside WG: Load rules from docs/expprt/export.json
29
+ load_verify_rules()
30
+ else:
31
+ # Outside WG: load declare_logic function
32
+ from logic.logic_discovery.auto_discovery import discover_logic
33
+ discover_logic()
34
+
35
+ # Logic from GenAI: (or, use your IDE w/ code completion)
36
+ from database.models import Product, Order, Item, Customer
37
+
38
+ # Ensure the customer's balance is less than their credit limit
39
+ Rule.constraint(validate=Customer, as_condition=lambda row: row.balance <= row.credit_limit, error_msg="Customer balance ({row.balance}) exceeds credit limit ({row.credit_limit})")
40
+
41
+ # Derive the customer's balance as the sum of order totals where not yet shipped.
42
+ Rule.sum(derive=Customer.balance, as_sum_of=Order.amount_total, where=lambda row: row.date_shipped is None)
39
43
 
40
- # 1. Customer.Balance <= CreditLimit
41
- Rule.constraint(validate=Customer,
42
- error_msg="balance exceeds credit limit",
43
- as_condition=lambda row: row.Balance <= row.CreditLimit)
44
+ # Derive the order's total amount from the sum of item amounts.
45
+ Rule.sum(derive=Order.amount_total, as_sum_of=Item.amount)
44
46
 
45
- # 2. Customer.Balance = Sum(Order.AmountTotal where date shipped is null)
46
- Rule.sum(derive=Customer.Balance, as_sum_of=Order.AmountTotal,
47
- where=lambda row: row.ShipDate is None)
48
47
 
49
- # 3. Order.AmountTotal = Sum(Items.Amount)
50
- Rule.sum(derive=Order.AmountTotal, as_sum_of=Item.Amount)
48
+ # ************ Python Customization Example ****************
51
49
 
50
+ # Calculate item amount based on quantity and unit price.
51
+ # Formerly, we had this rule:
52
+ # Rule.formula(derive=Item.amount, as_expression=lambda row: row.quantity * row.unit_price)
53
+ # If the derivation were more complex, we could use a Python function, like this:
52
54
  def derive_amount(row: models.Item, old_row: models.Item, logic_row: LogicRow):
53
- amount = row.Quantity * row.UnitPrice
54
- if row.Product.CarbonNeutral == True and row.Quantity >= 10:
55
+ amount = row.quantity * row.unit_price
56
+ product = row.product
57
+ if product.carbon_neutral == True and row.quantity >= 10:
55
58
  amount = amount * Decimal(0.9) # breakpoint here
56
59
  return amount
57
60
 
58
61
  # 4. Items.Amount = Quantity * UnitPrice
59
- Rule.formula(derive=models.Item.Amount, calling=derive_amount)
62
+ Rule.formula(derive=models.Item.amount, calling=derive_amount)
60
63
 
61
- # 5. Store the Items.UnitPrice as a copy from Product.UnitPrice
62
- Rule.copy(Item.UnitPrice, from_parent=Product.UnitPrice)
64
+ # Copy unit price from product to item.
65
+ Rule.copy(derive=Item.unit_price, from_parent=Product.unit_price)
63
66
 
67
+
68
+ # ************ Python Customization Example ****************
69
+
70
+ # Send order details to Kafka if order is shipped.
71
+ # Formeraly, we had this rule:
72
+ # Rule.after_flush_row_event(on_class=Order, calling=kafka_producer.send_row_to_kafka, if_condition=lambda row: row.date_shipped is not None, with_args={'topic': 'order_shipping'})
73
+ # If the integration were more complex, we could use Python, like this:
64
74
  #als: Demonstrate that logic == Rules + Python (for extensibility)
65
75
 
66
76
  def send_order_to_shipping(row: models.Order, old_row: models.Order, logic_row: LogicRow):
@@ -68,7 +78,7 @@ def declare_logic():
68
78
 
69
79
  Format row per shipping requirements, and send (e.g., a message)
70
80
 
71
- NB: the after_flush event makes Order.Id avaible. Contrast to congratulate_sales_rep().
81
+ NB: the after_flush event makes Order.Id avaible.
72
82
 
73
83
  Args:
74
84
  row (models.Order): inserted Order
@@ -79,38 +89,54 @@ def declare_logic():
79
89
  kafka_producer.send_kafka_message(logic_row=logic_row,
80
90
  row_dict_mapper=OrderShipping,
81
91
  kafka_topic="order_shipping",
82
- kafka_key=str(row.OrderID),
92
+ kafka_key=str(row.id),
83
93
  msg="Sending Order to Shipping")
84
94
 
85
95
  Rule.after_flush_row_event(on_class=models.Order, calling=send_order_to_shipping) # see above
86
96
 
97
+ # End Logic from GenAI
87
98
 
88
99
 
89
- def handle_all(logic_row: LogicRow): # OPTIMISTIC LOCKING, [TIME / DATE STAMPING]
100
+ def handle_all(logic_row: LogicRow): # #als: TIME / DATE STAMPING, OPTIMISTIC LOCKING
90
101
  """
91
102
  This is generic - executed for all classes.
92
103
 
93
- Invokes optimistic locking.
104
+ Invokes optimistic locking, and checks Grant permissions.
94
105
 
95
- You can optionally do time and date stamping here, as shown below.
106
+ Also provides user/date stamping.
96
107
 
97
108
  Args:
98
109
  logic_row (LogicRow): from LogicBank - old/new row, state
99
110
  """
100
111
 
101
- if not os.getenv("APILOGICPROJECT_NO_FLASK") is not None:
112
+ if os.getenv("APILOGICPROJECT_NO_FLASK") is not None:
113
+ print("\ndeclare_logic.py Using TestBase\n")
102
114
  return # enables rules to be used outside of Flask, e.g., test data loading
103
115
 
104
116
  if logic_row.is_updated() and logic_row.old_row is not None and logic_row.nest_level == 0:
105
117
  opt_locking.opt_lock_patch(logic_row=logic_row)
106
- enable_creation_stamping = False # CreatedOn time stamping
107
- if enable_creation_stamping:
118
+
119
+ Grant.process_updates(logic_row=logic_row)
120
+
121
+ did_stamping = False
122
+ if enable_stamping := False: # #als: DATE / USER STAMPING
108
123
  row = logic_row.row
109
124
  if logic_row.ins_upd_dlt == "ins" and hasattr(row, "CreatedOn"):
110
125
  row.CreatedOn = datetime.datetime.now()
111
- logic_row.log("early_row_event_all_classes - handle_all sets 'Created_on"'')
112
- Grant.process_updates(logic_row=logic_row)
113
-
126
+ did_stamping = True
127
+ if logic_row.ins_upd_dlt == "ins" and hasattr(row, "CreatedBy"):
128
+ row.CreatedBy = Security.current_user().id
129
+ # if Config.SECURITY_ENABLED == True else 'public'
130
+ did_stamping = True
131
+ if logic_row.ins_upd_dlt == "upd" and hasattr(row, "UpdatedOn"):
132
+ row.UpdatedOn = datetime.datetime.now()
133
+ did_stamping = True
134
+ if logic_row.ins_upd_dlt == "upd" and hasattr(row, "UpdatedBy"):
135
+ row.UpdatedBy = Security.current_user().id \
136
+ if Config.SECURITY_ENABLED == True else 'public'
137
+ did_stamping = True
138
+ if did_stamping:
139
+ logic_row.log("early_row_event_all_classes - handle_all did stamping")
114
140
  Rule.early_row_event_all_classes(early_row_event_all_classes=handle_all)
115
141
 
116
142
  #als rules report