hyperflask 0.2.3__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 (125) hide show
  1. {hyperflask-0.2.3 → hyperflask-0.3.0}/PKG-INFO +9 -11
  2. {hyperflask-0.2.3 → hyperflask-0.3.0}/README.md +3 -4
  3. {hyperflask-0.2.3 → hyperflask-0.3.0}/pyproject.toml +7 -8
  4. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/__init__.py +2 -1
  5. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/app.py +71 -16
  6. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/cli.py +17 -4
  7. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/components/core.py +21 -5
  8. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/factory.py +1 -1
  9. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/forms/widgets.py +1 -0
  10. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/layouts/email.mjml +3 -3
  11. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/layouts/web.html +8 -6
  12. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/model.py +6 -1
  13. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/page_helpers.py +3 -11
  14. hyperflask-0.3.0/src/hyperflask/security.py +81 -0
  15. hyperflask-0.3.0/src/hyperflask/static/alpine-csp.js +5 -0
  16. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/static/alpine.js +1 -1
  17. hyperflask-0.3.0/src/hyperflask/static/app.js +14 -0
  18. hyperflask-0.3.0/src/hyperflask/static/htmx-exts.js +125 -0
  19. hyperflask-0.3.0/src/hyperflask/ui/flash.html +17 -0
  20. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/forms.html +1 -1
  21. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/layout.html +1 -1
  22. hyperflask-0.3.0/src/hyperflask/ui/metadata.html +4 -0
  23. hyperflask-0.3.0/src/hyperflask/ui/skeleton.html +33 -0
  24. hyperflask-0.3.0/src/hyperflask/ui/sse.html +22 -0
  25. hyperflask-0.3.0/src/hyperflask/utils/htmx.py +64 -0
  26. hyperflask-0.2.3/src/hyperflask/reactivity.py +0 -52
  27. hyperflask-0.2.3/src/hyperflask/static/app.js +0 -47
  28. hyperflask-0.2.3/src/hyperflask/static/reactive.js +0 -39
  29. hyperflask-0.2.3/src/hyperflask/ui/flash.html +0 -8
  30. hyperflask-0.2.3/src/hyperflask/ui/sse.html +0 -6
  31. hyperflask-0.2.3/src/hyperflask/utils/htmx.py +0 -27
  32. hyperflask-0.2.3/src/hyperflask/utils/models.py +0 -5
  33. hyperflask-0.2.3/src/hyperflask/utils/request.py +0 -25
  34. hyperflask-0.2.3/src/hyperflask/utils/s3.py +0 -4
  35. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/__main__.py +0 -0
  36. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/components/__init__.py +0 -0
  37. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/components/alpine.py +0 -0
  38. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/components/jsx.py +0 -0
  39. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/components/webcomponent.py +0 -0
  40. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/forms/__init__.py +0 -0
  41. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/forms/fields.py +0 -0
  42. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/static/app.css +0 -0
  43. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/static/react.js +0 -0
  44. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/static/stimulus.js +0 -0
  45. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/static/translations.js +0 -0
  46. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Accordion.html +0 -0
  47. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/AccordionPanel.html +0 -0
  48. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Alert.html +0 -0
  49. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Avatar.html +0 -0
  50. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/AvatarGroup.html +0 -0
  51. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Badge.html +0 -0
  52. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Breadcrumbs.html +0 -0
  53. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/BreadcrumbsItem.html +0 -0
  54. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Button.html +0 -0
  55. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Card.html +0 -0
  56. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/CardActions.html +0 -0
  57. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/CardTitle.html +0 -0
  58. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/ChatBubble.html +0 -0
  59. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Checkbox.html +0 -0
  60. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/CollapsibleSubMenu.html +0 -0
  61. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Countdown.html +0 -0
  62. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Divider.html +0 -0
  63. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Drawer.html +0 -0
  64. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/DrawerContent.html +0 -0
  65. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/DrawerSide.html +0 -0
  66. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Dropdown.html +0 -0
  67. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/DropdownItem.html +0 -0
  68. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Fieldset.html +0 -0
  69. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/FieldsetLegend.html +0 -0
  70. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/FileInput.html +0 -0
  71. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Filter.html +0 -0
  72. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/FilterItem.html +0 -0
  73. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Footer.html +0 -0
  74. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/FooterLink.html +0 -0
  75. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/FooterNav.html +0 -0
  76. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Indicator.html +0 -0
  77. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Input.html +0 -0
  78. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/InputLabel.html +0 -0
  79. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Join.html +0 -0
  80. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Kbd.html +0 -0
  81. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Label.html +0 -0
  82. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/LabeledCheckbox.html +0 -0
  83. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/LabeledToggle.html +0 -0
  84. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Link.html +0 -0
  85. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/List.html +0 -0
  86. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/ListItem.html +0 -0
  87. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Loading.html +0 -0
  88. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Menu.html +0 -0
  89. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/MenuItem.html +0 -0
  90. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/MenuTitle.html +0 -0
  91. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Modal.html +0 -0
  92. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/ModalAction.html +0 -0
  93. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/ModalTitle.html +0 -0
  94. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Navbar.html +0 -0
  95. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Pagination.html +0 -0
  96. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/PaginationNext.html +0 -0
  97. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/PaginationPage.html +0 -0
  98. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/PaginationPrev.html +0 -0
  99. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Progress.html +0 -0
  100. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/README.md +0 -0
  101. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Radio.html +0 -0
  102. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Range.html +0 -0
  103. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Rating.html +0 -0
  104. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Select.html +0 -0
  105. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Stack.html +0 -0
  106. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Stat.html +0 -0
  107. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Stats.html +0 -0
  108. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Step.html +0 -0
  109. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Steps.html +0 -0
  110. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/SubMenu.html +0 -0
  111. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Swap.html +0 -0
  112. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Tab.html +0 -0
  113. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Table.html +0 -0
  114. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Tabs.html +0 -0
  115. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Textarea.html +0 -0
  116. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Toast.html +0 -0
  117. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Toggle.html +0 -0
  118. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Tooltip.html +0 -0
  119. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/icons.html +0 -0
  120. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/modal.html +0 -0
  121. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/utils/__init__.py +0 -0
  122. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/utils/html.py +0 -0
  123. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/utils/markdown.py +0 -0
  124. {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/utils/page_actions.py +0 -0
  125. {hyperflask-0.2.3 → 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.3
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.3"
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
@@ -28,13 +28,12 @@ from flask_debugtoolbar import DebugToolbarExtension
28
28
  # from hyperflask
29
29
  from .forms import Form
30
30
  from .utils.page_actions import page_action_url
31
- from .utils.htmx import htmx_oob
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
 
@@ -69,14 +68,28 @@ class Hyperflask(Flask):
69
68
  "OTEL_INSTRUMENT": instrument,
70
69
  "DRAMATIQ_BROKER": "dramatiq.brokers.redis:RedisBroker",
71
70
  "DEBUG_TB_INTERCEPT_REDIRECTS": False,
71
+ "SESSION_COOKIE_HTTPONLY": True,
72
+ "SESSION_COOKIE_SAMESITE": "Lax",
72
73
  # Hyperflask specific
73
74
  "LAYOUT": "layouts/default.html",
75
+ "ALPINE": False,
74
76
  "STATIC_MODE": static_mode,
75
- "ASSETS_INCLUDE_HTMX": True,
76
- "HTMX_EXT": ["hf-modal"],
77
+ "ASSETS_INCLUDE_ALPINE": False,
78
+ "HTMX_EXT": [],
77
79
  "HTMX_BOOST_SITE": False,
78
80
  "MARKDOWN_OPTIONS": {"extensions": ["fenced_code", "nl2br", "attr_list", "admonition", "codehilite"]},
79
- "MARKDOWN_SANITIZER_CONFIG": {}
81
+ "MARKDOWN_SANITIZER_CONFIG": {},
82
+ "FLASH_TOAST_OOB": True,
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
80
93
  })
