hyperflask 0.5.2__tar.gz → 0.7.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 (129) hide show
  1. {hyperflask-0.5.2 → hyperflask-0.7.0}/PKG-INFO +7 -8
  2. {hyperflask-0.5.2 → hyperflask-0.7.0}/README.md +5 -5
  3. {hyperflask-0.5.2 → hyperflask-0.7.0}/pyproject.toml +5 -3
  4. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/__init__.py +3 -0
  5. hyperflask-0.7.0/src/hyperflask/actors.py +128 -0
  6. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/app.py +42 -31
  7. hyperflask-0.7.0/src/hyperflask/cli/__init__.py +135 -0
  8. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/cli/runner.py +57 -39
  9. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/cli/worker.py +5 -2
  10. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/components/core.py +23 -2
  11. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/factory.py +8 -5
  12. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/layouts/web.html +2 -2
  13. hyperflask-0.5.2/src/hyperflask/model.py → hyperflask-0.7.0/src/hyperflask/models.py +2 -22
  14. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/static/app.css +2 -0
  15. hyperflask-0.7.0/src/hyperflask/static/app.js +11 -0
  16. hyperflask-0.7.0/src/hyperflask/static/htmx-exts.js +186 -0
  17. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/forms.html +3 -2
  18. hyperflask-0.7.0/src/hyperflask/ui/sse.html +24 -0
  19. hyperflask-0.7.0/src/hyperflask/utils/collections.py +10 -0
  20. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/utils/htmx.py +6 -11
  21. hyperflask-0.5.2/src/hyperflask/actors.py +0 -82
  22. hyperflask-0.5.2/src/hyperflask/cli/__init__.py +0 -46
  23. hyperflask-0.5.2/src/hyperflask/static/app.js +0 -14
  24. hyperflask-0.5.2/src/hyperflask/static/htmx-exts.js +0 -128
  25. hyperflask-0.5.2/src/hyperflask/ui/sse.html +0 -22
  26. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/__main__.py +0 -0
  27. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/cli/reloader.py +0 -0
  28. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/cli/scaffold.py +0 -0
  29. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/components/__init__.py +0 -0
  30. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/components/alpine.py +0 -0
  31. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/components/jsx.py +0 -0
  32. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/components/webcomponent.py +0 -0
  33. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/forms/__init__.py +0 -0
  34. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/forms/fields.py +0 -0
  35. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/forms/widgets.py +0 -0
  36. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/layouts/email.mjml +0 -0
  37. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/page_helpers.py +0 -0
  38. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/security.py +0 -0
  39. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/static/alpine-csp.js +0 -0
  40. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/static/alpine.js +0 -0
  41. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/static/react.js +0 -0
  42. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/static/stimulus.js +0 -0
  43. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/static/translations.js +0 -0
  44. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/Accordion.html +0 -0
  45. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/AccordionPanel.html +0 -0
  46. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/Alert.html +0 -0
  47. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/Avatar.html +0 -0
  48. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/AvatarGroup.html +0 -0
  49. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/Badge.html +0 -0
  50. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/Breadcrumbs.html +0 -0
  51. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/BreadcrumbsItem.html +0 -0
  52. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/Button.html +0 -0
  53. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/Card.html +0 -0
  54. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/CardActions.html +0 -0
  55. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/CardTitle.html +0 -0
  56. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/ChatBubble.html +0 -0
  57. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/Checkbox.html +0 -0
  58. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/CollapsibleSubMenu.html +0 -0
  59. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/Countdown.html +0 -0
  60. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/Divider.html +0 -0
  61. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/Drawer.html +0 -0
  62. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/DrawerContent.html +0 -0
  63. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/DrawerSide.html +0 -0
  64. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/Dropdown.html +0 -0
  65. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/DropdownItem.html +0 -0
  66. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/Fieldset.html +0 -0
  67. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/FieldsetLegend.html +0 -0
  68. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/FileInput.html +0 -0
  69. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/Filter.html +0 -0
  70. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/FilterItem.html +0 -0
  71. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/Footer.html +0 -0
  72. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/FooterLink.html +0 -0
  73. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/FooterNav.html +0 -0
  74. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/Indicator.html +0 -0
  75. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/Input.html +0 -0
  76. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/InputLabel.html +0 -0
  77. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/Join.html +0 -0
  78. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/Kbd.html +0 -0
  79. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/Label.html +0 -0
  80. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/LabeledCheckbox.html +0 -0
  81. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/LabeledToggle.html +0 -0
  82. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/Link.html +0 -0
  83. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/List.html +0 -0
  84. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/ListItem.html +0 -0
  85. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/Loading.html +0 -0
  86. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/Menu.html +0 -0
  87. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/MenuItem.html +0 -0
  88. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/MenuTitle.html +0 -0
  89. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/Modal.html +0 -0
  90. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/ModalAction.html +0 -0
  91. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/ModalTitle.html +0 -0
  92. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/Navbar.html +0 -0
  93. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/Pagination.html +0 -0
  94. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/PaginationNext.html +0 -0
  95. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/PaginationPage.html +0 -0
  96. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/PaginationPrev.html +0 -0
  97. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/Progress.html +0 -0
  98. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/README.md +0 -0
  99. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/Radio.html +0 -0
  100. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/Range.html +0 -0
  101. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/Rating.html +0 -0
  102. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/Select.html +0 -0
  103. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/Stack.html +0 -0
  104. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/Stat.html +0 -0
  105. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/Stats.html +0 -0
  106. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/Step.html +0 -0
  107. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/Steps.html +0 -0
  108. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/SubMenu.html +0 -0
  109. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/Swap.html +0 -0
  110. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/Tab.html +0 -0
  111. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/Table.html +0 -0
  112. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/Tabs.html +0 -0
  113. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/Textarea.html +0 -0
  114. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/Toast.html +0 -0
  115. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/Toggle.html +0 -0
  116. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/daisyui/Tooltip.html +0 -0
  117. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/flash.html +0 -0
  118. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/icons.html +0 -0
  119. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/layout.html +0 -0
  120. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/modal.html +0 -0
  121. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/ui/skeleton.html +0 -0
  122. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/utils/__init__.py +0 -0
  123. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/utils/freezer.py +0 -0
  124. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/utils/html.py +0 -0
  125. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/utils/image.py +0 -0
  126. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/utils/markdown.py +0 -0
  127. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/utils/metadata.py +0 -0
  128. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/utils/page_actions.py +0 -0
  129. {hyperflask-0.5.2 → hyperflask-0.7.0}/src/hyperflask/utils/tokens.py +0 -0
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperflask
3
- Version: 0.5.2
3
+ Version: 0.7.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
- Requires-Dist: flask>=3.0.0,<4.0.0
8
+ Requires-Dist: flask>=3.1.0,<4.0.0
9
9
  Requires-Dist: flask-assets-pipeline~=0.8
