ApiLogicServer 14.4.0__py3-none-any.whl → 14.5.3__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.
- api_logic_server_cli/add_cust/add_cust.py +269 -0
- api_logic_server_cli/api_logic_server.py +18 -238
- api_logic_server_cli/api_logic_server_info.yaml +3 -3
- api_logic_server_cli/cli.py +38 -28
- api_logic_server_cli/create_from_model/__pycache__/api_logic_server_utils.cpython-312.pyc +0 -0
- api_logic_server_cli/create_from_model/__pycache__/dbml.cpython-312.pyc +0 -0
- api_logic_server_cli/create_from_model/__pycache__/ont_build.cpython-312.pyc +0 -0
- api_logic_server_cli/create_from_model/__pycache__/ont_create.cpython-312.pyc +0 -0
- api_logic_server_cli/create_from_model/api_logic_server_utils.py +47 -0
- api_logic_server_cli/create_from_model/dbml.py +113 -58
- api_logic_server_cli/create_from_model/ont_build.py +83 -60
- api_logic_server_cli/create_from_model/ont_create.py +2 -1
- api_logic_server_cli/database/basic_demo.sqlite +0 -0
- api_logic_server_cli/database/basic_demo.txt +1 -0
- api_logic_server_cli/database/basic_demo_wg.sqlite +0 -0
- api_logic_server_cli/manager.py +3 -2
- api_logic_server_cli/prototypes/base/.vscode/launch.json +3 -2
- api_logic_server_cli/prototypes/base/config/config.py +66 -11
- api_logic_server_cli/prototypes/base/config/default.env +7 -1
- api_logic_server_cli/prototypes/base/database/test_data/readme.md +2 -1
- api_logic_server_cli/prototypes/base/integration/kafka/kafka_producer.py +5 -2
- api_logic_server_cli/prototypes/base/integration/n8n/n8n_producer.py +68 -21
- api_logic_server_cli/prototypes/base/integration/n8n/n8n_readme.md +19 -0
- api_logic_server_cli/prototypes/base/test/basic/server_test.py +1 -1
- api_logic_server_cli/prototypes/basic_demo/README.md +29 -52
- api_logic_server_cli/prototypes/basic_demo/customizations/api/.DS_Store +0 -0
- api_logic_server_cli/prototypes/basic_demo/customizations/api/api_discovery/mcp_discovery.py +139 -0
- api_logic_server_cli/prototypes/basic_demo/customizations/api/api_discovery/openapi.py +92 -0
- api_logic_server_cli/prototypes/basic_demo/customizations/config/default.env +13 -0
- api_logic_server_cli/prototypes/basic_demo/customizations/config/server_setup.py +388 -0
- api_logic_server_cli/prototypes/basic_demo/customizations/database/db.sqlite +0 -0
- api_logic_server_cli/prototypes/basic_demo/customizations/database/models.py +131 -0
- api_logic_server_cli/prototypes/basic_demo/customizations/database/system/SAFRSBaseX.py +136 -0
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/.DS_Store +0 -0
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/.DS_Store +0 -0
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/README_mcp.md +15 -0
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/mcp_client_executor.py +350 -0
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/mcp_schema.txt +47 -0
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/mcp_server_discovery.json +9 -0
- api_logic_server_cli/prototypes/{nw_no_cust/integration/mcp → basic_demo/customizations/integration/openai_function}/3_executor_test_agent.py +20 -6
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/openai_function/README_functon.md +201 -0
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/openai_function/ai_plugin.json +17 -0
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/openai_function/nw-swagger_3.json +1731 -0
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/openai_function/snippets.txt +5 -0
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/openai_function/swagger_3 genai_demo_with_get.json +1731 -0
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/openai_function/swagger_3.json +1782 -0
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/openai_function/swagger_3_genai_demo.json +264 -0
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/openai_function/swagger_3_genai_demo_with_update.json +1782 -0
- api_logic_server_cli/prototypes/basic_demo/customizations/logic/declare_logic.py +79 -41
- api_logic_server_cli/prototypes/basic_demo/customizations/security/declare_security.py +11 -12
- api_logic_server_cli/prototypes/basic_demo/customizations/ui/admin/admin.yaml +166 -0
- api_logic_server_cli/prototypes/basic_demo/iteration/api/{customize_api.py → api_discovery/order_b2b.py} +17 -23
- api_logic_server_cli/prototypes/basic_demo/iteration/database/db.sqlite +0 -0
- api_logic_server_cli/prototypes/basic_demo/iteration/integration/row_dict_maps/OrderB2B.py +6 -5
- api_logic_server_cli/prototypes/basic_demo/iteration/integration/row_dict_maps/OrderShipping.py +4 -4
- api_logic_server_cli/prototypes/basic_demo/iteration/logic/declare_logic.py +69 -43
- api_logic_server_cli/prototypes/basic_demo/iteration/ui/admin/admin.yaml +125 -50
- api_logic_server_cli/prototypes/manager/README.md +4 -0
- api_logic_server_cli/prototypes/nw/logic/declare_logic.py +2 -2
- api_logic_server_cli/prototypes/nw_no_cust/.obsidian/app.json +1 -0
- api_logic_server_cli/prototypes/nw_no_cust/.obsidian/appearance.json +1 -0
- api_logic_server_cli/prototypes/nw_no_cust/.obsidian/core-plugins.json +31 -0
- api_logic_server_cli/prototypes/nw_no_cust/.obsidian/workspace.json +166 -0
- api_logic_server_cli/prototypes/nw_no_cust/Tutorial.md +45 -26
- api_logic_server_cli/prototypes/nw_no_cust/api/api_discovery/openapi.py +130 -0
- api_logic_server_cli/prototypes/nw_no_cust/api/api_discovery/proper_update_def.json +71 -0
- api_logic_server_cli/prototypes/nw_no_cust/config/default.env +13 -0
- api_logic_server_cli/prototypes/ont_app/ontimize_seed/package-lock.json +9725 -1180
- api_logic_server_cli/prototypes/ont_app/ontimize_seed/package.json +3 -6
- api_logic_server_cli/prototypes/ont_app/ontimize_seed/src/app/shared/app.services.config.ts +1 -1
- api_logic_server_cli/prototypes/ont_app/ontimize_seed/src/assets/css/app.scss +4 -0
- api_logic_server_cli/prototypes/ont_app/ontimize_seed/src/assets/i18n/en.json +1 -1
- api_logic_server_cli/prototypes/ont_app/ontimize_seed/src/assets/i18n/es.json +14 -12
- api_logic_server_cli/prototypes/ont_app/templates/app_config.jinja +1 -1
- api_logic_server_cli/prototypes/ont_app/templates/date_template.html +1 -1
- api_logic_server_cli/prototypes/ont_app/templates/textarea_template.html +1 -1
- api_logic_server_cli/prototypes/ont_app/templates/timestamp_template.html +1 -1
- api_logic_server_cli/prototypes/sample_ai/logic/declare_logic.py +30 -13
- apilogicserver-14.5.3.dist-info/METADATA +168 -0
- {apilogicserver-14.4.0.dist-info → apilogicserver-14.5.3.dist-info}/RECORD +84 -61
- {apilogicserver-14.4.0.dist-info → apilogicserver-14.5.3.dist-info}/WHEEL +1 -1
- api_logic_server_cli/prototypes/basic_demo/apply_customizations.ps1 +0 -17
- api_logic_server_cli/prototypes/basic_demo/apply_customizations.sh +0 -14
- api_logic_server_cli/prototypes/basic_demo/apply_iteration.ps1 +0 -20
- api_logic_server_cli/prototypes/basic_demo/apply_iteration.sh +0 -15
- api_logic_server_cli/prototypes/nw_no_cust/integration/mcp/1_langchain_loader.py +0 -19
- api_logic_server_cli/prototypes/nw_no_cust/integration/mcp/2_gpt_mcp_prompt.txt +0 -19
- api_logic_server_cli/prototypes/nw_no_cust/integration/mcp/README.md +0 -17
- api_logic_server_cli/prototypes/nw_no_cust/integration/mcp/resources/curl.txt +0 -4
- api_logic_server_cli/prototypes/nw_no_cust/integration/mcp/resources/nw_swagger_3.yaml +0 -16660
- api_logic_server_cli/prototypes/nw_no_cust/integration/mcp/run_executor.py +0 -23
- apilogicserver-14.4.0.dist-info/METADATA +0 -76
- {apilogicserver-14.4.0.dist-info → apilogicserver-14.5.3.dist-info}/entry_points.txt +0 -0
- {apilogicserver-14.4.0.dist-info → apilogicserver-14.5.3.dist-info}/licenses/LICENSE +0 -0
- {apilogicserver-14.4.0.dist-info → apilogicserver-14.5.3.dist-info}/top_level.txt +0 -0
|
@@ -19,7 +19,18 @@ from integration.system.RowDictMapper import RowDictMapper
|
|
|
19
19
|
from flask import jsonify
|
|
20
20
|
import api.system.api_utils as api_utils
|
|
21
21
|
from config.config import Args
|
|
22
|
-
|
|
22
|
+
'''
|
|
23
|
+
wh_scheme = "http"
|
|
24
|
+
wh_server = "localhost" # or cloud.n8n.io...
|
|
25
|
+
wh_port = 5678
|
|
26
|
+
wh_endpoint = "webhook-test"
|
|
27
|
+
wh_path = "002fa0e8-f7aa-4e04-b4e3-e81aa29c6e69"
|
|
28
|
+
token = "YWRtaW46cA==" # Basic auth for n8n
|
|
29
|
+
N8N_PRODUCER = {"authorization": f"Basic {token}", "n8n_url": f'"{wh_scheme}://{wh_server}:{wh_port}/{wh_endpoint}/{wh_path}"'}
|
|
30
|
+
# Or enter the n8n_url directly:
|
|
31
|
+
N8N_PRODUCER = {"authorization": f"Basic {token}","n8n_url":"http://localhost:5678/webhook-test/002fa0e8-f7aa-4e04-b4e3-e81aa29c6e69"}
|
|
32
|
+
|
|
33
|
+
'''
|
|
23
34
|
producer = None
|
|
24
35
|
""" connected producer (or null if N8N not enabled in Config.py) """
|
|
25
36
|
|
|
@@ -31,27 +42,60 @@ logger.debug("n8n_connect imported")
|
|
|
31
42
|
|
|
32
43
|
def n8n_producer():
|
|
33
44
|
"""
|
|
34
|
-
Called by api_logic_server_run>server_setup to listen on kafka using confluent_kafka
|
|
35
|
-
|
|
36
|
-
Enabled by config.KAFKA_CONNECT (dict, of bootstrap.servers, client.id)
|
|
37
45
|
|
|
46
|
+
Enabled by config (dict, of bootstrap.servers, client.id)
|
|
47
|
+
OR
|
|
48
|
+
token = getenv("N8N_TOKEN", Args.instance.wh_token) #basic auth
|
|
49
|
+
scheme = getenv("N8N_SCHEME",Args.instance.wh_scheme) # http or https
|
|
50
|
+
server = getenv("N8N_SERVER", Args.instance.wh_server)
|
|
51
|
+
port = getenv("N8N_PORT",Args.instance.wh_port)
|
|
52
|
+
endpoint = getenv("N8N_ENDPOINT", wh_endpoint) # this can change for different workflows
|
|
53
|
+
path = getenv("N8N_PATH", Args.instance.wh_path)
|
|
54
|
+
conf = getenv("N8N_PRODUCER", Args.instance.n8n_producer)
|
|
38
55
|
Args:
|
|
39
56
|
none
|
|
40
57
|
"""
|
|
58
|
+
|
|
59
|
+
producer = configure_n8n_producer()
|
|
41
60
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
producer = conf
|
|
46
|
-
# good place to do defaults, get api keys, etc
|
|
47
|
-
logger.debug('N8N producer initialized')
|
|
61
|
+
def configure_n8n_producer(wh_endpoint: str = None, wh_path: str = None) -> dict:
|
|
62
|
+
"""
|
|
63
|
+
Need to be able to change the endpoint for different workflows and webhooks
|
|
48
64
|
|
|
65
|
+
Args:
|
|
66
|
+
wh_endpoint (str, optional): _description_. Defaults to None.
|
|
49
67
|
|
|
50
|
-
|
|
68
|
+
Returns:
|
|
69
|
+
dict: _description_
|
|
70
|
+
"""
|
|
71
|
+
global conf
|
|
72
|
+
conf = None
|
|
73
|
+
from os import getenv
|
|
74
|
+
endpoint = wh_endpoint if wh_endpoint is not None else Args.instance.wh_endpoint
|
|
75
|
+
n8n_path = wh_path if wh_path is not None else Args.instance.wh_path
|
|
76
|
+
token = getenv("N8N_TOKEN", Args.instance.wh_token)
|
|
77
|
+
scheme = getenv("N8N_SCHEME",Args.instance.wh_scheme)
|
|
78
|
+
server = getenv("N8N_SERVER", Args.instance.wh_server)
|
|
79
|
+
port = getenv("N8N_PORT",Args.instance.wh_port)
|
|
80
|
+
endpoint = getenv("N8N_ENDPOINT", endpoint)
|
|
81
|
+
path = getenv("N8N_PATH", n8n_path)
|
|
82
|
+
if token is None or scheme is None or server is None or port is None or path is None:
|
|
83
|
+
logger.debug('N8N producer not configured in config/Config.py or environment variables')
|
|
84
|
+
conf = getenv("N8N_PRODUCER", Args.instance.n8n_producer)
|
|
85
|
+
else:
|
|
86
|
+
conf = {"authorization": f"Basic {token}", "n8n_url": f'"{scheme}://{server}:{port}/{wh_endpoint if wh_endpoint is not None else endpoint}/{path}"'}
|
|
87
|
+
|
|
88
|
+
return conf
|
|
89
|
+
|
|
90
|
+
def send_n8n_message(http_method: str = "POST",
|
|
91
|
+
ins_upd_dlt: str = "ins",
|
|
92
|
+
msg: str = "",
|
|
51
93
|
wh_entity: str = "unknown",
|
|
52
94
|
logic_row: LogicRow = None,
|
|
53
95
|
row_dict_mapper: RowDictMapper = None,
|
|
54
|
-
payload: dict = None
|
|
96
|
+
payload: dict = None,
|
|
97
|
+
wh_endpoint: str = None,
|
|
98
|
+
wh_path: str= None) -> str:
|
|
55
99
|
""" Send N8N webhook message regarding [logic_row, mapped by row_dict_mapper or by payload]
|
|
56
100
|
|
|
57
101
|
* Typically called from declare_logic event
|
|
@@ -64,13 +108,16 @@ def send_n8n_message(http_method: str = "POST", ins_upd_dlt: str = "ins", msg: s
|
|
|
64
108
|
row_dict_mapper (RowDictMapper): (Optional) typically subclass of RowDictMapper, transforms row to dict
|
|
65
109
|
payload (str): (Optional) JSON data to be sent as string (json.dumps(row.to_dict()))
|
|
66
110
|
wh_entity (str): the webhook entity name pass in header
|
|
111
|
+
wh_endpoint (str): (Optional) override the webhook endpoint name modify the producer
|
|
112
|
+
wh_path (str): (Optional) override the webhook path name modify the producer
|
|
67
113
|
"""
|
|
68
114
|
|
|
69
|
-
global conf
|
|
70
|
-
if conf is None:
|
|
71
|
-
|
|
115
|
+
#global conf
|
|
116
|
+
#if conf is None:
|
|
117
|
+
# conf = Args.instance.n8n_producer #FIXME not sure why this fails - conf is None
|
|
72
118
|
#return "N8N not enabled in Config.py"
|
|
73
|
-
|
|
119
|
+
conf = configure_n8n_producer(wh_endpoint, wh_path)
|
|
120
|
+
logger.debug(f"n8n_producer: conf: {conf}")
|
|
74
121
|
if row_dict_mapper is None and payload is None and logic_row is None:
|
|
75
122
|
return "send_n8n_message: payload, logic_row, row_dict_mapper are all None - must provide one"
|
|
76
123
|
row_obj_dict = None
|
|
@@ -96,17 +143,18 @@ def send_n8n_message(http_method: str = "POST", ins_upd_dlt: str = "ins", msg: s
|
|
|
96
143
|
status = {"status_code": 500}
|
|
97
144
|
|
|
98
145
|
if conf is None:
|
|
99
|
-
logger.debug(f"n8n_producer: not configured in config/Config.py")
|
|
146
|
+
logger.debug(f"n8n_producer.py: N8N not configured in config/Config.py or environment variables")
|
|
100
147
|
else:
|
|
101
148
|
headers = {
|
|
102
149
|
"Authorization": conf['authorization'],
|
|
103
150
|
"Content-Type": "application/json",
|
|
104
151
|
"wh_state": wh_state,
|
|
105
|
-
"wh_entity": wh_entity
|
|
152
|
+
"wh_entity": wh_entity,
|
|
153
|
+
"wh_source": "api_logic_server",
|
|
106
154
|
}
|
|
107
155
|
endpoint = f'{conf["n8n_url"]}'
|
|
108
156
|
status = {"status_code": 500}
|
|
109
|
-
if http_method
|
|
157
|
+
if http_method.lower() == "post":
|
|
110
158
|
#Only passing payload in this example
|
|
111
159
|
status = requests.post(endpoint, json=row_obj_dict, headers=headers)
|
|
112
160
|
elif http_method.lower() == "get":
|
|
@@ -121,5 +169,4 @@ def send_n8n_message(http_method: str = "POST", ins_upd_dlt: str = "ins", msg: s
|
|
|
121
169
|
logger.error(f"\nn8n_producer fails with: {e}")
|
|
122
170
|
long_message = traceback.format_exc()
|
|
123
171
|
logger.error(long_message)
|
|
124
|
-
return long_message
|
|
125
|
-
|
|
172
|
+
return long_message
|
|
@@ -45,4 +45,23 @@ in the workflow (e.g. SendGrid email)
|
|
|
45
45
|
logic_row.debug(status)
|
|
46
46
|
|
|
47
47
|
Rule.after_flush_row_event(on_class=models.Customer, calling=call_n8n_workflow)
|
|
48
|
+
```
|
|
49
|
+
## Environment override
|
|
50
|
+
You can override the configuration of the N8N_PRODUCER or the components used to build the producer.
|
|
51
|
+
```
|
|
52
|
+
# source n8n.env
|
|
53
|
+
# to configure your n8n webhook
|
|
54
|
+
export N8N_TOKEN=YWRtaW46cA==
|
|
55
|
+
export N8N_SCHEME=http
|
|
56
|
+
export N8N_SERVER=localhost
|
|
57
|
+
export N8N_PORT=5678
|
|
58
|
+
# each webhook may have a unique name
|
|
59
|
+
export N8N_ENDPOINT=webhook_test
|
|
60
|
+
|
|
61
|
+
# the path will change between dev and production
|
|
62
|
+
export N8N_PATH=002fa0e8-f7aa-4e04-b4e3-e81aa29c6e69
|
|
63
|
+
|
|
64
|
+
# OR set the n8n producer before calling the webhook
|
|
65
|
+
# {"authorization": f"Basic {token}", "n8n_url": f'"{scheme}://{server}:{port}/{wh_endpoint if wh_endpoint is not None else endpoint}/{path}"'}
|
|
66
|
+
export N8N_PRODUCER=http://localhost:5678/webhook-test/002fa0e8-f7aa-4e04-b4e3-e81aa29c6e69
|
|
48
67
|
```
|
|
@@ -12,21 +12,19 @@ See how to build a complete database system -- in minutes instead of weeks or mo
|
|
|
12
12
|
|
|
13
13
|
We'll use API Logic Server (open source), providing:
|
|
14
14
|
|
|
15
|
-
| Key Feature
|
|
16
|
-
|
|
|
17
|
-
| **Automation**
|
|
18
|
-
| **Customization** | Declarative logic and security <br> 5 rules vs. 200 lines of Python | 40X less backend code
|
|
19
|
-
| **Iteration**
|
|
15
|
+
| Key Feature | Providing | Why It Matters |
|
|
16
|
+
| :---------------- | :------------------------------------------------------------------ | :-------------------------------------------------- |
|
|
17
|
+
| **Automation** | Instant Project Creation:<br>An API and an Admin web app | Unblock UI App Dev<br>Instant Agile Collaboration |
|
|
18
|
+
| **Customization** | Declarative logic and security <br> 5 rules vs. 200 lines of Python | 40X less backend code |
|
|
19
|
+
| **Iteration** | Revising the data model, and <br>Adding rules plus Python | Iterative development <br> Extensiblity with Python |
|
|
20
20
|
|
|
21
21
|
The entire process takes 10 minutes, instead of several weeks using traditional development.
|
|
22
22
|
|
|
23
23
|
You can use this article in several ways:
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
* Self-demo with video - you can also use [this video](https://www.youtube.com/watch?v=sD6RFp8S6Fg) (it's the same system, but the database is created with ChatGPT).
|
|
25
|
+
- Conceptual Overview - focus on the basic process. Operational details are moved to the Appendix to retain focus on the concepts.
|
|
26
|
+
- Self-demo - you can create this system yourself.
|
|
27
|
+
- Self-demo with video - you can also use [this video](https://www.youtube.com/watch?v=sD6RFp8S6Fg) (it's the same system, but the database is created with ChatGPT).
|
|
30
28
|
|
|
31
29
|
|
|
32
30
|
|
|
@@ -45,9 +43,7 @@ This creates a project by reading your schema. The database is Customer, Orders
|
|
|
45
43
|
You can open with VSCode, and run it as follows:
|
|
46
44
|
|
|
47
45
|
1. **Create Virtual Environment:** as shown in the Appendix.
|
|
48
|
-
|
|
49
46
|
2. **Start the Server:** F5 (also described in the Appendix).
|
|
50
|
-
|
|
51
47
|
3. **Start the Admin App:** either use the links provided in the IDE console, or click [http://localhost:5656/](http://localhost:5656/). The screen shown below should appear in your Browser.
|
|
52
48
|
|
|
53
49
|
The sections below explore the system that has been created (which would be similar for your own database).
|
|
@@ -58,7 +54,7 @@ The sections below explore the system that has been created (which would be simi
|
|
|
58
54
|
|
|
59
55
|
The system creates an API with end points for each table, with filtering, sorting, pagination, optimistic locking and related data access -- **[self-serve](https://apilogicserver.github.io/Docs/API-Self-Serve/), ready for custom app dev.**
|
|
60
56
|
|
|
61
|
-
|
|
57
|
+

|
|
62
58
|
|
|
63
59
|
### Admin App
|
|
64
60
|
|
|
@@ -66,32 +62,29 @@ It also creates an Admin App: multi-page, multi-table apps -- ready for **[busin
|
|
|
66
62
|
|
|
67
63
|
You can click Customer 2, see their Orders, and Items.
|
|
68
64
|
|
|
69
|
-
|
|
65
|
+

|
|
70
66
|
|
|
71
67
|
## 2. Customize in your IDE
|
|
72
68
|
|
|
73
69
|
While API/UI automation is a great start, it's critical to enforce logic and security. Here's how.
|
|
74
70
|
|
|
75
|
-
The follwing
|
|
71
|
+
The follwing *add customizations* process simulates adding security to your project, and using your IDE to declare logic and security in `logic/declare_logic.sh` and `security/declare_security.py`. You can diff these files to their created versions, and/or examine the declared logic.
|
|
76
72
|
|
|
77
73
|
In a terminal window for your project:
|
|
78
74
|
|
|
79
75
|
**1. Stop the Server** (Red Stop button, or Shift-F5 -- see Appendix)
|
|
80
76
|
|
|
81
|
-
**2.
|
|
77
|
+
**2. Add Customizations**
|
|
82
78
|
|
|
83
79
|
```bash
|
|
84
|
-
|
|
85
|
-
sh apply_customizations.sh
|
|
86
|
-
|
|
87
|
-
#windows
|
|
88
|
-
./apply_customizations.ps1
|
|
80
|
+
als add-cust
|
|
89
81
|
```
|
|
82
|
+
|
|
90
83
|
|
|
91
84
|
|
|
92
85
|
### Declare Security
|
|
93
86
|
|
|
94
|
-
The
|
|
87
|
+
The *add customizations* process above has simulated the `ApiLogicServer add-auth` command, and using your IDE to declare security in `logic/declare_logic.sh`.
|
|
95
88
|
|
|
96
89
|
To see security in action:
|
|
97
90
|
|
|
@@ -115,7 +108,7 @@ Logic (multi-table derivations and constraints) is a significant portion of a sy
|
|
|
115
108
|
|
|
116
109
|
Rules are declared in Python, simplified with IDE code completion. The screen below shows the 5 rules for **Check Credit Logic.**
|
|
117
110
|
|
|
118
|
-
The
|
|
111
|
+
The *add customizations* process above has simulated the process of using your IDE to declare logic in `logic/declare_logic.sh`.
|
|
119
112
|
|
|
120
113
|
To see logic in action:
|
|
121
114
|
|
|
@@ -169,11 +162,9 @@ Not only are spreadsheet-like rules 40X more concise, they meaningfully simplify
|
|
|
169
162
|
|
|
170
163
|
The follwing `apply_iteration` process simulates an iteration:
|
|
171
164
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
* revised logic - in `logic/declare_logic.py`, we replace the 2 lines for the `models.Item.Amount` formula with this (next screenshot shows revised logic executing with breakpoint):
|
|
165
|
+
- acquires a new database with `Product.CarbonNeutral`
|
|
166
|
+
- and a revised `ui/admin/admin.yaml` that shows this new column
|
|
167
|
+
- revised logic - in `logic/declare_logic.py`, we replace the 2 lines for the `models.Item.Amount` formula with this (next screenshot shows revised logic executing with breakpoint):
|
|
177
168
|
|
|
178
169
|
```python
|
|
179
170
|
def derive_amount(row: models.Item, old_row: models.Item, logic_row: LogicRow):
|
|
@@ -185,8 +176,6 @@ The follwing `apply_iteration` process simulates an iteration:
|
|
|
185
176
|
Rule.formula(derive=models.Item.Amount, calling=derive_amount)
|
|
186
177
|
```
|
|
187
178
|
|
|
188
|
-
* issues the `ApiLogicServer rebuild-from-database` command that rebuilds your project (the database models, the api), while preserving the customizations we made above.
|
|
189
|
-
|
|
190
179
|
In a terminal window for your project:
|
|
191
180
|
|
|
192
181
|
**1. Stop the Server** (Red Stop button, or Shift-F5 -- see Appendix)
|
|
@@ -194,12 +183,10 @@ In a terminal window for your project:
|
|
|
194
183
|
**2. Apply Iteration**
|
|
195
184
|
|
|
196
185
|
```bash
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
#windows
|
|
201
|
-
./apply_iteration.ps1
|
|
186
|
+
als add-cust
|
|
187
|
+
als rebuild-from-database
|
|
202
188
|
```
|
|
189
|
+
|
|
203
190
|
|
|
204
191
|
|
|
205
192
|
**3. Set the breakpoint as shown**
|
|
@@ -210,7 +197,7 @@ sh apply_iteration.sh
|
|
|
210
197
|
|
|
211
198
|
At the breakpoint, note you can use standard debugger services to debug your logic (examine `Item` attributes, step, etc).
|
|
212
199
|
|
|
213
|
-
|
|
200
|
+
!
|
|
214
201
|
|
|
215
202
|
|
|
216
203
|
|
|
@@ -250,11 +237,9 @@ Note we rebuilt the project from our altered database, illustrating we can **ite
|
|
|
250
237
|
|
|
251
238
|
Of course, we all know that all businesses the world over depend on the `hello world` app. This is provided in `api/customize_api`. Observe that it's:
|
|
252
239
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
* and, for database access, SQLAlchemy. Note all updates from custom APIs also enforce your logic.
|
|
240
|
+
- standard Python
|
|
241
|
+
- using Flask
|
|
242
|
+
- and, for database access, SQLAlchemy. Note all updates from custom APIs also enforce your logic.
|
|
258
243
|
|
|
259
244
|
|
|
260
245
|
|
|
@@ -270,7 +255,7 @@ API Logic Server also creates scripts for deployment. While these are ***not re
|
|
|
270
255
|
|
|
271
256
|
## Summary
|
|
272
257
|
|
|
273
|
-
|
|
258
|
+

|
|
274
259
|
|
|
275
260
|
In minutes - not days or weeks - you've used API Logic Server to convert an idea into **working software,** added **logic and security,** and **iterated** to meet new requirements.
|
|
276
261
|
|
|
@@ -282,8 +267,9 @@ To dive deeper, you can install [API Logic Server](https://apilogicserver.github
|
|
|
282
267
|
|
|
283
268
|
## Appendix: Database Schema
|
|
284
269
|
|
|
285
|
-
<img src="https://github.com/ApiLogicServer/Docs/blob/main/docs/images/basic_demo/basic_demo_data_model.jpeg?raw=true" width="500">
|
|
286
270
|
|
|
271
|
+
|
|
272
|
+

|
|
287
273
|
|
|
288
274
|
|
|
289
275
|
## Appendix: Procedures
|
|
@@ -321,28 +307,19 @@ For PyCharm, start the server with CTL-D, Stop with red stop button.
|
|
|
321
307
|
To enter a new Order:
|
|
322
308
|
|
|
323
309
|
1. Click `Customer 1``
|
|
324
|
-
|
|
325
310
|
2. Click `+ ADD NEW ORDER`
|
|
326
|
-
|
|
327
311
|
3. Set `Notes` to "hurry", and press `SAVE AND SHOW`
|
|
328
|
-
|
|
329
312
|
4. Click `+ ADD NEW ITEM`
|
|
330
|
-
|
|
331
313
|
5. Enter Quantity 1, lookup "Product 1", and click `SAVE AND ADD ANOTHER`
|
|
332
|
-
|
|
333
314
|
6. Enter Quantity 2000, lookup "Product 2", and click `SAVE`
|
|
334
|
-
|
|
335
315
|
7. Observe the constraint error, triggered by rollups from the `Item` to the `Order` and `Customer`
|
|
336
|
-
|
|
337
316
|
8. Correct the quantity to 2, and click `Save`
|
|
338
317
|
|
|
339
|
-
|
|
340
318
|
**4. Update the Order**
|
|
341
319
|
|
|
342
320
|
To explore our new logic for green products:
|
|
343
321
|
|
|
344
322
|
1. Access the previous order, and `ADD NEW ITEM`
|
|
345
|
-
|
|
346
323
|
2. Enter quantity 11, lookup product `Green`, and click `Save`.
|
|
347
324
|
|
|
348
325
|
|
|
Binary file
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
from flask import request, jsonify
|
|
2
|
+
from flask import Flask, redirect, send_from_directory, send_file
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import json
|
|
6
|
+
import io
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
from config.config import Args # circular import error if at top
|
|
10
|
+
|
|
11
|
+
app_logger = logging.getLogger("api_logic_server_app")
|
|
12
|
+
|
|
13
|
+
def add_service(app, api, project_dir, swagger_host: str, PORT: str, method_decorators = []):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
def get_server_url():
|
|
17
|
+
""" return the server URL for the OpenAPI spec """
|
|
18
|
+
result = f'http://{Args.instance.swagger_host}:{Args.instance.swagger_port}'
|
|
19
|
+
# get env variable API_LOGIC_SERVER_TUNNEL (or None)
|
|
20
|
+
if tunnel_url := os.getenv("API_LOGIC_SERVER_TUNNEL", None):
|
|
21
|
+
app_logger.info(f".. tunnel URL: {tunnel_url}")
|
|
22
|
+
result = tunnel_url
|
|
23
|
+
return result # + '/api'
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@app.before_request
|
|
27
|
+
def before_any_request():
|
|
28
|
+
# print(f"[DEBUG] Incoming request: {request.method} {request.url}")
|
|
29
|
+
if activate_openapi_logging := True:
|
|
30
|
+
if request.content_type == 'application/json' and request.method in ['POST', 'PUT', 'PATCH']:
|
|
31
|
+
# openapi: Incoming request: PATCH http://localhost:5656/api/Customer/1/ {'data': {'attributes': {'credit_limit': 5555}, 'type': 'Customer', 'id': '1'}}
|
|
32
|
+
# openapi: Incoming request: PATCH http://6f6f-2601-644-4900-d6f0-ecc9-6df3-8863-c5b2.ngrok-free.app/api/Customer/1 {'credit_limit': 5555}
|
|
33
|
+
|
|
34
|
+
app_logger.info(f"openapi: Incoming request: {request.method} {request.url} {str(request.json)}")
|
|
35
|
+
else:
|
|
36
|
+
app_logger.info(f"openapi: Incoming request: {request.method} {request.url}")
|
|
37
|
+
# app_logger.info(f"openapi: Incoming request headers: {request.headers}")
|
|
38
|
+
|
|
39
|
+
chatgpt_request_json = {
|
|
40
|
+
"credit_limit": 25000,
|
|
41
|
+
}
|
|
42
|
+
standard_request_json = {
|
|
43
|
+
"data": {
|
|
44
|
+
"type": "Customer",
|
|
45
|
+
"id": "ALFKI",
|
|
46
|
+
"attributes": {
|
|
47
|
+
"name": "Alice",
|
|
48
|
+
"credit_limit": 25000,
|
|
49
|
+
"balance": 12345
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
swagger_request_json = {
|
|
54
|
+
'data': {
|
|
55
|
+
'attributes': {
|
|
56
|
+
'credit_limit': 5555
|
|
57
|
+
},
|
|
58
|
+
'type': 'Customer',
|
|
59
|
+
'id': '1'
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@app.route('/mcp_server_executor', methods=['GET'])
|
|
66
|
+
def mcp_server_executor(path=None):
|
|
67
|
+
''' sample response printed in mcp_client_executor.py:
|
|
68
|
+
FIXME - incorrect.
|
|
69
|
+
But do provide: https://localhost:5656/.well-known/mcp.json
|
|
70
|
+
```
|
|
71
|
+
MCP MCP Response (simulated):
|
|
72
|
+
{
|
|
73
|
+
"get_json": {
|
|
74
|
+
"filter": {
|
|
75
|
+
"filter": {
|
|
76
|
+
"credit_limit": {
|
|
77
|
+
"gt": 4000
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
"headers": {
|
|
81
|
+
"Accept": "application/vnd.api+json",
|
|
82
|
+
"Authorization": "Bearer your_token"
|
|
83
|
+
},
|
|
84
|
+
"type": "Customer",
|
|
85
|
+
"url": "http://localhost:5656/api/Customer"
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
"name": "mcp_server_executor",
|
|
89
|
+
"openapiUrl": "TUNNEL_URL/api/openapi.json",
|
|
90
|
+
"serverUrl": "TUNNEL_URL/api"
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
'''
|
|
94
|
+
get_json = request.get_json()
|
|
95
|
+
app_logger.info(f"mcp_server_executor sees mcp request: \n{json.dumps(get_json, indent=4)}")
|
|
96
|
+
|
|
97
|
+
# process verb, filter here (stub for now)
|
|
98
|
+
filter_json = get_json['filter'] # {"credit_limit": {"gt": 4000}} # todo: bunch'o parsing here
|
|
99
|
+
|
|
100
|
+
filter_json = {"name": "credit_limit", "op": "gt", "val":4000} # https://github.com/thomaxxl/safrs/wiki/JsonApi-filtering
|
|
101
|
+
filter = json.dumps(filter_json) # {"name": "credit_limit", "op": "gt", "val": 4000}
|
|
102
|
+
get_uri = get_json['url'] + '?filter=' + filter # get_uri = "http://localhost:5656/api/Customer?filter[credit_limit]=1000"
|
|
103
|
+
response = requests.get(url=get_uri, headers= request.headers)
|
|
104
|
+
|
|
105
|
+
return response.json(), 200, {'Content-Type': 'application/json; charset=utf-8'}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@app.route('/.well-known/mcp.json', methods=['GET'])
|
|
109
|
+
def mcp_discovery(path=None):
|
|
110
|
+
''' called by mcp_client_executor for discovery, eg:
|
|
111
|
+
```
|
|
112
|
+
{
|
|
113
|
+
"tool_type": "json-api",
|
|
114
|
+
"schema_version": "1.0",
|
|
115
|
+
"base_url": "https://crm.company.com",
|
|
116
|
+
"resources": [
|
|
117
|
+
{
|
|
118
|
+
"name": "Customer",
|
|
119
|
+
"path": "/Customer",
|
|
120
|
+
"methods": ["GET", "PATCH"],
|
|
121
|
+
"fields": ["id", "name", "balance", "credit_limit"],
|
|
122
|
+
"filterable": ["name", "credit_limit"],
|
|
123
|
+
"example": "List customers with credit over 5000"
|
|
124
|
+
}
|
|
125
|
+
]
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
test: curl -X GET "http://localhost:5656/.well-known/mcp.json"
|
|
129
|
+
'''
|
|
130
|
+
# return docs/mcp_schema.json
|
|
131
|
+
schema_path = os.path.join(project_dir, "docs", "mcp_schema.json")
|
|
132
|
+
try:
|
|
133
|
+
with open(schema_path, "r") as schema_file:
|
|
134
|
+
schema = json.load(schema_file)
|
|
135
|
+
return jsonify(schema), 200
|
|
136
|
+
except Exception as e:
|
|
137
|
+
app_logger.error(f"Error loading MCP schema: {e}")
|
|
138
|
+
return jsonify({"error": "MCP schema not found"}), 404
|
|
139
|
+
pass
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from flask import request, jsonify
|
|
2
|
+
from flask import Flask, redirect, send_from_directory, send_file
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import json
|
|
6
|
+
import io
|
|
7
|
+
from config.config import Args # circular import error if at top
|
|
8
|
+
|
|
9
|
+
app_logger = logging.getLogger("api_logic_server_app")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def add_service(app, api, project_dir, swagger_host: str, PORT: str, method_decorators = []):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
def get_server_url():
|
|
17
|
+
""" return the server URL for the OpenAPI spec """
|
|
18
|
+
result = f'http://{Args.instance.swagger_host}:{Args.instance.swagger_port}'
|
|
19
|
+
# get env variable API_LOGIC_SERVER_TUNNEL (or None)
|
|
20
|
+
if tunnel_url := os.getenv("API_LOGIC_SERVER_TUNNEL", None):
|
|
21
|
+
app_logger.info(f".. tunnel URL: {tunnel_url}")
|
|
22
|
+
result = tunnel_url
|
|
23
|
+
return result # + '/api'
|
|
24
|
+
|
|
25
|
+
@app.route('/mcp.json')
|
|
26
|
+
def mcp(path=None):
|
|
27
|
+
'''
|
|
28
|
+
test: curl -X GET http://localhost:5656/mcp.json
|
|
29
|
+
'''
|
|
30
|
+
|
|
31
|
+
mcp_json = {
|
|
32
|
+
"name": "MCP genai_demo test",
|
|
33
|
+
"description": "You are an AI Planner + Executor for a live JSON:API server.",
|
|
34
|
+
"instructions": [
|
|
35
|
+
"When a user gives you a natural language goal (e.g., 'list customers from Germany'), you:",
|
|
36
|
+
"Identify the resource (Customer, Order, Product).",
|
|
37
|
+
"Map filters (e.g., Country=Germany).",
|
|
38
|
+
"Construct a JSON:API call to the live endpoint (through a function called fetch_resource).",
|
|
39
|
+
"Execute the live API call through the function.",
|
|
40
|
+
"Format and display the results neatly."
|
|
41
|
+
],
|
|
42
|
+
"serverUrl": "http://localhost:5656/api",
|
|
43
|
+
"openapiUrl": "http://localhost:5656/api/openapi.json",
|
|
44
|
+
"apiStandard": "JSON:API (application/vnd.api+json)"
|
|
45
|
+
}
|
|
46
|
+
mcp_json["serverUrl"] = get_server_url() + '/api'
|
|
47
|
+
mcp_json["openapiUrl"] = get_server_url() + '/api/openapi.json'
|
|
48
|
+
# return jsonify(mcp), 200, {'Content-Type': 'text/plain; charset=utf-8'}
|
|
49
|
+
return jsonify(mcp_json), 200, {'Content-Type': 'application/json; charset=utf-8'}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@app.route('/api/openapi.json')
|
|
53
|
+
def openapi(path=None):
|
|
54
|
+
""" return integration/openai_plugin/swagger_3.json
|
|
55
|
+
* with updated tunnel URL if API_LOGIC_SERVER_TUNNEL is set
|
|
56
|
+
|
|
57
|
+
test: curl -X GET http://localhost:5656/api/openapi.json
|
|
58
|
+
like: curl -X GET http://localhost:5656/api/swagger.json
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
# read dict from json file integration/openai_plugin/swagger_3.json
|
|
62
|
+
with open("integration/openai_function/swagger_3.json", "r") as json_file:
|
|
63
|
+
swagger_dict = json.load(json_file)
|
|
64
|
+
app_logger.info(f"openapi: Swagger JSON loaded: {swagger_dict}")
|
|
65
|
+
|
|
66
|
+
server_url = get_server_url()
|
|
67
|
+
swagger_dict["servers"][0]["url"] = server_url + '/api'
|
|
68
|
+
|
|
69
|
+
# convert dict to buffered stream
|
|
70
|
+
swagger_dict_mem = io.BytesIO(json.dumps(swagger_dict).encode('utf-8'))
|
|
71
|
+
return send_file(swagger_dict_mem, mimetype='text/json')
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@app.route('/api/ai_plugin')
|
|
75
|
+
def ai_plugin(path=None):
|
|
76
|
+
""" return integration/openai_plugin/ai_plugin.json (disparaged)
|
|
77
|
+
* with updated tunnel URL if API_LOGIC_SERVER_TUNNEL is set
|
|
78
|
+
|
|
79
|
+
test: curl -X GET http://localhost:5656/api/ai_plugin
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
# read dict from json file integration/openai_plugin/swagger_3.json
|
|
83
|
+
with open("integration/openai_plugin/ai_plugin.json", "r") as json_file:
|
|
84
|
+
swagger_dict = json.load(json_file)
|
|
85
|
+
app_logger.info(f"openapi: ai_plugin JSON loaded: {swagger_dict}")
|
|
86
|
+
|
|
87
|
+
server_url = get_server_url()
|
|
88
|
+
swagger_dict["servers"][0]["url"] = server_url
|
|
89
|
+
|
|
90
|
+
# convert dict to buffered stream
|
|
91
|
+
swagger_dict_mem = io.BytesIO(json.dumps(swagger_dict).encode('utf-8'))
|
|
92
|
+
return send_file(swagger_dict_mem, mimetype='text/json')
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
SECRET_KEY = "whatnothow"
|
|
2
|
+
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
|
3
|
+
SQLAlCHEMY_ECHO = False
|
|
4
|
+
# AGGREGATE_DEFAULTS = True
|
|
5
|
+
# ALL_DEFAULTS = True
|
|
6
|
+
# APILOGICPROJECT_KAFKA_PRODUCER = "{\"bootstrap.servers\": \"localhost:9092\"}"
|
|
7
|
+
# SQLALCHEMY_DATABASE_URI=db.sqlite
|
|
8
|
+
|
|
9
|
+
SECURITY_ENABLED = false
|
|
10
|
+
|
|
11
|
+
# if using tunnel for mcp, function, or ai_plugin
|
|
12
|
+
# eg, https://tunnel_url.ngrok-free.app
|
|
13
|
+
API_LOGIC_SERVER_TUNNEL = "TUNNEL_URL"
|