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.
- fastpwa-0.1.1b0/PKG-INFO +78 -0
- fastpwa-0.1.1b0/README.md +50 -0
- fastpwa-0.1.1b0/fastpwa/__init__.py +228 -0
- fastpwa-0.1.1b0/pyproject.toml +64 -0
fastpwa-0.1.1b0/PKG-INFO
ADDED
|
@@ -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
|
+
]
|