hyperflask 0.3.1__tar.gz → 0.4.0__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.0}/PKG-INFO +6 -5
  2. {hyperflask-0.3.1 → hyperflask-0.4.0}/pyproject.toml +6 -7
  3. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/__init__.py +2 -0
  4. hyperflask-0.4.0/src/hyperflask/actors.py +82 -0
  5. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/app.py +39 -19
  6. hyperflask-0.4.0/src/hyperflask/cli/__init__.py +53 -0
  7. hyperflask-0.4.0/src/hyperflask/cli/reloader.py +24 -0
  8. hyperflask-0.3.1/src/hyperflask/cli.py → hyperflask-0.4.0/src/hyperflask/cli/runner.py +17 -49
  9. hyperflask-0.4.0/src/hyperflask/cli/worker.py +90 -0
  10. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/components/__init__.py +4 -3
  11. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/components/alpine.py +1 -1
  12. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/components/core.py +48 -30
  13. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/components/jsx.py +1 -1
  14. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/components/webcomponent.py +1 -1
  15. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/factory.py +1 -0
  16. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/layouts/web.html +1 -8
  17. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/model.py +7 -0
  18. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/static/htmx-exts.js +8 -5
  19. hyperflask-0.4.0/src/hyperflask/utils/metadata.py +182 -0
  20. hyperflask-0.3.1/src/hyperflask/ui/metadata.html +0 -4
  21. {hyperflask-0.3.1 → hyperflask-0.4.0}/README.md +0 -0
  22. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/__main__.py +0 -0
  23. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/forms/__init__.py +0 -0
  24. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/forms/fields.py +0 -0
  25. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/forms/widgets.py +0 -0
  26. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/layouts/email.mjml +0 -0
  27. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/page_helpers.py +0 -0
  28. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/security.py +0 -0
  29. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/static/alpine-csp.js +0 -0
  30. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/static/alpine.js +0 -0
  31. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/static/app.css +0 -0
  32. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/static/app.js +0 -0
  33. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/static/react.js +0 -0
  34. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/static/stimulus.js +0 -0
  35. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/static/translations.js +0 -0
  36. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/Accordion.html +0 -0
  37. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/AccordionPanel.html +0 -0
  38. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/Alert.html +0 -0
  39. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/Avatar.html +0 -0
  40. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/AvatarGroup.html +0 -0
  41. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/Badge.html +0 -0
  42. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/Breadcrumbs.html +0 -0
  43. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/BreadcrumbsItem.html +0 -0
  44. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/Button.html +0 -0
  45. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/Card.html +0 -0
  46. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/CardActions.html +0 -0
  47. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/CardTitle.html +0 -0
  48. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/ChatBubble.html +0 -0
  49. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/Checkbox.html +0 -0
  50. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/CollapsibleSubMenu.html +0 -0
  51. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/Countdown.html +0 -0
  52. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/Divider.html +0 -0
  53. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/Drawer.html +0 -0
  54. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/DrawerContent.html +0 -0
  55. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/DrawerSide.html +0 -0
  56. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/Dropdown.html +0 -0
  57. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/DropdownItem.html +0 -0
  58. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/Fieldset.html +0 -0
  59. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/FieldsetLegend.html +0 -0
  60. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/FileInput.html +0 -0
  61. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/Filter.html +0 -0
  62. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/FilterItem.html +0 -0
  63. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/Footer.html +0 -0
  64. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/FooterLink.html +0 -0
  65. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/FooterNav.html +0 -0
  66. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/Indicator.html +0 -0
  67. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/Input.html +0 -0
  68. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/InputLabel.html +0 -0
  69. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/Join.html +0 -0
  70. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/Kbd.html +0 -0
  71. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/Label.html +0 -0
  72. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/LabeledCheckbox.html +0 -0
  73. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/LabeledToggle.html +0 -0
  74. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/Link.html +0 -0
  75. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/List.html +0 -0
  76. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/ListItem.html +0 -0
  77. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/Loading.html +0 -0
  78. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/Menu.html +0 -0
  79. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/MenuItem.html +0 -0
  80. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/MenuTitle.html +0 -0
  81. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/Modal.html +0 -0
  82. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/ModalAction.html +0 -0
  83. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/ModalTitle.html +0 -0
  84. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/Navbar.html +0 -0
  85. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/Pagination.html +0 -0
  86. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/PaginationNext.html +0 -0
  87. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/PaginationPage.html +0 -0
  88. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/PaginationPrev.html +0 -0
  89. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/Progress.html +0 -0
  90. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/README.md +0 -0
  91. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/Radio.html +0 -0
  92. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/Range.html +0 -0
  93. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/Rating.html +0 -0
  94. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/Select.html +0 -0
  95. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/Stack.html +0 -0
  96. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/Stat.html +0 -0
  97. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/Stats.html +0 -0
  98. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/Step.html +0 -0
  99. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/Steps.html +0 -0
  100. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/SubMenu.html +0 -0
  101. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/Swap.html +0 -0
  102. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/Tab.html +0 -0
  103. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/Table.html +0 -0
  104. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/Tabs.html +0 -0
  105. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/Textarea.html +0 -0
  106. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/Toast.html +0 -0
  107. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/Toggle.html +0 -0
  108. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/daisyui/Tooltip.html +0 -0
  109. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/flash.html +0 -0
  110. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/forms.html +0 -0
  111. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/icons.html +0 -0
  112. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/layout.html +0 -0
  113. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/modal.html +0 -0
  114. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/skeleton.html +0 -0
  115. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/ui/sse.html +0 -0
  116. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/utils/__init__.py +0 -0
  117. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/utils/html.py +0 -0
  118. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/utils/htmx.py +0 -0
  119. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/utils/markdown.py +0 -0
  120. {hyperflask-0.3.1 → hyperflask-0.4.0}/src/hyperflask/utils/page_actions.py +0 -0
  121. {hyperflask-0.3.1 → hyperflask-0.4.0}/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.0
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
18
  Requires-Dist: flask-mercure-sse~=0.3
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "hyperflask"
3
- version = "0.3.1"
3
+ version = "0.4.0"
4
4
  description = "Full stack web framework"