81
94
  if config:
82
95
  self.config.update(config)
@@ -89,12 +102,22 @@ class Hyperflask(Flask):
89
102
  self.config["COLLECTIONS_MARKDOWN_OPTIONS"] = self.config["MARKDOWN_OPTIONS"]
90
103
  self.config["MAIL_TEMPLATES_MARKDOWN_OPTIONS"] = self.config["MARKDOWN_OPTIONS"]
91
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
+
92
114
  #self.otel = Observability(self)
93
115
 
94
116
  self.jinja_env.add_extension(LayoutExtension)
95
117
  self.jinja_env.add_extension(WtformExtension)
96
118
  self.jinja_env.add_extension(MarkdownExtension)
97
119
  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)
98
121
  self.jinja_env.form_base_cls = Form
99
122
  self.jinja_env.default_layout = self.config["LAYOUT"]
100
123
  self.forms = self.jinja_env.forms
@@ -125,7 +148,7 @@ class Hyperflask(Flask):
125
148
  Geolocation(self)
126
149
  Files(self)
127
150
  MailTemplates(self, template_folder=emails_folder)
128
- 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)
129
152
  self.page_helper = file_routes.page_helper
130
153
  self.collections = Collections(self)
131
154
  self.sse = MercureSSE(self)
@@ -148,12 +171,13 @@ class Hyperflask(Flask):
148
171
  })
149
172
  self.assets.bundle(
150
173
  {"@hyperflask": ["app.js"],
151
- "@hyperflask/reactive": ["reactive.js"],
152
- "@hyperflask/alpine": ["alpine.js"]},
174
+ "@hyperflask/alpine": ["alpine-csp.js" if not self.config["CSP_UNSAFE_EVAL"] else "alpine.js"]},
153
175
  from_package="hyperflask",
154
176
  assets_folder="static"
155
177
  )
156
178
  self.assets.include("@hyperflask", 0)
179
+ if self.config["ALPINE"]:
180
+ self.assets.include("@hyperflask/alpine", 0)
157
181
 
