mirror of
https://github.com/fastapi/full-stack-fastapi-template.git
synced 2026-11-03 12:29:42 +00:00
♻️ Serve frontend from FastAPI (#2393)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
588dfd46e5
commit
e80d92452e
@@ -0,0 +1,11 @@
|
|||||||
|
.git
|
||||||
|
**/__pycache__
|
||||||
|
**/.venv
|
||||||
|
backend/app/frontend
|
||||||
|
backend/htmlcov
|
||||||
|
frontend/blob-report
|
||||||
|
frontend/dist
|
||||||
|
frontend/node_modules
|
||||||
|
frontend/playwright-report
|
||||||
|
frontend/test-results
|
||||||
|
node_modules
|
||||||
@@ -5,10 +5,11 @@ DOMAIN=localhost
|
|||||||
# To test the local Traefik config
|
# To test the local Traefik config
|
||||||
# DOMAIN=localhost.tiangolo.com
|
# DOMAIN=localhost.tiangolo.com
|
||||||
|
|
||||||
# Used by the backend to generate links in emails to the frontend
|
# Public frontend URL, used by the backend to generate links in emails.
|
||||||
|
# Keep the Vite URL for local development; deployments use the application URL.
|
||||||
FRONTEND_HOST=http://localhost:5173
|
FRONTEND_HOST=http://localhost:5173
|
||||||
# In staging and production, set this env var to the frontend host, e.g.
|
# In staging and production, set this env var to the frontend host, e.g.
|
||||||
# FRONTEND_HOST=https://dashboard.example.com
|
# FRONTEND_HOST=https://example.com
|
||||||
|
|
||||||
# Environment: local, staging, production
|
# Environment: local, staging, production
|
||||||
ENVIRONMENT=local
|
ENVIRONMENT=local
|
||||||
@@ -17,7 +18,7 @@ PROJECT_NAME="Full Stack FastAPI Project"
|
|||||||
STACK_NAME=full-stack-fastapi-project
|
STACK_NAME=full-stack-fastapi-project
|
||||||
|
|
||||||
# Backend
|
# Backend
|
||||||
BACKEND_CORS_ORIGINS="http://localhost,http://localhost:5173,https://localhost,https://localhost:5173,http://localhost.tiangolo.com"
|
BACKEND_CORS_ORIGINS="http://localhost,https://localhost,http://localhost.tiangolo.com"
|
||||||
SECRET_KEY=changethis
|
SECRET_KEY=changethis
|
||||||
FIRST_SUPERUSER=admin@example.com
|
FIRST_SUPERUSER=admin@example.com
|
||||||
FIRST_SUPERUSER_PASSWORD=changethis
|
FIRST_SUPERUSER_PASSWORD=changethis
|
||||||
@@ -42,4 +43,3 @@ SENTRY_DSN=
|
|||||||
|
|
||||||
# Configure these with your own Docker registry images
|
# Configure these with your own Docker registry images
|
||||||
DOCKER_IMAGE_BACKEND=backend
|
DOCKER_IMAGE_BACKEND=backend
|
||||||
DOCKER_IMAGE_FRONTEND=frontend
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
ENVIRONMENT: production
|
ENVIRONMENT: production
|
||||||
DOMAIN: ${{ secrets.DOMAIN_PRODUCTION }}
|
DOMAIN: ${{ secrets.DOMAIN_PRODUCTION }}
|
||||||
|
FRONTEND_HOST: https://${{ secrets.DOMAIN_PRODUCTION }}
|
||||||
STACK_NAME: ${{ secrets.STACK_NAME_PRODUCTION }}
|
STACK_NAME: ${{ secrets.STACK_NAME_PRODUCTION }}
|
||||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||||
FIRST_SUPERUSER: ${{ secrets.FIRST_SUPERUSER }}
|
FIRST_SUPERUSER: ${{ secrets.FIRST_SUPERUSER }}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
ENVIRONMENT: staging
|
ENVIRONMENT: staging
|
||||||
DOMAIN: ${{ secrets.DOMAIN_STAGING }}
|
DOMAIN: ${{ secrets.DOMAIN_STAGING }}
|
||||||
|
FRONTEND_HOST: https://${{ secrets.DOMAIN_STAGING }}
|
||||||
STACK_NAME: ${{ secrets.STACK_NAME_STAGING }}
|
STACK_NAME: ${{ secrets.STACK_NAME_STAGING }}
|
||||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||||
FIRST_SUPERUSER: ${{ secrets.FIRST_SUPERUSER }}
|
FIRST_SUPERUSER: ${{ secrets.FIRST_SUPERUSER }}
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- run: docker compose build
|
- run: docker compose build
|
||||||
- run: docker compose down -v --remove-orphans
|
- run: docker compose down -v --remove-orphans
|
||||||
- run: docker compose up -d --wait backend frontend adminer
|
- run: docker compose up -d --wait backend adminer
|
||||||
- name: Test backend is up
|
- name: Test backend is up
|
||||||
run: curl http://localhost:8000/api/v1/utils/health-check
|
run: curl http://localhost:8000/api/v1/utils/health-check
|
||||||
- name: Test frontend is up
|
- name: Test frontend is up
|
||||||
run: curl http://localhost:5173
|
run: curl http://localhost:8000
|
||||||
- run: docker compose down -v --remove-orphans
|
- run: docker compose down -v --remove-orphans
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
.vscode/*
|
.vscode/*
|
||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
node_modules/
|
node_modules/
|
||||||
|
backend/app/frontend/
|
||||||
/test-results/
|
/test-results/
|
||||||
/playwright-report/
|
/playwright-report/
|
||||||
/blob-report/
|
/blob-report/
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
- 🔍 [Pydantic](https://docs.pydantic.dev), used by FastAPI, for the data validation and settings management.
|
- 🔍 [Pydantic](https://docs.pydantic.dev), used by FastAPI, for the data validation and settings management.
|
||||||
- 💾 [PostgreSQL](https://www.postgresql.org) as the SQL database.
|
- 💾 [PostgreSQL](https://www.postgresql.org) as the SQL database.
|
||||||
- 🚀 [React](https://react.dev) for the frontend.
|
- 🚀 [React](https://react.dev) for the frontend.
|
||||||
|
- 🧩 Built into the backend image and served by FastAPI on the same domain as the API.
|
||||||
- 💃 Using TypeScript, hooks, [Vite](https://vitejs.dev), and other parts of a modern frontend stack.
|
- 💃 Using TypeScript, hooks, [Vite](https://vitejs.dev), and other parts of a modern frontend stack.
|
||||||
- 🎨 [Tailwind CSS](https://tailwindcss.com) and [shadcn/ui](https://ui.shadcn.com) for the frontend components.
|
- 🎨 [Tailwind CSS](https://tailwindcss.com) and [shadcn/ui](https://ui.shadcn.com) for the frontend components.
|
||||||
- 🤖 An automatically generated frontend client.
|
- 🤖 An automatically generated frontend client.
|
||||||
@@ -23,7 +24,7 @@
|
|||||||
- 📬 [Mailcatcher](https://mailcatcher.me) for local email testing during development.
|
- 📬 [Mailcatcher](https://mailcatcher.me) for local email testing during development.
|
||||||
- ✅ Tests with [Pytest](https://pytest.org).
|
- ✅ Tests with [Pytest](https://pytest.org).
|
||||||
- 📞 [Traefik](https://traefik.io) as a reverse proxy / load balancer.
|
- 📞 [Traefik](https://traefik.io) as a reverse proxy / load balancer.
|
||||||
- 🚢 Deployment instructions using Docker Compose, including how to set up a frontend Traefik proxy to handle automatic HTTPS certificates.
|
- 🚢 Deployment instructions using Docker Compose, including how to set up Traefik to handle automatic HTTPS certificates.
|
||||||
- 🏭 CI (continuous integration) and CD (continuous deployment) based on GitHub Actions.
|
- 🏭 CI (continuous integration) and CD (continuous deployment) based on GitHub Actions.
|
||||||
|
|
||||||
### Dashboard Login
|
### Dashboard Login
|
||||||
|
|||||||
@@ -1,3 +1,21 @@
|
|||||||
|
FROM oven/bun:1 AS frontend-build
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json bun.lock /app/
|
||||||
|
COPY frontend/package.json /app/frontend/
|
||||||
|
|
||||||
|
WORKDIR /app/frontend
|
||||||
|
|
||||||
|
RUN bun install
|
||||||
|
|
||||||
|
COPY ./frontend /app/frontend
|
||||||
|
|
||||||
|
ARG VITE_API_URL=
|
||||||
|
|
||||||
|
RUN bun run build
|
||||||
|
|
||||||
|
|
||||||
FROM python:3.14
|
FROM python:3.14
|
||||||
|
|
||||||
ENV PYTHONUNBUFFERED=1
|
ENV PYTHONUNBUFFERED=1
|
||||||
@@ -33,6 +51,8 @@ COPY ./backend/pyproject.toml ./backend/alembic.ini /app/backend/
|
|||||||
|
|
||||||
COPY ./backend/app /app/backend/app
|
COPY ./backend/app /app/backend/app
|
||||||
|
|
||||||
|
COPY --from=frontend-build /app/backend/app/frontend /app/backend/app/frontend
|
||||||
|
|
||||||
# Sync the project
|
# Sync the project
|
||||||
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#intermediate-layers
|
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#intermediate-layers
|
||||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import sentry_sdk
|
import sentry_sdk
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.routing import APIRoute
|
from fastapi.routing import APIRoute
|
||||||
@@ -6,6 +8,8 @@ from starlette.middleware.cors import CORSMiddleware
|
|||||||
from app.api.main import api_router
|
from app.api.main import api_router
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
|
|
||||||
|
FRONTEND_DIR = Path(__file__).parent / "frontend"
|
||||||
|
|
||||||
|
|
||||||
def custom_generate_unique_id(route: APIRoute) -> str:
|
def custom_generate_unique_id(route: APIRoute) -> str:
|
||||||
return f"{route.tags[0]}-{route.name}"
|
return f"{route.tags[0]}-{route.name}"
|
||||||
@@ -31,3 +35,4 @@ if settings.all_cors_origins:
|
|||||||
)
|
)
|
||||||
|
|
||||||
app.include_router(api_router, prefix=settings.API_V1_STR)
|
app.include_router(api_router, prefix=settings.API_V1_STR)
|
||||||
|
app.frontend("/", directory=FRONTEND_DIR)
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
set -e
|
set -e
|
||||||
set -x
|
set -x
|
||||||
|
|
||||||
coverage run -m pytest tests/
|
FASTAPI_ENV=development coverage run -m pytest tests/
|
||||||
coverage report
|
coverage report
|
||||||
coverage html --title "${@-coverage}"
|
coverage html --title "${@-coverage}"
|
||||||
|
|||||||
+9
-14
@@ -1,8 +1,7 @@
|
|||||||
services:
|
services:
|
||||||
|
|
||||||
# Local services are available on their ports, but also available on:
|
# Local services are available on their ports, but also available on:
|
||||||
# http://api.localhost.tiangolo.com: backend
|
# http://localhost.tiangolo.com: application
|
||||||
# http://dashboard.localhost.tiangolo.com: frontend
|
|
||||||
# etc. To enable it, update .env, set:
|
# etc. To enable it, update .env, set:
|
||||||
# DOMAIN=localhost.tiangolo.com
|
# DOMAIN=localhost.tiangolo.com
|
||||||
proxy:
|
proxy:
|
||||||
@@ -78,6 +77,13 @@ services:
|
|||||||
- .venv
|
- .venv
|
||||||
- path: ./backend/pyproject.toml
|
- path: ./backend/pyproject.toml
|
||||||
action: rebuild
|
action: rebuild
|
||||||
|
- path: ./frontend
|
||||||
|
action: rebuild
|
||||||
|
ignore:
|
||||||
|
- ./frontend/node_modules
|
||||||
|
- ./frontend/dist
|
||||||
|
- ./frontend/blob-report
|
||||||
|
- ./frontend/test-results
|
||||||
# TODO: remove once coverage is done locally
|
# TODO: remove once coverage is done locally
|
||||||
volumes:
|
volumes:
|
||||||
- ./backend/htmlcov:/app/backend/htmlcov
|
- ./backend/htmlcov:/app/backend/htmlcov
|
||||||
@@ -93,23 +99,11 @@ services:
|
|||||||
- "1080:1080"
|
- "1080:1080"
|
||||||
- "1025:1025"
|
- "1025:1025"
|
||||||
|
|
||||||
frontend:
|
|
||||||
restart: "no"
|
|
||||||
ports:
|
|
||||||
- "5173:80"
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: frontend/Dockerfile
|
|
||||||
args:
|
|
||||||
- VITE_API_URL=http://localhost:8000
|
|
||||||
- NODE_ENV=development
|
|
||||||
|
|
||||||
playwright:
|
playwright:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: frontend/Dockerfile.playwright
|
dockerfile: frontend/Dockerfile.playwright
|
||||||
args:
|
args:
|
||||||
- VITE_API_URL=http://backend:8000
|
|
||||||
- NODE_ENV=production
|
- NODE_ENV=production
|
||||||
ipc: host
|
ipc: host
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -118,6 +112,7 @@ services:
|
|||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
|
- PLAYWRIGHT_BASE_URL=http://backend:8000
|
||||||
- VITE_API_URL=http://backend:8000
|
- VITE_API_URL=http://backend:8000
|
||||||
- MAILCATCHER_HOST=http://mailcatcher:1080
|
- MAILCATCHER_HOST=http://mailcatcher:1080
|
||||||
# For the reports when run locally
|
# For the reports when run locally
|
||||||
|
|||||||
+2
-32
@@ -124,46 +124,16 @@ services:
|
|||||||
|
|
||||||
- traefik.http.services.${STACK_NAME?Variable not set}-backend.loadbalancer.server.port=8000
|
- traefik.http.services.${STACK_NAME?Variable not set}-backend.loadbalancer.server.port=8000
|
||||||
|
|
||||||
- traefik.http.routers.${STACK_NAME?Variable not set}-backend-http.rule=Host(`api.${DOMAIN?Variable not set}`)
|
- traefik.http.routers.${STACK_NAME?Variable not set}-backend-http.rule=Host(`${DOMAIN?Variable not set}`)
|
||||||
- traefik.http.routers.${STACK_NAME?Variable not set}-backend-http.entrypoints=http
|
- traefik.http.routers.${STACK_NAME?Variable not set}-backend-http.entrypoints=http
|
||||||
|
|
||||||
- traefik.http.routers.${STACK_NAME?Variable not set}-backend-https.rule=Host(`api.${DOMAIN?Variable not set}`)
|
- traefik.http.routers.${STACK_NAME?Variable not set}-backend-https.rule=Host(`${DOMAIN?Variable not set}`)
|
||||||
- traefik.http.routers.${STACK_NAME?Variable not set}-backend-https.entrypoints=https
|
- traefik.http.routers.${STACK_NAME?Variable not set}-backend-https.entrypoints=https
|
||||||
- traefik.http.routers.${STACK_NAME?Variable not set}-backend-https.tls=true
|
- traefik.http.routers.${STACK_NAME?Variable not set}-backend-https.tls=true
|
||||||
- traefik.http.routers.${STACK_NAME?Variable not set}-backend-https.tls.certresolver=le
|
- traefik.http.routers.${STACK_NAME?Variable not set}-backend-https.tls.certresolver=le
|
||||||
|
|
||||||
# Enable redirection for HTTP and HTTPS
|
# Enable redirection for HTTP and HTTPS
|
||||||
- traefik.http.routers.${STACK_NAME?Variable not set}-backend-http.middlewares=https-redirect
|
- traefik.http.routers.${STACK_NAME?Variable not set}-backend-http.middlewares=https-redirect
|
||||||
|
|
||||||
frontend:
|
|
||||||
image: '${DOCKER_IMAGE_FRONTEND?Variable not set}:${TAG-latest}'
|
|
||||||
restart: always
|
|
||||||
networks:
|
|
||||||
- traefik-public
|
|
||||||
- default
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: frontend/Dockerfile
|
|
||||||
args:
|
|
||||||
- VITE_API_URL=https://api.${DOMAIN?Variable not set}
|
|
||||||
- NODE_ENV=production
|
|
||||||
labels:
|
|
||||||
- traefik.enable=true
|
|
||||||
- traefik.docker.network=traefik-public
|
|
||||||
- traefik.constraint-label=traefik-public
|
|
||||||
|
|
||||||
- traefik.http.services.${STACK_NAME?Variable not set}-frontend.loadbalancer.server.port=80
|
|
||||||
|
|
||||||
- traefik.http.routers.${STACK_NAME?Variable not set}-frontend-http.rule=Host(`dashboard.${DOMAIN?Variable not set}`)
|
|
||||||
- traefik.http.routers.${STACK_NAME?Variable not set}-frontend-http.entrypoints=http
|
|
||||||
|
|
||||||
- traefik.http.routers.${STACK_NAME?Variable not set}-frontend-https.rule=Host(`dashboard.${DOMAIN?Variable not set}`)
|
|
||||||
- traefik.http.routers.${STACK_NAME?Variable not set}-frontend-https.entrypoints=https
|
|
||||||
- traefik.http.routers.${STACK_NAME?Variable not set}-frontend-https.tls=true
|
|
||||||
- traefik.http.routers.${STACK_NAME?Variable not set}-frontend-https.tls.certresolver=le
|
|
||||||
|
|
||||||
# Enable redirection for HTTP and HTTPS
|
|
||||||
- traefik.http.routers.${STACK_NAME?Variable not set}-frontend-http.middlewares=https-redirect
|
|
||||||
volumes:
|
volumes:
|
||||||
app-db-data:
|
app-db-data:
|
||||||
|
|
||||||
|
|||||||
+9
-14
@@ -11,8 +11,7 @@ But you have to configure a couple things first. 🤓
|
|||||||
## Preparation
|
## Preparation
|
||||||
|
|
||||||
* Have a remote server ready and available.
|
* Have a remote server ready and available.
|
||||||
* Configure the DNS records of your domain to point to the IP of the server you just created.
|
* Configure DNS records pointing to the server for the application domain and any supporting service subdomains you want to expose, e.g. `fastapi-project.example.com`, `traefik.fastapi-project.example.com`, and `adminer.fastapi-project.example.com`. Do the same for staging domains such as `staging.fastapi-project.example.com`.
|
||||||
* Configure a wildcard subdomain for your domain, so that you can have multiple subdomains for different services, e.g. `*.fastapi-project.example.com`. This will be useful for accessing different components, like `dashboard.fastapi-project.example.com`, `api.fastapi-project.example.com`, `traefik.fastapi-project.example.com`, `adminer.fastapi-project.example.com`, etc. And also for `staging`, like `dashboard.staging.fastapi-project.example.com`, `adminer.staging.fastapi-project.example.com`, etc.
|
|
||||||
* Install and configure [Docker](https://docs.docker.com/engine/install/) on the remote server (Docker Engine, not Docker Desktop).
|
* Install and configure [Docker](https://docs.docker.com/engine/install/) on the remote server (Docker Engine, not Docker Desktop).
|
||||||
|
|
||||||
## Public Traefik
|
## Public Traefik
|
||||||
@@ -112,7 +111,7 @@ Now that you have Traefik in place you can deploy your FastAPI project with Dock
|
|||||||
## Copy the Code
|
## Copy the Code
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
rsync -av --filter=":- .gitignore" ./ root@your-server.example.com:/root/code/app/
|
rsync -av --exclude=".git/" --filter=":- .gitignore" ./ root@your-server.example.com:/root/code/app/
|
||||||
```
|
```
|
||||||
|
|
||||||
Note: `--filter=":- .gitignore"` tells `rsync` to use the same rules as git, ignore files ignored by git, like the Python virtual environment.
|
Note: `--filter=":- .gitignore"` tells `rsync` to use the same rules as git, ignore files ignored by git, like the Python virtual environment.
|
||||||
@@ -167,17 +166,17 @@ Set the `FIRST_SUPER_USER_PASSWORD` to something different than `changethis`:
|
|||||||
export FIRST_SUPERUSER_PASSWORD="changethis"
|
export FIRST_SUPERUSER_PASSWORD="changethis"
|
||||||
```
|
```
|
||||||
|
|
||||||
Set the `BACKEND_CORS_ORIGINS` to include your domain:
|
Set the `FRONTEND_HOST` to the application URL. It is used to generate links in emails:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export BACKEND_CORS_ORIGINS="https://dashboard.${DOMAIN?Variable not set},https://api.${DOMAIN?Variable not set}"
|
export FRONTEND_HOST="https://${DOMAIN?Variable not set}"
|
||||||
```
|
```
|
||||||
|
|
||||||
You can set several other environment variables:
|
You can set several other environment variables:
|
||||||
|
|
||||||
* `PROJECT_NAME`: The name of the project, used in the API for the docs and emails.
|
* `PROJECT_NAME`: The name of the project, used in the API for the docs and emails.
|
||||||
* `STACK_NAME`: The name of the stack used for Docker Compose labels and project name, this should be different for `staging`, `production`, etc. You could use the same domain replacing dots with dashes, e.g. `fastapi-project-example-com` and `staging-fastapi-project-example-com`.
|
* `STACK_NAME`: The name of the stack used for Docker Compose labels and project name, this should be different for `staging`, `production`, etc. You could use the same domain replacing dots with dashes, e.g. `fastapi-project-example-com` and `staging-fastapi-project-example-com`.
|
||||||
* `BACKEND_CORS_ORIGINS`: A list of allowed CORS origins separated by commas.
|
* `BACKEND_CORS_ORIGINS`: A list of additional allowed CORS origins separated by commas. The frontend served by FastAPI uses the same origin and doesn't need to be added.
|
||||||
* `FIRST_SUPERUSER`: The email of the first superuser, this superuser will be the one that can create new users.
|
* `FIRST_SUPERUSER`: The email of the first superuser, this superuser will be the one that can create new users.
|
||||||
* `SMTP_HOST`: The SMTP server host to send emails, this would come from your email provider (E.g. Mailgun, Sparkpost, Sendgrid, etc).
|
* `SMTP_HOST`: The SMTP server host to send emails, this would come from your email provider (E.g. Mailgun, Sparkpost, Sendgrid, etc).
|
||||||
* `SMTP_USER`: The SMTP server user to send emails.
|
* `SMTP_USER`: The SMTP server user to send emails.
|
||||||
@@ -333,20 +332,16 @@ Traefik UI: `https://traefik.fastapi-project.example.com`
|
|||||||
|
|
||||||
### Production
|
### Production
|
||||||
|
|
||||||
Frontend: `https://dashboard.fastapi-project.example.com`
|
Application (frontend and API): `https://fastapi-project.example.com`
|
||||||
|
|
||||||
Backend API docs: `https://api.fastapi-project.example.com/docs`
|
Interactive API docs: `https://fastapi-project.example.com/docs`
|
||||||
|
|
||||||
Backend API base URL: `https://api.fastapi-project.example.com`
|
|
||||||
|
|
||||||
Adminer: `https://adminer.fastapi-project.example.com`
|
Adminer: `https://adminer.fastapi-project.example.com`
|
||||||
|
|
||||||
### Staging
|
### Staging
|
||||||
|
|
||||||
Frontend: `https://dashboard.staging.fastapi-project.example.com`
|
Application (frontend and API): `https://staging.fastapi-project.example.com`
|
||||||
|
|
||||||
Backend API docs: `https://api.staging.fastapi-project.example.com/docs`
|
Interactive API docs: `https://staging.fastapi-project.example.com/docs`
|
||||||
|
|
||||||
Backend API base URL: `https://api.staging.fastapi-project.example.com`
|
|
||||||
|
|
||||||
Adminer: `https://adminer.staging.fastapi-project.example.com`
|
Adminer: `https://adminer.staging.fastapi-project.example.com`
|
||||||
|
|||||||
+15
-27
@@ -10,11 +10,9 @@ docker compose watch
|
|||||||
|
|
||||||
* Now you can open your browser and interact with these URLs:
|
* Now you can open your browser and interact with these URLs:
|
||||||
|
|
||||||
Frontend, built with Docker, with routes handled based on the path: <http://localhost:5173>
|
Application, with the frontend and API served by FastAPI: <http://localhost:8000>
|
||||||
|
|
||||||
Backend, JSON based web API based on OpenAPI: <http://localhost:8000>
|
Automatic interactive API documentation with Swagger UI: <http://localhost:8000/docs>
|
||||||
|
|
||||||
Automatic interactive documentation with Swagger UI (from the OpenAPI backend): <http://localhost:8000/docs>
|
|
||||||
|
|
||||||
Adminer, database web administration: <http://localhost:8080>
|
Adminer, database web administration: <http://localhost:8080>
|
||||||
|
|
||||||
@@ -48,19 +46,13 @@ The backend is automatically configured to use Mailcatcher when running with Doc
|
|||||||
|
|
||||||
## Local Development
|
## Local Development
|
||||||
|
|
||||||
The Docker Compose files are configured so that each of the services is available in a different port in `localhost`.
|
The Docker Compose files are configured so that each supporting service is available in a different port in `localhost`.
|
||||||
|
|
||||||
For the backend and frontend, they use the same port that would be used by their local development server, so, the backend is at `http://localhost:8000` and the frontend at `http://localhost:5173`.
|
FastAPI serves the built frontend and the API as one application at `http://localhost:8000`. The API routes live under `/api`.
|
||||||
|
|
||||||
This way, you could turn off a Docker Compose service and start its local development service, and everything would keep working, because it all uses the same ports.
|
For frontend development with live reload, you can still run the local Vite development server separately.
|
||||||
|
|
||||||
For example, you can stop that `frontend` service in the Docker Compose, in another terminal, run:
|
Start the local frontend development server with:
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose stop frontend
|
|
||||||
```
|
|
||||||
|
|
||||||
And then start the local frontend development server:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bun run dev
|
bun run dev
|
||||||
@@ -81,11 +73,11 @@ fastapi dev app/main.py
|
|||||||
|
|
||||||
## Docker Compose in `localhost.tiangolo.com`
|
## Docker Compose in `localhost.tiangolo.com`
|
||||||
|
|
||||||
When you start the Docker Compose stack, it uses `localhost` by default, with different ports for each service (backend, frontend, adminer, etc).
|
When you start the Docker Compose stack, it uses `localhost` by default, with different ports for each service (backend, adminer, etc).
|
||||||
|
|
||||||
When you deploy it to production (or staging), it will deploy each service in a different subdomain, like `api.example.com` for the backend and `dashboard.example.com` for the frontend.
|
When you deploy it to production (or staging), the application uses one domain. The frontend is served at `/` and the API lives under `/api`.
|
||||||
|
|
||||||
In the guide about [deployment](deployment.md) you can read about Traefik, the configured proxy. That's the component in charge of transmitting traffic to each service based on the subdomain.
|
In the guide about [deployment](deployment.md) you can read about Traefik, the configured proxy. That's the component in charge of transmitting traffic to the application service based on the domain.
|
||||||
|
|
||||||
If you want to test that it's all working locally, you can edit the local `.env` file, and change:
|
If you want to test that it's all working locally, you can edit the local `.env` file, and change:
|
||||||
|
|
||||||
@@ -95,7 +87,7 @@ DOMAIN=localhost.tiangolo.com
|
|||||||
|
|
||||||
That will be used by the Docker Compose files to configure the base domain for the services.
|
That will be used by the Docker Compose files to configure the base domain for the services.
|
||||||
|
|
||||||
Traefik will use this to transmit traffic at `api.localhost.tiangolo.com` to the backend, and traffic at `dashboard.localhost.tiangolo.com` to the frontend.
|
Traefik will transmit application traffic at `localhost.tiangolo.com` to FastAPI, which serves both the frontend and API.
|
||||||
|
|
||||||
The domain `localhost.tiangolo.com` is a special domain that is configured (with all its subdomains) to point to `127.0.0.1`. This way you can use that for your local development.
|
The domain `localhost.tiangolo.com` is a special domain that is configured (with all its subdomains) to point to `127.0.0.1`. This way you can use that for your local development.
|
||||||
|
|
||||||
@@ -105,7 +97,7 @@ After you update it, run again:
|
|||||||
docker compose watch
|
docker compose watch
|
||||||
```
|
```
|
||||||
|
|
||||||
When deploying, for example in production, the main Traefik is configured outside of the Docker Compose files. For local development, there's an included Traefik in `compose.override.yml`, just to let you test that the domains work as expected, for example with `api.localhost.tiangolo.com` and `dashboard.localhost.tiangolo.com`.
|
When deploying, for example in production, the main Traefik is configured outside of the Docker Compose files. For local development, there's an included Traefik in `compose.override.yml`, just to let you test that the domain works as expected, for example with `localhost.tiangolo.com`.
|
||||||
|
|
||||||
## Docker Compose files and env vars
|
## Docker Compose files and env vars
|
||||||
|
|
||||||
@@ -188,9 +180,7 @@ The production or staging URLs would use these same paths, but with your own dom
|
|||||||
|
|
||||||
Development URLs, for local development.
|
Development URLs, for local development.
|
||||||
|
|
||||||
Frontend: <http://localhost:5173>
|
Application: <http://localhost:8000>
|
||||||
|
|
||||||
Backend: <http://localhost:8000>
|
|
||||||
|
|
||||||
Automatic Interactive Docs (Swagger UI): <http://localhost:8000/docs>
|
Automatic Interactive Docs (Swagger UI): <http://localhost:8000/docs>
|
||||||
|
|
||||||
@@ -206,13 +196,11 @@ MailCatcher: <http://localhost:1080>
|
|||||||
|
|
||||||
Development URLs, for local development.
|
Development URLs, for local development.
|
||||||
|
|
||||||
Frontend: <http://dashboard.localhost.tiangolo.com>
|
Application: <http://localhost.tiangolo.com>
|
||||||
|
|
||||||
Backend: <http://api.localhost.tiangolo.com>
|
Automatic Interactive Docs (Swagger UI): <http://localhost.tiangolo.com/docs>
|
||||||
|
|
||||||
Automatic Interactive Docs (Swagger UI): <http://api.localhost.tiangolo.com/docs>
|
Automatic Alternative Docs (ReDoc): <http://localhost.tiangolo.com/redoc>
|
||||||
|
|
||||||
Automatic Alternative Docs (ReDoc): <http://api.localhost.tiangolo.com/redoc>
|
|
||||||
|
|
||||||
Adminer: <http://localhost.tiangolo.com:8080>
|
Adminer: <http://localhost.tiangolo.com:8080>
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
# Stage 0, "build-stage", based on Bun, to build and compile the frontend
|
|
||||||
FROM oven/bun:1 AS build-stage
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY package.json bun.lock /app/
|
|
||||||
|
|
||||||
COPY frontend/package.json /app/frontend/
|
|
||||||
|
|
||||||
WORKDIR /app/frontend
|
|
||||||
|
|
||||||
RUN bun install
|
|
||||||
|
|
||||||
COPY ./frontend /app/frontend
|
|
||||||
ARG VITE_API_URL
|
|
||||||
|
|
||||||
RUN bun run build
|
|
||||||
|
|
||||||
|
|
||||||
# Stage 1, based on Nginx, to have only the compiled app, ready for production with Nginx
|
|
||||||
FROM nginx:1
|
|
||||||
|
|
||||||
COPY --from=build-stage /app/frontend/dist/ /usr/share/nginx/html
|
|
||||||
|
|
||||||
COPY ./frontend/nginx.conf /etc/nginx/conf.d/default.conf
|
|
||||||
COPY ./frontend/nginx-backend-not-found.conf /etc/nginx/extra-conf.d/backend-not-found.conf
|
|
||||||
@@ -17,5 +17,3 @@ WORKDIR /app/frontend
|
|||||||
RUN bun install
|
RUN bun install
|
||||||
|
|
||||||
COPY ./frontend /app/frontend
|
COPY ./frontend /app/frontend
|
||||||
|
|
||||||
ARG VITE_API_URL
|
|
||||||
|
|||||||
+8
-7
@@ -15,7 +15,7 @@ bun run dev
|
|||||||
|
|
||||||
* Then open your browser at http://localhost:5173/.
|
* Then open your browser at http://localhost:5173/.
|
||||||
|
|
||||||
Notice that this live server is not running inside Docker, it's for local development, and that is the recommended workflow. Once you are happy with your frontend, you can build the frontend Docker image and start it, to test it in a production-like environment. But building the image at every change will not be as productive as running the local development server with live reload.
|
Notice that this live server is not running inside Docker, it's for local development, and that is the recommended workflow. Once you are happy with your frontend, you can build the backend Docker image and start it, to test the production-like setup where FastAPI serves the built frontend at `http://localhost:8000`.
|
||||||
|
|
||||||
Check the file `package.json` to see other available options.
|
Check the file `package.json` to see other available options.
|
||||||
|
|
||||||
@@ -25,18 +25,19 @@ If you are developing an API-only app and want to remove the frontend, you can d
|
|||||||
|
|
||||||
* Remove the `./frontend` directory.
|
* Remove the `./frontend` directory.
|
||||||
|
|
||||||
* In the `compose.yml` file, remove the whole service / section `frontend`.
|
* In the `backend/app/main.py` file, remove the `app.frontend()` call.
|
||||||
|
|
||||||
* In the `compose.override.yml` file, remove the whole service / section `frontend` and `playwright`.
|
* In the `backend/Dockerfile` file, remove the frontend build stage and the `COPY --from=frontend-build` instruction.
|
||||||
|
|
||||||
|
* In the `compose.override.yml` file, remove the `playwright` service.
|
||||||
|
|
||||||
Done, you have a frontend-less (api-only) app. 🤓
|
Done, you have a frontend-less (api-only) app. 🤓
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
If you want, you can also remove the `FRONTEND` environment variables from:
|
If you want, you can also remove the `FRONTEND_HOST` environment variable from:
|
||||||
|
|
||||||
* `.env`
|
* `.env`
|
||||||
* `./scripts/*.sh`
|
|
||||||
|
|
||||||
But it would be only to clean them up, leaving them won't really have any effect either way.
|
But it would be only to clean them up, leaving them won't really have any effect either way.
|
||||||
|
|
||||||
@@ -71,10 +72,10 @@ Notice that everytime the backend changes (changing the OpenAPI schema), you sho
|
|||||||
|
|
||||||
## Using a Remote API
|
## Using a Remote API
|
||||||
|
|
||||||
If you want to use a remote API, you can set the environment variable `VITE_API_URL` to the URL of the remote API. For example, you can set it in the `frontend/.env` file:
|
By default, the built frontend uses the same origin as the FastAPI app. If you want to use a remote API while running the Vite development server, you can set the environment variable `VITE_API_URL` to the URL of the remote API. For example, you can set it in the `frontend/.env` file:
|
||||||
|
|
||||||
```env
|
```env
|
||||||
VITE_API_URL=https://api.my-domain.example.com
|
VITE_API_URL=https://my-domain.example.com
|
||||||
```
|
```
|
||||||
|
|
||||||
Then, when you run the frontend, it will use that URL as the base URL for the API.
|
Then, when you run the frontend, it will use that URL as the base URL for the API.
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
location /api {
|
|
||||||
return 404;
|
|
||||||
}
|
|
||||||
location /docs {
|
|
||||||
return 404;
|
|
||||||
}
|
|
||||||
location /redoc {
|
|
||||||
return 404;
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
server {
|
|
||||||
listen 80;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
root /usr/share/nginx/html;
|
|
||||||
index index.html index.htm;
|
|
||||||
try_files $uri /index.html =404;
|
|
||||||
}
|
|
||||||
|
|
||||||
include /etc/nginx/extra-conf.d/*.conf;
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import { defineConfig, devices } from '@playwright/test';
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
import 'dotenv/config'
|
import 'dotenv/config'
|
||||||
|
|
||||||
|
const baseURL = process.env.PLAYWRIGHT_BASE_URL ?? 'http://localhost:5173'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read environment variables from file.
|
* Read environment variables from file.
|
||||||
* https://github.com/motdotla/dotenv
|
* https://github.com/motdotla/dotenv
|
||||||
@@ -24,7 +26,7 @@ export default defineConfig({
|
|||||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||||
use: {
|
use: {
|
||||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||||
baseURL: 'http://localhost:5173',
|
baseURL,
|
||||||
|
|
||||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||||
trace: 'on-first-retry',
|
trace: 'on-first-retry',
|
||||||
@@ -83,9 +85,11 @@ export default defineConfig({
|
|||||||
],
|
],
|
||||||
|
|
||||||
/* Run your local dev server before starting the tests */
|
/* Run your local dev server before starting the tests */
|
||||||
webServer: {
|
webServer: process.env.PLAYWRIGHT_BASE_URL
|
||||||
command: 'bun run dev',
|
? undefined
|
||||||
url: 'http://localhost:5173',
|
: {
|
||||||
reuseExistingServer: !process.env.CI,
|
command: 'bun run dev',
|
||||||
},
|
url: baseURL,
|
||||||
|
reuseExistingServer: !process.env.CI,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { Toaster } from "./components/ui/sonner"
|
|||||||
import "./index.css"
|
import "./index.css"
|
||||||
import { routeTree } from "./routeTree.gen"
|
import { routeTree } from "./routeTree.gen"
|
||||||
|
|
||||||
OpenAPI.BASE = import.meta.env.VITE_API_URL
|
OpenAPI.BASE = import.meta.env.VITE_API_URL ?? ""
|
||||||
OpenAPI.TOKEN = async () => {
|
OpenAPI.TOKEN = async () => {
|
||||||
return localStorage.getItem("access_token") || ""
|
return localStorage.getItem("access_token") || ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import { PasswordInput } from "@/components/ui/password-input"
|
|||||||
import useAuth, { isLoggedIn } from "@/hooks/useAuth"
|
import useAuth, { isLoggedIn } from "@/hooks/useAuth"
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
username: z.email(),
|
username: z.email({ message: "Invalid email address" }),
|
||||||
password: z
|
password: z
|
||||||
.string()
|
.string()
|
||||||
.min(1, { message: "Password is required" })
|
.min(1, { message: "Password is required" })
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import useCustomToast from "@/hooks/useCustomToast"
|
|||||||
import { handleError } from "@/utils"
|
import { handleError } from "@/utils"
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
email: z.email(),
|
email: z.email({ message: "Invalid email address" }),
|
||||||
})
|
})
|
||||||
|
|
||||||
type FormData = z.infer<typeof formSchema>
|
type FormData = z.infer<typeof formSchema>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import useAuth, { isLoggedIn } from "@/hooks/useAuth"
|
|||||||
|
|
||||||
const formSchema = z
|
const formSchema = z
|
||||||
.object({
|
.object({
|
||||||
email: z.email(),
|
email: z.email({ message: "Invalid email address" }),
|
||||||
full_name: z.string().min(1, { message: "Full Name is required" }),
|
full_name: z.string().min(1, { message: "Full Name is required" }),
|
||||||
password: z
|
password: z
|
||||||
.string()
|
.string()
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {
|
||||||
readonly VITE_API_URL: string
|
readonly VITE_API_URL?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ImportMeta {
|
interface ImportMeta {
|
||||||
|
|||||||
@@ -56,13 +56,11 @@ test("User can reset password successfully using the link", async ({
|
|||||||
|
|
||||||
const selector = 'a[href*="/reset-password?token="]'
|
const selector = 'a[href*="/reset-password?token="]'
|
||||||
|
|
||||||
let url = await page.getAttribute(selector, "href")
|
const url = await page.getAttribute(selector, "href")
|
||||||
|
const resetUrl = new URL(url!)
|
||||||
// TODO: update var instead of doing a replace
|
|
||||||
url = url!.replace("http://localhost/", "http://localhost:5173/")
|
|
||||||
|
|
||||||
// Set the new password and confirm it
|
// Set the new password and confirm it
|
||||||
await page.goto(url)
|
await page.goto(`${resetUrl.pathname}${resetUrl.search}`)
|
||||||
|
|
||||||
await page.getByTestId("new-password-input").fill(newPassword)
|
await page.getByTestId("new-password-input").fill(newPassword)
|
||||||
await page.getByTestId("confirm-password-input").fill(newPassword)
|
await page.getByTestId("confirm-password-input").fill(newPassword)
|
||||||
@@ -110,11 +108,11 @@ test("Weak new password validation", async ({ page, request }) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const selector = 'a[href*="/reset-password?token="]'
|
const selector = 'a[href*="/reset-password?token="]'
|
||||||
let url = await page.getAttribute(selector, "href")
|
const url = await page.getAttribute(selector, "href")
|
||||||
url = url!.replace("http://localhost/", "http://localhost:5173/")
|
const resetUrl = new URL(url!)
|
||||||
|
|
||||||
// Set a weak new password
|
// Set a weak new password
|
||||||
await page.goto(url)
|
await page.goto(`${resetUrl.pathname}${resetUrl.search}`)
|
||||||
await page.getByTestId("new-password-input").fill(weakPassword)
|
await page.getByTestId("new-password-input").fill(weakPassword)
|
||||||
await page.getByTestId("confirm-password-input").fill(weakPassword)
|
await page.getByTestId("confirm-password-input").fill(weakPassword)
|
||||||
await page.getByRole("button", { name: "Reset Password" }).click()
|
await page.getByRole("button", { name: "Reset Password" }).click()
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ import { defineConfig } from "vite"
|
|||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
build: {
|
||||||
|
outDir: "../backend/app/frontend",
|
||||||
|
emptyOutDir: true,
|
||||||
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
"@": path.resolve(__dirname, "./src"),
|
"@": path.resolve(__dirname, "./src"),
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ extend-exclude = [
|
|||||||
"bun.lock",
|
"bun.lock",
|
||||||
"backend/coverage/",
|
"backend/coverage/",
|
||||||
"backend/htmlcov/",
|
"backend/htmlcov/",
|
||||||
|
"backend/app/frontend/",
|
||||||
"frontend/dist/",
|
"frontend/dist/",
|
||||||
"frontend/public/assets/images/",
|
"frontend/public/assets/images/",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ set -e
|
|||||||
set -x
|
set -x
|
||||||
|
|
||||||
cd backend
|
cd backend
|
||||||
uv run python -c "import app.main; import json; print(json.dumps(app.main.app.openapi()))" > ../openapi.json
|
FASTAPI_ENV=development uv run python -c "import app.main; import json; print(json.dumps(app.main.app.openapi()))" > ../openapi.json
|
||||||
cd ..
|
cd ..
|
||||||
mv openapi.json frontend/
|
mv openapi.json frontend/
|
||||||
bun run --filter frontend generate-client
|
bun run --filter frontend generate-client
|
||||||
|
|||||||
@@ -413,7 +413,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastapi"
|
name = "fastapi"
|
||||||
version = "0.139.0"
|
version = "0.141.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "annotated-doc" },
|
{ name = "annotated-doc" },
|
||||||
@@ -422,9 +422,9 @@ dependencies = [
|
|||||||
{ name = "typing-extensions" },
|
{ name = "typing-extensions" },
|
||||||
{ name = "typing-inspection" },
|
{ name = "typing-inspection" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/d3/af/a5f50ccfa659ec1802cb4ca842c23f06d906a8cc9aef6016a2caeea3d4ed/fastapi-0.139.0.tar.gz", hash = "sha256:99ab7b2d92223c76d6cf10757ab3f89d45b38267fc20b2a136cf02f6beac3145", size = 423016, upload-time = "2026-07-01T16:35:33.436Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/8a/02/91e3416a8fdd715abb903a952a6bec7cdd8d14eed55d415fc8595524c319/fastapi-0.141.1.tar.gz", hash = "sha256:e8822fc40db1e1858054d7a949a888695bc9bdce70139178e33bd2871a453ca1", size = 425799, upload-time = "2026-07-29T17:18:05.568Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/9e/7c/8e3c6ad324ea5cb36604fc3f968554887891c316d9dfde57761611d907ad/fastapi-0.139.0-py3-none-any.whl", hash = "sha256:cf15e1e9e667ddb0ad63811e60bd11390d1aac838ca4a7a23f421807b2308189", size = 130339, upload-time = "2026-07-01T16:35:32.19Z" },
|
{ url = "https://files.pythonhosted.org/packages/cb/03/10388a42375ee7e4ac9b94eb2c5c569c8b5795e377e701c9ac3ad63de890/fastapi-0.141.1-py3-none-any.whl", hash = "sha256:bfb91aa2d334c61cb35ba9a116fc123b3d3df31640b801cf57a7a78ec3f603b3", size = 131954, upload-time = "2026-07-29T17:18:04.364Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.optional-dependencies]
|
[package.optional-dependencies]
|
||||||
@@ -442,16 +442,16 @@ standard = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastapi-cli"
|
name = "fastapi-cli"
|
||||||
version = "0.0.27"
|
version = "0.0.32"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "rich-toolkit" },
|
{ name = "rich-toolkit" },
|
||||||
{ name = "typer" },
|
{ name = "typer" },
|
||||||
{ name = "uvicorn", extra = ["standard"] },
|
{ name = "uvicorn", extra = ["standard"] },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/37/d0/ee5678346811967b8d096d5d5604e71b50d6bf5a2abfbdb331157e2bbaa9/fastapi_cli-0.0.27.tar.gz", hash = "sha256:1dffb1e40c0c88f2e0171a8a252a2b615c1e63ff8c05626649e4badd6a84336a", size = 23630, upload-time = "2026-06-18T14:48:43.421Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/33/eb/3b534c6f8e157f9ddbf2a153512307c886cad0b258739c200dd8ff8c4452/fastapi_cli-0.0.32.tar.gz", hash = "sha256:38024d2345275e1b37ce8848727a580d84901b570e96b3256d9d36a9a5039424", size = 26636, upload-time = "2026-07-16T12:16:58.678Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/1a/ab/0a709f9488fe62647db80f8a277fb0ee62e85adc6746abf477ed373c9eb7/fastapi_cli-0.0.27-py3-none-any.whl", hash = "sha256:2e389a40f318e29fec8cb1e289f267f17c048876fb82dbfa869a10b16740495d", size = 13070, upload-time = "2026-06-18T14:48:44.311Z" },
|
{ url = "https://files.pythonhosted.org/packages/d5/53/56ae5ae17bb0a5d89d1d31e5320eb1865553ebbfbde91cdc4c221245f2a8/fastapi_cli-0.0.32-py3-none-any.whl", hash = "sha256:8dcc286fa32f01bbd3f65dd09cfd5a2540ed5f2230b77db7fd30978d6165f3c4", size = 14670, upload-time = "2026-07-16T12:16:57.297Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.optional-dependencies]
|
[package.optional-dependencies]
|
||||||
@@ -529,7 +529,9 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/c7/89/aaafc8e14de4ac882e02ccb963225329b0e8578aba4365e71eb678e45722/greenlet-3.5.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:1c31219badba285858ba8ed117f403dea7fafee6bade9a1991875aae530c3ceb", size = 287676, upload-time = "2026-06-17T17:33:31.514Z" },
|
{ url = "https://files.pythonhosted.org/packages/c7/89/aaafc8e14de4ac882e02ccb963225329b0e8578aba4365e71eb678e45722/greenlet-3.5.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:1c31219badba285858ba8ed117f403dea7fafee6bade9a1991875aae530c3ceb", size = 287676, upload-time = "2026-06-17T17:33:31.514Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b8/fc/2308249206c12ac70de7b9a00970f84f07d10b3cd60e05d2fbcaa84124e8/greenlet-3.5.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6f96ed6f4adc1066954ae95f45717657cb67468ef3b89e9a3632e14a625a8f39", size = 653552, upload-time = "2026-06-17T18:07:23.493Z" },
|
{ url = "https://files.pythonhosted.org/packages/b8/fc/2308249206c12ac70de7b9a00970f84f07d10b3cd60e05d2fbcaa84124e8/greenlet-3.5.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6f96ed6f4adc1066954ae95f45717657cb67468ef3b89e9a3632e14a625a8f39", size = 653552, upload-time = "2026-06-17T18:07:23.493Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/7c/24/47730d1f8f1336b9b089237521ed7a26eee997065dcb4cab81cdca333abc/greenlet-3.5.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5795e883e915333c0d5648faaa691857fbc7180136883edc377f50f0d509c2a8", size = 665756, upload-time = "2026-06-17T18:29:46.616Z" },
|
{ url = "https://files.pythonhosted.org/packages/7c/24/47730d1f8f1336b9b089237521ed7a26eee997065dcb4cab81cdca333abc/greenlet-3.5.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5795e883e915333c0d5648faaa691857fbc7180136883edc377f50f0d509c2a8", size = 665756, upload-time = "2026-06-17T18:29:46.616Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/23/5c/2664d290cbd1fef9eb3f69b5d3bc5aa91b6fa907519298ca6af93a90c6cb/greenlet-3.5.2-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6e9e49d732ee92a189bb7035e293029244aeba648297a9b856dc733d17ca7f0d", size = 669989, upload-time = "2026-06-17T18:39:30.79Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/99/69/d6c99db15dc0b5e892ac3cc7b942c8b21f4a9cc3bd9ea0bc3b0f339ffbd4/greenlet-3.5.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26aed8d9503ca78889141a9739d71b383efea5f472a7c522b5410f7eb2a1b163", size = 663228, upload-time = "2026-06-17T17:39:31.073Z" },
|
{ url = "https://files.pythonhosted.org/packages/99/69/d6c99db15dc0b5e892ac3cc7b942c8b21f4a9cc3bd9ea0bc3b0f339ffbd4/greenlet-3.5.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26aed8d9503ca78889141a9739d71b383efea5f472a7c522b5410f7eb2a1b163", size = 663228, upload-time = "2026-06-17T17:39:31.073Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/42/d4/fcb53fa9847d7fbd4723fbed9469c3869b9e3544c4e001d9d5aa2f66162d/greenlet-3.5.2-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:537c5c4f30395020bb9f48f53146070e3b997c3c75da14011ab732aaa19ce3ef", size = 472888, upload-time = "2026-06-17T18:41:22.511Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/4f/88/9e603f448e2bc107c883e95817b980fb9b45ba6aea0299b2e9978124bea2/greenlet-3.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:dbebc038fcdda8f8f21cce985fd04e34e0f42007e7fc7ab7ad285caf77974b95", size = 1620723, upload-time = "2026-06-17T18:22:14.817Z" },
|
{ url = "https://files.pythonhosted.org/packages/4f/88/9e603f448e2bc107c883e95817b980fb9b45ba6aea0299b2e9978124bea2/greenlet-3.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:dbebc038fcdda8f8f21cce985fd04e34e0f42007e7fc7ab7ad285caf77974b95", size = 1620723, upload-time = "2026-06-17T18:22:14.817Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/11/91/26da17e3777858c16fdb8d020a4c68f3a03cb92f238de8f5351d5d5186e9/greenlet-3.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a207023f1cf8695fd82580b8099c09c5809be18bc2282362cdfb965dd884a317", size = 1684227, upload-time = "2026-06-17T17:40:09.536Z" },
|
{ url = "https://files.pythonhosted.org/packages/11/91/26da17e3777858c16fdb8d020a4c68f3a03cb92f238de8f5351d5d5186e9/greenlet-3.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a207023f1cf8695fd82580b8099c09c5809be18bc2282362cdfb965dd884a317", size = 1684227, upload-time = "2026-06-17T17:40:09.536Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/2d/44/b3a11f7aa34cb38f1b7f3df8bcd9fcd09bac9d342c2a2c9b8686c804bcd2/greenlet-3.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:c674a1dd4fe41f6a93febe7ab366ceabf15080ea31a9307811c56dac5f435f73", size = 240257, upload-time = "2026-06-17T17:35:23.359Z" },
|
{ url = "https://files.pythonhosted.org/packages/2d/44/b3a11f7aa34cb38f1b7f3df8bcd9fcd09bac9d342c2a2c9b8686c804bcd2/greenlet-3.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:c674a1dd4fe41f6a93febe7ab366ceabf15080ea31a9307811c56dac5f435f73", size = 240257, upload-time = "2026-06-17T17:35:23.359Z" },
|
||||||
@@ -537,14 +539,18 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/47/ac/d3bad483e9f6cd1848604fdffa32cac25846dd6dfcec0e6f81c790185518/greenlet-3.5.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:a96457a30384de52d9c5d2fd33abf6c1daae3db392cd556738f408b1a79a1cf0", size = 295668, upload-time = "2026-06-17T17:36:02.293Z" },
|
{ url = "https://files.pythonhosted.org/packages/47/ac/d3bad483e9f6cd1848604fdffa32cac25846dd6dfcec0e6f81c790185518/greenlet-3.5.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:a96457a30384de52d9c5d2fd33abf6c1daae3db392cd556738f408b1a79a1cf0", size = 295668, upload-time = "2026-06-17T17:36:02.293Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/00/e9/3a7e557b895fd0469b00cd0b2bd498ba950e8bfdf6d7adeecf2c5e4130a6/greenlet-3.5.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4af5d4961818ab651d09c1448a03b1ba2a1726a076266ebb62330bab9f3238c", size = 652820, upload-time = "2026-06-17T18:07:24.95Z" },
|
{ url = "https://files.pythonhosted.org/packages/00/e9/3a7e557b895fd0469b00cd0b2bd498ba950e8bfdf6d7adeecf2c5e4130a6/greenlet-3.5.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4af5d4961818ab651d09c1448a03b1ba2a1726a076266ebb62330bab9f3238c", size = 652820, upload-time = "2026-06-17T18:07:24.95Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/78/67/6225d5c5e4afc04be0fd161eec82e4b72017e8a100d222f25d7b42b0140d/greenlet-3.5.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a1789a6244ea1ba61fd4386c9a6a31873e9b0234762103364be98ef87dcb19f3", size = 658697, upload-time = "2026-06-17T18:29:48.365Z" },
|
{ url = "https://files.pythonhosted.org/packages/78/67/6225d5c5e4afc04be0fd161eec82e4b72017e8a100d222f25d7b42b0140d/greenlet-3.5.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a1789a6244ea1ba61fd4386c9a6a31873e9b0234762103364be98ef87dcb19f3", size = 658697, upload-time = "2026-06-17T18:29:48.365Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/35/ad/9b3058f999b81750a9c6d9ec424f509462d232b58002086fe2ba63b66407/greenlet-3.5.2-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2ee6288f1933d698b4f098127ed17bda2910a75d2807915bd16294a972055d6c", size = 658945, upload-time = "2026-06-17T18:39:32.509Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/fa/99/6324b8ef916dcaddccb340b304c992ca3f947614ce0f2685d438187300b8/greenlet-3.5.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3be00501fb4a8c37f6b4b3c4773808ceb26ea65c7ea64fd5735d0f330b3786de", size = 656436, upload-time = "2026-06-17T17:39:32.509Z" },
|
{ url = "https://files.pythonhosted.org/packages/fa/99/6324b8ef916dcaddccb340b304c992ca3f947614ce0f2685d438187300b8/greenlet-3.5.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3be00501fb4a8c37f6b4b3c4773808ceb26ea65c7ea64fd5735d0f330b3786de", size = 656436, upload-time = "2026-06-17T17:39:32.509Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/92/75/1b6ecd8c027b69ab1b6798a84094df79aab5e69ac7e249c78b9d361dd1fa/greenlet-3.5.2-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:b4cad42662c796334c2d24607c411e3ed82481c1fb4e1e8ec3a5a8416060092e", size = 490529, upload-time = "2026-06-17T18:41:23.954Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a9/ee/f5bf9daac27c5e1b011965f64b5630a32b415daf7381b312943629e12c2a/greenlet-3.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1d554cd96841a68d464d75a3736f8e87408a7b02b1930a75fa32feb408ad62f8", size = 1617193, upload-time = "2026-06-17T18:22:16.252Z" },
|
{ url = "https://files.pythonhosted.org/packages/a9/ee/f5bf9daac27c5e1b011965f64b5630a32b415daf7381b312943629e12c2a/greenlet-3.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1d554cd96841a68d464d75a3736f8e87408a7b02b1930a75fa32feb408ad62f8", size = 1617193, upload-time = "2026-06-17T18:22:16.252Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/8a/21/b05d5b12715bda92ce27c118d64971d21e9b8f3563ed959a7d271e2d4223/greenlet-3.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3dff6cd3aac35f6cd3fc23460105acf576f5faf6c378de0bc088bf37c913864a", size = 1677512, upload-time = "2026-06-17T17:40:10.771Z" },
|
{ url = "https://files.pythonhosted.org/packages/8a/21/b05d5b12715bda92ce27c118d64971d21e9b8f3563ed959a7d271e2d4223/greenlet-3.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3dff6cd3aac35f6cd3fc23460105acf576f5faf6c378de0bc088bf37c913864a", size = 1677512, upload-time = "2026-06-17T17:40:10.771Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b8/97/1b8f1314b868041b327dc1051603e8142b826480cb0ecb8a7b7632aee9c4/greenlet-3.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:36cfea2aa075d544617176b2e84450480f0797070ad8799a8c41ada2fe449d32", size = 243145, upload-time = "2026-06-17T17:34:37.502Z" },
|
{ url = "https://files.pythonhosted.org/packages/b8/97/1b8f1314b868041b327dc1051603e8142b826480cb0ecb8a7b7632aee9c4/greenlet-3.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:36cfea2aa075d544617176b2e84450480f0797070ad8799a8c41ada2fe449d32", size = 243145, upload-time = "2026-06-17T17:34:37.502Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/36/07/1b5311775e04c718a118c504d7a3a312430e2a1bd1347226aff4774e4549/greenlet-3.5.2-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:a0314aa832c94633355dc6f3ee54f195159533355a323f26926fc63b98b2ccbb", size = 288315, upload-time = "2026-06-17T17:34:34.04Z" },
|
{ url = "https://files.pythonhosted.org/packages/36/07/1b5311775e04c718a118c504d7a3a312430e2a1bd1347226aff4774e4549/greenlet-3.5.2-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:a0314aa832c94633355dc6f3ee54f195159533355a323f26926fc63b98b2ccbb", size = 288315, upload-time = "2026-06-17T17:34:34.04Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ed/cc/6abcd2a486b58b9f77b7a93b690d59cb2c11a5906ed2ad4c63c7b9c1113d/greenlet-3.5.2-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24c59cb7db9d5c694cb8fd0c76eef8e456b2123afdfa7e4b8f2a67a0860d7682", size = 659130, upload-time = "2026-06-17T18:07:26.354Z" },
|
{ url = "https://files.pythonhosted.org/packages/ed/cc/6abcd2a486b58b9f77b7a93b690d59cb2c11a5906ed2ad4c63c7b9c1113d/greenlet-3.5.2-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24c59cb7db9d5c694cb8fd0c76eef8e456b2123afdfa7e4b8f2a67a0860d7682", size = 659130, upload-time = "2026-06-17T18:07:26.354Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f2/12/f4aaad6d3d383233f700ab322568a4f29f2c701a4861d85f4811d99689b2/greenlet-3.5.2-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7bb811753703739ad318112f16eccfaabdac050037b6d092debaa8b23566b4ce", size = 669724, upload-time = "2026-06-17T18:29:50.13Z" },
|
{ url = "https://files.pythonhosted.org/packages/f2/12/f4aaad6d3d383233f700ab322568a4f29f2c701a4861d85f4811d99689b2/greenlet-3.5.2-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7bb811753703739ad318112f16eccfaabdac050037b6d092debaa8b23566b4ce", size = 669724, upload-time = "2026-06-17T18:29:50.13Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/53/e0/4ce3a046b51e53934eae93d7f9c13975a97285741e9e1fcadf8751314c37/greenlet-3.5.2-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2debcd0ef9455b7d4879589903efc8e497d4b8fb8c0ae772309e44d1ca5e957f", size = 673494, upload-time = "2026-06-17T18:39:34.196Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/91/2a/a089811fc31c6bf8742f40a4e73470d6d401cef18e4314eb20dc399b377c/greenlet-3.5.2-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6d78b5c1c178dad90447f1b8452262709d3eef4c98f825569e74c9d0b2260ac9", size = 668089, upload-time = "2026-06-17T17:39:33.808Z" },
|
{ url = "https://files.pythonhosted.org/packages/91/2a/a089811fc31c6bf8742f40a4e73470d6d401cef18e4314eb20dc399b377c/greenlet-3.5.2-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6d78b5c1c178dad90447f1b8452262709d3eef4c98f825569e74c9d0b2260ac9", size = 668089, upload-time = "2026-06-17T17:39:33.808Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/52/e0/9c18721e63445dce02ee67e4c81c0f281626604ff55ae6f7b7f4354d7129/greenlet-3.5.2-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:9558cae989faeab6fbb425cd98a0cfa4190a47fba6443973fbee0a1eb0b0b6c3", size = 479721, upload-time = "2026-06-17T18:41:25.726Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/0f/1c/2f47c7d5fcfa98a62b705bf9a0505d86f4563c0d81cab1f7159ff1e743b7/greenlet-3.5.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:0977af2df83136f81c1f76e76d4e2fe7d0dc56ea9c101a86af26a95190b9ca32", size = 1625684, upload-time = "2026-06-17T18:22:17.664Z" },
|
{ url = "https://files.pythonhosted.org/packages/0f/1c/2f47c7d5fcfa98a62b705bf9a0505d86f4563c0d81cab1f7159ff1e743b7/greenlet-3.5.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:0977af2df83136f81c1f76e76d4e2fe7d0dc56ea9c101a86af26a95190b9ca32", size = 1625684, upload-time = "2026-06-17T18:22:17.664Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b9/bf/661dd24624f70b7b32972d7693d0344ecde10278f647d7b828baf739899c/greenlet-3.5.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f9ed777c6891d8253e54468576f55e27f8fc1a662a664f946a191003574c0a74", size = 1688043, upload-time = "2026-06-17T17:40:12.403Z" },
|
{ url = "https://files.pythonhosted.org/packages/b9/bf/661dd24624f70b7b32972d7693d0344ecde10278f647d7b828baf739899c/greenlet-3.5.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f9ed777c6891d8253e54468576f55e27f8fc1a662a664f946a191003574c0a74", size = 1688043, upload-time = "2026-06-17T17:40:12.403Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/60/49/d9bde1d15a21296b3b521fe083eb8aabd54ac05d15de9832918f3d639543/greenlet-3.5.2-cp315-cp315-win_amd64.whl", hash = "sha256:c0ea4eb3de23f0bac1d75205e10ccfa9b418b17b01a2d7bf19e3b69dda08900a", size = 240531, upload-time = "2026-06-17T17:35:47.448Z" },
|
{ url = "https://files.pythonhosted.org/packages/60/49/d9bde1d15a21296b3b521fe083eb8aabd54ac05d15de9832918f3d639543/greenlet-3.5.2-cp315-cp315-win_amd64.whl", hash = "sha256:c0ea4eb3de23f0bac1d75205e10ccfa9b418b17b01a2d7bf19e3b69dda08900a", size = 240531, upload-time = "2026-06-17T17:35:47.448Z" },
|
||||||
@@ -552,7 +558,9 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/92/15/907be5e8900901039bae752fa9a31c03a3c1e064833f35a4e49449184581/greenlet-3.5.2-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:98a52d6a50d4deaba304331d83ee3e10ebbdc1517fcca40b2715d1de4534065c", size = 296697, upload-time = "2026-06-17T17:37:15.887Z" },
|
{ url = "https://files.pythonhosted.org/packages/92/15/907be5e8900901039bae752fa9a31c03a3c1e064833f35a4e49449184581/greenlet-3.5.2-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:98a52d6a50d4deaba304331d83ee3e10ebbdc1517fcca40b2715d1de4534065c", size = 296697, upload-time = "2026-06-17T17:37:15.887Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/95/5c/08c57be575c3d6a3c023bbf22144a1c7dc6ed4d134527bb36ded4dbf04a8/greenlet-3.5.2-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1587ff8b58fdf806993ed1490a06ac19c22d47b219c68b30954380029045d8d4", size = 656710, upload-time = "2026-06-17T18:07:28.046Z" },
|
{ url = "https://files.pythonhosted.org/packages/95/5c/08c57be575c3d6a3c023bbf22144a1c7dc6ed4d134527bb36ded4dbf04a8/greenlet-3.5.2-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1587ff8b58fdf806993ed1490a06ac19c22d47b219c68b30954380029045d8d4", size = 656710, upload-time = "2026-06-17T18:07:28.046Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/8c/d0/749f917bdc9fc90fceea4aa65fbf6556e617a50714d1496bdc8ad190bb36/greenlet-3.5.2-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:feb721811d2754bfd16b48de151dd6b1f222c048e625151f2ca44cfdfd69f59c", size = 662629, upload-time = "2026-06-17T18:29:51.728Z" },
|
{ url = "https://files.pythonhosted.org/packages/8c/d0/749f917bdc9fc90fceea4aa65fbf6556e617a50714d1496bdc8ad190bb36/greenlet-3.5.2-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:feb721811d2754bfd16b48de151dd6b1f222c048e625151f2ca44cfdfd69f59c", size = 662629, upload-time = "2026-06-17T18:29:51.728Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/55/87/10776cd88df54d0f563e9e21e98363f2d6af94bedc553b1da0972fa87f80/greenlet-3.5.2-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a9476cbead736dc48ce89e3cd97acff95ecc48cbf21273603a438f9870c4a014", size = 663191, upload-time = "2026-06-17T18:39:35.639Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/5a/a5/68cefae3a07f6d0093a490cf28ab604f14578f3e60205a2a2b2d5cd70af2/greenlet-3.5.2-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7fe6062b1f35534e1e8fb28dfed406cf4eeff3e0bca3a0d9f8ff69f20a4abb00", size = 660147, upload-time = "2026-06-17T17:39:35.068Z" },
|
{ url = "https://files.pythonhosted.org/packages/5a/a5/68cefae3a07f6d0093a490cf28ab604f14578f3e60205a2a2b2d5cd70af2/greenlet-3.5.2-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7fe6062b1f35534e1e8fb28dfed406cf4eeff3e0bca3a0d9f8ff69f20a4abb00", size = 660147, upload-time = "2026-06-17T17:39:35.068Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/02/aa/26ddf92826a99d87bfb8fdb8f3a262a6f16495a5d8e579737baa92fb4543/greenlet-3.5.2-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:5930d3946ecae99fa7fc0e3f3ae515426ad85058ebd9bfc6c00cca8016e6206b", size = 498199, upload-time = "2026-06-17T18:41:27.464Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d2/6b/b9156d8397e4750220f54c7c5c34650f1e740a8d2f66eab9cfd1b7b53b69/greenlet-3.5.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b4ac902af825cbac8e9b2fccab8122236fd2ba6c8b71a080116d2c2ec72671b1", size = 1621675, upload-time = "2026-06-17T18:22:18.873Z" },
|
{ url = "https://files.pythonhosted.org/packages/d2/6b/b9156d8397e4750220f54c7c5c34650f1e740a8d2f66eab9cfd1b7b53b69/greenlet-3.5.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b4ac902af825cbac8e9b2fccab8122236fd2ba6c8b71a080116d2c2ec72671b1", size = 1621675, upload-time = "2026-06-17T18:22:18.873Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b0/e3/d3250f4fa01c211a93d04e34fded63187e648dbec17b9b1a14d388040593/greenlet-3.5.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:6f1e473c06ae8be00c9034c2bb10fa277b08a93287e3111c395b839f01d27e1f", size = 1680577, upload-time = "2026-06-17T17:40:14.055Z" },
|
{ url = "https://files.pythonhosted.org/packages/b0/e3/d3250f4fa01c211a93d04e34fded63187e648dbec17b9b1a14d388040593/greenlet-3.5.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:6f1e473c06ae8be00c9034c2bb10fa277b08a93287e3111c395b839f01d27e1f", size = 1680577, upload-time = "2026-06-17T17:40:14.055Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/55/ba/eaee8bda4419770d7096b5a009ebff0ab20a2a28cdd83c4b591bfdf36fa9/greenlet-3.5.2-cp315-cp315t-win_amd64.whl", hash = "sha256:3c2315045f9983e2e50d7e89d95405c21bddb8745f2da4487bc080ab3525f904", size = 243482, upload-time = "2026-06-17T17:37:34.741Z" },
|
{ url = "https://files.pythonhosted.org/packages/55/ba/eaee8bda4419770d7096b5a009ebff0ab20a2a28cdd83c4b591bfdf36fa9/greenlet-3.5.2-cp315-cp315t-win_amd64.whl", hash = "sha256:3c2315045f9983e2e50d7e89d95405c21bddb8745f2da4487bc080ab3525f904", size = 243482, upload-time = "2026-06-17T17:37:34.741Z" },
|
||||||
|
|||||||
Reference in New Issue
Block a user