-
-
Notifications
You must be signed in to change notification settings - Fork 798
Add a FastAPI Positron bootstrap. #4156
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 7 commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
c22de33
Refactor Django backend to allow for better re-use.
freakboy3742 76444ec
Add initial FastAPI server implementation.
freakboy3742 794d15a
Block iOS/Android/Web for FastAPI, web deployment elsewhere.
freakboy3742 5376c6e
Add CI for FastAPI Positron bootstrap.
freakboy3742 be4d9fc
Correct the reference to the app in the template.
freakboy3742 6580744
Add a changenote.
freakboy3742 11067b9
Factor common tools into a Positron base class.
freakboy3742 c3f0809
Add support for iOS and Android.
freakboy3742 18aa98d
Correct bootstrap name annotation.
freakboy3742 52bfcfe
Ensure server starts before shutting down.
freakboy3742 8b5843f
Refactor static and site specific bootstraps to support copying initi…
freakboy3742 6001dd8
Add initial PyScript Positron backend.
freakboy3742 3b9370a
Add CI for PyScript backend.
freakboy3742 160f60f
Rework template handling to avoid duplicating shared templates.
freakboy3742 385306d
Make the dummy FastAPI response prettier.
freakboy3742 f6ff168
Cleanup ruff issues.
freakboy3742 7721755
Merge branch 'main' into fastapi
freakboy3742 4304932
Merge branch 'main' into fastapi
freakboy3742 603fc7b
Remove PyScript plugin from this branch.
freakboy3742 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Toga Positron now has a bootstrap for FastAPI-based websites. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| from __future__ import annotations | ||
|
|
||
| from pathlib import Path | ||
|
|
||
| from briefcase.bootstraps import TogaGuiBootstrap | ||
|
|
||
|
|
||
| class BasePositronBootstrap(TogaGuiBootstrap): | ||
| display_name_annotation = "does not support Web deployment" | ||
|
|
||
| @property | ||
| def template_path(self): | ||
| return Path(__file__).parent / "templates" | ||
|
|
||
| def validate_path(self, value: str) -> bool: | ||
| """Validate that the value is a valid path.""" | ||
| if not value.startswith("/"): | ||
| raise ValueError("Path must start with a /") | ||
| return True | ||
|
|
||
| def templated_content(self, template_name, **context): | ||
| """Render a template for `template.name` with the provided context.""" | ||
| template = (self.template_path / f"{template_name}.tmpl").read_text( | ||
| encoding="utf-8" | ||
| ) | ||
| return template.format(**context) | ||
|
|
||
| def templated_file(self, template_name, output_path, **context): | ||
| """Render a template for `template.name` with the provided context, saving the | ||
| result in `output_path`.""" | ||
| (output_path / template_name).write_text( | ||
| self.templated_content(template_name, **context), encoding="utf-8" | ||
| ) | ||
|
|
||
| def pyproject_table_web(self): | ||
| return """\ | ||
| supported = false | ||
| """ |
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| from __future__ import annotations | ||
|
|
||
| from pathlib import Path | ||
| from typing import Any | ||
|
|
||
| from ..base import BasePositronBootstrap | ||
|
|
||
|
|
||
| class FastAPIPositronBootstrap(BasePositronBootstrap): | ||
| display_name_annotation = "does not support iOS/Android/Web deployment" | ||
| # Need a pydantic-core binary to make iOS/Android possible. | ||
| # display_name_annotation = "does not support Web deployment" | ||
|
|
||
| @property | ||
| def template_path(self): | ||
| return Path(__file__).parent / "templates" | ||
|
|
||
| def app_source(self): | ||
| return self.templated_content("app.py", initial_path=self.initial_path) | ||
|
|
||
| def pyproject_table_briefcase_app_extra_content(self): | ||
| return """ | ||
| requires = [ | ||
| "fastAPI == 0.128.0", | ||
| "uvicorn == 0.40.0", | ||
| ] | ||
| test_requires = [ | ||
| {% if cookiecutter.test_framework == "pytest" %} | ||
| "pytest", | ||
| {% endif %} | ||
| ] | ||
| """ | ||
|
|
||
| def pyproject_table_iOS(self): | ||
| return """\ | ||
| supported = false | ||
| """ | ||
|
|
||
| def pyproject_table_android(self): | ||
| return """\ | ||
| supported = false | ||
| """ | ||
|
|
||
| def extra_context(self, project_overrides: dict[str, str]) -> dict[str, Any] | None: | ||
| """Runs prior to other plugin hooks to provide additional context. | ||
|
|
||
| This can be used to prompt the user with additional questions or run arbitrary | ||
| logic to supplement the context provided to cookiecutter. | ||
|
|
||
| :param project_overrides: Any overrides provided by the user as -Q options that | ||
| haven't been consumed by the standard bootstrap wizard questions. | ||
| """ | ||
| self.initial_path = self.console.text_question( | ||
| intro=( | ||
| "What path do you want to use as the initial URL for the app's " | ||
| "webview?\n" | ||
| "\n" | ||
| "The value should start with a '/', but can be any path that your " | ||
| "FastAPI site will serve." | ||
| ), | ||
| description="Initial path", | ||
| default="/", | ||
| validator=self.validate_path, | ||
| override_value=project_overrides.pop("initial_path", None), | ||
| ) | ||
|
|
||
| return {} | ||
|
|
||
| def post_generate(self, base_path: Path): | ||
| app_path = base_path / "src" / self.context["module_name"] | ||
|
|
||
| # App files | ||
| for template_name in ["site.py"]: | ||
| self.console.debug(f"Writing {template_name}") | ||
| self.templated_file( | ||
| template_name, | ||
| app_path, | ||
| module_name=self.context["module_name"], | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import asyncio | ||
| import os | ||
| import shutil | ||
| import socketserver | ||
|
|
||
| import toga | ||
| import uvicorn | ||
|
|
||
| from .site import app as fastapi_app | ||
|
|
||
|
|
||
| class {{{{ cookiecutter.class_name }}}}(toga.App): | ||
| async def cleanup(self, app, **kwargs): | ||
| print("Shutting down...") | ||
| await self.server.shutdown() | ||
| return True | ||
|
|
||
| def startup(self): | ||
| # Create a uvicorn server on 127.0.0.1, any available port | ||
| config = uvicorn.Config(fastapi_app, host="127.0.0.1", port=0) | ||
| self.server = uvicorn.Server(config) | ||
|
|
||
| # Start the server asynchronously | ||
| asyncio.create_task(self.server.serve()) | ||
|
|
||
| self.web_view = toga.WebView() | ||
|
|
||
| self.on_exit = self.cleanup | ||
|
|
||
| self.main_window = toga.MainWindow() | ||
| self.main_window.content = self.web_view | ||
|
|
||
| async def on_running(self): | ||
| # uvicorn doesn't provide a way to wait until the server is running, | ||
| # or to get the auto-allocated port. See: | ||
| # https://github.com/Kludex/uvicorn/issues/761 | ||
| while self.server is None or not self.server.started: # noqa: ASYNC110 | ||
| await asyncio.sleep(0.01) | ||
|
|
||
| for server in self.server.servers: | ||
| for socket in server.sockets: | ||
| host, port = socket.getsockname() | ||
| break | ||
|
|
||
| # Point the webview at the internal server. | ||
| self.web_view.url = f"http://{{host}}:{{port}}/" | ||
| self.main_window.show() | ||
|
|
||
|
|
||
| def main(): | ||
| return {{{{ cookiecutter.class_name }}}}() | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| from fastapi import FastAPI | ||
|
|
||
| app = FastAPI() | ||
|
|
||
|
|
||
| @app.get("/") | ||
| async def root(): | ||
| return "Hello World" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.