jaaql-middleware-python 4.31.0__tar.gz → 4.32.1__tar.gz

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 (76) hide show
  1. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/PKG-INFO +12 -3
  2. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/constants.py +9 -1
  3. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/documentation/documentation_public.py +110 -0
  4. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/email/email_manager_service.py +112 -7
  5. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/mvc/controller.py +11 -0
  6. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/mvc/exception_queries.py +1 -1
  7. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/mvc/generated_queries.py +19 -15
  8. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/mvc/model.py +53 -0
  9. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/scripts/01.install_domains.generated.sql +1 -1
  10. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/scripts/04.install_jaaql_data_structures.generated.sql +2 -1
  11. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql_middleware_python.egg-info/PKG-INFO +12 -3
  12. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql_middleware_python.egg-info/requires.txt +1 -1
  13. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/LICENSE.txt +0 -0
  14. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/README.md +0 -0
  15. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/__init__.py +0 -0
  16. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/config/__init__.py +0 -0
  17. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/config/config-docker.ini +0 -0
  18. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/config/config-test.ini +0 -0
  19. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/config/config.ini +0 -0
  20. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/config_constants.py +0 -0
  21. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/db/__init__.py +0 -0
  22. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/db/db_interface.py +0 -0
  23. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/db/db_pg_interface.py +0 -0
  24. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/db/db_utils.py +0 -0
  25. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/db/db_utils_no_circ.py +0 -0
  26. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/documentation/__init__.py +0 -0
  27. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/documentation/documentation_internal.py +0 -0
  28. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/documentation/documentation_shared.py +0 -0
  29. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/email/__init__.py +0 -0
  30. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/email/email_manager.py +0 -0
  31. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/email/patch_ems.py +0 -0
  32. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/exceptions/__init__.py +0 -0
  33. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/exceptions/custom_http_status.py +0 -0
  34. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/exceptions/http_status_exception.py +0 -0
  35. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/exceptions/jaaql_interpretable_handled_errors.py +0 -0
  36. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/exceptions/not_yet_implement_exception.py +0 -0
  37. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/generated_constants.py +0 -0
  38. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/interpreter/__init__.py +0 -0
  39. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/interpreter/interpret_jaaql.py +0 -0
  40. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/jaaql.py +0 -0
  41. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/migrations/__init__.py +0 -0
  42. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/migrations/migrations.py +0 -0
  43. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/mvc/__init__.py +0 -0
  44. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/mvc/base_controller.py +0 -0
  45. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/mvc/base_model.py +0 -0
  46. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/mvc/controller_interface.py +0 -0
  47. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/mvc/handmade_queries.py +0 -0
  48. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/mvc/model_interface.py +0 -0
  49. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/mvc/response.py +0 -0
  50. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/openapi/__init__.py +0 -0
  51. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/openapi/swagger_documentation.py +0 -0
  52. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/patch.py +0 -0
  53. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/scripts/02.install_super_user.exceptions.sql +0 -0
  54. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/scripts/03.install_super_user.handwritten.sql +0 -0
  55. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/scripts/05.install_static_data.generated.sql +0 -0
  56. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/scripts/06.install_jaaql.exceptions.sql +0 -0
  57. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/scripts/ZZZZ.generated_functions_views_and_permissions.sql +0 -0
  58. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/scripts/ZZZZ.reset_references.sql +0 -0
  59. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/scripts/swagger_template.html +0 -0
  60. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/services/__init__.py +0 -0
  61. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/services/cached_canned_query_service.py +0 -0
  62. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/services/migrations_manager_service.py +0 -0
  63. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/services/patch_mms.py +0 -0
  64. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/services/patch_shared_var_service.py +0 -0
  65. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/services/shared_var_service.py +0 -0
  66. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/utilities/__init__.py +0 -0
  67. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/utilities/crypt_utils.py +0 -0
  68. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/utilities/options.py +0 -0
  69. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/utilities/utils.py +0 -0
  70. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/utilities/utils_no_project_imports.py +0 -0
  71. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql/utilities/vault.py +0 -0
  72. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql_middleware_python.egg-info/SOURCES.txt +0 -0
  73. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql_middleware_python.egg-info/dependency_links.txt +0 -0
  74. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/jaaql_middleware_python.egg-info/top_level.txt +0 -0
  75. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/setup.cfg +0 -0
  76. {jaaql-middleware-python-4.31.0 → jaaql_middleware_python-4.32.1}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: jaaql-middleware-python
