hyperflask 0.1.0__py3-none-any.whl
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.
Potentially problematic release.
This version of hyperflask might be problematic. Click here for more details.
- hyperflask/__init__.py +15 -0
- hyperflask/__main__.py +3 -0
- hyperflask/api.py +57 -0
- hyperflask/app.py +176 -0
- hyperflask/cli.py +140 -0
- hyperflask/components/__init__.py +54 -0
- hyperflask/components/alpine.py +16 -0
- hyperflask/components/core.py +60 -0
- hyperflask/components/webcomponent.py +23 -0
- hyperflask/factory.py +173 -0
- hyperflask/forms/__init__.py +51 -0
- hyperflask/forms/fields.py +17 -0
- hyperflask/forms/widgets.py +19 -0
- hyperflask/layout.html +28 -0
- hyperflask/page_helpers.py +36 -0
- hyperflask/reactivity.py +46 -0
- hyperflask/resources.py +149 -0
- hyperflask/static/alpine.js +5 -0
- hyperflask/static/app.js +5 -0
- hyperflask/static/htmx.js +5 -0
- hyperflask/static/reactive.js +5 -0
- hyperflask/static/stimulus.js +4 -0
- hyperflask/static/translations.js +55 -0
- hyperflask/ui/components/README.md +3 -0
- hyperflask/ui/components/actions/Button.html +6 -0
- hyperflask/ui/components/actions/Dropdown.html +16 -0
- hyperflask/ui/components/actions/DropdownItem.html +8 -0
- hyperflask/ui/components/actions/LinkButton.html +6 -0
- hyperflask/ui/components/actions/Modal.html +28 -0
- hyperflask/ui/components/actions/ModalAction.html +12 -0
- hyperflask/ui/components/actions/ModalTitle.html +6 -0
- hyperflask/ui/components/data-display/Accordion.html +8 -0
- hyperflask/ui/components/data-display/AccordionPanel.html +18 -0
- hyperflask/ui/components/data-input/Checkbox.html +4 -0
- hyperflask/ui/components/data-input/FileInput.html +4 -0
- hyperflask/ui/components/data-input/FormControl.html +15 -0
- hyperflask/ui/components/data-input/Input.html +4 -0
- hyperflask/ui/components/data-input/LabeledCheckbox.html +11 -0
- hyperflask/ui/components/data-input/LabeledTextInput.html +11 -0
- hyperflask/ui/components/data-input/Radiobox.html +4 -0
- hyperflask/ui/components/data-input/Range.html +4 -0
- hyperflask/ui/components/data-input/Rating.html +20 -0
- hyperflask/ui/components/data-input/Select.html +11 -0
- hyperflask/ui/components/data-input/Textarea.html +6 -0
- hyperflask/ui/components/data-input/Toggle.html +4 -0
- hyperflask/ui/components/feedback/Loading.html +4 -0
- hyperflask/ui/components/feedback/Toast.html +8 -0
- hyperflask/ui/components/layout/Footer.html +8 -0
- hyperflask/ui/components/layout/FooterLink.html +6 -0
- hyperflask/ui/components/layout/FooterNav.html +11 -0
- hyperflask/ui/components/layout/Indicator.html +11 -0
- hyperflask/ui/components/navigation/Link.html +6 -0
- hyperflask/ui/components/navigation/Navbar.html +21 -0
- hyperflask/ui/forms.html +16 -0
- hyperflask/ui/icons.html +5 -0
- hyperflask/ui/modal.html +7 -0
- hyperflask/utils/__init__.py +5 -0
- hyperflask/utils/htmx.py +24 -0
- hyperflask/utils/page_actions.py +25 -0
- hyperflask/utils/request.py +25 -0
- hyperflask/utils/tokens.py +28 -0
- hyperflask-0.1.0.dist-info/LICENSE +19 -0
- hyperflask-0.1.0.dist-info/METADATA +157 -0
- hyperflask-0.1.0.dist-info/RECORD +66 -0
- hyperflask-0.1.0.dist-info/WHEEL +4 -0
- hyperflask-0.1.0.dist-info/entry_points.txt +3 -0
hyperflask/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from flask import *
|
|
2
|
+
from .app import Hyperflask, db
|
|
3
|
+
from .utils.htmx import htmx_redirect, htmx_oob
|
|
4
|
+
|
|
5
|
+
# useful exports from hyperflask managed extensions
|
|
6
|
+
from flask_babel import _, _p, _n, _np, lazy_gettext
|
|
7
|
+
from flask_file_routes import page
|
|
8
|
+
from flask_files import save_file, validate_file
|
|
9
|
+
from flask_mailman_templates import send_mail
|
|
10
|
+
|
|
11
|
+
# useful exports from 3rd party libs
|
|
12
|
+
from htmx_flask import request, make_response
|
|
13
|
+
from jinja2_fragments.flask import render_block
|
|
14
|
+
from flask_apispec import use_args, use_kwargs, marshal_with
|
|
15
|
+
from slugify import slugify
|
hyperflask/__main__.py
ADDED
hyperflask/api.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import flask_apispec
|
|
2
|
+
from flask_apispec.extension import FlaskApiSpec
|
|
3
|
+
from flask_apispec.annotations import activate
|
|
4
|
+
import functools
|
|
5
|
+
import inspect
|
|
6
|
+
from .resources import Resource
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Api:
|
|
10
|
+
def __init__(self, app, version="v1", **kwargs):
|
|
11
|
+
self.url_prefix = f"/api/{version}"
|
|
12
|
+
self.endpoint_prefix = f"api_{version}_"
|
|
13
|
+
kwargs.setdefault("url_prefix", self.url_prefix)
|
|
14
|
+
kwargs.setdefault("spec_url", "/spec.json")
|
|
15
|
+
kwargs.setdefault("doc_url", "/")
|
|
16
|
+
self.app = app
|
|
17
|
+
self.docs = FlaskApiSpec(app, version=version, **kwargs)
|
|
18
|
+
self.spec = self.docs.spec
|
|
19
|
+
|
|
20
|
+
def expose(self, rule, endpoint=None, **kwargs):
|
|
21
|
+
def decorator(target):
|
|
22
|
+
nonlocal endpoint
|
|
23
|
+
if inspect.isclass(target) and issubclass(target, Resource):
|
|
24
|
+
for e in target.register(self.app, self.url_prefix, self.endpoint_prefix + (endpoint or "")):
|
|
25
|
+
self.docs.register(self.app.view_functions[e], endpoint=e)
|
|
26
|
+
else:
|
|
27
|
+
view_func = create_view_func(target)
|
|
28
|
+
endpoint = self.endpoint_prefix + (endpoint or view_func.__name__)
|
|
29
|
+
self.app.add_url_rule(self.url_prefix + rule, endpoint, view_func, **kwargs)
|
|
30
|
+
self.docs.register(view_func, endpoint)
|
|
31
|
+
return target
|
|
32
|
+
return decorator
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def args(*args, **kwargs):
|
|
36
|
+
kwargs["activate"] = False
|
|
37
|
+
return flask_apispec.use_args(*args, **kwargs)
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def kwargs(*args, **kwargs):
|
|
41
|
+
kwargs["activate"] = False
|
|
42
|
+
return flask_apispec.use_kwargs(*args, **kwargs)
|
|
43
|
+
|
|
44
|
+
@staticmethod
|
|
45
|
+
def marshal_with(*args, **kwargs):
|
|
46
|
+
kwargs["activate"] = False
|
|
47
|
+
return flask_apispec.marshal_with(*args, **kwargs)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def create_view_func(func):
|
|
51
|
+
@functools.wraps(func)
|
|
52
|
+
def view_func(*args, **kwargs):
|
|
53
|
+
return func(*args, **kwargs)
|
|
54
|
+
if hasattr(func, "__apispec__"):
|
|
55
|
+
view_func.__apispec__ = func.__apispec__
|
|
56
|
+
func.view_func = activate(view_func)
|
|
57
|
+
return view_func
|
hyperflask/app.py
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# flask
|
|
2
|
+
from flask import Flask
|
|
3
|
+
from jinja2 import ChoiceLoader, FileSystemLoader, PrefixLoader
|
|
4
|
+
# hyperflask extensions
|
|
5
|
+
from jinja_layout import LayoutExtension
|
|
6
|
+
from jinja_wtforms import WtformExtension
|
|
7
|
+
from flask_assets_pipeline import AssetsPipeline
|
|
8
|
+
from flask_super_macros import SuperMacros
|
|
9
|
+
from flask_collections import Collections
|
|
10
|
+
from flask_file_routes import FileRoutes
|
|
11
|
+
from flask_configurator import Config
|
|
12
|
+
from flask_babel import Babel, _, _p, _n, _np, lazy_gettext
|
|
13
|
+
from flask_geo import Geolocation
|
|
14
|
+
from flask_files import Files
|
|
15
|
+
from flask_sqlorm import FlaskSQLORM
|
|
16
|
+
from flask_mercure_sse import MercureSSE
|
|
17
|
+
from flask_mailman_templates import MailTemplates
|
|
18
|
+
from flask_observability import Observability
|
|
19
|
+
# 3rd party extensions
|
|
20
|
+
from flask_wtf.csrf import CSRFProtect
|
|
21
|
+
from flask_talisman import Talisman
|
|
22
|
+
from flask_frozen import Freezer
|
|
23
|
+
from htmx_flask import Htmx
|
|
24
|
+
from flask_mailman import Mail
|
|
25
|
+
from flask_dramatiq import Dramatiq
|
|
26
|
+
from flask_debugtoolbar import DebugToolbarExtension
|
|
27
|
+
# from hyperflask
|
|
28
|
+
from .forms import Form
|
|
29
|
+
from .utils.page_actions import page_action_url
|
|
30
|
+
from .utils.htmx import htmx_oob
|
|
31
|
+
from .components import register_components
|
|
32
|
+
from . import page_helpers
|
|
33
|
+
# others
|
|
34
|
+
from jinja_super_macros.registry import FileLoader
|
|
35
|
+
from jinja_wtforms.extractor import map_jinja_call_node_to_func
|
|
36
|
+
from sqlorm.sql_template import SQLTemplate
|
|
37
|
+
from periodiq import PeriodiqMiddleware
|
|
38
|
+
import os
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
db = FlaskSQLORM()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
map_jinja_call_node_to_func(_)
|
|
45
|
+
map_jinja_call_node_to_func(_p)
|
|
46
|
+
map_jinja_call_node_to_func(_n)
|
|
47
|
+
map_jinja_call_node_to_func(_np)
|
|
48
|
+
map_jinja_call_node_to_func(lazy_gettext)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Hyperflask(Flask):
|
|
52
|
+
config_class = Config
|
|
53
|
+
|
|
54
|
+
def __init__(self, *args, static_mode="hybrid", instrument=False, layouts_folder="layouts", partials_folder="partials",
|
|
55
|
+
emails_folder="emails", forms_folder="forms", assets_folder="assets", pages_folder="pages", migrations_folder="migrations",
|
|
56
|
+
config=None, config_filename="config.yml", **kwargs):
|
|
57
|
+
super().__init__(*args, **kwargs)
|
|
58
|
+
|
|
59
|
+
self.config.update({
|
|
60
|
+
"FREEZER_DESTINATION": os.path.join(self.root_path, "_site"),
|
|
61
|
+
"FREEZER_STATIC_IGNORE": [],
|
|
62
|
+
"WTF_CSRF_CHECK_DEFAULT": False,
|
|
63
|
+
"OTEL_INSTRUMENT": instrument,
|
|
64
|
+
"DRAMATIQ_BROKER": "dramatiq.brokers.redis:RedisBroker",
|
|
65
|
+
"DEBUG_TB_INTERCEPT_REDIRECTS": False,
|
|
66
|
+
"PAGES_MARKDOWN_OPTIONS": {"extensions": ["fenced_code", "nl2br", "attr_list", "admonition", "codehilite"]},
|
|
67
|
+
# Hyperflask specific
|
|
68
|
+
"LAYOUT": "layout.html",
|
|
69
|
+
"STATIC_MODE": static_mode,
|
|
70
|
+
"ASSETS_INCLUDE_HTMX": True,
|
|
71
|
+
"HTMX_BOOST_SITE": False
|
|
72
|
+
})
|
|
73
|
+
if config:
|
|
74
|
+
self.config.update(config)
|
|
75
|
+
if config_filename:
|
|
76
|
+
self.config.load(config_filename)
|
|
77
|
+
if self.debug:
|
|
78
|
+
self.config.setdefault("MAIL_BACKEND", "console")
|
|
79
|
+
|
|
80
|
+
self.otel = Observability(self)
|
|
81
|
+
|
|
82
|
+
self.jinja_env.add_extension(LayoutExtension)
|
|
83
|
+
self.jinja_env.add_extension(WtformExtension)
|
|
84
|
+
self.jinja_env.form_base_cls = Form
|
|
85
|
+
self.jinja_env.default_layout = self.config["LAYOUT"]
|
|
86
|
+
self.forms = self.jinja_env.forms
|
|
87
|
+
|
|
88
|
+
self.jinja_env.loader = ChoiceLoader([self.jinja_env.loader])
|
|
89
|
+
if layouts_folder and os.path.exists(os.path.join(self.root_path, layouts_folder)):
|
|
90
|
+
layout_loader = FileSystemLoader(os.path.join(self.root_path, layouts_folder))
|
|
91
|
+
self.jinja_env.loader.loaders.append(layout_loader)
|
|
92
|
+
self.jinja_env.loader.loaders.append(PrefixLoader({os.path.basename(layouts_folder): layout_loader}))
|
|
93
|
+
self.jinja_env.loader.loaders.extend([
|
|
94
|
+
FileLoader(os.path.join(os.path.dirname(__file__), "layout.html")),
|
|
95
|
+
FileLoader(os.path.join(os.path.dirname(__file__), "layout.html"), "hyperflask_layout.html"),
|
|
96
|
+
PrefixLoader({"ui": FileSystemLoader(os.path.join(os.path.dirname(__file__), "ui"))})
|
|
97
|
+
])
|
|
98
|
+
|
|
99
|
+
DebugToolbarExtension(self)
|
|
100
|
+
self.freezer = Freezer(self)
|
|
101
|
+
self.csrf = CSRFProtect(self)
|
|
102
|
+
#self.talisman = Talisman(self)
|
|
103
|
+
Htmx(self)
|
|
104
|
+
Mail(self)
|
|
105
|
+
self.dramatiq = Dramatiq(self)
|
|
106
|
+
self.dramatiq.broker.add_middleware(PeriodiqMiddleware())
|
|
107
|
+
self.actor = self.dramatiq.actor
|
|
108
|
+
|
|
109
|
+
SuperMacros(self)
|
|
110
|
+
Babel(self)
|
|
111
|
+
Geolocation(self)
|
|
112
|
+
Files(self)
|
|
113
|
+
MailTemplates(self, template_folder=emails_folder)
|
|
114
|
+
file_routes = FileRoutes(self, pages_folder=pages_folder, partials_folder=partials_folder)
|
|
115
|
+
self.page_helper = file_routes.page_helper
|
|
116
|
+
self.collections = Collections(self)
|
|
117
|
+
MercureSSE(self)
|
|
118
|
+
|
|
119
|
+
self.assets = AssetsPipeline(self, assets_folder=assets_folder, inline=True, include_inline_on_demand=False)
|
|
120
|
+
self.assets.state.watch_template_folders.extend([
|
|
121
|
+
os.path.join(self.root_path, "pages"),
|
|
122
|
+
os.path.join(self.root_path, "macros"),
|
|
123
|
+
os.path.join(self.root_path, "components"),
|
|
124
|
+
os.path.join(self.root_path, "forms")
|
|
125
|
+
])
|
|
126
|
+
self.assets.state.esbuild_aliases.update({
|
|
127
|
+
"assets": self.assets.state.assets_folder,
|
|
128
|
+
"@hyperflask": os.path.abspath(os.path.join(os.path.dirname(__file__), "static"))
|
|
129
|
+
})
|
|
130
|
+
self.assets.state.tailwind_suggested_content.extend(filter(bool, [
|
|
131
|
+
os.path.join(os.path.dirname(__file__), "ui") + "/**/*.html",
|
|
132
|
+
os.path.join(self.root_path, pages_folder) + "/**/*.html" if pages_folder else None,
|
|
133
|
+
os.path.join(self.root_path, pages_folder) + "/**/*.jpy" if pages_folder else None,
|
|
134
|
+
os.path.join(self.root_path, partials_folder) + "/**/*.html" if partials_folder else None,
|
|
135
|
+
os.path.join(self.root_path, partials_folder) + "/**/*.jpy" if partials_folder else None,
|
|
136
|
+
os.path.join(self.root_path, layouts_folder) + "/**/*.html" if layouts_folder else None,
|
|
137
|
+
os.path.join(self.root_path, "components") + "/**/*.html",
|
|
138
|
+
os.path.join(self.root_path, "components") + "/**/*.jpy",
|
|
139
|
+
]))
|
|
140
|
+
self.assets.state.copy_files_from_node_modules.update({
|
|
141
|
+
"bootstrap-icons/font": "bootstrap-icons"
|
|
142
|
+
})
|
|
143
|
+
self.assets.bundle(
|
|
144
|
+
{"@hyperflask": ["app.js"],
|
|
145
|
+
"@hyperflask/reactive": ["reactive.js"],
|
|
146
|
+
"@hyperflask/alpine": ["alpine.js"]},
|
|
147
|
+
assets_folder=os.path.join(os.path.dirname(__file__), "static")
|
|
148
|
+
)
|
|
149
|
+
self.assets.include("@hyperflask", 0)
|
|
150
|
+
|
|
151
|
+
self.macros.register_from_directory(os.path.join(os.path.dirname(__file__), "ui"))
|
|
152
|
+
if hasattr(file_routes, "loader"):
|
|
153
|
+
self.forms.register_from_loader(file_routes.loader, "pages")
|
|
154
|
+
self.macros.create_from_func(htmx_oob, "HtmxOob", receive_caller=True, caller_alias="html")
|
|
155
|
+
register_components(self)
|
|
156
|
+
|
|
157
|
+
if forms_folder and os.path.isdir(os.path.join(self.root_path, forms_folder)):
|
|
158
|
+
for macro in self.macros.create_from_directory(os.path.join(self.root_path, forms_folder)):
|
|
159
|
+
self.forms.register(self.macros.resolve_template(macro), macro)
|
|
160
|
+
|
|
161
|
+
self.jinja_env.globals.update(page_action_url=page_action_url)
|
|
162
|
+
|
|
163
|
+
self.collections.register_freezer_generator(self.freezer)
|
|
164
|
+
|
|
165
|
+
SQLTemplate.eval_globals.update(app=self)
|
|
166
|
+
db.init_app(self, migrations_folder=migrations_folder)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def static(func):
|
|
170
|
+
pass
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def dynamic(func=None, static_template=None):
|
|
174
|
+
def decorator(func):
|
|
175
|
+
pass
|
|
176
|
+
return decorator(func) if func else decorator
|
hyperflask/cli.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
from flask import current_app
|
|
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
|
|
4
|
+
import click
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import multiprocessing
|
|
8
|
+
import gunicorn.app.base
|
|
9
|
+
from .factory import load_config, create_app
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _create_app():
|
|
13
|
+
info = ScriptInfo()
|
|
14
|
+
try:
|
|
15
|
+
return info.load_app()
|
|
16
|
+
except NoAppException:
|
|
17
|
+
raise
|
|
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
|
+
@click.command("freeze")
|
|
27
|
+
def freeze_command():
|
|
28
|
+
current_app.freezer.freeze()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@click.command("build")
|
|
32
|
+
@click.pass_context
|
|
33
|
+
def build_command(ctx):
|
|
34
|
+
ctx.invoke(assets_cli.build)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@click.command("serve")
|
|
38
|
+
@click.option('--gunicorn-opt', multiple=True)
|
|
39
|
+
@pass_script_info
|
|
40
|
+
@click.pass_context
|
|
41
|
+
def serve_command(ctx, info, host, port, gunicorn_opt, **run_kwargs):
|
|
42
|
+
if os.environ.get("FLASK_DEBUG") == "1":
|
|
43
|
+
ctx.invoke(flask_run_command, host=host, port=port, **run_kwargs)
|
|
44
|
+
else:
|
|
45
|
+
options = {
|
|
46
|
+
'proc_name': 'hyperflask',
|
|
47
|
+
'bind': f'{host}:{port}',
|
|
48
|
+
'workers': 1 + multiprocessing.cpu_count() * 2,
|
|
49
|
+
'accesslog': '-',
|
|
50
|
+
'errorlog': '-',
|
|
51
|
+
'max_requests': 1000,
|
|
52
|
+
'max_requests_jitter': 100
|
|
53
|
+
}
|
|
54
|
+
for opt in gunicorn_opt:
|
|
55
|
+
key, value = opt.split('=', 1)
|
|
56
|
+
options[key] = int(value) if value.isdigit() else value
|
|
57
|
+
GunicornServer(info.load_app, options).run()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
serve_command.params = flask_run_command.params + serve_command.params
|
|
61
|
+
cli.add_command(serve_command)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@click.command("run")
|
|
65
|
+
@click.argument('processes', nargs=-1)
|
|
66
|
+
@click.option('--port', type=int, default=5000)
|
|
67
|
+
@click.option('--concurrency')
|
|
68
|
+
@click.option("--dev", is_flag=True)
|
|
69
|
+
def run_command(processes, port, concurrency, dev):
|
|
70
|
+
from honcho.command import _procfile, _parse_concurrency
|
|
71
|
+
from honcho.manager import Manager
|
|
72
|
+
from honcho.printer import Printer
|
|
73
|
+
from honcho import environ
|
|
74
|
+
|
|
75
|
+
if dev:
|
|
76
|
+
os.environ["FLASK_DEBUG"] = "1"
|
|
77
|
+
|
|
78
|
+
_processes = None
|
|
79
|
+
config = load_config()
|
|
80
|
+
for filename in [f"Procfile.{config['ENV']}", "Procfile"]:
|
|
81
|
+
if os.path.exists(filename):
|
|
82
|
+
_processes = _procfile(filename).processes
|
|
83
|
+
break
|
|
84
|
+
if _processes is None:
|
|
85
|
+
_processes = {
|
|
86
|
+
"web": [sys.argv[0], "serve", "--port", "$PORT"],
|
|
87
|
+
# "worker": [sys.argv[0], "worker"],
|
|
88
|
+
# "scheduler": [sys.argv[0], "periodiq"]
|
|
89
|
+
}
|
|
90
|
+
if dev:
|
|
91
|
+
_processes["assets"] = [sys.argv[0], "assets", "dev"]
|
|
92
|
+
|
|
93
|
+
processes = {name: " ".join(_processes[name]) if isinstance(_processes[name], list) else _processes[name]
|
|
94
|
+
for name in _processes if name in _processes}
|
|
95
|
+
|
|
96
|
+
manager = Manager(Printer(sys.stdout, colour=True))
|
|
97
|
+
|
|
98
|
+
for p in environ.expand_processes(processes,
|
|
99
|
+
concurrency=_parse_concurrency(concurrency),
|
|
100
|
+
port=port):
|
|
101
|
+
e = os.environ.copy()
|
|
102
|
+
e.update(p.env)
|
|
103
|
+
manager.add_process(p.name, p.cmd, quiet=p.quiet, env=e)
|
|
104
|
+
|
|
105
|
+
manager.loop()
|
|
106
|
+
sys.exit(manager.returncode)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
run_command.params.insert(0, _debug_option)
|
|
110
|
+
cli.add_command(run_command)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@click.command("dev")
|
|
114
|
+
@click.pass_context
|
|
115
|
+
def dev_command(ctx, **kwargs):
|
|
116
|
+
ctx.forward(run_command, dev=True)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
dev_command.params = list(run_command.params)
|
|
120
|
+
cli.add_command(dev_command)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def main():
|
|
124
|
+
cli.main()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class GunicornServer(gunicorn.app.base.BaseApplication):
|
|
128
|
+
def __init__(self, app_loader, options=None):
|
|
129
|
+
self.app_loader = app_loader
|
|
130
|
+
self.options = options or {}
|
|
131
|
+
super().__init__()
|
|
132
|
+
|
|
133
|
+
def load_config(self):
|
|
134
|
+
config = {key: value for key, value in self.options.items()
|
|
135
|
+
if key in self.cfg.settings and value is not None}
|
|
136
|
+
for key, value in config.items():
|
|
137
|
+
self.cfg.set(key.lower(), value)
|
|
138
|
+
|
|
139
|
+
def load(self):
|
|
140
|
+
return self.app_loader()
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import jinjapy
|
|
2
|
+
import importlib
|
|
3
|
+
import os
|
|
4
|
+
import importlib
|
|
5
|
+
import re
|
|
6
|
+
from importlib.metadata import entry_points
|
|
7
|
+
from .core import ComponentAdapter
|
|
8
|
+
from .alpine import AlpineAdapter
|
|
9
|
+
from .webcomponent import WebComponentAdapter
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def register_components(app, path=None, package_name=None):
|
|
13
|
+
if not path:
|
|
14
|
+
path = os.path.join(app.root_path, "components")
|
|
15
|
+
if not os.path.exists(path):
|
|
16
|
+
return
|
|
17
|
+
if not package_name:
|
|
18
|
+
package_name = "components"
|
|
19
|
+
if app.import_name != "__main__":
|
|
20
|
+
package_name = f"{app.import_name}.{package_name}"
|
|
21
|
+
|
|
22
|
+
url_prefix = "/__components/"
|
|
23
|
+
adapters = list(list_available_adapters())
|
|
24
|
+
force_adapters = app.config.get("COMPONENT_ADAPTERS", {})
|
|
25
|
+
|
|
26
|
+
loader = jinjapy.register_package(package_name, path, env=app.jinja_env)
|
|
27
|
+
for module_name, template in loader.list_files(module_with_package=False):
|
|
28
|
+
adapter_class = None
|
|
29
|
+
for pattern, adapter_import_path in force_adapters.items():
|
|
30
|
+
if re.match(pattern, template):
|
|
31
|
+
adapter_module, adapter_classname = adapter_import_path.split(":")
|
|
32
|
+
adapter_class = getattr(importlib.import_module(adapter_module), adapter_classname)
|
|
33
|
+
break
|
|
34
|
+
if not adapter_class:
|
|
35
|
+
matches = False
|
|
36
|
+
for adapter_class in adapters:
|
|
37
|
+
if adapter_class.matches(app, module_name, template):
|
|
38
|
+
matches = True
|
|
39
|
+
break
|
|
40
|
+
if not matches:
|
|
41
|
+
continue
|
|
42
|
+
if adapter_class:
|
|
43
|
+
name = os.path.basename(template).split(".")[0].replace("-", "_")
|
|
44
|
+
adapter = adapter_class(name, f"{package_name}.{module_name}" if module_name else None, template)
|
|
45
|
+
adapter.register(app, url_prefix)
|
|
46
|
+
app.macros.create_from_func(adapter.render, name, receive_caller=True)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def list_available_adapters():
|
|
50
|
+
yield AlpineAdapter
|
|
51
|
+
yield WebComponentAdapter
|
|
52
|
+
for entry in entry_points(group="hyperflask.component_adapters"):
|
|
53
|
+
yield entry.load()
|
|
54
|
+
yield ComponentAdapter
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from .core import ComponentAdapter
|
|
2
|
+
from flask import current_app
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class AlpineAdapter(ComponentAdapter):
|
|
6
|
+
@classmethod
|
|
7
|
+
def matches(cls, app, module_name, template):
|
|
8
|
+
if not ComponentAdapter.matches(app, module_name, template):
|
|
9
|
+
return False
|
|
10
|
+
tpl = app.jinja_env.loader.get_source(app.jinja_env, template)[0]
|
|
11
|
+
tags = ["x-data", "x-init", "x-show", "x-bind", "x-model", "x-text", "x-html", "x-if", "x-for", "x-on"]
|
|
12
|
+
return any(tag in tpl for tag in tags)
|
|
13
|
+
|
|
14
|
+
def render(self, caller, *args, **kwargs):
|
|
15
|
+
current_app.assets.include("@hyperflask/alpine")
|
|
16
|
+
return super().render(caller, *args, **kwargs)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from markupsafe import Markup
|
|
2
|
+
from flask import request, current_app
|
|
3
|
+
from flask.views import http_method_funcs
|
|
4
|
+
import importlib
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BaseComponentAdapter:
|
|
8
|
+
@classmethod
|
|
9
|
+
def matches(cls, app, module_name, template):
|
|
10
|
+
return False
|
|
11
|
+
|
|
12
|
+
def __init__(self, name, module_name, template):
|
|
13
|
+
self.name = name
|
|
14
|
+
self.module_name = module_name
|
|
15
|
+
self.template = template
|
|
16
|
+
|
|
17
|
+
def register(self, app, url_prefix):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
def render(self, caller, *args, **kwargs):
|
|
21
|
+
tpl = current_app.jinja_env.get_template(self.template)
|
|
22
|
+
ctx = dict(args=args, kwargs=kwargs, props=kwargs, caller=caller)
|
|
23
|
+
return Markup(tpl.render(**ctx))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ComponentAdapter(BaseComponentAdapter):
|
|
27
|
+
@classmethod
|
|
28
|
+
def matches(cls, app, module_name, template):
|
|
29
|
+
return module_name or template.endswith(".html") or template.endswith(".jpy")
|
|
30
|
+
|
|
31
|
+
def register(self, app, url_prefix):
|
|
32
|
+
if not self.module_name:
|
|
33
|
+
return
|
|
34
|
+
url = url_prefix + self.name.replace("_", "/")
|
|
35
|
+
module = importlib.import_module(self.module_name)
|
|
36
|
+
methods = [m.upper() for m in http_method_funcs if m in dir(module)]
|
|
37
|
+
def view_func():
|
|
38
|
+
props = {}
|
|
39
|
+
module = importlib.import_module(self.module_name)
|
|
40
|
+
m = getattr(module, request.method.lower(), None)
|
|
41
|
+
if m:
|
|
42
|
+
props = m()
|
|
43
|
+
if props is not None and not isinstance(props, dict):
|
|
44
|
+
return props
|
|
45
|
+
if not props:
|
|
46
|
+
props = {}
|
|
47
|
+
return app.macros[self.name](**props)
|
|
48
|
+
if methods:
|
|
49
|
+
app.add_url_rule(url, self.name, view_func=view_func, methods=methods)
|
|
50
|
+
|
|
51
|
+
def render(self, caller, *args, **kwargs):
|
|
52
|
+
tpl = current_app.jinja_env.get_template(self.template)
|
|
53
|
+
ctx = dict(args=args, kwargs=kwargs, props=kwargs, caller=caller)
|
|
54
|
+
if self.module_name:
|
|
55
|
+
module = importlib.import_module(self.module_name)
|
|
56
|
+
if module and "render" in dir(module):
|
|
57
|
+
_ctx = module.render(*args, **kwargs)
|
|
58
|
+
if _ctx:
|
|
59
|
+
ctx.update(_ctx)
|
|
60
|
+
return Markup(tpl.render(**ctx))
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from .core import BaseComponentAdapter
|
|
2
|
+
from jinja_super_macros import html_tag
|
|
3
|
+
from flask import current_app
|
|
4
|
+
from markupsafe import Markup
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class WebComponentAdapter(BaseComponentAdapter):
|
|
9
|
+
@classmethod
|
|
10
|
+
def matches(cls, app, module_name, template):
|
|
11
|
+
return template.endswith(".js")
|
|
12
|
+
|
|
13
|
+
def register(self, app, url_prefix):
|
|
14
|
+
tpl = app.jinja_env.get_template(self.template)
|
|
15
|
+
if "@components" not in app.assets.state.bundles:
|
|
16
|
+
app.assets.state.bundles["@components"] = []
|
|
17
|
+
app.assets.state.bundles["@components"].append(
|
|
18
|
+
app.assets.format_bundle_file(os.path.abspath(tpl.filename), self.template))
|
|
19
|
+
|
|
20
|
+
def render(self, caller, *args, **kwargs):
|
|
21
|
+
current_app.assets.include("@components")
|
|
22
|
+
tag_name = self.name.replace("_", "-")
|
|
23
|
+
return html_tag(tag_name, **kwargs) + (caller() if caller else "") + Markup(f"</{tag_name}>")
|