hyperflask 0.5.1__tar.gz → 0.6.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 (126) hide show
  1. {hyperflask-0.5.1 → hyperflask-0.6.0}/PKG-INFO +7 -8
  2. {hyperflask-0.5.1 → hyperflask-0.6.0}/README.md +5 -5
  3. {hyperflask-0.5.1 → hyperflask-0.6.0}/pyproject.toml +5 -3
  4. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/__init__.py +3 -0
  5. hyperflask-0.6.0/src/hyperflask/actors.py +128 -0
  6. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/app.py +46 -26
  7. hyperflask-0.6.0/src/hyperflask/cli/__init__.py +135 -0
  8. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/cli/runner.py +81 -58
  9. hyperflask-0.6.0/src/hyperflask/cli/scaffold.py +32 -0
  10. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/cli/worker.py +5 -2
  11. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/components/core.py +3 -0
  12. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/factory.py +8 -5
  13. hyperflask-0.5.1/src/hyperflask/model.py → hyperflask-0.6.0/src/hyperflask/models.py +2 -22
  14. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/static/app.css +2 -0
  15. hyperflask-0.6.0/src/hyperflask/utils/collections.py +10 -0
  16. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/utils/htmx.py +1 -3
  17. hyperflask-0.5.1/src/hyperflask/actors.py +0 -82
  18. hyperflask-0.5.1/src/hyperflask/cli/__init__.py +0 -49
  19. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/__main__.py +0 -0
  20. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/cli/reloader.py +0 -0
  21. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/components/__init__.py +0 -0
  22. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/components/alpine.py +0 -0
  23. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/components/jsx.py +0 -0
  24. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/components/webcomponent.py +0 -0
  25. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/forms/__init__.py +0 -0
  26. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/forms/fields.py +0 -0
  27. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/forms/widgets.py +0 -0
  28. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/layouts/email.mjml +0 -0
  29. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/layouts/web.html +0 -0
  30. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/page_helpers.py +0 -0
  31. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/security.py +0 -0
  32. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/static/alpine-csp.js +0 -0
  33. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/static/alpine.js +0 -0
  34. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/static/app.js +0 -0
  35. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/static/htmx-exts.js +0 -0
  36. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/static/react.js +0 -0
  37. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/static/stimulus.js +0 -0
  38. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/static/translations.js +0 -0
  39. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Accordion.html +0 -0
  40. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/AccordionPanel.html +0 -0
  41. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Alert.html +0 -0
  42. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Avatar.html +0 -0
  43. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/AvatarGroup.html +0 -0
  44. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Badge.html +0 -0
  45. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Breadcrumbs.html +0 -0
  46. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/BreadcrumbsItem.html +0 -0
  47. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Button.html +0 -0
  48. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Card.html +0 -0
  49. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/CardActions.html +0 -0
  50. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/CardTitle.html +0 -0
  51. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/ChatBubble.html +0 -0
  52. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Checkbox.html +0 -0
  53. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/CollapsibleSubMenu.html +0 -0
  54. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Countdown.html +0 -0
  55. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Divider.html +0 -0
  56. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Drawer.html +0 -0
  57. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/DrawerContent.html +0 -0
  58. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/DrawerSide.html +0 -0
  59. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Dropdown.html +0 -0
  60. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/DropdownItem.html +0 -0
  61. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Fieldset.html +0 -0
  62. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/FieldsetLegend.html +0 -0
  63. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/FileInput.html +0 -0
  64. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Filter.html +0 -0
  65. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/FilterItem.html +0 -0
  66. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Footer.html +0 -0
  67. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/FooterLink.html +0 -0
  68. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/FooterNav.html +0 -0
  69. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Indicator.html +0 -0
  70. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Input.html +0 -0
  71. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/InputLabel.html +0 -0
  72. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Join.html +0 -0
  73. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Kbd.html +0 -0
  74. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Label.html +0 -0
  75. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/LabeledCheckbox.html +0 -0
  76. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/LabeledToggle.html +0 -0
  77. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Link.html +0 -0
  78. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/List.html +0 -0
  79. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/ListItem.html +0 -0
  80. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Loading.html +0 -0
  81. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Menu.html +0 -0
  82. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/MenuItem.html +0 -0
  83. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/MenuTitle.html +0 -0
  84. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Modal.html +0 -0
  85. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/ModalAction.html +0 -0
  86. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/ModalTitle.html +0 -0
  87. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Navbar.html +0 -0
  88. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Pagination.html +0 -0
  89. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/PaginationNext.html +0 -0
  90. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/PaginationPage.html +0 -0
  91. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/PaginationPrev.html +0 -0
  92. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Progress.html +0 -0
  93. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/README.md +0 -0
  94. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Radio.html +0 -0
  95. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Range.html +0 -0
  96. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Rating.html +0 -0
  97. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Select.html +0 -0
  98. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Stack.html +0 -0
  99. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Stat.html +0 -0
  100. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Stats.html +0 -0
  101. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Step.html +0 -0
  102. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Steps.html +0 -0
  103. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/SubMenu.html +0 -0
  104. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Swap.html +0 -0
  105. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Tab.html +0 -0
  106. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Table.html +0 -0
  107. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Tabs.html +0 -0
  108. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Textarea.html +0 -0
  109. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Toast.html +0 -0
  110. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Toggle.html +0 -0
  111. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Tooltip.html +0 -0
  112. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/flash.html +0 -0
  113. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/forms.html +0 -0
  114. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/icons.html +0 -0
  115. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/layout.html +0 -0
  116. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/modal.html +0 -0
  117. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/skeleton.html +0 -0
  118. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/sse.html +0 -0
  119. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/utils/__init__.py +0 -0
  120. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/utils/freezer.py +0 -0
  121. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/utils/html.py +0 -0
  122. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/utils/image.py +0 -0
  123. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/utils/markdown.py +0 -0
  124. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/utils/metadata.py +0 -0
  125. {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/utils/page_actions.py +0 -0
  126. {hyperflask-0.5.1 → hyperflask-0.6.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.1
3
+ Version: 0.6.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 htmx-ext-sse 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 htmx-ext-sse 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.1"
3
+ version = "0.6.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,7 +12,6 @@ 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
@@ -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
@@ -57,7 +58,7 @@ class Hyperflask(Flask):
57
58
 
58
59
  def __init__(self, *args, static_mode=StaticMode.DYNAMIC, instrument=False, layouts_folder="layouts",
59
60
  emails_folder="emails", forms_folder="forms", assets_folder="assets", pages_folder="pages", migrations_folder="migrations",
60
- config=None, config_filename="config.yml", database_uri=None, proxy_fix=True, **kwargs):
61
+ config=None, config_filename="config.yml", database_uri=None, proxy_fix=True, healthcheck_url="/healthcheck", **kwargs):
61
62
 
62
63
  super().__init__(*args, **kwargs)
63
64
  if proxy_fix:
@@ -77,7 +78,6 @@ 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,
@@ -99,6 +99,7 @@ class Hyperflask(Flask):
99
99
  "HSTS_HEADER": False,
100
100
  "IMAGES_DEFAULT_LOADING": "lazy",
101
101
  "IMAGES_DEFAULT_DECODING": "async",
102
+ "HEALTHCHECK_URL": healthcheck_url,
102
103
  })
