hyperflask 0.3.1__tar.gz → 0.4.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 (121) hide show
  1. {hyperflask-0.3.1 → hyperflask-0.4.1}/PKG-INFO +7 -7
  2. {hyperflask-0.3.1 → hyperflask-0.4.1}/README.md +0 -1
  3. {hyperflask-0.3.1 → hyperflask-0.4.1}/pyproject.toml +7 -8
  4. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/__init__.py +2 -0
  5. hyperflask-0.4.1/src/hyperflask/actors.py +82 -0
  6. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/app.py +72 -20
  7. hyperflask-0.4.1/src/hyperflask/cli/__init__.py +53 -0
  8. hyperflask-0.4.1/src/hyperflask/cli/reloader.py +24 -0
  9. hyperflask-0.3.1/src/hyperflask/cli.py → hyperflask-0.4.1/src/hyperflask/cli/runner.py +34 -53
  10. hyperflask-0.4.1/src/hyperflask/cli/worker.py +90 -0
  11. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/components/__init__.py +4 -3
  12. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/components/alpine.py +1 -1
  13. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/components/core.py +48 -30
  14. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/components/jsx.py +1 -1
  15. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/components/webcomponent.py +1 -1
  16. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/factory.py +6 -10
  17. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/layouts/web.html +1 -8
  18. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/model.py +7 -0
  19. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/static/htmx-exts.js +8 -5
  20. hyperflask-0.4.1/src/hyperflask/utils/metadata.py +182 -0
  21. hyperflask-0.3.1/src/hyperflask/ui/metadata.html +0 -4
  22. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/__main__.py +0 -0
  23. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/forms/__init__.py +0 -0
  24. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/forms/fields.py +0 -0
  25. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/forms/widgets.py +0 -0
  26. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/layouts/email.mjml +0 -0
  27. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/page_helpers.py +0 -0
  28. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/security.py +0 -0
  29. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/static/alpine-csp.js +0 -0
  30. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/static/alpine.js +0 -0
  31. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/static/app.css +0 -0
  32. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/static/app.js +0 -0
  33. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/static/react.js +0 -0
  34. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/static/stimulus.js +0 -0
  35. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/static/translations.js +0 -0
  36. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Accordion.html +0 -0
  37. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/AccordionPanel.html +0 -0
  38. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Alert.html +0 -0
  39. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Avatar.html +0 -0
  40. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/AvatarGroup.html +0 -0
  41. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Badge.html +0 -0
  42. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Breadcrumbs.html +0 -0
  43. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/BreadcrumbsItem.html +0 -0
  44. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Button.html +0 -0
  45. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Card.html +0 -0
  46. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/CardActions.html +0 -0
  47. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/CardTitle.html +0 -0
  48. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/ChatBubble.html +0 -0
  49. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Checkbox.html +0 -0
  50. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/CollapsibleSubMenu.html +0 -0
  51. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Countdown.html +0 -0
  52. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Divider.html +0 -0
  53. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Drawer.html +0 -0
  54. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/DrawerContent.html +0 -0
  55. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/DrawerSide.html +0 -0
  56. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Dropdown.html +0 -0
  57. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/DropdownItem.html +0 -0
  58. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Fieldset.html +0 -0
  59. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/FieldsetLegend.html +0 -0
  60. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/FileInput.html +0 -0
  61. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Filter.html +0 -0
  62. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/FilterItem.html +0 -0
  63. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Footer.html +0 -0
  64. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/FooterLink.html +0 -0
  65. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/FooterNav.html +0 -0
  66. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Indicator.html +0 -0
  67. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Input.html +0 -0
  68. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/InputLabel.html +0 -0
  69. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Join.html +0 -0
  70. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Kbd.html +0 -0
  71. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Label.html +0 -0
  72. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/LabeledCheckbox.html +0 -0
  73. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/LabeledToggle.html +0 -0
  74. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Link.html +0 -0
  75. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/List.html +0 -0
  76. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/ListItem.html +0 -0
  77. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Loading.html +0 -0
  78. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Menu.html +0 -0
  79. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/MenuItem.html +0 -0
  80. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/MenuTitle.html +0 -0
  81. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Modal.html +0 -0
  82. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/ModalAction.html +0 -0
  83. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/ModalTitle.html +0 -0
  84. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Navbar.html +0 -0
  85. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Pagination.html +0 -0
  86. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/PaginationNext.html +0 -0
  87. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/PaginationPage.html +0 -0
  88. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/PaginationPrev.html +0 -0
  89. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Progress.html +0 -0
  90. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/README.md +0 -0
  91. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Radio.html +0 -0
  92. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Range.html +0 -0
  93. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Rating.html +0 -0
  94. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Select.html +0 -0
  95. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Stack.html +0 -0
  96. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Stat.html +0 -0
  97. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Stats.html +0 -0
  98. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Step.html +0 -0
  99. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Steps.html +0 -0
  100. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/SubMenu.html +0 -0
  101. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Swap.html +0 -0
  102. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Tab.html +0 -0
  103. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Table.html +0 -0
  104. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Tabs.html +0 -0
  105. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Textarea.html +0 -0
  106. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Toast.html +0 -0
  107. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Toggle.html +0 -0
  108. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Tooltip.html +0 -0
  109. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/flash.html +0 -0
  110. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/forms.html +0 -0
  111. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/icons.html +0 -0
  112. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/layout.html +0 -0
  113. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/modal.html +0 -0
  114. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/skeleton.html +0 -0
  115. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/sse.html +0 -0
  116. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/utils/__init__.py +0 -0
  117. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/utils/html.py +0 -0
  118. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/utils/htmx.py +0 -0
  119. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/utils/markdown.py +0 -0
  120. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/utils/page_actions.py +0 -0
  121. {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/utils/tokens.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperflask
3
- Version: 0.3.1
3
+ Version: 0.4.1
4
4
  Summary: Full stack web framework
5
5
  Author: Maxime Bouroumeau-Fuseau
6
6
  Author-email: Maxime Bouroumeau-Fuseau <maxime.bouroumeau@gmail.com>
@@ -11,17 +11,18 @@ Requires-Dist: flask-babel-hyper~=5.1
11
11
  Requires-Dist: flask-collections~=0.2
12
12
  Requires-Dist: flask-configurator~=0.1
13
13
  Requires-Dist: flask-debugtoolbar~=0.16
14
- Requires-Dist: flask-dramatiq~=0.6
15
14
  Requires-Dist: flask-file-routes~=0.3
16
15
  Requires-Dist: flask-files~=0.2
17
16
  Requires-Dist: flask-geo~=0.1
18
17
  Requires-Dist: flask-mailman-templates~=0.1
19
- Requires-Dist: flask-mercure-sse~=0.3
18
+ Requires-Dist: flask-mercure-sse~=0.4
20
19
  Requires-Dist: flask-sqlorm~=0.3
21
- Requires-Dist: flask-super-macros~=0.1
22
- Requires-Dist: flask-suspense~=0.1
20
+ Requires-Dist: flask-super-macros~=0.2
21
+ Requires-Dist: flask-suspense~=0.3
23
22
  Requires-Dist: flask-wtf~=1.2
24
23
  Requires-Dist: frozen-flask~=1.0
24
+ Requires-Dist: dramatiq>=1.18.0
25
+ Requires-Dist: dramatiq-sqlite~=0.1
25
26
  Requires-Dist: gunicorn~=23.0
26
27
  Requires-Dist: honcho~=2.0
27
28
  Requires-Dist: html-sanitizer~=2.6
@@ -30,7 +31,7 @@ Requires-Dist: jinja-layout~=0.5
30
31
  Requires-Dist: jinja-wtforms~=0.2
31
32
  Requires-Dist: periodiq~=0.13
32
33
  Requires-Dist: python-slugify~=8.0.4
33
- Requires-Dist: redis~=6.4
34
+ Requires-Dist: watchdog>=6.0.0
34
35
  Requires-Python: >=3.10
35
36
  Project-URL: Repository, https://github.com/hyperflask/hyperflask
36
37
  Description-Content-Type: text/markdown
@@ -95,7 +96,6 @@ A good part of these extensions is developed as part of the Hyperflask project.
95
96
  | [Flask-Collections](https://github.com/hyperflask/flask-collections) | Manage collections of static content
96
97
  | [Flask-Configurator](https://github.com/hyperflask/flask-configurator) | File based configuration
97
98
  | [Flask-DebugToolbar](https://github.com/pallets-eco/flask-debugtoolbar) | Debug Toolbar
98
- | [Flask-Dramatiq](https://flask-dramatiq.readthedocs.io) | Background tasks powered by [Dramatiq](https://dramatiq.io/) |
99
99
  | [Flask-File-Routes](https://github.com/hyperflask/flask-file-routes) | File-based routing with a new file format combining python and jinja template in a single file
100
100
  | [Flask-Files](https://github.com/hyperflask/flask-files) | [Fsspec](https://filesystem-spec.readthedocs.io/en/latest/) based files management (upload, storage and image manipulation)
101
101
  | [Frozen-Flask](https://github.com/Frozen-Flask/Frozen) | Generate a static website from your Flask app |
@@ -58,7 +58,6 @@ A good part of these extensions is developed as part of the Hyperflask project.
58
58
  | [Flask-Collections](https://github.com/hyperflask/flask-collections) | Manage collections of static content
59
59
  | [Flask-Configurator](https://github.com/hyperflask/flask-configurator) | File based configuration
60
60
  | [Flask-DebugToolbar](https://github.com/pallets-eco/flask-debugtoolbar) | Debug Toolbar
61
- | [Flask-Dramatiq](https://flask-dramatiq.readthedocs.io) | Background tasks powered by [Dramatiq](https://dramatiq.io/) |
62
61
  | [Flask-File-Routes](https://github.com/hyperflask/flask-file-routes) | File-based routing with a new file format combining python and jinja template in a single file
63
62
  | [Flask-Files](https://github.com/hyperflask/flask-files) | [Fsspec](https://filesystem-spec.readthedocs.io/en/latest/) based files management (upload, storage and image manipulation)
64
63
  | [Frozen-Flask](https://github.com/Frozen-Flask/Frozen) | Generate a static website from your Flask app |
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "hyperflask"
3
- version = "0.3.1"
3
+ version = "0.4.1"
4
4
  description = "Full stack web framework"
5
5
  authors = [
6
6
  {"name" = "Maxime Bouroumeau-Fuseau", email = "maxime.bouroumeau@gmail.com"}
@@ -15,19 +15,19 @@ dependencies = [
15
15
  "flask-collections ~=0.2",
16
16
  "flask-configurator ~=0.1",
17
17
  "flask-debugtoolbar ~=0.16",
18
- "flask-dramatiq ~=0.6",
19
18
  "flask-file-routes ~=0.3",
20
19
  "flask-files ~=0.2",
21
20
  "flask-geo ~=0.1",
22
21
  "flask-mailman-templates ~=0.1",
23
- "flask-mercure-sse ~=0.3",
22
+ "flask-mercure-sse ~=0.4",
24
23
  #"flask-observability ~=0.1"
25
24
  "flask-sqlorm ~=0.3",
26
- "flask-super-macros ~=0.1",
27
- "flask-suspense ~=0.1",
25
+ "flask-super-macros ~=0.2",
26
+ "flask-suspense ~=0.3",
28
27
  "flask-wtf ~=1.2",
29
28
  "frozen-flask ~=1.0",
30
- #"dramatiq-sqlite ~=0.1"
29
+ "dramatiq>=1.18.0",
30
+ "dramatiq-sqlite ~=0.1",
31
31
  "gunicorn ~=23.0",
32
32
  "honcho ~=2.0",
33
33
  "html-sanitizer ~=2.6",
@@ -36,11 +36,10 @@ dependencies = [
36
36
  "jinja-wtforms ~=0.2",
37
37
  "periodiq ~=0.13",
38
38
  "python-slugify ~=8.0.4",
39
- "redis ~=6.4",
39
+ "watchdog>=6.0.0",
40
40
  ]
41
41
 
42
42
  [tool.uv.sources]
43
- flask-dramatiq = { git = "https://gitlab.com/bersace/flask-dramatiq" }
44
43
  periodiq = { git = "https://gitlab.com/bersace/periodiq" }
45
44
 
46
45
  [project.urls]
@@ -10,8 +10,10 @@ from flask_files import save_file, validate_file
10
10
  from flask_mailman_templates import send_mail
11
11
  from flask_mercure_sse import publish_signal, mercure_publish
12
12
  from flask_suspense import render_template, stream_template, defer
13
+ from flask_assets_pipeline import asset_url, static_url
13
14
 
14
15
  # useful exports from 3rd party libs
15
16
  from htmx_flask import request, make_response
16
17
  from slugify import slugify
17
18
  from blinker import signal
19
+ from periodiq import cron
@@ -0,0 +1,82 @@
1
+ from threading import local
2
+ from dramatiq import Middleware, actor
3
+ import importlib
4
+
5
+
6
+ DRAMATIQ_BROKERS = {
7
+ "amqp": "dramatiq.brokers.rabbitmq:RabbitmqBroker",
8
+ "redis": "dramatiq.brokers.redis:RedisBroker",
9
+ "sqlite": "dramatiq_sqlite:SQLiteBroker",
10
+ }
11
+
12
+
13
+ def discover_broker(broker_cls=None, broker_url=None):
14
+ if not broker_url and broker_cls and "://" in broker_cls:
15
+ broker_url = broker_cls
16
+ broker_cls = None
17
+ if not broker_cls and broker_url:
18
+ if "://" not in broker_url:
19
+ broker_name = "sqlite"
20
+ else:
21
+ broker_name, broker_url = broker_url.split("://", 1)
22
+ if broker_name not in DRAMATIQ_BROKERS:
23
+ raise ValueError(f"Unknown dramatiq broker '{broker_name}'")
24
+ broker_cls = DRAMATIQ_BROKERS[broker_name]
25
+ if isinstance(broker_cls, str):
26
+ module_name, cls_name = broker_cls.split(":")
27
+ module = importlib.import_module(module_name)
28
+ broker_cls = getattr(module, cls_name)
29
+ return broker_cls, broker_url
30
+
31
+
32
+ def make_actor_decorator(app):
33
+ def actor_decorator(**kw):
34
+ def decorator(fn):
35
+ if getattr(app, 'dramatiq_broker', None):
36
+ kw['broker'] = app.dramatiq_broker
37
+ return actor(**kw)(fn)
38
+ return UndefinedBrokerActor(fn)
39
+ return decorator
40
+ return actor_decorator
41
+
42
+
43
+ class AppContextMiddleware(Middleware):
44
+ # Setup Flask app for actor. Borrowed from
45
+ # https://github.com/Bogdanp/flask_dramatiq_example.
46
+
47
+ state = local()
48
+
49
+ def __init__(self, app):
50
+ self.app = app
51
+
52
+ def before_process_message(self, broker, message):
53
+ context = self.app.app_context()
54
+ context.push()
55
+
56
+ self.state.context = context
57
+
58
+ def after_process_message(
59
+ self, broker, message, *, result=None, exception=None):
60
+ try:
61
+ context = self.state.context
62
+ context.pop(exception)
63
+ del self.state.context
64
+ except AttributeError:
65
+ pass
66
+
67
+ after_skip_message = after_process_message
68
+
69
+
70
+ class UndefinedBrokerActor:
71
+ def __init__(self, fn):
72
+ self.fn = fn
73
+ self.__name__ = fn.__name__
74
+
75
+ def __call__(self, *args, **kwargs):
76
+ return self.fn(*args, **kwargs)
77
+
78
+ def send(self, *args, **kwargs):
79
+ raise RuntimeError("No broker defined for Dramatiq actors.")
80
+
81
+ def send_with_options(self, *args, **kwargs):
82
+ raise RuntimeError("No broker defined for Dramatiq actors.")
@@ -1,11 +1,11 @@
1
1
  # flask
2
- from flask import Flask
2
+ from flask import Flask, has_request_context, request
3
3
  from jinja2 import ChoiceLoader, FileSystemLoader, PrefixLoader, TemplateNotFound
4
4
  from werkzeug.middleware.proxy_fix import ProxyFix
5
5
  # hyperflask extensions
6
6
  from jinja_layout import LayoutExtension
7
7
  from jinja_wtforms import WtformExtension
8
- from flask_assets_pipeline import AssetsPipeline
8
+ from flask_assets_pipeline import AssetsPipeline, asset_url, static_url
9
9
  from flask_super_macros import SuperMacros
10
10
  from flask_collections import Collections
11
11
  from flask_file_routes import FileRoutes, ModuleView, page
@@ -16,14 +16,13 @@ from flask_files import Files
16
16
  from flask_sqlorm import FlaskSQLORM
17
17
  from flask_mercure_sse import MercureSSE
18
18
  from flask_mailman_templates import MailTemplates
19
- from flask_suspense import Suspense, render_template, suspense_before_render_template, suspense_before_render_blocks
19
+ from flask_suspense import Suspense, render_template, suspense_before_render_template, suspense_before_render_macros
20
20
  #from flask_observability import Observability
21
21
  # 3rd party extensions
22
22
  from flask_wtf.csrf import CSRFProtect
23
23
  from flask_frozen import Freezer
24
24
  from htmx_flask import Htmx
25
25
  from flask_mailman import Mail
26
- from flask_dramatiq import Dramatiq
27
26
  from flask_debugtoolbar import DebugToolbarExtension
28
27
  # from hyperflask
29
28
  from .forms import Form
@@ -31,8 +30,10 @@ from .utils.page_actions import page_action_url
31
30
  from .utils.htmx import htmx_oob, respond_with_flash_messages
32
31
  from .utils.markdown import jinja_markdown, MarkdownExtension
33
32
  from .utils.html import sanitize_html, nl2br
33
+ from .utils.metadata import metadata_tags
34
34
  from .components import register_components
35
- from .model import Model, File as SQLFileType
35
+ from .model import Model, File as SQLFileType, UndefinedDatabase
36
+ from .actors import AppContextMiddleware, discover_broker, make_actor_decorator
36
37
  from .security import respond_with_security_headers, csp_nonce
37
38
  from . import page_helpers
38
39
  # others
@@ -40,6 +41,7 @@ from jinja_super_macros.registry import FileLoader
40
41
  from jinja_wtforms.extractor import map_jinja_call_node_to_func
41
42
  from sqlorm.sql_template import SQLTemplate
42
43
  from periodiq import PeriodiqMiddleware
44
+ import dramatiq
43
45
  import os
44
46
 
45
47
 
@@ -55,7 +57,7 @@ class Hyperflask(Flask):
55
57
 
56
58
  def __init__(self, *args, static_mode="hybrid", instrument=False, layouts_folder="layouts",
57
59
  emails_folder="emails", forms_folder="forms", assets_folder="assets", pages_folder="pages", migrations_folder="migrations",
58
- config=None, config_filename="config.yml", database_uri="sqlite://:memory:", proxy_fix=True, **kwargs):
60
+ config=None, config_filename="config.yml", database_uri=None, proxy_fix=True, **kwargs):
59
61
 
60
62
  super().__init__(*args, **kwargs)
61
63
  if proxy_fix:
@@ -66,11 +68,12 @@ class Hyperflask(Flask):
66
68
  "FREEZER_STATIC_IGNORE": [],
67
69
  "WTF_CSRF_CHECK_DEFAULT": False,
68
70
  "OTEL_INSTRUMENT": instrument,
69
- "DRAMATIQ_BROKER": "dramatiq.brokers.redis:RedisBroker",
70
71
  "DEBUG_TB_INTERCEPT_REDIRECTS": False,
71
72
  "SESSION_COOKIE_HTTPONLY": True,
72
73
  "SESSION_COOKIE_SAMESITE": "Lax",
74
+ "SQLORM_URI": database_uri,
73
75
  # Hyperflask specific
76
+ "URL_FOR_SERVER_NAME": None,
74
77
  "LAYOUT": "layouts/default.html",
75
78
  "ALPINE": False,
76
79
  "STATIC_MODE": static_mode,
@@ -111,13 +114,16 @@ class Hyperflask(Flask):
111
114
  })
112
115
  self.config.setdefault("HSTS_HEADER", "max-age=31556926; includeSubDomains") # 1 year
113
116
 
117
+ if (self.config['URL_FOR_SERVER_NAME'] or self.config.get('SERVER_NAME')) and not self.config.get('MERCURE_HUB_URL') and not self.config.get('MERCURE_PUBLIC_HUB_URL'):
118
+ self.config['MERCURE_PUBLIC_HUB_URL'] = True
119
+
114
120
  #self.otel = Observability(self)
115
121
 
116
122
  self.jinja_env.add_extension(LayoutExtension)
117
123
  self.jinja_env.add_extension(WtformExtension)
118
124
  self.jinja_env.add_extension(MarkdownExtension)
119
125
  self.jinja_env.filters.update(markdown=jinja_markdown, sanitize=sanitize_html, nl2br=nl2br)
120
- self.jinja_env.globals.update(page_action_url=page_action_url, app=self, csp_nonce=csp_nonce)
126
+ self.jinja_env.globals.update(page_action_url=page_action_url, app=self, csp_nonce=csp_nonce, metadata_tags=metadata_tags)
121
127
  self.jinja_env.form_base_cls = Form
122
128
  self.jinja_env.default_layout = self.config["LAYOUT"]
123
129
  self.forms = self.jinja_env.forms
@@ -133,14 +139,11 @@ class Hyperflask(Flask):
133
139
  ])
134
140
 
135
141
  DebugToolbarExtension(self)
136
- self.freezer = Freezer(self)
142
+ self.freezer = Freezer(self, with_no_argument_rules=False)
143
+ self.freezer.register_generator(lambda: freezer_url_generator(self))
137
144
  self.csrf = CSRFProtect(self)
138
- #self.talisman = Talisman(self)
139
145
  Htmx(self)
140
146
  Mail(self)
141
- self.dramatiq = Dramatiq(self)
142
- self.dramatiq.broker.add_middleware(PeriodiqMiddleware())
143
- self.actor = self.dramatiq.actor
144
147
 
145
148
  SuperMacros(self)
146
149
  self.components = self.macros # alias
@@ -205,10 +208,20 @@ class Hyperflask(Flask):
205
208
 
206
209
  self.collections.register_freezer_generator(self.freezer)
207
210
 
208
- SQLTemplate.eval_globals.update(app=self)
209
- self.db = FlaskSQLORM(self, database_uri=database_uri, migrations_folder=migrations_folder)
210
- self.db.Model = Model
211
- self.db.File = SQLFileType
211
+ if self.config.get('SQLORM_URI'):
212
+ SQLTemplate.eval_globals.update(app=self)
213
+ self.db = FlaskSQLORM(self, migrations_folder=migrations_folder)
214
+ self.db.Model = Model
215
+ self.db.File = SQLFileType
216
+ else:
217
+ self.db = UndefinedDatabase()
218
+
219
+ broker_cls, broker_url = discover_broker(self.config.get('DRAMATIQ_BROKER'), self.config.get('DRAMATIQ_BROKER_URL'))
220
+ if broker_cls:
221
+ self.dramatiq_broker = broker_cls(url=broker_url)
222
+ self.dramatiq_broker.add_middleware(AppContextMiddleware(self))
223
+ self.dramatiq_broker.add_middleware(PeriodiqMiddleware())
224
+ self.actor = make_actor_decorator(self)
212
225
 
213
226
  Suspense(self, nonce_getter="csp_nonce()")
214
227
 
@@ -216,9 +229,10 @@ class Hyperflask(Flask):
216
229
  def on_suspense_before_render_template(sender, **kwargs):
217
230
  csp_nonce() # we will need a nonce but the first call will be after the template started rendering
218
231
 
219
- @suspense_before_render_blocks.connect_via(self, weak=False)
220
- def on_suspense_before_render_blocks(sender, **kwargs):
221
- return f'<script nonce="{csp_nonce()}">htmx.bootstrap()</script>'
232
+ @suspense_before_render_macros.connect_via(self, weak=False)
233
+ def on_suspense_before_render_macros(sender, **kwargs):
234
+ return ""
235
+ return f'<script nonce="{csp_nonce()}">htmx.bootstrap()</script>' # waiting on PR 3441
222
236
 
223
237
  @self.errorhandler(404)
224
238
  def not_found(error):
@@ -237,8 +251,38 @@ class Hyperflask(Flask):
237
251
  def relative_import_name(self, name):
238
252
  return f"{self.import_name}.{name}" if self.import_name != "__main__" else name
239
253
 
254
+ @property
255
+ def base_url(self):
256
+ scheme = self.config.get('PREFERRED_URL_SCHEME', 'http')
257
+ if self.config.get('SERVER_NAME'):
258
+ return f"{scheme}://{self.config['SERVER_NAME']}"
259
+ if self.config['URL_FOR_SERVER_NAME']:
260
+ return f"{scheme}://{self.config['URL_FOR_SERVER_NAME']}"
261
+ if has_request_context():
262
+ return request.host_url
263
+ raise RuntimeError("No request context and no SERVER_NAME or URL_FOR_SERVER_NAME configured, cannot determine base_url")
264
+
265
+ def url_for(self, *args, **kwargs):
266
+ _external = kwargs.get("_external", False)
267
+ _scheme = kwargs.get("_scheme", None)
268
+ if _external and self.config['URL_FOR_SERVER_NAME']:
269
+ # we handle the host and scheme ourselves
270
+ kwargs['_external'] = False
271
+ kwargs.pop('_scheme', None)
272
+ else:
273
+ _external = False
274
+ url = super().url_for(*args, **kwargs)
275
+ if not _external:
276
+ return url
277
+ if not _scheme:
278
+ _scheme = self.config.get('PREFERRED_URL_SCHEME', 'http')
279
+ url = f"{_scheme}://{self.config['URL_FOR_SERVER_NAME']}{url}"
280
+ return url
281
+
240
282
 
241
283
  class HyperModuleView(ModuleView):
284
+ module_globals = dict(ModuleView.module_globals, asset_url=asset_url, static_url=static_url)
285
+
242
286
  def _render_template(self):
243
287
  return render_template(page.template)
244
288
 
@@ -251,3 +295,11 @@ def dynamic(func=None, static_template=None):
251
295
  def decorator(func):
252
296
  pass
253
297
  return decorator(func) if func else decorator
298
+
299
+
300
+ def freezer_url_generator(app):
301
+ """URL generator for URL rules that take no arguments."""
302
+ for rule in app.url_map.iter_rules():
303
+ if not rule.arguments and 'GET' in rule.methods and not rule.endpoint.startswith('mercure'):
304
+ app.logger.info(rule.endpoint)
305
+ yield rule.endpoint, {}
@@ -0,0 +1,53 @@
1
+ from flask import current_app
2
+ from flask.cli import FlaskGroup, shell_command, routes_command, ScriptInfo, NoAppException
3
+ from flask_assets_pipeline import cli as assets_cli
4
+ import click
5
+ from ..factory import create_app
6
+ from ..security import generate_csp_policy
7
+ from .runner import serve_command, run_command, dev_command
8
+ from .worker import worker_command, scheduler_command
9
+
10
+
11
+ def _create_app():
12
+ info = ScriptInfo()
13
+ try:
14
+ return info.load_app()
15
+ except NoAppException:
16
+ return create_app()
17
+
18
+
19
+ cli = FlaskGroup(name='kantree', help='Kantree', add_default_commands=False, create_app=_create_app)
20
+ cli.add_command(shell_command)
21
+ cli.add_command(routes_command)
22
+ cli.add_command(serve_command)
23
+ cli.add_command(run_command)
24
+ cli.add_command(dev_command)
25
+ cli.add_command(worker_command)
26
+ cli.add_command(scheduler_command)
27
+
28
+
29
+ @cli.command("init")
30
+ def init_command():
31
+ pass
32
+
33
+
34
+ @cli.command("freeze")
35
+ def freeze_command():
36
+ current_app.freezer.freeze()
37
+
38
+
39
+ @cli.command("build")
40
+ @click.pass_context
41
+ def build_command(ctx):
42
+ ctx.invoke(assets_cli.build)
43
+ ctx.invoke(freeze_command)
44
+
45
+
46
+ @cli.command("csp-header")
47
+ def csp_header_command():
48
+ click.echo(generate_csp_policy())
49
+
50
+
51
+
52
+ def main():
53
+ cli.main()
@@ -0,0 +1,24 @@
1
+ from werkzeug._reloader import WatchdogReloaderLoop, reloader_loops
2
+ import os.path
3
+
4
+
5
+ # watchdog is a dependency of hyperflask so it would always be selected as the reloader
6
+ class WatchdogWithExtraFilesPatternsReloaderLoop(WatchdogReloaderLoop):
7
+ def __init__(self, *args, extra_files=None, **kwargs):
8
+ # VERY HACKY SOLUTION !
9
+ # in werkzeug reloader, it is not possible to add patterns as extra_files
10
+ # to circumvent this and avoid copy/pasting big chunk of code, we patch os.path.abspath
11
+ # which is used to process the list of extra_files in the __init__
12
+ extra_files = list(extra_files) if extra_files else []
13
+ extra_files.append("*.jpy")
14
+ abspath = os.path.abspath
15
+ def patched_abspath(path):
16
+ if path.startswith("*."):
17
+ return path
18
+ return abspath(path)
19
+ os.path.abspath = patched_abspath
20
+ super().__init__(*args, extra_files=extra_files, **kwargs)
21
+ os.path.abspath = abspath
22
+
23
+
24
+ reloader_loops["auto"] = WatchdogWithExtraFilesPatternsReloaderLoop
@@ -1,48 +1,13 @@
1
- from flask import current_app
2
- from flask.cli import FlaskGroup, shell_command, routes_command, _debug_option, pass_script_info, run_command as flask_run_command, ScriptInfo, NoAppException
3
- from flask_assets_pipeline import cli as assets_cli
1
+ from flask.cli import _debug_option, pass_script_info, run_command as flask_run_command
4
2
  import click
5
3
  import os
6
4
  import sys
7
5
  import multiprocessing
8
6
  import gunicorn.app.base
9
- from .factory import load_config, create_app
10
- from .security import generate_csp_policy
11
-
12
-
13
- def _create_app():
14
- info = ScriptInfo()
15
- try:
16
- return info.load_app()
17
- except NoAppException:
18
- return create_app()
19
-
20
-
21
- cli = FlaskGroup(name='kantree', help='Kantree', add_default_commands=False, create_app=_create_app)
22
- cli.add_command(shell_command)
23
- cli.add_command(routes_command)
24
-
25
-
26
- @cli.command("init")
27
- def init_command():
28
- pass
29
-
30
-
31
- @cli.command("freeze")
32
- def freeze_command():
33
- current_app.freezer.freeze()
34
-
35
-
36
- @cli.command("build")
37
- @click.pass_context
38
- def build_command(ctx):
39
- ctx.invoke(assets_cli.build)
40
- ctx.invoke(freeze_command)
41
-
42
-
43
- @cli.command("csp-header")
44
- def csp_header_command():
45
- click.echo(generate_csp_policy())
7
+ from subprocess import Popen
8
+ from honcho.manager import Manager as BaseProcessManager
9
+ from ..factory import load_config
10
+ from . import reloader # import hacky patch of werkzeug reloader
46
11
 
47
12
 
48
13
  @click.command("serve")
@@ -57,8 +22,6 @@ def serve_command(ctx, info, host, port, gunicorn_opt, **run_kwargs):
57
22
  'proc_name': 'hyperflask',
58
23
  'bind': f'{host}:{port}',
59
24
  'workers': 1 + multiprocessing.cpu_count() * 2,
60
- 'accesslog': '-',
61
- 'errorlog': '-',
62
25
  'max_requests': 1000,
63
26
  'max_requests_jitter': 100
64
27
  }
@@ -69,7 +32,6 @@ def serve_command(ctx, info, host, port, gunicorn_opt, **run_kwargs):
69
32
 
70
33
 
71
34
  serve_command.params = flask_run_command.params + serve_command.params
72
- cli.add_command(serve_command)
73
35
 
74
36
 
75
37
  @click.command("run")
@@ -78,16 +40,20 @@ cli.add_command(serve_command)
78
40
  @click.option("--port", "-p", default=5000, help="The port to bind to.")
79
41
  @click.option('--concurrency')
80
42
  @click.option("--extend-procfile")
43
+ @click.option("--init-db/--no-init-db", is_flag=True, default=None)
81
44
  @click.option("--dev", is_flag=True)
82
- def run_command(processes, host, port, concurrency, extend_procfile, dev):
45
+ def run_command(processes, host, port, concurrency, extend_procfile, init_db, dev):
83
46
  from honcho.command import _procfile, _parse_concurrency
84
- from honcho.manager import Manager
85
47
  from honcho.printer import Printer
86
48
  from honcho import environ
87
49
 
88
50
  if dev:
89
51
  os.environ["FLASK_DEBUG"] = "1"
90
52
 
53
+ if init_db is None and dev or init_db:
54
+ print("Initializing database...")
55
+ Popen([sys.argv[0], "db", "init"]).wait()
56
+
91
57
  _processes = None
92
58
  config = load_config()
93
59
  for filename in [f"Procfile.{config['ENV']}", "Procfile"]:
@@ -99,18 +65,29 @@ def run_command(processes, host, port, concurrency, extend_procfile, dev):
99
65
  _processes = {}
100
66
  _processes.update({
101
67
  "web": [sys.argv[0], "serve", "--host", host, "--port", "$PORT"],
102
- # "worker": [sys.argv[0], "worker"],
103
- # "scheduler": [sys.argv[0], "periodiq"]
68
+ "worker": [sys.argv[0], "worker"],
69
+ "scheduler": [sys.argv[0], "scheduler"]
104
70
  })
105
71
  if dev:
106
72
  _processes["assets"] = [sys.argv[0], "assets", "dev"]
73
+ _processes["worker"].extend(["-p", "1", "-t", "1"])
107
74
  else:
108
75
  _processes["mercurehub"] = [sys.executable, "-m", "flask_mercure_sse.server"]
76
+ subscriber_secret_key = config.get("MERCURE_SUBSCRIBER_SECRET_KEY", config.get('SECRET_KEY'))
77
+ if subscriber_secret_key:
78
+ _processes["mercurehub"].extend(["--subscriber-secret", subscriber_secret_key])
79
+ else:
80
+ print("Warning: No MERCURE_SUBSCRIBER_SECRET_KEY or SECRET_KEY set, Mercure subscriber will not be authenticated.")
81
+ publisher_secret_key = config.get("MERCURE_PUBLISHER_SECRET_KEY", config.get('SECRET_KEY'))
82
+ if publisher_secret_key:
83
+ _processes["mercurehub"].extend(["--publisher-secret", publisher_secret_key])
84
+ else:
85
+ print("Warning: No MERCURE_PUBLISHER_SECRET_KEY or SECRET_KEY set, cannot publish Mercure messages.")
109
86
 
110
87
  processes = {name: " ".join(_processes[name]) if isinstance(_processes[name], list) else _processes[name]
111
- for name in _processes if name in _processes}
88
+ for name in _processes if not processes or name in processes}
112
89
 
113
- manager = Manager(Printer(sys.stdout, colour=True))
90
+ manager = ProcessManager(Printer(sys.stdout, colour=True))
114
91
 
115
92
  for p in environ.expand_processes(processes,
116
93
  concurrency=_parse_concurrency(concurrency),
@@ -119,12 +96,12 @@ def run_command(processes, host, port, concurrency, extend_procfile, dev):
119
96
  e.update(p.env)
120
97
  manager.add_process(p.name, p.cmd, quiet=p.quiet, env=e)
121
98
 
99
+ print("Starting Hyperflask processes...")
122
100
  manager.loop()
123
101
  sys.exit(manager.returncode)
124
102
 
125
103
 
126
104
  run_command.params.insert(0, _debug_option)
127
- cli.add_command(run_command)
128
105
 
129
106
 
130
107
  @click.command("dev")
@@ -134,11 +111,15 @@ def dev_command(ctx, **kwargs):
134
111
 
135
112
 
136
113
  dev_command.params = list(run_command.params)
137
- cli.add_command(dev_command)
138
114
 
139
115
 
140
- def main():
141
- cli.main()
116
+ class ProcessManager(BaseProcessManager):
117
+ # This override prevents the manager to stop if the worker and scheduler exit because dramatiq is unused
118
+ def _any_stopped(self):
119
+ def has_exited_because_dramatiq_unavailable(name, p):
120
+ return name.split('.')[0] in ('scheduler', 'worker') and p.get('returncode') == 10
121
+ return any(not has_exited_because_dramatiq_unavailable(name, p) and p.get('returncode') is not None
122
+ for name, p in self._processes.items())
142
123
 
143
124
 
144
125
  class GunicornServer(gunicorn.app.base.BaseApplication):