ApiLogicServer 15.0.37__py3-none-any.whl → 15.0.40__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- api_logic_server_cli/api_logic_server.py +2 -2
- api_logic_server_cli/api_logic_server_info.yaml +2 -2
- api_logic_server_cli/genai/genai_svcs.py +4 -3
- api_logic_server_cli/prototypes/base/.copilot-instructions.md +10 -0
- api_logic_server_cli/prototypes/base/.vscode/.copilot-instructions.md +178 -0
- api_logic_server_cli/prototypes/base/README_PROJECT.md +43 -0
- api_logic_server_cli/prototypes/base/api_logic_server_run.py +20 -8
- api_logic_server_cli/prototypes/base/docs/training/react_map.prompt.md +13 -0
- api_logic_server_cli/prototypes/base/docs/training/react_tree.prompt.md +10 -0
- api_logic_server_cli/prototypes/base/integration/mcp/readme-mcp.md +9 -0
- api_logic_server_cli/prototypes/base/logic/logic_discovery/readme_logic_discovery.md +9 -0
- api_logic_server_cli/prototypes/base/logic/logic_discovery/use_case.py +27 -0
- api_logic_server_cli/prototypes/base/logic/{readme_declare_logic.md → readme_logic.md} +70 -1
- api_logic_server_cli/prototypes/base/readme.md +30 -5
- api_logic_server_cli/prototypes/base/security/readme_security.md +17 -0
- api_logic_server_cli/prototypes/manager/.copilot-instructions.md +13 -0
- api_logic_server_cli/prototypes/manager/.vscode/.copilot-instructions.md +58 -0
- api_logic_server_cli/prototypes/manager/.vscode/ApiLogicServer.code-workspace +2 -2
- api_logic_server_cli/prototypes/manager/README.md +13 -1
- api_logic_server_cli/prototypes/manager/system/Manager_workspace.code-workspace +3 -3
- api_logic_server_cli/prototypes/nw/api/api_discovery/authentication_expose_api_models.py +53 -0
- api_logic_server_cli/prototypes/nw/api/api_discovery/auto_discovery.py +27 -0
- api_logic_server_cli/prototypes/nw/api/api_discovery/count_orders_by_month.html +76 -0
- api_logic_server_cli/prototypes/nw/api/api_discovery/count_orders_by_month.sql +1 -0
- api_logic_server_cli/prototypes/nw/api/api_discovery/dashboard_services.py +143 -0
- api_logic_server_cli/prototypes/nw/api/api_discovery/mcp_discovery.py +97 -0
- api_logic_server_cli/prototypes/nw/api/api_discovery/new_service.py +21 -0
- api_logic_server_cli/prototypes/nw/api/api_discovery/newer_service.py +21 -0
- api_logic_server_cli/prototypes/nw/api/api_discovery/number_of_sales_per_category.html +76 -0
- api_logic_server_cli/prototypes/nw/api/api_discovery/number_of_sales_per_category.sql +1 -0
- api_logic_server_cli/prototypes/nw/api/api_discovery/ontimize_api.py +495 -0
- api_logic_server_cli/prototypes/nw/api/api_discovery/sales_by_category.html +76 -0
- api_logic_server_cli/prototypes/nw/api/api_discovery/sales_by_category.sql +1 -0
- api_logic_server_cli/prototypes/nw/api/api_discovery/system.py +77 -0
- api_logic_server_cli/prototypes/nw/database/database_discovery/graphics_services.py +173 -0
- api_logic_server_cli/prototypes/nw/ui/admin/home.js +5 -0
- api_logic_server_cli/prototypes/nw/ui/reference_react_app/DEPARTMENT_TREE_VIEW.md +66 -0
- api_logic_server_cli/prototypes/nw/ui/reference_react_app/README.md +21 -4
- api_logic_server_cli/prototypes/nw/ui/reference_react_app/package.json +4 -0
- api_logic_server_cli/prototypes/nw/ui/reference_react_app/public/index.html +3 -0
- api_logic_server_cli/prototypes/nw/ui/reference_react_app/src/App.js +8 -1
- api_logic_server_cli/prototypes/nw/ui/reference_react_app/src/CustomLayout.js +20 -0
- api_logic_server_cli/prototypes/nw/ui/reference_react_app/src/Department.js +511 -24
- api_logic_server_cli/prototypes/nw/ui/reference_react_app/src/DepartmentTree.js +147 -0
- api_logic_server_cli/prototypes/nw/ui/reference_react_app/src/Employee.js +230 -18
- api_logic_server_cli/prototypes/nw/ui/reference_react_app/src/LandingPage.js +264 -0
- api_logic_server_cli/prototypes/nw/ui/reference_react_app/src/Supplier.js +359 -121
- api_logic_server_cli/prototypes/nw/ui/reference_react_app/src/index.js +1 -0
- {apilogicserver-15.0.37.dist-info → apilogicserver-15.0.40.dist-info}/METADATA +1 -1
- {apilogicserver-15.0.37.dist-info → apilogicserver-15.0.40.dist-info}/RECORD +54 -25
- api_logic_server_cli/prototypes/base/docs/training/admin_app_unused.md +0 -156
- {apilogicserver-15.0.37.dist-info → apilogicserver-15.0.40.dist-info}/WHEEL +0 -0
- {apilogicserver-15.0.37.dist-info → apilogicserver-15.0.40.dist-info}/entry_points.txt +0 -0
- {apilogicserver-15.0.37.dist-info → apilogicserver-15.0.40.dist-info}/licenses/LICENSE +0 -0
- {apilogicserver-15.0.37.dist-info → apilogicserver-15.0.40.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
from functools import wraps
|
|
2
|
+
import logging
|
|
3
|
+
import api.system.api_utils as api_utils
|
|
4
|
+
import contextlib
|
|
5
|
+
import yaml
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from flask_cors import cross_origin
|
|
8
|
+
import safrs
|
|
9
|
+
from flask import request, jsonify
|
|
10
|
+
from flask_jwt_extended import get_jwt, jwt_required, verify_jwt_in_request
|
|
11
|
+
from safrs import jsonapi_rpc
|
|
12
|
+
from database import models
|
|
13
|
+
import json
|
|
14
|
+
import sys
|
|
15
|
+
from sqlalchemy import text, select, update, insert, delete
|
|
16
|
+
from sqlalchemy.orm import load_only
|
|
17
|
+
import sqlalchemy
|
|
18
|
+
import requests
|
|
19
|
+
from datetime import date
|
|
20
|
+
from config.config import Args
|
|
21
|
+
from config.config import Config
|
|
22
|
+
import os
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from api.system.expression_parser import parsePayload
|
|
25
|
+
from api.system.gen_pdf_report import gen_report
|
|
26
|
+
from api.system.gen_csv_report import gen_report as csv_gen_report
|
|
27
|
+
from api.system.gen_pdf_report import export_pdf
|
|
28
|
+
#from api.gen_xlsx_report import xlsx_gen_report
|
|
29
|
+
|
|
30
|
+
# This is the Ontimize Bridge API - all endpoints will be prefixed with /ontimizeweb/services/rest
|
|
31
|
+
# called by api_logic_server_run.py, to customize api (new end points, services).
|
|
32
|
+
# separate from expose_api_models.py, to simplify merge if project recreated
|
|
33
|
+
# version 11.x - api_logic_server_cli/prototypes/ont_app/prototype/api/api_discovery/ontimize_api.py
|
|
34
|
+
|
|
35
|
+
app_logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
db = safrs.DB
|
|
38
|
+
session = db.session
|
|
39
|
+
_project_dir = None
|
|
40
|
+
class DotDict(dict):
|
|
41
|
+
""" dot.notation access to dictionary attributes """
|
|
42
|
+
# thanks: https://stackoverflow.com/questions/2352181/how-to-use-a-dot-to-access-members-of-dictionary/28463329
|
|
43
|
+
__getattr__ = dict.get
|
|
44
|
+
__setattr__ = dict.__setitem__
|
|
45
|
+
__delattr__ = dict.__delitem__
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def add_service(app, api, project_dir, swagger_host: str, PORT: str, method_decorators = []):
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
#def expose_services(app, api, project_dir, swagger_host: str, PORT: str):
|
|
52
|
+
# sourcery skip: avoid-builtin-shadow
|
|
53
|
+
""" Ontimize API - new end points for services
|
|
54
|
+
|
|
55
|
+
Brief background: see readme_customize_api.md
|
|
56
|
+
|
|
57
|
+
"""
|
|
58
|
+
_project_dir = project_dir
|
|
59
|
+
app_logger.debug("api/api_discovery/ontimize_api.py - services for ontimize")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def admin_required():
|
|
63
|
+
"""
|
|
64
|
+
Support option to bypass security (see cats, below).
|
|
65
|
+
"""
|
|
66
|
+
def wrapper(fn):
|
|
67
|
+
@wraps(fn)
|
|
68
|
+
def decorator(*args, **kwargs):
|
|
69
|
+
if Args.instance.security_enabled == False:
|
|
70
|
+
return fn(*args, **kwargs)
|
|
71
|
+
verify_jwt_in_request(True) # must be issued if security enabled
|
|
72
|
+
return fn(*args, **kwargs)
|
|
73
|
+
return decorator
|
|
74
|
+
return wrapper
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def gen_export(request) -> any:
|
|
78
|
+
payload = json.loads(request.data) if request.data != b'' else {}
|
|
79
|
+
type = payload.get("type") or "csv"
|
|
80
|
+
entity = payload.get("dao")
|
|
81
|
+
queryParm = payload.get("queryParm") or {}
|
|
82
|
+
columns = payload.get("columns") or []
|
|
83
|
+
columnTitles = payload.get("columnTitles") or []
|
|
84
|
+
if not entity:
|
|
85
|
+
return jsonify({})
|
|
86
|
+
resource = find_model(entity)
|
|
87
|
+
api_clz = resource["model"]
|
|
88
|
+
resources = getMetaData(api_clz.__name__)
|
|
89
|
+
attributes = resources["resources"][api_clz.__name__]["attributes"]
|
|
90
|
+
if type in ["csv",'CSV']:
|
|
91
|
+
return csv_gen_report(api_clz, request, entity, queryParm, columns, columnTitles, attributes)
|
|
92
|
+
elif type == "pdf":
|
|
93
|
+
payload["entity"] = entity
|
|
94
|
+
return export_pdf(api_clz, request, entity, queryParm, columns, columnTitles, attributes)
|
|
95
|
+
#elif type == "xlsx":
|
|
96
|
+
# return xlsx_gen_report(api_clz, request, entity, queryParm, columns, columnTitles, attributes)
|
|
97
|
+
|
|
98
|
+
return jsonify({"code":1,"message":f"Unknown export type {type}","data":None,"sqlTypes":None})
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _gen_report(request) -> any:
|
|
102
|
+
payload = json.loads(request.data)
|
|
103
|
+
|
|
104
|
+
if len(payload) == 3:
|
|
105
|
+
return jsonify({})
|
|
106
|
+
|
|
107
|
+
entity = payload["entity"]
|
|
108
|
+
resource = find_model(entity)
|
|
109
|
+
api_clz = resource["model"]
|
|
110
|
+
resources = getMetaData(api_clz.__name__)
|
|
111
|
+
attributes = resources["resources"][api_clz.__name__]["attributes"]
|
|
112
|
+
|
|
113
|
+
return gen_report(api_clz, request, _project_dir, payload, attributes)
|
|
114
|
+
@app.route("/api/export/csv", methods=['POST','OPTIONS'])
|
|
115
|
+
@app.route("/api/export/pdf", methods=['POST','OPTIONS'])
|
|
116
|
+
@app.route("/ontimizeweb/services/rest/export/pdf", methods=['POST','OPTIONS'])
|
|
117
|
+
@app.route("/ontimizeweb/services/rest/export/csv", methods=['POST','OPTIONS'])
|
|
118
|
+
@cross_origin()
|
|
119
|
+
@admin_required()
|
|
120
|
+
def export():
|
|
121
|
+
print(f"export {request.path}")
|
|
122
|
+
#if request.method == "OPTIONS":
|
|
123
|
+
# return jsonify(success=True)
|
|
124
|
+
return gen_export(request)
|
|
125
|
+
|
|
126
|
+
@app.route("/api/dynamicjasper", methods=['POST','OPTIONS'])
|
|
127
|
+
@app.route("/ontimizeweb/services/rest/dynamicjasper", methods=['POST','OPTIONS'])
|
|
128
|
+
@cross_origin()
|
|
129
|
+
@admin_required()
|
|
130
|
+
def dynamicjasper():
|
|
131
|
+
if request.method == "OPTIONS":
|
|
132
|
+
return jsonify(success=True)
|
|
133
|
+
return _gen_report(request)
|
|
134
|
+
|
|
135
|
+
@app.route("/api/bundle", methods=['POST','OPTIONS'])
|
|
136
|
+
@app.route("/ontimizeweb/services/rest/bundle", methods=['POST','OPTIONS'])
|
|
137
|
+
@cross_origin()
|
|
138
|
+
@admin_required()
|
|
139
|
+
def bundle():
|
|
140
|
+
if request.method == "OPTIONS":
|
|
141
|
+
return jsonify(success=True)
|
|
142
|
+
return jsonify({"code":0,"data":{},"message": None})
|
|
143
|
+
|
|
144
|
+
# Ontimize apiEndpoint path for all services
|
|
145
|
+
@app.route("/ontimizeweb/services/rest/<path:path>", methods=['GET','POST','PUT','PATCH','DELETE','OPTIONS'])
|
|
146
|
+
@cross_origin(supports_credentials=True)
|
|
147
|
+
@admin_required()
|
|
148
|
+
def api_search(path):
|
|
149
|
+
s = path.split("/")
|
|
150
|
+
clz_name = s[0]
|
|
151
|
+
clz_type = None if len(s) == 1 else s[1] #[2] TODO customerType search advancedSearch defer(photo)customerTypeAggregate
|
|
152
|
+
isSearch = s[len(s) -1] == "search"
|
|
153
|
+
method = request.method
|
|
154
|
+
rows = []
|
|
155
|
+
#CORS
|
|
156
|
+
if method == "OPTIONS":
|
|
157
|
+
return jsonify(success=True)
|
|
158
|
+
|
|
159
|
+
if clz_name == "endsession":
|
|
160
|
+
from flask import g
|
|
161
|
+
sessionid = request.args.get("sessionid")
|
|
162
|
+
if "access_token" in g and g.access_token == sessionid:
|
|
163
|
+
g.pop("access_token")
|
|
164
|
+
return jsonify({"code":0,"data":{},"message": None})
|
|
165
|
+
|
|
166
|
+
if clz_name == "dynamicjasper":
|
|
167
|
+
return _gen_report(request)
|
|
168
|
+
|
|
169
|
+
if clz_name in ["listReports", "bundle", "reportstore"]:
|
|
170
|
+
return jsonify({"code":0,"data":{},"message": None})
|
|
171
|
+
|
|
172
|
+
if clz_name == "export":
|
|
173
|
+
return gen_export(request)
|
|
174
|
+
|
|
175
|
+
if request.path == '/ontimizeweb/services/rest/users/login':
|
|
176
|
+
return login(request)
|
|
177
|
+
|
|
178
|
+
#api_clz = api_map.get(clz_name)
|
|
179
|
+
resource = find_model(clz_name)
|
|
180
|
+
if resource == None:
|
|
181
|
+
return jsonify(
|
|
182
|
+
{"code": 1, "message": f"Resource {clz_name} not found", "data": None}
|
|
183
|
+
)
|
|
184
|
+
api_attributes = resource["attributes"]
|
|
185
|
+
api_clz = resource["model"]
|
|
186
|
+
|
|
187
|
+
payload = '{}' if request.data == b'' else json.loads(request.data)
|
|
188
|
+
expressions, filter, columns, sqltypes, offset, pagesize, orderBy, data = parsePayload(api_clz, payload)
|
|
189
|
+
result = {}
|
|
190
|
+
if method == 'GET':
|
|
191
|
+
pagesize = 999 #if isSearch else pagesize
|
|
192
|
+
return get_rows(request, api_clz, filter, orderBy, columns, pagesize, offset)
|
|
193
|
+
|
|
194
|
+
if method in ['PUT','PATCH']:
|
|
195
|
+
sql_alchemy_row = session.query(api_clz).filter(text(filter)).one()
|
|
196
|
+
for key in DotDict(data):
|
|
197
|
+
setattr(sql_alchemy_row, key , DotDict(data)[key])
|
|
198
|
+
session.add(sql_alchemy_row)
|
|
199
|
+
result = sql_alchemy_row
|
|
200
|
+
#stmt = update(api_clz).where(text(filter)).values(data)
|
|
201
|
+
|
|
202
|
+
if method == 'DELETE':
|
|
203
|
+
#stmt = delete(api_clz).where(text(filter))
|
|
204
|
+
sql_alchemy_row = session.query(api_clz).filter(text(filter)).one()
|
|
205
|
+
session.delete(sql_alchemy_row)
|
|
206
|
+
result = sql_alchemy_row
|
|
207
|
+
|
|
208
|
+
if method == 'POST':
|
|
209
|
+
if data != None:
|
|
210
|
+
#this is an insert
|
|
211
|
+
sql_alchemy_row = api_clz()
|
|
212
|
+
row = DotDict(data)
|
|
213
|
+
for attr in api_attributes:
|
|
214
|
+
name = attr["name"]
|
|
215
|
+
if getattr(row, name) != None:
|
|
216
|
+
setattr(sql_alchemy_row, name , row[name])
|
|
217
|
+
session.add(sql_alchemy_row)
|
|
218
|
+
result = sql_alchemy_row
|
|
219
|
+
#stmt = insert(api_clz).values(data)
|
|
220
|
+
|
|
221
|
+
else:
|
|
222
|
+
#GET (sent as POST)
|
|
223
|
+
#rows = get_rows_by_query(api_clz, filter, orderBy, columns, pagesize, offset)
|
|
224
|
+
if "TypeAggregate" in clz_type:
|
|
225
|
+
return get_rows_agg(request, api_clz, clz_type, filter, columns)
|
|
226
|
+
else:
|
|
227
|
+
pagesize = 999 # if isSearch else pagesize
|
|
228
|
+
return get_rows(request, api_clz, None, orderBy, columns, pagesize, offset)
|
|
229
|
+
try:
|
|
230
|
+
session.commit()
|
|
231
|
+
session.flush()
|
|
232
|
+
except Exception as ex:
|
|
233
|
+
session.rollback()
|
|
234
|
+
msg = f"{ex.message if hasattr(ex, 'message') else ex}"
|
|
235
|
+
return jsonify(
|
|
236
|
+
{"code": 1, "message": f"{msg}", "data": [], "sqlTypes": None}
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
return jsonify({"code":0,"message":f"{method}:True","data":result,"sqlTypes":None}) #{f"{method}":True})
|
|
240
|
+
|
|
241
|
+
def find_model(clz_name:str) -> any:
|
|
242
|
+
clz_members = getMetaData()
|
|
243
|
+
resources = clz_members.get("resources")
|
|
244
|
+
for resource in resources:
|
|
245
|
+
if resource == clz_name:
|
|
246
|
+
return resources[resource]
|
|
247
|
+
return None
|
|
248
|
+
|
|
249
|
+
def login(request):
|
|
250
|
+
url = f"{request.scheme}://{request.host}/api/auth/login"
|
|
251
|
+
# no data is passed - uses basic auth in header
|
|
252
|
+
#requests.post(url=url, headers=request.headers, json = {})
|
|
253
|
+
username = ''
|
|
254
|
+
password = ''
|
|
255
|
+
auth = request.headers.get("Authorization", None)
|
|
256
|
+
if auth and auth.startswith("Basic"): # support basic auth
|
|
257
|
+
import base64
|
|
258
|
+
base64_message = auth[6:]
|
|
259
|
+
print(f"auth found: {auth}")
|
|
260
|
+
#base64_message = 'UHl0aG9uIGlzIGZ1bg=='
|
|
261
|
+
base64_bytes = base64_message.encode('ascii')
|
|
262
|
+
message_bytes = base64.b64decode(base64_bytes)
|
|
263
|
+
message = message_bytes.decode('ascii')
|
|
264
|
+
s = message.split(":")
|
|
265
|
+
username = s[0]
|
|
266
|
+
password = s[1]
|
|
267
|
+
from security.authentication_provider.abstract_authentication_provider import Abstract_Authentication_Provider
|
|
268
|
+
from security.system.authentication import create_access_token
|
|
269
|
+
|
|
270
|
+
authentication_provider : Abstract_Authentication_Provider = Config.SECURITY_PROVIDER
|
|
271
|
+
if not authentication_provider:
|
|
272
|
+
return jsonify({"code":1,"message":"No authentication provider configured"}), 401
|
|
273
|
+
user = authentication_provider.get_user(username, password)
|
|
274
|
+
if not user or not authentication_provider.check_password(user = user, password = password):
|
|
275
|
+
return jsonify({"code":1,"message":"Wrong username or password"}), 401
|
|
276
|
+
|
|
277
|
+
access_token = create_access_token(identity=user) # serialize and encode
|
|
278
|
+
from flask import g
|
|
279
|
+
g.access_token = access_token
|
|
280
|
+
#return jsonify(access_token=access_token)
|
|
281
|
+
return jsonify({"code":0,"message":"Login Successful","data":{"access_token":access_token}})
|
|
282
|
+
|
|
283
|
+
def get_rows_agg(request: any, api_clz, agg_type, filter, columns):
|
|
284
|
+
key = api_clz.__name__
|
|
285
|
+
resources = getMetaData(key)
|
|
286
|
+
attributes = resources["resources"][key]["attributes"]
|
|
287
|
+
list_of_columns = ""
|
|
288
|
+
sep = ""
|
|
289
|
+
attr_list = list(api_clz._s_columns)
|
|
290
|
+
table_name = api_clz._s_type
|
|
291
|
+
#api_clz.__mapper__.attrs #TODO map the columns to the attributes to build the select list
|
|
292
|
+
for a in attributes:
|
|
293
|
+
name = a["name"]
|
|
294
|
+
t = a["type"] #INTEGER or VARCHAR(N)
|
|
295
|
+
#list_of_columns.append(api_clz._sa_class_manager.get(n))
|
|
296
|
+
attr = a["attr"]
|
|
297
|
+
#MAY need to do upper case compares
|
|
298
|
+
if name in columns:
|
|
299
|
+
list_of_columns = f'{list_of_columns}{sep}{name}'
|
|
300
|
+
sep = ","
|
|
301
|
+
sql = f' count(*), {list_of_columns} from {table_name} group by {list_of_columns}'
|
|
302
|
+
print(sql)
|
|
303
|
+
# TODO HARDCODED for now....
|
|
304
|
+
data = {}
|
|
305
|
+
if "customerTypeAggregate" == agg_type:
|
|
306
|
+
data = {"data": [
|
|
307
|
+
{
|
|
308
|
+
"AMOUNT": 24,
|
|
309
|
+
"DESCRIPTION": "Normal"
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
"AMOUNT": 15,
|
|
313
|
+
"DESCRIPTION": "VIP"
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
"AMOUNT": 36,
|
|
317
|
+
"DESCRIPTION": "Other"
|
|
318
|
+
}
|
|
319
|
+
]
|
|
320
|
+
}
|
|
321
|
+
elif "accountTypeAggregate" == agg_type:
|
|
322
|
+
data = {"data": [
|
|
323
|
+
{
|
|
324
|
+
"AMOUNT": 32,
|
|
325
|
+
"ACCOUNTTYPENAME": "Savings",
|
|
326
|
+
"ACCOUNTTYPEID": 1
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
"AMOUNT": 36,
|
|
330
|
+
"ACCOUNTTYPENAME": "Checking",
|
|
331
|
+
"ACCOUNTTYPEID": 0
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
"AMOUNT": 30,
|
|
335
|
+
"ACCOUNTTYPENAME": "Payroll",
|
|
336
|
+
"ACCOUNTTYPEID": 3
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
"AMOUNT": 23,
|
|
340
|
+
"ACCOUNTTYPENAME": "Market",
|
|
341
|
+
"ACCOUNTTYPEID": 2
|
|
342
|
+
}
|
|
343
|
+
]
|
|
344
|
+
}
|
|
345
|
+
elif "employeeTypeAggregate" == agg_type:
|
|
346
|
+
data = {"data": [
|
|
347
|
+
{
|
|
348
|
+
"AMOUNT": 27,
|
|
349
|
+
"EMPLOYEETYPENAME": "Manager"
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
"AMOUNT": 485,
|
|
353
|
+
"EMPLOYEETYPENAME": "Employee"
|
|
354
|
+
}
|
|
355
|
+
]
|
|
356
|
+
}
|
|
357
|
+
data["code"] = 0
|
|
358
|
+
data["message"] = ""
|
|
359
|
+
data["sqlType"] = {}
|
|
360
|
+
#rows = session.query(text(sql)).all()
|
|
361
|
+
#rows = session.query(models.Account.ACCOUNTTYPEID,func.count(models.Account.AccountID)).group_by(models.Account.ACCOUNTTYPEID).all()
|
|
362
|
+
return data
|
|
363
|
+
|
|
364
|
+
def get_rows(request: any, api_clz, filter: str, order_by: str, columns: list, pagesize: int, offset: int):
|
|
365
|
+
# New Style
|
|
366
|
+
key = api_clz.__name__.lower()
|
|
367
|
+
resources = getMetaData(api_clz.__name__)
|
|
368
|
+
attributes = resources["resources"][api_clz.__name__]["attributes"]
|
|
369
|
+
list_of_columns = []
|
|
370
|
+
for a in attributes:
|
|
371
|
+
name = a["name"]
|
|
372
|
+
col = a["attr"].columns[0]
|
|
373
|
+
desc = col.description
|
|
374
|
+
t = a["type"] #INTEGER or VARCHAR(N)
|
|
375
|
+
#MAY need to do upper case compares
|
|
376
|
+
if desc in columns:
|
|
377
|
+
list_of_columns.append((col,name))
|
|
378
|
+
else:
|
|
379
|
+
if name in columns:
|
|
380
|
+
list_of_columns.append(name)
|
|
381
|
+
|
|
382
|
+
from api.system.custom_endpoint import CustomEndpoint
|
|
383
|
+
request.method = 'GET'
|
|
384
|
+
r = CustomEndpoint(model_class=api_clz, fields=list_of_columns, filter_by=filter, pagesize=pagesize, offset=offset)
|
|
385
|
+
result = r.execute(request=request)
|
|
386
|
+
service_type: str = Config.ONTIMIZE_SERVICE_TYPE
|
|
387
|
+
return r.transform(service_type, key, result) # JSONAPI or LAC or OntimizeEE ARGS.service_type
|
|
388
|
+
|
|
389
|
+
def get_rows_by_query(api_clz, filter, orderBy, columns, pagesize, offset):
|
|
390
|
+
#Old Style
|
|
391
|
+
rows = []
|
|
392
|
+
results = session.query(api_clz) # or list of columns?
|
|
393
|
+
|
|
394
|
+
if columns:
|
|
395
|
+
#stmt = select(api_clz).options(load_only(Book.title, Book.summary))
|
|
396
|
+
pass #TODO
|
|
397
|
+
|
|
398
|
+
if orderBy:
|
|
399
|
+
results = results.order_by(text(parseOrderBy(orderBy)))
|
|
400
|
+
|
|
401
|
+
if filter:
|
|
402
|
+
results = results.filter(text(filter))
|
|
403
|
+
|
|
404
|
+
results = results.limit(pagesize) \
|
|
405
|
+
.offset(offset)
|
|
406
|
+
|
|
407
|
+
for row in results.all():
|
|
408
|
+
rows.append(row.to_dict())
|
|
409
|
+
|
|
410
|
+
return rows
|
|
411
|
+
|
|
412
|
+
def parseData(data:dict = None) -> str:
|
|
413
|
+
# convert dict to str
|
|
414
|
+
result = ""
|
|
415
|
+
join = ""
|
|
416
|
+
if data:
|
|
417
|
+
for d in data:
|
|
418
|
+
result += f'{join}{d}="{data[d]}"'
|
|
419
|
+
join = ","
|
|
420
|
+
return result
|
|
421
|
+
|
|
422
|
+
def parseOrderBy(orderBy) -> str:
|
|
423
|
+
#[{'columnName': 'SURNAME', 'ascendent': True}]
|
|
424
|
+
result = ""
|
|
425
|
+
if orderBy and len(orderBy) > 0:
|
|
426
|
+
result = f"{orderBy[0]['columnName']}" #TODO for desc
|
|
427
|
+
return result
|
|
428
|
+
|
|
429
|
+
def fix_payload(data, sqltypes):
|
|
430
|
+
import datetime
|
|
431
|
+
if sqltypes:
|
|
432
|
+
for t in sqltypes:
|
|
433
|
+
if sqltypes[t] == 91: #Date
|
|
434
|
+
with contextlib.suppress(Exception):
|
|
435
|
+
my_date = float(data[t])/1000
|
|
436
|
+
data[t] = datetime.datetime.fromtimestamp(my_date) #.strftime('%Y-%m-%d %H:%M:%S')
|
|
437
|
+
"""
|
|
438
|
+
Converts SQLAlchemy result (mapped or raw) to dict array of un-nested rows
|
|
439
|
+
|
|
440
|
+
Args:
|
|
441
|
+
result (object): list of serializable objects (e.g., dict)
|
|
442
|
+
|
|
443
|
+
Returns:
|
|
444
|
+
list of rows as dicts
|
|
445
|
+
"""
|
|
446
|
+
rows = []
|
|
447
|
+
for each_row in result:
|
|
448
|
+
row_as_dict = {}
|
|
449
|
+
print(f'type(each_row): {type(each_row)}')
|
|
450
|
+
if isinstance (each_row, sqlalchemy.engine.row.Row): # raw sql, eg, sample catsql
|
|
451
|
+
key_to_index = each_row._key_to_index # note: SQLAlchemy 2 specific
|
|
452
|
+
for name, value in key_to_index.items():
|
|
453
|
+
row_as_dict[name] = each_row[value]
|
|
454
|
+
else:
|
|
455
|
+
row_as_dict = each_row.to_dict() # safrs helper
|
|
456
|
+
rows.append(row_as_dict)
|
|
457
|
+
return rows
|
|
458
|
+
|
|
459
|
+
def getMetaData(resource_name:str = None, include_attributes: bool = True) -> dict:
|
|
460
|
+
import inspect
|
|
461
|
+
import sys
|
|
462
|
+
resource_list = [] # array of attributes[], name (so, the name is last...)
|
|
463
|
+
resource_objs = {} # objects, named = resource_name
|
|
464
|
+
|
|
465
|
+
models_name = "database.models"
|
|
466
|
+
cls_members = inspect.getmembers(sys.modules["database.models"], inspect.isclass)
|
|
467
|
+
for each_cls_member in cls_members:
|
|
468
|
+
each_class_def_str = str(each_cls_member)
|
|
469
|
+
if (f"'{models_name}." in each_class_def_str and
|
|
470
|
+
"Ab" not in each_class_def_str):
|
|
471
|
+
each_resource_name = each_cls_member[0]
|
|
472
|
+
each_resource_class = each_cls_member[1]
|
|
473
|
+
each_resource_mapper = each_resource_class.__mapper__
|
|
474
|
+
if resource_name is None or resource_name == each_resource_name:
|
|
475
|
+
resource_object = {"name": each_resource_name}
|
|
476
|
+
resource_list.append(resource_object)
|
|
477
|
+
resource_objs[each_resource_name] = {}
|
|
478
|
+
if include_attributes:
|
|
479
|
+
attr_list = []
|
|
480
|
+
for each_attr in each_resource_mapper.attrs:
|
|
481
|
+
if not each_attr._is_relationship:
|
|
482
|
+
try:
|
|
483
|
+
attribute_object = {"name": each_attr.key,
|
|
484
|
+
"attr": each_attr,
|
|
485
|
+
"type": str(each_attr.expression.type)}
|
|
486
|
+
except Exception as ex:
|
|
487
|
+
attribute_object = {"name": each_attr.key,
|
|
488
|
+
"exception": f"{ex}"}
|
|
489
|
+
attr_list.append(attribute_object)
|
|
490
|
+
resource_object["attributes"] = attr_list
|
|
491
|
+
resource_objs[each_resource_name] = {"attributes": attr_list, "model": each_resource_class}
|
|
492
|
+
# pick the format you like
|
|
493
|
+
#return_result = {"resources": resource_list}
|
|
494
|
+
return_result = {"resources": resource_objs}
|
|
495
|
+
return return_result
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Bar Chart Example</title>
|
|
7
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<canvas id="myChart" width="400" height="200"></canvas>
|
|
11
|
+
<script>
|
|
12
|
+
// Input data
|
|
13
|
+
fetch('http://localhost:5656/api/GraphicsServices/sales_by_category', {
|
|
14
|
+
method: 'POST',
|
|
15
|
+
headers: {
|
|
16
|
+
'accept': 'application/vnd.api+json',
|
|
17
|
+
'Content-Type': 'application/json'
|
|
18
|
+
},
|
|
19
|
+
body: JSON.stringify({}) // Add any required payload here
|
|
20
|
+
})
|
|
21
|
+
.then(response => {
|
|
22
|
+
if (!response.ok) {
|
|
23
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
24
|
+
}
|
|
25
|
+
return response.json();
|
|
26
|
+
})
|
|
27
|
+
.then(result => {
|
|
28
|
+
const labels = [];
|
|
29
|
+
const data = [];
|
|
30
|
+
const type = result.meta.result.chart_type;
|
|
31
|
+
const title = result.meta.result.title;
|
|
32
|
+
const columns = result.meta.result.columns;
|
|
33
|
+
console.log(JSON.stringify(result));
|
|
34
|
+
result.meta.result.results.forEach(item => {
|
|
35
|
+
labels.push(item[columns[0]]);
|
|
36
|
+
data.push(parseFloat(item[columns[1]]));
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Create the bar chart
|
|
40
|
+
const ctx = document.getElementById('myChart').getContext('2d');
|
|
41
|
+
const myChart = new Chart(ctx, {
|
|
42
|
+
type: type,
|
|
43
|
+
data: {
|
|
44
|
+
labels: labels,
|
|
45
|
+
datasets: [{
|
|
46
|
+
label: title,
|
|
47
|
+
data: data,
|
|
48
|
+
backgroundColor: 'rgba(75, 192, 192, 0.2)',
|
|
49
|
+
borderColor: 'rgba(75, 192, 192, 1)',
|
|
50
|
+
borderWidth: 1
|
|
51
|
+
}]
|
|
52
|
+
},
|
|
53
|
+
options: {
|
|
54
|
+
responsive: true,
|
|
55
|
+
plugins: {
|
|
56
|
+
legend: {
|
|
57
|
+
position: 'top',
|
|
58
|
+
},
|
|
59
|
+
title: {
|
|
60
|
+
display: true,
|
|
61
|
+
text: title
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
scales: {
|
|
65
|
+
y: {
|
|
66
|
+
beginAtZero: true
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
})
|
|
72
|
+
.catch(error => console.error('Error fetching data:', error));
|
|
73
|
+
|
|
74
|
+
</script>
|
|
75
|
+
</body>
|
|
76
|
+
</html>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
SELECT Category.CategoryName, SUM(OrderDetail.Quantity * OrderDetail.UnitPrice * (1 - OrderDetail.Discount)) AS TotalSales FROM Category JOIN Product ON Category.Id = Product.CategoryId JOIN OrderDetail ON Product.Id = OrderDetail.ProductId JOIN `Order` ON OrderDetail.OrderId = `Order`.Id WHERE `Order`.ShippedDate IS NOT NULL GROUP BY Category.CategoryName ORDER BY TotalSales DESC;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from flask import request, jsonify
|
|
2
|
+
import logging
|
|
3
|
+
import api.system.api_utils as api_utils
|
|
4
|
+
|
|
5
|
+
app_logger = logging.getLogger("api_logic_server_app")
|
|
6
|
+
|
|
7
|
+
def add_service(app, api, project_dir, swagger_host: str, PORT: str, method_decorators = []):
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
###################
|
|
12
|
+
# Internal Services
|
|
13
|
+
###################
|
|
14
|
+
|
|
15
|
+
@app.route('/server_log')
|
|
16
|
+
def server_log():
|
|
17
|
+
"""
|
|
18
|
+
Used by test/*.py - enables client app to log msg into server's console log
|
|
19
|
+
"""
|
|
20
|
+
return api_utils.server_log(request, jsonify)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@app.route('/metadata')
|
|
24
|
+
def metadata():
|
|
25
|
+
"""
|
|
26
|
+
Swagger provides typical API discovery. This is for tool providers
|
|
27
|
+
requiring programmatic access to api definition, e.g.,
|
|
28
|
+
to drive artifact code generation.
|
|
29
|
+
|
|
30
|
+
Returns json for list of 1 / all resources, with optional attribute name/type, eg
|
|
31
|
+
|
|
32
|
+
curl -X GET "http://localhost:5656/metadata?resource=Category&include=attributes"
|
|
33
|
+
|
|
34
|
+
curl -X GET "http://localhost:5656/metadata?include=attributes"
|
|
35
|
+
"""
|
|
36
|
+
import inspect
|
|
37
|
+
import sys
|
|
38
|
+
from sqlalchemy.ext.declarative import declarative_base
|
|
39
|
+
|
|
40
|
+
resource_name = request.args.get('resource')
|
|
41
|
+
include_attributes = False
|
|
42
|
+
include = request.args.get('include')
|
|
43
|
+
if include:
|
|
44
|
+
include_attributes = "attributes" in include
|
|
45
|
+
resource_list = [] # array of attributes[], name (so, the name is last...)
|
|
46
|
+
resource_objs = {} # objects, named = resource_name
|
|
47
|
+
|
|
48
|
+
models_name = "database.models"
|
|
49
|
+
cls_members = inspect.getmembers(sys.modules["database.models"], inspect.isclass)
|
|
50
|
+
for each_cls_member in cls_members:
|
|
51
|
+
each_class_def_str = str(each_cls_member)
|
|
52
|
+
if (f"'{models_name}." in str(each_class_def_str) and
|
|
53
|
+
"Ab" not in str(each_class_def_str)):
|
|
54
|
+
each_resource_name = each_cls_member[0]
|
|
55
|
+
each_resource_class = each_cls_member[1]
|
|
56
|
+
each_resource_mapper = each_resource_class.__mapper__
|
|
57
|
+
if resource_name is None or resource_name == each_resource_name:
|
|
58
|
+
resource_object = {"name": each_resource_name}
|
|
59
|
+
resource_list.append(resource_object)
|
|
60
|
+
resource_objs[each_resource_name] = {}
|
|
61
|
+
if include_attributes:
|
|
62
|
+
attr_list = []
|
|
63
|
+
for each_attr in each_resource_mapper.attrs:
|
|
64
|
+
if not each_attr._is_relationship:
|
|
65
|
+
try:
|
|
66
|
+
attribute_object = {"name": each_attr.key,
|
|
67
|
+
"type": str(each_attr.expression.type)}
|
|
68
|
+
except:
|
|
69
|
+
attribute_object = {"name": each_attr.key,
|
|
70
|
+
"type": "unkown"}
|
|
71
|
+
attr_list.append(attribute_object)
|
|
72
|
+
resource_object["attributes"] = attr_list
|
|
73
|
+
resource_objs[each_resource_name] = {"attributes": attr_list}
|
|
74
|
+
# pick the format you like
|
|
75
|
+
return_result = {"resources": resource_list}
|
|
76
|
+
return_result = {"resources": resource_objs}
|
|
77
|
+
return jsonify(return_result)
|