fastapi-reloader 1.3__py2.py3-none-any.whl → 1.3.2__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.
@@ -1,4 +1,4 @@
1
1
  from .core import send_reload_signal
2
- from .patcher import patch_for_auto_reloading
2
+ from .patcher import auto_refresh_middleware, html_injection_middleware, patch_for_auto_reloading, reloader_route_middleware
3
3
 
4
- __all__ = ["patch_for_auto_reloading", "send_reload_signal"]
4
+ __all__ = ["auto_refresh_middleware", "html_injection_middleware", "patch_for_auto_reloading", "reloader_route_middleware", "send_reload_signal"]
fastapi_reloader/core.py CHANGED
@@ -13,6 +13,7 @@ requests: dict[int, list[Queue[Literal[0, 1]]]] = defaultdict(list)
13
13
 
14
14
 
15
15
  def send_reload_signal():
16
+ """Broadcast a reload signal to all connected clients and break their long-polling connections."""
16
17
  for subscribers in requests.values():
17
18
  for queue in subscribers:
18
19
  queue.put_nowait(1)
@@ -1,11 +1,13 @@
1
1
  from collections.abc import Awaitable, Callable
2
2
  from contextlib import asynccontextmanager
3
+ from copy import copy
3
4
  from math import inf
4
- from typing import TypeGuard
5
+ from typing import Generic, TypeGuard, TypeVar
5
6
 
6
7
  from asgi_lifespan import LifespanManager
7
8
  from fastapi import FastAPI, Request, Response
8
9
  from fastapi.responses import StreamingResponse
10
+ from starlette.applications import Starlette
9
11
  from starlette.middleware import Middleware
10
12
  from starlette.middleware.base import BaseHTTPMiddleware
11
13
  from starlette.types import ASGIApp
@@ -14,11 +16,49 @@ from .core import reload_router
14
16
 
15
17
 
16
18
  def is_streaming_response(response: Response) -> TypeGuard[StreamingResponse]:
17
- # In fact, it may not be a fastapi's StreamingResponse, but a starlette's one with the same interface
19
+ # In fact, it may not be a Starlette's StreamingResponse, but an internal one with the same interface
18
20
  return hasattr(response, "body_iterator")
19
21
 
20
22
 
21
- def patch_for_auto_reloading(app: ASGIApp):
23
+ async def _injection_http_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", "")) or res.headers.get("content-encoding", "identity") != "identity":
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
+
36
+ yield b'\n\n <script src="/---fastapi-reloader---/poller.js"></script>'
37
+
38
+ headers = {k: v for k, v in res.headers.items() if k.lower() not in {"content-length", "transfer-encoding"}}
39
+
40
+ return StreamingResponse(response(), res.status_code, headers, res.media_type)
41
+
42
+
43
+ T = TypeVar("T", bound=ASGIApp)
44
+
45
+
46
+ class UniversalMiddleware(Middleware, Generic[T]):
47
+ """Adapt an ASGI middleware so it can serve both Starlette/FastAPI middleware slots and plain ASGI usage."""
48
+
49
+ def __init__(self, asgi_middleware: Callable[[ASGIApp], T]):
50
+ self.fn = asgi_middleware
51
+ super().__init__(self)
52
+
53
+ def __call__(self, app):
54
+ return self.fn(app)
55
+
56
+
57
+ html_injection_middleware = UniversalMiddleware(lambda app: BaseHTTPMiddleware(app, _injection_http_middleware))
58
+ """This middleware injects the HMR client script into HTML responses."""
59
+
60
+
61
+ def _wrap_asgi_app(app: ASGIApp):
22
62
  @asynccontextmanager
23
63
  async def lifespan(_):
24
64
  async with LifespanManager(app, inf, inf):
@@ -28,25 +68,24 @@ def patch_for_auto_reloading(app: ASGIApp):
28
68
  new_app.include_router(reload_router)
