FastPWA 0.1.1b0__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,78 @@
1
+ Metadata-Version: 2.1
2
+ Name: FastPWA
3
+ Version: 0.1.1b0
4
+ Summary: Make your FastAPI app installable on mobile devices.
5
+ Keywords: pwa,progressive,web,app,windows,android,iphone,apple,ios,safari
6
+ Author-Email: Cody M Sommer <bassmastacod@gmail.com>
7
+ License: MIT
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Programming Language :: Python :: 3.7
10
+ Classifier: Programming Language :: Python :: 3.8
11
+ Classifier: Programming Language :: Python :: 3.9
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Development Status :: 2 - Pre-Alpha
18
+ Classifier: Intended Audience :: Developers
19
+ Classifier: Topic :: Software Development :: Libraries
20
+ Classifier: Typing :: Typed
21
+ Project-URL: Repository, https://github.com/BassMastaCod/FastPWA.git
22
+ Project-URL: Issues, https://github.com/BassMastaCod/FastPWA/issues
23
+ Requires-Python: >=3.7
24
+ Requires-Dist: fastapi
25
+ Requires-Dist: pydantic
26
+ Requires-Dist: jinja2
27
+ Description-Content-Type: text/markdown
28
+
29
+ # πŸš€ FastPWA
30
+ FastPWA is a minimal FastAPI extension that makes your app installable as a Progressive Web App (PWA). It handles manifest generation, service worker registration, and automatic asset injectionβ€”giving you a native-like install prompt with almost no setup.
31
+
32
+ ## 🌟 What It Does
33
+ - 🧾 Generates a compliant webmanifest from your app metadata
34
+ - βš™οΈ Registers a basic service worker for installability
35
+ - πŸ–ΌοΈ Discovers and injects favicon and static assets (index.css, index.js, etc.)
36
+ - 🧩 Mounts static folders and serves your HTML entrypoint
37
+
38
+ ## πŸ“¦ Installation
39
+ ```commandline
40
+ pip install fastpwa
41
+ ```
42
+
43
+ ## πŸ§ͺ Quickstart
44
+ ```python
45
+ from fastpwa import PWA
46
+
47
+ app = PWA(title="My App", summary="Installable FastAPI app", prefix="app")
48
+ app.static_mount("static") # Mounts static assets and discovers favicon
49
+
50
+ app.register_pwa(html="static/index.html") # Registers manifest, SW, and index route
51
+ ```
52
+
53
+ ## πŸ“ Static Folder Layout
54
+ FastPWA auto-discovers and injects these assets if present:
55
+ ```
56
+ static/
57
+ β”œβ”€β”€ index.html
58
+ β”œβ”€β”€ index.css
59
+ β”œβ”€β”€ index.js
60
+ β”œβ”€β”€ global.css
61
+ β”œβ”€β”€ global.js
62
+ └── favicon.png
63
+ ```
64
+
65
+ ## 🧬 Manifest Customization
66
+ You can override manifest fields via `register_pwa()`:
67
+ ```python
68
+ app.register_pwa(
69
+ html="static/index.html",
70
+ app_name="MyApp",
71
+ app_description="A simple installable app",
72
+ color="#3367D6",
73
+ background_color="#FFFFFF"
74
+ )
75
+ ```
76
+
77
+ ## πŸ“œ License
78
+ MIT
@@ -0,0 +1,50 @@
1
+ # πŸš€ FastPWA
2
+ FastPWA is a minimal FastAPI extension that makes your app installable as a Progressive Web App (PWA). It handles manifest generation, service worker registration, and automatic asset injectionβ€”giving you a native-like install prompt with almost no setup.
3
+
4
+ ## 🌟 What It Does
5
+ - 🧾 Generates a compliant webmanifest from your app metadata
6
+ - βš™οΈ Registers a basic service worker for installability
7
+ - πŸ–ΌοΈ Discovers and injects favicon and static assets (index.css, index.js, etc.)
8
+ - 🧩 Mounts static folders and serves your HTML entrypoint
9
+
10
+ ## πŸ“¦ Installation
11
+ ```commandline
12
+ pip install fastpwa
13
+ ```
14
+
15
+ ## πŸ§ͺ Quickstart
16
+ ```python
17
+ from fastpwa import PWA
18
+
19
+ app = PWA(title="My App", summary="Installable FastAPI app", prefix="app")
20
+ app.static_mount("static") # Mounts static assets and discovers favicon
21
+
22
+ app.register_pwa(html="static/index.html") # Registers manifest, SW, and index route
23
+ ```
24
+
25
+ ## πŸ“ Static Folder Layout
26
+ FastPWA auto-discovers and injects these assets if present:
27
+ ```
28
+ static/
29
+ β”œβ”€β”€ index.html
30
+ β”œβ”€β”€ index.css
31
+ β”œβ”€β”€ index.js
32
+ β”œβ”€β”€ global.css
33
+ β”œβ”€β”€ global.js
34
+ └── favicon.png
35
+ ```
36
+
37
+ ## 🧬 Manifest Customization
38
+ You can override manifest fields via `register_pwa()`:
39
+ ```python
40
+ app.register_pwa(
41
+ html="static/index.html",
42
+ app_name="MyApp",
43
+ app_description="A simple installable app",
44
+ color="#3367D6",
45
+ background_color="#FFFFFF"
46
+ )
47
+ ```
48
+
49
+ ## πŸ“œ License
50
+ MIT
@@ -0,0 +1,228 @@
1
+ import logging
2
+ from pathlib import Path
3
+ from typing import Optional
4
+
5
+ from fastapi import FastAPI, Request
6
+ from fastapi.responses import HTMLResponse
7
+ from fastapi.staticfiles import StaticFiles
8
+ from jinja2 import Environment, BaseLoader
9
+ from pydantic import BaseModel
10
+
11
+ BASE_TEMPLATE = '''
12
+ <!DOCTYPE html>
13
+ <html lang="en">
14
+ <head>
15
+ <meta charset="UTF-8" />
16
+ <title>{{ app_title }}</title>
17
+ {% if favicon %}
18
+ <link rel="icon" href="{{ favicon.src }}" type="{{ favicon.type }}">
19
+ <link rel="apple-touch-icon" href="{{ favicon.src }}">
20
+ {% endif %}
21
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
22
+ <meta name="description" content="{{ description }}">
23
+ {% if color %}
24
+ <meta name="theme-color" content="{{ color }}">
25
+ {% endif %}
26
+ <link rel="manifest" href="{{ app_id }}.webmanifest?path={{ request.url.path }}">
27
+ <script>
28
+ if ('serviceWorker' in navigator) {
29
+ navigator.serviceWorker.register('service-worker.js?path={{ request.url.path }}')
30
+ .then(reg => console.log('SW registered:', reg.scope))
31
+ .catch(err => console.error('SW registration failed:', err));
32
+ }
33
+ </script>
34
+ {% for path in css %}
35
+ <link rel="stylesheet" href="{{ path }}">
36
+ {% endfor %}
37
+ {% for path in js %}
38
+ <script src="{{ path }}" type="module"></script>
39
+ {% endfor %}
40
+ </head>
41
+ <body>
42
+ {{ body | safe }}
43
+ </body>
44
+ </html>
45
+ '''
46
+
47
+
48
+ SERVICE_WORKER = '''
49
+ self.addEventListener('install', (event) => {
50
+ self.skipWaiting();
51
+ });
52
+
53
+ self.addEventListener('activate', (event) => {
54
+ return self.clients.claim();
55
+ });
56
+ '''
57
+
58
+
59
+ logger = logging.getLogger("fastpwa")
60
+ if not logger.hasHandlers():
61
+ handler = logging.StreamHandler()
62
+ handler.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
63
+ logger.addHandler(handler)
64
+ logger.setLevel(logging.INFO)
65
+
66
+
67
+ def ensure_list(value: Optional[str | list[str]]) -> list[str]:
68
+ if value is None:
69
+ return []
70
+ if isinstance(value, list):
71
+ return value
72
+ return [value]
73
+
74
+
75
+ class Icon(BaseModel):
76
+ src: str
77
+ sizes: str
78
+ type: str
79
+
80
+ @classmethod
81
+ def from_file(cls, file: Path, mount_path: str) -> 'Icon':
82
+ match (file.suffix.lower()):
83
+ case '.ico':
84
+ image_type = 'x-icon'
85
+ case '.png':
86
+ image_type = 'png'
87
+ case '.svg':
88
+ image_type = 'svg+xml'
89
+ case _:
90
+ raise ValueError(f'Unsupported icon file type: {file.suffix}')
91
+ return cls(
92
+ src=f'{mount_path}/{file.name}',
93
+ sizes='any',
94
+ type=f'image/{image_type}'
95
+ )
96
+
97
+
98
+ class Shortcut(BaseModel):
99
+ name: str
100
+ short_name: Optional[str]
101
+ description: Optional[str]
102
+ url: str
103
+ icons: list[Icon] = []
104
+
105
+
106
+ class Manifest(BaseModel):
107
+ name: str
108
+ short_name: str
109
+ description: str
110
+ start_url: str
111
+ scope: str
112
+ id: str
113
+ display: str
114
+ theme_color: Optional[str]
115
+ background_color: str
116
+ icons: list[Icon] = []
117
+ shortcuts: list[Shortcut] = []
118
+
119
+
120
+ class PWA(FastAPI):
121
+ def __init__(self, *,
122
+ title: Optional[str] = 'FastPWA App',
123
+ summary: Optional[str] = 'Installable FastAPI app',
124
+ prefix: Optional[str] = None,
125
+ **kwargs):
126
+ self.title = None
127
+ self.summary = None
128
+ self.docs_url = None
129
+ super().__init__(
130
+ title=title,
131
+ summary=summary,
132
+ docs_url=kwargs.pop('docs_url', f'{prefix}/api/docs'),
133
+ redoc_url=kwargs.pop('redoc_url', f'{prefix}/api/redoc'),
134
+ openapi_url=kwargs.pop('openapi_url', f'{prefix}/api/openapi.json'),
135
+ **kwargs
136
+ )
137
+
138
+ self.index_css = []
139
+ self.index_js = []
140
+ self.global_css = []
141
+ self.global_js = []
142
+ self.favicon = None
143
+ self.prefix = '' if not prefix else '/' + prefix.strip('/')
144
+ self.env = Environment(loader=BaseLoader())
145
+ self.template = self.env.from_string(BASE_TEMPLATE)
146
+ logger.info(f'Established {title} API, viewable at {self.docs_url}')
147
+
148
+ @property
149
+ def pwa_id(self):
150
+ return self.title.lower().replace(' ', '-')
151
+
152
+ def static_mount(self, folder: str | Path):
153
+ folder = Path(folder)
154
+ if not folder.exists():
155
+ raise ValueError(f'Static folder "{folder}" does not exist.')
156
+ mount_path = f'{self.prefix}/{folder.name}'
157
+
158
+ self.mount(mount_path, StaticFiles(directory=str(folder)), name=folder.name)
159
+ logger.info(f'Mounted static folder "{folder}" at {mount_path}')
160
+
161
+ self._discover_assets(folder, mount_path)
162
+ self._discover_favicon(folder, mount_path)
163
+
164
+ def _discover_assets(self, folder, mount_path):
165
+ asset_mapping = {
166
+ 'index.css': self.index_css,
167
+ 'index.js': self.index_js,
168
+ 'global.css': self.global_css,
169
+ 'global.js': self.global_js
170
+ }
171
+ for file in [f for f in folder.rglob('*.*') if f.name in asset_mapping]:
172
+ rel_path = file.relative_to(folder)
173
+ web_path = f'{mount_path}/{rel_path.as_posix()}'
174
+ asset_mapping[file.name].append(web_path)
175
+ logger.info(f'Discovered asset at "{web_path}"; will automatically be included in HTML.')
176
+
177
+ def _discover_favicon(self, folder, mount_path):
178
+ for file in folder.rglob('favicon.*'):
179
+ self.favicon = Icon.from_file(file, mount_path)
180
+ logger.info(f'Discovered favicon: {self.favicon}')
181
+ break
182
+
183
+ def register_pwa(self,
184
+ html: Optional[str | Path] = None,
185
+ css: Optional[str | list[str]] = None,
186
+ js: Optional[str | list[str]] = None,
187
+ app_name: Optional[str] = None,
188
+ app_description: Optional[str] = None,
189
+ icon: Optional[Icon] = None,
190
+ color: Optional[str] = None,
191
+ background_color: Optional[str] = '#FFFFFF',
192
+ dynamic_path = False):
193
+ @self.get(f'{self.prefix}/{self.pwa_id}.webmanifest', include_in_schema=False)
194
+ async def manifest(path: Optional[str] = self.prefix) -> Manifest:
195
+ return Manifest(
196
+ name=self.title,
197
+ short_name=self.title.replace(' ', ''),
198
+ description=self.summary,
199
+ start_url=path,
200
+ scope=path,
201
+ id=self.pwa_id,
202
+ display='standalone',
203
+ theme_color=color,
204
+ background_color=background_color,
205
+ icons=[self.favicon]
206
+ )
207
+
208
+ @self.get(f'{self.prefix}/service-worker.js', include_in_schema=False)
209
+ async def sw_js():
210
+ return HTMLResponse(content=SERVICE_WORKER, media_type='application/javascript')
211
+
212
+ app_name = app_name or self.title
213
+ route = f'{self.prefix}/{{path:path}}' if dynamic_path else f'{self.prefix}/'
214
+ @self.get(route, include_in_schema=False)
215
+ async def index(request: Request) -> HTMLResponse:
216
+ return HTMLResponse(self.template.render(
217
+ request=request,
218
+ prefix=self.prefix,
219
+ app_id=self.pwa_id,
220
+ app_title=app_name,
221
+ description=app_description or self.summary,
222
+ favicon=icon or self.favicon or None,
223
+ color=color,
224
+ css=ensure_list(css) + self.index_css + self.global_css,
225
+ js=ensure_list(js) + self.index_js + self.global_js,
226
+ body=Path(html).read_text(encoding='utf-8')
227
+ ))
228
+ logger.info(f'Registered Progressive Web App {app_name} at {route.replace('{path:path}', '*')}')
@@ -0,0 +1,64 @@
1
+ [build-system]
2
+ requires = [
3
+ "pdm-backend",
4
+ ]
5
+ build-backend = "pdm.backend"
6
+
7
+ [project]
8
+ name = "FastPWA"
9
+ dynamic = []
10
+ authors = [
11
+ { name = "Cody M Sommer", email = "bassmastacod@gmail.com" },
12
+ ]
13
+ description = "Make your FastAPI app installable on mobile devices."
14
+ keywords = [
15
+ "pwa",
16
+ "progressive",
17
+ "web",
18
+ "app",
19
+ "windows",
20
+ "android",
21
+ "iphone",
22
+ "apple",
23
+ "ios",
24
+ "safari",
25
+ ]
26
+ readme = "README.md"
27
+ requires-python = ">=3.7"
28
+ dependencies = [
29
+ "fastapi",
30
+ "pydantic",
31
+ "jinja2",
32
+ ]
33
+ classifiers = [
34
+ "License :: OSI Approved :: MIT License",
35
+ "Programming Language :: Python :: 3.7",
36
+ "Programming Language :: Python :: 3.8",
37
+ "Programming Language :: Python :: 3.9",
38
+ "Programming Language :: Python :: 3.10",
39
+ "Programming Language :: Python :: 3.11",
40
+ "Programming Language :: Python :: 3.12",
41
+ "Programming Language :: Python :: 3.13",
42
+ "Programming Language :: Python :: 3.14",
43
+ "Development Status :: 2 - Pre-Alpha",
44
+ "Intended Audience :: Developers",
45
+ "Topic :: Software Development :: Libraries",
46
+ "Typing :: Typed",
47
+ ]
48
+ version = "0.1.1b0"
49
+
50
+ [project.license]
51
+ text = "MIT"
52
+
53
+ [project.urls]
54
+ Repository = "https://github.com/BassMastaCod/FastPWA.git"
55
+ Issues = "https://github.com/BassMastaCod/FastPWA/issues"
56
+
57
+ [tool.pdm.version]
58
+ source = "scm"
59
+
60
+ [tool.pytest.ini_options]
61
+ pythonpath = "fastpwa"
62
+ addopts = [
63
+ "--import-mode=importlib",
64
+ ]