ApiLogicServer 14.5.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 +7 -21
- api_logic_server_cli/api_logic_server.py +4 -2
- api_logic_server_cli/api_logic_server_info.yaml +2 -2
- 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/prototypes/basic_demo/customizations/api/api_discovery/{mcp_server_executor.py → mcp_discovery.py} +1 -0
- api_logic_server_cli/prototypes/basic_demo/customizations/config/server_setup.py +388 -0
- api_logic_server_cli/prototypes/basic_demo/customizations/database/system/SAFRSBaseX.py +136 -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 +3 -1
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/mcp_client_executor.py +82 -27
- api_logic_server_cli/prototypes/basic_demo/customizations/logic/declare_logic.py +22 -2
- api_logic_server_cli/prototypes/basic_demo/iteration/logic/declare_logic.py +1 -1
- 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
- apilogicserver-14.5.3.dist-info/METADATA +168 -0
- {apilogicserver-14.5.0.dist-info → apilogicserver-14.5.3.dist-info}/RECORD +24 -42
- api_logic_server_cli/prototypes/basic_demo/customizations/api/api_discovery/proper_update_def.json +0 -71
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/1_langchain_loader.py +0 -71
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/2_gpt_mcp_prompt.txt +0 -19
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/multi_mcp_flow/multi_mcp_flow.png +0 -0
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/multi_mcp_flow/multi_mcp_orchestration.yaml +0 -49
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/multi_mcp_flow/wny mcp flows.png +0 -0
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/natlang_to_api.py +0 -73
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/curl.txt +0 -5
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/images/MCP Overview.png +0 -0
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/images/MCP_Arch.png +0 -0
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/images/MCP_Overview_Executor.png +0 -0
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/invoke_llm/1 - prompt_messages_array.json +0 -10
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/invoke_llm/2 - completion_tool_context.json +0 -12
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/llm_schema.txt +0 -38
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/nw_swagger_2.yaml +0 -17393
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/nw_swagger_3.yaml +0 -16660
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/nw_swagger_3_relaxed.yaml +0 -109
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/proxy_server.py +0 -51
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/proxy_serverZ.py +0 -72
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/validate_jsonapi.py +0 -64
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/run_executor.py +0 -23
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/swagger_converter.py +0 -65
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/z_old/3_executor_test_agent.py +0 -52
- api_logic_server_cli/prototypes/manager/README_X.md +0 -663
- apilogicserver-14.5.0.dist-info/METADATA +0 -76
- {apilogicserver-14.5.0.dist-info → apilogicserver-14.5.3.dist-info}/WHEEL +0 -0
- {apilogicserver-14.5.0.dist-info → apilogicserver-14.5.3.dist-info}/entry_points.txt +0 -0
- {apilogicserver-14.5.0.dist-info → apilogicserver-14.5.3.dist-info}/licenses/LICENSE +0 -0
- {apilogicserver-14.5.0.dist-info → apilogicserver-14.5.3.dist-info}/top_level.txt +0 -0
|
@@ -25,28 +25,12 @@ Note: many require: rebuild-from-database --project_name=./ --db_url=sqlite:///d
|
|
|
25
25
|
'''
|
|
26
26
|
|
|
27
27
|
def add_genai_customizations(project: Project, do_show_messages: bool = True, do_security: bool = True):
|
|
28
|
-
""" Add customizations to genai (default creation)
|
|
28
|
+
""" Add customizations `prototypes/genai_demo` to genai (default creation)
|
|
29
29
|
|
|
30
30
|
0. Initial: create_project_and_overlay_prototypes() -- minor: just creates the readme
|
|
31
31
|
* When done with genai logic prompt, logic is pre-created (in logic/declare_logic.py)
|
|
32
32
|
1. Deep copy prototypes/genai_demo (adds logic and security, and custom end point)
|
|
33
33
|
|
|
34
|
-
WebGenAI DX:
|
|
35
|
-
|
|
36
|
-
0. Convention: click the Blue Button
|
|
37
|
-
* Home/Create Project
|
|
38
|
-
* Home/Open App
|
|
39
|
-
* Landing
|
|
40
|
-
* Overview[Manager]/Open
|
|
41
|
-
* Overview/GitHub
|
|
42
|
-
* App Home / Develop --> GitHub
|
|
43
|
-
0. demo --> codespaces. Where are instructions (what is CS, how do I load/run)?
|
|
44
|
-
1. Name can be any, iff created with APILOGICPROJECT_IS_GENAI_DEMO
|
|
45
|
-
2. Bypass duplicate discovery logic iff created with APILOGICPROJECT_IS_GENAI_DEMO
|
|
46
|
-
3. TODO:
|
|
47
|
-
* cd project
|
|
48
|
-
* als add-cust # add customizations
|
|
49
|
-
* run, and use place b2b order service - end point is not activated.
|
|
50
34
|
|
|
51
35
|
Args:
|
|
52
36
|
"""
|
|
@@ -251,7 +235,9 @@ def add_cust(project: Project, models_py_path: Path, project_name: str):
|
|
|
251
235
|
if not models_py_path.exists():
|
|
252
236
|
raise Exception("Customizations are northwind/genai-specific - models.py does not exist")
|
|
253
237
|
|
|
254
|
-
project_is_genai_demo = False
|
|
238
|
+
project_is_genai_demo = False
|
|
239
|
+
''' can't use project.is_genai_demo because this is not the create command...'''
|
|
240
|
+
|
|
255
241
|
if project.project_directory_path.joinpath('docs/project_is_genai_demo.txt').exists():
|
|
256
242
|
project_is_genai_demo = True
|
|
257
243
|
|
|
@@ -260,8 +246,8 @@ def add_cust(project: Project, models_py_path: Path, project_name: str):
|
|
|
260
246
|
add_nw_customizations(project=project, do_security=False)
|
|
261
247
|
log.info("\nNext step - add authentication:\n $ ApiLogicServer add-auth --db_url=auth\n\n")
|
|
262
248
|
|
|
263
|
-
elif project_is_genai_demo and create_utils.does_file_contain(search_for="Customer", in_file=models_py_path):
|
|
264
|
-
|
|
249
|
+
# elif project_is_genai_demo and create_utils.does_file_contain(search_for="Customer", in_file=models_py_path):
|
|
250
|
+
# add_genai_customizations(project=project, do_security=False)
|
|
265
251
|
|
|
266
252
|
elif project_name == 'sample_ai' and create_utils.does_file_contain(search_for="CustomerName = Column(Text", in_file=models_py_path):
|
|
267
253
|
cocktail_napkin_path = project.project_directory_path.joinpath('logic/cocktail-napkin.jpg')
|
|
@@ -271,7 +257,7 @@ def add_cust(project: Project, models_py_path: Path, project_name: str):
|
|
|
271
257
|
else:
|
|
272
258
|
add_sample_ai_iteration(project=project)
|
|
273
259
|
|
|
274
|
-
elif project_name == 'basic_demo' and create_utils.does_file_contain(search_for="Customer", in_file=models_py_path):
|
|
260
|
+
elif (project_is_genai_demo or project_name == 'basic_demo') and create_utils.does_file_contain(search_for="Customer", in_file=models_py_path):
|
|
275
261
|
cocktail_napkin_path = project.project_directory_path.joinpath('logic/cocktail-napkin.jpg')
|
|
276
262
|
is_customized = cocktail_napkin_path.exists()
|
|
277
263
|
if not is_customized:
|
|
@@ -12,9 +12,10 @@ ApiLogicServer CLI: given a database url, create [and run] customizable ApiLogic
|
|
|
12
12
|
Called from api_logic_server_cli.py, by instantiating the ProjectRun object.
|
|
13
13
|
'''
|
|
14
14
|
|
|
15
|
-
__version__ = "14.05.
|
|
15
|
+
__version__ = "14.05.03" # last public release: 14.04.00
|
|
16
16
|
recent_changes = \
|
|
17
17
|
f'\n\nRecent Changes:\n' +\
|
|
18
|
+
"\t05/19/2024 - 14.05.03: mcp filters with working date range (AND), email stub, use basic_demo custs for genai_demo \n"\
|
|
18
19
|
"\t05/16/2024 - 14.05.00: safrs 3.1.7, running mcp preview \n"\
|
|
19
20
|
"\t04/27/2024 - 14.04.00: Graphics preview, Vibe install fix, Improved IDE Chat Logic, MCP Exploration \n"\
|
|
20
21
|
"\t03/30/2024 - 14.03.25: WebGenAI fixes for Kafka and Keycloak \n"\
|
|
@@ -417,7 +418,8 @@ def create_project_and_overlay_prototypes(project: 'ProjectRun', msg: str) -> st
|
|
|
417
418
|
# readme now opens automatically, so use that...
|
|
418
419
|
shutil.move(project.project_directory_path.joinpath('readme.md'),
|
|
419
420
|
project.project_directory_path.joinpath('readme_standard.md'))
|
|
420
|
-
create_utils.copy_md(project = project, from_doc_file = "Sample-Genai.md", to_project_file='readme.md')
|
|
421
|
+
# create_utils.copy_md(project = project, from_doc_file = "Sample-Genai.md", to_project_file='readme.md')
|
|
422
|
+
create_utils.copy_md(project = project, from_doc_file = "Sample-Basic-Demo.md", to_project_file='readme.md')
|
|
421
423
|
|
|
422
424
|
if "postgres" or "mysql" in project.db_url:
|
|
423
425
|
fixup_devops_for_postgres_mysql(project)
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
last_created_date: May
|
|
1
|
+
last_created_date: May 18, 2025 16:45:45
|
|
2
2
|
last_created_project_name: ../../../servers/basic_demo
|
|
3
|
-
last_created_version: 14.
|
|
3
|
+
last_created_version: 14.05.02
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
###############################################################################
|
|
4
|
+
#
|
|
5
|
+
# Initalization functions used by api_logic_server_run.py
|
|
6
|
+
#
|
|
7
|
+
# You typically do not customize this file,
|
|
8
|
+
# except to override Creation Defaults and Logging, below.
|
|
9
|
+
#
|
|
10
|
+
###############################################################################
|
|
11
|
+
|
|
12
|
+
"""
|
|
13
|
+
Operation:
|
|
14
|
+
1. api_logic_server_run.py - imports config
|
|
15
|
+
1. captures args
|
|
16
|
+
2. api_logic_server_run.py - imports server_setup
|
|
17
|
+
1. server_setup#logging_setup()
|
|
18
|
+
3. api_logic_server_run.py - server_setup.api_logic_server_setup
|
|
19
|
+
On error, NOT CALLED: constraint_handler or ValidationErrorExt (!)
|
|
20
|
+
|
|
21
|
+
+ Operation:
|
|
22
|
+
1. api_logic_server_run.py - imports config
|
|
23
|
+
1. captures args
|
|
24
|
+
1. config#logging_setup()
|
|
25
|
+
2. api_logic_server_run.py - imports server_setup
|
|
26
|
+
3. api_logic_server_run.py - server_setup.api_logic_server_setup
|
|
27
|
+
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
start_up_message = "normal start"
|
|
31
|
+
|
|
32
|
+
import traceback
|
|
33
|
+
try:
|
|
34
|
+
import os, logging, logging.config, sys, yaml # failure here means venv probably not set
|
|
35
|
+
except:
|
|
36
|
+
track = traceback.format_exc()
|
|
37
|
+
print(" ")
|
|
38
|
+
print(track)
|
|
39
|
+
print("venv probably not set")
|
|
40
|
+
print("Please see https://apilogicserver.github.io/Docs/Project-Env/ \n")
|
|
41
|
+
exit(1)
|
|
42
|
+
|
|
43
|
+
from flask_sqlalchemy import SQLAlchemy
|
|
44
|
+
import json
|
|
45
|
+
from pathlib import Path
|
|
46
|
+
if os.getenv("EXPERIMENT") == '+':
|
|
47
|
+
import config
|
|
48
|
+
from config.config import Args
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
setup_path = Path(os.path.abspath(os.path.dirname(__file__)))
|
|
52
|
+
project_path = setup_path.parent
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def is_docker() -> bool:
|
|
56
|
+
""" running docker? dir exists: /home/api_logic_server """
|
|
57
|
+
path = '/home/api_logic_server'
|
|
58
|
+
path_result = os.path.isdir(path) # this *should* exist only on docker
|
|
59
|
+
env_result = "DOCKER" == os.getenv('APILOGICSERVER_RUNNING')
|
|
60
|
+
# assert path_result == env_result
|
|
61
|
+
return path_result
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
if is_docker():
|
|
65
|
+
sys.path.append(os.path.abspath('/home/api_logic_server'))
|
|
66
|
+
|
|
67
|
+
logic_alerts = True
|
|
68
|
+
""" Set False to silence startup message """
|
|
69
|
+
declare_logic_message = ""
|
|
70
|
+
declare_security_message = "ALERT: *** Security Not Enabled ***"
|
|
71
|
+
|
|
72
|
+
project_dir = str(project_path)
|
|
73
|
+
os.chdir(project_dir) # so admin app can find images, code
|
|
74
|
+
import api.system.api_utils as api_utils
|
|
75
|
+
logic_logger_activate_debug = False
|
|
76
|
+
""" True prints all rules on startup """
|
|
77
|
+
|
|
78
|
+
args = ""
|
|
79
|
+
arg_num = 0
|
|
80
|
+
for each_arg in sys.argv:
|
|
81
|
+
args += each_arg
|
|
82
|
+
arg_num += 1
|
|
83
|
+
if arg_num < len(sys.argv):
|
|
84
|
+
args += ", "
|
|
85
|
+
project_name = os.path.basename(os.path.normpath(project_path))
|
|
86
|
+
|
|
87
|
+
from typing import TypedDict
|
|
88
|
+
import safrs # fails without venv - see https://apilogicserver.github.io/Docs/Project-Env/
|
|
89
|
+
from database.system.SAFRSBaseX import SAFRSBase
|
|
90
|
+
from safrs import ValidationError, SAFRSAPI as _SAFRSAPI
|
|
91
|
+
#from safrs import ValidationError, SAFRSBase, SAFRSAPI as _SAFRSAPI
|
|
92
|
+
from logic_bank.logic_bank import LogicBank
|
|
93
|
+
from logic_bank.exceptions import LBActivateException
|
|
94
|
+
from logic_bank.exec_row_logic.logic_row import LogicRow
|
|
95
|
+
from logic_bank.rule_type.constraint import Constraint
|
|
96
|
+
from .activate_logicbank import activate_logicbank
|
|
97
|
+
from sqlalchemy.ext.declarative import declarative_base
|
|
98
|
+
from sqlalchemy.orm import Session
|
|
99
|
+
import socket
|
|
100
|
+
import warnings
|
|
101
|
+
from flask import Flask, redirect, send_from_directory, send_file
|
|
102
|
+
from flask_cors import CORS
|
|
103
|
+
from safrs import ValidationError, SAFRSAPI
|
|
104
|
+
import ui.admin.admin_loader as AdminLoader
|
|
105
|
+
from security.system.authentication import configure_auth
|
|
106
|
+
import database.bind_dbs as bind_dbs
|
|
107
|
+
import oracledb
|
|
108
|
+
import integration.kafka.kafka_producer as kafka_producer
|
|
109
|
+
import integration.kafka.kafka_consumer as kafka_consumer
|
|
110
|
+
import integration.n8n.n8n_producer as n8n_producer
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
if os.getenv("EXPERIMENT") == '+':
|
|
114
|
+
app_logger = logging.getLogger("api_logic_server_app")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class SAFRSAPI(_SAFRSAPI):
|
|
118
|
+
"""
|
|
119
|
+
Extends SAFRSAPI to handle client_uri
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
_SAFRSAPI (_type_): _description_
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
def __init__(self, *args, **kwargs):
|
|
126
|
+
client_uri = kwargs.pop('client_uri', None)
|
|
127
|
+
if client_uri:
|
|
128
|
+
kwargs['port'] = None
|
|
129
|
+
kwargs['host'] = client_uri
|
|
130
|
+
super().__init__(*args, **kwargs)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# ==================================
|
|
135
|
+
# Set
|
|
136
|
+
# ==================================
|
|
137
|
+
|
|
138
|
+
def get_args(flask_app: Flask) -> Args:
|
|
139
|
+
"""
|
|
140
|
+
Get Args, update logging
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Args: typed access to flask_app.config
|
|
144
|
+
"""
|
|
145
|
+
args = Args(flask_app=flask_app) # creation defaults
|
|
146
|
+
|
|
147
|
+
import config.config as config
|
|
148
|
+
flask_app.config.from_object(config.Config)
|
|
149
|
+
app_logger.debug(f"\nserver_setup - get_args: Config args: \n{args}") # # config file (e.g., db uri's)
|
|
150
|
+
|
|
151
|
+
args.get_cli_args(dunder_name=__name__, args=args)
|
|
152
|
+
app_logger.debug(f"\nserver_setup - get_args: CLI args: \n{args}") # api_logic_server_run cl args
|
|
153
|
+
|
|
154
|
+
flask_app.config.from_prefixed_env(prefix="APILOGICPROJECT") # env overrides (e.g., docker)
|
|
155
|
+
app_logger.debug(f"\nserver_setup - get_args: ENV args: \n{args}\n\n")
|
|
156
|
+
|
|
157
|
+
if args.verbose: # export APILOGICPROJECT_VERBOSE=True
|
|
158
|
+
app_logger.setLevel(logging.DEBUG)
|
|
159
|
+
safrs.log.setLevel(logging.DEBUG) # notset 0, debug 10, info 20, warn 30, error 40, critical 50
|
|
160
|
+
authentication_logger = logging.getLogger('security.system.authentication')
|
|
161
|
+
authentication_logger.setLevel(logging.DEBUG)
|
|
162
|
+
authorization_logger = logging.getLogger('security.system.authorization')
|
|
163
|
+
authorization_logger.setLevel(logging.DEBUG)
|
|
164
|
+
auth_provider_logger = logging.getLogger('security.authentication_provider.sql.auth_provider')
|
|
165
|
+
auth_provider_logger.setLevel(logging.DEBUG)
|
|
166
|
+
# sqlachemy_logger = logging.getLogger('sqlalchemy.engine')
|
|
167
|
+
# sqlachemy_logger.setLevel(logging.DEBUG)
|
|
168
|
+
|
|
169
|
+
if app_logger.getEffectiveLevel() <= logging.DEBUG:
|
|
170
|
+
api_utils.sys_info(flask_app.config)
|
|
171
|
+
app_logger.debug(f"\nserver_setup - get_args: ENV args: \n{args}\n\n")
|
|
172
|
+
return args
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# ==================================
|
|
176
|
+
# LOGGING SETUP
|
|
177
|
+
# ==================================
|
|
178
|
+
|
|
179
|
+
def logging_setup() -> logging.Logger:
|
|
180
|
+
"""
|
|
181
|
+
Setup Logging
|
|
182
|
+
"""
|
|
183
|
+
global app_logger, debug_value, project_path
|
|
184
|
+
logging_config = f'{project_path}/config/logging.yml'
|
|
185
|
+
if os.getenv('APILOGICPROJECT_LOGGING_CONFIG'):
|
|
186
|
+
logging_config = project_path.joinpath(os.getenv("APILOGICPROJECT_LOGGING_CONFIG"))
|
|
187
|
+
with open(logging_config,'rt') as f: # see also logic/declare_logic.py
|
|
188
|
+
config=yaml.safe_load(f.read())
|
|
189
|
+
f.close()
|
|
190
|
+
logging.config.dictConfig(config) # log levels: notset 0, debug 10, info 20, warn 30, error 40, critical 50
|
|
191
|
+
app_logger = logging.getLogger("api_logic_server_app")
|
|
192
|
+
debug_value = os.getenv('APILOGICPROJECT_DEBUG')
|
|
193
|
+
if debug_value is not None: # > export APILOGICPROJECT_DEBUG=True
|
|
194
|
+
debug_value = debug_value.upper()
|
|
195
|
+
if debug_value.startswith("F") or debug_value.startswith("N"):
|
|
196
|
+
app_logger.setLevel(logging.INFO)
|
|
197
|
+
else:
|
|
198
|
+
app_logger.setLevel(logging.DEBUG)
|
|
199
|
+
app_logger.debug(f'\nDEBUG level set from env\n')
|
|
200
|
+
app_logger.info(f'\nAPI Logic Project Server Setup ({project_name}) Starting with CLI args: \n.. {args}\n')
|
|
201
|
+
app_logger.info(f'Created August 03, 2024 09:34:01 at {str(project_path)}\n')
|
|
202
|
+
return app_logger
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class ValidationErrorExt(ValidationError):
|
|
206
|
+
"""
|
|
207
|
+
This exception is raised when invalid input has been detected (client side input)
|
|
208
|
+
Always send back the message to the client in the response
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
def __init__(self, message="", status_code=400, api_code=2001, detail=None, error_attributes=None):
|
|
212
|
+
Exception.__init__(self)
|
|
213
|
+
self.error_attributes = error_attributes
|
|
214
|
+
self.status_code = status_code
|
|
215
|
+
self.message = message
|
|
216
|
+
self.api_code = api_code
|
|
217
|
+
self.detail: TypedDict = detail
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def validate_db_uri(flask_app):
|
|
221
|
+
"""
|
|
222
|
+
|
|
223
|
+
For sqlite, verify the SQLALCHEMY_DATABASE_URI file exists
|
|
224
|
+
|
|
225
|
+
* Since the name is not reported by SQLAlchemy
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
flask_app (_type_): initialize flask app
|
|
229
|
+
"""
|
|
230
|
+
|
|
231
|
+
db_uri = flask_app.config['SQLALCHEMY_DATABASE_URI']
|
|
232
|
+
app_logger.debug(f'sqlite_db_path validity check with db_uri: {db_uri}')
|
|
233
|
+
if 'sqlite' not in db_uri:
|
|
234
|
+
return
|
|
235
|
+
sqlite_db_path = ""
|
|
236
|
+
if db_uri.startswith('sqlite:////'): # eg, sqlite:////Users/val/dev/ApiLogicServer/ApiLogicServer-dev/servers/ai_customer_orders/database/db.sqlite
|
|
237
|
+
sqlite_db_path = Path(db_uri[9:])
|
|
238
|
+
app_logger.debug(f'\t.. Absolute: {str(sqlite_db_path)}')
|
|
239
|
+
else: # eg, sqlite:///../database/db.sqlite
|
|
240
|
+
db_relative_path = db_uri[10:]
|
|
241
|
+
db_relative_path = db_relative_path.replace('../', '') # relative
|
|
242
|
+
sqlite_db_path = Path(os.getcwd()).joinpath(db_relative_path)
|
|
243
|
+
app_logger.debug(f'\t.. Relative: {str(sqlite_db_path)}')
|
|
244
|
+
if db_uri == 'sqlite:///database/db.sqlite':
|
|
245
|
+
raise ValueError(f'This fails, please use; sqlite:///../database/db.sqlite')
|
|
246
|
+
if sqlite_db_path.is_file():
|
|
247
|
+
app_logger.debug(f'\t.. sqlite_db_path is a valid file\n')
|
|
248
|
+
else: # remove this if you wish
|
|
249
|
+
raise ValueError(f'sqlite database does not exist: {str(sqlite_db_path)}')
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
# ==========================================================
|
|
254
|
+
# API Logic Server Setup
|
|
255
|
+
# - Opens Database(s)
|
|
256
|
+
# - Setup API, Logic, Security, Optimistic Locking
|
|
257
|
+
# ==========================================================
|
|
258
|
+
|
|
259
|
+
def api_logic_server_setup(flask_app: Flask, args: Args):
|
|
260
|
+
"""
|
|
261
|
+
API Logic Server Setup
|
|
262
|
+
|
|
263
|
+
1. Opens Database(s)
|
|
264
|
+
2. Setup API, Logic, Security, Optimistic Locking
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
flask_app (_type_): configured flask_app (servers, ports, db uri's)
|
|
269
|
+
args (_type_): typed access to flask_app.config
|
|
270
|
+
|
|
271
|
+
Raises:
|
|
272
|
+
ValidationErrorExt: rebadge LogicBank errors for SAFRS API
|
|
273
|
+
"""
|
|
274
|
+
|
|
275
|
+
from sqlalchemy import exc as sa_exc
|
|
276
|
+
|
|
277
|
+
global logic_logger_activate_debug, declare_logic_message, declare_security_message
|
|
278
|
+
|
|
279
|
+
with warnings.catch_warnings():
|
|
280
|
+
|
|
281
|
+
safrs_log_level = safrs.log.getEffectiveLevel()
|
|
282
|
+
db_logger = logging.getLogger('sqlalchemy')
|
|
283
|
+
db_log_level = db_logger.getEffectiveLevel()
|
|
284
|
+
safrs_init_logger = logging.getLogger("safrs.safrs_init")
|
|
285
|
+
authorization_logger = logging.getLogger('security.system.authorization')
|
|
286
|
+
authorization_log_level = authorization_logger.getEffectiveLevel()
|
|
287
|
+
do_hide_chatty_logging = True and not args.verbose
|
|
288
|
+
# eg, system startup health check: read on API and relationship - hide many log entries
|
|
289
|
+
if do_hide_chatty_logging and app_logger.getEffectiveLevel() <= logging.INFO:
|
|
290
|
+
safrs.log.setLevel(logging.WARN) # notset 0, debug 10, info 20, warn 30, error 40, critical 50
|
|
291
|
+
db_logger.setLevel(logging.WARN)
|
|
292
|
+
safrs_init_logger.setLevel(logging.WARN)
|
|
293
|
+
authorization_logger.setLevel(logging.WARN)
|
|
294
|
+
|
|
295
|
+
bind_dbs.bind_dbs(flask_app)
|
|
296
|
+
|
|
297
|
+
# https://stackoverflow.com/questions/34674029/sqlalchemy-query-raises-unnecessary-warning-about-sqlite-and-decimal-how-to-spe
|
|
298
|
+
warnings.simplefilter("ignore", category=sa_exc.SAWarning) # alert - disable for safety msgs
|
|
299
|
+
|
|
300
|
+
def constraint_handler(message: str, constraint: Constraint, logic_row: LogicRow):
|
|
301
|
+
""" format LogicBank constraint exception for SAFRS """
|
|
302
|
+
if constraint is not None and hasattr(constraint, 'error_attributes'):
|
|
303
|
+
|
|
304
|
+
detail = {"model": logic_row.name, "error_attributes": constraint.error_attributes}
|
|
305
|
+
else:
|
|
306
|
+
detail = {"model": logic_row.name}
|
|
307
|
+
raise ValidationErrorExt(message=message, detail=detail)
|
|
308
|
+
|
|
309
|
+
admin_enabled = os.name != "nt"
|
|
310
|
+
admin_enabled = False
|
|
311
|
+
""" internal use, for future enhancements """
|
|
312
|
+
if admin_enabled:
|
|
313
|
+
flask_app.config.update(SQLALCHEMY_BINDS={'admin': 'sqlite:////tmp/4LSBE.sqlite.4'})
|
|
314
|
+
|
|
315
|
+
db = SQLAlchemy()
|
|
316
|
+
db.init_app(flask_app)
|
|
317
|
+
flask_app.db = db
|
|
318
|
+
with flask_app.app_context():
|
|
319
|
+
|
|
320
|
+
with open(Path(project_path).joinpath('security/system/custom_swagger.json')) as json_file:
|
|
321
|
+
custom_swagger = json.load(json_file)
|
|
322
|
+
safrs_api = SAFRSAPI(flask_app, app_db= db, host=args.swagger_host, port=args.swagger_port, client_uri=args.client_uri,
|
|
323
|
+
prefix = args.api_prefix, custom_swagger=custom_swagger)
|
|
324
|
+
|
|
325
|
+
if os.getenv('APILOGICSERVER_ORACLE_THICK'):
|
|
326
|
+
oracledb.init_oracle_client(lib_dir=os.getenv('APILOGICSERVER_ORACLE_THICK'))
|
|
327
|
+
|
|
328
|
+
db = safrs.DB # valid only after is initialized, above
|
|
329
|
+
session: Session = db.session
|
|
330
|
+
|
|
331
|
+
if admin_enabled: # unused (internal dev use)
|
|
332
|
+
db.create_all()
|
|
333
|
+
db.create_all(bind='admin')
|
|
334
|
+
session.commit()
|
|
335
|
+
|
|
336
|
+
from api import expose_api_models, customize_api
|
|
337
|
+
|
|
338
|
+
import database.models
|
|
339
|
+
app_logger.info("Data Model Loaded, customizing...")
|
|
340
|
+
from database import customize_models
|
|
341
|
+
|
|
342
|
+
activate_logicbank(session, constraint_handler)
|
|
343
|
+
|
|
344
|
+
method_decorators : list = []
|
|
345
|
+
safrs_init_logger.setLevel(logging.WARN)
|
|
346
|
+
expose_api_models.expose_models(safrs_api, method_decorators)
|
|
347
|
+
app_logger.info(f'Declare API - api/expose_api_models, endpoint for each table on {args.swagger_host}:{args.swagger_port}, customizing...')
|
|
348
|
+
customize_api.expose_services(flask_app, safrs_api, project_dir, swagger_host=args.swagger_host, PORT=args.port) # custom services
|
|
349
|
+
|
|
350
|
+
if args.security_enabled:
|
|
351
|
+
configure_auth(flask_app, database, method_decorators)
|
|
352
|
+
|
|
353
|
+
if args.security_enabled:
|
|
354
|
+
from security import declare_security # activate security
|
|
355
|
+
app_logger.info("..declare security - security/declare_security.py"
|
|
356
|
+
# not accurate: + f' -- {len(database.database_discovery.authentication_models.metadata.tables)}'
|
|
357
|
+
+ ' authentication tables loaded')
|
|
358
|
+
declare_security_message = declare_security.declare_security_message
|
|
359
|
+
|
|
360
|
+
from api.system.opt_locking import opt_locking
|
|
361
|
+
from config.config import OptLocking
|
|
362
|
+
if args.opt_locking == OptLocking.IGNORED.value:
|
|
363
|
+
app_logger.info("\nOptimistic Locking: ignored")
|
|
364
|
+
else:
|
|
365
|
+
opt_locking.opt_locking_setup(session)
|
|
366
|
+
|
|
367
|
+
kafka_producer.kafka_producer()
|
|
368
|
+
kafka_consumer.kafka_consumer(safrs_api = safrs_api)
|
|
369
|
+
|
|
370
|
+
n8n_producer.n8n_producer()
|
|
371
|
+
|
|
372
|
+
SAFRSBase._s_auto_commit = False
|
|
373
|
+
session.close()
|
|
374
|
+
|
|
375
|
+
safrs.log.setLevel(safrs_log_level)
|
|
376
|
+
db_logger.setLevel(db_log_level)
|
|
377
|
+
authorization_logger.setLevel(authorization_log_level)
|
|
378
|
+
|
|
379
|
+
if os.getenv('APILOGICPROJECT_DEBUG'): # temp debug since logging in config is not happening
|
|
380
|
+
KAFKA_SERVER = os.getenv('KAFKA_SERVER')
|
|
381
|
+
is_empty = False
|
|
382
|
+
if KAFKA_SERVER is not None:
|
|
383
|
+
is_empty = KAFKA_SERVER == ""
|
|
384
|
+
is_none = KAFKA_SERVER is None
|
|
385
|
+
app_logger.debug(f'\nDEBUG KAFKA_SERVER: [{KAFKA_SERVER}] (is_empty: {is_empty}) (is_none: {is_none}) \n')
|
|
386
|
+
app_logger.debug(f'... Args.instance.kafka_producer: {Args.instance.kafka_producer}\n')
|
|
387
|
+
|
|
388
|
+
|
|
@@ -0,0 +1,136 @@
|
|
|
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
|
+
return query.filter(operator.and_(*expressions))
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class TestBase(Base):
|
|
125
|
+
__abstract__ = True
|
|
126
|
+
def __init__(self, *args, **kwargs):
|
|
127
|
+
for name, val in kwargs.items():
|
|
128
|
+
col = getattr(self.__class__, name)
|
|
129
|
+
if 'amount_total' == name:
|
|
130
|
+
debug_stop = 'stop'
|
|
131
|
+
if val is not None:
|
|
132
|
+
if str(col.type) in ["DATE", "DATETIME"]:
|
|
133
|
+
pass
|
|
134
|
+
else:
|
|
135
|
+
kwargs[name] = col.type.python_type(val)
|
|
136
|
+
return super().__init__(*args, **kwargs)
|
|
Binary file
|
|
@@ -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`.
|