ApiLogicServer 14.3.20__py3-none-any.whl → 14.4.0__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 (107) hide show
  1. api_logic_server_cli/api_logic_server.py +5 -14
  2. api_logic_server_cli/api_logic_server_info.yaml +3 -3
  3. api_logic_server_cli/cli.py +52 -5
  4. api_logic_server_cli/create_from_model/__pycache__/create_db_from_model.cpython-312.pyc +0 -0
  5. api_logic_server_cli/create_from_model/__pycache__/ont_build.cpython-312.pyc +0 -0
  6. api_logic_server_cli/create_from_model/__pycache__/ont_create.cpython-312.pyc +0 -0
  7. api_logic_server_cli/create_from_model/create_db_from_model.py +2 -0
  8. api_logic_server_cli/create_from_model/ont_build.py +19 -14
  9. api_logic_server_cli/create_from_model/ont_create.py +5 -5
  10. api_logic_server_cli/create_from_model/safrs-react-admin-npm-build/static/.DS_Store +0 -0
  11. api_logic_server_cli/database/nw-gold-fix.sql +62 -0
  12. api_logic_server_cli/database/nw-gold.sqlite +0 -0
  13. api_logic_server_cli/fragments/docker-compose.yml +27 -0
  14. api_logic_server_cli/genai/genai.py +43 -11
  15. api_logic_server_cli/genai/genai_graphics.py +379 -0
  16. api_logic_server_cli/genai/genai_logic_builder.py +2 -2
  17. api_logic_server_cli/genai/genai_svcs.py +24 -8
  18. api_logic_server_cli/manager.py +19 -10
  19. api_logic_server_cli/prototypes/.DS_Store +0 -0
  20. api_logic_server_cli/prototypes/base/.DS_Store +0 -0
  21. api_logic_server_cli/prototypes/base/.vscode/launch.json +19 -0
  22. api_logic_server_cli/prototypes/base/api/expose_api_models.py +3 -1
  23. api_logic_server_cli/prototypes/base/api_logic_server_run.py +5 -2
  24. api_logic_server_cli/prototypes/base/config/activate_logicbank.py +1 -0
  25. api_logic_server_cli/prototypes/base/config/config.py +95 -24
  26. api_logic_server_cli/prototypes/base/config/logging.yml +1 -0
  27. api_logic_server_cli/prototypes/base/config/server_setup.py +33 -1
  28. api_logic_server_cli/prototypes/base/database/test_data/readme.md +3 -1
  29. api_logic_server_cli/prototypes/base/devops/docker-standard-image/docker-compose-standard-image.yml +7 -2
  30. api_logic_server_cli/prototypes/base/docs/graphics/readme.md +12 -0
  31. api_logic_server_cli/prototypes/base/docs/training/logic_bank_api.prompt +314 -0
  32. api_logic_server_cli/prototypes/base/docs/training/logic_example.py +41 -0
  33. api_logic_server_cli/prototypes/base/integration/kafka/kafka_producer.py +7 -3
  34. api_logic_server_cli/prototypes/base/integration/system/FlaskKafka.py +5 -1
  35. api_logic_server_cli/prototypes/base/security/authentication_provider/keycloak/auth_provider.py +1 -1
  36. api_logic_server_cli/prototypes/base/security/declare_security.py +4 -0
  37. api_logic_server_cli/prototypes/base/ui/admin/admin_loader.py +3 -1
  38. api_logic_server_cli/prototypes/base/ui/templates/bar_chart.jinja +64 -0
  39. api_logic_server_cli/prototypes/genai_demo/ui/admin/admin.yaml +1 -1
  40. api_logic_server_cli/prototypes/manager/README.md +56 -5
  41. api_logic_server_cli/prototypes/manager/system/genai/.DS_Store +0 -0
  42. api_logic_server_cli/prototypes/manager/system/genai/examples/.DS_Store +0 -0
  43. api_logic_server_cli/prototypes/manager/system/genai/examples/genai_demo/.DS_Store +0 -0
  44. api_logic_server_cli/prototypes/manager/system/genai/examples/genai_demo/genai_demo.prompt +0 -8
  45. api_logic_server_cli/prototypes/manager/system/genai/examples/genai_demo/genai_demo.response_example +90 -60
  46. api_logic_server_cli/prototypes/manager/system/genai/examples/genai_demo/genai_demo_docs_logic/docs/002_create_db_models.prompt +4 -4
  47. api_logic_server_cli/prototypes/manager/system/genai/examples/genai_demo/genai_demo_docs_logic/docs/003_create_db_models.response +77 -47
  48. api_logic_server_cli/prototypes/manager/system/genai/examples/genai_demo/genai_demo_informal.prompt +1 -1
  49. api_logic_server_cli/prototypes/manager/system/genai/graphics_templates/dashboard_services.jinja +83 -0
  50. api_logic_server_cli/prototypes/manager/system/genai/graphics_templates/graphics_dashboard_WIP.py +34 -0
  51. api_logic_server_cli/prototypes/manager/system/genai/graphics_templates/graphics_services_api_xxx.py +32 -0
  52. api_logic_server_cli/prototypes/manager/system/genai/graphics_templates/graphics_services_db.jinja +46 -0
  53. api_logic_server_cli/prototypes/manager/system/genai/graphics_templates/graphics_services_db_each_method.jinja +36 -0
  54. api_logic_server_cli/prototypes/manager/system/genai/graphics_templates/html_template.jinja +76 -0
  55. api_logic_server_cli/prototypes/manager/system/genai/graphics_templates/index.html +19 -0
  56. api_logic_server_cli/prototypes/manager/system/genai/graphics_templates/sales_by_region.jinja +63 -0
  57. api_logic_server_cli/prototypes/manager/system/genai/prompt_inserts/graphics.prompt +22 -0
  58. api_logic_server_cli/prototypes/manager/system/genai/prompt_inserts/graphics_request.prompt +5 -0
  59. api_logic_server_cli/prototypes/manager/system/genai/prompt_inserts/response_format.prompt +15 -0
  60. api_logic_server_cli/prototypes/manager/system/genai/prompt_inserts/sqlite_inserts.prompt +2 -0
  61. api_logic_server_cli/prototypes/manager/system/install-ApiLogicServer-dev/install-ApiLogicServer-dev.ps1 +100 -0
  62. api_logic_server_cli/prototypes/manager/system/install-ApiLogicServer-dev/install-ApiLogicServer-dev.sh +116 -0
  63. api_logic_server_cli/prototypes/manager/system/install-ApiLogicServer-dev/readme.md +7 -0
  64. api_logic_server_cli/prototypes/manager/system/style-guide.yaml +2 -2
  65. api_logic_server_cli/prototypes/manager/webgenai/README.md +6 -0
  66. api_logic_server_cli/prototypes/nw/docs/graphics/count_orders_by_category.prompt +1 -0
  67. api_logic_server_cli/prototypes/nw/docs/graphics/order_count_by_month.prompt +1 -0
  68. api_logic_server_cli/prototypes/nw/docs/graphics/request copy.json +892 -0
  69. api_logic_server_cli/prototypes/nw/docs/graphics/request.json +6 -0
  70. api_logic_server_cli/prototypes/nw/docs/graphics/response.json +17 -0
  71. api_logic_server_cli/prototypes/nw/docs/graphics/response.yaml +59 -0
  72. api_logic_server_cli/prototypes/nw/docs/graphics/sales_by_category.prompt +1 -0
  73. api_logic_server_cli/prototypes/nw/ui/admin/home.js +5 -4
  74. api_logic_server_cli/prototypes/nw/ui/app_model_custom.yaml +851 -1082
  75. api_logic_server_cli/prototypes/nw_no_cust/docs/graphics/count_orders_by_category.prompt +1 -0
  76. api_logic_server_cli/prototypes/nw_no_cust/docs/graphics/request copy.json +892 -0
  77. api_logic_server_cli/prototypes/nw_no_cust/docs/graphics/request.json +6 -0
  78. api_logic_server_cli/prototypes/nw_no_cust/docs/graphics/response.json +17 -0
  79. api_logic_server_cli/prototypes/nw_no_cust/docs/graphics/response.yaml +59 -0
  80. api_logic_server_cli/prototypes/nw_no_cust/docs/graphics/sales_by_category.prompt +1 -0
  81. api_logic_server_cli/prototypes/nw_no_cust/docs/graphics/sales_by_employee.prompt +1 -0
  82. api_logic_server_cli/prototypes/nw_no_cust/integration/mcp/1_langchain_loader.py +19 -0
  83. api_logic_server_cli/prototypes/nw_no_cust/integration/mcp/2_gpt_mcp_prompt.txt +19 -0
  84. api_logic_server_cli/prototypes/nw_no_cust/integration/mcp/3_executor_test_agent.py +38 -0
  85. api_logic_server_cli/prototypes/nw_no_cust/integration/mcp/README.md +17 -0
  86. api_logic_server_cli/prototypes/nw_no_cust/integration/mcp/resources/curl.txt +4 -0
  87. api_logic_server_cli/prototypes/nw_no_cust/integration/mcp/resources/nw_swagger_3.yaml +16660 -0
  88. api_logic_server_cli/prototypes/nw_no_cust/integration/mcp/run_executor.py +23 -0
  89. api_logic_server_cli/prototypes/ont_app/ontimize_seed/nginx/nginx.conf +2 -2
  90. api_logic_server_cli/prototypes/ont_app/ontimize_seed/package.json +6 -6
  91. api_logic_server_cli/prototypes/ont_app/ontimize_seed/src/app/app.config.ts +2 -1
  92. api_logic_server_cli/prototypes/ont_app/ontimize_seed/src/environments/environment.prod.ts +5 -5
  93. api_logic_server_cli/prototypes/ont_app/ontimize_seed/src/environments/environment.ts +5 -5
  94. api_logic_server_cli/prototypes/ont_app/templates/app_config.jinja +1 -1
  95. api_logic_server_cli/prototypes/ont_app/templates/detail_template.html +1 -1
  96. api_logic_server_cli/prototypes/ont_app/templates/new_template.html +16 -16
  97. apilogicserver-14.4.0.dist-info/METADATA +76 -0
  98. {apilogicserver-14.3.20.dist-info → apilogicserver-14.4.0.dist-info}/RECORD +102 -59
  99. {apilogicserver-14.3.20.dist-info → apilogicserver-14.4.0.dist-info}/WHEEL +1 -1
  100. api_logic_server_cli/prototypes/manager/system/genai/prompt_inserts/zsqlite_inserts_iterations.prompt +0 -29
  101. api_logic_server_cli/prototypes/manager/webgenai/docker-compose-webg.yml +0 -33
  102. api_logic_server_cli/prototypes/manager/webgenai/webg_config/license.json +0 -6
  103. api_logic_server_cli/prototypes/manager/webgenai/webg_config/web_genai.txt +0 -13
  104. apilogicserver-14.3.20.dist-info/METADATA +0 -167
  105. {apilogicserver-14.3.20.dist-info → apilogicserver-14.4.0.dist-info}/entry_points.txt +0 -0
  106. {apilogicserver-14.3.20.dist-info → apilogicserver-14.4.0.dist-info}/licenses/LICENSE +0 -0
  107. {apilogicserver-14.3.20.dist-info → apilogicserver-14.4.0.dist-info}/top_level.txt +0 -0
