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
32 changes: 32 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: CI

on:
push:
branches: [dev]
pull_request:
branches: [main, dev]

jobs:
test:
name: Test (Python ${{ matrix.python-version }})
runs-on: ubuntu-latest

strategy:
matrix:
python-version: ["3.11", "3.12", "3.13"]

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

- name: Setup Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: pip

- name: Install dependencies
run: pip install -e ".[dev]"

- name: Run tests
run: python -m pytest tests/ -v
89 changes: 89 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
name: Release

on:
push:
branches: [main]

jobs:
test:
name: Test before release
runs-on: ubuntu-latest

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

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip

- name: Install dependencies
run: pip install -e ".[dev]"

- name: Run tests
run: python -m pytest tests/ -v

publish:
name: Publish to PyPI
runs-on: ubuntu-latest
needs: test

permissions:
contents: write

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

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip

- name: Install build tools
run: pip install build twine

- name: Check if version is already published
id: version_check
run: |
PKG_VERSION=$(python -c "import tomllib; f=open('pyproject.toml','rb'); d=tomllib.load(f); print(d['project']['version'])")
PYPI_VERSION=$(pip index versions apiforgepy 2>/dev/null | grep -oP '(?<=apiforgepy \()[\d.]+(?=\))' | head -1 || echo "none")
echo "pkg_version=$PKG_VERSION" >> $GITHUB_OUTPUT
echo "pypi_version=$PYPI_VERSION" >> $GITHUB_OUTPUT
if [ "$PKG_VERSION" != "$PYPI_VERSION" ]; then
echo "should_publish=true" >> $GITHUB_OUTPUT
echo "New version detected: $PKG_VERSION (PyPI has $PYPI_VERSION)"
else
echo "should_publish=false" >> $GITHUB_OUTPUT
echo "Version $PKG_VERSION is already on PyPI — skipping publish."
fi

- name: Build package
if: steps.version_check.outputs.should_publish == 'true'
run: python -m build

