hyperflask 0.3.1__tar.gz → 0.4.1__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.
- {hyperflask-0.3.1 → hyperflask-0.4.1}/PKG-INFO +7 -7
- {hyperflask-0.3.1 → hyperflask-0.4.1}/README.md +0 -1
- {hyperflask-0.3.1 → hyperflask-0.4.1}/pyproject.toml +7 -8
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/__init__.py +2 -0
- hyperflask-0.4.1/src/hyperflask/actors.py +82 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/app.py +72 -20
- hyperflask-0.4.1/src/hyperflask/cli/__init__.py +53 -0
- hyperflask-0.4.1/src/hyperflask/cli/reloader.py +24 -0
- hyperflask-0.3.1/src/hyperflask/cli.py → hyperflask-0.4.1/src/hyperflask/cli/runner.py +34 -53
- hyperflask-0.4.1/src/hyperflask/cli/worker.py +90 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/components/__init__.py +4 -3
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/components/alpine.py +1 -1
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/components/core.py +48 -30
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/components/jsx.py +1 -1
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/components/webcomponent.py +1 -1
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/factory.py +6 -10
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/layouts/web.html +1 -8
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/model.py +7 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/static/htmx-exts.js +8 -5
- hyperflask-0.4.1/src/hyperflask/utils/metadata.py +182 -0
- hyperflask-0.3.1/src/hyperflask/ui/metadata.html +0 -4
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/__main__.py +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/forms/__init__.py +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/forms/fields.py +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/forms/widgets.py +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/layouts/email.mjml +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/page_helpers.py +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/security.py +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/static/alpine-csp.js +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/static/alpine.js +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/static/app.css +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/static/app.js +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/static/react.js +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/static/stimulus.js +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/static/translations.js +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Accordion.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/AccordionPanel.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Alert.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Avatar.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/AvatarGroup.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Badge.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Breadcrumbs.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/BreadcrumbsItem.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Button.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Card.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/CardActions.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/CardTitle.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/ChatBubble.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Checkbox.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/CollapsibleSubMenu.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Countdown.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Divider.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Drawer.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/DrawerContent.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/DrawerSide.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Dropdown.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/DropdownItem.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Fieldset.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/FieldsetLegend.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/FileInput.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Filter.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/FilterItem.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Footer.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/FooterLink.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/FooterNav.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Indicator.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Input.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/InputLabel.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Join.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Kbd.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Label.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/LabeledCheckbox.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/LabeledToggle.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Link.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/List.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/ListItem.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Loading.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Menu.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/MenuItem.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/MenuTitle.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Modal.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/ModalAction.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/ModalTitle.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Navbar.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Pagination.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/PaginationNext.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/PaginationPage.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/PaginationPrev.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Progress.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/README.md +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Radio.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Range.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Rating.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Select.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Stack.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Stat.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Stats.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Step.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Steps.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/SubMenu.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Swap.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Tab.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Table.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Tabs.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Textarea.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Toast.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Toggle.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/daisyui/Tooltip.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/flash.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/forms.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/icons.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/layout.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/modal.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/skeleton.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/ui/sse.html +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/utils/__init__.py +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/utils/html.py +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/utils/htmx.py +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/utils/markdown.py +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/utils/page_actions.py +0 -0
- {hyperflask-0.3.1 → hyperflask-0.4.1}/src/hyperflask/utils/tokens.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hyperflask
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: Full stack web framework
|
|
5
5
|
Author: Maxime Bouroumeau-Fuseau
|
|
6
6
|
Author-email: Maxime Bouroumeau-Fuseau <maxime.bouroumeau@gmail.com>
|
|
@@ -11,17 +11,18 @@ Requires-Dist: flask-babel-hyper~=5.1
|
|
|
11
11
|
Requires-Dist: flask-collections~=0.2
|
|
12
12
|
Requires-Dist: flask-configurator~=0.1
|
|
13
13
|
Requires-Dist: flask-debugtoolbar~=0.16
|
|
14
|
-
Requires-Dist: flask-dramatiq~=0.6
|
|
15
14
|
Requires-Dist: flask-file-routes~=0.3
|
|
16
15
|
Requires-Dist: flask-files~=0.2
|
|
17
16
|
Requires-Dist: flask-geo~=0.1
|
|
18
17
|
Requires-Dist: flask-mailman-templates~=0.1
|
|
19
|
-
Requires-Dist: flask-mercure-sse~=0.
|
|
18
|
+
Requires-Dist: flask-mercure-sse~=0.4
|
|
20
19
|
Requires-Dist: flask-sqlorm~=0.3
|
|
21
|
-
Requires-Dist: flask-super-macros~=0.
|
|
22
|
-
Requires-Dist: flask-suspense~=0.
|
|
20
|
+
Requires-Dist: flask-super-macros~=0.2
|
|
21
|
+
Requires-Dist: flask-suspense~=0.3
|
|
23
22
|
Requires-Dist: flask-wtf~=1.2
|
|
24
23
|
Requires-Dist: frozen-flask~=1.0
|
|
24
|
+
Requires-Dist: dramatiq>=1.18.0
|
|
25
|
+
Requires-Dist: dramatiq-sqlite~=0.1
|
|
25
26
|
Requires-Dist: gunicorn~=23.0
|
|
26
27
|
Requires-Dist: honcho~=2.0
|
|
27
28
|
Requires-Dist: html-sanitizer~=2.6
|
|
@@ -30,7 +31,7 @@ Requires-Dist: jinja-layout~=0.5
|
|
|
30
31
|
Requires-Dist: jinja-wtforms~=0.2
|
|
31
32
|
Requires-Dist: periodiq~=0.13
|
|
32
33
|
Requires-Dist: python-slugify~=8.0.4
|
|
33
|
-
Requires-Dist:
|
|
34
|
+
Requires-Dist: watchdog>=6.0.0
|
|
34
35
|
Requires-Python: >=3.10
|
|
35
36
|
Project-URL: Repository, https://github.com/hyperflask/hyperflask
|
|
36
37
|
Description-Content-Type: text/markdown
|
|
@@ -95,7 +96,6 @@ A good part of these extensions is developed as part of the Hyperflask project.
|
|
|
95
96
|
| [Flask-Collections](https://github.com/hyperflask/flask-collections) | Manage collections of static content
|
|
96
97
|
| [Flask-Configurator](https://github.com/hyperflask/flask-configurator) | File based configuration
|
|
97
98
|
| [Flask-DebugToolbar](https://github.com/pallets-eco/flask-debugtoolbar) | Debug Toolbar
|
|
98
|
-
| [Flask-Dramatiq](https://flask-dramatiq.readthedocs.io) | Background tasks powered by [Dramatiq](https://dramatiq.io/) |
|
|
99
99
|
| [Flask-File-Routes](https://github.com/hyperflask/flask-file-routes) | File-based routing with a new file format combining python and jinja template in a single file
|
|
100
100
|
| [Flask-Files](https://github.com/hyperflask/flask-files) | [Fsspec](https://filesystem-spec.readthedocs.io/en/latest/) based files management (upload, storage and image manipulation)
|
|
101
101
|
| [Frozen-Flask](https://github.com/Frozen-Flask/Frozen) | Generate a static website from your Flask app |
|
|
@@ -58,7 +58,6 @@ A good part of these extensions is developed as part of the Hyperflask project.
|
|
|
58
58
|
| [Flask-Collections](https://github.com/hyperflask/flask-collections) | Manage collections of static content
|
|
59
59
|
| [Flask-Configurator](https://github.com/hyperflask/flask-configurator) | File based configuration
|
|
60
60
|
| [Flask-DebugToolbar](https://github.com/pallets-eco/flask-debugtoolbar) | Debug Toolbar
|
|
61
|
-
| [Flask-Dramatiq](https://flask-dramatiq.readthedocs.io) | Background tasks powered by [Dramatiq](https://dramatiq.io/) |
|
|
62
61
|
| [Flask-File-Routes](https://github.com/hyperflask/flask-file-routes) | File-based routing with a new file format combining python and jinja template in a single file
|
|
63
62
|
| [Flask-Files](https://github.com/hyperflask/flask-files) | [Fsspec](https://filesystem-spec.readthedocs.io/en/latest/) based files management (upload, storage and image manipulation)
|
|
64
63
|
| [Frozen-Flask](https://github.com/Frozen-Flask/Frozen) | Generate a static website from your Flask app |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "hyperflask"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.1"
|
|
4
4
|
description = "Full stack web framework"
|
|
5
5
|
authors = [
|
|
6
6
|
{"name" = "Maxime Bouroumeau-Fuseau", email = "maxime.bouroumeau@gmail.com"}
|
|
@@ -15,19 +15,19 @@ dependencies = [
|
|
|
15
15
|
"flask-collections ~=0.2",
|
|
16
16
|
"flask-configurator ~=0.1",
|
|
17
17
|
"flask-debugtoolbar ~=0.16",
|
|
18
|
-
"flask-dramatiq ~=0.6",
|
|
19
18
|
"flask-file-routes ~=0.3",
|
|
20
19
|
"flask-files ~=0.2",
|
|
21
20
|
"flask-geo ~=0.1",
|
|
22
21
|
"flask-mailman-templates ~=0.1",
|
|
23
|
-
"flask-mercure-sse ~=0.
|
|
22
|
+
"flask-mercure-sse ~=0.4",
|
|
24
23
|
#"flask-observability ~=0.1"
|
|
25
24
|
"flask-sqlorm ~=0.3",
|
|
26
|
-
"flask-super-macros ~=0.
|
|
27
|
-
"flask-suspense ~=0.
|
|
25
|
+
"flask-super-macros ~=0.2",
|
|
26
|
+
"flask-suspense ~=0.3",
|
|
28
27
|
"flask-wtf ~=1.2",
|
|
29
28
|
"frozen-flask ~=1.0",
|
|
30
|
-
|
|
29
|
+
"dramatiq>=1.18.0",
|
|
30
|
+
"dramatiq-sqlite ~=0.1",
|
|
31
31
|
"gunicorn ~=23.0",
|
|
32
32
|
"honcho ~=2.0",
|
|
33
33
|
"html-sanitizer ~=2.6",
|
|
@@ -36,11 +36,10 @@ dependencies = [
|
|
|
36
36
|
"jinja-wtforms ~=0.2",
|
|
37
37
|
"periodiq ~=0.13",
|
|
38
38
|
"python-slugify ~=8.0.4",
|
|
39
|
-
"
|
|
39
|
+
"watchdog>=6.0.0",
|
|
40
40
|
]
|
|
41
41
|
|
|
42
42
|
[tool.uv.sources]
|
|
43
|
-
flask-dramatiq = { git = "https://gitlab.com/bersace/flask-dramatiq" }
|
|
44
43
|
periodiq = { git = "https://gitlab.com/bersace/periodiq" }
|
|
45
44
|
|
|
46
45
|
[project.urls]
|
|
@@ -10,8 +10,10 @@ from flask_files import save_file, validate_file
|
|
|
10
10
|
from flask_mailman_templates import send_mail
|
|
11
11
|
from flask_mercure_sse import publish_signal, mercure_publish
|
|
12
12
|
from flask_suspense import render_template, stream_template, defer
|
|
13
|
+
from flask_assets_pipeline import asset_url, static_url
|
|
13
14
|
|
|
14
15
|
# useful exports from 3rd party libs
|
|
15
16
|
from htmx_flask import request, make_response
|
|
16
17
|
from slugify import slugify
|
|
17
18
|
from blinker import signal
|
|
19
|
+
from periodiq import cron
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from threading import local
|
|
2
|
+
from dramatiq import Middleware, actor
|
|
3
|
+
import importlib
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
DRAMATIQ_BROKERS = {
|
|
7
|
+
"amqp": "dramatiq.brokers.rabbitmq:RabbitmqBroker",
|
|
8
|
+
"redis": "dramatiq.brokers.redis:RedisBroker",
|
|
9
|
+
"sqlite": "dramatiq_sqlite:SQLiteBroker",
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def discover_broker(broker_cls=None, broker_url=None):
|
|
14
|
+
if not broker_url and broker_cls and "://" in broker_cls:
|
|
15
|
+
broker_url = broker_cls
|
|
16
|
+
broker_cls = None
|
|
17
|
+
if not broker_cls and broker_url:
|
|
18
|
+
if "://" not in broker_url:
|
|
19
|
+
broker_name = "sqlite"
|
|
20
|
+
else:
|
|
21
|
+
broker_name, broker_url = broker_url.split("://", 1)
|
|
22
|
+
if broker_name not in DRAMATIQ_BROKERS:
|
|
23
|
+
raise ValueError(f"Unknown dramatiq broker '{broker_name}'")
|
|
24
|
+
broker_cls = DRAMATIQ_BROKERS[broker_name]
|
|
25
|
+
if isinstance(broker_cls, str):
|
|
26
|
+
module_name, cls_name = broker_cls.split(":")
|
|
27
|
+
module = importlib.import_module(module_name)
|
|
28
|
+
broker_cls = getattr(module, cls_name)
|
|
29
|
+
return broker_cls, broker_url
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def make_actor_decorator(app):
|
|
33
|
+
def actor_decorator(**kw):
|
|
34
|
+
def decorator(fn):
|
|
35
|
+
if getattr(app, 'dramatiq_broker', None):
|
|
36
|
+
kw['broker'] = app.dramatiq_broker
|
|
37
|
+
return actor(**kw)(fn)
|
|
38
|
+
return UndefinedBrokerActor(fn)
|
|
39
|
+
return decorator
|
|
40
|
+
return actor_decorator
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class AppContextMiddleware(Middleware):
|
|
44
|
+
# Setup Flask app for actor. Borrowed from
|
|
45
|
+
# https://github.com/Bogdanp/flask_dramatiq_example.
|
|
46
|
+
|
|
47
|
+
state = local()
|
|
48
|
+
|
|
49
|
+
def __init__(self, app):
|
|
50
|
+
self.app = app
|
|
51
|
+
|
|
52
|
+
def before_process_message(self, broker, message):
|
|
53
|
+
context = self.app.app_context()
|
|
54
|
+
context.push()
|
|
55
|
+
|
|
56
|
+
self.state.context = context
|
|
57
|
+
|
|
58
|
+
def after_process_message(
|
|
59
|
+
self, broker, message, *, result=None, exception=None):
|
|
60
|
+
try:
|
|
61
|
+
context = self.state.context
|
|
62
|
+
context.pop(exception)
|
|
63
|
+
del self.state.context
|
|
64
|
+
except AttributeError:
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
after_skip_message = after_process_message
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class UndefinedBrokerActor:
|
|
71
|
+
def __init__(self, fn):
|
|
72
|
+
self.fn = fn
|
|
73
|
+
self.__name__ = fn.__name__
|
|
74
|
+
|
|
75
|
+
def __call__(self, *args, **kwargs):
|
|
76
|
+
return self.fn(*args, **kwargs)
|
|
77
|
+
|
|
78
|
+
def send(self, *args, **kwargs):
|
|
79
|
+
raise RuntimeError("No broker defined for Dramatiq actors.")
|
|
80
|
+
|
|
81
|
+
def send_with_options(self, *args, **kwargs):
|
|
82
|
+
raise RuntimeError("No broker defined for Dramatiq actors.")
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# flask
|
|
2
|
-
from flask import Flask
|
|
2
|
+
from flask import Flask, has_request_context, request
|
|
3
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
|
|
7
7
|
from jinja_wtforms import WtformExtension
|
|
8
|
-
from flask_assets_pipeline import AssetsPipeline
|
|
8
|
+
from flask_assets_pipeline import AssetsPipeline, asset_url, static_url
|
|
9
9
|
from flask_super_macros import SuperMacros
|
|
10
10
|
from flask_collections import Collections
|
|
11
11
|
from flask_file_routes import FileRoutes, ModuleView, page
|
|
@@ -16,14 +16,13 @@ from flask_files import Files
|
|
|
16
16
|
from flask_sqlorm import FlaskSQLORM
|
|
17
17
|
from flask_mercure_sse import MercureSSE
|
|
18
18
|
from flask_mailman_templates import MailTemplates
|
|
19
|
-
from flask_suspense import Suspense, render_template, suspense_before_render_template,
|
|
19
|
+
from flask_suspense import Suspense, render_template, suspense_before_render_template, suspense_before_render_macros
|
|
20
20
|
#from flask_observability import Observability
|
|
21
21
|
# 3rd party extensions
|
|
22
22
|
from flask_wtf.csrf import CSRFProtect
|
|
23
23
|
from flask_frozen import Freezer
|
|
24
24
|
from htmx_flask import Htmx
|
|
25
25
|
from flask_mailman import Mail
|
|
26
|
-
from flask_dramatiq import Dramatiq
|
|
27
26
|
from flask_debugtoolbar import DebugToolbarExtension
|
|
28
27
|
# from hyperflask
|
|
29
28
|
from .forms import Form
|
|
@@ -31,8 +30,10 @@ from .utils.page_actions import page_action_url
|
|
|
31
30
|
from .utils.htmx import htmx_oob, respond_with_flash_messages
|
|
32
31
|
from .utils.markdown import jinja_markdown, MarkdownExtension
|
|
33
32
|
from .utils.html import sanitize_html, nl2br
|
|
33
|
+
from .utils.metadata import metadata_tags
|
|
34
34
|
from .components import register_components
|
|
35
|
-
from .model import Model, File as SQLFileType
|
|
35
|
+
from .model import Model, File as SQLFileType, UndefinedDatabase
|
|
36
|
+
from .actors import AppContextMiddleware, discover_broker, make_actor_decorator
|
|
36
37
|
from .security import respond_with_security_headers, csp_nonce
|
|
37
38
|
from . import page_helpers
|
|
38
39
|
# others
|
|
@@ -40,6 +41,7 @@ from jinja_super_macros.registry import FileLoader
|
|
|
40
41
|
from jinja_wtforms.extractor import map_jinja_call_node_to_func
|
|
41
42
|
from sqlorm.sql_template import SQLTemplate
|
|
42
43
|
from periodiq import PeriodiqMiddleware
|
|
44
|
+
import dramatiq
|
|
43
45
|
import os
|
|
44
46
|
|
|
45
47
|
|
|
@@ -55,7 +57,7 @@ class Hyperflask(Flask):
|
|
|
55
57
|
|
|
56
58
|
def __init__(self, *args, static_mode="hybrid", instrument=False, layouts_folder="layouts",
|
|
57
59
|
emails_folder="emails", forms_folder="forms", assets_folder="assets", pages_folder="pages", migrations_folder="migrations",
|
|
58
|
-
config=None, config_filename="config.yml", database_uri=
|
|
60
|
+
config=None, config_filename="config.yml", database_uri=None, proxy_fix=True, **kwargs):
|
|
59
61
|
|
|
60
62
|
super().__init__(*args, **kwargs)
|
|
61
63
|
if proxy_fix:
|
|
@@ -66,11 +68,12 @@ class Hyperflask(Flask):
|
|
|
66
68
|
"FREEZER_STATIC_IGNORE": [],
|
|
67
69
|
"WTF_CSRF_CHECK_DEFAULT": False,
|
|
68
70
|
"OTEL_INSTRUMENT": instrument,
|
|
69
|
-
"DRAMATIQ_BROKER": "dramatiq.brokers.redis:RedisBroker",
|
|
70
71
|
"DEBUG_TB_INTERCEPT_REDIRECTS": False,
|
|
71
72
|
"SESSION_COOKIE_HTTPONLY": True,
|
|
72
73
|
"SESSION_COOKIE_SAMESITE": "Lax",
|
|
74
|
+
"SQLORM_URI": database_uri,
|
|
73
75
|
# Hyperflask specific
|
|
76
|
+
"URL_FOR_SERVER_NAME": None,
|
|
74
77
|
"LAYOUT": "layouts/default.html",
|
|
75
78
|
"ALPINE": False,
|
|
76
79
|
"STATIC_MODE": static_mode,
|
|
@@ -111,13 +114,16 @@ class Hyperflask(Flask):
|
|
|
111
114
|
})
|
|
112
115
|
self.config.setdefault("HSTS_HEADER", "max-age=31556926; includeSubDomains") # 1 year
|
|
113
116
|
|
|
117
|
+
if (self.config['URL_FOR_SERVER_NAME'] or self.config.get('SERVER_NAME')) and not self.config.get('MERCURE_HUB_URL') and not self.config.get('MERCURE_PUBLIC_HUB_URL'):
|
|
118
|
+
self.config['MERCURE_PUBLIC_HUB_URL'] = True
|
|
119
|
+
|
|
114
120
|
#self.otel = Observability(self)
|
|
115
121
|
|
|
116
122
|
self.jinja_env.add_extension(LayoutExtension)
|
|
117
123
|
self.jinja_env.add_extension(WtformExtension)
|
|
118
124
|
self.jinja_env.add_extension(MarkdownExtension)
|
|
119
125
|
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)
|
|
126
|
+
self.jinja_env.globals.update(page_action_url=page_action_url, app=self, csp_nonce=csp_nonce, metadata_tags=metadata_tags)
|
|
121
127
|
self.jinja_env.form_base_cls = Form
|
|
122
128
|
self.jinja_env.default_layout = self.config["LAYOUT"]
|
|
123
129
|
self.forms = self.jinja_env.forms
|
|
@@ -133,14 +139,11 @@ class Hyperflask(Flask):
|
|
|
133
139
|
])
|
|
134
140
|
|
|
135
141
|
DebugToolbarExtension(self)
|
|
136
|
-
self.freezer = Freezer(self)
|
|
142
|
+
self.freezer = Freezer(self, with_no_argument_rules=False)
|
|
143
|
+
self.freezer.register_generator(lambda: freezer_url_generator(self))
|
|
137
144
|
self.csrf = CSRFProtect(self)
|
|
138
|
-
#self.talisman = Talisman(self)
|
|
139
145
|
Htmx(self)
|
|
140
146
|
Mail(self)
|
|
141
|
-
self.dramatiq = Dramatiq(self)
|
|
142
|
-
self.dramatiq.broker.add_middleware(PeriodiqMiddleware())
|
|
143
|
-
self.actor = self.dramatiq.actor
|
|
144
147
|
|
|
145
148
|
SuperMacros(self)
|
|
146
149
|
self.components = self.macros # alias
|
|
@@ -205,10 +208,20 @@ class Hyperflask(Flask):
|
|
|
205
208
|
|
|
206
209
|
self.collections.register_freezer_generator(self.freezer)
|
|
207
210
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
211
|
+
if self.config.get('SQLORM_URI'):
|
|
212
|
+
SQLTemplate.eval_globals.update(app=self)
|
|
213
|
+
self.db = FlaskSQLORM(self, migrations_folder=migrations_folder)
|
|
214
|
+
self.db.Model = Model
|
|
215
|
+
self.db.File = SQLFileType
|
|
216
|
+
else:
|
|
217
|
+
self.db = UndefinedDatabase()
|
|
218
|
+
|
|
219
|
+
broker_cls, broker_url = discover_broker(self.config.get('DRAMATIQ_BROKER'), self.config.get('DRAMATIQ_BROKER_URL'))
|
|
220
|
+
if broker_cls:
|
|
221
|
+
self.dramatiq_broker = broker_cls(url=broker_url)
|
|
222
|
+
self.dramatiq_broker.add_middleware(AppContextMiddleware(self))
|
|
223
|
+
self.dramatiq_broker.add_middleware(PeriodiqMiddleware())
|
|
224
|
+
self.actor = make_actor_decorator(self)
|
|
212
225
|
|
|
213
226
|
Suspense(self, nonce_getter="csp_nonce()")
|
|
214
227
|
|
|
@@ -216,9 +229,10 @@ class Hyperflask(Flask):
|
|
|
216
229
|
def on_suspense_before_render_template(sender, **kwargs):
|
|
217
230
|
csp_nonce() # we will need a nonce but the first call will be after the template started rendering
|
|
218
231
|
|
|
219
|
-
@
|
|
220
|
-
def
|
|
221
|
-
return
|
|
232
|
+
@suspense_before_render_macros.connect_via(self, weak=False)
|
|
233
|
+
def on_suspense_before_render_macros(sender, **kwargs):
|
|
234
|
+
return ""
|
|
235
|
+
return f'<script nonce="{csp_nonce()}">htmx.bootstrap()</script>' # waiting on PR 3441
|
|
222
236
|
|
|
223
237
|
@self.errorhandler(404)
|
|
224
238
|
def not_found(error):
|
|
@@ -237,8 +251,38 @@ class Hyperflask(Flask):
|
|
|
237
251
|
def relative_import_name(self, name):
|
|
238
252
|
return f"{self.import_name}.{name}" if self.import_name != "__main__" else name
|
|
239
253
|
|
|
254
|
+
@property
|
|
255
|
+
def base_url(self):
|
|
256
|
+
scheme = self.config.get('PREFERRED_URL_SCHEME', 'http')
|
|
257
|
+
if self.config.get('SERVER_NAME'):
|
|
258
|
+
return f"{scheme}://{self.config['SERVER_NAME']}"
|
|
259
|
+
if self.config['URL_FOR_SERVER_NAME']:
|
|
260
|
+
return f"{scheme}://{self.config['URL_FOR_SERVER_NAME']}"
|
|
261
|
+
if has_request_context():
|
|
262
|
+
return request.host_url
|
|
263
|
+
raise RuntimeError("No request context and no SERVER_NAME or URL_FOR_SERVER_NAME configured, cannot determine base_url")
|
|
264
|
+
|
|
265
|
+
def url_for(self, *args, **kwargs):
|
|
266
|
+
_external = kwargs.get("_external", False)
|
|
267
|
+
_scheme = kwargs.get("_scheme", None)
|
|
268
|
+
if _external and self.config['URL_FOR_SERVER_NAME']:
|
|
269
|
+
# we handle the host and scheme ourselves
|
|
270
|
+
kwargs['_external'] = False
|
|
271
|
+
kwargs.pop('_scheme', None)
|
|
272
|
+
else:
|
|
273
|
+
_external = False
|
|
274
|
+
url = super().url_for(*args, **kwargs)
|
|
275
|
+
if not _external:
|
|
276
|
+
return url
|
|
277
|
+
if not _scheme:
|
|
278
|
+
_scheme = self.config.get('PREFERRED_URL_SCHEME', 'http')
|
|
279
|
+
url = f"{_scheme}://{self.config['URL_FOR_SERVER_NAME']}{url}"
|
|
280
|
+
return url
|
|
281
|
+
|
|
240
282
|
|
|
241
283
|
class HyperModuleView(ModuleView):
|
|
284
|
+
module_globals = dict(ModuleView.module_globals, asset_url=asset_url, static_url=static_url)
|
|
285
|
+
|
|
242
286
|
def _render_template(self):
|
|
243
287
|
return render_template(page.template)
|
|
244
288
|
|
|
@@ -251,3 +295,11 @@ def dynamic(func=None, static_template=None):
|
|
|
251
295
|
def decorator(func):
|
|
252
296
|
pass
|
|
253
297
|
return decorator(func) if func else decorator
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def freezer_url_generator(app):
|
|
301
|
+
"""URL generator for URL rules that take no arguments."""
|
|
302
|
+
for rule in app.url_map.iter_rules():
|
|
303
|
+
if not rule.arguments and 'GET' in rule.methods and not rule.endpoint.startswith('mercure'):
|
|
304
|
+
app.logger.info(rule.endpoint)
|
|
305
|
+
yield rule.endpoint, {}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from flask import current_app
|
|
2
|
+
from flask.cli import FlaskGroup, shell_command, routes_command, ScriptInfo, NoAppException
|
|
3
|
+
from flask_assets_pipeline import cli as assets_cli
|
|
4
|
+
import click
|
|
5
|
+
from ..factory import create_app
|
|
6
|
+
from ..security import generate_csp_policy
|
|
7
|
+
from .runner import serve_command, run_command, dev_command
|
|
8
|
+
from .worker import worker_command, scheduler_command
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _create_app():
|
|
12
|
+
info = ScriptInfo()
|
|
13
|
+
try:
|
|
14
|
+
return info.load_app()
|
|
15
|
+
except NoAppException:
|
|
16
|
+
return create_app()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
cli = FlaskGroup(name='kantree', help='Kantree', add_default_commands=False, create_app=_create_app)
|
|
20
|
+
cli.add_command(shell_command)
|
|
21
|
+
cli.add_command(routes_command)
|
|
22
|
+
cli.add_command(serve_command)
|
|
23
|
+
cli.add_command(run_command)
|
|
24
|
+
cli.add_command(dev_command)
|
|
25
|
+
cli.add_command(worker_command)
|
|
26
|
+
cli.add_command(scheduler_command)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@cli.command("init")
|
|
30
|
+
def init_command():
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@cli.command("freeze")
|
|
35
|
+
def freeze_command():
|
|
36
|
+
current_app.freezer.freeze()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@cli.command("build")
|
|
40
|
+
@click.pass_context
|
|
41
|
+
def build_command(ctx):
|
|
42
|
+
ctx.invoke(assets_cli.build)
|
|
43
|
+
ctx.invoke(freeze_command)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@cli.command("csp-header")
|
|
47
|
+
def csp_header_command():
|
|
48
|
+
click.echo(generate_csp_policy())
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def main():
|
|
53
|
+
cli.main()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from werkzeug._reloader import WatchdogReloaderLoop, reloader_loops
|
|
2
|
+
import os.path
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
# watchdog is a dependency of hyperflask so it would always be selected as the reloader
|
|
6
|
+
class WatchdogWithExtraFilesPatternsReloaderLoop(WatchdogReloaderLoop):
|
|
7
|
+
def __init__(self, *args, extra_files=None, **kwargs):
|
|
8
|
+
# VERY HACKY SOLUTION !
|
|
9
|
+
# in werkzeug reloader, it is not possible to add patterns as extra_files
|
|
10
|
+
# to circumvent this and avoid copy/pasting big chunk of code, we patch os.path.abspath
|
|
11
|
+
# which is used to process the list of extra_files in the __init__
|
|
12
|
+
extra_files = list(extra_files) if extra_files else []
|
|
13
|
+
extra_files.append("*.jpy")
|
|
14
|
+
abspath = os.path.abspath
|
|
15
|
+
def patched_abspath(path):
|
|
16
|
+
if path.startswith("*."):
|
|
17
|
+
return path
|
|
18
|
+
return abspath(path)
|
|
19
|
+
os.path.abspath = patched_abspath
|
|
20
|
+
super().__init__(*args, extra_files=extra_files, **kwargs)
|
|
21
|
+
os.path.abspath = abspath
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
reloader_loops["auto"] = WatchdogWithExtraFilesPatternsReloaderLoop
|
|
@@ -1,48 +1,13 @@
|
|
|
1
|
-
from flask import
|
|
2
|
-
from flask.cli import FlaskGroup, shell_command, routes_command, _debug_option, pass_script_info, run_command as flask_run_command, ScriptInfo, NoAppException
|
|
3
|
-
from flask_assets_pipeline import cli as assets_cli
|
|
1
|
+
from flask.cli import _debug_option, pass_script_info, run_command as flask_run_command
|
|
4
2
|
import click
|
|
5
3
|
import os
|
|
6
4
|
import sys
|
|
7
5
|
import multiprocessing
|
|
8
6
|
import gunicorn.app.base
|
|
9
|
-
from
|
|
10
|
-
from .
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def _create_app():
|
|
14
|
-
info = ScriptInfo()
|
|
15
|
-
try:
|
|
16
|
-
return info.load_app()
|
|
17
|
-
except NoAppException:
|
|
18
|
-
return create_app()
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
cli = FlaskGroup(name='kantree', help='Kantree', add_default_commands=False, create_app=_create_app)
|
|
22
|
-
cli.add_command(shell_command)
|
|
23
|
-
cli.add_command(routes_command)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
@cli.command("init")
|
|
27
|
-
def init_command():
|
|
28
|
-
pass
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
@cli.command("freeze")
|
|
32
|
-
def freeze_command():
|
|
33
|
-
current_app.freezer.freeze()
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
@cli.command("build")
|
|
37
|
-
@click.pass_context
|
|
38
|
-
def build_command(ctx):
|
|
39
|
-
ctx.invoke(assets_cli.build)
|
|
40
|
-
ctx.invoke(freeze_command)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
@cli.command("csp-header")
|
|
44
|
-
def csp_header_command():
|
|
45
|
-
click.echo(generate_csp_policy())
|
|
7
|
+
from subprocess import Popen
|
|
8
|
+
from honcho.manager import Manager as BaseProcessManager
|
|
9
|
+
from ..factory import load_config
|
|
10
|
+
from . import reloader # import hacky patch of werkzeug reloader
|
|
46
11
|
|
|
47
12
|
|
|
48
13
|
@click.command("serve")
|
|
@@ -57,8 +22,6 @@ def serve_command(ctx, info, host, port, gunicorn_opt, **run_kwargs):
|
|
|
57
22
|
'proc_name': 'hyperflask',
|
|
58
23
|
'bind': f'{host}:{port}',
|
|
59
24
|
'workers': 1 + multiprocessing.cpu_count() * 2,
|
|
60
|
-
'accesslog': '-',
|
|
61
|
-
'errorlog': '-',
|
|
62
25
|
'max_requests': 1000,
|
|
63
26
|
'max_requests_jitter': 100
|
|
64
27
|
}
|
|
@@ -69,7 +32,6 @@ def serve_command(ctx, info, host, port, gunicorn_opt, **run_kwargs):
|
|
|
69
32
|
|
|
70
33
|
|
|
71
34
|
serve_command.params = flask_run_command.params + serve_command.params
|
|
72
|
-
cli.add_command(serve_command)
|
|
73
35
|
|
|
74
36
|
|
|
75
37
|
@click.command("run")
|
|
@@ -78,16 +40,20 @@ cli.add_command(serve_command)
|
|
|
78
40
|
@click.option("--port", "-p", default=5000, help="The port to bind to.")
|
|
79
41
|
@click.option('--concurrency')
|
|
80
42
|
@click.option("--extend-procfile")
|
|
43
|
+
@click.option("--init-db/--no-init-db", is_flag=True, default=None)
|
|
81
44
|
@click.option("--dev", is_flag=True)
|
|
82
|
-
def run_command(processes, host, port, concurrency, extend_procfile, dev):
|
|
45
|
+
def run_command(processes, host, port, concurrency, extend_procfile, init_db, dev):
|
|
83
46
|
from honcho.command import _procfile, _parse_concurrency
|
|
84
|
-
from honcho.manager import Manager
|
|
85
47
|
from honcho.printer import Printer
|
|
86
48
|
from honcho import environ
|
|
87
49
|
|
|
88
50
|
if dev:
|
|
89
51
|
os.environ["FLASK_DEBUG"] = "1"
|
|
90
52
|
|
|
53
|
+
if init_db is None and dev or init_db:
|
|
54
|
+
print("Initializing database...")
|
|
55
|
+
Popen([sys.argv[0], "db", "init"]).wait()
|
|
56
|
+
|
|
91
57
|
_processes = None
|
|
92
58
|
config = load_config()
|
|
93
59
|
for filename in [f"Procfile.{config['ENV']}", "Procfile"]:
|
|
@@ -99,18 +65,29 @@ def run_command(processes, host, port, concurrency, extend_procfile, dev):
|
|
|
99
65
|
_processes = {}
|
|
100
66
|
_processes.update({
|
|
101
67
|
"web": [sys.argv[0], "serve", "--host", host, "--port", "$PORT"],
|
|
102
|
-
|
|
103
|
-
|
|
68
|
+
"worker": [sys.argv[0], "worker"],
|
|
69
|
+
"scheduler": [sys.argv[0], "scheduler"]
|
|
104
70
|
})
|
|
105
71
|
if dev:
|
|
106
72
|
_processes["assets"] = [sys.argv[0], "assets", "dev"]
|
|
73
|
+
_processes["worker"].extend(["-p", "1", "-t", "1"])
|
|
107
74
|
else:
|
|
108
75
|
_processes["mercurehub"] = [sys.executable, "-m", "flask_mercure_sse.server"]
|
|
76
|
+
subscriber_secret_key = config.get("MERCURE_SUBSCRIBER_SECRET_KEY", config.get('SECRET_KEY'))
|
|
77
|
+
if subscriber_secret_key:
|
|
78
|
+
_processes["mercurehub"].extend(["--subscriber-secret", subscriber_secret_key])
|
|
79
|
+
else:
|
|
80
|
+
print("Warning: No MERCURE_SUBSCRIBER_SECRET_KEY or SECRET_KEY set, Mercure subscriber will not be authenticated.")
|
|
81
|
+
publisher_secret_key = config.get("MERCURE_PUBLISHER_SECRET_KEY", config.get('SECRET_KEY'))
|
|
82
|
+
if publisher_secret_key:
|
|
83
|
+
_processes["mercurehub"].extend(["--publisher-secret", publisher_secret_key])
|
|
84
|
+
else:
|
|
85
|
+
print("Warning: No MERCURE_PUBLISHER_SECRET_KEY or SECRET_KEY set, cannot publish Mercure messages.")
|
|
109
86
|
|
|
110
87
|
processes = {name: " ".join(_processes[name]) if isinstance(_processes[name], list) else _processes[name]
|
|
111
|
-
for name in _processes if name in
|
|
88
|
+
for name in _processes if not processes or name in processes}
|
|
112
89
|
|
|
113
|
-
manager =
|
|
90
|
+
manager = ProcessManager(Printer(sys.stdout, colour=True))
|
|
114
91
|
|
|
115
92
|
for p in environ.expand_processes(processes,
|
|
116
93
|
concurrency=_parse_concurrency(concurrency),
|
|
@@ -119,12 +96,12 @@ def run_command(processes, host, port, concurrency, extend_procfile, dev):
|
|
|
119
96
|
e.update(p.env)
|
|
120
97
|
manager.add_process(p.name, p.cmd, quiet=p.quiet, env=e)
|
|
121
98
|
|
|
99
|
+
print("Starting Hyperflask processes...")
|
|
122
100
|
manager.loop()
|
|
123
101
|
sys.exit(manager.returncode)
|
|
124
102
|
|
|
125
103
|
|
|
126
104
|
run_command.params.insert(0, _debug_option)
|
|
127
|
-
cli.add_command(run_command)
|
|
128
105
|
|
|
129
106
|
|
|
130
107
|
@click.command("dev")
|
|
@@ -134,11 +111,15 @@ def dev_command(ctx, **kwargs):
|
|
|
134
111
|
|
|
135
112
|
|
|
136
113
|
dev_command.params = list(run_command.params)
|
|
137
|
-
cli.add_command(dev_command)
|
|
138
114
|
|
|
139
115
|
|
|
140
|
-
|
|
141
|
-
|
|
116
|
+
class ProcessManager(BaseProcessManager):
|
|
117
|
+
# This override prevents the manager to stop if the worker and scheduler exit because dramatiq is unused
|
|
118
|
+
def _any_stopped(self):
|
|
119
|
+
def has_exited_because_dramatiq_unavailable(name, p):
|
|
120
|
+
return name.split('.')[0] in ('scheduler', 'worker') and p.get('returncode') == 10
|
|
121
|
+
return any(not has_exited_because_dramatiq_unavailable(name, p) and p.get('returncode') is not None
|
|
122
|
+
for name, p in self._processes.items())
|
|
142
123
|
|
|
143
124
|
|
|
144
125
|
class GunicornServer(gunicorn.app.base.BaseApplication):
|