-
Notifications
You must be signed in to change notification settings - Fork 0
/
api_logic_server_run.py
319 lines (254 loc) · 12.7 KB
/
api_logic_server_run.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
#!/usr/bin/env python3
###############################################################################
#
# This file initializes and starts the API Logic Server (v 09.01.27, August 01, 2023 18:02:21):
# $ python3 api_logic_server_run.py [--help]
#
# Then, access the Admin App and API via the Browser, eg:
# http://localhost:5656
#
# You typically do not customize this file,
# except to override Creation Defaults and Logging, below.
#
# See Main Code (at end).
# Use log messages to understand API and Logic activation.
#
###############################################################################
import traceback
try:
import os, logging, logging.config, sys, yaml # failure here means venv probably not set
except:
track = traceback.format_exc()
print(" ")
print(track)
print("venv probably not set")
print("Please see https://apilogicserver.github.io/Docs/Project-Env/ \n")
exit(1)
from flask_sqlalchemy import SQLAlchemy
import json
from pathlib import Path
from config import Args
def is_docker() -> bool:
""" running docker? dir exists: /home/api_logic_server """
path = '/home/api_logic_server'
path_result = os.path.isdir(path) # this *should* exist only on docker
env_result = "DOCKER" == os.getenv('APILOGICSERVER_RUNNING')
# assert path_result == env_result
return path_result
current_path = os.path.abspath(os.path.dirname(__file__))
sys.path.append(current_path)
if is_docker():
sys.path.append(os.path.abspath('/home/api_logic_server'))
project_dir = str(current_path)
os.chdir(project_dir) # so admin app can find images, code
import util as util
logic_logger_activate_debug = False
""" True prints all rules on startup """
args = ""
arg_num = 0
for each_arg in sys.argv:
args += each_arg
arg_num += 1
if arg_num < len(sys.argv):
args += ", "
project_name = os.path.basename(os.path.normpath(current_path))
from typing import TypedDict
import safrs # fails without venv - see https://apilogicserver.github.io/Docs/Project-Env/
from safrs import ValidationError, SAFRSBase, SAFRSAPI as _SAFRSAPI
from logic_bank.logic_bank import LogicBank
from logic_bank.exec_row_logic.logic_row import LogicRow
from logic_bank.rule_type.constraint import Constraint
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import Session
import socket
import warnings
from flask import Flask, redirect, send_from_directory, send_file
from safrs import ValidationError, SAFRSBase, SAFRSAPI
import ui.admin.admin_loader as AdminLoader
from security.system.authentication import configure_auth
import database.multi_db as multi_db
class SAFRSAPI(_SAFRSAPI):
"""
Extends SAFRSAPI to handle client_uri
Args:
_SAFRSAPI (_type_): _description_
"""
def __init__(self, *args, **kwargs):
client_uri = kwargs.pop('client_uri', None)
if client_uri:
kwargs['port'] = None
kwargs['host'] = client_uri
super().__init__(*args, **kwargs)
# ==================================
# LOGGING SETUP
# ==================================
current_path = os.path.abspath(os.path.dirname(__file__))
with open(f'{current_path}/logging.yml','rt') as f: # see also logic/declare_logic.py
config=yaml.safe_load(f.read())
f.close()
logging.config.dictConfig(config) # log levels: notset 0, debug 10, info 20, warn 30, error 40, critical 50
app_logger = logging.getLogger("api_logic_server_app")
debug_value = os.getenv('APILOGICPROJECT_DEBUG')
if debug_value is not None: # > export APILOGICPROJECT_DEBUG=True
debug_value = debug_value.upper()
if debug_value.startswith("F") or debug_value.startswith("N"):
app_logger.setLevel(logging.INFO)
else:
app_logger.setLevel(logging.DEBUG)
app_logger.debug(f'\nDEBUG level set from env\n')
app_logger.info(f'\nAPI Logic Project ({project_name}) Starting with CLI args: \n.. {args}\n')
app_logger.info(f'Created August 01, 2023 18:02:21 at {str(current_path)}\n')
class ValidationErrorExt(ValidationError):
"""
This exception is raised when invalid input has been detected (client side input)
Always send back the message to the client in the response
"""
def __init__(self, message="", status_code=400, api_code=2001, detail=None, error_attributes=None):
Exception.__init__(self)
self.error_attributes = error_attributes
self.status_code = status_code
self.message = message
self.api_code = api_code
self.detail: TypedDict = detail
# ==========================================================
# API Logic Server Setup
# - Opens Database(s)
# - Setup API, Logic, Security, Optimistic Locking
# ==========================================================
def api_logic_server_setup(flask_app: Flask, args: Args):
"""
API Logic Server Setup
1. Opens Database(s)
2. Setup API, Logic, Security, Optimistic Locking
Args:
flask_app (_type_): configured flask_app (servers, ports, db uri's)
args (_type_): typed access to flask_app.config
Raises:
ValidationErrorExt: rebadge LogicBank errors for SAFRS API
"""
from sqlalchemy import exc as sa_exc
global logic_logger_activate_debug
with warnings.catch_warnings():
safrs_log_level = safrs.log.getEffectiveLevel()
db_logger = logging.getLogger('sqlalchemy')
db_log_level = db_logger.getEffectiveLevel()
safrs_init_logger = logging.getLogger("safrs.safrs_init")
do_hide_chatty_logging = True and not args.verbose
if do_hide_chatty_logging and app_logger.getEffectiveLevel() <= logging.INFO:
safrs.log.setLevel(logging.WARN) # notset 0, debug 10, info 20, warn 30, error 40, critical 50
db_logger.setLevel(logging.WARN)
safrs_init_logger.setLevel(logging.WARN)
multi_db.bind_dbs(flask_app)
# https://stackoverflow.com/questions/34674029/sqlalchemy-query-raises-unnecessary-warning-about-sqlite-and-decimal-how-to-spe
warnings.simplefilter("ignore", category=sa_exc.SAWarning) # alert - disable for safety msgs
def constraint_handler(message: str, constraint: Constraint, logic_row: LogicRow):
""" format LogicBank constraint exception for SAFRS """
if constraint.error_attributes:
detail = {"model": logic_row.name, "error_attributes": constraint.error_attributes}
else:
detail = {"model": logic_row.name}
raise ValidationErrorExt(message=message, detail=detail)
admin_enabled = os.name != "nt"
admin_enabled = False
""" internal use, for future enhancements """
if admin_enabled:
flask_app.config.update(SQLALCHEMY_BINDS={'admin': 'sqlite:////tmp/4LSBE.sqlite.4'})
db = SQLAlchemy()
db.init_app(flask_app)
with flask_app.app_context():
with open(Path(current_path).joinpath('security/system/custom_swagger.json')) as json_file:
custom_swagger = json.load(json_file)
safrs_api = SAFRSAPI(flask_app, app_db= db, host=args.swagger_host, port=args.swagger_port, client_uri=args.client_uri,
prefix = args.api_prefix, custom_swagger=custom_swagger)
db = safrs.DB # valid only after is initialized, above
session: Session = db.session
if admin_enabled: # unused (internal dev use)
db.create_all()
db.create_all(bind='admin')
session.commit()
from api import expose_api_models, customize_api
import database.models
app_logger.info("Data Model Loaded, customizing...")
from database import customize_models
from logic import declare_logic
logic_logger = logging.getLogger('logic_logger')
logic_logger_level = logic_logger.getEffectiveLevel()
if logic_logger_activate_debug == False:
logic_logger.setLevel(logging.INFO)
app_logger.info("")
LogicBank.activate(session=session, activator=declare_logic.declare_logic, constraint_event=constraint_handler)
logic_logger.setLevel(logic_logger_level)
app_logger.info("Declare Logic complete - logic/declare_logic.py (rules + code)"
+ f' -- {len(database.models.metadata.tables)} tables loaded\n') # db opened 1st access
method_decorators : list = []
safrs_init_logger.setLevel(logging.WARN)
expose_api_models.expose_models(safrs_api, method_decorators)
app_logger.info(f'Declare API - api/expose_api_models, endpoint for each table on {args.swagger_host}:{args.swagger_port}, customizing...')
customize_api.expose_services(flask_app, safrs_api, project_dir, swagger_host=args.swagger_host, PORT=args.port) # custom services
if args.security_enabled:
configure_auth(flask_app, database, method_decorators)
multi_db.expose_db_apis(flask_app, session, safrs_api, method_decorators)
if args.security_enabled:
from security import declare_security # activate security
app_logger.info("..declare security - security/declare_security.py"
# not accurate: + f' -- {len(database.authentication_models.metadata.tables)}'
+ ' authentication tables loaded')
from api.system.opt_locking import opt_locking
from config import OptLocking
if args.opt_locking == OptLocking.IGNORED.value:
app_logger.info("\nOptimistic Locking: ignored")
else:
opt_locking.opt_locking_setup(session)
SAFRSBase._s_auto_commit = False
session.close()
safrs.log.setLevel(safrs_log_level)
db_logger.setLevel(db_log_level)
# ==================================
# MAIN CODE
# ==================================
flask_app = Flask("API Logic Server", template_folder='ui/templates') # templates to load ui/admin/admin.yaml
args = Args(flask_app=flask_app) # creation defaults
flask_app.config.from_object("config.Config")
app_logger.debug(f"\nConfig args: \n{args}") # config file (e.g., db uri's)
args.get_cli_args(dunder_name=__name__, args=args)
app_logger.debug(f"\nCLI args: \n{args}") # api_logic_server_run cl args
flask_app.config.from_prefixed_env(prefix="APILOGICPROJECT") # env overrides (e.g., docker)
app_logger.debug(f"\nENV args: \n{args}\n\n")
if args.verbose:
app_logger.setLevel(logging.DEBUG)
safrs.log.setLevel(logging.DEBUG) # notset 0, debug 10, info 20, warn 30, error 40, critical 50
if app_logger.getEffectiveLevel() <= logging.DEBUG:
util.sys_info(flask_app.config)
app_logger.debug(f"\nENV args: \n{args}\n\n")
api_logic_server_setup(flask_app, args)
AdminLoader.admin_events(flask_app = flask_app, args = args, validation_error = ValidationError)
if __name__ == "__main__":
msg = f'API Logic Project loaded (not WSGI), version 09.01.27\n'
if is_docker():
msg += f' (running from docker container at flask_host: {args.flask_host} - may require refresh)\n'
else:
msg += f' (running locally at flask_host: {args.flask_host})\n'
app_logger.info(f'\n{msg}')
if args.create_and_run:
app_logger.info(f'==> Customizable API Logic Project created and running:\n'
f'..Open it with your IDE at {project_dir}\n')
if os.getenv('CODESPACES'):
app_logger.info(f'API Logic Project (name: {project_name}) starting on Codespaces:\n'
f'..Explore data and API on codespaces, swagger_host: {args.http_scheme}://{args.swagger_host}/\n')
else:
app_logger.info(f'API Logic Project (name: {project_name}) starting:\n'
f'..Explore data and API at http_scheme://swagger_host:port {args.http_scheme}://{args.swagger_host}:{args.port}\n'
f'.... with flask_host: {args.flask_host}\n'
f'.... and swagger_port: {args.swagger_port}')
flask_app.run(host=args.flask_host, threaded=True, port=args.port)
else:
msg = f'API Logic Project Loaded (WSGI), version 09.01.27\n'
if is_docker():
msg += f' (running from docker container at {args.flask_host} - may require refresh)\n'
else:
msg += f' (running locally at flask_host: {args.flask_host})\n'
app_logger.info(f'\n{msg}')
app_logger.info(f'API Logic Project (name: {project_name}) starting:\n'
f'..Explore data and API at http_scheme://swagger_host:port {args.http_scheme}://{args.swagger_host}:{args.port}\n'
f'.... with flask_host: {args.flask_host}\n'
f'.... and swagger_port: {args.swagger_port}')