29
69
  new_app.mount("/", app)
30
70
 
31
- async def hmr_middleware(request: Request, call_next: Callable[[Request], Awaitable[Response]]):
32
- res = await call_next(request)
33
-
34
- if request.method != "GET" or "html" not in (res.headers.get("content-type", "")):
35
- return res
71
+ return new_app
36
72
 
37
- async def response():
38
- if is_streaming_response(res):
39
- async for chunk in res.body_iterator:
40
- yield chunk
41
- else:
42
- yield res.body
43
73
 
44
- yield b'\n\n <script src="/---fastapi-reloader---/poller.js"></script>'
74
+ reloader_route_middleware = UniversalMiddleware(_wrap_asgi_app)
75
+ """This middleware wraps the app with a FastAPI app that handles reload signals."""
45
76
 
46
- headers = {k: v for k, v in res.headers.items() if k.lower() not in {"content-length", "content-encoding", "transfer-encoding"}}
47
77
 
48
- return StreamingResponse(response(), res.status_code, headers, res.media_type)
78
+ def patch_for_auto_reloading(app: ASGIApp): # this function is preserved for backward compatibility
79
+ if isinstance(app, Starlette): # both FastAPI and Starlette have user_middleware attribute
80
+ new_app = copy(app)
81
+ new_app.user_middleware = [*app.user_middleware, html_injection_middleware] # before compression middlewares
82
+ return _wrap_asgi_app(new_app)
49
83
 
50
- new_app.user_middleware.append(Middleware(BaseHTTPMiddleware, dispatch=hmr_middleware)) # the last middleware is the first one to be called
84
+ new_app = _wrap_asgi_app(app)
85
+ new_app.user_middleware.append(html_injection_middleware) # the last middleware is the first one to be called
51
86
 
52
87
  return new_app
88
+
89
+
90
+ auto_refresh_middleware = UniversalMiddleware(patch_for_auto_reloading)
91
+ """This middleware combines the two middlewares above to enable the full functionality of this package."""
@@ -1,4 +1,17 @@
1
- (async function () {
1
+ async function poll() {
2
+ while (true) {
3
+ try {
4
+ const res = await fetch("/---fastapi-reloader---", { method: "HEAD" });
5
+ if (res.ok) {
6
+ break;
7
+ } else if (res.status !== 502) {
8
+ return;
9
+ }
10
+ } catch (error) {}
11
+ }
12
+ }
13
+
14
+ async function main() {
2
15
  const response = await fetch("/---fastapi-reloader---/0");
3
16
  const reader = response.body.getReader();
4
17
  const decoder = new TextDecoder();
@@ -10,20 +23,11 @@
10
23
  if (value) {
11
24
  const chunk = decoder.decode(value, { stream: true });
12
25
  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
- } else if (res.status !== 502) {
21
- return;
22
- }
23
- } catch (error) {}
24
- }
26
+ await poll();
25
27
  location.reload();
26
28
  }
27
29
  }
28
30
  }