103
104
  if config:
104
105
  self.config.update(config)
@@ -150,7 +151,7 @@ class Hyperflask(Flask):
150
151
  PrefixLoader({"ui": FileSystemLoader(os.path.join(os.path.dirname(__file__), "ui"))})
151
152
  ])
152
153
 
153
- DebugToolbarExtension(self)
154
+ HyperDebugToolbarExtension(self)
154
155
  self.freezer = Freezer(self, with_no_argument_rules=False, log_url_for=self.config.get('FREEZER_LOG_URL_FOR', False))
155
156
  self.freezer.register_generator(lambda: freezer_url_generator(self))
156
157
  self.csrf = CSRFProtect(self)
@@ -160,7 +161,6 @@ class Hyperflask(Flask):
160
161
  SuperMacros(self)
161
162
  self.components = self.macros # alias
162
163
  Babel(self, extract_locale_from_request="locale")
163
- Geolocation(self)
164
164
  Files(self)
165
165
  MailTemplates(self, template_folder=emails_folder)
166
166
  file_routes = FileRoutes(self, pages_folder=pages_folder, module_view_class=HyperModuleView)
@@ -234,7 +234,9 @@ class Hyperflask(Flask):
234
234
  self.dramatiq_broker = broker_cls(url=broker_url)
235
235
  self.dramatiq_broker.add_middleware(AppContextMiddleware(self))