3
- Version: 4.31.0
3
+ Version: 4.32.1
4
4
  Summary: The jaaql package, allowing for rapid development and deployment of RESTful HTTP applications
5
5
  Home-page: https://github.com/JAAQL/JAAQL-middleware-python
6
6
  Author: Software Quality Measurement and Improvement bv
@@ -10,7 +10,7 @@ Description-Content-Type: text/markdown
10
10
  License-File: LICENSE.txt
11
11
  Requires-Dist: jaaql-monitor~=1.6.3
12
12
  Requires-Dist: psycopg[binary]~=3.1.18
13
- Requires-Dist: Pillow~=10.3.0
13
+ Requires-Dist: Pillow~=11.2.1
14
14
  Requires-Dist: cryptography~=44.0.2
15
15
  Requires-Dist: flask~=3.1.0
16
16
  Requires-Dist: coverage~=7.4.3
@@ -33,6 +33,15 @@ Requires-Dist: jwcrypto~=1.5.6
33
33
  Requires-Dist: jinja2~=3.1.6
34
34
  Requires-Dist: idna~=3.10
35
35
  Requires-Dist: certifi~=2025.1.31
36
+ Dynamic: author
37
+ Dynamic: author-email
38
+ Dynamic: description
39
+ Dynamic: description-content-type
40
+ Dynamic: home-page
41
+ Dynamic: license
42
+ Dynamic: license-file
43
+ Dynamic: requires-dist
44
+ Dynamic: summary
36
45
 
37
46
  # JAAQL-middleware-python
38
47
  Please navigate to docker/docker.md to see setup instructions
@@ -45,6 +45,14 @@ KEY__registered = "registered"
45
45
  KEY__restrictions = "restrictions"
46
46
  KEY__command = "command"
47
47
  KEY__args = "args"
48
+ KEY__document_id = "document_id"
49
+ KEY__as_attachment = "as_attachment"
50
+ KEY__create_file = "create_file"
51
+ KEY__attachment_name = "name"
52
+ KEY__filename = "filename"
53
+ KEY__completed = "completed"
54
+ KEY__content = "content"
55
+ KEY__render_as = "render_as"
48
56
 
49
57
  CRON_minute = "minute"
50
58
  CRON_hour = "hour"
@@ -183,5 +191,5 @@ ROLE__postgres = "postgres"
183
191
 
184
192
  PROTOCOL__postgres = "postgresql://"
185
193
 
186
- VERSION = "4.31.0"
194
+ VERSION = "4.32.1"
187
195
 
@@ -423,3 +423,113 @@ DOCUMENTATION__webhook = SwaggerDocumentation(
423
423
  )
424
424
  ]
425
425
  )
