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
4 changes: 1 addition & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,7 @@ jobs:
uv sync --all-groups

- name: Run security checks
run: |
uv pip install safety
uv run safety check
run: uv run pip-audit

integration:
runs-on: ubuntu-latest
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/dependency-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ jobs:
uses: actions/checkout@v4
- name: 'Dependency Review'
uses: actions/dependency-review-action@v3
continue-on-error: true # Requires Dependency graph + Advanced Security (private repos)
1 change: 1 addition & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ permissions:
contents: write
pages: write
id-token: write
pull-requests: write # required for docs-preview to comment on PRs

jobs:
build-docs:
Expand Down
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,14 @@ ipython_config.py
# install all needed dependencies.
#Pipfile.lock

# Cursor
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
# refer to https://docs.cursor.com/context/ignore-files
.cursorignore
.cursorindexingignore
.claude/settings.local.json

# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/

Expand Down Expand Up @@ -152,3 +160,5 @@ dmypy.json
# Cython debug symbols
cython_debug/
config/endpoints.json

.claude/*
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# Python version should stay in sync with pyproject.toml requires-python (>=3.12)
FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim

WORKDIR /app
Expand Down
6 changes: 4 additions & 2 deletions cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,10 @@ def cli():


@cli.command()
@click.argument("host", default="127.0.0.1")
@click.argument("port", type=int, default=8000)
@click.option("--host", "-h", default="127.0.0.1", show_default=True, help="Bind host")
@click.option(
"--port", "-p", type=int, default=8000, show_default=True, help="Bind port"
)
@click.option("--reload", is_flag=True, help="Enable auto-reload")
def serve(host: str, port: int, reload: bool):
"""Start the API server."""
Expand Down
4 changes: 2 additions & 2 deletions config/endpoint_config.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
"""Endpoint configuration models and validation."""

from enum import Enum
from enum import StrEnum
from typing import Any

from pydantic import BaseModel, Field, field_validator


class HTTPMethod(str, Enum):
class HTTPMethod(StrEnum):
"""HTTP methods supported by endpoints."""

GET = "GET"
Expand Down
7 changes: 6 additions & 1 deletion core/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,12 @@ def error_rate(self) -> float:


class MetricsCollector:
"""Collects metrics for the API."""
"""Collects metrics for the API.

Note: State is per-process. When running multiple Gunicorn workers,
each worker maintains its own metrics. Use a shared store (e.g. Redis
or Prometheus) if aggregated cross-worker metrics are needed.
"""

def __init__(self):
"""Initialize metrics collector."""
Expand Down
8 changes: 7 additions & 1 deletion core/rate_limiter.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@


class RateLimiter:
"""Simple in-memory rate limiter."""
"""Simple in-memory rate limiter.

Note: State is per-process. When running multiple Gunicorn workers,
each worker maintains its own rate limit counters, effectively
multiplying the allowed rate by the number of workers. Use a shared
store (e.g. Redis) if accurate cross-worker rate limiting is needed.
"""

def __init__(self):
"""Initialize rate limiter."""
Expand Down
2 changes: 2 additions & 0 deletions core/services/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

from core.services.base import BaseService

__all__ = ["BaseService", "register_service", "get_service", "list_services"]

# Service registry
_service_registry: dict[str, type[BaseService]] = {}

Expand Down
34 changes: 31 additions & 3 deletions docs/about/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,29 @@ All notable changes to Apiary will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.1.1] - 2026-02-14

### Fixed

- **pytest-asyncio version**: Changed from `>=1.3.0` (nonexistent) to `>=0.24.0`
- **pytest.ini**: Removed buggy `--ignore=tests` directive that contradicted `testpaths=tests`, consolidated all pytest config into `pyproject.toml`
- **HTTPMethod enum**: Migrated from `(str, Enum)` to `StrEnum` (Python 3.12+, fixes ruff UP042)

### Changed

- **Health readiness probe**: Removed hardcoded CoinLore crypto API dependency check from `/health/ready`; now checks configuration validity and reports registered services without coupling to specific service implementations
- **CLI serve command**: Changed `host` and `port` from positional arguments to `--host`/`-h` and `--port`/`-p` options with defaults shown
- **Service registry exports**: Added `__all__` to `core/services/__init__.py` for clean public API

### Added

- **`py.typed` marker**: PEP 561 compliance for downstream type checking
- **Multi-worker documentation**: Added warnings to `RateLimiter` and `MetricsCollector` classes noting that in-memory state is per-process and not shared across Gunicorn workers
- **Deployment docs**: Added multi-worker caveats to monitoring and deployment configuration guides
- **Version bump**: `0.1.0` to `0.1.1` in `pyproject.toml`

---

## [0.1.0] - 2026-01-26

### Added
Expand Down Expand Up @@ -103,13 +126,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Upgrade Notes

This is the initial release, no upgrade paths exist yet.
#### From 0.1.0 to 0.1.1

- **CLI**: `apiary serve HOST PORT` is now `apiary serve --host HOST --port PORT`. The old positional syntax no longer works.
- **Health check**: `/health/ready` no longer checks the CoinLore API. If you were relying on the `crypto_api` field in the readiness response, it has been replaced by a `services` field listing all registered services.
- **pytest config**: `pytest.ini` has been removed. All pytest configuration is now in `pyproject.toml`. If you had local customizations in `pytest.ini`, migrate them to `[tool.pytest.ini_options]` in `pyproject.toml`.

### Breaking Changes

None (initial release)
- `apiary serve` positional arguments replaced with `--host`/`--port` options (v0.1.1)

---

[0.1.0]: https://github.com/lancereinsmith/apiary/releases/tag/v0.1.0
[Unreleased]: https://github.com/lancereinsmith/apiary/compare/v0.1.0...HEAD
[0.1.1]: https://github.com/lancereinsmith/apiary/releases/tag/v0.1.1
[Unreleased]: https://github.com/lancereinsmith/apiary/compare/v0.1.1...HEAD
8 changes: 7 additions & 1 deletion docs/deployment/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,13 @@ export ENABLE_REDOC=false
ExecStart=gunicorn -w 4 -k uvicorn.workers.UvicornWorker app:api
```

Workers = (2 × CPU cores) + 1
Workers = (2 x CPU cores) + 1

!!! note "Multi-Worker Consideration"
Rate limiting and metrics use in-memory storage that is not shared between
workers. With 4 workers, clients effectively get 4x the configured rate
limit and `/metrics` shows per-worker data. Use a single worker if accurate
rate limiting is critical, or use Redis for shared state.

### nginx

Expand Down
7 changes: 7 additions & 0 deletions docs/deployment/monitoring.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ curl <https://yourdomain.com/health/ready>
curl <https://yourdomain.com/metrics>
```

!!! note "Multi-Worker Limitation"
Metrics and rate limiting use in-memory storage, so each Gunicorn worker
maintains its own counters. With multiple workers, the `/metrics` endpoint
shows per-worker data and rate limits are effectively multiplied by the
number of workers. For aggregated metrics across workers, use an external
collector like Prometheus or Redis.

## Logs

```bash
Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ pytest --cov # With coverage
# Use different port
uvicorn app:api --port 8001
# Or use CLI
uv run apiary serve 127.0.0.1 8001
uv run apiary serve --port 8001
```

### Settings File Not Found
Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,7 @@ lsof -i:8000

# Use different port
uvicorn app:api --port 8001
# Or: uv run apiary serve 127.0.0.1 8001
# Or: uv run apiary serve --port 8001
```

### Endpoint Not Found
Expand Down
35 changes: 26 additions & 9 deletions docs/guide/builtin-endpoints.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ Response:

**Endpoint:** `GET /health/ready`

Kubernetes-compatible readiness probe. Checks if the application and its dependencies are ready to serve traffic.
Kubernetes-compatible readiness probe. Checks configuration validity and reports registered services.

```bash
curl http://localhost:8000/health/ready
Expand All @@ -112,22 +112,39 @@ Response (healthy):
```json
{
"status": "ready",
"version": "0.1.1",
"uptime_seconds": 3600,
"dependencies": {
"external_apis": "healthy"
},
"uptime_seconds": 3600
"configuration": {
"status": "healthy"
},
"services": {
"status": "healthy",
"registered": ["crypto", "hello"],
"count": 2
}
}
}
```

Response (degraded):
Response (unready):

```json
{
"status": "degraded",
"status": "unready",
"version": "0.1.1",
"uptime_seconds": 3600,
"dependencies": {
"external_apis": "unhealthy"
},
"uptime_seconds": 3600
"configuration": {
"status": "unhealthy",
"error": "..."
},
"services": {
"status": "healthy",
"registered": [],
"count": 0
}
}
}
```

Expand Down
Empty file added py.typed
Empty file.
28 changes: 18 additions & 10 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,41 +6,43 @@ build-backend = "hatchling.build"

[project]
name = "apiary"
version = "0.1.0"
version = "0.1.1"
description = "Apiary - Personal API service for various projects"
requires-python = ">=3.12"
dependencies = [
"fastapi>=0.128.0",
"fastapi>=0.129.0",
"uvicorn[standard]>=0.40.0",
"jinja2>=3.1.6",
"aiofiles>=25.1.0",
"requests>=2.32.5",
"beautifulsoup4>=4.14.3",
"gunicorn>=24.1.1",
"gunicorn>=25.1.0",
"uvloop>=0.22.1",
"httptools>=0.7.1",
"httpx>=0.28.1",
"pydantic>=2.12.5",
"pydantic-settings>=2.12.0",
"pydantic-settings>=2.13.0",
"python-dotenv>=1.2.1",
"watchdog>=6.0.0",
"click>=8.3.1",
"urllib3>=2.6.3",
]

[dependency-groups]
dev = [
"pytest>=9.0.2",
"pytest-asyncio>=1.3.0",
"pytest-asyncio>=0.24.0",
"pytest-cov>=7.0.0",
"ruff>=0.14.14",
"rumdl>=0.1.1",
"ty>=0.0.13",
"ruff>=0.15.1",
"rumdl>=0.1.21",
"ty>=0.0.17",
"pip-audit>=2.10.0",
]
docs = [
"mkdocs>=1.6.1",
"mkdocs-material>=9.7.1",
"mkdocs-click>=0.8.1",
"mkdocstrings[python]>=1.0.2",
"mkdocs-click>=0.9.0",
"mkdocstrings[python]>=1.0.3",
]

[tool.uv]
Expand Down Expand Up @@ -84,4 +86,10 @@ exclude = [".git", ".github", "node_modules", "vendor", "dist", "build", "CHANGE
# ——— Pytest (optional: remove [tool.pytest.ini_options] and the test group if you have no tests) ———
[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"
addopts = ["-v", "--tb=short", "--strict-markers", "--strict-config"]
markers = [
"unit: Unit tests",
"integration: Integration tests",
"slow: Slow running tests",
]
19 changes: 0 additions & 19 deletions pytest.ini

This file was deleted.

6 changes: 5 additions & 1 deletion routers/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
"""Router modules for API endpoints."""
"""Router modules for API endpoints.

Routers are auto-discovered by app.py from this directory
and from routers_custom/ (if present).
"""
Loading
Loading