5
5
  authors = [
6
6
  {"name" = "Maxime Bouroumeau-Fuseau", email = "maxime.bouroumeau@gmail.com"}
@@ -15,7 +15,6 @@ 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",
@@ -23,11 +22,12 @@ dependencies = [
23
22
  "flask-mercure-sse ~=0.3",
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.")
@@ -5,7 +5,7 @@ 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,10 +68,10 @@ 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
74
76
  "LAYOUT": "layouts/default.html",
75
77
  "ALPINE": False,
@@ -117,7 +119,7 @@ class Hyperflask(Flask):
117
119
  self.jinja_env.add_extension(WtformExtension)
118
120
  self.jinja_env.add_extension(MarkdownExtension)
119
121
  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)
122
+ self.jinja_env.globals.update(page_action_url=page_action_url, app=self, csp_nonce=csp_nonce, metadata_tags=metadata_tags)
121
123
  self.jinja_env.form_base_cls = Form
122
124
  self.jinja_env.default_layout = self.config["LAYOUT"]
123
125
  self.forms = self.jinja_env.forms
@@ -133,14 +135,11 @@ class Hyperflask(Flask):
133
135
  ])
134
136
 
135
137
  DebugToolbarExtension(self)
136
- self.freezer = Freezer(self)
138
+ self.freezer = Freezer(self, with_no_argument_rules=False)
139
+ self.freezer.register_generator(lambda: freezer_url_generator(self))
137
140
  self.csrf = CSRFProtect(self)
138
- #self.talisman = Talisman(self)
139
141
  Htmx(self)
140
142
  Mail(self)
