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.
- {hyperflask-0.5.1 → hyperflask-0.6.0}/PKG-INFO +7 -8
- {hyperflask-0.5.1 → hyperflask-0.6.0}/README.md +5 -5
- {hyperflask-0.5.1 → hyperflask-0.6.0}/pyproject.toml +5 -3
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/__init__.py +3 -0
- hyperflask-0.6.0/src/hyperflask/actors.py +128 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/app.py +46 -26
- hyperflask-0.6.0/src/hyperflask/cli/__init__.py +135 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/cli/runner.py +81 -58
- hyperflask-0.6.0/src/hyperflask/cli/scaffold.py +32 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/cli/worker.py +5 -2
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/components/core.py +3 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/factory.py +8 -5
- hyperflask-0.5.1/src/hyperflask/model.py → hyperflask-0.6.0/src/hyperflask/models.py +2 -22
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/static/app.css +2 -0
- hyperflask-0.6.0/src/hyperflask/utils/collections.py +10 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/utils/htmx.py +1 -3
- hyperflask-0.5.1/src/hyperflask/actors.py +0 -82
- hyperflask-0.5.1/src/hyperflask/cli/__init__.py +0 -49
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/__main__.py +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/cli/reloader.py +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/components/__init__.py +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/components/alpine.py +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/components/jsx.py +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/components/webcomponent.py +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/forms/__init__.py +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/forms/fields.py +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/forms/widgets.py +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/layouts/email.mjml +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/layouts/web.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/page_helpers.py +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/security.py +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/static/alpine-csp.js +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/static/alpine.js +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/static/app.js +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/static/htmx-exts.js +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/static/react.js +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/static/stimulus.js +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/static/translations.js +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Accordion.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/AccordionPanel.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Alert.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Avatar.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/AvatarGroup.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Badge.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Breadcrumbs.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/BreadcrumbsItem.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Button.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Card.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/CardActions.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/CardTitle.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/ChatBubble.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Checkbox.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/CollapsibleSubMenu.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Countdown.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Divider.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Drawer.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/DrawerContent.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/DrawerSide.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Dropdown.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/DropdownItem.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Fieldset.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/FieldsetLegend.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/FileInput.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Filter.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/FilterItem.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Footer.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/FooterLink.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/FooterNav.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Indicator.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Input.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/InputLabel.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Join.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Kbd.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Label.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/LabeledCheckbox.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/LabeledToggle.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Link.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/List.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/ListItem.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Loading.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Menu.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/MenuItem.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/MenuTitle.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Modal.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/ModalAction.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/ModalTitle.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Navbar.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Pagination.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/PaginationNext.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/PaginationPage.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/PaginationPrev.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Progress.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/README.md +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Radio.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Range.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Rating.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Select.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Stack.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Stat.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Stats.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Step.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Steps.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/SubMenu.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Swap.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Tab.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Table.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Tabs.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Textarea.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Toast.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Toggle.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/daisyui/Tooltip.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/flash.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/forms.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/icons.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/layout.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/modal.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/skeleton.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/ui/sse.html +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/utils/__init__.py +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/utils/freezer.py +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/utils/html.py +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/utils/image.py +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/utils/markdown.py +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/utils/metadata.py +0 -0
- {hyperflask-0.5.1 → hyperflask-0.6.0}/src/hyperflask/utils/page_actions.py +0 -0
- {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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
120
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
81
|
-
|
|
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.
|
|
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.
|
|
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 .
|
|
37
|
-
from .actors import AppContextMiddleware, discover_broker,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
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,
|
|
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()
|