158
182
  self.extensions['mail_templates'].jinja_env.add_extension(LayoutExtension)
159
183
  self.extensions['mail_templates'].jinja_env.default_layout = "layout.mjml"
@@ -175,7 +199,9 @@ class Hyperflask(Flask):
175
199
  for macro in self.macros.create_from_directory(os.path.join(self.root_path, forms_folder)):
176
200
  self.forms.register(self.macros.resolve_template(macro), macro)
177
201
 
178
- self.jinja_env.globals.update(page_action_url=page_action_url)
202
+ self.after_request(respond_with_security_headers)
203
+ if self.config["FLASH_TOAST_OOB"]:
204
+ self.after_request(respond_with_flash_messages)
179
205
 
180
206
  self.collections.register_freezer_generator(self.freezer)
181
207
 
@@ -184,10 +210,39 @@ class Hyperflask(Flask):
184
210
  self.db.Model = Model
185
211
  self.db.File = SQLFileType
186
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
+
187
237
  def relative_import_name(self, name):
188
238
  return f"{self.import_name}.{name}" if self.import_name != "__main__" else name
189
239
 
190
240
 
241
+ class HyperModuleView(ModuleView):
242
+ def _render_template(self):
243
+ return render_template(page.template)
244
+
245
+
191
246
  def static(func):
192
247
  pass
193
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
 
@@ -21,10 +21,10 @@
21
21
  </mj-style>
22
22
  {% endblock %}
23
23
  </mj-head>
24
- <mj-body {% block body_tags %}background-color="{{config.get("EMAIL_BACKGROUND_COLOR", "#c8c8c8")}}" width="600px"{% endblock %}>
25
- <mj-section full-width="full-width" background-color="{{config.get("EMAIL_BACKGROUND_ACCENT_COLOR", "#a6a6a6")}}" padding-bottom="0">
24
+ <mj-body {% block body_tags %}background-color="{{config.get("EMAIL_BACKGROUND_COLOR", "#ebebebff")}}" width="600px"{% endblock %}>
25
+ <mj-section full-width="full-width" background-color="{{config.get("EMAIL_BACKGROUND_ACCENT_COLOR", "#d1d1d1ff")}}" padding-bottom="0">
26
26
  <mj-column width="100%">
27
- <mj-text color="#4d4d4d" font-weight="bold" align="center" font-size="16px" letter-spacing="1px">
27
+ <mj-text color="#474747ff" font-weight="bold" align="center" font-size="16px" letter-spacing="1px">
28
28
  {% block brand %}
29
29
  {{config.get("SITE_TITLE", "")}}
30
30
  {% endblock %}
@@ -6,11 +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
- {% if config["PWA"] -%}
10
- <link rel="manifest" href="{{url_for("pwa_manifest")}}" />
11
- {%- endif %}
12
- {% 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 %}
13
- {% 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 %}
14
16
  {% block head_assets %}{% endblock %}
15
17
  {%- endblock %}
16
18
  </head>
@@ -25,7 +27,7 @@
25
27
  {% block footer %}{% endblock %}
26
28
  {% block flash_messages %}
27
29
  {% autoescape false %} {# Jinja bug #}
28
- <{FlashMessages}/>
30
+ <{FlashMessagesToast}/>
29
31
  {% endautoescape %}
30
32
  {% endblock %}
31
33
  {% block body_end %}{% endblock %}
@@ -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]))
@@ -1,5 +1,5 @@
1
1
  from flask_file_routes import DEFAULT_HELPERS
2
- from flask import g, current_app
2
+ from flask import current_app
3
3
  from .utils.page_actions import page_action
4
4
 
5
5
 
@@ -18,19 +18,11 @@ def form(page):
18
18
  return form_helper
19
19
 
20
20
 
21
- def stream(page):
22
- g.__page_stream__ = True
23
- return
24
-
25
-
26
21
  def action(page):
27
- def decorator(func=None, name=None):
28
- return page_action(func, name)
29
- return decorator
22
+ return page_action
30
23
 
31
24
 
32
25
  DEFAULT_HELPERS.update(csrf_protect=csrf_protect,
33
26
  forms=forms,
34
27
  form=form,
35
- stream=stream,
36
- action=action)
28
+ action=action)
@@ -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
@@ -0,0 +1,14 @@
1
+ import htmx from "htmx.org";
2
+ import "htmx-ext-sse";
3
+ import "./htmx-exts";
4
+ import "./app.css";
5
+
6
+ const DEFAULT_HTMX_EXTS = ['hf-modal', 'hx-bottom-scroll-on-trigger', 'hx-active-url', 'hx-stream'];
7
+ document.body.setAttribute('hx-ext', [document.body.getAttribute('hx-ext'), ...DEFAULT_HTMX_EXTS].filter(Boolean).join(","))
8
+
9
+ window.htmx = htmx;
10
+ export { htmx };
11
+
12
+ if (window.Alpine) {
13
+ window.Alpine.start()
14
+ }