426
+
427
+ EXAMPLE__document_id = "b47dc954-d608-4e1b-8a8c-d8b754ee554b"
428
+
429
+ ARG_RES__document_id = SwaggerArgumentResponse(
430
+ name=KEY__document_id,
431
+ description="A document that can be used to fetch the document",
432
+ arg_type=str,
433
+ example=EXAMPLE__document_id
434
+ )
435
+
436
+ ARG_RES__renderable_document = [
437
+ SwaggerArgumentResponse(
438
+ name=KEY__attachment_name,
439
+ description="The name of the renderable document in the database",
440
+ arg_type=str,
441
+ example=["my_pdf_template"]
442
+ ),
443
+ SwaggerArgumentResponse(
444
+ name=KEY__parameters,
445
+ description="Any parameters to pass to the url as http GET parameters",
446
+ arg_type=ARG_RESP__allow_all
447
+ )
448
+ ]
449
+
450
+ DOCUMENTATION__document = SwaggerDocumentation(
451
+ tags="Documents",
452
+ methods=[
453
+ SwaggerMethod(
454
+ name="Triggers a document render",
455
+ description="Triggers a render of a document which can then be downloaded. Document is available for 5 minutes after the document has "
456
+ "been rendered",
457
+ method=REST__POST,
458
+ body=ARG_RES__renderable_document + [
459
+ SwaggerArgumentResponse(
460
+ name=KEY__create_file,
461
+ description="Whether or not to create the file. You will then be provided with a URL when it is ready which can be downloaded "
462
+ "from. Otherwise you will be sent back a boolean",
463
+ arg_type=bool
464
+ ),
465
+ SwaggerArgumentResponse(
466
+ name=KEY__application,
467
+ description="The application of the document template",
468
+ arg_type=str,
469
+ example=["out-and-about"]
470
+ )
471
+ ],
472
+ response=SwaggerResponse(
473
+ description="A document id",
474
+ response=ARG_RES__document_id
475
+ )
476
+ ),
477
+ SwaggerMethod(
478
+ name="Download document",
479
+ description="Downloads the document. Can also be used as a polling endpoint to see if the document is ready",
480
+ method=REST__GET,
481
+ arguments=[
482
+ ARG_RES__document_id,
483
+ SwaggerArgumentResponse(
484
+ name=KEY__as_attachment,
485
+ description="Whether in the browser the 'Content-Disposition' header should be set as attachment",
486
+ arg_type=bool,
487
+ required=False,
488
+ condition="Defaults to false"
489
+ )
490
+ ],
491
+ response=[
492
+ SwaggerFlatResponse(
493
+ description="A link to the raw file data. This URL is called with GET and no security parameters. Can only be called once",
494
+ body="https://www.jaaql.io/api/rendered_documents/" + EXAMPLE__document_id + ".pdf"
495
+ ),
496
+ SwaggerFlatResponse(
497
+ description="The url to the document. Will be deleted after 5 minutes",
498
+ code=HTTPStatus.CREATED,
499
+ body="https://www.jaaql.io/rendered_documents/" + EXAMPLE__document_id + ".pdf"
500
+ ),
501
+ SwaggerFlatResponse(
502
+ description="Document still rendering",
503
+ code=HTTPStatus.TOO_EARLY,
504
+ body=ERR__document_still_rendering
505
+ ),
506
+ SwaggerFlatResponse(
507
+ description="Document id not found. Either expired or did not exist",
508
+ body=ERR__document_id_not_found,
509
+ code=HTTPStatus.NOT_FOUND
510
+ )
511
+ ]
512
+ )
513
+ ]
514
+ )
515
+
516
+ DOCUMENTATION__rendered_document = SwaggerDocumentation(
517
+ tags="Documents",
518
+ security=False,
519
+ methods=SwaggerMethod(
520
+ name="Stream rendered document",
521
+ description="Streams a rendered document as a downlaod",
522
+ method=REST__GET,
523
+ arguments=[ARG_RES__document_id, SwaggerArgumentResponse(
524
+ name=KEY__as_attachment,
525
+ description="Whether in the browser the 'Content-Disposition' header should be set as attachment",
526
+ arg_type=bool,
527
+ required=False,
528
+ condition="Defaults to false"
529
+ )],
530
+ response=SwaggerFlatResponse(
531
+ description="The raw file data. Cannot be re-downloaded after this",
532
+ body=BODY__file
533
+ )
534
+ )
535
+ )
@@ -1,7 +1,8 @@
1
- import os
2
1
  import traceback
3
2
  import uuid
4
3
  import time
4
+ from http import HTTPStatus
5
+ from urllib.parse import urlparse
5
6
 
6
7
  from selenium.common.exceptions import NoSuchElementException
7
8
  from datetime import datetime
@@ -67,6 +68,15 @@ KEY__attachment_application = "application"
67
68
  KEY__template_base = "template_base"
68
69
 
69
70
 
71
+ QUERY__purge_rendered_documents = "DELETE FROM document_request rd USING document_template able WHERE rd.template = able.name AND completed is not null and completed > current_timestamp + interval '5 minutes' RETURNING rd.uuid as document_id, rd.create_file, 'pdf' as render_as"
72
+ QUERY__fetch_unrendered_document = "SELECT able.content_path as url, a.base_url, 'pdf' as render_as, rd.uuid as document_id, rd.encrypted_parameters as parameters, rd.create_file, rd.encrypted_access_token as oauth_token FROM document_request rd INNER JOIN document_template able ON rd.template = able.name INNER JOIN application a ON rd.application = a.name WHERE rd.completed is null ORDER BY rd.request_timestamp LIMIT 1"
73
+ QUERY__mark_rendered_document_completed = "UPDATE document_request SET completed = current_timestamp, file_name = :file_name, content = :content WHERE uuid = :document_id"
74
+ QUERY__mark_rendered_document_completed_with_error = "UPDATE document_request SET completed = current_timestamp WHERE uuid = :document_id"
75
+
76
+
77
+ CHROME_DEBUGGING = True
78
+
79
+
70
80
  class DrivenChrome:
