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.
Files changed (95) hide show
  1. api_logic_server_cli/add_cust/add_cust.py +269 -0
  2. api_logic_server_cli/api_logic_server.py +18 -238
  3. api_logic_server_cli/api_logic_server_info.yaml +3 -3
  4. api_logic_server_cli/cli.py +38 -28
  5. api_logic_server_cli/create_from_model/__pycache__/api_logic_server_utils.cpython-312.pyc +0 -0
  6. api_logic_server_cli/create_from_model/__pycache__/dbml.cpython-312.pyc +0 -0
  7. api_logic_server_cli/create_from_model/__pycache__/ont_build.cpython-312.pyc +0 -0
  8. api_logic_server_cli/create_from_model/__pycache__/ont_create.cpython-312.pyc +0 -0
  9. api_logic_server_cli/create_from_model/api_logic_server_utils.py +47 -0
  10. api_logic_server_cli/create_from_model/dbml.py +113 -58
  11. api_logic_server_cli/create_from_model/ont_build.py +83 -60
  12. api_logic_server_cli/create_from_model/ont_create.py +2 -1
  13. api_logic_server_cli/database/basic_demo.sqlite +0 -0
  14. api_logic_server_cli/database/basic_demo.txt +1 -0
  15. api_logic_server_cli/database/basic_demo_wg.sqlite +0 -0
  16. api_logic_server_cli/manager.py +3 -2
  17. api_logic_server_cli/prototypes/base/.vscode/launch.json +3 -2
  18. api_logic_server_cli/prototypes/base/config/config.py +66 -11
  19. api_logic_server_cli/prototypes/base/config/default.env +7 -1
  20. api_logic_server_cli/prototypes/base/database/test_data/readme.md +2 -1
  21. api_logic_server_cli/prototypes/base/integration/kafka/kafka_producer.py +5 -2
  22. api_logic_server_cli/prototypes/base/integration/n8n/n8n_producer.py +68 -21
  23. api_logic_server_cli/prototypes/base/integration/n8n/n8n_readme.md +19 -0
  24. api_logic_server_cli/prototypes/base/test/basic/server_test.py +1 -1
  25. api_logic_server_cli/prototypes/basic_demo/README.md +29 -52
  26. api_logic_server_cli/prototypes/basic_demo/customizations/api/.DS_Store +0 -0
  27. api_logic_server_cli/prototypes/basic_demo/customizations/api/api_discovery/mcp_discovery.py +139 -0
  28. api_logic_server_cli/prototypes/basic_demo/customizations/api/api_discovery/openapi.py +92 -0
  29. api_logic_server_cli/prototypes/basic_demo/customizations/config/default.env +13 -0
  30. api_logic_server_cli/prototypes/basic_demo/customizations/config/server_setup.py +388 -0
  31. api_logic_server_cli/prototypes/basic_demo/customizations/database/db.sqlite +0 -0
  32. api_logic_server_cli/prototypes/basic_demo/customizations/database/models.py +131 -0
  33. api_logic_server_cli/prototypes/basic_demo/customizations/database/system/SAFRSBaseX.py +136 -0
  34. api_logic_server_cli/prototypes/basic_demo/customizations/integration/.DS_Store +0 -0
  35. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/.DS_Store +0 -0
  36. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/README_mcp.md +15 -0
  37. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/mcp_client_executor.py +350 -0
  38. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/mcp_schema.txt +47 -0
  39. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/mcp_server_discovery.json +9 -0
  40. api_logic_server_cli/prototypes/{nw_no_cust/integration/mcp → basic_demo/customizations/integration/openai_function}/3_executor_test_agent.py +20 -6
  41. api_logic_server_cli/prototypes/basic_demo/customizations/integration/openai_function/README_functon.md +201 -0
  42. api_logic_server_cli/prototypes/basic_demo/customizations/integration/openai_function/ai_plugin.json +17 -0
  43. api_logic_server_cli/prototypes/basic_demo/customizations/integration/openai_function/nw-swagger_3.json +1731 -0
  44. api_logic_server_cli/prototypes/basic_demo/customizations/integration/openai_function/snippets.txt +5 -0
  45. api_logic_server_cli/prototypes/basic_demo/customizations/integration/openai_function/swagger_3 genai_demo_with_get.json +1731 -0
  46. api_logic_server_cli/prototypes/basic_demo/customizations/integration/openai_function/swagger_3.json +1782 -0
  47. api_logic_server_cli/prototypes/basic_demo/customizations/integration/openai_function/swagger_3_genai_demo.json +264 -0
  48. api_logic_server_cli/prototypes/basic_demo/customizations/integration/openai_function/swagger_3_genai_demo_with_update.json +1782 -0
  49. api_logic_server_cli/prototypes/basic_demo/customizations/logic/declare_logic.py +79 -41
  50. api_logic_server_cli/prototypes/basic_demo/customizations/security/declare_security.py +11 -12
  51. api_logic_server_cli/prototypes/basic_demo/customizations/ui/admin/admin.yaml +166 -0
  52. api_logic_server_cli/prototypes/basic_demo/iteration/api/{customize_api.py → api_discovery/order_b2b.py} +17 -23
  53. api_logic_server_cli/prototypes/basic_demo/iteration/database/db.sqlite +0 -0
  54. api_logic_server_cli/prototypes/basic_demo/iteration/integration/row_dict_maps/OrderB2B.py +6 -5
  55. api_logic_server_cli/prototypes/basic_demo/iteration/integration/row_dict_maps/OrderShipping.py +4 -4
  56. api_logic_server_cli/prototypes/basic_demo/iteration/logic/declare_logic.py +69 -43
  57. api_logic_server_cli/prototypes/basic_demo/iteration/ui/admin/admin.yaml +125 -50
  58. api_logic_server_cli/prototypes/manager/README.md +4 -0
  59. api_logic_server_cli/prototypes/nw/logic/declare_logic.py +2 -2
  60. api_logic_server_cli/prototypes/nw_no_cust/.obsidian/app.json +1 -0
  61. api_logic_server_cli/prototypes/nw_no_cust/.obsidian/appearance.json +1 -0
  62. api_logic_server_cli/prototypes/nw_no_cust/.obsidian/core-plugins.json +31 -0
  63. api_logic_server_cli/prototypes/nw_no_cust/.obsidian/workspace.json +166 -0
  64. api_logic_server_cli/prototypes/nw_no_cust/Tutorial.md +45 -26
  65. api_logic_server_cli/prototypes/nw_no_cust/api/api_discovery/openapi.py +130 -0
  66. api_logic_server_cli/prototypes/nw_no_cust/api/api_discovery/proper_update_def.json +71 -0
  67. api_logic_server_cli/prototypes/nw_no_cust/config/default.env +13 -0
  68. api_logic_server_cli/prototypes/ont_app/ontimize_seed/package-lock.json +9725 -1180
  69. api_logic_server_cli/prototypes/ont_app/ontimize_seed/package.json +3 -6
  70. api_logic_server_cli/prototypes/ont_app/ontimize_seed/src/app/shared/app.services.config.ts +1 -1
  71. api_logic_server_cli/prototypes/ont_app/ontimize_seed/src/assets/css/app.scss +4 -0
  72. api_logic_server_cli/prototypes/ont_app/ontimize_seed/src/assets/i18n/en.json +1 -1
  73. api_logic_server_cli/prototypes/ont_app/ontimize_seed/src/assets/i18n/es.json +14 -12
  74. api_logic_server_cli/prototypes/ont_app/templates/app_config.jinja +1 -1
  75. api_logic_server_cli/prototypes/ont_app/templates/date_template.html +1 -1
  76. api_logic_server_cli/prototypes/ont_app/templates/textarea_template.html +1 -1
  77. api_logic_server_cli/prototypes/ont_app/templates/timestamp_template.html +1 -1
  78. api_logic_server_cli/prototypes/sample_ai/logic/declare_logic.py +30 -13
  79. apilogicserver-14.5.3.dist-info/METADATA +168 -0
  80. {apilogicserver-14.4.0.dist-info → apilogicserver-14.5.3.dist-info}/RECORD +84 -61
  81. {apilogicserver-14.4.0.dist-info → apilogicserver-14.5.3.dist-info}/WHEEL +1 -1
  82. api_logic_server_cli/prototypes/basic_demo/apply_customizations.ps1 +0 -17
  83. api_logic_server_cli/prototypes/basic_demo/apply_customizations.sh +0 -14
  84. api_logic_server_cli/prototypes/basic_demo/apply_iteration.ps1 +0 -20
  85. api_logic_server_cli/prototypes/basic_demo/apply_iteration.sh +0 -15
  86. api_logic_server_cli/prototypes/nw_no_cust/integration/mcp/1_langchain_loader.py +0 -19
  87. api_logic_server_cli/prototypes/nw_no_cust/integration/mcp/2_gpt_mcp_prompt.txt +0 -19
  88. api_logic_server_cli/prototypes/nw_no_cust/integration/mcp/README.md +0 -17
  89. api_logic_server_cli/prototypes/nw_no_cust/integration/mcp/resources/curl.txt +0 -4
  90. api_logic_server_cli/prototypes/nw_no_cust/integration/mcp/resources/nw_swagger_3.yaml +0 -16660
  91. api_logic_server_cli/prototypes/nw_no_cust/integration/mcp/run_executor.py +0 -23
  92. apilogicserver-14.4.0.dist-info/METADATA +0 -76
  93. {apilogicserver-14.4.0.dist-info → apilogicserver-14.5.3.dist-info}/entry_points.txt +0 -0
  94. {apilogicserver-14.4.0.dist-info → apilogicserver-14.5.3.dist-info}/licenses/LICENSE +0 -0
  95. {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)
@@ -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`.