fastapi-reloader 1.3.2__py2.py3-none-any.whl → 1.3.4__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/core.py +5 -17
- fastapi_reloader/patcher.py +20 -2
- fastapi_reloader/runtime.js +20 -6
- {fastapi_reloader-1.3.2.dist-info → fastapi_reloader-1.3.4.dist-info}/METADATA +3 -1
- fastapi_reloader-1.3.4.dist-info/RECORD +8 -0
- fastapi_reloader-1.3.2.dist-info/RECORD +0 -8
- {fastapi_reloader-1.3.2.dist-info → fastapi_reloader-1.3.4.dist-info}/WHEEL +0 -0
- {fastapi_reloader-1.3.2.dist-info → fastapi_reloader-1.3.4.dist-info}/entry_points.txt +0 -0
fastapi_reloader/core.py
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
from asyncio import Queue, ensure_future, sleep
|
2
2
|
from collections import defaultdict
|
3
3
|
from itertools import count
|
4
|
-
from pathlib import Path
|
5
4
|
from typing import Literal
|
6
5
|
|
7
6
|
from fastapi import APIRouter, Response
|
@@ -22,26 +21,15 @@ def send_reload_signal():
|
|
22
21
|
reload_router = APIRouter(prefix="/---fastapi-reloader---", tags=["hmr"])
|
23
22
|
|
24
23
|
|
25
|
-
runtime_js = Path(__file__, "../runtime.js").resolve().read_text()
|
26
|
-
|
27
|
-
|
28
|
-
def get_js():
|
29
|
-
return runtime_js.replace("/0", f"/{get_id()}")
|
30
|
-
|
31
|
-
|
32
24
|
@reload_router.head("")
|
33
25
|
async def heartbeat():
|
34
|
-
return Response(status_code=
|
35
|
-
|
36
|
-
|
37
|
-
@reload_router.get("/poller.js")
|
38
|
-
async def get_poller_js():
|
39
|
-
return Response(get_js(), media_type="application/javascript")
|
26
|
+
return Response(status_code=202)
|
40
27
|
|
41
28
|
|
42
|
-
@reload_router.get("
|
43
|
-
async def
|
29
|
+
@reload_router.get("")
|
30
|
+
async def subscribe():
|
44
31
|
async def event_generator():
|
32
|
+
key = get_id()
|
45
33
|
queue = Queue[Literal[0, 1]]()
|
46
34
|
|
47
35
|
stopped = False
|
@@ -66,4 +54,4 @@ async def simple_refresh_trigger(key: int):
|
|
66
54
|
heartbeat_future.cancel()
|
67
55
|
requests[key].remove(queue)
|
68
56
|
|
69
|
-
return StreamingResponse(event_generator(), media_type="text/plain")
|
57
|
+
return StreamingResponse(event_generator(), 201, media_type="text/plain")
|
fastapi_reloader/patcher.py
CHANGED
@@ -1,7 +1,10 @@
|
|
1
1
|
from collections.abc import Awaitable, Callable
|
2
2
|
from contextlib import asynccontextmanager
|
3
3
|
from copy import copy
|
4
|
+
from inspect import ismethod
|
4
5
|
from math import inf
|
6
|
+
from pathlib import Path
|
7
|
+
from types import MethodType
|
5
8
|
from typing import Generic, TypeGuard, TypeVar
|
6
9
|
|
7
10
|
from asgi_lifespan import LifespanManager
|
@@ -20,12 +23,19 @@ def is_streaming_response(response: Response) -> TypeGuard[StreamingResponse]:
|
|
20
23
|
return hasattr(response, "body_iterator")
|
21
24
|
|
22
25
|
|
26
|
+
INJECTION = f"\n\n<script>\n{Path(__file__, '../runtime.js').resolve().read_text()}\n</script>".encode()
|
27
|
+
|
28
|
+
FLAG = " fastapi-reloader-injected " # to avoid double injection
|
29
|
+
|
30
|
+
|
23
31
|
async def _injection_http_middleware(request: Request, call_next: Callable[[Request], Awaitable[Response]]):
|
24
32
|
res = await call_next(request)
|
25
33
|
|
26
|
-
if request.method != "GET" or "html" not in (res.headers.get("content-type", "")) or res.headers.get("content-encoding", "identity") != "identity":
|
34
|
+
if request.scope.get(FLAG) or request.method != "GET" or "html" not in (res.headers.get("content-type", "")) or res.headers.get("content-encoding", "identity") != "identity":
|
27
35
|
return res
|
28
36
|
|
37
|
+
request.scope[FLAG] = True
|
38
|
+
|
29
39
|
async def response():
|
30
40
|
if is_streaming_response(res):
|
31
41
|
async for chunk in res.body_iterator:
|
@@ -33,7 +43,7 @@ async def _injection_http_middleware(request: Request, call_next: Callable[[Requ
|
|
33
43
|
else:
|
34
44
|
yield res.body
|
35
45
|
|
36
|
-
yield
|
46
|
+
yield INJECTION
|
37
47
|
|
38
48
|
headers = {k: v for k, v in res.headers.items() if k.lower() not in {"content-length", "transfer-encoding"}}
|
39
49
|
|
@@ -79,6 +89,14 @@ def patch_for_auto_reloading(app: ASGIApp): # this function is preserved for ba
|
|
79
89
|
if isinstance(app, Starlette): # both FastAPI and Starlette have user_middleware attribute
|
80
90
|
new_app = copy(app)
|
81
91
|
new_app.user_middleware = [*app.user_middleware, html_injection_middleware] # before compression middlewares
|
92
|
+
|
93
|
+
# OTEL patches the app's build_middleware_stack method and keep a reference to the original build_middleware_stack.
|
94
|
+
# But both methods are bound to the original app instance, so we need to rebind them to the new app instance.
|
95
|
+
# The following loop generically rebinds all these methods, preventing potential issues caused by similar patches.
|
96
|
+
for i in dir(new_app):
|
97
|
+
if ismethod(method := getattr(new_app, i)) and method.__self__ is app:
|
98
|
+
setattr(new_app, i, MethodType(method.__func__, new_app))
|
99
|
+
|
82
100
|
return _wrap_asgi_app(new_app)
|
83
101
|
|
84
102
|
new_app = _wrap_asgi_app(app)
|
fastapi_reloader/runtime.js
CHANGED
@@ -11,23 +11,37 @@ async function poll() {
|
|
11
11
|
}
|
12
12
|
}
|
13
13
|
|
14
|
-
|
15
|
-
|
14
|
+
/** @param {Response} response */
|
15
|
+
async function wait(response) {
|
16
16
|
const reader = response.body.getReader();
|
17
17
|
const decoder = new TextDecoder();
|
18
18
|
while (true) {
|
19
19
|
const { done, value } = await reader.read();
|
20
20
|
if (done) {
|
21
|
-
|
21
|
+
return;
|
22
22
|
}
|
23
23
|
if (value) {
|
24
24
|
const chunk = decoder.decode(value, { stream: true });
|
25
25
|
if (chunk.includes("1")) {
|
26
|
-
|
27
|
-
location.reload();
|
26
|
+
return;
|
28
27
|
}
|
29
28
|
}
|
30
29
|
}
|
31
30
|
}
|
32
31
|
|
33
|
-
main()
|
32
|
+
async function main() {
|
33
|
+
const response = await fetch("/---fastapi-reloader---").catch(() => null);
|
34
|
+
if (response?.ok && response.body) {
|
35
|
+
await wait(response).catch(() => null);
|
36
|
+
await poll();
|
37
|
+
location.reload();
|
38
|
+
} else {
|
39
|
+
await poll();
|
40
|
+
return await main();
|
41
|
+
}
|
42
|
+
}
|
43
|
+
|
44
|
+
if (!window.__fastapi_reloader_loaded) {
|
45
|
+
window.__fastapi_reloader_loaded = true;
|
46
|
+
main();
|
47
|
+
}
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: fastapi-reloader
|
3
|
-
Version: 1.3.
|
3
|
+
Version: 1.3.4
|
4
4
|
Project-URL: Homepage, https://github.com/promplate/hmr
|
5
5
|
Requires-Dist: asgi-lifespan~=2.0
|
6
6
|
Requires-Dist: fastapi~=0.115
|
@@ -74,6 +74,8 @@ app = html_injection_middleware(app)
|
|
74
74
|
app.user_middleware.append(html_injection_middleware)
|
75
75
|
```
|
76
76
|
|
77
|
+
> It's safe to add `html_injection_middleware` in multiple places, even if their scopes overlap. We have safeguards in place to prevent double injection on both server and client side.
|
78
|
+
|
77
79
|
The `auto_refresh_middleware` is a convenient wrapper that applies both `reloader_route_middleware` and `html_injection_middleware`. However, you can add them separately for more control:
|
78
80
|
|
79
81
|
- **Fine-grained control**: If a sub-router in your application uses compression, you must add `html_injection_middleware` before the compression middleware on that router.
|
@@ -0,0 +1,8 @@
|
|
1
|
+
fastapi_reloader-1.3.4.dist-info/METADATA,sha256=7h8S-serIhPPn1UnyUCuIJOA6NgOI-I6oUDQ9mu-xOs,6500
|
2
|
+
fastapi_reloader-1.3.4.dist-info/WHEEL,sha256=pz1FfwQ2kf9tI4G8U2ObRTKdvsTSmrreuBTtdnO8pJw,94
|
3
|
+
fastapi_reloader-1.3.4.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
|
4
|
+
fastapi_reloader/__init__.py,sha256=qWjX076aoLEZxZoOIvtmg84khic2FBXpAWuRWVbouTY,309
|
5
|
+
fastapi_reloader/core.py,sha256=cqB9uVdPa-0SMafaFH2Ho7m8npD1u503JIGX43oZ4es,1485
|
6
|
+
fastapi_reloader/patcher.py,sha256=KJjjbT0uDjZv1Q95slW3MBoUggPrMdCamjnlTy-iTKc,4133
|
7
|
+
fastapi_reloader/runtime.js,sha256=kG73Qx2civNzrG36Hfsu9U_esmjYh4EqynpVcmUqB8o,1036
|
8
|
+
fastapi_reloader-1.3.4.dist-info/RECORD,,
|
@@ -1,8 +0,0 @@
|
|
1
|
-
fastapi_reloader-1.3.2.dist-info/METADATA,sha256=D7-0CWUurSwKz-MdsPAQL-Tm4SE6Lg0X5V_3q3I2qSQ,6314
|
2
|
-
fastapi_reloader-1.3.2.dist-info/WHEEL,sha256=pz1FfwQ2kf9tI4G8U2ObRTKdvsTSmrreuBTtdnO8pJw,94
|
3
|
-
fastapi_reloader-1.3.2.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
|
4
|
-
fastapi_reloader/__init__.py,sha256=qWjX076aoLEZxZoOIvtmg84khic2FBXpAWuRWVbouTY,309
|
5
|
-
fastapi_reloader/core.py,sha256=pug78QfiDLaX7uBj9zbjAwphMlsFGlam0qEpUnHOhfo,1779
|
6
|
-
fastapi_reloader/patcher.py,sha256=MXZjva7zIdSYN4j5RGxBMy2UtJ34pSj3ylwzcKfoAKU,3318
|
7
|
-
fastapi_reloader/runtime.js,sha256=pUPeMnMuKGHhBTudp6bFuNTTyZhGSvrvvWy_lWVN8nA,713
|
8
|
-
fastapi_reloader-1.3.2.dist-info/RECORD,,
|
File without changes
|
File without changes
|