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.
Files changed (49) hide show
  1. api_logic_server_cli/add_cust/add_cust.py +7 -21
  2. api_logic_server_cli/api_logic_server.py +4 -2
  3. api_logic_server_cli/api_logic_server_info.yaml +2 -2
  4. api_logic_server_cli/create_from_model/__pycache__/dbml.cpython-312.pyc +0 -0
  5. api_logic_server_cli/create_from_model/__pycache__/ont_build.cpython-312.pyc +0 -0
  6. api_logic_server_cli/prototypes/basic_demo/customizations/api/api_discovery/{mcp_server_executor.py → mcp_discovery.py} +1 -0
  7. api_logic_server_cli/prototypes/basic_demo/customizations/config/server_setup.py +388 -0
  8. api_logic_server_cli/prototypes/basic_demo/customizations/database/system/SAFRSBaseX.py +136 -0
  9. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/.DS_Store +0 -0
  10. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/README_mcp.md +3 -1
  11. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/mcp_client_executor.py +82 -27
  12. api_logic_server_cli/prototypes/basic_demo/customizations/logic/declare_logic.py +22 -2
  13. api_logic_server_cli/prototypes/basic_demo/iteration/logic/declare_logic.py +1 -1
  14. api_logic_server_cli/prototypes/nw/logic/declare_logic.py +2 -2
  15. api_logic_server_cli/prototypes/nw_no_cust/.obsidian/app.json +1 -0
  16. api_logic_server_cli/prototypes/nw_no_cust/.obsidian/appearance.json +1 -0
  17. api_logic_server_cli/prototypes/nw_no_cust/.obsidian/core-plugins.json +31 -0
  18. api_logic_server_cli/prototypes/nw_no_cust/.obsidian/workspace.json +166 -0
  19. apilogicserver-14.5.3.dist-info/METADATA +168 -0
  20. {apilogicserver-14.5.0.dist-info → apilogicserver-14.5.3.dist-info}/RECORD +24 -42
  21. api_logic_server_cli/prototypes/basic_demo/customizations/api/api_discovery/proper_update_def.json +0 -71
  22. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/1_langchain_loader.py +0 -71
  23. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/2_gpt_mcp_prompt.txt +0 -19
  24. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/multi_mcp_flow/multi_mcp_flow.png +0 -0
  25. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/multi_mcp_flow/multi_mcp_orchestration.yaml +0 -49
  26. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/multi_mcp_flow/wny mcp flows.png +0 -0
  27. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/natlang_to_api.py +0 -73
  28. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/curl.txt +0 -5
  29. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/images/MCP Overview.png +0 -0
  30. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/images/MCP_Arch.png +0 -0
  31. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/images/MCP_Overview_Executor.png +0 -0
  32. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/invoke_llm/1 - prompt_messages_array.json +0 -10
  33. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/invoke_llm/2 - completion_tool_context.json +0 -12
  34. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/llm_schema.txt +0 -38
  35. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/nw_swagger_2.yaml +0 -17393
  36. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/nw_swagger_3.yaml +0 -16660
  37. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/nw_swagger_3_relaxed.yaml +0 -109
  38. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/proxy_server.py +0 -51
  39. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/proxy_serverZ.py +0 -72
  40. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/validate_jsonapi.py +0 -64
  41. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/run_executor.py +0 -23
  42. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/swagger_converter.py +0 -65
  43. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/z_old/3_executor_test_agent.py +0 -52
  44. api_logic_server_cli/prototypes/manager/README_X.md +0 -663
  45. apilogicserver-14.5.0.dist-info/METADATA +0 -76
  46. {apilogicserver-14.5.0.dist-info → apilogicserver-14.5.3.dist-info}/WHEEL +0 -0
  47. {apilogicserver-14.5.0.dist-info → apilogicserver-14.5.3.dist-info}/entry_points.txt +0 -0
  48. {apilogicserver-14.5.0.dist-info → apilogicserver-14.5.3.dist-info}/licenses/LICENSE +0 -0
  49. {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 # can't use project.is_genai_demo because this is not the create command...
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
- add_genai_customizations(project=project, do_security=False)
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.00" # last public release: 14.04.00
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 15, 2025 12:52:13
1
+ last_created_date: May 18, 2025 16:45:45
2
2
  last_created_project_name: ../../../servers/basic_demo
3
- last_created_version: 14.04.08
3
+ last_created_version: 14.05.02
@@ -111,6 +111,7 @@ def add_service(app, api, project_dir, swagger_host: str, PORT: str, method_deco
111
111
  ```
112
112
  {
113
113
  "tool_type": "json-api",
114
+ "schema_version": "1.0",
114
115
  "base_url": "https://crm.company.com",
115
116
  "resources": [
116
117
  {
@@ -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)
@@ -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`.