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.

Files changed (66) hide show
  1. hyperflask/__init__.py +15 -0
  2. hyperflask/__main__.py +3 -0
  3. hyperflask/api.py +57 -0
  4. hyperflask/app.py +176 -0
  5. hyperflask/cli.py +140 -0
  6. hyperflask/components/__init__.py +54 -0
  7. hyperflask/components/alpine.py +16 -0
  8. hyperflask/components/core.py +60 -0
  9. hyperflask/components/webcomponent.py +23 -0
  10. hyperflask/factory.py +173 -0
  11. hyperflask/forms/__init__.py +51 -0
  12. hyperflask/forms/fields.py +17 -0
  13. hyperflask/forms/widgets.py +19 -0
  14. hyperflask/layout.html +28 -0
  15. hyperflask/page_helpers.py +36 -0
  16. hyperflask/reactivity.py +46 -0
  17. hyperflask/resources.py +149 -0
  18. hyperflask/static/alpine.js +5 -0
  19. hyperflask/static/app.js +5 -0
  20. hyperflask/static/htmx.js +5 -0
  21. hyperflask/static/reactive.js +5 -0
  22. hyperflask/static/stimulus.js +4 -0
  23. hyperflask/static/translations.js +55 -0
  24. hyperflask/ui/components/README.md +3 -0
  25. hyperflask/ui/components/actions/Button.html +6 -0
  26. hyperflask/ui/components/actions/Dropdown.html +16 -0
  27. hyperflask/ui/components/actions/DropdownItem.html +8 -0
  28. hyperflask/ui/components/actions/LinkButton.html +6 -0
  29. hyperflask/ui/components/actions/Modal.html +28 -0
  30. hyperflask/ui/components/actions/ModalAction.html +12 -0
  31. hyperflask/ui/components/actions/ModalTitle.html +6 -0
  32. hyperflask/ui/components/data-display/Accordion.html +8 -0
  33. hyperflask/ui/components/data-display/AccordionPanel.html +18 -0
  34. hyperflask/ui/components/data-input/Checkbox.html +4 -0
  35. hyperflask/ui/components/data-input/FileInput.html +4 -0
  36. hyperflask/ui/components/data-input/FormControl.html +15 -0
  37. hyperflask/ui/components/data-input/Input.html +4 -0
  38. hyperflask/ui/components/data-input/LabeledCheckbox.html +11 -0
  39. hyperflask/ui/components/data-input/LabeledTextInput.html +11 -0
  40. hyperflask/ui/components/data-input/Radiobox.html +4 -0
  41. hyperflask/ui/components/data-input/Range.html +4 -0
  42. hyperflask/ui/components/data-input/Rating.html +20 -0
  43. hyperflask/ui/components/data-input/Select.html +11 -0
  44. hyperflask/ui/components/data-input/Textarea.html +6 -0
  45. hyperflask/ui/components/data-input/Toggle.html +4 -0
  46. hyperflask/ui/components/feedback/Loading.html +4 -0
  47. hyperflask/ui/components/feedback/Toast.html +8 -0
  48. hyperflask/ui/components/layout/Footer.html +8 -0
  49. hyperflask/ui/components/layout/FooterLink.html +6 -0
  50. hyperflask/ui/components/layout/FooterNav.html +11 -0
  51. hyperflask/ui/components/layout/Indicator.html +11 -0
  52. hyperflask/ui/components/navigation/Link.html +6 -0
  53. hyperflask/ui/components/navigation/Navbar.html +21 -0
  54. hyperflask/ui/forms.html +16 -0
  55. hyperflask/ui/icons.html +5 -0
  56. hyperflask/ui/modal.html +7 -0
  57. hyperflask/utils/__init__.py +5 -0
  58. hyperflask/utils/htmx.py +24 -0
  59. hyperflask/utils/page_actions.py +25 -0
  60. hyperflask/utils/request.py +25 -0
  61. hyperflask/utils/tokens.py +28 -0
  62. hyperflask-0.1.0.dist-info/LICENSE +19 -0
  63. hyperflask-0.1.0.dist-info/METADATA +157 -0
  64. hyperflask-0.1.0.dist-info/RECORD +66 -0
  65. hyperflask-0.1.0.dist-info/WHEEL +4 -0
  66. 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
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ main()
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}>")