hyperflask 0.2.4__tar.gz → 0.3.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 (119) hide show
  1. {hyperflask-0.2.4 → hyperflask-0.3.0}/PKG-INFO +9 -11
  2. {hyperflask-0.2.4 → hyperflask-0.3.0}/README.md +3 -4
  3. {hyperflask-0.2.4 → hyperflask-0.3.0}/pyproject.toml +7 -8
  4. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/__init__.py +2 -1
  5. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/app.py +64 -14
  6. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/cli.py +17 -4
  7. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/components/core.py +21 -5
  8. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/factory.py +1 -1
  9. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/forms/widgets.py +1 -0
  10. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/layouts/web.html +7 -2
  11. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/model.py +6 -1
  12. hyperflask-0.3.0/src/hyperflask/security.py +81 -0
  13. hyperflask-0.3.0/src/hyperflask/static/alpine-csp.js +5 -0
  14. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/static/alpine.js +1 -1
  15. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/static/app.js +1 -1
  16. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/static/htmx-exts.js +32 -14
  17. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/forms.html +1 -1
  18. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/layout.html +1 -1
  19. hyperflask-0.3.0/src/hyperflask/ui/metadata.html +4 -0
  20. hyperflask-0.3.0/src/hyperflask/ui/skeleton.html +33 -0
  21. hyperflask-0.3.0/src/hyperflask/ui/sse.html +22 -0
  22. hyperflask-0.2.4/src/hyperflask/ui/sse.html +0 -14
  23. hyperflask-0.2.4/src/hyperflask/utils/models.py +0 -5
  24. hyperflask-0.2.4/src/hyperflask/utils/request.py +0 -25
  25. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/__main__.py +0 -0
  26. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/components/__init__.py +0 -0
  27. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/components/alpine.py +0 -0
  28. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/components/jsx.py +0 -0
  29. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/components/webcomponent.py +0 -0
  30. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/forms/__init__.py +0 -0
  31. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/forms/fields.py +0 -0
  32. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/layouts/email.mjml +0 -0
  33. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/page_helpers.py +0 -0
  34. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/static/app.css +0 -0
  35. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/static/react.js +0 -0
  36. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/static/stimulus.js +0 -0
  37. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/static/translations.js +0 -0
  38. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Accordion.html +0 -0
  39. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/AccordionPanel.html +0 -0
  40. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Alert.html +0 -0
  41. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Avatar.html +0 -0
  42. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/AvatarGroup.html +0 -0
  43. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Badge.html +0 -0
  44. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Breadcrumbs.html +0 -0
  45. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/BreadcrumbsItem.html +0 -0
  46. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Button.html +0 -0
  47. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Card.html +0 -0
  48. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/CardActions.html +0 -0
  49. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/CardTitle.html +0 -0
  50. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/ChatBubble.html +0 -0
  51. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Checkbox.html +0 -0
  52. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/CollapsibleSubMenu.html +0 -0
  53. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Countdown.html +0 -0
  54. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Divider.html +0 -0
  55. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Drawer.html +0 -0
  56. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/DrawerContent.html +0 -0
  57. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/DrawerSide.html +0 -0
  58. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Dropdown.html +0 -0
  59. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/DropdownItem.html +0 -0
  60. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Fieldset.html +0 -0
  61. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/FieldsetLegend.html +0 -0
  62. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/FileInput.html +0 -0
  63. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Filter.html +0 -0
  64. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/FilterItem.html +0 -0
  65. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Footer.html +0 -0
  66. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/FooterLink.html +0 -0
  67. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/FooterNav.html +0 -0
  68. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Indicator.html +0 -0
  69. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Input.html +0 -0
  70. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/InputLabel.html +0 -0
  71. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Join.html +0 -0
  72. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Kbd.html +0 -0
  73. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Label.html +0 -0
  74. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/LabeledCheckbox.html +0 -0
  75. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/LabeledToggle.html +0 -0
  76. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Link.html +0 -0
  77. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/List.html +0 -0
  78. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/ListItem.html +0 -0
  79. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Loading.html +0 -0
  80. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Menu.html +0 -0
  81. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/MenuItem.html +0 -0
  82. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/MenuTitle.html +0 -0
  83. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Modal.html +0 -0
  84. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/ModalAction.html +0 -0
  85. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/ModalTitle.html +0 -0
  86. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Navbar.html +0 -0
  87. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Pagination.html +0 -0
  88. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/PaginationNext.html +0 -0
  89. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/PaginationPage.html +0 -0
  90. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/PaginationPrev.html +0 -0
  91. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Progress.html +0 -0
  92. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/README.md +0 -0
  93. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Radio.html +0 -0
  94. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Range.html +0 -0
  95. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Rating.html +0 -0
  96. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Select.html +0 -0
  97. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Stack.html +0 -0
  98. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Stat.html +0 -0
  99. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Stats.html +0 -0
  100. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Step.html +0 -0
  101. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Steps.html +0 -0
  102. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/SubMenu.html +0 -0
  103. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Swap.html +0 -0
  104. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Tab.html +0 -0
  105. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Table.html +0 -0
  106. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Tabs.html +0 -0
  107. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Textarea.html +0 -0
  108. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Toast.html +0 -0
  109. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Toggle.html +0 -0
  110. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Tooltip.html +0 -0
  111. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/flash.html +0 -0
  112. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/icons.html +0 -0
  113. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/ui/modal.html +0 -0
  114. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/utils/__init__.py +0 -0
  115. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/utils/html.py +0 -0
  116. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/utils/htmx.py +0 -0
  117. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/utils/markdown.py +0 -0
  118. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/utils/page_actions.py +0 -0
  119. {hyperflask-0.2.4 → hyperflask-0.3.0}/src/hyperflask/utils/tokens.py +0 -0