71
81
  def __init__(self, db_interface: DBInterface, db_key: bytes, is_deployed: bool):
72
82
  self.attachments = Queue()
@@ -79,9 +89,21 @@ class DrivenChrome:
79
89
  self.db_key = db_key
80
90
  self.template_dir_path = os.path.join(DIR__www, DIR__render_template)
81
91
 
82
- self.a4_params = {'landscape': False, 'paperWidth': 8.27, 'paperHeight': 11.69, 'printBackground': True}
92
+ self.a4_params = {
93
+ "landscape": False,
94
+ "paperWidth": 8.27, # A-4 portrait
95
+ "paperHeight": 11.69,
96
+ "marginTop": 0, # remove default 1 cm margins
97
+ "marginBottom": 0,
98
+ "marginLeft": 0,
99
+ "marginRight": 0,
100
+ "scale": 0.8,
101
+ "printBackground": True, # keep gradient / images
102
+ "preferCSSPageSize": True # honour @page { size:A4; margin:0 }
103
+ }
83
104
 
84
105
  threading.Thread(target=self.start_chrome, daemon=True).start()
106
+ threading.Thread(target=self.purge_rendered_documents, daemon=True).start()
85
107
 
86
108
  def parameters_to_get_str(self, access_token: str, parameters: dict):
87
109
  if parameters is None:
@@ -90,9 +112,25 @@ class DrivenChrome:
90
112
 
91
113
  return "?" + "&".join([key + "=" + quote(itm if isinstance(itm, bytes) else itm.encode("UTF-8")) for key, itm in parameters.items()])
92
114
 
93
- def chrome_page_to_pdf(self, url: str, access_token: str, parameters: dict):
115
+ def chrome_page_to_pdf(self, url: str, access_token: str, parameters: dict, document_id: str):
94
116
  with self.chrome_lock:
95
- self.driver.get(url + self.parameters_to_get_str(access_token, parameters))
117
+ origin = "{uri.scheme}://{uri.netloc}".format(uri=urlparse(url))
118
+ self.driver.execute_cdp_cmd(
119
+ "Storage.clearDataForOrigin",
120
+ {
121
+ "origin": origin,
122
+ "storageTypes": "local_storage,session_storage"
123
+ }
124
+ )
125
+
126
+ old_handle = self.driver.current_window_handle
127
+ self.driver.switch_to.new_window('tab')
128
+ self.driver.switch_to.window(old_handle)
129
+ self.driver.close()
130
+ survivor_handles = [h for h in self.driver.window_handles if h != old_handle]
131
+ self.driver.switch_to.window(survivor_handles[0])
132
+
133
+ self.driver.get(url + self.parameters_to_get_str(access_token, parameters) + "&BS_document_id=" + document_id)
96
134
  start_time = datetime.now()
97
135
  while True:
98
136
  passed = False
@@ -122,13 +160,79 @@ class DrivenChrome:
122
160
 
123
161
  if time_delta_ms(start_time, datetime.now()) > TIMEOUT__attachment_render:
124
162
  raise HttpStatusException(ERR__attachment_timeout_render)
163
+ else:
164
+ time.sleep(0.1)
125
165
 
