Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: CI

on:
push:
branches: [ master ]
pull_request:
branches: [ master ]

jobs:
tests:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v5

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'

- name: Install dependencies
run: uv sync

- name: Start Cloud Tasks emulator
run: |
docker compose -f docker-compose.yaml up -d

- name: Run tests
run: make test

- name: Show emulator logs on failure
if: failure()
run: |
docker compose -f docker-compose.yaml logs --no-color | cat

- name: Stop emulator
if: always()
run: |
docker compose -f docker-compose.yaml down -v
30 changes: 7 additions & 23 deletions .github/workflows/lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,34 +15,18 @@ jobs:
steps:
- name: Check out repository
uses: actions/checkout@v6

- name: Install uv
uses: astral-sh/setup-uv@v5

- name: Set up python
id: setup-python
uses: actions/setup-python@v6
with:
python-version: '3.11'

- name: Install Poetry
uses: snok/install-poetry@v1
with:
virtualenvs-create: true
virtualenvs-in-project: true
installer-parallel: true

- name: Load cached venv
id: cached-poetry-dependencies
uses: actions/cache@v5
with:
path: .venv
key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }}

- name: Install dependencies
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
run: poetry install --no-interaction --no-root

- name: Install project
run: poetry install --no-interaction
run: uv sync

- name: Run tests
- name: Run lints
run: |
source .venv/bin/activate
sh scripts/lint.sh
make lint
23 changes: 11 additions & 12 deletions .github/workflows/pypi-publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,29 @@ jobs:
steps:
- uses: actions/checkout@v6

- name: Install uv
uses: astral-sh/setup-uv@v5

- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.11'
cache: 'pip'

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install poetry

- name: Validate version
id: validate_version
run: |
TAG_VERSION=${GITHUB_REF#refs/tags/v}
PYPROJECT_VERSION=$(poetry version -s)
PYPROJECT_VERSION=$(uv version)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uv version output includes project name, breaking comparison

High Severity

The uv version command outputs the version in the format project-name version-number (e.g., fastapi-gcp-tasks 0.1.1), whereas the old poetry version -s outputted just the bare version string (e.g., 0.1.1). Since TAG_VERSION is extracted as a bare version like 0.1.1, comparing it against PYPROJECT_VERSION from uv version will always fail, blocking every PyPI publish.

Fix in Cursor Fix in Web

echo "Tag version: $TAG_VERSION"
echo "Pyproject version: $PYPROJECT_VERSION"
if [ "$TAG_VERSION" != "$PYPROJECT_VERSION" ]; then
echo "Error: Tag version ($TAG_VERSION) does not match pyproject.toml version ($PYPROJECT_VERSION)."
exit 1
fi

- name: Build and publish to pypi
uses: JRubics/poetry-publish@v2.1
with:
pypi_token: ${{ secrets.PYPI_TOKEN }}
- name: Build package
run: uv build

- name: Publish to PyPI
env:
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }}
run: uv publish
13 changes: 13 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.PHONY: lint format test

lint:
uv run mypy fastapi_gcp_tasks
uv run ruff check fastapi_gcp_tasks tests scripts examples
uv run ruff format fastapi_gcp_tasks tests scripts examples --check

format:
uv run ruff check fastapi_gcp_tasks tests examples scripts --fix
uv run ruff format fastapi_gcp_tasks tests examples scripts

test:
uv run pytest -q -x -s
26 changes: 24 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ DelayedRoute = DelayedRouteBuilder(
base_url="http://localhost:8000"
queue_path=queue_path(
project="gcp-project-id",
location="asia-south1",
location="us-central1",
queue="test-queue",
),
)
Expand Down Expand Up @@ -367,9 +367,31 @@ async def my_task(ct_headers: CloudTasksHeaders = Depends()):

Check the file [fastapi_cloud_tasks/dependencies.py](fastapi_gcp_tasks/dependencies.py) for details.

## Development

### Prerequisites

- [uv](https://docs.astral.sh/uv/)
- Docker (for the Cloud Tasks emulator)

### Running tests

```sh
docker compose up -d # start emulator
make test # run tests
docker compose down # stop emulator
```

### Linting & formatting

```sh
make lint # check
make format # auto-fix
```

## Contributing

- Run the `format.sh` and `lint.sh` scripts before raising a PR.
- Run `make lint` and `make format` before raising a PR.
- Add examples and/or tests for new features.
- If the change is massive, open an issue to discuss it before writing code.

Expand Down
8 changes: 8 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
services:
tasks-emulator:
image: ghcr.io/aertje/cloud-tasks-emulator:1.2.0
command: ["--host", "0.0.0.0", "--port", "8123"]
ports:
- "8123:8123"


2 changes: 1 addition & 1 deletion examples/full/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
# Check main.py for how this is used.
TASK_LISTENER_BASE_URL = os.getenv("TASK_LISTENER_BASE_URL", default="http://localhost:8000/_fastapi_cloud_tasks")
TASK_PROJECT_ID = os.getenv("TASK_PROJECT_ID", default="sample-project")
TASK_LOCATION = os.getenv("TASK_LOCATION", default="asia-south1")
TASK_LOCATION = os.getenv("TASK_LOCATION", default="us-central1")
SCHEDULED_LOCATION = os.getenv("SCHEDULED_LOCATION", default="us-central1")
TASK_QUEUE = os.getenv("TASK_QUEUE", default="test-queue")

Expand Down
11 changes: 11 additions & 0 deletions examples/full/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,18 @@
),
)

# No Cloud Scheduler emulator exists, so pass a dummy client when running locally
# to avoid requiring GCP credentials. The client is never used in local mode
# because the .schedule() call below is guarded by `if not IS_LOCAL`.
scheduled_client = None
if IS_LOCAL:
from google.auth.credentials import AnonymousCredentials
from google.cloud import scheduler_v1

scheduled_client = scheduler_v1.CloudSchedulerClient(credentials=AnonymousCredentials())

ScheduledRoute = ScheduledRouteBuilder(
client=scheduled_client,
base_url=TASK_LISTENER_BASE_URL,
location_path=SCHEDULED_LOCATION_PATH,
pre_create_hook=chained_hook(
Expand Down
2 changes: 1 addition & 1 deletion examples/simple/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
# Edit values below to match your project
queue_path=queue_path(
project=os.getenv("TASK_PROJECT_ID", default="gcp-project-id"),
location=os.getenv("TASK_LOCATION", default="asia-south1"),
location=os.getenv("TASK_LOCATION", default="us-central1"),
queue=os.getenv("TASK_QUEUE", default="test-queue"),
),
)
Expand Down
2 changes: 1 addition & 1 deletion fastapi_gcp_tasks/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def ensure_queue(
# We extract information from the queue path to make the public api simpler
parsed_queue_path = client.parse_queue_path(path=path)
create_req = tasks_v2.CreateQueueRequest(
parent=location_path(**parsed_queue_path),
parent=location_path(project=parsed_queue_path["project"], location=parsed_queue_path["location"]),
queue=tasks_v2.Queue(name=path, **kwargs),
)
try:
Expand Down
Loading