ApiLogicServer 15.0.37__py3-none-any.whl → 15.0.38__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 (40) hide show
  1. api_logic_server_cli/api_logic_server.py +2 -2
  2. api_logic_server_cli/api_logic_server_info.yaml +3 -3
  3. api_logic_server_cli/genai/genai_svcs.py +4 -3
  4. api_logic_server_cli/prototypes/base/docs/training/react_map.prompt.md +13 -0
  5. api_logic_server_cli/prototypes/base/docs/training/react_tree.prompt.md +10 -0
  6. api_logic_server_cli/prototypes/manager/system/Manager_workspace.code-workspace +3 -3
  7. api_logic_server_cli/prototypes/nw/api/api_discovery/authentication_expose_api_models.py +53 -0
  8. api_logic_server_cli/prototypes/nw/api/api_discovery/auto_discovery.py +27 -0
  9. api_logic_server_cli/prototypes/nw/api/api_discovery/count_orders_by_month.html +76 -0
  10. api_logic_server_cli/prototypes/nw/api/api_discovery/count_orders_by_month.sql +1 -0
  11. api_logic_server_cli/prototypes/nw/api/api_discovery/dashboard_services.py +143 -0
  12. api_logic_server_cli/prototypes/nw/api/api_discovery/mcp_discovery.py +97 -0
  13. api_logic_server_cli/prototypes/nw/api/api_discovery/new_service.py +21 -0
  14. api_logic_server_cli/prototypes/nw/api/api_discovery/newer_service.py +21 -0
  15. api_logic_server_cli/prototypes/nw/api/api_discovery/number_of_sales_per_category.html +76 -0
  16. api_logic_server_cli/prototypes/nw/api/api_discovery/number_of_sales_per_category.sql +1 -0
  17. api_logic_server_cli/prototypes/nw/api/api_discovery/ontimize_api.py +495 -0
  18. api_logic_server_cli/prototypes/nw/api/api_discovery/sales_by_category.html +76 -0
  19. api_logic_server_cli/prototypes/nw/api/api_discovery/sales_by_category.sql +1 -0
  20. api_logic_server_cli/prototypes/nw/api/api_discovery/system.py +77 -0
  21. api_logic_server_cli/prototypes/nw/database/database_discovery/graphics_services.py +173 -0
  22. api_logic_server_cli/prototypes/nw/ui/admin/home.js +5 -0
  23. api_logic_server_cli/prototypes/nw/ui/reference_react_app/DEPARTMENT_TREE_VIEW.md +66 -0
  24. api_logic_server_cli/prototypes/nw/ui/reference_react_app/package.json +4 -0
  25. api_logic_server_cli/prototypes/nw/ui/reference_react_app/public/index.html +3 -0
  26. api_logic_server_cli/prototypes/nw/ui/reference_react_app/src/App.js +8 -1
  27. api_logic_server_cli/prototypes/nw/ui/reference_react_app/src/CustomLayout.js +20 -0
  28. api_logic_server_cli/prototypes/nw/ui/reference_react_app/src/Department.js +511 -24
  29. api_logic_server_cli/prototypes/nw/ui/reference_react_app/src/DepartmentTree.js +147 -0
  30. api_logic_server_cli/prototypes/nw/ui/reference_react_app/src/Employee.js +230 -18
  31. api_logic_server_cli/prototypes/nw/ui/reference_react_app/src/LandingPage.js +264 -0
  32. api_logic_server_cli/prototypes/nw/ui/reference_react_app/src/Supplier.js +359 -121
  33. api_logic_server_cli/prototypes/nw/ui/reference_react_app/src/index.js +1 -0
  34. {apilogicserver-15.0.37.dist-info → apilogicserver-15.0.38.dist-info}/METADATA +1 -1
  35. {apilogicserver-15.0.37.dist-info → apilogicserver-15.0.38.dist-info}/RECORD +39 -19
  36. api_logic_server_cli/prototypes/base/docs/training/admin_app_unused.md +0 -156
  37. {apilogicserver-15.0.37.dist-info → apilogicserver-15.0.38.dist-info}/WHEEL +0 -0
  38. {apilogicserver-15.0.37.dist-info → apilogicserver-15.0.38.dist-info}/entry_points.txt +0 -0
  39. {apilogicserver-15.0.37.dist-info → apilogicserver-15.0.38.dist-info}/licenses/LICENSE +0 -0
  40. {apilogicserver-15.0.37.dist-info → apilogicserver-15.0.38.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)