126
- return filename, base64.b64decode(self.driver.execute_cdp_cmd("Page.printToPDF", self.a4_params)["data"])
166
+ pdf_data = base64.b64decode(self.driver.execute_cdp_cmd("Page.printToPDF", self.a4_params)["data"])
167
+ self.driver.execute_cdp_cmd("HeapProfiler.collectGarbage", {})
168
+ return filename, pdf_data
169
+
170
+ def purge_rendered_documents(self):
171
+ while True:
172
+ resp = execute_supplied_statement(self.db_interface, QUERY__purge_rendered_documents, as_objects=True)
173
+ for to_purge in resp:
174
+ if to_purge[KEY__create_file]:
175
+ try:
176
+ os.remove(os.path.join(self.template_dir_path,
177
+ str(to_purge[KEY__document_id]) + "." + to_purge[KEY__render_as]))
178
+ except FileNotFoundError:
179
+ pass
180
+ time.sleep(5)
181
+
182
+ def render_document_requests(self):
183
+ while True:
184
+ resp = None
185
+ try:
186
+ resp = execute_supplied_statement_singleton(self.db_interface, QUERY__fetch_unrendered_document,
187
+ as_objects=True,
188
+ decrypt_columns=[KEY__parameters, KEY__oauth_token],
189
+ encryption_key=self.db_key)
190
+ parameters = {}
191
+ if resp[KEY__parameters]:
192
+ parameters = json.loads(resp[KEY__parameters])
193
+
194
+ base_url = EmailAttachment.static_format_attached_url(resp[KG__application__base_url] + "/" + resp["url"], self.is_deployed)
195
+
196
+ filename, content = self.chrome_page_to_pdf(base_url, resp[KEY__oauth_token], parameters, str(resp[KEY__document_id]))
197
+
198
+ inputs = {KEY__document_id: resp[KEY__document_id], KEY__content: None, KG__document_request__file_name: filename}
199
+ if resp[KEY__create_file]:
200
+ if not os.path.exists(self.template_dir_path):
201
+ os.mkdir(self.template_dir_path)
202
+ with open(os.path.join(self.template_dir_path,
203
+ str(resp[KEY__document_id]) + "." + resp[KEY__render_as]), "wb") as f:
204
+ f.write(content)
205
+ else:
206
+ inputs[KEY__content] = content
207
+
208
+ execute_supplied_statement(self.db_interface, QUERY__mark_rendered_document_completed, inputs)
209
+ except HttpStatusException as ex:
210
+ if ex.response_code == HTTPStatus.UNPROCESSABLE_ENTITY:
211
+ if resp is not None:
212
+ execute_supplied_statement(self.db_interface,
213
+ QUERY__mark_rendered_document_completed_with_error,
214
+ { KEY__document_id: resp[KEY__document_id] })
215
+ time.sleep(0.25)
216
+ else:
217
+ traceback.print_exc()
127
218
 
128
219
  def start_chrome(self):
129
220
  options = Options()
130
221
  options.add_argument("--window-size=1920,1080")
131
- options.headless = True
222
+ options.add_argument("--force-color-profile=srgb")
223
+ options.add_argument("--font-render-hinting=none")
224
+ options.add_argument("--disable-gpu")
225
+ options.add_argument("--disable-dev-shm-usage") # ✱ stop /dev/shm exhaustion
226
+ options.add_argument("--disable-extensions")
227
+ options.add_argument("--headless=new")
228
+ if CHROME_DEBUGGING:
229
+ options.add_argument("--enable-logging=stderr")
230
+ options.add_argument("--v=1")
231
+ options.set_capability(
232
+ "goog:loggingPrefs",
233
+ {"browser": "ALL", "performance": "ALL"}
234
+ )
235
+
132
236
  service_args = []
133
237
 
134
238
  if self.is_deployed:
@@ -136,6 +240,7 @@ class DrivenChrome:
136
240
 
137
241
  with webdriver.Chrome(options=options, service=Service(service_args=service_args)) as driver:
138
242
  self.driver = driver
243
+ threading.Thread(target=self.render_document_requests, daemon=True).start()
139
244
  while True:
140
245
  current_attachment: 'EmailAttachment' = self.attachments.get()
141
246
  try:
@@ -144,7 +249,7 @@ class DrivenChrome:
144
249
  if not content_path.endswith(".html"):
145
250
  content_path += ".html"
146
251
  content_path = current_attachment.template_base + "/" + content_path
147
- filename, content = self.chrome_page_to_pdf(content_path, current_attachment.access_token, current_attachment.parameters)
252
+ filename, content = self.chrome_page_to_pdf(content_path, current_attachment.access_token, current_attachment.parameters, str(uuid.uuid4()))
148
253
  current_attachment.content = content
149
254
  current_attachment.filename = filename
150
255
  except HttpStatusException as ex:
@@ -182,3 +182,14 @@ class JAAQLController(BaseJAAQLController):
182
182
  @self.publish_route('/.well-known/jwks', DOCUMENTATION__jwks)
183
183
  def fetch_jwks():
184
184
  return self.model.fetch_jwks()
185
+
186
+ @self.publish_route('/documents', DOCUMENTATION__document)
187
+ def documents(http_inputs: dict, auth_token: str, ip_address: str, response: JAAQLResponse):
188
+ if self.is_get():
189
+ return self.model.fetch_document(http_inputs, response)
190
+ else:
191
+ return self.model.render_document(http_inputs, auth_token, ip_address)
192
+
193
+ @self.publish_route('/rendered_documents', DOCUMENTATION__rendered_document)
194
+ def documents(http_inputs: dict):
195
+ return self.model.fetch_document_stream(http_inputs)
@@ -1,5 +1,5 @@
1
1
  """
2
- This script was generated from jaaql.exceptions.fxli at 2025-04-14, 14:08:43
2
+ This script was generated from jaaql.exceptions.fxli at 2025-05-21, 11:25:44
3
3
  """
