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
|
@@ -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,131 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
from sqlalchemy import DECIMAL, DateTime # API Logic Server GenAI assist
|
|
3
|
+
from sqlalchemy import Boolean, Column, DECIMAL, Date, ForeignKey, Integer, String
|
|
4
|
+
from sqlalchemy.orm import relationship
|
|
5
|
+
from sqlalchemy.ext.declarative import declarative_base
|
|
6
|
+
|
|
7
|
+
########################################################################################################################
|
|
8
|
+
# Classes describing database for SqlAlchemy ORM, initially created by schema introspection.
|
|
9
|
+
#
|
|
10
|
+
# Alter this file per your database maintenance policy
|
|
11
|
+
# See https://apilogicserver.github.io/Docs/Project-Rebuild/#rebuilding
|
|
12
|
+
#
|
|
13
|
+
# Created: May 14, 2025 10:47:47
|
|
14
|
+
# Database: sqlite:////Users/val/dev/ApiLogicServer/ApiLogicServer-dev/servers/basic_demo/database/db.sqlite
|
|
15
|
+
# Dialect: sqlite
|
|
16
|
+
#
|
|
17
|
+
# mypy: ignore-errors
|
|
18
|
+
########################################################################################################################
|
|
19
|
+
|
|
20
|
+
from database.system.SAFRSBaseX import SAFRSBaseX, TestBase
|
|
21
|
+
from flask_login import UserMixin
|
|
22
|
+
import safrs, flask_sqlalchemy, os
|
|
23
|
+
from safrs import jsonapi_attr
|
|
24
|
+
from flask_sqlalchemy import SQLAlchemy
|
|
25
|
+
from sqlalchemy.orm import relationship
|
|
26
|
+
from sqlalchemy.orm import Mapped
|
|
27
|
+
from sqlalchemy.sql.sqltypes import NullType
|
|
28
|
+
from typing import List
|
|
29
|
+
|
|
30
|
+
db = SQLAlchemy()
|
|
31
|
+
Base = declarative_base() # type: flask_sqlalchemy.model.DefaultMeta
|
|
32
|
+
metadata = Base.metadata
|
|
33
|
+
|
|
34
|
+
#NullType = db.String # datatype fixup
|
|
35
|
+
#TIMESTAMP= db.TIMESTAMP
|
|
36
|
+
|
|
37
|
+
from sqlalchemy.dialects.sqlite import *
|
|
38
|
+
|
|
39
|
+
if os.getenv('APILOGICPROJECT_NO_FLASK') is None or os.getenv('APILOGICPROJECT_NO_FLASK') == 'None':
|
|
40
|
+
Base = SAFRSBaseX # enables rules to be used outside of Flask, e.g., test data loading
|
|
41
|
+
else:
|
|
42
|
+
Base = TestBase # ensure proper types, so rules work for data loading
|
|
43
|
+
print('*** Models.py Using TestBase ***')
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Customer(Base): # type: ignore
|
|
48
|
+
__tablename__ = 'customer'
|
|
49
|
+
_s_collection_name = 'Customer' # type: ignore
|
|
50
|
+
|
|
51
|
+
id = Column(Integer, primary_key=True)
|
|
52
|
+
name = Column(String)
|
|
53
|
+
balance : DECIMAL = Column(DECIMAL)
|
|
54
|
+
credit_limit : DECIMAL = Column(DECIMAL)
|
|
55
|
+
email = Column(String)
|
|
56
|
+
email_opt_out = Column(Boolean)
|
|
57
|
+
|
|
58
|
+
# parent relationships (access parent)
|
|
59
|
+
|
|
60
|
+
# child relationships (access children)
|
|
61
|
+
EmailList : Mapped[List["Email"]] = relationship(back_populates="customer")
|
|
62
|
+
OrderList : Mapped[List["Order"]] = relationship(back_populates="customer")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class Product(Base): # type: ignore
|
|
67
|
+
__tablename__ = 'product'
|
|
68
|
+
_s_collection_name = 'Product' # type: ignore
|
|
69
|
+
|
|
70
|
+
id = Column(Integer, primary_key=True)
|
|
71
|
+
name = Column(String)
|
|
72
|
+
unit_price : DECIMAL = Column(DECIMAL)
|
|
73
|
+
|
|
74
|
+
# parent relationships (access parent)
|
|
75
|
+
|
|
76
|
+
# child relationships (access children)
|
|
77
|
+
ItemList : Mapped[List["Item"]] = relationship(back_populates="product")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class Email(Base): # type: ignore
|
|
82
|
+
__tablename__ = 'email'
|
|
83
|
+
_s_collection_name = 'Email' # type: ignore
|
|
84
|
+
|
|
85
|
+
id = Column(Integer, primary_key=True)
|
|
86
|
+
message = Column(String)
|
|
87
|
+
customer_id = Column(ForeignKey('customer.id'), nullable=False)
|
|
88
|
+
CreatedOn = Column(Date)
|
|
89
|
+
|
|
90
|
+
# parent relationships (access parent)
|
|
91
|
+
customer : Mapped["Customer"] = relationship(back_populates=("EmailList"))
|
|
92
|
+
|
|
93
|
+
# child relationships (access children)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class Order(Base): # type: ignore
|
|
98
|
+
__tablename__ = 'order'
|
|
99
|
+
_s_collection_name = 'Order' # type: ignore
|
|
100
|
+
|
|
101
|
+
id = Column(Integer, primary_key=True)
|
|
102
|
+
notes = Column(String)
|
|
103
|
+
customer_id = Column(ForeignKey('customer.id'), nullable=False)
|
|
104
|
+
CreatedOn = Column(Date)
|
|
105
|
+
date_shipped = Column(Date)
|
|
106
|
+
amount_total : DECIMAL = Column(DECIMAL)
|
|
107
|
+
|
|
108
|
+
# parent relationships (access parent)
|
|
109
|
+
customer : Mapped["Customer"] = relationship(back_populates=("OrderList"))
|
|
110
|
+
|
|
111
|
+
# child relationships (access children)
|
|
112
|
+
ItemList : Mapped[List["Item"]] = relationship(back_populates="order")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class Item(Base): # type: ignore
|
|
117
|
+
__tablename__ = 'item'
|
|
118
|
+
_s_collection_name = 'Item' # type: ignore
|
|
119
|
+
|
|
120
|
+
id = Column(Integer, primary_key=True)
|
|
121
|
+
order_id = Column(ForeignKey('order.id'))
|
|
122
|
+
product_id = Column(ForeignKey('product.id'), nullable=False)
|
|
123
|
+
quantity = Column(Integer, nullable=False)
|
|
124
|
+
amount : DECIMAL = Column(DECIMAL)
|
|
125
|
+
unit_price : DECIMAL = Column(DECIMAL)
|
|
126
|
+
|
|
127
|
+
# parent relationships (access parent)
|
|
128
|
+
order : Mapped["Order"] = relationship(back_populates=("ItemList"))
|
|
129
|
+
product : Mapped["Product"] = relationship(back_populates=("ItemList"))
|
|
130
|
+
|
|
131
|
+
# child relationships (access children)
|
|
@@ -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
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Model Context Protocol is a way for:
|
|
2
|
+
|
|
3
|
+
1. **Bus User ad hoc flows** using existing published mcp services (vs. hard-coding in IT as an endpoint; flows can be cached for repeated use)
|
|
4
|
+
|
|
5
|
+
* ***Natural Language access*** to corporate databases for improved user interfaces
|
|
6
|
+
|
|
7
|
+
* LLMs ***choreograph*** multiple MCP calls (to 1 or more MCP servers) in a chain of calls - an agentic workflow. MCPs support shared contexts and goals, enabling the LLM to use the result from 1 call to determine whether the goals has been reached, or which service is appropriate to call next
|
|
8
|
+
|
|
9
|
+
3. Chat agents to ***discover*** and ***call*** external servers, be they databases, APIs, file systems, etc. MCPs support shared contexts and goals, enabling the LLM
|
|
10
|
+
|
|
11
|
+
* ***Corporate database participation*** in such flows, by making key functions available as MCP calls.
|
|
12
|
+
|
|
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`.
|