flask-spa 0.1.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.
- flask_spa-0.1.1/PKG-INFO +10 -0
- flask_spa-0.1.1/README.md +0 -0
- flask_spa-0.1.1/pyproject.toml +19 -0
- flask_spa-0.1.1/setup.cfg +4 -0
- flask_spa-0.1.1/src/flask_spa.egg-info/PKG-INFO +10 -0
- flask_spa-0.1.1/src/flask_spa.egg-info/SOURCES.txt +11 -0
- flask_spa-0.1.1/src/flask_spa.egg-info/dependency_links.txt +1 -0
- flask_spa-0.1.1/src/flask_spa.egg-info/requires.txt +1 -0
- flask_spa-0.1.1/src/flask_spa.egg-info/top_level.txt +1 -0
- flask_spa-0.1.1/src/spa_flask/__init__.py +59 -0
- flask_spa-0.1.1/src/spa_flask/components.py +59 -0
- flask_spa-0.1.1/src/spa_flask/events.py +22 -0
- flask_spa-0.1.1/src/spa_flask/middleware.py +62 -0
flask_spa-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flask-spa
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Components to build Flask single page applications
|
|
5
|
+
Author-email: Martin Mohnhaupt <martin.mohnhaupt@etik.com>
|
|
6
|
+
Maintainer-email: Martin Mohnhaupt <martin.mohnhaupt@etik.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Requires-Python: >=3.13
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: flask>=3.1.2
|
|
File without changes
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "flask-spa"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = "Components to build Flask single page applications"
|
|
5
|
+
authors = [{ name = "Martin Mohnhaupt", email = "martin.mohnhaupt@etik.com" }]
|
|
6
|
+
maintainers = [
|
|
7
|
+
{ name = "Martin Mohnhaupt", email = "martin.mohnhaupt@etik.com" },
|
|
8
|
+
]
|
|
9
|
+
license = "MIT"
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
requires-python = ">=3.13"
|
|
12
|
+
dependencies = ["flask>=3.1.2"]
|
|
13
|
+
|
|
14
|
+
[build-system]
|
|
15
|
+
requires = ["setuptools>=42", "wheel"]
|
|
16
|
+
build-backend = "setuptools.build_meta"
|
|
17
|
+
|
|
18
|
+
[tool.ruff]
|
|
19
|
+
line-length = 230
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flask-spa
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Components to build Flask single page applications
|
|
5
|
+
Author-email: Martin Mohnhaupt <martin.mohnhaupt@etik.com>
|
|
6
|
+
Maintainer-email: Martin Mohnhaupt <martin.mohnhaupt@etik.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Requires-Python: >=3.13
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: flask>=3.1.2
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/flask_spa.egg-info/PKG-INFO
|
|
4
|
+
src/flask_spa.egg-info/SOURCES.txt
|
|
5
|
+
src/flask_spa.egg-info/dependency_links.txt
|
|
6
|
+
src/flask_spa.egg-info/requires.txt
|
|
7
|
+
src/flask_spa.egg-info/top_level.txt
|
|
8
|
+
src/spa_flask/__init__.py
|
|
9
|
+
src/spa_flask/components.py
|
|
10
|
+
src/spa_flask/events.py
|
|
11
|
+
src/spa_flask/middleware.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
flask>=3.1.2
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
spa_flask
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from os import PathLike
|
|
2
|
+
|
|
3
|
+
from flask import Flask
|
|
4
|
+
|
|
5
|
+
from .components import Alert, ConfirmDialog, Dialog, Severity, SpaComponent, Toast
|
|
6
|
+
from .events import SpaEvent
|
|
7
|
+
from .middleware import FlaskMiddleware
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SpaFlask(Flask):
|
|
11
|
+
"""The `SpaFlask` class inherits the `Flask` class. It integrates the `SpaMiddleware` and adds functionality to th base class.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
Flask (Flask): the `Flask` base class
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
import_name: str,
|
|
20
|
+
static_url_path: str | None = None,
|
|
21
|
+
static_folder: str | PathLike[str] | None = "static",
|
|
22
|
+
static_host: str | None = None,
|
|
23
|
+
host_matching: bool = False,
|
|
24
|
+
subdomain_matching: bool = False,
|
|
25
|
+
template_folder: str | PathLike[str] | None = "templates",
|
|
26
|
+
instance_path: str | None = None,
|
|
27
|
+
instance_relative_config: bool = False,
|
|
28
|
+
root_path: str | None = None,
|
|
29
|
+
):
|
|
30
|
+
# Call the base class initializer
|
|
31
|
+
super().__init__(import_name, static_url_path, static_folder, static_host, host_matching, subdomain_matching, template_folder, instance_path, instance_relative_config, root_path)
|
|
32
|
+
# Add the Flask middleware
|
|
33
|
+
self._middleware = FlaskMiddleware(self)
|
|
34
|
+
|
|
35
|
+
def set_title(self, title: str):
|
|
36
|
+
self._middleware.title(title)
|
|
37
|
+
|
|
38
|
+
def create_component(self, object: SpaComponent) -> None:
|
|
39
|
+
self._middleware._events.append(SpaEvent.create_from_object(object))
|
|
40
|
+
|
|
41
|
+
def toast(self, severity: Severity, header: str, message: str, redirect_url="") -> None:
|
|
42
|
+
component = Toast(severity, header, message, redirect_url)
|
|
43
|
+
event = SpaEvent.create_from_object(component)
|
|
44
|
+
self._middleware.add_event(event)
|
|
45
|
+
|
|
46
|
+
def alert(self, severity: Severity, message: str, redirect_url: str = "") -> None:
|
|
47
|
+
component = Alert(severity, message, redirect_url)
|
|
48
|
+
event = SpaEvent.create_from_object(component)
|
|
49
|
+
self._middleware.add_event(event)
|
|
50
|
+
|
|
51
|
+
def dialog(self, header: str, message: str, lang: str, accept_url: str, dismiss_url: str) -> None:
|
|
52
|
+
component = Dialog(header, message, lang, accept_url, dismiss_url)
|
|
53
|
+
event = SpaEvent.create_from_object(component)
|
|
54
|
+
self._middleware.add_event(event)
|
|
55
|
+
|
|
56
|
+
def confirm(self, header: str, message: str, lang: str, accept_url: str, dismiss_url: str) -> None:
|
|
57
|
+
component = ConfirmDialog(header, message, lang, accept_url, dismiss_url)
|
|
58
|
+
event = SpaEvent.create_from_object(component)
|
|
59
|
+
self._middleware.add_event(event)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from enum import StrEnum
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class SpaComponent:
|
|
6
|
+
"""Base class for SPA components"""
|
|
7
|
+
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Severity(StrEnum):
|
|
12
|
+
"""Class representing a severity level"""
|
|
13
|
+
|
|
14
|
+
DANGER = "danger"
|
|
15
|
+
WARNING = "warning"
|
|
16
|
+
SUCCESS = "success"
|
|
17
|
+
INFO = "info"
|
|
18
|
+
DEFAULT = "primary"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class Dialog(SpaComponent):
|
|
23
|
+
"""Class representing a dialog"""
|
|
24
|
+
|
|
25
|
+
header: str = "Header not defined"
|
|
26
|
+
body: str = "Body not defined"
|
|
27
|
+
lang: str = "en"
|
|
28
|
+
accept_url: str = ""
|
|
29
|
+
dismiss_url: str = ""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class ConfirmDialog(SpaComponent):
|
|
34
|
+
"""Class representing a confirmation dialog"""
|
|
35
|
+
|
|
36
|
+
header: str = "Header not defined"
|
|
37
|
+
body: str = "Body not defined"
|
|
38
|
+
lang: str = "en"
|
|
39
|
+
accept_url: str = ""
|
|
40
|
+
dismiss_url: str = ""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class Toast(SpaComponent):
|
|
45
|
+
"""Class representing a toast message"""
|
|
46
|
+
|
|
47
|
+
category: Severity = Severity.DEFAULT
|
|
48
|
+
header: str = "Header not defined"
|
|
49
|
+
body: str = "Body not defined"
|
|
50
|
+
redirect_url: str = ""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class Alert(SpaComponent):
|
|
55
|
+
"""Class representing an alert message"""
|
|
56
|
+
|
|
57
|
+
category: Severity = Severity.DEFAULT
|
|
58
|
+
message: str = "Message not defined"
|
|
59
|
+
redirect_url: str = ""
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from dataclasses import asdict, dataclass, field
|
|
2
|
+
from typing import Dict
|
|
3
|
+
|
|
4
|
+
from .components import Alert, ConfirmDialog, Dialog, SpaComponent, Toast
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class SpaEvent:
|
|
9
|
+
type: str = ""
|
|
10
|
+
detail: Dict = field(default_factory=Dict)
|
|
11
|
+
|
|
12
|
+
@staticmethod
|
|
13
|
+
def create_from_object(object: SpaComponent) -> "SpaEvent":
|
|
14
|
+
if isinstance(object, Dialog):
|
|
15
|
+
return SpaEvent("nd:dialog", asdict(object))
|
|
16
|
+
if isinstance(object, Toast):
|
|
17
|
+
return SpaEvent("nd:toast", asdict(object))
|
|
18
|
+
if isinstance(object, Alert):
|
|
19
|
+
return SpaEvent("nd:alert", asdict(object))
|
|
20
|
+
if isinstance(object, ConfirmDialog):
|
|
21
|
+
return SpaEvent("nd:confirm", asdict(object))
|
|
22
|
+
return SpaEvent()
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from dataclasses import asdict, is_dataclass
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
from flask import Flask, Request, Response, request
|
|
6
|
+
|
|
7
|
+
from .components import SpaComponent
|
|
8
|
+
from .events import SpaEvent
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class FlaskMiddleware:
|
|
12
|
+
def __init__(self, app: Flask) -> None:
|
|
13
|
+
self._app = app
|
|
14
|
+
self._wsgi_app = app.wsgi_app
|
|
15
|
+
self._request: Request
|
|
16
|
+
self._response: Response
|
|
17
|
+
self._title: str | None = None
|
|
18
|
+
self._events: List[SpaEvent] = []
|
|
19
|
+
|
|
20
|
+
with app.app_context():
|
|
21
|
+
print("MW", "First call")
|
|
22
|
+
|
|
23
|
+
app.wsgi_app = self
|
|
24
|
+
|
|
25
|
+
@app.before_request
|
|
26
|
+
def _():
|
|
27
|
+
self._request = request
|
|
28
|
+
|
|
29
|
+
@app.after_request
|
|
30
|
+
def _(response: Response):
|
|
31
|
+
return response
|
|
32
|
+
|
|
33
|
+
def __call__(self, environ, start_response):
|
|
34
|
+
|
|
35
|
+
def custom_start_response(status: str, headers: List, exc_info=None):
|
|
36
|
+
if self._events:
|
|
37
|
+
payload = [asdict(_) for _ in self._events]
|
|
38
|
+
headers.append(("x-nd-event", json.dumps(payload)))
|
|
39
|
+
self._events.clear()
|
|
40
|
+
|
|
41
|
+
if self._title:
|
|
42
|
+
headers.append(("x-nd-title", self._title))
|
|
43
|
+
self._title = ""
|
|
44
|
+
|
|
45
|
+
return start_response(status, headers, exc_info)
|
|
46
|
+
|
|
47
|
+
return self._wsgi_app(environ, custom_start_response)
|
|
48
|
+
|
|
49
|
+
def as_json(self, obj) -> str:
|
|
50
|
+
result = json.dumps(None)
|
|
51
|
+
if is_dataclass(obj) and not isinstance(obj, type):
|
|
52
|
+
result = json.dumps(asdict(obj))
|
|
53
|
+
return result
|
|
54
|
+
|
|
55
|
+
def title(self, title) -> None:
|
|
56
|
+
self._title = title
|
|
57
|
+
|
|
58
|
+
def send(self, obj: SpaComponent) -> None:
|
|
59
|
+
self._events.append(SpaEvent.create_from_object(obj))
|
|
60
|
+
|
|
61
|
+
def add_event(self, event: SpaEvent):
|
|
62
|
+
self._events.append(event)
|