- name: Publish to PyPI
if: steps.version_check.outputs.should_publish == 'true'
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
run: twine upload dist/*

- name: Create GitHub Release
if: steps.version_check.outputs.should_publish == 'true'
uses: actions/github-script@v7
with:
script: |
const version = '${{ steps.version_check.outputs.pkg_version }}';
await github.rest.repos.createRelease({
owner: context.repo.owner,
repo: context.repo.repo,
tag_name: `v${version}`,
name: `v${version}`,
body: `## apiforgepy v${version}\n\nSee [CHANGELOG.md](https://github.com/APIForge-Organisation/sdk-python/blob/main/CHANGELOG.md) for details.\n\n\`\`\`bash\npip install apiforgepy==${version}\n\`\`\``,
draft: false,
prerelease: version.startsWith('0.'),
});
67 changes: 67 additions & 0 deletions .github/workflows/sync-dashboard.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: Sync Dashboard UI

on:
repository_dispatch:
types: [dashboard-ui-updated]

jobs:
sync:
name: Fetch and embed updated UI
runs-on: ubuntu-latest

steps:
- name: Checkout dev
uses: actions/checkout@v4
with:
ref: dev
token: ${{ secrets.GH_PAT }}

- name: Fetch latest ui.html from dashboard-ui
run: |
curl -sSfL \
"https://raw.githubusercontent.com/APIForge-Organisation/dashboard-ui/main/ui.html" \
-o /tmp/ui.html

- name: Embed HTML into dashboard.py
run: |
python3 - << 'PYEOF'
import re

with open('/tmp/ui.html', 'r', encoding='utf-8') as f:
html = f.read()

with open('apiforgepy/dashboard.py', 'r', encoding='utf-8') as f:
content = f.read()

# Escape backslashes and triple-quotes for a Python triple-quoted string
escaped = html.replace('\\', '\\\\').replace('"""', '\\"\\"\\"')

start = '# <<DASHBOARD_UI_START>>'
end = '# <<DASHBOARD_UI_END>>'
block = f'{start}\n_HTML = """\\\n{escaped}\n"""\n{end}'

new_content = re.sub(
re.escape(start) + r'.*?' + re.escape(end),
block,
content,
flags=re.DOTALL,
)

with open('apiforgepy/dashboard.py', 'w', encoding='utf-8') as f:
f.write(new_content)

print('dashboard.py updated.')
PYEOF

- name: Commit and push if changed
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add apiforgepy/dashboard.py
if git diff --staged --quiet; then
echo "No changes — already up to date."
else
git commit -m "chore: sync dashboard UI from dashboard-ui@${{ github.event.client_payload.sha }}"
git push origin dev
echo "Pushed updated dashboard.py to dev."
fi
16 changes: 16 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
__pycache__/
*.py[cod]
*.egg-info/
dist/
build/
.eggs/
.venv/
venv/
.apiforge.db
*.db-shm
*.db-wal
.pytest_cache/
.coverage
htmlcov/
.env
.env.*
43 changes: 43 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Changelog

All notable changes to `apiforgepy` are documented here.

Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) — versioning follows [Semantic Versioning](https://semver.org/).

---

## [1.0.0] — 2026-05-15

### Fixed

- `/api/summary` now returns a flat response structure (`calls_24h`, `error_rate_24h`, `avg_p90_24h`, `active_routes`) matching the React dashboard frontend — fixes "0 requests" display

### Added

- `/api/global-timeseries` endpoint consumed by the dashboard overview chart
- `/api/releases` endpoint (returns `[]` for now — release tracking planned)

### Changed

- Dashboard UI now loads React and Babel from jsDelivr CDN instead of local assets, making `ui.html` SDK-agnostic

---

## [0.1.0] — 2026-05-15

### Added

- Starlette/FastAPI middleware `ApiForgeMiddleware` — drop-in observability with zero mandatory configuration
- Local-first mode with SQLite storage via Python's built-in `sqlite3` module (requires Python ≥ 3.11)
- Per-endpoint metrics: P50 / P90 / P99 latency, request count, 2xx / 4xx / 5xx breakdown
- In-memory aggregation with configurable flush interval (default: 60s), thread-safe
- Circuit breaker on the transport layer — middleware never crashes the host application
- Built-in dashboard on port 4242 with dark theme, Chart.js time series, routes table and insights panel
- REST API: `/api/summary`, `/api/routes`, `/api/timeseries`, `/api/insights`
- Three automatic insight types: `ANOMALY` (Z-score P99), `DEAD` (endpoint inactive 21+ days), `PERF`/`OK` (regression or improvement after a release)
- API Health Score (0–100) combining availability, performance, stability and quality
- Configurable sampling rate, ignored paths, environment label, release tag and service name
- `middleware.shutdown()` for graceful teardown (flushes buffer, closes SQLite)

[1.0.0]: https://github.com/APIForge-Organisation/sdk-python/releases/tag/v1.0.0
[0.1.0]: https://github.com/APIForge-Organisation/sdk-python/releases/tag/v0.1.0
97 changes: 97 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# apiforgepy

**API observability & intelligence for FastAPI/Starlette — local-first, privacy-first.**

[![PyPI version](https://img.shields.io/pypi/v/apiforgepy?color=0066FF)](https://pypi.org/project/apiforgepy/)
[![CI](https://img.shields.io/github/actions/workflow/status/APIForge-Organisation/sdk-python/ci.yml?branch=main&label=CI)](https://github.com/APIForge-Organisation/sdk-python/actions)
[![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
[![Python](https://img.shields.io/badge/python-%3E%3D3.11-brightgreen)](https://python.org)

> Track latency, error rates, and behavioral trends of your APIs. Everything stays on your machine.

**→ [Full documentation](https://apiforge-organisation.github.io/docs/)**

---

## Install

```bash
pip install apiforgepy
```

## Quick start

```python
from fastapi import FastAPI
from apiforgepy import ApiForgeMiddleware

app = FastAPI()

app.add_middleware(
ApiForgeMiddleware,
mode="local",
)

@app.get("/users/{user_id}")
def get_user(user_id: int):
return {"id": user_id}

# Dashboard → http://localhost:4242
```

## Configuration

```python
app.add_middleware(
ApiForgeMiddleware,
mode="local",
db_path=".apiforge.db",
dashboard_port=4242, # set to 0 to disable
flush_interval=60_000, # ms
env="production",
release="v1.4.0", # enables release regression detection
service="user-service",
sampling=1.0, # 0.0–1.0
ignore_paths=["/health", "/favicon.ico"],
)
```

## What you get

- **Latency percentiles** — P50 / P90 / P99 per endpoint, updated every 60s
- **Error rate by route** — 2xx / 4xx / 5xx breakdown in real time
- **API Health Score** — a single 0–100 score summarizing your API's health
- **Automatic insights** — plain-language alerts with no configuration
- **Dead endpoint detection** — routes with no traffic in 21+ days
- **Release impact tracking** — before/after comparison on every deploy

## Graceful shutdown

```python
import signal

mw = None

@asynccontextmanager
async def lifespan(app):
yield
if mw:
mw.shutdown()

app = FastAPI(lifespan=lifespan)
mw = ApiForgeMiddleware.__new__(ApiForgeMiddleware)
app.add_middleware(ApiForgeMiddleware, mode="local")
```

## Privacy by design

The middleware **never** reads request or response bodies, headers, cookies, or tokens. Route parameters are captured as patterns only (`/users/{user_id}` — never `/users/42`). In local mode, zero data leaves your machine.

## Requirements

- Python ≥ 3.11
- Starlette ≥ 0.27 (FastAPI ≥ 0.110 includes this)

## License

MIT — [APIForge Organisation](https://github.com/APIForge-Organisation)
Loading
Loading