4
4
 
5
5
  from jaaql.utilities.crypt_utils import get_repeatable_salt
@@ -1,5 +1,5 @@
1
1
  """
2
- This script was generated from build_and_run.fxls at 2025-04-14, 14:08:43
2
+ This script was generated from build_and_run.fxls at 2025-05-21, 11:25:44
3
3
  """
4
4
 
5
5
  from jaaql.db.db_interface import DBInterface
@@ -717,17 +717,18 @@ KG__document_request__render_timestamp = "render_timestamp"
717
717
  KG__document_request__create_file = "create_file"
718
718
  KG__document_request__file_name = "file_name"
719
719
  KG__document_request__content = "content"
720
+ KG__document_request__completed = "completed"
720
721
 
721
722
  # Generated queries for table 'document_request'
722
723
  QG__document_request_delete = "DELETE FROM document_request WHERE uuid = :uuid"
723
724
  QG__document_request_insert = """
724
- INSERT INTO document_request (application, template, uuid,
725
- encrypted_access_token, encrypted_parameters, render_timestamp,
726
- create_file, file_name, content)
727
- VALUES (:application, :template, :uuid,
728
- :encrypted_access_token, :encrypted_parameters, :render_timestamp,
729
- :create_file, :file_name, :content)
730
- RETURNING request_timestamp
725
+ INSERT INTO document_request (application, template, encrypted_access_token,
726
+ encrypted_parameters, render_timestamp, create_file,
727
+ file_name, content, completed)
728
+ VALUES (:application, :template, :encrypted_access_token,
729
+ :encrypted_parameters, :render_timestamp, :create_file,
730
+ :file_name, :content, :completed)
731
+ RETURNING uuid, request_timestamp
731
732
  """
732
733
  QG__document_request_select_all = "SELECT * FROM document_request"
733
734
  QG__document_request_select = "SELECT * FROM document_request WHERE uuid = :uuid"
@@ -743,7 +744,8 @@ QG__document_request_update = """
743
744
  render_timestamp = COALESCE(:render_timestamp, render_timestamp),
744
745
  create_file = COALESCE(:create_file, create_file),
745
746
  file_name = COALESCE(:file_name, file_name),
746
- content = COALESCE(:content, content)
747
+ content = COALESCE(:content, content),
748
+ completed = COALESCE(:completed, completed)
747
749
  WHERE
748
750
  uuid = :uuid
749
751
  """
@@ -769,6 +771,7 @@ def document_request__update(
769
771
  application=None, template=None, request_timestamp=None,
770
772
  encrypted_access_token=None, encrypted_parameters=None, render_timestamp=None,
771
773
  create_file=None, file_name=None, content=None,
774
+ completed=None,
772
775
  encryption_salts=None
773
776
  ):