141
- self.dramatiq = Dramatiq(self)
142
- self.dramatiq.broker.add_middleware(PeriodiqMiddleware())
143
- self.actor = self.dramatiq.actor
144
143
 
145
144
  SuperMacros(self)
146
145
  self.components = self.macros # alias
@@ -205,10 +204,20 @@ class Hyperflask(Flask):
205
204
 
206
205
  self.collections.register_freezer_generator(self.freezer)
207
206
 
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
207
+ if self.config.get('SQLORM_URI'):
208
+ SQLTemplate.eval_globals.update(app=self)
209
+ self.db = FlaskSQLORM(self, migrations_folder=migrations_folder)
210
+ self.db.Model = Model
211
+ self.db.File = SQLFileType
212
+ else:
213
+ self.db = UndefinedDatabase()
214
+
215
+ broker_cls, broker_url = discover_broker(self.config.get('DRAMATIQ_BROKER'), self.config.get('DRAMATIQ_BROKER_URL'))
216
+ if broker_cls:
217
+ self.dramatiq_broker = broker_cls(url=broker_url)
218
+ self.dramatiq_broker.add_middleware(AppContextMiddleware(self))
219
+ self.dramatiq_broker.add_middleware(PeriodiqMiddleware())
220
+ self.actor = make_actor_decorator(self)
212
221
 
213
222
  Suspense(self, nonce_getter="csp_nonce()")
214
223
 
@@ -216,9 +225,10 @@ class Hyperflask(Flask):
216
225
  def on_suspense_before_render_template(sender, **kwargs):
217
226
  csp_nonce() # we will need a nonce but the first call will be after the template started rendering
218
227
 
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>'
228
+ @suspense_before_render_macros.connect_via(self, weak=False)
229
+ def on_suspense_before_render_macros(sender, **kwargs):
230
+ return ""
231
+ return f'<script nonce="{csp_nonce()}">htmx.bootstrap()</script>' # waiting on PR 3441
222
232
 
223
233
  @self.errorhandler(404)
224
234
  def not_found(error):
@@ -239,6 +249,8 @@ class Hyperflask(Flask):
239
249
 
240
250
 
241
251
  class HyperModuleView(ModuleView):
252
+ module_globals = dict(ModuleView.module_globals, asset_url=asset_url, static_url=static_url)
253
+
242
254
  def _render_template(self):
243
255
  return render_template(page.template)
244
256
 
@@ -251,3 +263,11 @@ def dynamic(func=None, static_template=None):
251
263
  def decorator(func):
252
264
  pass
253
265
  return decorator(func) if func else decorator
266
+
267
+
268
+ def freezer_url_generator(app):
269
+ """URL generator for URL rules that take no arguments."""
270
+ for rule in app.url_map.iter_rules():
271
+ if not rule.arguments and 'GET' in rule.methods and not rule.endpoint.startswith('mercure'):
272
+ app.logger.info(rule.endpoint)
273
+ 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,12 @@
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 honcho.manager import Manager as BaseProcessManager
8
+ from ..factory import load_config
9
+ from . import reloader # import hacky patch of werkzeug reloader
46
10
 
47
11
 
48
12
  @click.command("serve")
@@ -69,7 +33,6 @@ def serve_command(ctx, info, host, port, gunicorn_opt, **run_kwargs):
69
33
 
70
34
 
71
35
  serve_command.params = flask_run_command.params + serve_command.params
72
- cli.add_command(serve_command)
73
36
 
74
37
 
75
38
  @click.command("run")
@@ -81,7 +44,6 @@ cli.add_command(serve_command)
81
44
  @click.option("--dev", is_flag=True)