10
10
  Requires-Dist: flask-babel-hyper~=5.1
11
11
  Requires-Dist: flask-collections~=0.3
@@ -13,7 +13,6 @@ Requires-Dist: flask-configurator~=0.1
13
13
  Requires-Dist: flask-debugtoolbar~=0.16
14
14
  Requires-Dist: flask-file-routes~=0.3
15
15
  Requires-Dist: flask-files~=0.2
16
- Requires-Dist: flask-geo~=0.1
17
16
  Requires-Dist: flask-mailman-templates~=0.1
18
17
  Requires-Dist: flask-mercure-sse~=0.4
19
18
  Requires-Dist: flask-sqlorm~=0.3
@@ -43,7 +42,7 @@ Description-Content-Type: text/markdown
43
42
 
44
43
  **Full stack Python web framework to build websites and web apps with as little boilerplate as possible**
45
44
 
46
- [hyperflask.dev](https://hyperflask.dev) • [Get started](https://hyperflask.dev/getting-started) • [Docs](https://hyperflask.dev/guides) • [Example apps](https://github.com/hyperflask/examples)
45
+ [hyperflask.dev](https://hyperflask.dev) • [Get started](https://hyperflask.dev/getting-started) • [Docs](https://hyperflask.dev/guides) • [Example app](https://github.com/hyperflask/getting-started)
47
46
 
48
47
  </div>
49
48
 
@@ -74,11 +73,10 @@ Features and technologies:
74
73
  - Dev environment based on [Development Containers](https://containers.dev/)
75
74
  - Optimized for VScode with breakpoint debugging for frontend and backend
76
75
  - [Mailpit](https://github.com/axllent/mailpit) for email testing
77
- - Deployments using [docker-web-deploy](https://github.com/hyperflask/docker-web-deploy)
78
76
 
79
77
  ## Project status
80
78
 
81
- **Hyperflask is being actively developed and is not yet ready to be used.**
79
+ **Hyperflask is in BETA and can start to be used for non-critical production apps**
82
80
 
83
81
  Checkout the list of all the projects developed as part of the Hyperflask stack and their current development status on the [Hyperflask Github organization homepage](https://github.com/hyperflask).
84
82
 
@@ -116,5 +114,6 @@ A good part of these extensions is developed as part of the Hyperflask project.
116
114
  1. Create your project directory: `mkdir example-project && cd example-project`
117
115
  2. Create and activate a virtualenv: `python -m venv .venv && source .venv/bin/activate`
118
116
  3. `pip install hyperflask`
119
- 4. Run `hyperflask init .`
120
- 5. Start a development server using `hyperflask dev`
117
+ 5. Run `npm install esbuild tailwindcss @tailwindcss/cli htmx.org@4 bootstrap-icons daisyui`
118
+ 4. Run `mkdir pages && echo "Hello world" > pages/index.jpy`
119
+ 6. Start a development server using `hyperflask dev`
@@ -4,7 +4,7 @@
4
4
 
5
5
  **Full stack Python web framework to build websites and web apps with as little boilerplate as possible**
6
6
 
7
- [hyperflask.dev](https://hyperflask.dev) • [Get started](https://hyperflask.dev/getting-started) • [Docs](https://hyperflask.dev/guides) • [Example apps](https://github.com/hyperflask/examples)
7
+ [hyperflask.dev](https://hyperflask.dev) • [Get started](https://hyperflask.dev/getting-started) • [Docs](https://hyperflask.dev/guides) • [Example app](https://github.com/hyperflask/getting-started)
8
8
 
9
9
  </div>
10
10
 
@@ -35,11 +35,10 @@ Features and technologies:
35
35
  - Dev environment based on [Development Containers](https://containers.dev/)
36
36
  - Optimized for VScode with breakpoint debugging for frontend and backend
37
37
  - [Mailpit](https://github.com/axllent/mailpit) for email testing
38
- - Deployments using [docker-web-deploy](https://github.com/hyperflask/docker-web-deploy)
39
38
 
40
39
  ## Project status
41
40
 
42
- **Hyperflask is being actively developed and is not yet ready to be used.**
41
+ **Hyperflask is in BETA and can start to be used for non-critical production apps**
43
42
 
44
43
  Checkout the list of all the projects developed as part of the Hyperflask stack and their current development status on the [Hyperflask Github organization homepage](https://github.com/hyperflask).
45
44
 
@@ -77,5 +76,6 @@ A good part of these extensions is developed as part of the Hyperflask project.
77
76
  1. Create your project directory: `mkdir example-project && cd example-project`
78
77
  2. Create and activate a virtualenv: `python -m venv .venv && source .venv/bin/activate`
79
78
  3. `pip install hyperflask`
80
- 4. Run `hyperflask init .`
81
- 5. Start a development server using `hyperflask dev`
79
+ 5. Run `npm install esbuild tailwindcss @tailwindcss/cli htmx.org@4 bootstrap-icons daisyui`
80
+ 4. Run `mkdir pages && echo "Hello world" > pages/index.jpy`
81
+ 6. Start a development server using `hyperflask dev`
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "hyperflask"
3
- version = "0.5.2"
3
+ version = "0.7.0"
4
4
  description = "Full stack web framework"
5
5
  authors = [
6
6
  {"name" = "Maxime Bouroumeau-Fuseau", email = "maxime.bouroumeau@gmail.com"}
@@ -9,7 +9,7 @@ readme = "README.md"
9
9
  license = "MIT"
10
10
  requires-python = ">=3.10"
11
11
  dependencies = [
12
- "flask>=3.0.0,<4.0.0",
12
+ "flask>=3.1.0,<4.0.0",
13
13
  "flask-assets-pipeline ~=0.8",
14
14
  "flask-babel-hyper ~=5.1",
15
15
  "flask-collections ~=0.3",
@@ -17,7 +17,6 @@ dependencies = [
17
17
  "flask-debugtoolbar ~=0.16",
18
18
  "flask-file-routes ~=0.3",
19
19
  "flask-files ~=0.2",
20
- "flask-geo ~=0.1",
21
20
  "flask-mailman-templates ~=0.1",
22
21
  "flask-mercure-sse ~=0.4",
23
22
  #"flask-observability ~=0.1"
@@ -49,6 +48,9 @@ Repository = "https://github.com/hyperflask/hyperflask"
49
48
  [project.scripts]
50
49
  hyperflask = "hyperflask.cli:main"
51
50
 
51
+ [project.entry-points."flask_collections"]
52
+ model = "hyperflask.model:ModelCollection"
53
+
52
54
  [dependency-groups]
53
55
  dev = [
54
56
  "pytest>=8.4.1",
@@ -3,6 +3,8 @@ from .app import Hyperflask
3
3
  from .utils.htmx import htmx_redirect, htmx_oob
4
4
  from .utils.freezer import StaticMode, static, dynamic
5
5
  from .security import csp_nonce
6
+ from .actors import actor, ActorGroup
7
+ from .models import Model
6
8
 
7
9
  # useful exports from hyperflask managed extensions
8
10
  from flask_babel import gettext, _, pgettext, _p, ngettext, _n, npgettext, _np, lazy_gettext, _lazy
@@ -18,3 +20,4 @@ from htmx_flask import request, make_response
18
20
  from slugify import slugify
19
21
  from blinker import signal
20
22
  from periodiq import cron
23
+ from sqlorm import transaction
@@ -0,0 +1,128 @@
1
+ from threading import local
2
+ import typing as t
3
+ import importlib
4
+ from datetime import timedelta
5
+ from dramatiq import Middleware, actor as register_actor, Message
6
+ from flask import has_app_context, current_app
7
+ from sqlorm.helpers import after_commit as sqlorm_after_commit
8
+
9
+
10
+ DRAMATIQ_BROKERS = {
11
+ "amqp": "dramatiq.brokers.rabbitmq:RabbitmqBroker",
12
+ "redis": "dramatiq.brokers.redis:RedisBroker",
13
+ "sqlite": "dramatiq_sqlite:SQLiteBroker",
14
+ }
15
+
16
+
17
+ def discover_broker(broker_cls=None, broker_url=None):
18
+ if not broker_url and broker_cls and "://" in broker_cls:
19
+ broker_url = broker_cls
20
+ broker_cls = None
21
+ if not broker_cls and broker_url:
22
+ if "://" not in broker_url:
23
+ broker_name = "sqlite"
24
+ else:
25
+ broker_name, broker_url = broker_url.split("://", 1)
26
+ if broker_name not in DRAMATIQ_BROKERS:
27
+ raise ValueError(f"Unknown dramatiq broker '{broker_name}'")
28
+ broker_cls = DRAMATIQ_BROKERS[broker_name]
29
+ if isinstance(broker_cls, str):
30
+ module_name, cls_name = broker_cls.split(":")
31
+ module = importlib.import_module(module_name)
32
+ broker_cls = getattr(module, cls_name)
33
+ return broker_cls, broker_url
34
+
35
+
36
+ class actor:
37
+ @classmethod
38
+ def bind(cls, broker):
39
+ return type('bound_actor', (cls,), {'__broker__': broker})
40
+
41
+ def __init__(self, *args, **kwargs):
42
+ self.actor_args = (args, kwargs)
43
+ self.fn = None
44
+ self.actor = None
45
+
46
+ def __call__(self, *args, **kwargs):
47
+ if not self.fn:
48
+ self.fn = args[0]
49
+ if getattr(self, '__broker__', None):
50
+ self.register(self.__broker__)
51
+ elif has_app_context() and getattr(current_app, 'dramatiq_broker', None):
52
+ self.register(current_app.dramatiq_broker)
53
+ return self
54
+ return self.fn(*args, **kwargs)
55
+
56
+ def register(self, broker):
57
+ self.actor = register_actor(broker=broker, *self.actor_args[0], **self.actor_args[1])(self.fn)
58
+
59
+ def __getattr__(self, name):
60
+ if self.actor:
61
+ return getattr(self.actor, name)
62
+ raise AttributeError(name)
63
+
64
+ def send(self, *args, **kwargs):
65
+ return self.send_with_options(args=args, kwargs=kwargs)
66
+
67
+ def send_with_options(
68
+ self,
69
+ *,
70
+ args: tuple = (),
71
+ kwargs: t.Optional[dict[str, t.Any]] = None,
72
+ delay: t.Optional[timedelta | int] = None,
73
+ after_commit: bool = True,
74
+ **options,
75
+ ) -> Message:
76
+ if not self.actor:
77
+ raise RuntimeError("No broker defined for Dramatiq actors.")
78
+
79
+ if isinstance(delay, timedelta):
80
+ delay = int(delay.total_seconds() * 1000)
81
+
82
+ message = self.actor.message_with_options(args=args, kwargs=kwargs, **options)
83
+
84
+ if after_commit:
85
+ sqlorm_after_commit(lambda: self.actor.broker.enqueue(message, delay=delay))
86
+ return message
87
+
88
+ return self.actor.broker.enqueue(message, delay=delay)
89
+
90
+
91
+ class ActorGroup(list):
92
+ def actor(self, *args, **kwargs):
93
+ def decorator(fn):
94
+ a = actor(*args, **kwargs)(fn)
95
+ self.append(a)
96
+ return a
97
+ return decorator
98
+
99
+ def register(self, broker):
100
+ for a in self:
101
+ a.register(broker)
102
+
103
+
104
+ class AppContextMiddleware(Middleware):
105
+ # Setup Flask app for actor. Borrowed from
106
+ # https://github.com/Bogdanp/flask_dramatiq_example.
107
+
108
+ state = local()
109
+
110
+ def __init__(self, app):
111
+ self.app = app
112
+
113
+ def before_process_message(self, broker, message):
114
+ context = self.app.app_context()
115
+ context.push()
116
+
117
+ self.state.context = context
118
+
119
+ def after_process_message(
120
+ self, broker, message, *, result=None, exception=None):
121
+ try:
122
+ context = self.state.context
123
+ context.pop(exception)
124
+ del self.state.context
125
+ except AttributeError:
126
+ pass
127
+
128
+ after_skip_message = after_process_message
@@ -1,5 +1,6 @@
1
1
  # flask
2
- from flask import Flask, has_request_context, request
2
+ from flask import Flask, has_request_context, request, current_app
3
+ from flask.globals import _cv_app
3
4
  from jinja2 import ChoiceLoader, FileSystemLoader, PrefixLoader, TemplateNotFound
4
5
  from werkzeug.middleware.proxy_fix import ProxyFix
5
6
  # hyperflask extensions
@@ -11,12 +12,11 @@ from flask_collections import Collections
11
12
  from flask_file_routes import FileRoutes, ModuleView, page
12
13
  from flask_configurator import Config
13
14
  from flask_babel import Babel, _, _p, _n, _np, lazy_gettext
14
- from flask_geo import Geolocation
15
15
  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_macros
19
+ from flask_suspense import Suspense, render_template, suspense_before_render_template, suspense_after_render_macros
20
20
  #from flask_observability import Observability
21
21
  # 3rd party extensions
22
22
  from flask_wtf.csrf import CSRFProtect
@@ -33,13 +33,14 @@ from .utils.metadata import metadata_tags
33
33
  from .utils.freezer import StaticMode, Freezer, freezer_url_generator
34
34
  from .utils.image import image_tag
35
35
  from .components import register_components
36
- from .model import Model, File as SQLFileType, UndefinedDatabase
37
- from .actors import AppContextMiddleware, discover_broker, make_actor_decorator
36
+ from .models import Model, File as SQLFileType, UndefinedDatabase
37
+ from .actors import AppContextMiddleware, discover_broker, actor
38
38
  from .security import respond_with_security_headers, csp_nonce
39
39
  from . import page_helpers
40
40
  # others
41
41
  from jinja_super_macros.registry import FileLoader
42
42
  from jinja_wtforms.extractor import map_jinja_call_node_to_func
43
+ from sqlorm import transaction
43
44
  from sqlorm.sql_template import SQLTemplate
44
45
  from periodiq import PeriodiqMiddleware
45
46
  import os
@@ -77,12 +78,11 @@ class Hyperflask(Flask):
77
78
  "SESSION_COOKIE_SAMESITE": "Lax",
78
79
  "SQLORM_URI": database_uri,
79
80
  # Hyperflask specific
80
- "URL_FOR_SERVER_NAME": None,
81
81
  "LAYOUT": "layouts/default.html",
82
82
  "ALPINE": False,
83
83
  "STATIC_MODE": static_mode,
84
84
  "ASSETS_INCLUDE_ALPINE": False,
85
- "HTMX_EXT": [],
85
+ "HTMX_CONFIG": {},
86
86
  "HTMX_BOOST_SITE": False,
87
87
  "MARKDOWN_OPTIONS": {"extensions": ["fenced_code", "nl2br", "attr_list", "admonition", "codehilite"]},
88
88
  "MARKDOWN_SANITIZER_CONFIG": {},
@@ -111,6 +111,8 @@ class Hyperflask(Flask):
111
111
  self.config["PAGES_MARKDOWN_OPTIONS"] = self.config["MARKDOWN_OPTIONS"]
112
112
  self.config["COLLECTIONS_MARKDOWN_OPTIONS"] = self.config["MARKDOWN_OPTIONS"]
113
113
  self.config["MAIL_TEMPLATES_MARKDOWN_OPTIONS"] = self.config["MARKDOWN_OPTIONS"]
114
+ # error responses are not swapped (only out of band swaps are processed)
115
+ self.config["HTMX_CONFIG"] = {"noSwap": [204, 304, "4xx", "5xx"], **self.config["HTMX_CONFIG"]}
114
116
 
115
117
  if self.config['SERVER_SECURED']:
116
118
  self.config.update({
@@ -151,7 +153,7 @@ class Hyperflask(Flask):
151
153
  PrefixLoader({"ui": FileSystemLoader(os.path.join(os.path.dirname(__file__), "ui"))})
152
154
  ])
153
155
 
154
- DebugToolbarExtension(self)
156
+ HyperDebugToolbarExtension(self)
155
157
  self.freezer = Freezer(self, with_no_argument_rules=False, log_url_for=self.config.get('FREEZER_LOG_URL_FOR', False))
156
158
  self.freezer.register_generator(lambda: freezer_url_generator(self))
157
159
  self.csrf = CSRFProtect(self)
@@ -161,7 +163,6 @@ class Hyperflask(Flask):
161
163
  SuperMacros(self)
162
164
  self.components = self.macros # alias
163
165
  Babel(self, extract_locale_from_request="locale")
164
- Geolocation(self)
165
166
  Files(self)
166
167
  MailTemplates(self, template_folder=emails_folder)
167
168
  file_routes = FileRoutes(self, pages_folder=pages_folder, module_view_class=HyperModuleView)
@@ -235,7 +236,9 @@ class Hyperflask(Flask):
235
236
  self.dramatiq_broker = broker_cls(url=broker_url)
236
237
  self.dramatiq_broker.add_middleware(AppContextMiddleware(self))
237
238
  self.dramatiq_broker.add_middleware(PeriodiqMiddleware())
238
- self.actor = make_actor_decorator(self)
239
+ self.actor = actor.bind(self.dramatiq_broker)
240
+ else:
241
+ self.actor = actor
239
242
 
240
243
  Suspense(self, nonce_getter="csp_nonce()")
241
244
 
@@ -243,10 +246,11 @@ class Hyperflask(Flask):
243
246
  def on_suspense_before_render_template(sender, **kwargs):
244
247
  csp_nonce() # we will need a nonce but the first call will be after the template started rendering
245
248
 
246
- @suspense_before_render_macros.connect_via(self, weak=False)
247
- def on_suspense_before_render_macros(sender, **kwargs):
248
- return ""
249
- return f'<script nonce="{csp_nonce()}">htmx.bootstrap()</script>' # waiting on PR 3441
249
+ @suspense_after_render_macros.connect_via(self, weak=False)
250
+ def on_suspense_after_render_macros(sender, **kwargs):
251
+ # process the content inserted by suspense macros during htmx requests
252
+ # (during full page loads, htmx is not loaded yet and processes the whole page once loaded)
253
+ return f'<script nonce="{csp_nonce()}">window.htmx?.process(document.body)</script>'
250
254
 
251
255
  @self.errorhandler(404)
252
256
  def not_found(error):
@@ -281,32 +285,39 @@ class Hyperflask(Flask):
281
285
  scheme = self.config.get('PREFERRED_URL_SCHEME', 'http')
282
286
  if self.config.get('SERVER_NAME'):
283
287
  return f"{scheme}://{self.config['SERVER_NAME']}"
284
- if self.config['URL_FOR_SERVER_NAME']:
285
- return f"{scheme}://{self.config['URL_FOR_SERVER_NAME']}"
286
288
  if has_request_context():
287
289
  return request.host_url
288
- raise RuntimeError("No request context and no SERVER_NAME or URL_FOR_SERVER_NAME configured, cannot determine base_url")
290
+ raise RuntimeError("No request context and no SERVER_NAME configured, cannot determine base_url")
289
291
 
290
292
  def url_for(self, *args, **kwargs):
291
293
  _external = kwargs.get("_external", False)
292
- _scheme = kwargs.get("_scheme", None)
293
- if _external and self.config['URL_FOR_SERVER_NAME']:
294
- # we handle the host and scheme ourselves
295
- kwargs['_external'] = False
296
- kwargs.pop('_scheme', None)
297
- else:
298
- _external = False
299
- url = super().url_for(*args, **kwargs)
300
- if not _external:
294
+ if not has_request_context() and not _external and not self.config.get('SERVER_NAME'):
295
+ self.config['SERVER_NAME'] = 'http://localhost:5000'
296
+ app_ctx = _cv_app.get(None)
297
+ if app_ctx is not None:
298
+ prev_url_adapter = app_ctx.url_adapter
299
+ app_ctx.url_adapter = self.create_url_adapter(None)
300
+ url = super().url_for(*args, **kwargs)
301
+ if app_ctx is not None:
302
+ app_ctx.url_adapter = prev_url_adapter
303
+ url = url[:len(self.config['SERVER_NAME']):]
304
+ self.config['SERVER_NAME'] = None
301
305
  return url
302
- if not _scheme:
303
- _scheme = self.config.get('PREFERRED_URL_SCHEME', 'http')
304
- url = f"{_scheme}://{self.config['URL_FOR_SERVER_NAME']}{url}"
305
- return url
306
+ return super().url_for(*args, **kwargs)
306
307
 
307
308
 
308
309
  class HyperModuleView(ModuleView):
309
- module_globals = dict(ModuleView.module_globals, asset_url=asset_url, static_url=static_url)
310
+ module_globals = dict(ModuleView.module_globals,
311
+ asset_url=asset_url,
312
+ static_url=static_url,
313
+ transaction=transaction)
310
314
 
311
315
  def _render_template(self):
312
316
  return render_template(page.template)
317
+
318
+
319
+ class HyperDebugToolbarExtension(DebugToolbarExtension):
320
+ def _show_toolbar(self):
321
+ if current_app.config.get("DEBUG_TB_ENABLED", True) is False:
322
+ return False
323
+ return super()._show_toolbar()
@@ -0,0 +1,135 @@
1
+ import os
2
+ import json
3
+ import click
4
+ from flask import current_app
5
+ from flask.cli import FlaskGroup, shell_command, routes_command, ScriptInfo, NoAppException
6
+ from flask_assets_pipeline import cli as assets_cli
7
+ from ..factory import create_app
8
+ from ..security import generate_csp_policy
9
+ from ..utils.freezer import StaticMode
10
+ from .runner import serve_command, run_command, dev_command
11
+ from .worker import worker_command, scheduler_command
12
+ from .scaffold import gen
13
+
14
+
15
+ def _create_app():
16
+ try:
17
+ if os.environ.get("FLASK_APP") == "0":
18
+ raise NoAppException("")
19
+ info = ScriptInfo(os.environ.get("FLASK_APP"))
20
+ return info.load_app()
21
+ except NoAppException:
22
+ return create_app(os.environ.get("FLASK_ROOT"))
23
+
24
+
25
+ cli = FlaskGroup(name='kantree', help='Kantree', add_default_commands=False, create_app=_create_app)
26
+ cli.add_command(shell_command)
27
+ cli.add_command(routes_command)
28
+ cli.add_command(serve_command)
29
+ cli.add_command(run_command)
30
+ cli.add_command(dev_command)
31
+ cli.add_command(worker_command)
32
+ cli.add_command(scheduler_command)
33
+ cli.add_command(gen)
34
+
35
+
36
+ @cli.command("build")
37
+ @click.pass_context
38
+ def build_command(ctx):
39
+ ctx.invoke(assets_cli.build)
40
+ if StaticMode(current_app.config['STATIC_MODE']) != StaticMode.DYNAMIC:
41
+ current_app.freezer.freeze()
42
+
43
+
44
+ @cli.command("csp-header")
45
+ def csp_header_command():
46
+ click.echo(generate_csp_policy())
47
+
48
+
49
+ @cli.command("encode-session-cookie")
50
+ @click.argument("data")
51
+ def encode_session_cookie_command(data):
52
+ """Encode a session cookie value from JSON data"""
53
+ serializer = current_app.session_interface.get_signing_serializer(current_app)
54
+ value = serializer.dumps(json.loads(data))
55
+ click.echo(value)
56
+
57
+
58
+ @cli.command("decode-session-cookie")
59
+ @click.argument("cookie", required=False)
60
+ @click.option("--cookie-file", "-f", type=click.Path(exists=True), help="Path to a file containing a list of cookies (from test-request command)")
61
+ def decode_session_cookie_command(cookie, cookie_file):
62
+ """Decode a session cookie value to JSON data"""
63
+ if not cookie and cookie_file:
64
+ with open(cookie_file, "r") as f:
65
+ for cookie_data in f.readlines():
66
+ key, value = cookie_data.split(";", 1)[0].split("=", 1)
67
+ if key.strip() == current_app.config["SESSION_COOKIE_NAME"]:
68
+ cookie = value.strip()
69
+ break
70
+ elif not cookie:
71
+ click.echo("Either COOKIE argument or --cookie-file option must be provided", err=True)
72
+ return
73
+ try:
74
+ serializer = current_app.session_interface.get_signing_serializer(current_app)
75
+ data = serializer.loads(cookie)
76
+ click.echo(json.dumps(data, indent=2))
77
+ except Exception as e:
78
+ click.echo("Cannot decode cookie value", err=True)
79
+ return
80
+
81
+
82
+ @cli.command("test-request")
83
+ @click.argument("path")
84
+ @click.option("--method", "-m", default="GET", help="HTTP method to use")
85
+ @click.option("--data", "-d", default=None, help="Data to send with the request")
86
+ @click.option("--header", "-h", multiple=True, help="Headers to include in the request")
87
+ @click.option("--follow-redirects/--no-follow-redirects", default=True, help="Whether to follow redirects")
88
+ @click.option("--session", "-s", help="JSON data that will be used to populate the session cookie")
89
+ @click.option("--cookie", "-c", multiple=True, help="Cookies to include in the request")
90
+ @click.option("--cookie-file", "-f", type=click.Path(), help="Path to read and write cookies from, helpful for preserving cookies between requests")
91
+ @click.pass_context
92
+ def test_request_command(ctx, path, method, data, header, follow_redirects, cookie, session, cookie_file):
93
+ """Simulate a request to the application and display the response. The path can include query parameters.
94
+ """
95
+ current_app.config["DEBUG_TB_ENABLED"] = False
96
+ client = current_app.test_client()
97
+ headers = []
98
+ cookies = {}
99
+ for header_ in header:
100
+ key, value = header_.split(":", 1)
101
+ headers.append((key.strip(), value.strip()))
102
+ if cookie_file and os.path.exists(cookie_file):
103
+ with open(cookie_file, "r") as f:
104
+ for cookie_data in f.readlines():
105
+ key, value = cookie_data.split(";", 1)[0].split("=", 1)
106
+ cookies[key.strip()] = value.strip()
107
+ client.set_cookie(key=key.strip(), value=value.strip())
108
+ if cookie:
109
+ for cookie_ in cookie:
110
+ key, value = cookie_.split("=", 1)
111
+ cookies[key.strip()] = value.strip()
112
+ client.set_cookie(key=key.strip(), value=value.strip())
113
+ if session:
114
+ serializer = current_app.session_interface.get_signing_serializer(current_app)
115
+ session_cookie = serializer.dumps(json.loads(session))
116
+ client.set_cookie(key=current_app.config["SESSION_COOKIE_NAME"], value=session_cookie)
117
+
118
+ resp = client.open(path=path, method=method, data=data, headers=headers, follow_redirects=follow_redirects)
119
+
120
+ click.echo(f"HTTP/1.1 {resp.status}")
121
+ for key, value in resp.headers.items():
122
+ click.echo(f"{key}: {value}")
123
+ if key.lower() == "set-cookie":
124
+ name, value = value.split(";", 1)[0].split("=", 1)
125
+ cookies[name.strip()] = value.strip()
126
+ click.echo("")
127
+ click.echo(resp.get_data(as_text=True))
128
+ if cookie_file and cookies:
129
+ with open(cookie_file, "w") as f:
130
+ for name, value in cookies.items():
131
+ f.write(f"{name}={value}\n")
132
+
133
+
134
+ def main():
135
+ cli.main()