-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcustomize_api.py
493 lines (396 loc) · 18.2 KB
/
customize_api.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
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
from functools import wraps
import logging
from flask_jwt_extended import get_jwt, jwt_required, verify_jwt_in_request
from config.config import Config, Args
from security.system.authorization import Security
import api.system.api_utils as api_utils
from typing import List
import safrs
import sqlalchemy
from flask import request, jsonify
from safrs import jsonapi_rpc, SAFRSAPI
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import object_mapper
from database import models
from flask_cors import cross_origin
from logic_bank.rule_bank.rule_bank import RuleBank
import integration.system.RowDictMapper as row_dict_mapper
from integration.row_dict_maps.OrderById import OrderById
from integration.row_dict_maps.OrderShipping import OrderShipping
from integration.row_dict_maps.OrderB2B import OrderB2B
# Customize this file to add endpoints/services, using SQLAlchemy as required
# Separate from expose_api_models.py, to simplify merge if project rebuilt
# Called by api_logic_server_run.py
app_logger = logging.getLogger("api_logic_server_app")
def expose_services(app, api, project_dir, swagger_host: str, PORT: str):
""" #als: Customize API - new end points for services
Brief background: see readme_customize_api.md
Your Code Goes Here
Illustrates Customized APIs and Data Access.
1. Observe that APIs not limited to database objects, but are extensible.
2. See: https://apilogicserver.github.io/Docs/Sample-Integration/
3. See: https://apilogicserver.github.io/Docs/API-Customize/
4. See: https://github.com/thomaxxl/safrs/wiki/Customization
#### Illustrate Reusable Integration Services
1. ServicesEndPoint OrderB2B() -
* Illustrates: Reusable IntegrationServices to POST order
2. OrderShipping_Test() -
* Illustrates: SQLAlchemy related row retrieval, reformat as multi-table dict and convert to json
#### Illustrate Using Flask and SQLAlchemy
1. join_order() -
* Illustrates: SQLAlchemy parent join fields
2. CategoriesEndPoint get_cats() - swagger, row security
* Uses row_dict_mapper.rows_to_dict
3. filters_cats() - model query with filters
* Uses manual result creation (not util)
4. raw_sql_cats() - raw sql (non-modeled objects)
* Uses row_dict_mapper.rows_to_dict
"""
app_logger.debug("api/customize_api.py - expose custom services")
api.expose_object(ServicesEndPoint) # Swagger-visible services
api.expose_object(CategoriesEndPoint)
@app.route('/hello_world')
def hello_world():
"""
Illustrates:
* Use standard Flask, here for non-database endpoints.
Test it with:
http://localhost:5656/hello_world?user=ApiLogicServer
"""
user = request.args.get('user')
# app_logger.info(f'hello_world returning: hello, {user}')
app_logger.info(f'{user}')
return jsonify({"result": f'hello, {user}'})
def bypass_security():
"""
Support option to bypass security (see cats, below).
See: https://flask-jwt-extended.readthedocs.io/en/stable/custom_decorators/
"""
def wrapper(fn):
@wraps(fn)
def decorator(*args, **kwargs):
if Config.SECURITY_ENABLED == False:
return fn(*args, **kwargs)
verify_jwt_in_request(True) # must be issued if security enabled
return fn(*args, **kwargs)
return decorator
return wrapper
def admin_required():
"""
Support option to bypass security (see cats, below).
"""
def wrapper(fn):
@wraps(fn)
def decorator(*args, **kwargs):
if Args.instance.security_enabled == False:
return fn(*args, **kwargs)
verify_jwt_in_request(True) # must be issued if security enabled
return fn(*args, **kwargs)
return decorator
return wrapper
@app.route('/OrderShipping_Test', methods=['GET','OPTIONS'])
@admin_required()
@jwt_required()
@cross_origin(supports_credentials=True)
def OrderShipping_Test():
"""
Illustrates redundant (declare_logic)
1. SQLAlchemy row retrieval
2. RowDictMapper to reformat row as multi-table dict, and then json
$(venv) ApiLogicServer login --user=admin --password=p
$(venv) ApiLogicServer curl "http://localhost:5656/OrderShipping_Test?id=10643"
"""
request_id = request.args.get('id')
if request_id is None:
request_id = 10643
db = safrs.DB # #als: SQLAlchemy retrieval
session = db.session # sqlalchemy.orm.scoping.scoped_session
Security.set_user_sa() # an endpoint that requires no auth header (see also @bypass_security)
the_order : models.Order = session.query(models.Order) \
.filter(models.Order.Id == request_id).one()
order_def = OrderShipping()
dict_row = order_def.row_to_dict(row = the_order)
return jsonify({"OrderShipping_Test with Items and Product": dict_row})
#########################################################
# Illustrate using SQLAlchemy in standard Flask endpoints
#########################################################
@app.route('/join_order')
@bypass_security()
def join_order():
"""
Illustrates: SQLAlchemy join fields, by manual code
Better: use RowDictMapper (see OrderB2B, below)
$(venv) ApiLogicServer curl "http://localhost:5656/join_order?id=11077"
Returns:
_type_: _description_
"""
request_id = request.args.get('id')
if request_id is None:
request_id = 11078
db = safrs.DB # Use the safrs.DB, not db!
session = db.session # sqlalchemy.orm.scoping.scoped_session
Security.set_user_sa() # an endpoint that requires no auth header (see also @bypass_security)
the_order : models.Order = session.query(models.Order) \
.filter(models.Order.Id == request_id).one()
dict_row = {}
dict_row["id"] = the_order.Id
dict_row["AmountTotal"] = the_order.AmountTotal
dict_row["SalesRepLastName"] = the_order.Employee.LastName # access join field
return jsonify({"order_with_join_attr": dict_row})
@app.route('/filters_cats')
@bypass_security()
def filters_cats():
"""
Illustrates:
* Explore SQLAlchemy and/or filters.
Test (returns rows 2-5) (no auth):
curl -X GET "http://localhost:5656/filters_cats" [no-filter | simple-filter]"
"""
from sqlalchemy import and_, or_
filter_type = request.args.get('filter')
if filter_type is None:
filter_type = "multiple filters"
db = safrs.DB # Use the safrs.DB, not db!
session = db.session # sqlalchemy.orm.scoping.scoped_session
Security.set_user_sa() # an endpoint that requires no auth header (see also @bypass_security)
if filter_type.startswith("n"):
results = session.query(models.Category) # .filter(models.Category.Id > 1)
elif filter_type.startswith("s"): # normally coded like this
results = session.query(models.Category) \
.filter(models.Category.Id > 1) \
.filter(or_((models.Category.Client_id == 2), (models.Category.Id == 5)))
else: # simulate grant logic (multiple filters)
client_grant = models.Category.Client_id == 2
id_grant = models.Category.Id == 5
grant_filter = or_( client_grant, id_grant)
results = session.query(models.Category) \
.filter(models.Category.Id > 1) \
.filter(grant_filter)
return_result = []
for each_result in results:
row = { 'id': each_result.Id, 'name': each_result.CategoryName}
return_result.append(row)
return jsonify({ "success": True, "result": return_result})
@app.route('/raw_sql_cats')
@bypass_security()
def raw_sql_cats():
"""
Illustrates:
* #als: "Raw" SQLAlchemy table queries (non-mapped objects)
* Observe phyical column name: CategoryName_ColumnName
* Contrast to models.py, get_cats()
Test (auth optional):
curl -X GET "http://localhost:5656/raw_sql_cats"
"""
DB = safrs.DB
sql_query = DB.text("SELECT * FROM CategoryTableNameTest")
with DB.engine.begin() as connection:
query_result = connection.execute(sql_query).all()
rows_to_dict_rows = row_dict_mapper.rows_to_dict(query_result)
response = {"result": rows_to_dict_rows}
return response
###################
# Internal Services
###################
@app.route('/server_log')
def server_log():
"""
Used by test/*.py - enables client app to log msg into server's console log
"""
return api_utils.server_log(request, jsonify)
@app.route('/metadata')
def metadata():
"""
Swagger provides typical API discovery. This is for tool providers
requiring programmatic access to api definition, e.g.,
to drive artifact code generation.
Returns json for list of 1 / all resources, with optional attribute name/type, eg
curl -X GET "http://localhost:5656/metadata?resource=Category&include=attributes"
curl -X GET "http://localhost:5656/metadata?include=attributes"
"""
import inspect
import sys
from sqlalchemy.ext.declarative import declarative_base
resource_name = request.args.get('resource')
include_attributes = False
include = request.args.get('include')
if include:
include_attributes = "attributes" in include
resource_list = [] # array of attributes[], name (so, the name is last...)
resource_objs = {} # objects, named = resource_name
models_name = "database.models"
cls_members = inspect.getmembers(sys.modules["database.models"], inspect.isclass)
for each_cls_member in cls_members:
each_class_def_str = str(each_cls_member)
if (f"'{models_name}." in str(each_class_def_str) and
"Ab" not in str(each_class_def_str)):
each_resource_name = each_cls_member[0]
each_resource_class = each_cls_member[1]
each_resource_mapper = each_resource_class.__mapper__
if resource_name is None or resource_name == each_resource_name:
resource_object = {"name": each_resource_name}
resource_list.append(resource_object)
resource_objs[each_resource_name] = {}
if include_attributes:
attr_list = []
for each_attr in each_resource_mapper.attrs:
if not each_attr._is_relationship:
try:
attribute_object = {"name": each_attr.key,
"type": str(each_attr.expression.type)}
except:
attribute_object = {"name": each_attr.key,
"type": "unkown"}
attr_list.append(attribute_object)
resource_object["attributes"] = attr_list
resource_objs[each_resource_name] = {"attributes": attr_list}
# pick the format you like
return_result = {"resources": resource_list}
return_result = {"resources": resource_objs}
return jsonify(return_result)
@app.route('/stop')
def stop():
"""
Use this to stop the server from the Browser.
* See: https://stackoverflow.com/questions/15562446/how-to-stop-flask-application-without-using-ctrl-c
* See: https://github.com/thomaxxl/safrs/wiki/Customization
Usage:
http://localhost:5656/stop?msg=API stop - Stop API Logic Server
"""
import os, signal
msg = request.args.get('msg')
app_logger.info(f'\nStopped server: {msg}\n')
os.kill(os.getpid(), signal.SIGINT)
return jsonify({ "success": True, "message": "Server is shutting down..." })
"""
Illustrates #als: custom end point with swagger, RowDictMapper
* Custom service - visible in swagger
* Services *not* requiring authentication (contrast to CategoriesEndPoint, below)
* Use OrderB2B (extends RowDictMapper) to map json to rows with aliasing, joins and lookups
* Recall business logic is not in service, but encapsulated for reuse in logic/declare_logic.py
"""
class ServicesEndPoint(safrs.JABase):
@classmethod
@jsonapi_rpc(http_methods=["POST"])
def OrderB2B(self, *args, **kwargs): # yaml comment => swagger description
""" # yaml creates Swagger description
args :
order:
AccountId: "ALFKI"
Given: "Steven"
Surname: "Buchanan"
Items :
- ProductName: "Chai"
QuantityOrdered: 1
- ProductName: "Chang"
QuantityOrdered: 2
---
Note attribute alias, Lookup automation in OrderB2B
See: https://apilogicserver.github.io/Docs/Sample-Integration/
Test with swagger, or, from command line:
$(venv) ApiLogicServer login --user=admin --password=p
$(venv) ApiLogicServer curl "'POST' 'http://localhost:5656/api/ServicesEndPoint/OrderB2B'" --data '
{"meta": {"args": {"order": {
"AccountId": "ALFKI",
"Surname": "Buchanan",
"Given": "Steven",
"Items": [
{
"ProductName": "Chai",
"QuantityOrdered": 1
},
{
"ProductName": "Chang",
"QuantityOrdered": 2
}
]
}
}}}'
"""
db = safrs.DB # Use the safrs.DB, not db!
session = db.session # sqlalchemy.orm.scoping.scoped_session
order_b2b_def = OrderB2B()
request_dict_data = request.json["meta"]["args"]["order"]
sql_alchemy_row = order_b2b_def.dict_to_row(row_dict = request_dict_data, session = session)
session.add(sql_alchemy_row)
return {"Thankyou For Your OrderB2B"} # automatic commit, which executes transaction logic
#################################################################################
# The example above is a best practice,
# using the OrderB2B object for mapping, alias, and lookup IntegrationServices.
#
# Contrast to the discouraged examples below
#################################################################################
@classmethod
@jsonapi_rpc(http_methods=["POST"])
def add_order_by_id(self, *args, **kwargs): # yaml comment => swagger description
""" # yaml creates Swagger description
args:
order :
AccountId: ALFKI
SalesRepId: 1
Items :
- ProductId: 1
QuantityOrdered: 1
- ProductId: 2
QuantityOrdered: 2
---
Best Practice: use OrderB2B for automated map/alias, Lookups etc.
eg, how would a B2B partner determine a SalesRepId or a ProductId?
Test using swagger -> try it out (includes sample data, above)
"""
db = safrs.DB # Use the safrs.DB, not db!
session = db.session # sqlalchemy.orm.scoping.scoped_session
order_id_def = OrderById()
request_dict_data = request.json["meta"]["args"]["order"]
sql_alchemy_row = order_id_def.dict_to_row(row_dict = request_dict_data, session = session)
session.add(sql_alchemy_row)
return {"Thankyou For Your OrderById"} # automatic commit, which executes transaction logic
@classmethod
@jsonapi_rpc(http_methods=["POST"])
def add_order(self, *args, **kwargs): # yaml comment => swagger description
""" # yaml creates Swagger description
args :
CustomerId: ALFKI
EmployeeId: 1
Freight: 10
OrderDetailList :
- ProductId: 1
Quantity: 1
Discount: 0
- ProductId: 2
Quantity: 2
Discount: 0
---
Best Practice: use OrderB2B for automated map/alias, Lookups etc.
Test using swagger -> try it out (includes sample data, above)
"""
db = safrs.DB # Use the safrs.DB, not db!
session = db.session # sqlalchemy.orm.scoping.scoped_session
new_order = models.Order()
session.add(new_order)
row_dict_mapper.json_to_entities(kwargs, new_order) # generic function - any db object
return {"Thankyou For Your Order"} # automatic commit, which executes transaction logic
"""
Illustrates #als: auth required
* Swagger-visible RPC that requires authentication (@jwt_required()).
* Row Security
Test in swagger (auth required)
* Post to endpoint auth to obtain <access_token> value - copy to clipboard
* Row Security - Users determines results
* u1 - 1 row, u2 - 4 rows, admin - 9 rows
* Authorize (top of swagger), using Bearer <access_token>
* Post to CategoriesEndPoint/get_cats, observe results depend on login
"""
class CategoriesEndPoint(safrs.JABase):
@staticmethod
@jwt_required()
@jsonapi_rpc(http_methods=['POST'], valid_jsonapi=False)
def get_cats():
db = safrs.DB
session = db.session
result = session.query(models.Category)
for each_row in result:
app_logger.debug(f'each_row: {each_row}')
rows = row_dict_mapper.rows_to_dict(result)
response = {"result": rows}
return response