82
45
  def run_command(processes, host, port, concurrency, extend_procfile, 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
 
@@ -99,24 +61,27 @@ def run_command(processes, host, port, concurrency, extend_procfile, dev):
99
61
  _processes = {}
100
62
  _processes.update({
101
63
  "web": [sys.argv[0], "serve", "--host", host, "--port", "$PORT"],
102
- # "worker": [sys.argv[0], "worker"],
103
- # "scheduler": [sys.argv[0], "periodiq"]
64
+ "worker": [sys.argv[0], "worker"],
65
+ "scheduler": [sys.argv[0], "scheduler"]
104
66
  })
105
67
  if dev:
106
68
  _processes["assets"] = [sys.argv[0], "assets", "dev"]
69
+ _processes["worker"].extend(["-p", "1", "-t", "1"])
107
70
  else:
108
71
  _processes["mercurehub"] = [sys.executable, "-m", "flask_mercure_sse.server"]
109
72
 
110
73
  processes = {name: " ".join(_processes[name]) if isinstance(_processes[name], list) else _processes[name]
111
74
  for name in _processes if name in _processes}
112
75
 
113
- manager = Manager(Printer(sys.stdout, colour=True))
76
+ manager = ProcessManager(Printer(sys.stdout, colour=True))
114
77
 
115
78
  for p in environ.expand_processes(processes,
116
79
  concurrency=_parse_concurrency(concurrency),
117
80
  port=port):
118
81
  e = os.environ.copy()
119
82
  e.update(p.env)
83
+ if p.name != "scheduler":
84
+ e["FLASK_SKIP_DB_INIT"] = "0"
120
85
  manager.add_process(p.name, p.cmd, quiet=p.quiet, env=e)
121
86
 
122
87
  manager.loop()
@@ -124,7 +89,6 @@ def run_command(processes, host, port, concurrency, extend_procfile, dev):
124
89
 
125
90
 
126
91
  run_command.params.insert(0, _debug_option)
127
- cli.add_command(run_command)
128
92
 
129
93
 
130
94
  @click.command("dev")
@@ -134,11 +98,15 @@ def dev_command(ctx, **kwargs):
134
98
 
135
99
 
136
100
  dev_command.params = list(run_command.params)
137
- cli.add_command(dev_command)
138
101
 
139
102
 
140
- def main():
141
- cli.main()
103
+ class ProcessManager(BaseProcessManager):
104
+ # This override prevents the manager to stop if the worker and scheduler exit because dramatiq is unused
105
+ def _any_stopped(self):
106
+ def has_exited_because_dramatiq_unavailable(name, p):
107
+ return name.split('.')[0] in ('scheduler', 'worker') and p.get('returncode') == 10
108
+ return any(not has_exited_because_dramatiq_unavailable(name, p) and p.get('returncode') is not None
109
+ for name, p in self._processes.items())
142
110
 
143
111
 
144
112
  class GunicornServer(gunicorn.app.base.BaseApplication):
@@ -0,0 +1,90 @@
1
+ import click
2
+ import sys
3
+ from flask import current_app
4
+ from flask.cli import with_appcontext
5
+ import periodiq
6
+ from dramatiq import set_broker
7
+ from dramatiq.cli import (
8
+ CPUS,
9
+ main as dramatiq_worker,
10
+ make_argument_parser as dramatiq_argument_parser,
11
+ )
12
+
13
+
14
+ @click.command("worker")
15
+ @click.option('-v', '--verbose', is_flag=True)
16
+ @click.option('-p', '--processes', default=CPUS,
17
+ metavar='PROCESSES', show_default=True,
18
+ help="the number of worker processes to run")
19
+ @click.option('-t', '--threads', default=8,
20
+ metavar='THREADS', show_default=True,
21
+ help="the number of worker threads per process")
22
+ @click.option('-Q', '--queues', type=str, default=None,
23
+ metavar='QUEUES', show_default=True,
24
+ help="listen to a subset of queues, comma separated")
25
+ @with_appcontext
26
+ def worker_command(verbose, processes, threads, queues):
27
+ """Run dramatiq workers.
28
+ """
29
+ check_dramatiq_availability()
30
+
31
+ parser = dramatiq_argument_parser()
32
+
33
+ command = [
34
+ "--processes", str(processes),
35
+ "--threads", str(threads),
36
+ # This module does not have broker local. Thus dramatiq fallbacks to
37
+ # global broker.
38
+ __name__,
39
+ ]
40
+ if verbose:
41
+ command += ["-v"]
42
+ if current_app.config['DEBUG']:
43
+ command += ["--watch", current_app.root_path]
44
+
45
+ queues = queues.split(",") if queues else []
46
+ if queues:
47
+ command += ["--queues"] + queues
48
+
49
+ args = parser.parse_args(command)
50
+ dramatiq_worker(args)
51
+
52
+
53
+ @click.command("scheduler")
54
+ @click.option('-v', '--verbose', is_flag=True)
55
+ @with_appcontext
56
+ def scheduler_command(verbose):
57
+ """Run periodiq scheduler.
58
+ """
59
+ check_dramatiq_availability()
60
+
61
+ periodic_actors = [
62
+ a for a in current_app.dramatiq_broker.actors.values()
63
+ if 'periodic' in a.options
64
+ ]
65
+ if not periodic_actors:
66
+ click.echo("No scheduled Dramatiq actors found.")
67
+ sys.exit(10)
68
+
69
+ command = [
70
+ # This module does not have broker local. Thus dramatiq fallbacks to
71
+ # global broker.
72
+ __name__,
73
+ ]
74
+
75
+ if verbose:
76
+ command += ["-v"]
77
+
78
+ parser = periodiq.make_argument_parser()
79
+ args = parser.parse_args(command)
80
+ periodiq.main(args)
81
+
82
+
83
+ def check_dramatiq_availability():
84
+ if not getattr(current_app, 'dramatiq_broker', None):
85
+ click.echo("Dramatiq broker is not configured.")
86
+ sys.exit(10)
87
+ if not current_app.dramatiq_broker.actors:
88
+ click.echo("No Dramatiq actors found.")
89
+ sys.exit(10)
90
+ set_broker(current_app.dramatiq_broker)
@@ -25,9 +25,10 @@ def discover_components(app, path=None, package_name=None):
25
25
 
26
26
  loader = jinjapy.register_package(package_name, path, env=app.jinja_env)
27
27
  for module_name, template in loader.list_files(module_with_package=False):
28
+ filename = template or (loader.prefix + module_name.replace(".", os.sep) + ".py")
28
29
  adapter_class = None
29
30
  for pattern, adapter_import_path in force_adapters.items():
30
- if re.match(pattern, template):
31
+ if re.match(pattern, filename):
31
32
  adapter_module, adapter_classname = adapter_import_path.split(":")
32
33
  adapter_class = getattr(importlib.import_module(adapter_module), adapter_classname)
33
34
  break
@@ -40,8 +41,8 @@ def discover_components(app, path=None, package_name=None):
40
41
  if not matches:
41
42
  continue
42
43
  if adapter_class:
43
- name = os.path.basename(template).split(".")[0].replace("-", "_")
44
- adapter = adapter_class(name, f"{package_name}.{module_name}" if module_name else None, template)
44
+ name = (os.path.basename(template).split(".")[0] if template else module_name.split('.')[-1]).replace("-", "_")
45
+ adapter = adapter_class(name, f"{package_name}.{module_name}" if module_name else None, template, filename)
45
46
  components.append(adapter)
46
47
 
47
48
  return loader, components
@@ -4,7 +4,7 @@ from .core import ComponentAdapter
4
4
  class AlpineAdapter(ComponentAdapter):
5
5
  @classmethod
6
6
  def matches(cls, app, module_name, template):
7
- if not ComponentAdapter.matches(app, module_name, template):
7
+ if not template or not ComponentAdapter.matches(app, module_name, template):
8
8
  return False
9
9
  tpl = app.jinja_env.loader.get_source(app.jinja_env, template)[0]
10
10
  return any(tag in tpl for tag in ["x-data", "x-init"])