fastapi-reloader 1.0__py2.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.
- fastapi_reloader/__init__.py +4 -0
- fastapi_reloader/core.py +63 -0
- fastapi_reloader/patcher.py +43 -0
- fastapi_reloader/runtime.js +27 -0
- fastapi_reloader-1.0.dist-info/METADATA +84 -0
- fastapi_reloader-1.0.dist-info/RECORD +8 -0
- fastapi_reloader-1.0.dist-info/WHEEL +4 -0
- fastapi_reloader-1.0.dist-info/entry_points.txt +4 -0
fastapi_reloader/core.py
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
from asyncio import Queue, ensure_future, sleep
|
2
|
+
from collections import defaultdict
|
3
|
+
from itertools import count
|
4
|
+
from pathlib import Path
|
5
|
+
from typing import Literal
|
6
|
+
|
7
|
+
from fastapi import APIRouter, Response
|
8
|
+
from fastapi.responses import StreamingResponse
|
9
|
+
|
10
|
+
get_id = count().__next__
|
11
|
+
|
12
|
+
requests: dict[int, list[Queue[Literal[0, 1]]]] = defaultdict(list)
|
13
|
+
|
14
|
+
|
15
|
+
def send_reload_signal():
|
16
|
+
for subscribers in requests.values():
|
17
|
+
for queue in subscribers:
|
18
|
+
queue.put_nowait(1)
|
19
|
+
|
20
|
+
|
21
|
+
hmr_router = APIRouter(prefix="/---fastapi-reloader---", tags=["hmr"])
|
22
|
+
|
23
|
+
|
24
|
+
runtime_js = Path(__file__, "../runtime.js").read_text()
|
25
|
+
|
26
|
+
|
27
|
+
def get_js():
|
28
|
+
return runtime_js.replace("/0", f"/{get_id()}")
|
29
|
+
|
30
|
+
|
31
|
+
@hmr_router.head("")
|
32
|
+
async def heartbeat():
|
33
|
+
return Response(status_code=200)
|
34
|
+
|
35
|
+
|
36
|
+
@hmr_router.get("/{key:int}")
|
37
|
+
async def simple_refresh_trigger(key: int):
|
38
|
+
async def event_generator():
|
39
|
+
queue = Queue[Literal[0, 1]]()
|
40
|
+
|
41
|
+
stopped = False
|
42
|
+
|
43
|
+
async def heartbeat():
|
44
|
+
while not stopped:
|
45
|
+
queue.put_nowait(0)
|
46
|
+
await sleep(1)
|
47
|
+
|
48
|
+
requests[key].append(queue)
|
49
|
+
|
50
|
+
heartbeat_future = ensure_future(heartbeat())
|
51
|
+
|
52
|
+
try:
|
53
|
+
yield "0\n"
|
54
|
+
while True:
|
55
|
+
value = await queue.get()
|
56
|
+
yield f"{value}\n"
|
57
|
+
if value == 1:
|
58
|
+
break
|
59
|
+
finally:
|
60
|
+
heartbeat_future.cancel()
|
61
|
+
requests[key].remove(queue)
|
62
|
+
|
63
|
+
return StreamingResponse(event_generator(), media_type="text/plain")
|
@@ -0,0 +1,43 @@
|
|
1
|
+
from collections.abc import Awaitable, Callable
|
2
|
+
from typing import TypeGuard
|
3
|
+
|
4
|
+
from fastapi import FastAPI, Request, Response
|
5
|
+
from fastapi.responses import StreamingResponse
|
6
|
+
from starlette.middleware import Middleware
|
7
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
8
|
+
from starlette.types import ASGIApp
|
9
|
+
|
10
|
+
from .core import get_js, hmr_router
|
11
|
+
|
12
|
+
|
13
|
+
def is_streaming_response(response: Response) -> TypeGuard[StreamingResponse]:
|
14
|
+
# In fact, it may not be a fastapi's StreamingResponse, but a starlette's one with the same interface
|
15
|
+
return hasattr(response, "body_iterator")
|
16
|
+
|
17
|
+
|
18
|
+
def patch_for_auto_reloading(app: ASGIApp):
|
19
|
+
new_app = FastAPI(openapi_url=None)
|
20
|
+
new_app.include_router(hmr_router)
|
21
|
+
new_app.mount("/", app)
|
22
|
+
|
23
|
+
async def hmr_middleware(request: Request, call_next: Callable[[Request], Awaitable[Response]]):
|
24
|
+
res = await call_next(request)
|
25
|
+
|
26
|
+
if request.method != "GET" or "html" not in (res.headers.get("content-type", "")):
|
27
|
+
return res
|
28
|
+
|
29
|
+
async def response():
|
30
|
+
if is_streaming_response(res):
|
31
|
+
async for chunk in res.body_iterator:
|
32
|
+
yield chunk
|
33
|
+
else:
|
34
|
+
yield res.body
|
35
|
+
yield f"\n\n <script> {get_js()} </script>".encode()
|
36
|
+
|
37
|
+
headers = {k: v for k, v in res.headers.items() if k.lower() not in {"content-length", "content-encoding", "transfer-encoding"}}
|
38
|
+
|
39
|
+
return StreamingResponse(response(), res.status_code, headers, res.media_type)
|
40
|
+
|
41
|
+
new_app.user_middleware.append(Middleware(BaseHTTPMiddleware, dispatch=hmr_middleware)) # the last middleware is the first one to be called
|
42
|
+
|
43
|
+
return new_app
|
@@ -0,0 +1,27 @@
|
|
1
|
+
(async function () {
|
2
|
+
const response = await fetch("/---fastapi-reloader---/0");
|
3
|
+
const reader = response.body.getReader();
|
4
|
+
const decoder = new TextDecoder();
|
5
|
+
while (true) {
|
6
|
+
const { done, value } = await reader.read();
|
7
|
+
if (done) {
|
8
|
+
break;
|
9
|
+
}
|
10
|
+
if (value) {
|
11
|
+
const chunk = decoder.decode(value, { stream: true });
|
12
|
+
if (chunk.includes("1")) {
|
13
|
+
while (true) {
|
14
|
+
try {
|
15
|
+
const res = await fetch("/---fastapi-reloader---", {
|
16
|
+
method: "HEAD",
|
17
|
+
});
|
18
|
+
if (res.ok) {
|
19
|
+
break;
|
20
|
+
}
|
21
|
+
} catch (error) {}
|
22
|
+
}
|
23
|
+
location.reload();
|
24
|
+
}
|
25
|
+
}
|
26
|
+
}
|
27
|
+
})();
|
@@ -0,0 +1,84 @@
|
|
1
|
+
Metadata-Version: 2.1
|
2
|
+
Name: fastapi-reloader
|
3
|
+
Version: 1.0
|
4
|
+
Project-URL: Homepage, https://github.com/promplate/hmr
|
5
|
+
Requires-Dist: fastapi~=0.115
|
6
|
+
Requires-Dist: hmr~=0.4.0
|
7
|
+
Description-Content-Type: text/markdown
|
8
|
+
|
9
|
+
# FastAPI Reloader
|
10
|
+
|
11
|
+
[](https://pypi.org/project/fastapi-reloader/)
|
12
|
+
[](https://pepy.tech/projects/fastapi-reloader/)
|
13
|
+
|
14
|
+
A lightweight middleware ASGI applications that enables automatic browser page reloading during development.
|
15
|
+
|
16
|
+
## Features
|
17
|
+
|
18
|
+
- 🔄 Automatic browser refresh when code changes (work with `uvicorn-hmr`)
|
19
|
+
- 🚀 Works with any ASGI application
|
20
|
+
- 🔌 Simple integration with just two function calls
|
21
|
+
|
22
|
+
## Installation
|
23
|
+
|
24
|
+
```sh
|
25
|
+
pip install fastapi-reloader
|
26
|
+
```
|
27
|
+
|
28
|
+
For a more comprehensive development experience, consider using `uvicorn-hmr` which includes this package:
|
29
|
+
|
30
|
+
```sh
|
31
|
+
pip install uvicorn-hmr[all]
|
32
|
+
```
|
33
|
+
|
34
|
+
Then run your app with:
|
35
|
+
|
36
|
+
```sh
|
37
|
+
uvicorn-hmr main:app --reload
|
38
|
+
```
|
39
|
+
|
40
|
+
## Advanced Usage
|
41
|
+
|
42
|
+
### Manual Integration
|
43
|
+
|
44
|
+
```python
|
45
|
+
from fastapi import FastAPI
|
46
|
+
from fastapi_reloader import patch_for_auto_reloading
|
47
|
+
|
48
|
+
app = FastAPI() # or some other ASGI app
|
49
|
+
|
50
|
+
app = patch_for_auto_reloading(app) # this will return a new FastAPI app
|
51
|
+
```
|
52
|
+
|
53
|
+
### Manual Trigger
|
54
|
+
|
55
|
+
You can manually trigger a reload from your code:
|
56
|
+
|
57
|
+
```python
|
58
|
+
from fastapi_reloader import send_reload_signal
|
59
|
+
|
60
|
+
send_reload_signal() # When you need to trigger a reload
|
61
|
+
```
|
62
|
+
|
63
|
+
## How It Works
|
64
|
+
|
65
|
+
The package injects a small JavaScript snippet into your HTML responses that:
|
66
|
+
|
67
|
+
1. Opens a long-lived connection to the server
|
68
|
+
2. Listens for reload signals
|
69
|
+
3. Start polling for heartbeat when `send_reload_signal` is called
|
70
|
+
4. Reloads the page when heartbeat from new server is received
|
71
|
+
|
72
|
+
## Configuration
|
73
|
+
|
74
|
+
The package works out-of-the-box with default settings. No additional configuration is required.
|
75
|
+
|
76
|
+
## Limitations
|
77
|
+
|
78
|
+
- Unlike `uvicorn-hmr`, which does on-demand fine-grained reloading on the server side, this package simply reloads all the pages in the browser.
|
79
|
+
- Designed for development use only (not for production)
|
80
|
+
- Requires JavaScript to be enabled in the browser
|
81
|
+
|
82
|
+
## Contributing
|
83
|
+
|
84
|
+
Contributions are welcome! Please open an issue or submit a pull request.
|
@@ -0,0 +1,8 @@
|
|
1
|
+
fastapi_reloader-1.0.dist-info/METADATA,sha256=Jwsu7h94NyFlc-8qTGKmuHzJE2t-zG-9-SK5K6DuNKM,2260
|
2
|
+
fastapi_reloader-1.0.dist-info/WHEEL,sha256=EIrqRoDPs10x40oLVL4n4Pk5qQrfAB4xusd3Yd1GnUQ,94
|
3
|
+
fastapi_reloader-1.0.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
|
4
|
+
fastapi_reloader/__init__.py,sha256=VE5m-0M92Weme8COqLs0LwIx5g6FHhqyOLGAgY6Zkzs,145
|
5
|
+
fastapi_reloader/core.py,sha256=1U4CFQhu0I8Y8HqRfZBzI71WthGUF4D7MHIxp8_hg6U,1528
|
6
|
+
fastapi_reloader/patcher.py,sha256=d2wG3eD6Hs0URBhJag0uMeYq-QQP_YJZXOkOCGllp7s,1656
|
7
|
+
fastapi_reloader/runtime.js,sha256=ltpmyOLzDaUjjb2WcEldWpgyh69gvi4nCaRnTDjT1jc,676
|
8
|
+
fastapi_reloader-1.0.dist-info/RECORD,,
|