236
236
  self.dramatiq_broker.add_middleware(PeriodiqMiddleware())
237
- self.actor = make_actor_decorator(self)
237
+ self.actor = actor.bind(self.dramatiq_broker)
238
+ else:
239
+ self.actor = actor
238
240
 
239
241
  Suspense(self, nonce_getter="csp_nonce()")
240
242
 
@@ -261,6 +263,17 @@ class Hyperflask(Flask):
261
263
  except TemplateNotFound:
262
264
  return self.make_response(("500 Internal Server Error", 500))
263
265
 
266
+ if self.config['HEALTHCHECK_URL']:
267
+ @self.route(self.config['HEALTHCHECK_URL'])
268
+ def healthcheck():
269
+ if not isinstance(self.db, UndefinedDatabase):
270
+ try:
271
+ with self.db as tx:
272
+ tx.execute("SELECT 1")
273
+ except Exception:
274
+ return {"status": "error"}, 500
275
+ return {"status": "ok"}
276
+
264
277
  def relative_import_name(self, name):
265
278
  return f"{self.import_name}.{name}" if self.import_name != "__main__" else name
266
279
 
@@ -269,32 +282,39 @@ class Hyperflask(Flask):
269
282
  scheme = self.config.get('PREFERRED_URL_SCHEME', 'http')
270
283
  if self.config.get('SERVER_NAME'):
271
284
  return f"{scheme}://{self.config['SERVER_NAME']}"
272
- if self.config['URL_FOR_SERVER_NAME']:
273
- return f"{scheme}://{self.config['URL_FOR_SERVER_NAME']}"
274
285
  if has_request_context():
275
286
  return request.host_url
276
- raise RuntimeError("No request context and no SERVER_NAME or URL_FOR_SERVER_NAME configured, cannot determine base_url")
287
+ raise RuntimeError("No request context and no SERVER_NAME configured, cannot determine base_url")
277
288
 
278
289
  def url_for(self, *args, **kwargs):
279
290
  _external = kwargs.get("_external", False)
280
- _scheme = kwargs.get("_scheme", None)
281
- if _external and self.config['URL_FOR_SERVER_NAME']:
282
- # we handle the host and scheme ourselves
283
- kwargs['_external'] = False
284
- kwargs.pop('_scheme', None)
285
- else:
286
- _external = False
287
- url = super().url_for(*args, **kwargs)
288
- if not _external:
291
+ if not has_request_context() and not _external and not self.config.get('SERVER_NAME'):
292
+ self.config['SERVER_NAME'] = 'http://localhost:5000'
293
+ app_ctx = _cv_app.get(None)
294
+ if app_ctx is not None:
295
+ prev_url_adapter = app_ctx.url_adapter
296
+ app_ctx.url_adapter = self.create_url_adapter(None)
297
+ url = super().url_for(*args, **kwargs)
298
+ if app_ctx is not None:
299
+ app_ctx.url_adapter = prev_url_adapter
300
+ url = url[:len(self.config['SERVER_NAME']):]
301
+ self.config['SERVER_NAME'] = None
289
302
  return url
290
- if not _scheme:
291
- _scheme = self.config.get('PREFERRED_URL_SCHEME', 'http')
292
- url = f"{_scheme}://{self.config['URL_FOR_SERVER_NAME']}{url}"
293
- return url
303
+ return super().url_for(*args, **kwargs)
294
304
 
295
305
 
296
306
  class HyperModuleView(ModuleView):
297
- module_globals = dict(ModuleView.module_globals, asset_url=asset_url, static_url=static_url)
307
+ module_globals = dict(ModuleView.module_globals,
308
+ asset_url=asset_url,
309
+ static_url=static_url,
310
+ transaction=transaction)
298
311
 
299
312
  def _render_template(self):
300
313
  return render_template(page.template)
314
+
315
+
316
+ class HyperDebugToolbarExtension(DebugToolbarExtension):
317
+ def _show_toolbar(self):
318
+ if current_app.config.get("DEBUG_TB_ENABLED", True) is False:
319
+ return False
320
+ 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()