@@ -4,11 +4,12 @@ from pathlib import Path
4
4
  import os
5
5
  import typing
6
6
  from dotenv import load_dotenv
7
- import logging
7
+ import logging, logging.config
8
8
  from enum import Enum
9
9
  import socket
10
10
  import json
11
11
 
12
+
12
13
  '''
13
14
  #als: configuration settings
14
15
 
@@ -57,6 +58,39 @@ def is_docker() -> bool:
57
58
  # assert path_result == env_result
58
59
  return path_result
59
60
 
61
+
62
+
63
+ # ==================================
64
+ # LOGGING SETUP
65
+ # ==================================
66
+
67
+ def logging_setup() -> logging.Logger:
68
+ """
69
+ Setup Logging
70
+ """
71
+ import yaml
72
+ global app_logger, debug_value, project_path
73
+ logging_config = f'{project_path}/config/logging.yml'
74
+ if os.getenv('APILOGICPROJECT_LOGGING_CONFIG'):
75
+ logging_config = project_path.joinpath(os.getenv("APILOGICPROJECT_LOGGING_CONFIG"))
76
+ with open(logging_config,'rt') as f: # see also logic/declare_logic.py
77
+ config=yaml.safe_load(f.read())
78
+ f.close()
79
+ logging.config.dictConfig(config) # log levels: notset 0, debug 10, info 20, warn 30, error 40, critical 50
80
+ app_logger = logging.getLogger("api_logic_server_app")
81
+ debug_value = os.getenv('APILOGICPROJECT_DEBUG')
82
+ if debug_value is not None: # > export APILOGICPROJECT_DEBUG=True
83
+ debug_value = debug_value.upper()
84
+ if debug_value.startswith("F") or debug_value.startswith("N"):
85
+ app_logger.setLevel(logging.INFO)
86
+ else:
87
+ app_logger.setLevel(logging.DEBUG)
88
+ app_logger.debug(f'\nDEBUG level set from env\n')
89
+ # app_logger.info(f'\nAPI Logic Project Server Setup ({project_name}) Starting with CLI args: \n.. {args}\n')
90
+ # app_logger.info(f'Created August 03, 2024 09:34:01 at {str(project_path)}\n')
91
+ return app_logger
92
+
93
+
60
94
  class Config:
61
95
  """
62
96
 
@@ -67,6 +101,8 @@ class Config:
67
101
  Code should therefore access these ONLY as described in Args, below.
68
102
 
69
103
  """
104
+ if os.getenv("EXPERIMENT") == '+':
105
+ logging_setup() # set up logging as early as possible so capture critical config logging
70
106
 
71
107
  # Project Creation Defaults (overridden from args, env variables)
72
108
  CREATED_API_PREFIX = "/api"
@@ -116,15 +152,16 @@ class Config:
116
152
  # als add-auth --provider-type=sql --db-url=
117
153
  # als add-auth --provider-type=keycloak --db-url=localhost
118
154
  # als add-auth --provider-type=keycloak --db-url=http://10.0.0.77:8080
119
- kc_base = 'http://localhost:8080' # e.g., 'http://localhost:8080'
155
+ kc_base = os.getenv('KEYCLOAK_BASE','https://localhost:8080')
156
+ #kc_base = 'http://localhost:8080'
120
157
  ''' keycloak location '''
121
- KEYCLOAK_REALM = 'kcals'
122
- KEYCLOAK_BASE = f'{kc_base}/realms/{KEYCLOAK_REALM}'
123
- KEYCLOAK_BASE_URL = f'{kc_base}'
124
- KEYCLOAK_CLIENT_ID = 'alsclient'
158
+ KEYCLOAK_REALM = os.getenv('KEYCLOAK_REALM','kcals')
159
+ KEYCLOAK_BASE = os.getenv('KEYCLOAK_BASE',f'{kc_base}')
160
+ KEYCLOAK_BASE_URL = os.getenv('KEYCLOAK_BASE_URL',f'{kc_base}/realms/{KEYCLOAK_REALM}')
161
+ KEYCLOAK_CLIENT_ID = os.getenv('KEYCLOAK_CLIENT_ID','alsclient')
125
162
  ''' keycloak client id '''
126
163
 
127
- SECURITY_ENABLED = False # disables security (regardless of SECURITY_PROVIDER)
164
+ SECURITY_ENABLED = os.getenv("SECURITY_ENABLED",False)
128
165
  SECURITY_PROVIDER = None
129
166
  if os.getenv('SECURITY_ENABLED'): # e.g. export SECURITY_ENABLED=true
130
167
  security_export = os.getenv('SECURITY_ENABLED') # type: ignore # type: str
@@ -141,6 +178,8 @@ class Config:
141
178
  app_logger.debug(f'config.py - security enabled')
142
179
  else:
143
180
  app_logger.info(f'config.py - security disabled')
181
+
182
+ app_logger.info(f'SECURITY_PROVIDER={SECURITY_PROVIDER}')
144
183
 
145
184
  # Begin Multi-Database URLs (from ApiLogicServer add-db...)
146
185
  auth_db_path = str(project_path.joinpath('database/authentication_db.sqlite'))
@@ -164,11 +203,24 @@ class Config:
164
203
  SQLALCHEMY_TRACK_MODIFICATIONS = False
165
204
  PROPAGATE_EXCEPTIONS = False
166
205
 
167
- KAFKA_PRODUCER = '{"bootstrap.servers": "localhost:9092"}' # , "client.id": "aaa.b.c.d"}'
168
- KAFKA_PRODUCER = None # comment out to enable Kafka producer
169
- KAFKA_CONSUMER = '{"bootstrap.servers": "localhost:9092", "group.id": "als-default-group1"}'
170
- KAFKA_CONSUMER = None # comment out to enable Kafka consumer
171
-
206
+ KAFKA_PRODUCER = None
207
+ KAFKA_CONSUMER = None
208
+ KAFKA_CONSUMER_GROUP = None
209
+ KAFKA_SERVER = None
210
+ KAFKA_SERVER = os.getenv('KAFKA_SERVER', None) # 'localhost:9092' # if running locally default
211
+ if KAFKA_SERVER is not None and KAFKA_SERVER != "None" and KAFKA_SERVER != "":
212
+ app_logger.info(f'config.py - KAFKA_SERVER: {KAFKA_SERVER}')
213
+ KAFKA_PRODUCER = os.getenv('KAFKA_PRODUCER',{"bootstrap.servers": f"{KAFKA_SERVER}"}) # , "client.id": "aaa.b.c.d"}'
214
+ KAFKA_CONSUMER_GROUP = os.getenv('KAFKA_CONSUMER_GROUP') #'als-default-group1'
215
+ if KAFKA_CONSUMER_GROUP is not None: # and KAFKA_CONSUMER_GROUP != "None":
216
+ KAFKA_CONSUMER = os.getenv('KAFKA_CONSUMER', {"bootstrap.servers": f"{KAFKA_SERVER}", "group.id": f"{KAFKA_CONSUMER_GROUP}", "enable.auto.commit": "false", "auto.offset.reset": "earliest"})
217
+ else:
218
+ app_logger.info(f'config.py - KAFKA_SERVER: {KAFKA_SERVER} - not set, no kafka producer/consumer')
219
+ producer_is_empty = "" == KAFKA_PRODUCER
220
+ app_logger.info(f'config.py - KAFKA_PRODUCER: {KAFKA_PRODUCER} (is_empty={producer_is_empty})')
221
+ app_logger.info(f'config.py - KAFKA_CONSUMER: {KAFKA_CONSUMER}')
222
+ app_logger.info(f'config.py - KAFKA_CONSUMER_GROUP: {KAFKA_CONSUMER_GROUP}')
223
+ app_logger.info(f'config.py - KAFKA_SERVER: {KAFKA_SERVER}')
172
224
  # N8N Webhook Args (for testing)
173
225
  # see https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.webhook/?utm_source=n8n_app&utm_medium=node_settings_modal-credential_link&utm_campaign=n8n-nodes-base.webhook#path
174
226
  wh_scheme = "http"
@@ -245,6 +297,7 @@ class Args():
245
297
  self.http_scheme = Config.CREATED_HTTP_SCHEME
246
298
  self.kafka_producer = Config.KAFKA_PRODUCER
247
299
  self.kafka_consumer = Config.KAFKA_CONSUMER
300
+ self.kafka_consumer_group = Config.KAFKA_CONSUMER_GROUP
248
301
  self.n8n_producer = Config.N8N_PRODUCER
249
302
  self.keycloak_base = Config.KEYCLOAK_BASE
250
303
  self.keycloak_realm = Config.KEYCLOAK_REALM
@@ -463,7 +516,7 @@ class Args():
463
516
  @property
464
517
  def kafka_producer(self) -> dict:
465
518
  """ kafka connect string """
466
- if "KAFKA_PRODUCER" in self.flask_app.config:
519
+ if "KAFKA_PRODUCER" in self.flask_app.config and self.flask_app.config["KAFKA_PRODUCER"] is not None:
467
520
  if self.flask_app.config["KAFKA_PRODUCER"] is not None:
468
521
  value = self.flask_app.config["KAFKA_PRODUCER"]
469
522
  if isinstance(value, dict):
@@ -480,23 +533,32 @@ class Args():
480
533
  @property
481
534
  def kafka_consumer(self) -> dict:
482
535
  """ kafka enable consumer """
483
- if "KAFKA_CONSUMER" in self.flask_app.config:
484
- if self.flask_app.config["KAFKA_CONSUMER"] is not None:
485
- return json.loads(self.flask_app.config["KAFKA_CONSUMER"])
536
+ if "KAFKA_CONSUMER" in self.flask_app.config and self.flask_app.config["KAFKA_CONSUMER"] is not None:
537
+ value = self.flask_app.config["KAFKA_CONSUMER"]
538
+ if isinstance(value, dict):
539
+ pass # eg, from VSCode Run Config: "APILOGICPROJECT_KAFKA_PRODUCER": "{\"bootstrap.servers\": \"localhost:9092\"}",
540
+ else:
541
+ value = json.loads(self.flask_app.config["KAFKA_CONSUMER"])
542
+ return value
486
543
  return None
487
544
 
488
545
  @kafka_consumer.setter
489
546
  def kafka_consumer(self, a: str):
490
547
  self.flask_app.config["KAFKA_CONSUMER"] = a
491
548
 
492
- def __str__(self) -> str:
493
- rtn = f'.. flask_host: {self.flask_host}, port: {self.port}, \n'\
494
- f'.. swagger_host: {self.swagger_host}, swagger_port: {self.swagger_port}, \n'\
495
- f'.. client_uri: {self.client_uri}, \n'\
496
- f'.. http_scheme: {self.http_scheme}, api_prefix: {self.api_prefix}, \n'\
497
- f'.. | verbose: {self.verbose}, create_and_run: {self.create_and_run}'
498
- return rtn
499
-
549
+
550
+ @property
551
+ def kafka_consumer_group(self) -> dict:
552
+ """ kafka enable consumer group """
553
+ if "KAFKA_CONSUMER_GROUP" in self.flask_app.config:
554
+ if self.flask_app.config["KAFKA_CONSUMER_GROUP"] is not None:
555
+ return self.flask_app.config["KAFKA_CONSUMER_GROUP"]
556
+ return None
557
+
558
+ @kafka_consumer_group.setter
559
+ def kafka_consumer_group(self, a: str):
560
+ self.flask_app.config["KAFKA_CONSUMER_GROUP"] = a
561
+
500
562
  @property
501
563
  def n8n_producer(self) -> dict:
502
564
  """ n8n connect string """
@@ -515,6 +577,15 @@ class Args():
515
577
  self.flask_app.config["N8N_PRODUCER"] = a
516
578
 
517
579
 
580
+ def __str__(self) -> str:
581
+ rtn = f'.. flask_host: {self.flask_host}, port: {self.port}, \n'\
582
+ f'.. swagger_host: {self.swagger_host}, swagger_port: {self.swagger_port}, \n'\
583
+ f'.. client_uri: {self.client_uri}, \n'\
584
+ f'.. http_scheme: {self.http_scheme}, api_prefix: {self.api_prefix}, \n'\
585
+ f'.. | verbose: {self.verbose}, create_and_run: {self.create_and_run}'
586
+ return rtn
587
+
588
+
518
589
  def get_cli_args(self, args: 'Args', dunder_name: str):
519
590
  """
520
591
  returns tuple of start args:
@@ -1,4 +1,5 @@
1
1
  version: 1
2
+ # notset 0, debug 10, info 20, warn 30, error 40, critical 50
2
3
 
3
4
  formatters:
4
5
  simple:
@@ -9,6 +9,24 @@
9
9
  #
10
10
  ###############################################################################
11
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
+
12
30
  start_up_message = "normal start"
13
31
 
14
32
  import traceback
@@ -25,6 +43,8 @@ except:
25
43
  from flask_sqlalchemy import SQLAlchemy
26
44
  import json
27
45
  from pathlib import Path
46
+ if os.getenv("EXPERIMENT") == '+':
47
+ import config
28
48
  from config.config import Args
29
49
 
30
50
 
@@ -88,6 +108,9 @@ import integration.kafka.kafka_consumer as kafka_consumer
88
108
  import integration.n8n.n8n_producer as n8n_producer
89
109
 
90
110
 
111
+ if os.getenv("EXPERIMENT") == '+':
112
+ app_logger = logging.getLogger("api_logic_server_app")
113
+
91
114
 
92
115
  class SAFRSAPI(_SAFRSAPI):
93
116
  """
@@ -121,7 +144,7 @@ def get_args(flask_app: Flask) -> Args:
121
144
 
122
145
  import config.config as config
123
146
  flask_app.config.from_object(config.Config)
124
- app_logger.debug(f"\nserver_setup - get_args: Config args: \n{args}") # FIXME # config file (e.g., db uri's)
147
+ app_logger.debug(f"\nserver_setup - get_args: Config args: \n{args}") # # config file (e.g., db uri's)
125
148
 
126
149
  args.get_cli_args(dunder_name=__name__, args=args)
127
150
  app_logger.debug(f"\nserver_setup - get_args: CLI args: \n{args}") # api_logic_server_run cl args
@@ -351,4 +374,13 @@ def api_logic_server_setup(flask_app: Flask, args: Args):
351
374
  db_logger.setLevel(db_log_level)
352
375
  authorization_logger.setLevel(authorization_log_level)
353
376
 
377
+ if os.getenv('APILOGICPROJECT_DEBUG'): # temp debug since logging in config is not happening
378
+ KAFKA_SERVER = os.getenv('KAFKA_SERVER')
379
+ is_empty = False
380
+ if KAFKA_SERVER is not None:
381
+ is_empty = KAFKA_SERVER == ""
382
+ is_none = KAFKA_SERVER is None
383
+ app_logger.debug(f'\nDEBUG KAFKA_SERVER: [{KAFKA_SERVER}] (is_empty: {is_empty}) (is_none: {is_none}) \n')
384
+ app_logger.debug(f'... Args.instance.kafka_producer: {Args.instance.kafka_producer}\n')
385
+
354
386
 
@@ -7,4 +7,6 @@ You can rebuild the test data, using Logic Bank rules for proper derivations, to
7
7
  als genai-utils --rebuild-test-data
8
8
  ```
9
9
 
10
- You can explore the generated `database/test_data/test_data_code.py` to control test data generation.
10
+ You can explore the generated `database/test_data/test_data_code.py` to control test data generation.
11
+
12
+ If required, you can copy `database/test_data/test_data_preamble.py` to a new file, to rebuild your database (e.g., from an altered model file), and load your own test data (if any).
@@ -14,8 +14,13 @@ services:
14
14
  image: apilogicserver/api_logic_server
15
15
  container_name: api_logic_project
16
16
  environment:
17
- - APILOGICPROJECT_VERBOSE=true
18
- - SECURITY_ENABLED=true
17
+ - APILOGICPROJECT_VERBOSE=false
18
+ - APILOGICPROJECT_SECURITY_ENABLED=false
19
+ - APILOGICPROJECT_KEYCLOAK_REALM=kcals
20
+ # use your IP, below....
21
+ - APILOGICPROJECT_KEYCLOAK_BASE_URL=http://10.0.0.249:8080/realms/kcals
22
+ - APILOGICPROJECT_KEYCLOAK_BASE=http://10.0.0.249:8080
23
+ - APILOGICPROJECT_KEYCLOAK_CLIENT_ID=alsclient
19
24
  env_file:
20
25
  - ./env.list
21
26
  volumes:
@@ -0,0 +1,12 @@
1
+ ### Add Natural Language Logic to Your Project
2
+
3
+ You can add Natural Language logic files to this directory, e.g.:
4
+
5
+ * `.sales_by_category.prompt`: Graph Sales by Category
6
+
7
+ Then, use GenAI to create executable logic in your `api/api_discovery` directory, e.g.,
8
+
9
+ ```bash
10
+ als genai-graphics
11
+ ```
12
+ 1. Consider renaming your graphics files afterward (`valid_names.z-prompt`)so they are skipped on future runs
@@ -0,0 +1,314 @@
1
+ Here is the simplified API for LogicBank:
2
+
3
+ Translate the user prompt into a series of calls to Rule methods, described here.
4
+
5
+ Do not generate import statements.
6
+
7
+ If you create sum, count or formula LogicBank rules, you MUST create a corresponding column in the data model.
8
+
9
+ Use only the methods provided below.
10
+
11
+
12
+ class Rule:
13
+ """ Invoke these functions to declare rules """
14
+
15
+ @staticmethod
16
+ def sum(derive: Column, as_sum_of: any, where: any = None, insert_parent: bool=False):
17
+ """
18
+ Derive parent column as sum of designated child column, optional where
19
+
20
+ Example
21
+ Prompt
22
+ Customer.Balance = Sum(Order.amount_total where date_shipped is null)
23
+ Response
24
+ Rule.sum(derive=Customer.Balance, as_sum_of=Order.AmountTotal,
25
+ where=lambda row: row.ShippedDate is None)
26
+
27
+ Args:
28
+ derive: name of parent <class.attribute> being derived
29
+ as_sum_of: name of child <class.attribute> being summed
30
+ where: optional where clause, designates which child rows are summed. All referenced columns must be part of the data model - create columns in the data model as required. Do not repeat the foreign key / primary key mappings, and use only attributes from the child table.
31
+ insert_parent: create parent if it does not exist. Do not use unless directly requested.
32
+ """
33
+ return Sum(derive, as_sum_of, where, insert_parent)
34
+
35
+
36
+ @staticmethod
37
+ def count(derive: Column, as_count_of: object, where: any = None, str = "", insert_parent: bool=False):
38
+ """
39
+ Derive parent column as count of designated child rows
40
+
41
+ Example
42
+ Prompt
43
+ Customer.UnPaidOrders = count(Orders where ShippedDate is None)
44
+ Response
45
+ Rule.count(derive=Customer.UnPaidOrders, as_count_of=Order,
46
+ where=Lambda row: row.ShippedDate is None)
47
+
48
+ Args:
49
+ derive: name of parent <class.attribute> being derived
50
+ as_count_of: name of child <class> being counted
51
+ where: optional where clause, designates which child rows are counted. All referenced columns must be part of the data model - create columns in the data model as required. Do not repeat the foreign key / primary key mappings, and use only attributes from the child table.
52
+ insert_parent: create parent if it does not exist. Do not use unless directly requested.
53
+ """
54
+ return Count(derive, as_count_of, where, insert_parent)
55
+
56
+
57
+ @staticmethod
58
+ def constraint(validate: object,
59
+ calling: Callable = None,
60
+ as_condition: any = None,
61
+ error_msg: str = "(error_msg not provided)",
62
+ error_attributes=None):
63
+ """
64
+ Constraints declare condition that must be true for all commits
65
+
66
+ Example
67
+ Prompt
68
+ Customer.balance <= credit_limit
69
+ Response
70
+ Rule.constraint(validate=Customer,
71
+ as_condition=lambda row: row.Balance <= row.CreditLimit,
72
+ error_msg="balance ({row.Balance}) exceeds credit ({row.CreditLimit})")
73
+
74
+ Args:
75
+ validate: name of mapped <class>
76
+ as_condition: lambda, passed row (simple constraints). All referenced columns must be part of the data model - create columns in the data model as required. Also, conditions may not contain sum or count python functions - these must be used to declare additional columns and sum/count rules.
77
+ error_msg: string, with {row.attribute} replacements
78
+ error_attributes: list of attributes
79
+
80
+ """
81
+ if error_attributes is None:
82
+ error_attributes = []
83
+ return Constraint(validate=validate, as_condition=as_condition,
84
+ error_attributes=error_attributes, error_msg=error_msg)
85
+
86
+
87
+ @staticmethod
88
+ def formula(derive: Column,
89
+ as_expression: Callable = None,
90
+ no_prune: bool = False):
91
+ """
92
+ Formulas declare column value, based on current and parent rows
93
+
94
+ Example
95
+ Prompt
96
+ Item.amount = quantity * unit_price
97
+ Response
98
+ Rule.formula(derive=OrderDetail.Amount,
99
+ as_expression=lambda row: row.UnitPrice * row.Quantity)
100
+
101
+ Args:
102
+ derive: <class.attribute> being derived
103
+ as_expression: lambda, passed row (for syntax checking). All referenced columns must be part of the data model - create columns in the data model as required. Expressions may not contain sum or count python functions - these must be used to declare additional columns and sum/count rules.
104
+ no_prune: disable pruning (rarely used, default False)
105
+ """
106
+ return Formula(derive=derive,
107
+ as_expression=as_expression,
108
+ no_prune=no_prune)
109
+
110
+
111
+ @staticmethod
112
+ def copy(derive: Column, from_parent: any):
113
+ """
114
+ Copy declares child column copied from parent column.
115
+
116
+ Example:
117
+ Prompt
118
+ Store the Item.unit_price as a copy from Product.unit_price
119
+ Response
120
+ Rule.copy(derive=OrderDetail.UnitPrice, from_parent=Product.UnitPrice)
121
+
122
+ Args:
123
+ derive: <class.attribute> being copied into
124
+ from_parent: <parent-class.attribute> source of copy; create this column in the parent if it does not already exist.
125
+ """
126
+ return Copy(derive=derive, from_parent=from_parent)
127
+
128
+
129
+ @staticmethod
130
+ def after_flush_row_event(on_class: object, calling: Callable = None,
131
+ if_condition: any = None,
132
+ when_condition: any = None,
133
+ with_args: dict = None):
134
+
135
+ Example:
136
+ Prompt:
137
+ Send the Order to Kafka topic 'order_shipping' if the date_shipped is not None
138
+ Response:
139
+ Rule.after_flush_row_event(on_class=Order, calling=kafka_producer.send_row_to_kafka,
140
+ if_condition=lambda row: row.date_shipped is not None,
141
+ with_args={"topic": "order_shipping"})
142
+ Prompt:
143
+ Send the Product to Kafka topic 'ready_to_ship' if the is_complete is True
144
+ Response:
145
+ Rule.after_flush_row_event(on_class=Product, calling=kafka_producer.send_row_to_kafka,
146
+ if_condition=lambda row: row.is_complete is True,
147
+ with_args={"topic": "ready_to_ship"})
148
+
149
+
150
+ Expanded example:
151
+
152
+ Prompt:
153
+ 1. Customer.balance <= credit_limit
154
+ 2. Customer.balance = Sum(Order.amount_total where date_shipped is null)
155
+ 3. Order.amount_total = Sum(Item.amount)
156
+ 4. Item.amount = quantity * unit_price
157
+ 5. Store the Item.unit_price as a copy from Product.unit_price
158
+
159
+ Response:
160
+ Rule.sum(derive=CustomerAccount.balance, as_sum_of=Order.amount_total, where=lambda row: row.date_shipped is None)
161
+ Rule.sum(derive=Order.amount_total, as_sum_of=Item.amount)
162
+ Rule.formula(derive=Item.amount, as_expression=lambda row: row.quantity * row.unit_price)
163
+ Rule.copy(derive=Item.unit_price, from_parent=Product.unit_price)
164
+ Rule.constraint(validate=CustomerAccount,
165
+ as_condition=lambda row: row.balance <= row.credit_limit,
166
+ error_msg="Customer balance ({row.balance}) exceeds credit limit ({row.credit_limit})")
167
+
168
+
169
+ Equivalent expanded example using informal syntax:
170
+
171
+ Prompt:
172
+ 1. The Customer's balance is less than the credit limit
173
+ 2. The Customer's balance is the sum of the Order amount_total where date_shipped is null
174
+ 3. The Order's amount_total is the sum of the Item amount
175
+ 4. The Item amount is the quantity * unit_price
176
+ 5. The Item unit_price is copied from the Product unit_price
177
+
178
+ Response is the same:
179
+ Rule.sum(derive=CustomerAccount.balance, as_sum_of=Order.amount_total, where=lambda row: row.date_shipped is None)
180
+ Rule.sum(derive=Order.amount_total, as_sum_of=Item.amount)
181
+ Rule.formula(derive=Item.amount, as_expression=lambda row: row.quantity * row.unit_price)
182
+ Rule.copy(derive=Item.unit_price, from_parent=Product.unit_price)
183
+ Rule.constraint(validate=CustomerAccount,
184
+ as_condition=lambda row: row.balance <= row.credit_limit,
185
+ error_msg="Customer balance ({row.balance}) exceeds credit limit ({row.credit_limit})")
186
+
187
+
188
+ Intermediate sum/count values require a new column, with a LogicBank sum/count rule. For example:
189
+
190
+ Prompt:
191
+ The sum of the child value cannot exceed the parent limit
192
+
193
+ Response is to create 2 rules - a derivation and a constraint, as follows:
194
+ First Rule to Create:
195
+ Rule.sum(derive=Parent.value_total, as_sum_of=Child.value)
196
+ And, be sure to create the second Rule:
197
+ Rule.constraint(validate=Parent,
198
+ as_condition=lambda row: row.value_total <= row.limit,
199
+ error_msg="Parent value total ({row.value_total}) exceeds limit ({row.limit})")
200
+
201
+ Intermediate sum/count values also work for counts. For example:
202
+
203
+ Prompt:
204
+ A airplane cannot have more passengers than its seating capacity.
205
+
206
+ Response is to create 2 rules - a count derivation and a constraint, as follows:
207
+ First Rule to Create:
208
+ Rule.count(derive=Airplane.passenger_count, as_count_of=Passengers)
209
+ And, be sure to create the second Rule:
210
+ Rule.constraint(validate=Airplane,
211
+ as_condition=lambda row: row.passenger_count <= row.seating_capacity,
212
+ error_msg="Airplane value total ({row.passenger_count}) exceeds limit ({row.seating_capacity})")
213
+
214
+
215
+ Intermediate sums in formulas also require a new column, with a LogicBank sum rule. For example:
216
+
217
+ Prompt:
218
+ An Employees' skill summary is the sum of their Employee Skill ratings, plus 2 * years of service.
219
+
220
+ Response is to create 2 rules - a derivation and a constraint, as follows:
221
+ First Rule to Create:
222
+ Rule.sum(derive=Employee.skill_rating_total, as_sum_of=EmployeeSkill.rating)
223
+ And, be sure to create the second Rule:
224
+ Rule.Formula(derive=Employee.skill_summary,
225
+ as_expression=lambda row: row.skill_rating_total + 2 * row.years_of_service)
226
+
227
+
228
+ Prompt:
229
+ A student cannot be an honor student unless they have more than 2 service activities.
230
+
231
+ Response is to create 2 rules - a count derivation and a constraint, as follows:
232
+ First Rule to Create:
233
+ Rule.count(derive=Student.service_activity_count, as_count_of=Activities, where='service' in name)
234
+ And, be sure to create the second Rule:
235
+ Rule.constraint(validate=Student,
236
+ as_condition=lambda row: row.is_honor_student and service_activity_count < 2,
237
+ error_msg="Honor Students must have at least 2 service activities")
238
+
239
+
240
+ For "more than" constraints, create columns with count rules:
241
+
242
+ Prompt: Reject Employees with more than 3 Felonies.
243
+
244
+ Response:
245
+ First Rule is to create:
246
+ Rule.count(derive=Employee.felony_count, as_count_of=Felonies)
247
+ And, be sure to create the contraint rule:
248
+ Rule.constraint(validate=Employee,
249
+ as_condition=lambda row: row.felony_count<=3,
250
+ error_msg="Employee has excessive Felonies")
251
+
252
+
253
+ For "any" constraints, create columns with count rules:
254
+
255
+ Prompt: Reject Employees with any class 5 Felonies or more than 3 Felonies.
256
+
257
+ Response:
258
+ First Rule is to create:
259
+ Rule.count(derive=Employee.class_5_felony_count, as_count_of=Felonies, where=class>5)
260
+ Rule.count(derive=Employee.felony_count, as_count_of=Felonies)
261
+ And, be sure to create the contraint rule:
262
+ Rule.constraint(validate=Employee,
263
+ as_condition=lambda row: row.class_5_felony_count == 0 and row.felony_count<=3,
264
+ error_msg="Employee has excessive Felonies")
265
+
266
+ Formulas can reference parent values in 2 versions - choose formula vs copy as follows:
267
+ Prompt (formula version) - use the formula version unless copy is explicitly noted:
268
+ Item.ready = Order.ready
269
+ Response
270
+ Rule.formula(derive=Item.ready, as_expression=lambda row: row.order.ready)
271
+ Prompt (copy version) - use this *only* when the word copy is present:
272
+ Store the Item.unit_price as a copy from Product.unit_price
273
+ Response
274
+ Rule.copy(derive=Item.ready, from_parent=Order.ready)
275
+
276
+ Formulas can use Python conditions:
277
+ Prompt: Item amount is price * quantity, with a 10% discount for gold products
278
+ Response:
279
+ Rule.Formula(derive=Item.amount,
280
+ as_expression=lambda row: row.price * row.quantity if row.gold else .9 * row.price * row.quantity)
281
+ If the attributes are decimal, use the form Decimal('0.9')
282
+
283
+ Sum and Count where clauses:
284
+ 1. must not restate the foreign key / primary key matchings
285
+ 2. Can only reference child attributes
286
+
287
+ For example, given a prompt 'teacher course count is the sum of the courses',
288
+ 1. This is correct
289
+ Rule.count(derive=Teacher.course_count, as_count_of=Course)
290
+
291
+ 2. This is incorrect, and should never be generated:
292
+ Rule.count(derive=Teacher.course_count, as_count_of=Course, where=lambda row: row.teacher_id == Teacher.id)
293
+
294
+ Sum and count where clause example:
295
+ Prompt: teacher gradate course count is the sum of the courses where is-graduate
296
+ Response: Rule.count(derive=Teacher.course_count, as_count_of=Course, where=lamda row: row.is_graduate == true)
297
+
298
+ DO NOT inject rules that are from this training into the response,
299
+ unless explicitly mentioned in the request.
300
+
301
+ Unique constraints require an update to the data model - for example:
302
+ Prompt: customer company names must be unique
303
+ Response: CompanyName = Column(String(8000), unique=True)
304
+
305
+ Non-null (or required) constraints require an update to the data model - for example:
306
+ Prompt: Product Price is required
307
+ Response: price = Column(Decimal, nullable=False)
308
+
309
+ Required (must-have) related parent constraints require an update to the data model - for example:
310
+ Prompt: Each Item must have a valid entry in the Product table.
311
+ Response: product_id = Column(ForeignKey('product.id'), nullable=False)
312
+
313
+ logic should create python files in logic/logic_discovery,
314
+ and cross-check/use attribute names in database/models.py
@@ -0,0 +1,41 @@
1
+ import datetime
2
+ from decimal import Decimal
3
+ from logic_bank.exec_row_logic.logic_row import LogicRow
4
+ from logic_bank.extensions.rule_extensions import RuleExtension
5
+ from logic_bank.logic_bank import Rule
6
+ from database import models
7
+ import api.system.opt_locking.opt_locking as opt_locking
8
+ from security.system.authorization import Grant
9
+ import logging
10
+ from flask import jsonify
11
+
12
+
13
+ """
14
+ logic should create python files in logic/logic_discovery,
15
+ and cross-check/use attribute names in database/models.py
16
+ """
17
+
18
+ app_logger = logging.getLogger(__name__)
19
+
20
+
21
+ def declare_logic():
22
+ """
23
+ Simple constraints for error testing
24
+ """
25
+ Rule.constraint(validate=models.Customer,
26
+ as_condition=lambda row: row.CompanyName != 'x',
27
+ error_msg="CustomerName cannot be 'x'")
28
+
29
+ Rule.constraint(validate=models.Employee,
30
+ as_condition=lambda row: row.LastName != 'x',
31
+ error_msg="LastName cannot be 'x'")
32
+
33
+ def valid_category_description(row: models.Category, old_row: models.Category, logic_row: LogicRow):
34
+ if logic_row.ins_upd_dlt == "upd":
35
+ return row.Description != 'x'
36
+ else:
37
+ return True
38
+ Rule.constraint(validate=models.Category,
39
+ calling=valid_category_description,
40
+ error_msg="Description cannot be 'x'")
41
+