diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 941ab1a..43548cf 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,15 +8,7 @@ updates: - "dependencies" cooldown: default-days: 7 - - package-ecosystem: "pip" - directory: "/" - schedule: - interval: "monthly" - labels: - - "dependencies" - cooldown: - default-days: 7 - - package-ecosystem: "docker" + - package-ecosystem: "uv" directory: "/" schedule: interval: "monthly" diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml deleted file mode 100644 index a0b4da7..0000000 --- a/.github/workflows/build.yaml +++ /dev/null @@ -1,43 +0,0 @@ -name: Build and Publish Multi-arch Docker Image - -on: - push: - tags: - - '*' - -permissions: {} - -jobs: - build-and-publish: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - - steps: - - name: Checkout Repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - persist-credentials: false - - - name: Set up QEMU - uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - - - name: Login to GitHub Container Registry - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build and Push Docker Image - env: - REF_NAME: ${{ github.ref_name }} - run: | - docker buildx build \ - -t "ghcr.io/canonical/api_demo_server:${REF_NAME}" \ - --platform linux/amd64,linux/arm64,linux/ppc64le \ - --push . diff --git a/.github/workflows/integration-test.yaml b/.github/workflows/integration-test.yaml index 9c4da99..53feb80 100644 --- a/.github/workflows/integration-test.yaml +++ b/.github/workflows/integration-test.yaml @@ -16,5 +16,8 @@ jobs: with: persist-credentials: false + - name: Pack the rock + uses: canonical/craft-actions/rockcraft-pack@210d55fa6cb7a1ab39e3194315d6a03249af3d34 + - name: Run the integration test run: make integration diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 90f6492..092583e 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -16,6 +16,6 @@ jobs: with: persist-credentials: false - name: Install uv - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 - name: Lint the code run: make lint diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 0000000..5e8b58f --- /dev/null +++ b/.github/workflows/publish.yaml @@ -0,0 +1,80 @@ +name: Build and publish image + +on: + push: + tags: + - '*' + +permissions: {} + +jobs: + prepare: + runs-on: ubuntu-latest + outputs: + build-matrix: ${{ steps.set-matrix.outputs.build-matrix }} + steps: + - name: Set Build Matrix + id: set-matrix + env: + TAG: ${{ github.ref_name }} + run: | + matrix=$(jq --null-input --compact-output \ + --arg tag "${TAG}" \ + '{ + include: [{ + name: "api-demo-server", + tag: $tag, + directory: ".", + "artifact-name": "api-demo-server", + "run-tests": true, + lfs: false, + "lfs-include": "" + }] + }') + echo "build-matrix=${matrix}" >> $GITHUB_OUTPUT + fi + + build: + needs: [prepare] + strategy: + matrix: ${{ fromJSON(needs.prepare.outputs.build-matrix) }} + uses: canonical/oci-factory/.github/workflows/Build-Rock.yaml@43f2710f69c29e43fd90f9e600896a7e32de96de + with: + rock-repo: ${{ github.event.pull_request.head.repo.full_name || github.repository }} + rock-repo-commit: ${{ github.head_ref || github.ref_name }} + rockfile-directory: ${{ matrix.directory }} + oci-archive-name: ${{ matrix.artifact-name }} + # We don't have any special architecture or performance requirements, + # so use GitHub's runners instead of Canonical's self-hosted runners. + arch-map: '{"amd64":["ubuntu-24.04"],"arm64":["ubuntu-24.04-arm"]}' + rockcraft-test: ${{ matrix.run-tests }} + secrets: + source-github-token: ${{ secrets.REPO_CLONER_TOKEN }} + + test: + needs: [prepare, build] + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.prepare.outputs.build-matrix) }} + uses: canonical/oci-factory/.github/workflows/Test-Rock.yaml@43f2710f69c29e43fd90f9e600896a7e32de96de + with: + oci-archive-name: ${{ matrix.artifact-name }} + + upload-ghcr: + needs: [prepare, test] + runs-on: ubuntu-latest + strategy: + matrix: ${{ fromJSON(needs.prepare.outputs.build-matrix) }} + fail-fast: false + permissions: + packages: write + steps: + - name: Upload Rock to GHCR + uses: canonical/oci-factory/.github/actions/upload-rock@43f2710f69c29e43fd90f9e600896a7e32de96de + with: + artifact_name: ${{ matrix.artifact-name }} + tags: ${{ matrix.tag }} + name: ${{ matrix.name }} + registry: ghcr.io/${{ github.repository }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/security.yaml b/.github/workflows/security.yaml index 9fa6d9d..8901373 100644 --- a/.github/workflows/security.yaml +++ b/.github/workflows/security.yaml @@ -18,7 +18,7 @@ jobs: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: persist-credentials: false - - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 - name: Export locked dependencies (excluding this project) run: uv export --frozen --no-emit-project --no-hashes --format requirements-txt -o requirements.txt - name: Run pip-audit diff --git a/.github/workflows/zizmor.yaml b/.github/workflows/zizmor.yaml index 201aaa9..eec8276 100644 --- a/.github/workflows/zizmor.yaml +++ b/.github/workflows/zizmor.yaml @@ -20,7 +20,7 @@ jobs: persist-credentials: false - name: Install uv - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 - name: Run zizmor run: uvx zizmor@v1.23.1 --format=sarif . > results.sarif diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a3d1c28 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.rock +.craft-spread-* +.spread-reuse.* diff --git a/.scripts/integration-test.sh b/.scripts/integration-test.sh index d6e4c10..89c6d96 100755 --- a/.scripts/integration-test.sh +++ b/.scripts/integration-test.sh @@ -3,11 +3,32 @@ set -euo pipefail cleanup_docker() { docker compose down --volumes --remove-orphans + docker image rm api-demo-server:integration } +rock_path="${ROCK_PATH:-}" +if [[ -z "$rock_path" ]]; then + set -- ./*.rock + if [[ "$1" == "./*.rock" ]]; then + echo "No rocks were found in $(pwd)" >&2 + exit 1 + elif [[ $# -gt 1 ]]; then + echo "Found more than one rock $*" >&2 + exit 1 + else + rock_path="$1" + fi +fi +if [[ ! -f "$rock_path" ]]; then + echo "$rock_path is not a file" >&2 + exit 1 +fi + set -x +rockcraft.skopeo --insecure-policy \ + copy "oci-archive:${rock_path}" "docker-daemon:api-demo-server:integration" trap cleanup_docker EXIT -docker compose up --build --detach --wait +docker compose up --detach --wait curl --silent --fail --request POST http://localhost:8000/createtable curl --silent --fail --request POST http://localhost:8000/addname/ --data "name=Alice" response="$(curl --silent --fail http://localhost:8000/names)" diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index de0792b..0000000 --- a/Dockerfile +++ /dev/null @@ -1,16 +0,0 @@ -FROM ubuntu:22.04 -LABEL org.opencontainers.image.source=https://github.com/canonical/api_demo_server - -RUN apt-get update && apt-get install -y \ - python3.10 \ - python3-pip -COPY ./pyproject.toml . -COPY ./LICENSE . - -# create dummy project folder just to keep the layer with dependencies untouched until pyproject is changed -RUN mkdir -p src/api_demo_server && echo "__version__ = '0.0.1.dev0'" > src/api_demo_server/__init__.py -RUN python3 -m pip install . -COPY ./src/api_demo_server ./src/api_demo_server -WORKDIR /src -EXPOSE 8000 -ENTRYPOINT ["uvicorn", "api_demo_server.app:app", "--host=0.0.0.0"] diff --git a/Makefile b/Makefile index 9cf0522..060b7fa 100644 --- a/Makefile +++ b/Makefile @@ -2,8 +2,8 @@ MAKEFLAGS += --always-make format: - uv run ruff format uv run ruff check --fix + uv run ruff format lint: uv run ruff check diff --git a/README.md b/README.md index 3905df1..325db5d 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,70 @@ -# Description -This is a demo server based on Python FastAPI. -Server is used to show connections to PostgreSQL and Prometheus. +This is a demo Python server written with FastAPI. The server connects to PostgreSQL and exposes Prometheus metrics. -To see API reference start the server and open: http://127.0.0.1:8000/docs -To get prometheus metrics: http://127.0.0.1:8000/metrics +The server is packaged as a "rock" using Canonical's OCI-compliant format for container images. -# Usage -Download and start PostgreSQL container: -``` -docker run --name postgres -e POSTGRES_PASSWORD=mysecretpassword -d postgres -``` +# Build and run the container image -Get psql container IP: -``` -docker ps # get postgres container ID -docker inspect | grep IPAddress -``` +You'll need [Rockcraft](https://documentation.ubuntu.com/rockcraft/stable/) and [Docker](https://docs.docker.com/). -Build a docker container via: -``` -docker build -t api_demo_server . -``` +By default, Docker requires `sudo`. To use Docker as a regular user, run `sudo usermod -aG docker $USER` then log out and log back in again. -Start demo server: -``` -docker run --rm -e DEMO_SERVER_DB_HOST= -p 8000:8000 api_demo_server -``` +1. Build the container image from source: -# Configuration via environment variables -You can configure application by applying following environment variables: + ```text + rockcraft pack + ``` -| Environment Variable | Value | Description | -|------------------------- |------------------- |------------------------------------------------- | -| DEMO_SERVER_LOGFILE | \ | Path to the file where logs should be written | -| DEMO_SERVER_DB_HOST | \ | IP address of the host where Database is hosted | -| DEMO_SERVER_DB_PORT | \ | Port of the host where Database is hosted | -| DEMO_SERVER_DB_USER | \ | Username that has access to `names` Database | -| DEMO_SERVER_DB_PASSWORD | \ | Password to the `DEMO_SERVER_DB_USER` user | + This creates a `.rock` file. The name of the `.rock` file depends on your system architecture. -# Publish to registry +2. Make the container image available to Docker: + ```text + rockcraft.skopeo --insecure-policy \ + copy "oci-archive:" "docker-daemon:api-demo-server:integration" + ``` + + Where `` is the path to the `.rock` file. + +3. Run the container image alongside PostgreSQL: + + ```text + docker compose up + ``` + +4. In a separate terminal, check that the server is available: + + ```text + curl http://localhost:8000/version + ``` + + This returns a JSON object that contains the server's version number. + +The server has several other API endpoints, including: + +- API docs - http://localhost:8000/docs +- Prometheus metrics - http://localhost:8000/metrics + +# Run automated tests + +If you modify the server, rebuild the container image and run the PostgreSQL integration tests: + +```text +rockcraft pack +make integration ``` -docker buildx build -t ghcr.io/canonical/api_demo_server:1.0.4 --platform linux/amd64,linux/arm64,linux/ppc64le --push . -``` + +The integration tests require Docker. + +Alternatively, run `rockcraft test`. This builds the container image and runs the integration tests inside a LXD container. It's slower, but you don't need Docker on your system. + +# Server environment variables + +- `DEMO_SERVER_LOGFILE` - Path to the file where logs should be written +- `DEMO_SERVER_DB_HOST` - IP address of the database host +- `DEMO_SERVER_DB_PORT` - Port of the database host +- `DEMO_SERVER_DB_USER` - Username that has access to the database +- `DEMO_SERVER_DB_PASSWORD` - Password for the `DEMO_SERVER_DB_USER` user + +# Deploy the container image as a Juju charm + +See [From zero to hero: Write your first Kubernetes charm](https://documentation.ubuntu.com/ops/latest/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/) diff --git a/compose.yaml b/compose.yaml index 625fc22..c4681de 100644 --- a/compose.yaml +++ b/compose.yaml @@ -10,7 +10,7 @@ services: retries: 10 demo-server: - build: . + image: api-demo-server:integration environment: DEMO_SERVER_DB_HOST: postgres ports: diff --git a/pyproject.toml b/pyproject.toml index 30afd86..cc51889 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "api_demo_server" -dynamic = ["version"] +version = "2.0.0" description = "FastAPI demo server" authors = [ { name = "The Charm Tech team at Canonical Ltd." }, @@ -9,9 +9,9 @@ license = "Apache-2.0" license-files = ["LICENSE"] requires-python = ">=3.10" dependencies = [ - "fastapi==0.136.1", - "uvicorn==0.46.0", - "python-multipart==0.0.31", + "fastapi==0.138.1", + "uvicorn==0.49.0", + "python-multipart==0.0.32", "psycopg2-binary==2.9.12", "starlette==1.3.1", "starlette_exporter==0.23.0" @@ -26,12 +26,9 @@ dev = [ [build-system] requires = [ - "flit_core >=3.2,<4", + "uv_build>=0.11.16,<1", ] -build-backend = "flit_core.buildapi" - -[tool.flit.module] -name = "api_demo_server" +build-backend = "uv_build" [tool.ruff] line-length = 99 diff --git a/rockcraft.yaml b/rockcraft.yaml new file mode 100644 index 0000000..134b904 --- /dev/null +++ b/rockcraft.yaml @@ -0,0 +1,78 @@ +name: api-demo-server +base: bare +build-base: ubuntu@24.04 +run-user: _daemon_ +version: "2.0.0" +summary: FastAPI demo server +description: A web server that can be integrated with a PostgreSQL database. +license: Apache-2.0 +platforms: + amd64: + arm64: + +parts: + python-runtime: + plugin: nil + stage-packages: + - python3.12-venv_ensurepip + # ^ This sets up symlinks from lib and lib64 to usr/. + # Exclude the symlinks (for now) so they don't interfere with uv. + stage: + - -lib/ + - -lib64/ + api-demo-server: + after: + - python-runtime + source: . + plugin: uv + build-snaps: + - astral-uv + # Copy the venv's lib dir to usr/lib, then exclude lib and lib64. + # The next part will create symlinks from lib and lib64 to usr/. + override-prime: | + craftctl default + cp --archive --no-clobber ${CRAFT_PART_INSTALL}/lib/* ${CRAFT_PRIME}/usr/lib/ + rm -rf ${CRAFT_PRIME}/lib/ ${CRAFT_PRIME}/lib64/ + stage: + - -lib/ + - -lib64/ + runtime-base: + plugin: nil + after: + - api-demo-server + stage-packages: + # Create symlinks from bin, lib, and lib64 to usr/. + # Also create definitions for the _daemon_ user. + - base-files_base + # Create a chisel manifest for the deb-security-manifest part. + - base-files_chisel + stage: + - -bin/ # We want the venv's bin dir insead of a symlink to usr/bin. + deb-security-manifest: + plugin: make + source: https://github.com/canonical/rocks-security-manifest + source-type: git + source-tag: v0.1.0 + override-prime: gen_manifest + +services: + fastapi: + override: replace + summary: FastAPI demo server + command: /bin/uvicorn api_demo_server.app:app --host 0.0.0.0 --port 8000 + startup: enabled + environment: + DEMO_SERVER_LOGFILE: /tmp/demo_server.log + on-check-failure: + server-up: restart + +# This check is internal to the container. +# It doesn't mean the container is ready to process external requests. +checks: + server-up: + override: replace + period: 1s + timeout: 5s + threshold: 5 + http: + url: http://127.0.0.1:8000/version diff --git a/spread.yaml b/spread.yaml new file mode 100644 index 0000000..f21ac32 --- /dev/null +++ b/spread.yaml @@ -0,0 +1,27 @@ +project: api-demo-server + +backends: + craft: + type: craft + systems: + - ubuntu-24.04: + +suites: + spread/integration-test/: + summary: Environment for the integration test + + prepare: | + sudo snap install docker + + # For 'rockcraft.skopeo' + sudo snap install --classic rockcraft + + # Wait for docker daemon to come online + sudo apt install --yes retry + retry --times=10 --delay 2 -- docker images + sudo apt remove --yes retry + +exclude: + - .git + +kill-timeout: 1h diff --git a/spread/.extension b/spread/.extension new file mode 100755 index 0000000..e2e538b --- /dev/null +++ b/spread/.extension @@ -0,0 +1,195 @@ +#!/bin/bash + +usage() { + echo "usage: $(basename "$0") [command]" + echo "valid commands:" + echo " allocate Create a backend instance to run tests on" + echo " discard Destroy a backend instance used to run tests" + echo " backend-prepare Set up the system to run tests" + echo " backend-restore Restore the system after the tests ran" + echo " backend-prepare-each Prepare the system before each test" + echo " backend-restore-each Restore the system after each test run" +} + +prepare() { + case "$SPREAD_SYSTEM" in + fedora*) + dnf update -y + dnf install -y snapd + while ! snap install snapd; do + echo "waiting for snapd..." + sleep 2 + done + ;; + debian*) + apt update + apt install -y snapd + while ! snap install snapd; do + echo "waiting for snapd..." + sleep 2 + done + ;; + ubuntu*) + apt update + ;; + esac + + snap wait system seed.loaded + snap refresh --hold + + if systemctl is-enabled unattended-upgrades.service; then + systemctl disable --now unattended-upgrades.service + systemctl mask unattended-upgrades.service + fi +} + +restore() { + case "$SPREAD_SYSTEM" in + ubuntu* | debian*) + apt autoremove -y --purge + ;; + esac + + rm -Rf "$PROJECT_PATH" + mkdir -p "$PROJECT_PATH" +} + +prepare_each() { + true +} + +restore_each() { + true +} + +allocate_lxdvm() { + name=$(echo "$SPREAD_SYSTEM" | tr '[:punct:]' -) + system=$(echo "$SPREAD_SYSTEM" | tr / -) + if [[ "$system" =~ ^ubuntu- ]]; then + image="ubuntu:${system#ubuntu-}" + else + image="images:$(echo "$system" | tr - /)" + fi + + VM_NAME="${VM_NAME:-spread-${name}-${RANDOM}}" + DISK="${DISK:-20}" + CPU="${CPU:-4}" + MEM="${MEM:-8}" + + lxc launch --vm \ + "${image}" \ + "${VM_NAME}" \ + -c limits.cpu="${CPU}" \ + -c limits.memory="${MEM}GiB" \ + -d root,size="${DISK}GiB" + + while ! lxc exec "${VM_NAME}" -- true &>/dev/null; do sleep 0.5; done + lxc exec "${VM_NAME}" -- sed -i 's/^\s*#\?\s*\(PermitRootLogin\|PasswordAuthentication\)\>.*/\1 yes/' /etc/ssh/sshd_config + lxc exec "${VM_NAME}" -- bash -c "if [ -d /etc/ssh/sshd_config.d ]; then echo -e 'PermitRootLogin yes\nPasswordAuthentication yes' > /etc/ssh/sshd_config.d/00-spread.conf; fi" + lxc exec "${VM_NAME}" -- bash -c "echo root:${SPREAD_PASSWORD} | sudo chpasswd || true" + + # Print the instance address to stdout + ADDR="" + while [ -z "$ADDR" ]; do ADDR=$(lxc ls -f csv | grep "^${VM_NAME}" | cut -d"," -f3 | cut -d" " -f1); done + echo "$ADDR" 1>&3 +} + +discard_lxdvm() { + instance_name="$(lxc ls -f csv | sed ':a;N;$!ba;s/(docker0)\n/(docker0) /' | grep "$SPREAD_SYSTEM_ADDRESS " | cut -f1 -d",")" + lxc delete -f "$instance_name" +} + +allocate_ci() { + if [ -z "$CI" ]; then + echo "This backend is intended to be used only in CI systems." + exit 1 + fi + sudo sed -i 's/^\s*#\?\s*\(PermitRootLogin\|PasswordAuthentication\)\>.*/\1 yes/' /etc/ssh/sshd_config + if [ -d /etc/ssh/sshd_config.d ]; then echo -e 'PermitRootLogin yes\nPasswordAuthentication yes' | sudo tee /etc/ssh/sshd_config.d/00-spread.conf; fi + sudo systemctl daemon-reload + sudo systemctl restart ssh + + echo "root:${SPREAD_PASSWORD}" | sudo chpasswd || true + + # Print the instance address to stdout + echo localhost >&3 +} + +discard_ci() { + true +} + +allocate() { + exec 3>&1 + exec 1>&2 + + case "$1" in + lxd-vm) + allocate_lxdvm + ;; + ci) + allocate_ci + ;; + *) + echo "unsupported backend $1" 2>&1 + ;; + esac +} + +discard() { + case "$1" in + lxd-vm) + discard_lxdvm + ;; + ci) + discard_ci + ;; + *) + echo "unsupported backend $1" 2>&1 + ;; + esac +} + +set -e + +while getopts "" o; do + case "${o}" in + *) + usage + exit 1 + ;; + esac +done +shift $((OPTIND - 1)) + +CMD="$1" +PARM="$2" + +if [ -z "$CMD" ]; then + usage + exit 0 +fi + +case "$CMD" in +allocate) + allocate "$PARM" + ;; +discard) + discard "$PARM" + ;; +backend-prepare) + prepare + ;; +backend-restore) + restore + ;; +backend-prepare-each) + prepare_each + ;; +backend-restore-each) + restore_each + ;; +*) + echo "unknown command $CMD" >&2 + ;; +esac diff --git a/spread/integration-test/task/task.yaml b/spread/integration-test/task/task.yaml new file mode 100644 index 0000000..23fcaf4 --- /dev/null +++ b/spread/integration-test/task/task.yaml @@ -0,0 +1,5 @@ +summary: Integration test + +execute: | + cd "$PROJECT_PATH" + ROCK_PATH="$CRAFT_ARTIFACT" .scripts/integration-test.sh diff --git a/src/api_demo_server/__init__.py b/src/api_demo_server/__init__.py index 9fc20a7..8c0d5d5 100644 --- a/src/api_demo_server/__init__.py +++ b/src/api_demo_server/__init__.py @@ -1 +1 @@ -__version__ = "2.0.0.dev0" +__version__ = "2.0.0" diff --git a/uv.lock b/uv.lock index 56bf14f..bb9fe41 100644 --- a/uv.lock +++ b/uv.lock @@ -40,6 +40,7 @@ wheels = [ [[package]] name = "api-demo-server" +version = "2.0.0" source = { editable = "." } dependencies = [ { name = "fastapi" }, @@ -59,12 +60,12 @@ dev = [ [package.metadata] requires-dist = [ - { name = "fastapi", specifier = "==0.136.1" }, + { name = "fastapi", specifier = "==0.138.1" }, { name = "psycopg2-binary", specifier = "==2.9.12" }, - { name = "python-multipart", specifier = "==0.0.31" }, + { name = "python-multipart", specifier = "==0.0.32" }, { name = "starlette", specifier = "==1.3.1" }, { name = "starlette-exporter", specifier = "==0.23.0" }, - { name = "uvicorn", specifier = "==0.46.0" }, + { name = "uvicorn", specifier = "==0.49.0" }, ] [package.metadata.requires-dev] @@ -266,7 +267,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -275,7 +276,7 @@ wheels = [ [[package]] name = "fastapi" -version = "0.136.1" +version = "0.138.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, @@ -284,9 +285,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5d/45/c130091c2dfa061bbfe3150f2a5091ef1adf149f2a8d2ae769ecaf6e99a2/fastapi-0.136.1.tar.gz", hash = "sha256:7af665ad7acfa0a3baf8983d393b6b471b9da10ede59c60045f49fbc89a0fa7f", size = 397448, upload-time = "2026-04-23T16:49:44.046Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/c9/5e8defe249899c0dc900643695fc07829a67fc88b4ff2cdb03fcbdbf5a4b/fastapi-0.138.1.tar.gz", hash = "sha256:96e3702dce09ee0dce48856135620d3d865ca684a79fe7513fd7b13a12f82862", size = 419646, upload-time = "2026-06-25T15:40:42.115Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/ff/2e4eca3ade2c22fe1dea7043b8ee9dabe47753349eb1b56a202de8af6349/fastapi-0.136.1-py3-none-any.whl", hash = "sha256:a6e9d7eeada96c93a4d69cb03836b44fa34e2854accb7244a1ece36cd4781c3f", size = 117683, upload-time = "2026-04-23T16:49:42.437Z" }, + { url = "https://files.pythonhosted.org/packages/38/a9/69a6924f645eb4dd8cd625bf255b3625990eb3e14e073438a53c405dcd3e/fastapi-0.138.1-py3-none-any.whl", hash = "sha256:b994cae7ba8b82c976a728b544244de31333fa5f7d261f9a1dffe526444cae23", size = 129182, upload-time = "2026-06-25T15:40:40.771Z" }, ] [[package]] @@ -739,11 +740,11 @@ wheels = [ [[package]] name = "python-multipart" -version = "0.0.31" +version = "0.0.32" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/64/7e/9b35ad8f3d9ca680f7c87a88f19612fdd8da9796c4d3b46e560ac79dcc4a/python_multipart-0.0.31.tar.gz", hash = "sha256:fc631183bb13e56db3158a4909908dfb2e23565286744e798241e63750e5d680", size = 46689, upload-time = "2026-06-04T08:27:49.014Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/42/55c32bb9b12693c092ad250a0e82edb5b31ddeda6eb772de5f308b3804ad/python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e", size = 46881, upload-time = "2026-06-04T16:18:58.647Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/1e/7f7f299527a5a8ad90acd5f2f78dfa6c8495c6301a3205106ea68a84de96/python_multipart-0.0.31-py3-none-any.whl", hash = "sha256:8408153d68a9773291fc1da39a8b85a50044bddbabd2dd72e9229776b7b15e28", size = 29996, upload-time = "2026-06-04T08:27:47.804Z" }, + { url = "https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042, upload-time = "2026-06-04T16:18:57.319Z" }, ] [[package]] @@ -954,14 +955,14 @@ wheels = [ [[package]] name = "uvicorn" -version = "0.46.0" +version = "0.49.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1f/93/041fca8274050e40e6791f267d82e0e2e27dd165627bd640d3e0e378d877/uvicorn-0.46.0.tar.gz", hash = "sha256:fb9da0926999cc6cb22dc7cd71a94a632f078e6ae47ff683c5c420750fb7413d", size = 88758, upload-time = "2026-04-23T07:16:00.151Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/1f/fa18009dea8469069cca78a4e877a008ab78f08b064bfc9ab891579077ff/uvicorn-0.49.0.tar.gz", hash = "sha256:ebf4271aa580d9de97f93192d4595176df6e91f9aae919ca73e4fc07df1e66a3", size = 91284, upload-time = "2026-06-03T22:01:30.448Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/31/a3/5b1562db76a5a488274b2332a97199b32d0442aca0ed193697fd47786316/uvicorn-0.46.0-py3-none-any.whl", hash = "sha256:bbebbcbed972d162afca128605223022bedd345b7bc7855ce66deb31487a9048", size = 70926, upload-time = "2026-04-23T07:15:58.355Z" }, + { url = "https://files.pythonhosted.org/packages/88/fa/e1388bbcf24ef3274f45c0c1c7b501fd14971037c1b6ee23610553307497/uvicorn-0.49.0-py3-none-any.whl", hash = "sha256:ba3d14c3ee7e41c6c654c46c9eb489d33213cdd30aa1696eab1374337c13f68f", size = 71376, upload-time = "2026-06-03T22:01:29.037Z" }, ]