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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ 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)