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
29 changes: 29 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
.git
.github
.venv
.pytest_cache
.mypy_cache
.ruff_cache
.coverage
.cursor
.cursorrules
.specify
__pycache__
**/__pycache__
*.pyc
*.pyo
*.pyd
*.db
*.log
dist
build
site
docs
examples
tests
overrides
mkdocs.yml
README.md
LICENSE
*.md
specs
92 changes: 92 additions & 0 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
name: Publish Docker image

# Builds the ready-to-use LightAPI image and pushes it to Docker Hub.
#
# Triggers:
# - on push of a `v*` tag (release) → tags the image with that version + `latest`
# - on push to `master` → tags the image with `master`
# - manual workflow_dispatch → tags the image with `manual-<run-number>`
#
# Required repository secrets:
# - DOCKERHUB_USERNAME Docker Hub username (e.g. `iklobato`)
# - DOCKERHUB_TOKEN Docker Hub access token (read+write+delete scope)

on:
push:
tags: ["v*.*.*"]
branches: ["master"]
workflow_dispatch:

permissions:
contents: read

jobs:
publish:
name: Build & push iklob1/lightapi
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Read package version
id: version
shell: bash
run: |
# Extract version from pyproject.toml (e.g. 0.1.21)
VERSION=$(python -c "
import tomllib, pathlib
data = tomllib.loads(pathlib.Path('pyproject.toml').read_text())
print(data['project']['version'])
")
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Detected lightapi version: $VERSION"

- name: Set up QEMU
uses: docker/setup-qemu-action@v3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Compute image tags
id: meta
uses: docker/metadata-action@v5
with:
images: iklob1/lightapi
tags: |
# On a v*.*.* tag → :0.1.21, :0.1, :latest
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
# On a push to master → :master (rolling head)
type=raw,value=master,enable=${{ github.ref == 'refs/heads/master' }}
# Manual run → :manual-<run-number>
type=raw,value=manual-${{ github.run_number }},enable=${{ github.event_name == 'workflow_dispatch' }}

- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
LIGHTAPI_VERSION=${{ steps.version.outputs.version }}
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Image digest summary
run: |
echo "### Published image" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "Tags:" >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
echo "${{ steps.meta.outputs.tags }}" >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
135 changes: 135 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# Changelog

All notable changes to this project will be documented in this file.

The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
Versions align with [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

---

## [Unreleased] — 0.1.24

### Fixed
- **`login_validator` exception → 401**: any exception raised by a `login_validator`
now returns `401 Unauthorized` (same as returning `None`) instead of propagating
as `500 Internal Server Error`. The exception is logged at `WARNING` level.
- **`SearchFilter` LIKE wildcard injection**: `%` and `_` in `?search=` values were
treated as SQL LIKE wildcards, causing `hello_world` to match `helloXworld` and
a bare `%` to match every row. Both characters are now escaped before the
`ILIKE` pattern is applied.
- **`OrderingFilter` empty whitelist**: when `Meta.filtering.ordering` was not set,
the guard `if allowed and …` short-circuited to allow ordering by any column.
An empty or omitted whitelist now disables ordering entirely, consistent with how
`FieldFilter` and `SearchFilter` handle unconfigured backends.
- **`PATCH null` clears `Optional` fields**: the `v is not None` guard in the PATCH
update path prevented users from ever clearing a nullable column. The guard now
consults the SQLAlchemy column inspector and allows `null` through for nullable
columns; non-nullable columns still ignore `null` values.
- **`from_dict` fields were silently dropped**: `LightApi.from_dict()` stored field
types as plain values without setting `__annotations__`, so `RestEndpointMeta`
never created the corresponding columns. All user-defined fields were absent from
every response. `__annotations__` is now populated before `type()` is called.
- **`from_dict` methods not enforced**: passing `methods=["GET", "POST"]` had no
effect because `class_attrs["__bases__"]` is ignored by `type()`. `HttpMethod`
mixins are now passed as real base-class arguments, so unlisted verbs correctly
return `405 Method Not Allowed`.

### Changed
- `test_login_validator_exception_returns_500` updated to assert `401` and verify
the `"Invalid credentials"` response body, reflecting the corrected behaviour.

### Documentation
- `README.md`: completed `LightApi()` constructor signature (added `mode`,
`auth_path`, `session_manager`, `rate_limiter`, `login_validator`,
`use_test_isolation`); corrected rate-limiter dict keys
(`requests_per_minute` / `requests_per_hour` / `requests_per_day`); clarified
that the rate limiter applies to `/auth/login` only; added notes on
`SearchFilter` literal matching, `OrderingFilter` whitelist requirement, PATCH
null-clearing for `Optional` fields, and `login_validator` exception handling.
- `docs/advanced/filtering.md`: added LIKE-literal paragraph and empty-whitelist
behavior for `OrderingFilter`.
- `docs/api-reference/filters.md`: same additions in API-reference form.
- `docs/advanced/authentication.md`: documented that validator exceptions yield 401.
- `docs/api-reference/rest.md`: added `### PATCH and Optional fields` subsection.
- `docs/api-reference/core.md`: expanded `login_validator` description; noted that
`from_dict` `methods` key enforces HTTP verbs.

---

## [0.1.23] — 2025-01-xx

### Fixed
- Docker image published under `iklob1/lightapi` instead of `iklobato/lightapi`.

---

## [0.1.22] — 2025-01-xx

### Added
- Published ready-to-use `iklob1/lightapi` Docker image with multi-arch support
(`linux/amd64`, `linux/arm64`). Mount a YAML config and run without any Python
install.

---

## [0.1.21] — 2025-01-xx

### Added
- 18 example scripts covering every LightAPI feature (`examples/01_minimal.py`
through `examples/18_full_api.py`).
- `mode` parameter on `LightApi` for explicit sync/async selection (auto-detected
from engine type and `async def` overrides when omitted).
- Global `rate_limiter` parameter on `LightApi`; configures the `/auth/login`
rate-limiter via `RateLimiter` instance or `{"requests_per_minute": N, …}` dict.
- `validate_credentials` support on auth backends.
- `authentication/` submodule replacing the flat `auth.py` module.

### Fixed
- Async engine handling; `AsyncEngine` unwrapped correctly for sync callers.
- SQLAlchemy test-isolation pollution across test sessions.
- Auth-checker bugs with missing `Meta.authentication` configurations.

### Changed
- `_registry.py` service-locator pattern removed; session management is now
injected directly via `SessionManager`.

---

## [0.1.20] — 2025-01-xx

### Changed
- Removed legacy v1 features and aligned documentation with v2 implementation.

---

## [0.1.19] — 2025-01-xx

### Changed
- Database connection now configured exclusively via `LIGHTAPI_DATABASE_URL`
environment variable when no `engine` or `database_url` argument is passed.

---

## [0.1.18] — 2025-01-xx

### Changed
- Linter and type-checker configuration aligned; `ruff` and `mypy` clean across
the core package.

---

## [0.1.17] — 2025-01-xx

### Fixed
- Test-suite failures in v2 integration tests resolved.

---

[Unreleased]: https://github.com/iklobato/lightapi/compare/v0.1.23...HEAD
[0.1.23]: https://github.com/iklobato/lightapi/compare/v0.1.22...v0.1.23
[0.1.22]: https://github.com/iklobato/lightapi/compare/v0.1.21...v0.1.22
[0.1.21]: https://github.com/iklobato/lightapi/compare/v0.1.20...v0.1.21
[0.1.20]: https://github.com/iklobato/lightapi/compare/v0.1.19...v0.1.20
[0.1.19]: https://github.com/iklobato/lightapi/compare/v0.1.18...v0.1.19
[0.1.18]: https://github.com/iklobato/lightapi/compare/v0.1.17...v0.1.18
[0.1.17]: https://github.com/iklobato/lightapi/compare/v0.1.16...v0.1.17
65 changes: 65 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# syntax=docker/dockerfile:1.7

# Ready-to-run LightAPI image.
# Users mount their config at /app/lightapi.yaml and run the container —
# no Python code or build step required on their side.
#
# Build: docker build -t lightapi:local .
# Run: docker run --rm -p 8000:8000 \
# -v "$(pwd)/lightapi.yaml:/app/lightapi.yaml:ro" \
# -e DATABASE_URL=sqlite:////app/data.db \
# lightapi:local

FROM python:3.12-slim AS base

ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1 \
PIP_NO_CACHE_DIR=1 \
LIGHTAPI_CONFIG=/app/lightapi.yaml \
LIGHTAPI_HOST=0.0.0.0 \
LIGHTAPI_PORT=8000 \
LIGHTAPI_LOG_LEVEL=info

WORKDIR /app

# Build dependencies for psycopg2 / asyncpg native code. Keep build-essential
# in the layer so we can compile, then remove it.
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
build-essential \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*

# Install LightAPI + async + PostgreSQL drivers. We use the published wheel
# rather than copying the source so the image stays useful even when this
# Dockerfile is built outside the repo.
ARG LIGHTAPI_VERSION
RUN if [ -n "$LIGHTAPI_VERSION" ]; then \
pip install "lightapi[async]==$LIGHTAPI_VERSION" psycopg2-binary ; \
else \
pip install "lightapi[async]" psycopg2-binary ; \
fi \
&& apt-get purge -y --auto-remove build-essential \
&& rm -rf /root/.cache

# Copy the launcher. Everything user-supplied lives outside /app/entrypoint.py.
COPY docker/entrypoint.py /app/entrypoint.py

# Non-root runtime user
RUN groupadd --system --gid 1001 lightapi \
&& useradd --system --uid 1001 --gid lightapi --home-dir /app --no-create-home lightapi \
&& chown -R lightapi:lightapi /app
USER lightapi

EXPOSE 8000

# Lightweight health check — touches the root path expecting any HTTP response.
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD python -c "import urllib.request, sys; \
import os; \
url='http://127.0.0.1:'+os.environ.get('LIGHTAPI_PORT','8000')+'/'; \
sys.exit(0 if urllib.request.urlopen(url, timeout=2).status else 1)" \
|| exit 1

ENTRYPOINT ["python", "/app/entrypoint.py"]
Loading
Loading