diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..67208bf --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,85 @@ +name: CI Pipeline + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + detect-projects: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + steps: + - uses: actions/checkout@v4 + + - name: Find Python projects + id: set-matrix + run: | + # Find all directories containing both pyproject.toml and uv.lock + projects=$(find . -name "pyproject.toml" -exec dirname {} \; | while read dir; do + if [ -f "$dir/uv.lock" ]; then + echo "$dir" + fi + done | sed 's|^\./||' | sed 's/.*/"&"/' | paste -sd, - | sed 's/^/[/;s/$/]/') + echo "matrix={\"project\":$projects}" >> $GITHUB_OUTPUT + echo "Found projects: $projects" + echo "Full matrix output: {\"project\":$projects}" + + lint-and-test: + needs: detect-projects + if: ${{ needs.detect-projects.outputs.matrix != '{"project":[]}' }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.detect-projects.outputs.matrix) }} + + defaults: + run: + working-directory: ${{ matrix.project }} + + steps: + - uses: actions/checkout@v4 + + - name: Extract Python version from pyproject.toml + id: python-version + run: | + # Extract minimum Python version from requires-python + version=$(grep -oP 'requires-python\s*=\s*">=\K[0-9]+\.[0-9]+' pyproject.toml || echo "3.12") + echo "version=$version" >> $GITHUB_OUTPUT + echo "Using Python $version for ${{ matrix.project }}" + + - name: Install uv + uses: astral-sh/setup-uv@v7 + + - name: Set up Python + run: uv python install ${{ steps.python-version.outputs.version }} + + - name: Install dependencies + run: uv sync --frozen + + - name: Install pre-commit + run: uv tool install pre-commit + + - name: Run pre-commit hooks + env: + SKIP: ruff,ty + run: | + # Run pre-commit hooks only on files in this project (skip ruff/ty, we run them separately) + find . -type f \( -name "*.py" -o -name "*.toml" -o -name "*.yaml" -o -name "*.yml" -o -name "*.json" -o -name "*.md" \) -not -path "./.venv/*" | xargs uv tool run pre-commit run --config ${{ github.workspace }}/.pre-commit-config.yaml --files || true + + - name: Run ruff linter + run: uv run ruff check . --select "I,E,F,Q,UP,FAST" --line-length 120 + + - name: Run type checker + run: uv run ty check || true + + - name: Run pytest + run: | + # Check if tests directory or test files exist + if [ -d "tests" ] || find . -name "test_*.py" -o -name "*_test.py" | grep -q .; then + uv run pytest -v + else + echo "No tests found in ${{ matrix.project }}" + fi diff --git a/.gitignore b/.gitignore index 587fc1d..d0abdab 100644 --- a/.gitignore +++ b/.gitignore @@ -213,4 +213,4 @@ marimo/_lsp/ __marimo__/ # Streamlit -.streamlit/secrets.toml \ No newline at end of file +.streamlit/secrets.toml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..03daefc --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,30 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: check-added-large-files + args: ['--maxkb=600'] + - id: check-docstring-first + - id: check-merge-conflict + - id: check-symlinks + - id: check-toml + - id: check-yaml + args: [ --unsafe ] + - id: detect-private-key + - id: end-of-file-fixer + - id: requirements-txt-fixer + + - repo: local + hooks: + - id: ruff + name: ruff (local) + entry: uv run ruff check --fix --select "I,E,F,Q,UP,FAST" + language: system + types: [python] + + - id: ty + name: ty (local) + entry: uv run ty check + language: system + types: [python] + args: [] diff --git a/Chapter01/platform-home/.pre-commit-config.yaml b/Chapter01/platform-home/.pre-commit-config.yaml index ac46c7f..e3fba1a 100644 --- a/Chapter01/platform-home/.pre-commit-config.yaml +++ b/Chapter01/platform-home/.pre-commit-config.yaml @@ -32,9 +32,9 @@ repos: entry: uv run ruff check --fix language: system types: [python] - - id: mypy - name: mypy (local) - entry: uv run mypy + - id: ty + name: ty (local) + entry: uv run ty check language: system types: [python] args: [] diff --git a/Chapter01/platform-home/pyproject.toml b/Chapter01/platform-home/pyproject.toml index 23d7966..bfbb628 100644 --- a/Chapter01/platform-home/pyproject.toml +++ b/Chapter01/platform-home/pyproject.toml @@ -13,11 +13,11 @@ dev = [ "pre-commit>=4.2.0", "pytest>=8.4.1", "ruff>=0.12.8", - "ty>=0.0.8", + "ty>=0.0.8" ] [tool.ruff] line-length = 61 [tool.ruff.lint] -select = ["I", "E4", "E7", "F", "Q"] +select = ["I", "E4", "E7", "F", "Q", "UP", "FAST"] diff --git a/Chapter02/parents-portal/app/home.py b/Chapter02/parents-portal/app/home.py index 625ee90..1f19949 100644 --- a/Chapter02/parents-portal/app/home.py +++ b/Chapter02/parents-portal/app/home.py @@ -12,7 +12,7 @@ COOKIE_EXPIRATION_TIME = 60 -cookies_store = TTLCache( # type: ignore[var-annotated] +cookies_store = TTLCache( maxsize=100, ttl=COOKIE_EXPIRATION_TIME, timer=lambda: time.monotonic(), @@ -32,7 +32,7 @@ class Lang(StrEnum): async def home( request: Request, lang: Lang, - name: str = Query(default=""), + name: str = Query(default=""), # noqa: FAST002 ): request_cookie = request.cookies.get("TRACKING") cookie_content = cookies_store.get(request_cookie, "Invalid") diff --git a/Chapter02/parents-portal/pyproject.toml b/Chapter02/parents-portal/pyproject.toml index fb91629..c2887f4 100644 --- a/Chapter02/parents-portal/pyproject.toml +++ b/Chapter02/parents-portal/pyproject.toml @@ -19,7 +19,10 @@ profile = "jinja" line-length = 66 [tool.ruff.lint] -select = ["I", "E4", "E7", "E9", "F", "Q"] +select = ["I", "E", "F", "Q", "UP", "FAST"] + +[tool.ruff.lint.pycodestyle] +max-line-length=120 [dependency-groups] dev = [ diff --git a/Chapter02/parents-portal/tests/conftest.py b/Chapter02/parents-portal/tests/conftest.py index 4b1b8d9..fe51d27 100644 --- a/Chapter02/parents-portal/tests/conftest.py +++ b/Chapter02/parents-portal/tests/conftest.py @@ -6,7 +6,6 @@ @pytest.fixture(scope="module") def test_client(): - """Create a TestClient for the app, shared across all tests in the module.""" return TestClient(app) diff --git a/Chapter02/parents-portal/tests/test_cookies.py b/Chapter02/parents-portal/tests/test_cookies.py index ca988f6..313c4b2 100644 --- a/Chapter02/parents-portal/tests/test_cookies.py +++ b/Chapter02/parents-portal/tests/test_cookies.py @@ -28,7 +28,7 @@ def test_home_with_cookie_expired(test_client): # Advance time by expiration + 1 second so the cookie expires with freeze_time( - datetime.datetime.now(datetime.timezone.utc) + datetime.datetime.now(datetime.UTC) + datetime.timedelta(seconds=COOKIE_EXPIRATION_TIME + 1) ): response = test_client.get("/home") diff --git a/Chapter03/reservation-service/presentation/api/routes.py b/Chapter03/reservation-service/presentation/api/routes.py index 9913200..80ae14b 100644 --- a/Chapter03/reservation-service/presentation/api/routes.py +++ b/Chapter03/reservation-service/presentation/api/routes.py @@ -90,10 +90,10 @@ async def create_availability_slot( ) async def list_available_slots( request: Request, - week_day: WeekDaySchema | None = Query( + week_day: WeekDaySchema | None = Query( # noqa: FAST002 None, description="Filter by day of the week" ), - time_slot: TimeSlotSchema | None = Query( + time_slot: TimeSlotSchema | None = Query( # noqa: FAST002 None, description="Filter by time slot" ), ) -> list[SlotResponseSchema]: diff --git a/Chapter03/reservation-service/presentation/main.py b/Chapter03/reservation-service/presentation/main.py index 8dcea9f..6c8667d 100644 --- a/Chapter03/reservation-service/presentation/main.py +++ b/Chapter03/reservation-service/presentation/main.py @@ -69,4 +69,4 @@ async def measuring_request_performance( return response -app.add_middleware(StateCheckMiddleware) +app.add_middleware(StateCheckMiddleware) # ty: ignore[invalid-argument-type] known issue with ty diff --git a/Chapter03/reservation-service/pyproject.toml b/Chapter03/reservation-service/pyproject.toml index b329794..2e95ec4 100644 --- a/Chapter03/reservation-service/pyproject.toml +++ b/Chapter03/reservation-service/pyproject.toml @@ -21,7 +21,10 @@ dev = [ line-length = 66 [tool.ruff.lint] -select = ["I", "E4", "E7", "F", "Q", "FAST", "UP"] +select = ["I", "E", "F", "Q", "FAST", "UP"] + +[tool.ruff.lint.pycodestyle] +max-line-length=120 [tool.pytest.ini_options] pythonpath = ["."]