@@ -1,32 +1,31 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperflask
3
- Version: 0.2.4
3
+ Version: 0.3.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>
7
7
  License-Expression: MIT
8
8
  Requires-Dist: flask>=3.0.0,<4.0.0
9
- Requires-Dist: flask-assets-pipeline~=0.6
9
+ Requires-Dist: flask-assets-pipeline~=0.7
10
10
  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
14
  Requires-Dist: flask-dramatiq~=0.6
15
- Requires-Dist: flask-file-routes~=0.2
15
+ Requires-Dist: flask-file-routes~=0.3
16
16
  Requires-Dist: flask-files~=0.2
17
17
  Requires-Dist: flask-geo~=0.1
18
18
  Requires-Dist: flask-mailman-templates~=0.1
19
- Requires-Dist: flask-mercure-sse~=0.2
20
- Requires-Dist: flask-sqlorm~=0.2
19
+ Requires-Dist: flask-mercure-sse~=0.3
20
+ Requires-Dist: flask-sqlorm~=0.3
21
21
  Requires-Dist: flask-super-macros~=0.1
22
- Requires-Dist: flask-talisman~=1.1
22
+ Requires-Dist: flask-suspense~=0.1
23
23
  Requires-Dist: flask-wtf~=1.2
24
24
  Requires-Dist: frozen-flask~=1.0
25
25
  Requires-Dist: gunicorn~=23.0
26
26
  Requires-Dist: honcho~=2.0
27
27
  Requires-Dist: html-sanitizer~=2.6
28
28
  Requires-Dist: htmx-flask~=0.1
29
- Requires-Dist: jinja2-fragments~=1.9
30
29
  Requires-Dist: jinja-layout~=0.4
31
30
  Requires-Dist: jinja-wtforms~=0.2
32
31
  Requires-Dist: periodiq~=0.13