774
777
  execute_supplied_statement(
@@ -786,7 +789,8 @@ def document_request__update(
786
789
  KG__document_request__render_timestamp: render_timestamp,
787
790
  KG__document_request__create_file: create_file,
788
791
  KG__document_request__file_name: file_name,
789
- KG__document_request__content: content
792
+ KG__document_request__content: content,
793
+ KG__document_request__completed: completed
790
794
  }, encryption_key=encryption_key, encryption_salts=encryption_salts, encrypt_parameters=[
791
795
  KG__document_request__encrypted_access_token,
792
796
  KG__document_request__encrypted_parameters
@@ -826,10 +830,10 @@ def document_request__select_all(
826
830
 
827
831
  def document_request__insert(
828
832
  connection: DBInterface, encryption_key: bytes,
829
- application, template, uuid,
830
- encrypted_access_token, create_file,
833
+ application, template, encrypted_access_token,
834
+ create_file,
831
835
  encrypted_parameters=None, render_timestamp=None, file_name=None,
832
- content=None,
836
+ content=None, completed=None,
833
837
  encryption_salts=None
834
838
  ):
835
839
  return execute_supplied_statement_singleton(
@@ -837,13 +841,13 @@ def document_request__insert(
837
841
  {
838
842
  KG__document_request__application: application,
839
843
  KG__document_request__template: template,
840
- KG__document_request__uuid: uuid,
841
844
  KG__document_request__encrypted_access_token: encrypted_access_token,
842
845
  KG__document_request__encrypted_parameters: encrypted_parameters,
843
846
  KG__document_request__render_timestamp: render_timestamp,
844
847
  KG__document_request__create_file: create_file,
845
848
  KG__document_request__file_name: file_name,
846
- KG__document_request__content: content
849
+ KG__document_request__content: content,
850
+ KG__document_request__completed: completed
847
851
  }, encryption_key=encryption_key, encryption_salts=encryption_salts, encrypt_parameters=[
848
852
  KG__document_request__encrypted_access_token,
849
853
  KG__document_request__encrypted_parameters
@@ -13,6 +13,8 @@ import re
13
13
 
14
14
  import jwt
15
15
  from jwcrypto import jwe
16
+ from io import BytesIO
17
+ from flask import send_file
16
18
 
17
19
  from jwt import PyJWKClient
18
20
 
@@ -112,6 +114,10 @@ SIGNUP__completed = 3
112
114
 
113
115
  KEY__is_the_anonymous_user = "is_the_anonymous_user"
114
116
 
117
+ QUERY__ins_rendered_document = "INSERT INTO document_request (encrypted_parameters, encrypted_access_token, template, create_file, application) VALUES (:parameters, :oauth_token, :name, :create_file, :application) RETURNING uuid as document_id"
118
+ QUERY__purge_rendered_document = "DELETE FROM document_request WHERE completed is not null and uuid = :document_id RETURNING content"
119
+ QUERY__fetch_rendered_document = "SELECT app.base_url, rd.uuid as document_id, 'pdf' as render_as, rd.file_name, rd.create_file, rd.completed, rd.encrypted_access_token as oauth_token FROM document_request rd INNER JOIN document_template able ON rd.template = able.name INNER JOIN application app ON app.name = rd.application WHERE rd.uuid = :document_id"
120
+
115
121
 
116
122
  class JAAQLModel(BaseJAAQLModel):
117
123
  VERIFICATION_QUEUE = None
@@ -1255,6 +1261,53 @@ WHERE
1255
1261
  KEY__username: account[KG__account__username]
1256
1262
  }
1257
1263
 
1264
+ def fetch_document(self, inputs: dict, response: JAAQLResponse):
1265
+ res = execute_supplied_statement_singleton(self.jaaql_lookup_connection, QUERY__fetch_rendered_document,
1266
+ {KEY__document_id: inputs[KEY__document_id]}, singleton_message=ERR__document_id_not_found,
1267
+ as_objects=True)
1268
+
1269
+ if not res[KEY__completed]:
1270
+ raise HttpStatusException(ERR__document_still_rendering, HTTPStatus.TOO_EARLY)
1271
+
1272
+ if res[KEY__create_file]:
1273
+ if inputs[KEY__as_attachment] is not None:
1274
+ raise HttpStatusException(ERR__as_attachment_unexpected)
1275
+ response.response_code = HTTPStatus.CREATED
1276
+ return res[KG__application__base_url] + "/" + DIR__render_template + "/" + str(res[KEY__document_id]) + "." + res[KEY__render_as]
1277
+ else:
1278
+ return res[KG__application__base_url] + "/api/rendered_documents/" + res[KEY__document_id]
1279
+
1280
+ def fetch_document_stream(self, inputs: dict):
1281
+ res = execute_supplied_statement_singleton(self.jaaql_lookup_connection, QUERY__fetch_rendered_document,
1282
+ {KEY__document_id: inputs[KEY__document_id]}, singleton_message=ERR__document_id_not_found,
1283
+ as_objects=True)
1284
+
1285
+ if not res[KEY__completed]:
1286
+ raise HttpStatusException(ERR__document_still_rendering, HTTPStatus.TOO_EARLY)
1287
+
1288
+ if res[KEY__create_file]:
1289
+ raise HttpStatusException(ERR__document_created_file)
1290
+ else:
1291
+ content = execute_supplied_statement_singleton(self.jaaql_lookup_connection, QUERY__purge_rendered_document,
1292
+ {KEY__document_id: inputs[KEY__document_id]}, as_objects=True)[KEY__content]
1293
+ as_attachment = False
1294
+ if inputs[KEY__as_attachment]:
1295
+ as_attachment = True
1296
+
1297
+ buffer = BytesIO()
1298
+ buffer.write(content)
1299
+ buffer.seek(0)
1300
+
1301
+ return send_file(buffer, as_attachment=as_attachment, download_name=res[KEY__filename])
1302
+
1303
+ def render_document(self, inputs: dict, auth_token: str, ip_address: str):
1304
+ inputs[KEY__oauth_token] = self.refresh_auth_token(auth_token, ip_address)
1305
+ if inputs[KEY__parameters] is not None:
1306
+ inputs[KEY__parameters] = json.dumps(inputs[KEY__parameters])
1307
+ return execute_supplied_statement_singleton(self.jaaql_lookup_connection, QUERY__ins_rendered_document, inputs,
1308
+ encryption_key=self.get_db_crypt_key(), encrypt_parameters=[KEY__parameters, KEY__oauth_token],
1309
+ as_objects=True)
1310
+
1258
1311
  def send_email(self, is_the_anonymous_user: bool, account_id: str, inputs: dict, username: str, auth_token: str):
1259
1312
  app = application__select(self.jaaql_lookup_connection, inputs[KEY__application])
1260
1313
 
@@ -1,7 +1,7 @@
1
1
  -- This installation module was generated from ..\..\Packages/DBMS/Postgres/15/domains.jsql.fxlp for Postgres/15
2
2
 
3
3
  CREATE DOMAIN encrypted__email_server_password AS character varying(256);
4
- CREATE DOMAIN encrypted__access_token AS character varying(64);
4
+ CREATE DOMAIN encrypted__access_token AS text;
5
5
  CREATE DOMAIN encrypted__oidc_client_id AS character varying(200);
6
6
  CREATE DOMAIN encrypted__oidc_client_secret AS character varying(200);
7
7
  CREATE DOMAIN encrypted__oidc_sub AS character varying(200);
@@ -143,7 +143,7 @@ create table document_template (
143
143
  create table document_request (
144
144
  application internet_name not null,
145
145
  template object_name not null,
146
- uuid uuid not null,
146
+ uuid uuid not null default gen_random_uuid(),
147
147
  request_timestamp timestamptz not null default current_timestamp,
148
148
  encrypted_access_token encrypted__access_token not null,
149
149
  encrypted_parameters text,
@@ -151,6 +151,7 @@ create table document_request (
151
151
  create_file bool not null,
152
152
  file_name file_name,
153
153
  content bytea,
154
+ completed timestamptz,
154
155
  primary key (uuid) );
155
156
  -- federation_procedure...
156
157
  create table federation_procedure (
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: jaaql-middleware-python
3
- Version: 4.31.0
3
+ Version: 4.32.1
4
4
  Summary: The jaaql package, allowing for rapid development and deployment of RESTful HTTP applications
5
5
  Home-page: https://github.com/JAAQL/JAAQL-middleware-python
6
6
  Author: Software Quality Measurement and Improvement bv
@@ -10,7 +10,7 @@ Description-Content-Type: text/markdown
10
10
  License-File: LICENSE.txt
11
11
  Requires-Dist: jaaql-monitor~=1.6.3
12
12
  Requires-Dist: psycopg[binary]~=3.1.18
13
- Requires-Dist: Pillow~=10.3.0
13
+ Requires-Dist: Pillow~=11.2.1
14
14
  Requires-Dist: cryptography~=44.0.2
15
15
  Requires-Dist: flask~=3.1.0
16
16
  Requires-Dist: coverage~=7.4.3
@@ -33,6 +33,15 @@ Requires-Dist: jwcrypto~=1.5.6
33
33
  Requires-Dist: jinja2~=3.1.6
34
34
  Requires-Dist: idna~=3.10
35
35
  Requires-Dist: certifi~=2025.1.31
36
+ Dynamic: author
37
+ Dynamic: author-email
38
+ Dynamic: description
39
+ Dynamic: description-content-type
40
+ Dynamic: home-page
41
+ Dynamic: license
42
+ Dynamic: license-file
43
+ Dynamic: requires-dist
44
+ Dynamic: summary
36
45
 
37
46
  # JAAQL-middleware-python
38
47
  Please navigate to docker/docker.md to see setup instructions
@@ -1,6 +1,6 @@
1
1
  jaaql-monitor~=1.6.3
2
2
  psycopg[binary]~=3.1.18
3
- Pillow~=10.3.0
3
+ Pillow~=11.2.1
4
4
  cryptography~=44.0.2
5
5
  flask~=3.1.0
6
6
  coverage~=7.4.3