29
- })();
31
+ }
32
+
33
+ main();
@@ -0,0 +1,160 @@
1
+ Metadata-Version: 2.1
2
+ Name: fastapi-reloader
3
+ Version: 1.3.2
4
+ Project-URL: Homepage, https://github.com/promplate/hmr
5
+ Requires-Dist: asgi-lifespan~=2.0
6
+ Requires-Dist: fastapi~=0.115
7
+ Description-Content-Type: text/markdown
8
+
9
+ # FastAPI Reloader
10
+
11
+ [![PyPI - Version](https://img.shields.io/pypi/v/fastapi-reloader)](https://pypi.org/project/fastapi-reloader/)
12
+ [![PyPI - Downloads](https://img.shields.io/pypi/dw/fastapi-reloader)](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 server restarts
19
+ - 🚀 Works with any ASGI application and any event loop
20
+ - 🔌 Simple integration with just two steps
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 --refresh
38
+ ```
39
+
40
+ ## Standalone Usage
41
+
42
+ You can also use `fastapi-reloader` as a standalone package without `uvicorn-hmr`. However, there are a few things to keep in mind.
43
+
44
+ It's important to understand the roles of the different packages:
45
+
46
+ - `uvicorn-hmr` Provides server-side hot module reloading (as a drop-in replacement for `uvicorn --reload`).
47
+ - `fastapi-reloader` Triggers a [browser page refresh](https://developer.mozilla.org/docs/Web/API/Location/reload "window.reload") when the server restarts.
48
+
49
+ If you configure `fastapi-reloader` manually and run your ASGI app with a standard reloader like `uvicorn --reload`, the behavior will be similar to `uvicorn-hmr --refresh` but with "cold" (process-restarting) server reloads instead of "hot" (in-process) reloads provided by [`hmr`](https://pyth-on-line.promplate.dev/hmr).
50
+
51
+ The manual integration steps are quite straightforward:
52
+
53
+ ### Step 1: Add Middleware
54
+
55
+ `fastapi-reloader` works by injecting a `<script>` tag into HTML responses.
56
+
57
+ ```python
58
+ from fastapi import FastAPI
59
+ from fastapi_reloader import auto_refresh_middleware
60
+
61
+ app = FastAPI()
62
+ app.add_middleware(auto_refresh_middleware)
63
+ ```
64
+
65
+ Note that this middleware must be placed **before** any compression middleware (like Starlette's `GZipMiddleware`).
66
+
67
+ If you can't place it as early as possible in the middleware stack, you can use the following ways:
68
+
69
+ ```python
70
+ # Approach 1: wrap your app with it (works with any ASGI app)
71
+ app = html_injection_middleware(app)
72
+
73
+ # Approach 2: manually add it to the user_middleware list (supported FastAPI, Starlette etc.)
74
+ app.user_middleware.append(html_injection_middleware)
75
+ ```
76
+
77
+ 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
+
79
+ - **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.
80
+ - **Scoped reloading**: If you only want to enable auto-reloading for a specific part of your app, you can apply `html_injection_middleware` only to that sub-router.
81
+
82
+ > The `reloader_route_middleware` mounts the necessary endpoints and should typically be added to the main application instance.
83
+
84
+ ```python
85
+ from fastapi import FastAPI
86
+ from starlette.middleware.gzip import GZipMiddleware
87
+ from fastapi_reloader import html_injection_middleware, reloader_route_middleware
88
+
89
+ app = FastAPI()
90
+ # Apply the reloader routes to the main app
91
+ app.add_middleware(reloader_route_middleware)
92
+
93
+ # Apply HTML injection middleware before compression ones
94
+ app.add_middleware(html_injection_middleware)
95
+ app.add_middleware(GZipMiddleware) # or BrotliMiddleware, ZstMiddleware, etc.
96
+ ```
97
+
98
+ FastAPI routers needs further configuration like this (because [`fastapi.APIRouter`](https://fastapi.tiangolo.com/reference/apirouter/) doesn't support middlewares directly):
99
+
100
+ ```python
101
+ app.mount(router.prefix, html_injection_middleware(router)) # place this first to shadows the next line
102
+ app.include_router(router) # this can't be removed because FastAPI needs it to generate OpenAPI schema
103
+ ```
104
+
105
+ ### Step 2: Manually Triggering Reloads
106
+
107
+ When used standalone, you **have to** add a few lines into your code to manually *trigger the reload signal* (aka. call `send_reload_signal()`) before your ASGI server shuts down (I mean, when a server receives a shutdown signal like SIGINT/SIGTERM or it want to restart because of code changes etc.). `fastapi-reloader` works with any ASGI server, but most of them wait for open connections to close before shutting down. The long-polling connection internally used by `uvicorn-hmr` won't close on its own (because we have no idea of when it will happen), preventing the server from shutting down gracefully (behaving like a deadlock).
108
+
109
+ So you need to hook into your ASGI server's shutdown process to call `send_reload_signal()`. This can be done through subclassing, monkey-patching, or using a library like [`dowhen`](https://github.com/gaogaotiantian/dowhen).
110
+
111
+ Here is an example for `uvicorn`:
112
+
113
+ ```python
114
+ from fastapi_reloader import send_reload_signal
115
+ import uvicorn
116
+
117
+ _shutdown = uvicorn.Server.shutdown
118
+
119
+ def shutdown(self, *args, **kwargs):
120
+ send_reload_signal()
121
+ return _shutdown(self, *args, **kwargs)
122
+
123
+ uvicorn.Server.shutdown = shutdown
124
+ ```
125
+
126
+ and start the server as normal
127
+
128
+ ```sh
129
+ uvicorn main:app --reload
130
+ ```
131
+
132
+ or this way:
133
+
134
+ ```python
135
+ if __name__ == "__main__":
136
+ uvicorn.run("main:app", reload=True)
137
+ ```
138
+
139
+ ## How It Works
140
+
141
+ The package injects a small JavaScript snippet into your HTML responses that:
142
+
143
+ 1. Opens a long-lived connection to the server
144
+ 2. Listens for reload signals
145
+ 3. Starts polling for heartbeat when `send_reload_signal` is called
146
+ 4. Reloads the page when heartbeat from new server is received
147
+
148
+ ## Configuration
149
+
150
+ The package works out-of-the-box with default settings. No additional configuration is required.
151
+
152
+ ## Limitations
153
+
154
+ - Unlike `uvicorn-hmr`, which does on-demand fine-grained reloading on the server side, this package simply reloads all the pages in the browser.
155
+ - Designed for development use only (not for production)
156
+ - Requires JavaScript to be enabled in the browser
157
+
158
+ ## Contributing
159
+
160
+ Contributions are welcome! Please open an issue or submit a pull request.
@@ -0,0 +1,8 @@
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,,
@@ -1,84 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: fastapi-reloader
3
- Version: 1.3
4
- Project-URL: Homepage, https://github.com/promplate/hmr
5
- Requires-Dist: asgi-lifespan~=2.0
6
- Requires-Dist: fastapi~=0.115
7
- Description-Content-Type: text/markdown
8
-
9
- # FastAPI Reloader
10
-
11
- [![PyPI - Version](https://img.shields.io/pypi/v/fastapi-reloader)](https://pypi.org/project/fastapi-reloader/)
12
- [![PyPI - Downloads](https://img.shields.io/pypi/dw/fastapi-reloader)](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.
@@ -1,8 +0,0 @@
1
- fastapi_reloader-1.3.dist-info/METADATA,sha256=QrT6bBHmbNP3BbLMDtW7M2nskhCK_AsXgZcELTC5oCk,2268
2
- fastapi_reloader-1.3.dist-info/WHEEL,sha256=pz1FfwQ2kf9tI4G8U2ObRTKdvsTSmrreuBTtdnO8pJw,94
3
- fastapi_reloader-1.3.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
4
- fastapi_reloader/__init__.py,sha256=VE5m-0M92Weme8COqLs0LwIx5g6FHhqyOLGAgY6Zkzs,145
5
- fastapi_reloader/core.py,sha256=eAyPhBZaFiKepy5NcU966adAJwqFTpHlK_uXn1cJQU8,1676
6
- fastapi_reloader/patcher.py,sha256=NOLTYVkbaZxRN6BSRRbsC0gUtdxn7OdARLZppcZ12jY,1921
7
- fastapi_reloader/runtime.js,sha256=H9SCpyGpBUmfvcdlzpU4RXnIBCYSQwIqXZ7jd1bdtSo,743
8
- fastapi_reloader-1.3.dist-info/RECORD,,