@@ -108,7 +107,7 @@ A good part of these extensions is developed as part of the Hyperflask project.
108
107
  | [Flask-Mercure-SSE](https://github.com/hyperflask/flask-mercure-sse) | Push events via server-sent events using the [Mercure](https://mercure.rocks) protocol
109
108
  | [Flask-SQLORM](https://github.com/hyperflask/flask-sqlorm) | Flask integration of [sqlorm](https://github.com/hyperflask/sqlorm)
110
109
  | [Flask-Super-Macros](https://github.com/hyperflask/flask-super-macros) | Better macro management for Jinja
111
- | [Flask-Talisman](https://github.com/wntrblm/flask-talisman) | HTTP security headers for Flask
110
+ | [Flask-Suspense](https://github.com/hyperflask/flask-suspense) | Suspense for Flask
112
111
  | [Flask-WTF](https://flask-wtf.readthedocs.io) | [WTForms](https://wtforms.readthedocs.io) integration
113
112
 
114
113
  ## Using without Hyperflask-Start
@@ -116,6 +115,5 @@ A good part of these extensions is developed as part of the Hyperflask project.
116
115
  1. Create your project directory: `mkdir example-project && cd example-project`
117
116
  2. Create and activate a virtualenv: `python -m venv .venv && source .venv/bin/activate`
118
117
  3. `pip install hyperflask`
119
- 4. Create a pages directory: `mkdir pages`
120
- 5. Create your index page: `echo "hello world" > pages/index.html`
121
- 6. Start a development server using `hyperflask dev`
118
+ 4. Run `hyperflask init .`
119
+ 5. Start a development server using `hyperflask dev`
@@ -70,7 +70,7 @@ A good part of these extensions is developed as part of the Hyperflask project.
70
70
  | [Flask-Mercure-SSE](https://github.com/hyperflask/flask-mercure-sse) | Push events via server-sent events using the [Mercure](https://mercure.rocks) protocol
71
71
  | [Flask-SQLORM](https://github.com/hyperflask/flask-sqlorm) | Flask integration of [sqlorm](https://github.com/hyperflask/sqlorm)
72
72
  | [Flask-Super-Macros](https://github.com/hyperflask/flask-super-macros) | Better macro management for Jinja
73
- | [Flask-Talisman](https://github.com/wntrblm/flask-talisman) | HTTP security headers for Flask
73
+ | [Flask-Suspense](https://github.com/hyperflask/flask-suspense) | Suspense for Flask
74
74
  | [Flask-WTF](https://flask-wtf.readthedocs.io) | [WTForms](https://wtforms.readthedocs.io) integration
75
75
 
76
76
  ## Using without Hyperflask-Start
@@ -78,6 +78,5 @@ A good part of these extensions is developed as part of the Hyperflask project.
78
78
  1. Create your project directory: `mkdir example-project && cd example-project`
79
79
  2. Create and activate a virtualenv: `python -m venv .venv && source .venv/bin/activate`
80
80
  3. `pip install hyperflask`
81
- 4. Create a pages directory: `mkdir pages`
82
- 5. Create your index page: `echo "hello world" > pages/index.html`
83
- 6. Start a development server using `hyperflask dev`
81
+ 4. Run `hyperflask init .`
82
+ 5. Start a development server using `hyperflask dev`
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "hyperflask"
3
- version = "0.2.4"
3
+ version = "0.3.0"
4
4
  description = "Full stack web framework"
5
5
  authors = [
6
6
  {"name" = "Maxime Bouroumeau-Fuseau", email = "maxime.bouroumeau@gmail.com"}
@@ -10,21 +10,21 @@ license = "MIT"
10
10
  requires-python = ">=3.10"
11
11
  dependencies = [
12
12
  "flask>=3.0.0,<4.0.0",
13
- "flask-assets-pipeline ~=0.6",
13
+ "flask-assets-pipeline ~=0.7",
14
14
  "flask-babel-hyper ~=5.1",
15
15
  "flask-collections ~=0.2",
16
16
  "flask-configurator ~=0.1",
17
17
  "flask-debugtoolbar ~=0.16",
18
18
  "flask-dramatiq ~=0.6",
19
- "flask-file-routes ~=0.2",
19
+ "flask-file-routes ~=0.3",
20
20
  "flask-files ~=0.2",
21
21
  "flask-geo ~=0.1",
22
22
  "flask-mailman-templates ~=0.1",
23
- "flask-mercure-sse ~=0.2",
23
+ "flask-mercure-sse ~=0.3",
24
24
  #"flask-observability ~=0.1"
25
- "flask-sqlorm ~=0.2",
25
+ "flask-sqlorm ~=0.3",
26
26
  "flask-super-macros ~=0.1",
27
- "flask-talisman ~=1.1",
27
+ "flask-suspense ~=0.1",
28
28
  "flask-wtf ~=1.2",
29
29
  "frozen-flask ~=1.0",
30
30
  #"dramatiq-sqlite ~=0.1"
@@ -32,12 +32,11 @@ dependencies = [
32
32
  "honcho ~=2.0",
33
33
  "html-sanitizer ~=2.6",
34
34
  "htmx-flask ~=0.1",
35
- "jinja2-fragments ~=1.9",
36
35
  "jinja-layout ~=0.4",
37
36
  "jinja-wtforms ~=0.2",
38
37
  "periodiq ~=0.13",
39
38
  "python-slugify ~=8.0.4",
40
- "redis ~=6.4"
39
+ "redis ~=6.4",
41
40
  ]
42
41
 
43
42
  [tool.uv.sources]
@@ -1,6 +1,7 @@
1
1
  from flask import *
2
2
  from .app import Hyperflask
3
3
  from .utils.htmx import htmx_redirect, htmx_oob
4
+ from .security import csp_nonce
4
5
 
5
6
  # useful exports from hyperflask managed extensions
6
7
  from flask_babel import gettext, _, pgettext, _p, ngettext, _n, npgettext, _np, lazy_gettext, _lazy
@@ -8,9 +9,9 @@ from flask_file_routes import page
8
9
  from flask_files import save_file, validate_file
9
10
  from flask_mailman_templates import send_mail
10
11
  from flask_mercure_sse import publish_signal, mercure_publish
12
+ from flask_suspense import render_template, stream_template, defer
11
13
 
12
14
  # useful exports from 3rd party libs
13
15
  from htmx_flask import request, make_response
14
- from jinja2_fragments.flask import render_block
15
16
  from slugify import slugify
16
17
  from blinker import signal
@@ -1,6 +1,6 @@
1
1
  # flask
2
2
  from flask import Flask
3
- from jinja2 import ChoiceLoader, FileSystemLoader, PrefixLoader
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
@@ -8,7 +8,7 @@ from jinja_wtforms import WtformExtension
8
8
  from flask_assets_pipeline import AssetsPipeline
9
9
  from flask_super_macros import SuperMacros
10
10
  from flask_collections import Collections
11
- from flask_file_routes import FileRoutes
11
+ from flask_file_routes import FileRoutes, ModuleView, page
12
12
  from flask_configurator import Config
13
13
  from flask_babel import Babel, _, _p, _n, _np, lazy_gettext
14
14
  from flask_geo import Geolocation
@@ -16,10 +16,10 @@ 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
20
  #from flask_observability import Observability
20
21
  # 3rd party extensions
21
22
  from flask_wtf.csrf import CSRFProtect
22
- from flask_talisman import Talisman
23
23
  from flask_frozen import Freezer
24
24
  from htmx_flask import Htmx
25
25
  from flask_mailman import Mail
@@ -31,10 +31,9 @@ from .utils.page_actions import page_action_url
31
31
  from .utils.htmx import htmx_oob, respond_with_flash_messages
32
32
  from .utils.markdown import jinja_markdown, MarkdownExtension
33
33
  from .utils.html import sanitize_html, nl2br
34
- from .utils.models import File as SQLFileType
35
- from .components import discover_components, register_components
36
- from .components.jsx import ReactAdapter
37
- from .model import Model
34
+ from .components import register_components
35
+ from .model import Model, File as SQLFileType
36
+ from .security import respond_with_security_headers, csp_nonce
38
37
  from . import page_helpers
39
38
  # others
40
39
  from jinja_super_macros.registry import FileLoader
@@ -54,7 +53,7 @@ map_jinja_call_node_to_func(lazy_gettext, "lazy_gettext")
54
53
  class Hyperflask(Flask):
55
54
  config_class = Config
56
55
 
57
- def __init__(self, *args, static_mode="hybrid", instrument=False, layouts_folder="layouts", partials_folder="partials",
56
+ def __init__(self, *args, static_mode="hybrid", instrument=False, layouts_folder="layouts",
58
57
  emails_folder="emails", forms_folder="forms", assets_folder="assets", pages_folder="pages", migrations_folder="migrations",
59
58
  config=None, config_filename="config.yml", database_uri="sqlite://:memory:", proxy_fix=True, **kwargs):
60
59
 
@@ -73,14 +72,24 @@ class Hyperflask(Flask):
73
72
  "SESSION_COOKIE_SAMESITE": "Lax",
74
73
  # Hyperflask specific
75
74
  "LAYOUT": "layouts/default.html",
75
+ "ALPINE": False,
76
76
  "STATIC_MODE": static_mode,
77
- "ASSETS_INCLUDE_HTMX": True,
78
- "HTMX_EXT": ["hf-modal"],
77
+ "ASSETS_INCLUDE_ALPINE": False,
78
+ "HTMX_EXT": [],
79
79
  "HTMX_BOOST_SITE": False,
80
80
  "MARKDOWN_OPTIONS": {"extensions": ["fenced_code", "nl2br", "attr_list", "admonition", "codehilite"]},
81
81
  "MARKDOWN_SANITIZER_CONFIG": {},
82
82
  "FLASH_TOAST_OOB": True,
83
- "FLASH_TOAST_REMOVE_AFTER": None
83
+ "FLASH_TOAST_REMOVE_AFTER": None,
84
+ "SERVER_SECURED": False,
85
+ "CSP_HEADER": True,
86
+ "CSP_SAFE_SRC": ["'self'"],
87
+ "CSP_UNSAFE_EVAL": True,
88
+ "CSP_UNSAFE_INLINE": True,
89
+ "CSP_FRAME_ANCESTORS": True,
90
+ "CSP_FRAME_ANCESTORS_SAFE_ENDPOINTS": [],
91
+ "REFERRER_POLICY": "strict-origin-when-cross-origin",
92
+ "HSTS_HEADER": False
84
93
  })
85
94
  if config:
86
95
  self.config.update(config)
@@ -93,13 +102,22 @@ class Hyperflask(Flask):
93
102
  self.config["COLLECTIONS_MARKDOWN_OPTIONS"] = self.config["MARKDOWN_OPTIONS"]
94
103
  self.config["MAIL_TEMPLATES_MARKDOWN_OPTIONS"] = self.config["MARKDOWN_OPTIONS"]
95
104
 
105
+ if self.config['SERVER_SECURED']:
106
+ self.config.update({
107
+ 'FORCE_URL_SCHEME': 'https',
108
+ 'PREFERRED_URL_SCHEME': 'https',
109
+ 'SESSION_COOKIE_SECURE': True,
110
+ 'REMEMBER_COOKIE_SECURE': True
111
+ })
112
+ self.config.setdefault("HSTS_HEADER", "max-age=31556926; includeSubDomains") # 1 year
113
+
96
114
  #self.otel = Observability(self)
97
115
 
98
116
  self.jinja_env.add_extension(LayoutExtension)
99
117
  self.jinja_env.add_extension(WtformExtension)
100
118
  self.jinja_env.add_extension(MarkdownExtension)
101
119
  self.jinja_env.filters.update(markdown=jinja_markdown, sanitize=sanitize_html, nl2br=nl2br)
102
- self.jinja_env.globals.update(page_action_url=page_action_url, app=self)
120
+ self.jinja_env.globals.update(page_action_url=page_action_url, app=self, csp_nonce=csp_nonce)
103
121
  self.jinja_env.form_base_cls = Form
104
122
  self.jinja_env.default_layout = self.config["LAYOUT"]
105
123
  self.forms = self.jinja_env.forms
@@ -130,7 +148,7 @@ class Hyperflask(Flask):
130
148
  Geolocation(self)
131
149
  Files(self)
132
150
  MailTemplates(self, template_folder=emails_folder)
133
- file_routes = FileRoutes(self, pages_folder=pages_folder, partials_folder=partials_folder)
151
+ file_routes = FileRoutes(self, pages_folder=pages_folder, module_view_class=HyperModuleView)
134
152
  self.page_helper = file_routes.page_helper
135
153
  self.collections = Collections(self)
136
154
  self.sse = MercureSSE(self)
@@ -153,11 +171,13 @@ class Hyperflask(Flask):
153
171
  })
154
172
  self.assets.bundle(
155
173
  {"@hyperflask": ["app.js"],
156
- "@hyperflask/alpine": ["alpine.js"]},
174
+ "@hyperflask/alpine": ["alpine-csp.js" if not self.config["CSP_UNSAFE_EVAL"] else "alpine.js"]},
157
175
  from_package="hyperflask",
158
176
  assets_folder="static"
159
177
  )
160
178
  self.assets.include("@hyperflask", 0)
179
+ if self.config["ALPINE"]:
180
+ self.assets.include("@hyperflask/alpine", 0)
161
181
 
162
182
  self.extensions['mail_templates'].jinja_env.add_extension(LayoutExtension)
163
183
  self.extensions['mail_templates'].jinja_env.default_layout = "layout.mjml"
@@ -179,6 +199,7 @@ class Hyperflask(Flask):
179
199
  for macro in self.macros.create_from_directory(os.path.join(self.root_path, forms_folder)):
180
200
  self.forms.register(self.macros.resolve_template(macro), macro)
181
201
 
202
+ self.after_request(respond_with_security_headers)
182
203
  if self.config["FLASH_TOAST_OOB"]:
183
204
  self.after_request(respond_with_flash_messages)
184
205
 
@@ -189,10 +210,39 @@ class Hyperflask(Flask):
189
210
  self.db.Model = Model
190
211
  self.db.File = SQLFileType
191
212
 
213
+ Suspense(self, nonce_getter="csp_nonce()")
214
+
215
+ @suspense_before_render_template.connect_via(self, weak=False)
216
+ def on_suspense_before_render_template(sender, **kwargs):
217
+ csp_nonce() # we will need a nonce but the first call will be after the template started rendering
218
+
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>'
222
+
223
+ @self.errorhandler(404)
224
+ def not_found(error):
225
+ try:
226
+ return render_template("404.html"), 404
227
+ except TemplateNotFound:
228
+ return self.make_response(("404 Not Found", 404))
229
+
230
+ @self.errorhandler(500)
231
+ def internal_server_error(error):
232
+ try:
233
+ return render_template("500.html"), 500
234
+ except TemplateNotFound:
235
+ return self.make_response(("500 Internal Server Error", 500))
236
+
192
237
  def relative_import_name(self, name):
193
238
  return f"{self.import_name}.{name}" if self.import_name != "__main__" else name
194
239
 
195
240
 
241
+ class HyperModuleView(ModuleView):
242
+ def _render_template(self):
243
+ return render_template(page.template)
244
+
245
+
196
246
  def static(func):
197
247
  pass
198
248
 
@@ -33,6 +33,7 @@ cli.add_command(freeze_command)
33
33
  @click.pass_context
34
34
  def build_command(ctx):
35
35
  ctx.invoke(assets_cli.build)
36
+ ctx.invoke(freeze_command)
36
37
 
37
38
  cli.add_command(build_command)
38
39
 
@@ -69,8 +70,9 @@ cli.add_command(serve_command)
69
70
  @click.option("--host", "-h", default="127.0.0.1", help="The interface to bind to.")
70
71
  @click.option("--port", "-p", default=5000, help="The port to bind to.")
71
72
  @click.option('--concurrency')
73
+ @click.option("--extend-procfile")
72
74
  @click.option("--dev", is_flag=True)
73
- def run_command(processes, host, port, concurrency, dev):
75
+ def run_command(processes, host, port, concurrency, extend_procfile, dev):
74
76
  from honcho.command import _procfile, _parse_concurrency
75
77
  from honcho.manager import Manager
76
78
  from honcho.printer import Printer
@@ -85,14 +87,18 @@ def run_command(processes, host, port, concurrency, dev):
85
87
  if os.path.exists(filename):
86
88
  _processes = _procfile(filename).processes
87
89
  break
88
- if _processes is None:
89
- _processes = {
90
+ if _processes is None or extend_procfile:
91
+ if not _processes:
92
+ _processes = {}
93
+ _processes.update({
90
94
  "web": [sys.argv[0], "serve", "--host", host, "--port", "$PORT"],
91
95
  # "worker": [sys.argv[0], "worker"],
92
96
  # "scheduler": [sys.argv[0], "periodiq"]
93
- }
97
+ })
94
98
  if dev:
95
99
  _processes["assets"] = [sys.argv[0], "assets", "dev"]
100
+ else:
101
+ _processes["mercurehub"] = [sys.executable, "-m", "flask_mercure_sse.server"]
96
102
 
97
103
  processes = {name: " ".join(_processes[name]) if isinstance(_processes[name], list) else _processes[name]
98
104
  for name in _processes if name in _processes}
@@ -124,6 +130,13 @@ dev_command.params = list(run_command.params)
124
130
  cli.add_command(dev_command)
125
131
 
126
132
 
133
+ @click.command("init")
134
+ def init_command():
135
+ pass
136
+
137
+ cli.add_command(init_command)
138
+
139
+
127
140
  def main():
128
141
  cli.main()
129
142
 
@@ -2,6 +2,7 @@ from markupsafe import Markup
2
2
  from flask import request, current_app
3
3
  from flask.views import http_method_funcs
4
4
  from flask_file_routes import page
5
+ from flask_suspense import render_template
5
6
  from jinjapy import extract_frontmatter
6
7
  import importlib
7
8
  import re
@@ -55,19 +56,34 @@ class ComponentAdapter(BaseComponentAdapter):
55
56
  return props
56
57
  if not props:
57
58
  props = {}
58
- return self.render(None, **props)
59
+ ctx = self.get_render_context(None, **props)
60
+ return render_template(self.template, **ctx)
59
61
 
60
62
  if methods:
61
63
  app.add_url_rule(url, self.name, view_func=view_func, methods=methods)
62
64
 
63
- def render(self, caller, *args, **kwargs):
64
- tpl = current_app.jinja_env.get_template(self.template)
65
+ def get_render_context(self, caller, *args, **kwargs):
65
66
  ctx = kwargs.pop('_ctx', {})
66
- ctx.update(dict(args=args, kwargs=kwargs, props=kwargs, caller=caller, children=caller))
67
+ ctx.update(dict(args=args, kwargs=kwargs, props=PropsWrapper(kwargs), caller=caller, children=caller))
67
68
  if self.module_name:
68
69
  module = importlib.import_module(self.module_name)
69
70
  if module and "render" in dir(module):
70
71
  _ctx = module.render(*args, **kwargs)
71
72
  if _ctx:
72
73
  ctx.update(_ctx)
73
- return Markup(tpl.render(**ctx))
74
+ return ctx
75
+
76
+ def render(self, caller, *args, **kwargs):
77
+ tpl = current_app.jinja_env.get_template(self.template)
78
+ ctx = self.get_render_context(caller, *args, **kwargs)
79
+ return Markup(tpl.render(**ctx)) # do not use render_template() so we don't trigger flask signals
80
+
81
+
82
+ class PropsWrapper:
83
+ def __init__(self, props):
84
+ self._props = props
85
+
86
+ def __getattr__(self, name):
87
+ if name in self._props:
88
+ return self._props[name]
89
+ raise Exception(f"No such prop: {name}")
@@ -68,7 +68,7 @@ def create_app(root_path=None):
68
68
  try_import(app, "signals")
69
69
  try_import(app, "app")
70
70
 
71
- if models:
71
+ if models and app.config.get("INIT_DATABASE", True):
72
72
  db.init_db()
73
73
 
74
74
  return app
@@ -5,6 +5,7 @@ def create_widget_class(basecls, cssclass="input"):
5
5
  class Input(basecls):
6
6
  def __call__(self, field, **kwargs):
7
7
  extend_class(kwargs, cssclass)
8
+ kwargs = {k.replace('_', '-'): v for k, v in kwargs.items() if v is not None}
8
9
  return super().__call__(field, **kwargs)
9
10
  return Input
10
11
 
@@ -6,8 +6,13 @@
6
6
  <meta charset="utf-8">
7
7
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
8
8
  {% endblock %}
9
- {% block page_title %}<title>{% if page_title is defined %}{{ page_title }}{% if config.get("SITE_TITLE") %} | {% endif %}{% endif %}{% if config.get("SITE_TITLE") %}{{config["SITE_TITLE"]}}{% endif %}</title>{% endblock %}
10
- {% asset_tags %}
9
+ {% block page_title %}
10
+ <title>{% if site_title is defined %}{{site_title}}{% else %}{% if page_title is defined %}{{ page_title }}{% if config.get("SITE_TITLE") %} | {% endif %}{% endif %}{% if config.get("SITE_TITLE") %}{{config["SITE_TITLE"]}}{% endif %}{% endif %}</title>
11
+ {% endblock %}
12
+ {% if metadata is defined %}
13
+ <{PageMetadata metadata=metadata}/>
14
+ {% endif %}
15
+ {% asset_tags nonce=csp_nonce %}
11
16
  {% block head_assets %}{% endblock %}
12
17
  {%- endblock %}
13
18
  </head>
@@ -1,10 +1,15 @@
1
- from flask_sqlorm import Model as BaseModel
2
1
  from flask import current_app
2
+ from flask_sqlorm import Model as BaseModel
3
+ from flask_files import save_file, File
3
4
  from sqlorm.resultset import CompositeResultSet
5
+ from sqlorm import SQLType
4
6
  from markupsafe import Markup
5
7
  import abc
6
8
 
7
9
 
10
+ File = SQLType("text", save_file, File.from_uri)
11
+
12
+
8
13
  class ModelResultSet(CompositeResultSet):
9
14
  def __str__(self):
10
15
  return Markup("".join([str(item) for item in self]))
@@ -0,0 +1,81 @@
1
+ from flask import request, current_app, g
2
+ from urllib.parse import urlparse, urljoin
3
+ import secrets
4
+
5
+
6
+ def is_safe_redirect_url(target):
7
+ host_url = urlparse(request.host_url)
8
+ redirect_url = urlparse(urljoin(request.host_url, target))
9
+ return (
10
+ redirect_url.scheme in ("http", "https")
11
+ and host_url.netloc == redirect_url.netloc
12
+ )
13
+
14
+
15
+ def respond_with_security_headers(response):
16
+ if current_app.config["CSP_HEADER"] is not False:
17
+ response.headers['X-XSS-Protection'] = '1; mode=block'
18
+ response.headers['X-Content-Type-Options'] = 'nosniff'
19
+ response.headers['Content-Security-Policy'] = generate_csp_policy()
20
+
21
+ if current_app.config['HSTS_HEADER']:
22
+ response.headers['Strict-Transport-Security'] = current_app.config['HSTS_HEADER']
23
+
24
+ if current_app.config['REFERRER_POLICY']:
25
+ response.headers['Referrer-Policy'] = current_app.config['REFERRER_POLICY']
26
+
27
+ return response
28
+
29
+
30
+ def generate_csp_policy():
31
+ policy = current_app.config["CSP_HEADER"]
32
+ if policy is True:
33
+ policy = {
34
+ "default-src": "'safe-src'",
35
+ "manifest-src": "'self'",
36
+ "connect-src": 'https: wss:' if current_app.config['SERVER_SECURED'] else 'http: ws:',
37
+ "img-src": "* data: blob:",
38
+ "media-src": "* data: blob:",
39
+ "frame-src": "* data: blob:",
40
+ "object-src": "* data: blob:"
41
+ }
42
+ policy.setdefault("script-src", policy["default-src"])
43
+ policy.setdefault("style-src", policy["default-src"])
44
+
45
+ if not current_app.config['CSP_FRAME_ANCESTORS']:
46
+ policy["frame-ancestors"] = "'none'"
47
+ elif isinstance(current_app.config['CSP_FRAME_ANCESTORS'], (list, tuple)):
48
+ frame_ancestors = list(current_app.config['CSP_FRAME_ANCESTORS'])
49
+ add_frame_ancestors = True
50
+ for endpoint in current_app.config['CSP_FRAME_ANCESTORS_SAFE_ENDPOINTS']:
51
+ ancestors = []
52
+ if isinstance(endpoint, (list, tuple)):
53
+ endpoint, ancestors = endpoint
54
+ if request.endpoint == endpoint:
55
+ if ancestors:
56
+ frame_ancestors.extend(ancestors)
57
+ else:
58
+ add_frame_ancestors = False
59
+ break
60
+ if add_frame_ancestors:
61
+ policy["frame-ancestors"] = ' '.join(set(frame_ancestors))
62
+
63
+ if current_app.config.get('CSP_UNSAFE_EVAL'):
64
+ policy["script-src"] += " 'unsafe-eval'"
65
+
66
+ unsafe_inline = current_app.debug or current_app.config.get('CSP_UNSAFE_INLINE')
67
+ if unsafe_inline:
68
+ policy["script-src"] += " 'unsafe-inline'"
69
+ policy["style-src"] += " 'unsafe-inline'"
70
+ elif 'csp_nonce' in g:
71
+ policy["style-src"] += f" 'nonce-{g.csp_nonce}'"
72
+ policy["script-src"] += f" 'nonce-{g.csp_nonce}'"
73
+
74
+ safe_srcs = " ".join(set(current_app.config['CSP_SAFE_SRC']))
75
+ return "; ".join(("%s %s" % (k, v.replace("'safe-src'", safe_srcs))).strip() for k, v in policy.items()) + ";"
76
+
77
+
78
+ def csp_nonce():
79
+ if 'csp_nonce' not in g:
80
+ g.csp_nonce = secrets.token_urlsafe(16)[:16]
81
+ return g.csp_nonce
@@ -0,0 +1,5 @@
1
+ import Alpine from '@alpinejs/csp'
2
+
3
+ window.Alpine = Alpine
4
+
5
+ export default Alpine
@@ -2,4 +2,4 @@ import Alpine from 'alpinejs'
2
2
 
3
3
  window.Alpine = Alpine
4
4
 
5
- export default Alpine
5
+ export default Alpine
@@ -3,7 +3,7 @@ import "htmx-ext-sse";
3
3
  import "./htmx-exts";
4
4
  import "./app.css";
5
5
 
6
- const DEFAULT_HTMX_EXTS = ['hf-modal', 'hx-bottom-scroll-on-trigger', 'hx-active-url'];
6
+ const DEFAULT_HTMX_EXTS = ['hf-modal', 'hx-bottom-scroll-on-trigger', 'hx-active-url', 'hx-stream'];
7
7
  document.body.setAttribute('hx-ext', [document.body.getAttribute('hx-ext'), ...DEFAULT_HTMX_EXTS].filter(Boolean).join(","))
8
8
 
9
9
  window.htmx = htmx;
@@ -4,7 +4,7 @@ import htmx from "htmx.org";
4
4
  let triggerElement = false, enableTriggerElement;
5
5
  htmx.defineExtension("hf-modal", {
6
6
  getSelectors() {
7
- return ['[hf-modal]'];
7
+ return ['[hf-modal]'];
8
8
  },
9
9
  onEvent(name, evt) {
10
10
  if (name === "htmx:beforeProcessNode" && evt.target.hasAttribute("hf-modal")) {
@@ -42,21 +42,21 @@ import htmx from "htmx.org";
42
42
 
43
43
  htmx.defineExtension("hx-bottom-scroll-on-trigger", {
44
44
  getSelectors() {
45
- return ['[hx-bottom-scroll-on-trigger]'];
45
+ return ['[hx-bottom-scroll-on-trigger]'];
46
46
  },
47
47
  onEvent(name, evt) {
48
48
  if (name === "htmx:afterProcessNode" && evt.target.hasAttribute("hx-bottom-scroll-on-trigger")) {
49
- const elt = evt.target;
50
- elt.scrollTop = elt.scrollHeight - elt.clientHeight;
51
- elt.classList.add("at-bottom");
52
- elt.addEventListener("scroll", (e) => {
53
- if (e.target.scrollTop < e.target.scrollHeight - e.target.clientHeight - 20) {
54
- e.target.classList.remove("at-bottom");
55
- } else {
56
- e.target.classList.add("at-bottom");
57
- }
58
- });
59
- return;
49
+ const elt = evt.target;
50
+ elt.scrollTop = elt.scrollHeight - elt.clientHeight;
51
+ elt.classList.add("at-bottom");
52
+ elt.addEventListener("scroll", (e) => {
53
+ if (e.target.scrollTop < e.target.scrollHeight - e.target.clientHeight - 20) {
54
+ e.target.classList.remove("at-bottom");
55
+ } else {
56
+ e.target.classList.add("at-bottom");
57
+ }
58
+ });
59
+ return;
60
60
  }
61
61
 
62
62
  const eventName = evt.target.getAttribute("hx-bottom-scroll-on-trigger");
@@ -96,7 +96,7 @@ htmx.defineExtension("hx-active-url", {
96
96
  document.addEventListener("htmx:pushedIntoHistory", ensureLinksAreActive);
97
97
  },
98
98
  getSelectors() {
99
- return ['[hx-href]'];
99
+ return ['[hx-href]'];
100
100
  },
101
101
  onEvent(name, evt) {
102
102
  if (name === "htmx:beforeProcessNode" && evt.target.hasAttribute("hx-href")) {
@@ -105,3 +105,21 @@ htmx.defineExtension("hx-active-url", {
105
105
  }
106
106
  }
107
107
  });
108
+
109
+ htmx.defineExtension("hx-stream", {
110
+ getSelectors() {
111
+ return ['[hx-stream]'];
112
+ },
113
+ onEvent: function (name, evt) {
114
+ if (name === "htmx:beforeRequest" && evt.target.hasAttribute("hx-stream")) {
115
+ const target = evt.detail.target;
116
+ const xhr = evt.detail.xhr;
117
+ xhr.addEventListener('readystatechange', function () {
118
+ if (xhr.readyState === 2 || xhr.readyState === 3) {
119
+ target.innerHTML = xhr.responseText;
120
+ }
121
+ });
122
+ }
123
+ return true;
124
+ }
125
+ });
@@ -26,7 +26,7 @@
26
26
 
27
27
  {% macro FormField(field=None, label=None) -%}
28
28
  {% if label %}<{Label}>{{label}}</{Label}>{% elif field.label and label == None %}<{Label for=field.label.field_id}>{{field.label.text}}</{Label}>{% endif %}
29
- {{field if field else caller()}}
29
+ {{field(**kwargs) if field else caller()}}
30
30
  {%- endmacro %}
31
31
 
32
32
  {% macro SubmitButton() %}
@@ -16,7 +16,7 @@
16
16
 
17
17
  {% macro Sidebar() -%}
18
18
  {%- set inner = caller() -%}
19
- <{DrawerSide id="sidebar" drawer_id="app-drawer" class=html_class("min-w-[250px] bg-base-200", kwargs.pop('class', None)) **kwargs }>
19
+ <{DrawerSide id="sidebar" drawer_id="app-drawer" class=html_class("min-w-[250px] max-w-[250px] bg-base-200", kwargs.pop('class', None)) **kwargs }>
20
20
  <div class="flex flex-col h-full w-full" id="sidebar-content">
21
21
  {{inner}}
22
22
  </div>
@@ -0,0 +1,4 @@
1
+
2
+ {% macro PageMetadata(metadata) -%}
3
+
4
+ {%- endmacro %}
@@ -0,0 +1,33 @@
1
+
2
+ {% macro SkeletonLines(with_title=True, nb_lines=4) -%}
3
+ <div class="max-w-full animate-pulse">
4
+ <div
5
+ class="block w-4/5 h-3 mb-4 font-sans text-5xl antialiased font-semibold leading-tight tracking-normal bg-gray-300 rounded-full text-inherit">
6
+ &nbsp;
7
+ </div>
8
+ {% for _ in range(nb_lines - 1) -%}
9
+ <div
10
+ class="block h-2 mb-2 font-sans text-base antialiased font-light leading-relaxed bg-gray-300 rounded-full text-inherit w-full">
11
+ &nbsp;
12
+ </div>
13
+ {%- endfor %}
14
+ </div>
15
+ {%- endmacro %}
16
+
17
+ {% macro SkeletonBlock(icon=None) -%}
18
+ {{html_tag("div", class_="grid h-full animate-pulse place-items-center rounded-lg bg-gray-300 text-xxl text-gray-500", **kwargs)}}
19
+ {% if icon %}<{Icon icon}/>{% endif %}
20
+ </div>
21
+ {%- endmacro -%}
22
+
23
+ {% macro SkeletonCard() -%}
24
+
25
+ {%- endmacro %}
26
+
27
+ {% macro SkeletonList() -%}
28
+
29
+ {%- endmacro %}
30
+
31
+ {% macro SkeletonTable() -%}
32
+
33
+ {%- endmacro %}
@@ -0,0 +1,22 @@
1
+
2
+ {% macro MercureStream(topic=None, private=True, hx_swap="beforeend", type=None, auto_scroll=False, with_subscriptions=False) -%}
3
+ {% set sse_url = mercure_authentified_hub_url(topic, with_subscriptions=with_subscriptions) if private else mercure_hub_url(topic, with_subscriptions=with_subscriptions) %}
4
+ <div {% if topic %}hx-ext="sse" sse-connect="{{sse_url}}"{% endif %}
5
+ sse-swap="{{mercure_topic(topic if topic and not type and app.extensions['mercure_sse'].type_is_topic else (type or 'message'))}}"
6
+ {% if auto_scroll %}hx-bottom-scroll-on-trigger="htmx:sseMessage"{% endif %} {{html_attrs(hx_swap=hx_swap, class_="mercure-stream", **kwargs)}}>
7
+ {{ caller() }}
8
+ </div>
9
+ {%- endmacro %}
10
+
11
+ {% macro MercureStreamContext(topics, private=True, with_subscriptions=False) -%}
12
+ {% set sse_url = mercure_authentified_hub_url(topics, with_subscriptions=with_subscriptions) if private else mercure_hub_url(topics, with_subscriptions=with_subscriptions) %}
13
+ <div hx-ext="sse" sse-connect="{{sse_url}}" {{html_attrs(class_="mercure-stream-context", **kwargs)}}>
14
+ {{ caller() }}
15
+ </div>
16
+ {%- endmacro %}
17
+
18
+ {% macro MercurePresence(hx_get, topic=None) -%}
19
+ <div {% if topic %}hx-ext="sse" sse-connect="{{mercure_authentified_hub_url(mercure_subscription_topic(topic))}}"{% endif %}
20
+ hx-trigger="sse:Subscription" {{html_attrs(hx_get=hx_get, class_="mercure-presence", **kwargs)}}>
21
+ </div>
22
+ {%- endmacro %}
@@ -1,14 +0,0 @@
1
-
2
- {% macro MercureStream(topic=None, private=True, hx_swap="beforeend", type=None, auto_scroll=False) -%}
3
- <div {% if topic %}hx-ext="sse" sse-connect="{{mercure_authentified_hub_url(topic) if private else mercure_hub_url(topic)}}"{% endif %}
4
- sse-swap="{{mercure_topic(topic if topic and not type and app.extensions['mercure_sse'].type_is_topic else (type or 'message'))}}"
5
- hx-swap="{{hx_swap}}" {% if auto_scroll %}hx-bottom-scroll-on-trigger="htmx:sseMessage"{% endif %} {{html_attrs(**kwargs)}}>
6
- {{ caller() }}
7
- </div>
8
- {%- endmacro %}
9
-
10
- {% macro MercureStreamContext(topics, private=True) -%}
11
- <div hx-ext="sse" sse-connect="{{mercure_authentified_hub_url(topics) if private else mercure_hub_url(topic)}}" {{html_attrs(**kwargs)}}>
12
- {{ caller() }}
13
- </div>
14
- {%- endmacro %}
@@ -1,5 +0,0 @@
1
- from sqlorm import SQLType
2
- from flask_files import save_file, File
3
-
4
-
5
- File = SQLType("text", save_file, File.from_uri)
@@ -1,25 +0,0 @@
1
- from flask import request
2
- from urllib.parse import urlparse, urljoin
3
- import functools
4
-
5
-
6
- def is_safe_redirect_url(target):
7
- host_url = urlparse(request.host_url)
8
- redirect_url = urlparse(urljoin(request.host_url, target))
9
- return (
10
- redirect_url.scheme in ("http", "https")
11
- and host_url.netloc == redirect_url.netloc
12
- )
13
-
14
-
15
- def marshal_with_depends(content_types):
16
- def decorator(func):
17
- @functools.wraps(func)
18
- def wrapper(*args, **kwargs):
19
- response = func(*args, **kwargs)
20
- response.headers["Content-Type"] = content_types
21
- return response
22
-
23
- return wrapper
24
-
25
- return decorator