ApiLogicServer 14.5.0__py3-none-any.whl → 14.5.4__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 (50) hide show
  1. api_logic_server_cli/add_cust/add_cust.py +7 -21
  2. api_logic_server_cli/api_logic_server.py +4 -2
  3. api_logic_server_cli/api_logic_server_info.yaml +2 -2
  4. api_logic_server_cli/create_from_model/__pycache__/dbml.cpython-312.pyc +0 -0
  5. api_logic_server_cli/create_from_model/__pycache__/ont_build.cpython-312.pyc +0 -0
  6. api_logic_server_cli/create_from_model/dbml.py +3 -0
  7. api_logic_server_cli/prototypes/basic_demo/customizations/api/api_discovery/{mcp_server_executor.py → mcp_discovery.py} +1 -43
  8. api_logic_server_cli/prototypes/basic_demo/customizations/config/server_setup.py +388 -0
  9. api_logic_server_cli/prototypes/basic_demo/customizations/database/system/SAFRSBaseX.py +139 -0
  10. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/.DS_Store +0 -0
  11. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/README_mcp.md +3 -1
  12. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/mcp_client_executor.py +144 -131
  13. api_logic_server_cli/prototypes/basic_demo/customizations/logic/declare_logic.py +22 -2
  14. api_logic_server_cli/prototypes/basic_demo/iteration/logic/declare_logic.py +1 -1
  15. api_logic_server_cli/prototypes/nw/logic/declare_logic.py +2 -2
  16. api_logic_server_cli/prototypes/nw_no_cust/.obsidian/app.json +1 -0
  17. api_logic_server_cli/prototypes/nw_no_cust/.obsidian/appearance.json +1 -0
  18. api_logic_server_cli/prototypes/nw_no_cust/.obsidian/core-plugins.json +31 -0
  19. api_logic_server_cli/prototypes/nw_no_cust/.obsidian/workspace.json +166 -0
  20. apilogicserver-14.5.4.dist-info/METADATA +168 -0
  21. {apilogicserver-14.5.0.dist-info → apilogicserver-14.5.4.dist-info}/RECORD +25 -43
  22. {apilogicserver-14.5.0.dist-info → apilogicserver-14.5.4.dist-info}/WHEEL +1 -1
  23. api_logic_server_cli/prototypes/basic_demo/customizations/api/api_discovery/proper_update_def.json +0 -71
  24. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/1_langchain_loader.py +0 -71
  25. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/2_gpt_mcp_prompt.txt +0 -19
  26. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/multi_mcp_flow/multi_mcp_flow.png +0 -0
  27. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/multi_mcp_flow/multi_mcp_orchestration.yaml +0 -49
  28. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/multi_mcp_flow/wny mcp flows.png +0 -0
  29. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/natlang_to_api.py +0 -73
  30. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/curl.txt +0 -5
  31. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/images/MCP Overview.png +0 -0
  32. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/images/MCP_Arch.png +0 -0
  33. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/images/MCP_Overview_Executor.png +0 -0
  34. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/invoke_llm/1 - prompt_messages_array.json +0 -10
  35. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/invoke_llm/2 - completion_tool_context.json +0 -12
  36. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/llm_schema.txt +0 -38
  37. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/nw_swagger_2.yaml +0 -17393
  38. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/nw_swagger_3.yaml +0 -16660
  39. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/nw_swagger_3_relaxed.yaml +0 -109
  40. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/proxy_server.py +0 -51
  41. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/proxy_serverZ.py +0 -72
  42. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/validate_jsonapi.py +0 -64
  43. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/run_executor.py +0 -23
  44. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/swagger_converter.py +0 -65
  45. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/z_old/3_executor_test_agent.py +0 -52
  46. api_logic_server_cli/prototypes/manager/README_X.md +0 -663
  47. apilogicserver-14.5.0.dist-info/METADATA +0 -76
  48. {apilogicserver-14.5.0.dist-info → apilogicserver-14.5.4.dist-info}/entry_points.txt +0 -0
  49. {apilogicserver-14.5.0.dist-info → apilogicserver-14.5.4.dist-info}/licenses/LICENSE +0 -0
  50. {apilogicserver-14.5.0.dist-info → apilogicserver-14.5.4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,139 @@
1
+ import json
2
+ from sqlalchemy.ext.declarative import declarative_base
3
+ from sqlalchemy import Column, DECIMAL, Date, ForeignKey, Integer, String
4
+ from safrs import SAFRSBase
5
+ from flask_login import UserMixin
6
+ import safrs, flask_sqlalchemy
7
+ from safrs import jsonapi_attr
8
+ from flask_sqlalchemy import SQLAlchemy
9
+ from datetime import datetime
10
+ import operator
11
+ import json
12
+
13
+
14
+ Base = declarative_base() # type: flask_sqlalchemy.model.DefaultMeta
15
+ #vh new x
16
+ @classmethod
17
+ def jsonapi_filter(cls):
18
+ """
19
+ Use this to override SAFRS JSON:API filtering
20
+
21
+ Returns:
22
+ _type_: SQLAlchemy query filter
23
+ """
24
+ from sqlalchemy import text, or_, and_
25
+ from flask import request
26
+ expressions = []
27
+ sqlWhere = ""
28
+ query = cls._s_query
29
+ if args := request.args:
30
+ from api.system.expression_parser import advancedFilter
31
+ expressions, sqlWhere = advancedFilter(cls, args)
32
+ if sqlWhere != "":
33
+ return query.filter(text(sqlWhere))
34
+ else:
35
+ return query.filter(and_(*expressions))
36
+
37
+ class SAFRSBaseX(SAFRSBase, safrs.DB.Model):
38
+ __abstract__ = True
39
+ if do_enable_ont_advanced_filters := False:
40
+ jsonapi_filter = jsonapi_filter
41
+
42
+ def _s_parse_attr_value(self, attr_name: str, attr_val: any):
43
+ """
44
+ Parse the given jsonapi attribute value so it can be stored in the db
45
+ :param attr_name: attribute name
46
+ :param attr_val: attribute value
47
+ :return: parsed value
48
+ """
49
+ attr = self.__class__._s_jsonapi_attrs.get(attr_name, None)
50
+ if hasattr(attr, "type"): # pragma: no cover
51
+
52
+ if str(attr.type) in ["DATE", "DATETIME"] and attr_val:
53
+ try:
54
+ attr_val = attr_val.replace("T", " ")
55
+ datetime.strptime(attr_val, '%Y-%m-%d %H:%M')
56
+ attr_val += ":00"
57
+ except ValueError:
58
+ pass
59
+ except Exception as exc:
60
+ safrs.log.warning(exc)
61
+
62
+
63
+ return super()._s_parse_attr_value(attr_name, attr_val)
64
+
65
+
66
+ @classmethod
67
+ def _s_filter(cls, *filter_args, **filter_kwargs):
68
+ """
69
+ Apply a filter to this model
70
+ :param filter_args: A list of filters information to apply, passed as a request URL parameter.
71
+ Each filter object has the following fields:
72
+ - name: The name of the field you want to filter on.
73
+ - op: The operation you want to use (all sqlalchemy operations are available). The valid values are:
74
+ - like: Invoke SQL like (or "ilike", "match", "notilike")
75
+ - eq: check if field is equal to something
76
+ - ge: check if field is greater than or equal to something
77
+ - gt: check if field is greater than to something
78
+ - ne: check if field is not equal to something
79
+ - is_: check if field is a value
80
+ - is_not: check if field is not a value
81
+ - le: check if field is less than or equal to something
82
+ - lt: check if field is less than to something
83
+ - val: The value that you want to compare.
84
+ :return: sqla query object
85
+ """
86
+ try:
87
+ filters = json.loads(filter_args[0])
88
+ except json.decoder.JSONDecodeError:
89
+ raise ValidationError("Invalid filter format (see https://github.com/thomaxxl/safrs/wiki)")
90
+
91
+ if not isinstance(filters, list):
92
+ filters = [filters]
93
+
94
+ expressions = []
95
+ query = cls._s_query
96
+
97
+ for filt in filters:
98
+ if not isinstance(filt, dict):
99
+ safrs.log.warning(f"Invalid filter '{filt}'")
100
+ continue
101
+ attr_name = filt.get("name")
102
+ attr_val = filt.get("val")
103
+ if attr_name != "id" and attr_name not in cls._s_jsonapi_attrs:
104
+ raise ValidationError(f'Invalid filter "{filt}", unknown attribute "{attr_name}"')
105
+
106
+ op_name = filt.get("op", "").strip("_")
107
+ attr = cls._s_jsonapi_attrs[attr_name] if attr_name != "id" else cls.id
108
+ if op_name in ["in", "notin"]:
109
+ op = getattr(attr, op_name + "_")
110
+ query = query.filter(op(attr_val))
111
+ elif op_name in ["like", "ilike", "match", "notilike"] and hasattr(attr, "like"):
112
+ # => attr is Column or InstrumentedAttribute
113
+ like = getattr(attr, op_name)
114
+ expressions.append(like(attr_val))
115
+ elif not hasattr(operator, op_name):
116
+ raise ValidationError(f'Invalid filter "{filt}", unknown operator "{op_name}"')
117
+ else:
118
+ op = getattr(operator, op_name)
119
+ expressions.append(op(attr, attr_val))
120
+
121
+ if len(filters) > 1:
122
+ return query.filter(operator.and_(*expressions))
123
+ else:
124
+ return query.filter(*expressions)
125
+
126
+
127
+ class TestBase(Base):
128
+ __abstract__ = True
129
+ def __init__(self, *args, **kwargs):
130
+ for name, val in kwargs.items():
131
+ col = getattr(self.__class__, name)
132
+ if 'amount_total' == name:
133
+ debug_stop = 'stop'
134
+ if val is not None:
135
+ if str(col.type) in ["DATE", "DATETIME"]:
136
+ pass
137
+ else:
138
+ kwargs[name] = col.type.python_type(val)
139
+ return super().__init__(*args, **kwargs)
@@ -10,4 +10,6 @@ Model Context Protocol is a way for:
10
10
 
11
11
  * ***Corporate database participation*** in such flows, by making key functions available as MCP calls.
12
12
 
13
- This example is [explained here](https://apilogicserver.github.io/Docs/Integration-MCP/).
13
+ This example is [explained here](https://apilogicserver.github.io/Docs/Integration-MCP/).
14
+
15
+ > Note: this sample uses multi-term filters. These are usually OR'd together, but this example requires AND. This is provided by `database/system/SAFRSBaseX.py` (see `return query.filter(operator.and_(*expressions)`) in `_s_filter()`), activated in `config/server_setup.py`.
@@ -10,16 +10,11 @@ Notes:
10
10
  * See: integration/mcp/README_mcp.md
11
11
  * python api_logic_server_run.py
12
12
 
13
- ToDo - email example is incomplete:
14
- 1. Add email event handler (ala nw_sample/logic/declare_logic.py#send_n8n_message())
15
- 2. And, respect the customer email_opt_out
16
- 3. Needs to use date range
17
- 4. Data incomplete
18
-
19
13
  """
20
14
 
21
15
  import json
22
16
  import os
17
+ import re
23
18
  import openai
24
19
  import requests
25
20
 
@@ -83,30 +78,20 @@ def discover_mcp_servers():
83
78
 
84
79
 
85
80
  def get_user_nl_query():
86
- """ Get the natural language query from the user. """
81
+ """ Get the natural language query from the user.
82
+ Add instructions for the LLM to generate a tool context block.
83
+
84
+ """
87
85
 
88
86
  global test_type
89
- # this doesn't work -- missing commands for mcp_server_executor....
90
- default_request = "List the orders created more than 30 days ago, and post an email message to the order's customer offering a discount"
91
-
92
- default_request = "List the orders created more than 30 days ago, and send a discount email to the customer for each one."
93
- # date range? curl -X GET "http://localhost:5656/api/Order?filter=[{\’name\'}: {\’CreatedOn\’}, {\’op\’}: {\’gt\’}, {\’val\’}: {\’2022-05-14\’}]”
94
-
95
- default_request = "List the orders for customer 5, and send a discount email to the customer for each one."
96
87
 
97
- if test_type != 'orchestration':
98
- default_request = "List customers with credit over 1000"
99
-
100
- query = sys.argv[1] if len(sys.argv) > 1 else default_request
101
-
102
- query += """
103
- Respond with a JSON array of tool context blocks using:
104
- - tool: 'json-api'
105
- - JSON:API-compliant filtering (e.g., filter[CreatedOn][lt])
106
- - Use {{ order.customer_id }} as a placeholder in the second step.
107
- - Include method, url, query_params or body, headers, expected_output.
108
- """
109
- return query
88
+ if len(sys.argv) > 1 and sys.argv[1] != 'go':
89
+ request = sys.argv[1]
90
+ else:
91
+ request = "List the unshipped orders created before 2023-07-14, and send a discount email to the customer for each one."
92
+ if test_type != 'orchestration':
93
+ request = "List customers with credit over 1000"
94
+ return request
110
95
 
111
96
 
112
97
  def query_llm_with_nl(nl_query):
@@ -131,53 +116,63 @@ def query_llm_with_nl(nl_query):
131
116
 
132
117
  # setup default tool_context to bypass LLM call and save 2-3 secs in testing
133
118
  if test_type == 'orchestration': # orchestration: emails to pending orders
119
+
134
120
  tool_context = \
135
- [
136
- {
137
- "tool": "json-api",
138
- "method": "GET",
139
- "url": "http://localhost:5656/api/Order",
140
- "query_params": {
141
- "filter[customer_id]": 5
142
- },
143
- "headers": {
144
- "Content-Type": "application/vnd.api+json"
145
- },
146
- "expected_output": "JSON array of orders for customer 5"
147
- },
148
- {
149
- "tool": "email",
150
- "method": "POST",
151
- "url": "http://localhost:5656/api/Email",
152
- "body": {
153
- "to": "{{ order.customer_id }}",
154
- "subject": "Discount Offer",
155
- "message": "Dear Customer, We are offering a discount on your recent orders. Please check your account for more details."
121
+ {
122
+ "tool_type": "json-api",
123
+ "schema_version": "1.0",
124
+ "base_url": "http://localhost:5656/api",
125
+ "resources": [
126
+ {
127
+ "path": "/Order",
128
+ "method": "GET",
129
+ "query_params": [
130
+ {
131
+ "name": "date_shipped",
132
+ "op": "eq",
133
+ "val": None
134
+ },
135
+ {
136
+ "name": "date_created",
137
+ "op": "lt",
138
+ "val": "2023-07-14"
139
+ }
140
+ ],
141
+ "headers": [],
142
+ "expected_output": []
156
143
  },
157
- "headers": {
158
- "Content-Type": "application/json"
159
- },
160
- "expected_output": "Email sent confirmation"
161
- }
162
- ]
163
- else: # simple get request - list customers with credit over 4000
144
+ {
145
+ "path": "/Email",
146
+ "method": "POST",
147
+ "body": {
148
+ "customer_id": "{{ order.customer_id }}",
149
+ "message": "You have a discount on your unshipped order."
150
+ },
151
+ "headers": [],
152
+ "expected_output": []
153
+ }
154
+ ]
155
+ }
156
+ else: # simple get request - list customers with credit over 1000
164
157
  tool_context = \
165
- [
166
- {
167
- "tool": "json-api",
168
- "method": "GET",
169
- "url": "http://localhost:5656/api/Customer",
170
- "query_params": {
171
- "filter[credit_limit][gt]": 1000
172
- },
173
- "headers": {
174
- "Content-Type": "application/vnd.api+json"
175
- },
176
- "expected_output": "JSON array of customers with credit limit over 1000"
177
- }
178
- ]
179
-
180
- # Call the OpenAI API to generate the tool context
158
+ {
159
+ "tool_type": "json-api",
160
+ "schema_version": "1.0",
161
+ "base_url": "http://localhost:5656/api",
162
+ "resources": [
163
+ {
164
+ "path": "/Customer",
165
+ "method": "GET",
166
+ "query_params": [
167
+ {
168
+ "name": "credit_limit",
169
+ "op": "gt",
170
+ "val": "1000"
171
+ }
172
+ ]
173
+ }
174
+ ]
175
+ } # Call the OpenAI API to generate the tool context
181
176
  if create_tool_context_from_llm: # saves 2-3 seconds...
182
177
  response = openai.chat.completions.create(
183
178
  model="gpt-4",
@@ -186,13 +181,17 @@ def query_llm_with_nl(nl_query):
186
181
  )
187
182
 
188
183
  tool_context_str = response.choices[0].message.content
184
+ tool_context_str_no_cr = tool_context_str.replace("\n", '') # convert single quotes to double quotes
189
185
  try:
190
- tool_context = json.loads(tool_context_str)
186
+ tool_context = json.loads(tool_context_str_no_cr)
191
187
  except json.JSONDecodeError:
192
188
  print("Failed to decode JSON from response:", tool_context_str)
193
189
  return None
194
190
 
195
- print("\n2. generated tool context from LLM:\n", json.dumps(tool_context, indent=4))
191
+ print("\n\n2a. LLM request:\n", json.dumps(messages, indent=4))
192
+ print("\n2b. NL Query:\n", nl_query)
193
+ print("\n2c. schema_text: \n", json.dumps(json.loads(schema_text), indent=4)) #schema_text[0:100])
194
+ print("\n2d. generated tool context from LLM:\n", json.dumps(tool_context, indent=4))
196
195
  return tool_context
197
196
 
198
197
 
@@ -213,71 +212,85 @@ def process_tool_context(tool_context):
213
212
 
214
213
  def get_query_param_filter(query_params):
215
214
  """ return json:api filter
215
+
216
+ eg
217
+ curl -qg 'http://localhost:5656/api/Order?filter=[{"name":"date_shipped","op":"eq","val":null},{"name":"CreatedOn","op":"lt","val":"2023-07-14"}]'
218
+
219
+ curl -qg 'http://localhost:5656/api/Order?filter=[{"name":"date_shipped","op":"gt","val":"2023-07-14"}]'
220
+ curl -qg 'http://localhost:5656/api/Order?filter=[{"name":"date_shipped","op":"eq","val":null}]'
221
+ curl -qg 'http://localhost:5656/api/Customer?filter=[{"name":"credit_limit","op":"gt","val":"1000"}]'
216
222
 
217
- query_params might be:
218
- "query_params": {
219
- "filter[credit_limit][gt]": 1000 }
220
- or:
221
- "query_params": {
222
- "filter[customer_id]": 5},
223
+ query_params might be simple:
224
+ "query_params": [ {"name": "credit_limit", "op": "gt", "val": "1000"} ]
225
+ ==> ?filter=[{"name":"credit_limit","op":"gt","val":"1000"}]
226
+
227
+ or a list:
228
+ "query_params": [
229
+ {
230
+ "name": "date_shipped",
231
+ "op": "eq",
232
+ "val": None
233
+ },
234
+ {
235
+ "name": "date_created",
236
+ "op": "lt",
237
+ "val": "2023-07-14"
238
+ }
239
+ ],
223
240
 
224
241
  """
225
- query_param_filter = ''
226
- if isinstance(query_params, dict):
227
- for each_key, each_value in query_params.items():
228
- if isinstance(each_value, dict):
229
- for sub_key, sub_value in each_value.items():
230
- query_param_filter += f"&{each_key}[{sub_key}]={sub_value}"
231
- else:
232
- query_param_filter += f"&{each_key}={each_value}"
233
- # query_params = ''
234
- elif isinstance(query_params, dict):
235
- assert False, "Query Params dict tbd"
236
- return query_param_filter
237
-
238
-
239
- if isinstance(tool_context, dict):
240
- query_params = tool_context["query_params"]
241
- query_param_filter = get_query_param_filter(query_params)
242
- mcp_response = requests.get(
243
- tool_context["url"],
244
- headers=tool_context["headers"],
245
- params=query_param_filter
246
- )
247
- elif isinstance(tool_context, list):
248
- context_data = {}
242
+
249
243
  added_rows = 0
250
244
 
251
- for each_block in tool_context:
252
- if each_block["tool"] in ["json-api", "email"]:
253
- if each_block["method"] == "GET":
254
- query_param_filter = get_query_param_filter(each_block["query_params"])
255
- mcp_response = requests.get(
256
- url = each_block["url"],
257
- headers=each_block["headers"],
258
- params=query_param_filter
245
+ query_param_filter = ''
246
+ assert isinstance(query_params, list), "Query Params filter expected to be a list"
247
+ query_param_filter = 'filter=' + str(query_params)
248
+ # use urlencode to convert to JSON:API format...
249
+ # val urllib.parse.quote() or urllib.parse.urlencode()
250
+ # tool instructions... filtering, email etc
251
+ query_param_filter = query_param_filter.replace("'", '"') # convert single quotes to double quotes
252
+ query_param_filter = query_param_filter.replace("None", 'null')
253
+ query_param_filter = query_param_filter.replace("date_created", 'CreatedOn') # TODO - why this name?
254
+ return query_param_filter # end get_query_param_filter
255
+
256
+ assert isinstance(tool_context, (dict, list)), "Tool context expected to be a dictionary"
257
+ context_data = {}
258
+ added_rows = 0
259
+
260
+ for each_block in tool_context["resources"]:
261
+ if True: # TODO - add check for "tool": "json-api"
262
+ if each_block["method"] == "GET":
263
+ query_param_filter = get_query_param_filter(each_block["query_params"])
264
+ headers = {"Content-Type": "application/vnd.api+json"}
265
+ if "headers" in each_block:
266
+ headers.update(each_block["headers"])
267
+ mcp_response = requests.get(
268
+ url = tool_context["base_url"] + each_block["path"],
269
+ headers=headers,
270
+ params=query_param_filter
271
+ )
272
+ context_data = mcp_response.json()['data'] # result rows...
273
+ elif each_block["method"] in ["POST"]:
274
+ for each_order in context_data:
275
+ url = tool_context["base_url"] + each_block["path"]
276
+ json_update_data = { 'data': {"type": "Email", 'attributes': {} } }
277
+ json_update_data_attributes = json_update_data["data"]["attributes"]
278
+ json_update_data_attributes["customer_id"] = context_data[0]['attributes']["customer_id"] # TODO - fix
279
+ json_update_data_attributes["message"] = each_block["body"]["message"]
280
+ # eg: POST http://localhost:5656/api/Email {'data': {'type': 'Email', 'attributes': {'customer_id': 5, 'message': {'to': '{{ order.customer_id }}', 'subject': 'Discount for your order', 'body': 'Dear customer, you have a discount for your recent order. Thank you for shopping with us.'}}}}
281
+ headers = {"Content-Type": "application/vnd.api+json"}
282
+ if "headers" in each_block:
283
+ headers.update(each_block["headers"])
284
+ mcp_response = requests.post(
285
+ url=url,
286
+ headers=headers,
287
+ json=json_update_data
259
288
  )
260
- context_data = mcp_response.json()['data'] # result rows...
261
- elif each_block["method"] in ["POST"]:
262
- add_rows = 0
263
- for each_order in context_data:
264
- url = each_block["url"]
265
- json_update_data = { 'data': {"type": "Email", 'attributes': {} } }
266
- json_update_data_attributes = json_update_data["data"]["attributes"]
267
- json_update_data_attributes["customer_id"] = context_data[0]['attributes']["customer_id"]
268
- json_update_data_attributes["message"] = each_block["body"]["message"]
269
- # eg: POST http://localhost:5656/api/Email {'data': {'type': 'Email', 'attributes': {'customer_id': 5, 'message': {'to': '{{ order.customer_id }}', 'subject': 'Discount for your order', 'body': 'Dear customer, you have a discount for your recent order. Thank you for shopping with us.'}}}}
270
- mcp_response = requests.post(
271
- url=url,
272
- headers=each_block["headers"],
273
- json=json_update_data
274
- )
275
- add_rows += 1
276
- pass
277
- else:
278
- print("Invalid tool context format. Expected a dictionary or a list.")
279
- return None
289
+ added_rows += 1
290
+ pass
280
291
  print("\n3. MCP Server (als) Response:\n", mcp_response.text)
292
+ if added_rows > 0:
293
+ print(f"...Added {added_rows} rows to the database; last row (only) shown above.")
281
294
  return mcp_response
282
295
 
283
296
 
@@ -9,6 +9,7 @@ import api.system.opt_locking.opt_locking as opt_locking
9
9
  from security.system.authorization import Grant, Security
10
10
  from logic.load_verify_rules import load_verify_rules
11
11
  import integration.kafka.kafka_producer as kafka_producer
12
+ from integration.n8n.n8n_producer import send_n8n_message
12
13
  import logging
13
14
 
14
15
  app_logger = logging.getLogger(__name__)
@@ -32,7 +33,7 @@ def declare_logic():
32
33
  discover_logic()
33
34
 
34
35
  # Logic from GenAI: (or, use your IDE w/ code completion)
35
- from database.models import Product, Order, Item, Customer
36
+ from database.models import Product, Order, Item, Customer, Email
36
37
 
37
38
  # Ensure the customer's balance is less than their credit limit
38
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})")
@@ -54,6 +55,25 @@ def declare_logic():
54
55
 
55
56
  # End Logic from GenAI
56
57
 
58
+ def send_mail(row: Email, old_row: Email, logic_row: LogicRow):
59
+ """
60
+
61
+ #als: Send N8N email message (also see discovery/integration.py)
62
+
63
+ Args:
64
+ row (Email): inserted Email
65
+ old_row (Email): n/a
66
+ logic_row (LogicRow): bundles curr/old row, with ins/upd/dlt logic
67
+ """
68
+ if logic_row.is_inserted():
69
+ customer = row.customer # parent accessor
70
+ if customer.email_opt_out:
71
+ logic_row.log("customer opted out of email")
72
+ return
73
+ logic_row.log(f"send email {row.message} to {customer.email} (stub, eg use N8N") # see in log
74
+
75
+ Rule.after_flush_row_event(on_class=Email, calling=send_mail) # see above
76
+
57
77
 
58
78
  def handle_all(logic_row: LogicRow): # #als: TIME / DATE STAMPING, OPTIMISTIC LOCKING
59
79
  """
@@ -77,7 +97,7 @@ def declare_logic():
77
97
  Grant.process_updates(logic_row=logic_row)
78
98
 
79
99
  did_stamping = False
80
- if enable_stamping := False: # #als: DATE / USER STAMPING
100
+ if enable_stamping := True: # #als: DATE / USER STAMPING
81
101
  row = logic_row.row
82
102
  if logic_row.ins_upd_dlt == "ins" and hasattr(row, "CreatedOn"):
83
103
  row.CreatedOn = datetime.datetime.now()
@@ -119,7 +119,7 @@ def declare_logic():
119
119
  Grant.process_updates(logic_row=logic_row)
120
120
 
121
121
  did_stamping = False
122
- if enable_stamping := False: # #als: DATE / USER STAMPING
122
+ if enable_stamping := True: # #als: DATE / USER STAMPING
123
123
  row = logic_row.row
124
124
  if logic_row.ins_upd_dlt == "ins" and hasattr(row, "CreatedOn"):
125
125
  row.CreatedOn = datetime.datetime.now()
@@ -148,8 +148,8 @@ def declare_logic():
148
148
  "Order Date": row.OrderDate,
149
149
  #"items": [row.OrderDetailList]
150
150
  },
151
- ins_upd_dlt="upd", wh_entity="Order",
152
- msg="1. /Webhook integration.py: n8n, sending ready Order payload")
151
+ ins_upd_dlt="upd", wh_entity="Order",
152
+ msg="1. /Webhook integration.py: n8n, sending ready Order payload")
153
153
 
154
154
  logic_row.log("send_order_to_shipping - sent order to shipping and N8N/sendgrid") # see in log
155
155
 
@@ -0,0 +1,31 @@
1
+ {
2
+ "file-explorer": true,
3
+ "global-search": true,
4
+ "switcher": true,
5
+ "graph": true,
6
+ "backlink": true,
7
+ "canvas": true,
8
+ "outgoing-link": true,
9
+ "tag-pane": true,
10
+ "properties": false,
11
+ "page-preview": true,
12
+ "daily-notes": true,
13
+ "templates": true,
14
+ "note-composer": true,
15
+ "command-palette": true,
16
+ "slash-command": false,
17
+ "editor-status": true,
18
+ "bookmarks": true,
19
+ "markdown-importer": false,
20
+ "zk-prefixer": false,
21
+ "random-note": false,
22
+ "outline": true,
23
+ "word-count": true,
24
+ "slides": false,
25
+ "audio-recorder": false,
26
+ "workspaces": false,
27
+ "file-recovery": true,
28
+ "publish": false,
29
+ "sync": true,
30
+ "webviewer": false
31
+ }