fix: delete artefacts
This commit is contained in:
30
.github/PULL_REQUEST_TEMPLATE.md
vendored
30
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,30 +0,0 @@
|
|||||||
## Description
|
|
||||||
<!-- Brief description of changes -->
|
|
||||||
|
|
||||||
## Type of Change
|
|
||||||
<!-- Mark with [x] -->
|
|
||||||
- [ ] 🚀 Feature (`feat`)
|
|
||||||
- [ ] 🐛 Bug Fix (`fix`)
|
|
||||||
- [ ] 📝 Documentation (`docs`)
|
|
||||||
- [ ] ♻️ Refactor (`refactor`)
|
|
||||||
- [ ] 🎨 Code Style (`style`)
|
|
||||||
- [ ] ✅ Tests (`test`)
|
|
||||||
- [ ] 🔧 Chore (`chore`)
|
|
||||||
|
|
||||||
## Checklist
|
|
||||||
- [ ] Code follows project style guidelines (ruff, isort)
|
|
||||||
- [ ] Tests added/updated (if applicable)
|
|
||||||
- [ ] Documentation updated (if applicable)
|
|
||||||
- [ ] Commit message follows convention (`type: description`)
|
|
||||||
- [ ] Branch rebased to single commit before merge
|
|
||||||
- [ ] No cache files in commit (`__pycache__`, `*.pyc`)
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
<!-- Describe how changes were tested -->
|
|
||||||
|
|
||||||
## Related Issues
|
|
||||||
<!-- Link to issues if applicable -->
|
|
||||||
Fixes #
|
|
||||||
|
|
||||||
## Screenshots (if applicable)
|
|
||||||
<!-- Add screenshots for UI changes -->
|
|
||||||
13
.gitignore
vendored
13
.gitignore
vendored
@@ -8,14 +8,6 @@ site/
|
|||||||
*.pyc
|
*.pyc
|
||||||
*.pyo
|
*.pyo
|
||||||
|
|
||||||
# opencode skills (agent-only)
|
|
||||||
.opencode/
|
|
||||||
AGENTS.md
|
|
||||||
.github/
|
|
||||||
|
|
||||||
# Scripts (except hooks)
|
|
||||||
scripts/
|
|
||||||
|
|
||||||
# IDE
|
# IDE
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
@@ -36,13 +28,8 @@ htmlcov/
|
|||||||
|
|
||||||
# Environment
|
# Environment
|
||||||
.env
|
.env
|
||||||
.env.example
|
|
||||||
.venv/
|
.venv/
|
||||||
venv/
|
venv/
|
||||||
|
|
||||||
# uv cache
|
# uv cache
|
||||||
.uv/
|
.uv/
|
||||||
|
|
||||||
# Scripts cache
|
|
||||||
scripts/__pycache__/
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
when:
|
|
||||||
event: [push, pull_request]
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: comment
|
|
||||||
image: mcs94/gitea-comment
|
|
||||||
settings:
|
|
||||||
gitea_address: https://git.pyaqa.ru
|
|
||||||
gitea_token:
|
|
||||||
from_secret: gitea_token
|
|
||||||
comment: >
|
|
||||||
✅ Build ${CI_BUILD_EVENT} of `${CI_REPO_NAME}` has status `${CI_BUILD_STATUS}`.
|
|
||||||
|
|
||||||
📝 Commit by ${CI_COMMIT_AUTHOR} on `${CI_COMMIT_BRANCH}`:
|
|
||||||
|
|
||||||
`${CI_COMMIT_MESSAGE}`
|
|
||||||
|
|
||||||
🌐 ${CI_BUILD_LINK}
|
|
||||||
|
|
||||||
depends_on:
|
|
||||||
- lint
|
|
||||||
- type
|
|
||||||
- test
|
|
||||||
|
|
||||||
44
AGENTS.md
Normal file
44
AGENTS.md
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# Blog AGENTS.md
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
- Python 3.13+, FastAPI, pydantic, uvicorn
|
||||||
|
- Package manager: `uv`
|
||||||
|
- CI: Woodpecker (lint, test, type on push/PR to `dev`)
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
```bash
|
||||||
|
uv sync --group dev # Install all dev dependencies
|
||||||
|
uv run pytest # Run tests (coverage >= 70% required)
|
||||||
|
uv run pytest tests/unit/ # Run single test directory
|
||||||
|
uv run ruff check . --fix # Lint
|
||||||
|
uv run ruff format # Format
|
||||||
|
uv run isort . # Sort imports
|
||||||
|
uv run mypy . # Type check (strict mode)
|
||||||
|
uv run blog # Start dev server (port 8000)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pre-commit order
|
||||||
|
`ruff check --fix` → `ruff format` → `isort` → `mypy`
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
```
|
||||||
|
app/
|
||||||
|
main.py # Entry point, uvicorn.run(app_factory)
|
||||||
|
core/config.py # Settings from .env via pydantic-settings
|
||||||
|
core/exceptions.py
|
||||||
|
common/error_handler.py
|
||||||
|
api/v1/
|
||||||
|
modules/
|
||||||
|
tests/
|
||||||
|
unit/
|
||||||
|
integration/
|
||||||
|
e2e/
|
||||||
|
api/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key conventions
|
||||||
|
- All commands use `uv run` prefix
|
||||||
|
- pytest: asyncio_mode=auto, coverage on `app/`
|
||||||
|
- mypy: strict=true with pydantic plugin
|
||||||
|
- isort: black profile, filter_files=true
|
||||||
|
- `.env` loaded by pydantic-settings (not in repo)
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
# API Endpoints
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
| Method | Endpoint | Description |
|
|
||||||
|--------|----------|-------------|
|
|
||||||
| GET | `/` | Health check |
|
|
||||||
|
|
||||||
## Health Check
|
|
||||||
|
|
||||||
```http
|
|
||||||
GET /
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response:** `200 OK`
|
|
||||||
|
|
||||||
Returns application status.
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
# API Reference
|
|
||||||
|
|
||||||
This section contains auto-generated API documentation from source code docstrings.
|
|
||||||
|
|
||||||
## Modules
|
|
||||||
|
|
||||||
::: app.main
|
|
||||||
handler: python
|
|
||||||
options:
|
|
||||||
members:
|
|
||||||
- lifespan
|
|
||||||
- app_factory
|
|
||||||
- main
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
# Code Style
|
|
||||||
|
|
||||||
## Linting & Formatting
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Run all linters
|
|
||||||
uv run ruff check . --fix
|
|
||||||
uv run ruff format .
|
|
||||||
uv run isort . --profile black --filter-files
|
|
||||||
|
|
||||||
# Type checking
|
|
||||||
uv run mypy .
|
|
||||||
```
|
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check docstring style
|
|
||||||
uv run pydocstyle app/
|
|
||||||
|
|
||||||
# Check documentation coverage
|
|
||||||
uv run interrogate app/ -v
|
|
||||||
|
|
||||||
# Build documentation
|
|
||||||
uv run mkdocs build
|
|
||||||
|
|
||||||
# Serve documentation locally
|
|
||||||
uv run mkdocs serve
|
|
||||||
```
|
|
||||||
|
|
||||||
## Pre-commit Hooks
|
|
||||||
|
|
||||||
This project uses pre-commit hooks to ensure code quality:
|
|
||||||
|
|
||||||
- ruff check
|
|
||||||
- ruff format
|
|
||||||
- isort
|
|
||||||
- mypy
|
|
||||||
|
|
||||||
Install hooks:
|
|
||||||
```bash
|
|
||||||
uv run pre-commit install
|
|
||||||
```
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
# Setup Guide
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
- Python 3.13+
|
|
||||||
- uv package manager
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Clone repository
|
|
||||||
git clone https://github.com/pyaqa/blog.git
|
|
||||||
cd blog
|
|
||||||
|
|
||||||
# Install dependencies
|
|
||||||
uv sync
|
|
||||||
|
|
||||||
# Run tests
|
|
||||||
uv run pytest
|
|
||||||
|
|
||||||
# Start development server
|
|
||||||
uv run python -m app.main
|
|
||||||
```
|
|
||||||
|
|
||||||
## Development Server
|
|
||||||
|
|
||||||
The server runs on `http://0.0.0.0:8000` by default.
|
|
||||||
|
|
||||||
Access interactive API docs at:
|
|
||||||
- Swagger UI: `http://localhost:8000/docs`
|
|
||||||
- ReDoc: `http://localhost:8000/redoc`
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
# Blog API
|
|
||||||
|
|
||||||
Welcome to the Blog API documentation.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- FastAPI-based REST API
|
|
||||||
- Python 3.13+
|
|
||||||
- Async support
|
|
||||||
- Type hints throughout
|
|
||||||
|
|
||||||
## Quick Start
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Install dependencies
|
|
||||||
uv sync
|
|
||||||
|
|
||||||
# Run development server
|
|
||||||
uv run python -m app.main
|
|
||||||
```
|
|
||||||
|
|
||||||
## API Endpoints
|
|
||||||
|
|
||||||
See [API Reference](api/endpoints.md) for detailed endpoint documentation.
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
See [Development Guide](development/setup.md) for setup instructions.
|
|
||||||
50
mkdocs.yml
50
mkdocs.yml
@@ -1,50 +0,0 @@
|
|||||||
site_name: Blog API Documentation
|
|
||||||
site_description: FastAPI Blog Application Documentation
|
|
||||||
site_author: Blog Team
|
|
||||||
repo_url: https://github.com/pyaqa/blog
|
|
||||||
|
|
||||||
theme:
|
|
||||||
name: mkdocs
|
|
||||||
palette:
|
|
||||||
- scheme: default
|
|
||||||
primary: indigo
|
|
||||||
accent: indigo
|
|
||||||
toggle:
|
|
||||||
icon: material/brightness-7
|
|
||||||
name: Switch to dark mode
|
|
||||||
- scheme: slate
|
|
||||||
primary: indigo
|
|
||||||
accent: indigo
|
|
||||||
toggle:
|
|
||||||
icon: material/brightness-4
|
|
||||||
name: Switch to light mode
|
|
||||||
|
|
||||||
plugins:
|
|
||||||
- search
|
|
||||||
- mkdocstrings:
|
|
||||||
handlers:
|
|
||||||
python:
|
|
||||||
options:
|
|
||||||
docstring_style: google
|
|
||||||
show_root_heading: true
|
|
||||||
show_source: true
|
|
||||||
show_bases: true
|
|
||||||
|
|
||||||
markdown_extensions:
|
|
||||||
- pymdownx.highlight:
|
|
||||||
anchor_linenums: true
|
|
||||||
- pymdownx.inlinehilite
|
|
||||||
- pymdownx.snippets
|
|
||||||
- pymdownx.superfences
|
|
||||||
- admonition
|
|
||||||
- pymdownx.details
|
|
||||||
- tables
|
|
||||||
|
|
||||||
nav:
|
|
||||||
- Home: index.md
|
|
||||||
- API Reference:
|
|
||||||
- Overview: api/index.md
|
|
||||||
- Endpoints: api/endpoints.md
|
|
||||||
- Development:
|
|
||||||
- Setup: development/setup.md
|
|
||||||
- Code Style: development/codestyle.md
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
# Development Scripts
|
|
||||||
|
|
||||||
## clean_cache.sh
|
|
||||||
|
|
||||||
Clean all Python cache files:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash scripts/clean_cache.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
Removes:
|
|
||||||
- `__pycache__/` directories
|
|
||||||
- `*.pyc`, `*.pyo` files
|
|
||||||
- `.pytest_cache/`
|
|
||||||
- `.mypy_cache/`
|
|
||||||
- `.ruff_cache/`
|
|
||||||
- `.coverage`
|
|
||||||
- `htmlcov/`
|
|
||||||
|
|
||||||
## update_readme.py
|
|
||||||
|
|
||||||
Update README.md with latest project information:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
uv run python scripts/update_readme.py
|
|
||||||
```
|
|
||||||
|
|
||||||
Check if update needed (for CI):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
uv run python scripts/update_readme.py --check
|
|
||||||
```
|
|
||||||
|
|
||||||
## post-commit
|
|
||||||
|
|
||||||
Git hook for auto-updating README after commits.
|
|
||||||
|
|
||||||
Install:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp scripts/post-commit .git/hooks/post-commit
|
|
||||||
chmod +x .git/hooks/post-commit
|
|
||||||
```
|
|
||||||
|
|
||||||
## Disable Python Cache During Development
|
|
||||||
|
|
||||||
Set environment variables before running Python:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Option 1: Export variables
|
|
||||||
export PYTHONDONTWRITEBYTECODE=1
|
|
||||||
export UV_NO_CACHE=1
|
|
||||||
|
|
||||||
# Option 2: Use with command
|
|
||||||
PYTHONDONTWRITEBYTECODE=1 uv run python -m app.main
|
|
||||||
|
|
||||||
# Option 3: Add to .env (not committed)
|
|
||||||
echo "PYTHONDONTWRITEBYTECODE=1" >> .env
|
|
||||||
```
|
|
||||||
|
|
||||||
Or use the clean script periodically:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash scripts/clean_cache.sh
|
|
||||||
```
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
echo "Cleaning Python cache files..."
|
|
||||||
|
|
||||||
find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
|
|
||||||
|
|
||||||
find . -type f -name "*.pyc" -delete 2>/dev/null || true
|
|
||||||
|
|
||||||
find . -type f -name "*.pyo" -delete 2>/dev/null || true
|
|
||||||
|
|
||||||
rm -rf .pytest_cache/ 2>/dev/null || true
|
|
||||||
|
|
||||||
rm -rf .mypy_cache/ 2>/dev/null || true
|
|
||||||
|
|
||||||
rm -rf .ruff_cache/ 2>/dev/null || true
|
|
||||||
|
|
||||||
rm -f .coverage 2>/dev/null || true
|
|
||||||
rm -rf htmlcov/ 2>/dev/null || true
|
|
||||||
|
|
||||||
echo "✓ Cache cleaned"
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
COMMIT_MSG_FILE="$1"
|
|
||||||
if [ -z "$COMMIT_MSG_FILE" ]; then
|
|
||||||
echo "Checking for cache files in staged changes..."
|
|
||||||
|
|
||||||
CACHE_FILES=$(git diff --cached --name-only | grep -E "__pycache__|\.pyc$|\.pyo$" || true)
|
|
||||||
|
|
||||||
if [ -n "$CACHE_FILES" ]; then
|
|
||||||
echo "❌ Attempting to commit Python cache files!"
|
|
||||||
echo ""
|
|
||||||
echo "Files:"
|
|
||||||
echo "$CACHE_FILES"
|
|
||||||
echo ""
|
|
||||||
echo "Run: bash scripts/clean_cache.sh"
|
|
||||||
echo "Or: git reset HEAD <files>"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "✓ No cache files in staged changes"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
COMMIT_MSG=$(cat "$COMMIT_MSG_FILE")
|
|
||||||
|
|
||||||
if ! echo "$COMMIT_MSG" | grep -qE "^(feat|fix|docs|style|refactor|test|chore): [a-z].{0,49}$"; then
|
|
||||||
echo "❌ Invalid commit message format!"
|
|
||||||
echo ""
|
|
||||||
echo "Current message: $COMMIT_MSG"
|
|
||||||
echo ""
|
|
||||||
echo "Expected format: <type>: <short description>"
|
|
||||||
echo ""
|
|
||||||
echo "Types:"
|
|
||||||
echo " feat - New feature"
|
|
||||||
echo " fix - Bug fix"
|
|
||||||
echo " docs - Documentation"
|
|
||||||
echo " style - Code style"
|
|
||||||
echo " refactor - Refactoring"
|
|
||||||
echo " test - Tests"
|
|
||||||
echo " chore - Maintenance"
|
|
||||||
echo ""
|
|
||||||
echo "Rules:"
|
|
||||||
echo " - Max 50 characters"
|
|
||||||
echo " - Lowercase after type"
|
|
||||||
echo " - Imperative mood (add, not added)"
|
|
||||||
echo " - No period at end"
|
|
||||||
echo ""
|
|
||||||
echo "Good examples:"
|
|
||||||
echo " feat: add user authentication"
|
|
||||||
echo " fix: resolve database timeout"
|
|
||||||
echo " docs: update API docs"
|
|
||||||
echo ""
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if echo "$COMMIT_MSG" | grep -qE "\.$"; then
|
|
||||||
echo "❌ Commit message should not end with a period"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "✓ Commit message valid: $COMMIT_MSG"
|
|
||||||
exit 0
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Post-commit hook: Update README.md automatically
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
echo "Updating README.md..."
|
|
||||||
|
|
||||||
# Run README update script
|
|
||||||
uv run python scripts/update_readme.py
|
|
||||||
|
|
||||||
# Check if README changed
|
|
||||||
if ! git diff --quiet README.md; then
|
|
||||||
echo "✓ README.md was updated"
|
|
||||||
echo " Review changes and commit if needed:"
|
|
||||||
echo " git add README.md && git commit -m 'docs: update README [skip ci]'"
|
|
||||||
else
|
|
||||||
echo "✓ README.md is up to date"
|
|
||||||
fi
|
|
||||||
@@ -1,358 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
import re
|
|
||||||
import subprocess
|
|
||||||
import tomllib
|
|
||||||
from datetime import datetime
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
|
|
||||||
def get_project_root() -> Path:
|
|
||||||
return Path(__file__).parent.parent
|
|
||||||
|
|
||||||
|
|
||||||
def get_pyproject() -> dict[str, Any]:
|
|
||||||
root = get_project_root()
|
|
||||||
with open(root / "pyproject.toml", "rb") as f:
|
|
||||||
return tomllib.load(f)
|
|
||||||
|
|
||||||
|
|
||||||
def get_latest_commits(count: int = 10) -> list[dict[str, str]]:
|
|
||||||
result = subprocess.run(
|
|
||||||
["git", "log", "--format=%H|%s|%ad|%an", "--date=short", f"-n{count}"],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
cwd=get_project_root(),
|
|
||||||
)
|
|
||||||
|
|
||||||
commits = []
|
|
||||||
for line in result.stdout.strip().split("\n"):
|
|
||||||
if line:
|
|
||||||
parts = line.split("|")
|
|
||||||
if len(parts) >= 4:
|
|
||||||
commits.append(
|
|
||||||
{
|
|
||||||
"hash": parts[0][:7],
|
|
||||||
"message": parts[1],
|
|
||||||
"date": parts[2],
|
|
||||||
"author": parts[3],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return commits
|
|
||||||
|
|
||||||
|
|
||||||
def get_last_tag() -> str | None:
|
|
||||||
result = subprocess.run(
|
|
||||||
["git", "describe", "--tags", "--abbrev=0"],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
cwd=get_project_root(),
|
|
||||||
)
|
|
||||||
return result.stdout.strip() if result.returncode == 0 else None
|
|
||||||
|
|
||||||
|
|
||||||
def get_ignored_files() -> set[str]:
|
|
||||||
gitignore_path = get_project_root() / ".gitignore"
|
|
||||||
ignored = set()
|
|
||||||
if gitignore_path.exists():
|
|
||||||
for line in gitignore_path.read_text().splitlines():
|
|
||||||
line = line.strip()
|
|
||||||
if line and not line.startswith("#"):
|
|
||||||
ignored.add(line.rstrip("/"))
|
|
||||||
return ignored
|
|
||||||
|
|
||||||
|
|
||||||
def commit_has_tracked_changes(commit_hash: str) -> bool:
|
|
||||||
result = subprocess.run(
|
|
||||||
["git", "diff-tree", "--no-commit-id", "--name-only", "-r", commit_hash],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
cwd=get_project_root(),
|
|
||||||
)
|
|
||||||
if not result.stdout.strip():
|
|
||||||
return False
|
|
||||||
|
|
||||||
ignored = get_ignored_files()
|
|
||||||
for file_path in result.stdout.strip().split("\n"):
|
|
||||||
if not file_path:
|
|
||||||
continue
|
|
||||||
parts = file_path.split("/")
|
|
||||||
is_ignored = False
|
|
||||||
for i in range(len(parts)):
|
|
||||||
path_part = "/".join(parts[: i + 1])
|
|
||||||
for pattern in ignored:
|
|
||||||
if pattern.endswith("*"):
|
|
||||||
if path_part.startswith(pattern[:-1]):
|
|
||||||
is_ignored = True
|
|
||||||
break
|
|
||||||
elif path_part == pattern or parts[-1] == pattern:
|
|
||||||
is_ignored = True
|
|
||||||
break
|
|
||||||
if is_ignored:
|
|
||||||
break
|
|
||||||
if not is_ignored:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def commit_has_skip_ci_message(commit_hash: str) -> bool:
|
|
||||||
result = subprocess.run(
|
|
||||||
["git", "log", "-1", "--format=%s", commit_hash],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
cwd=get_project_root(),
|
|
||||||
)
|
|
||||||
msg = result.stdout.strip().lower()
|
|
||||||
return "[skip ci]" in msg or "[skip-ci]" in msg or "[ci skip]" in msg
|
|
||||||
|
|
||||||
|
|
||||||
def commit_only_changes_readme(commit_hash: str) -> bool:
|
|
||||||
result = subprocess.run(
|
|
||||||
["git", "diff-tree", "--no-commit-id", "--name-only", "-r", commit_hash],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
cwd=get_project_root(),
|
|
||||||
)
|
|
||||||
files = [f.strip() for f in result.stdout.strip().split("\n") if f.strip()]
|
|
||||||
return files == ["README.md"]
|
|
||||||
|
|
||||||
|
|
||||||
def get_commits_since_tag(tag: str | None) -> list[dict[str, str]]:
|
|
||||||
if tag:
|
|
||||||
result = subprocess.run(
|
|
||||||
["git", "log", "--format=%H|%s|%ad|%an", "--date=short", f"{tag}..HEAD"],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
cwd=get_project_root(),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
result = subprocess.run(
|
|
||||||
["git", "log", "--format=%H|%s|%ad|%an", "--date=short", "-n10"],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
cwd=get_project_root(),
|
|
||||||
)
|
|
||||||
|
|
||||||
commits = []
|
|
||||||
for line in result.stdout.strip().split("\n"):
|
|
||||||
if line:
|
|
||||||
parts = line.split("|")
|
|
||||||
if len(parts) >= 4:
|
|
||||||
commit_hash = parts[0]
|
|
||||||
if commit_has_skip_ci_message(commit_hash):
|
|
||||||
continue
|
|
||||||
if commit_only_changes_readme(commit_hash):
|
|
||||||
continue
|
|
||||||
if not commit_has_tracked_changes(commit_hash):
|
|
||||||
continue
|
|
||||||
commits.append(
|
|
||||||
{
|
|
||||||
"hash": commit_hash[:7],
|
|
||||||
"message": parts[1],
|
|
||||||
"date": parts[2],
|
|
||||||
"author": parts[3],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return commits
|
|
||||||
|
|
||||||
|
|
||||||
def categorize_commits(commits: list[dict[str, str]]) -> dict[str, list[str]]:
|
|
||||||
categories: dict[str, list[str]] = {
|
|
||||||
"Added": [],
|
|
||||||
"Changed": [],
|
|
||||||
"Fixed": [],
|
|
||||||
"Removed": [],
|
|
||||||
"Other": [],
|
|
||||||
}
|
|
||||||
|
|
||||||
for commit in commits:
|
|
||||||
msg = commit["message"].lower()
|
|
||||||
entry = f"- {commit['message']} ({commit['hash']})"
|
|
||||||
|
|
||||||
if msg.startswith("feat") or "add" in msg:
|
|
||||||
categories["Added"].append(entry)
|
|
||||||
elif msg.startswith("fix") or "fix" in msg:
|
|
||||||
categories["Fixed"].append(entry)
|
|
||||||
elif msg.startswith("change") or "update" in msg:
|
|
||||||
categories["Changed"].append(entry)
|
|
||||||
elif msg.startswith("remove") or "delete" in msg:
|
|
||||||
categories["Removed"].append(entry)
|
|
||||||
else:
|
|
||||||
categories["Other"].append(entry)
|
|
||||||
|
|
||||||
return categories
|
|
||||||
|
|
||||||
|
|
||||||
def format_changelog(commits: list[dict[str, str]], version: str = "v0.1.0") -> str:
|
|
||||||
categorized = categorize_commits(commits)
|
|
||||||
today = datetime.now().strftime("%Y-%m-%d")
|
|
||||||
|
|
||||||
lines = [f"### [{version}] - {today}"]
|
|
||||||
|
|
||||||
for section, entries in categorized.items():
|
|
||||||
if entries:
|
|
||||||
lines.append(f"\n#### {section}")
|
|
||||||
lines.extend(entries)
|
|
||||||
|
|
||||||
return "\n".join(lines)
|
|
||||||
|
|
||||||
|
|
||||||
def get_dependencies(pyproject: dict[str, Any]) -> dict[str, list[str]]:
|
|
||||||
deps: dict[str, list[str]] = {
|
|
||||||
"runtime": [],
|
|
||||||
"tests": [],
|
|
||||||
"lints": [],
|
|
||||||
"types": [],
|
|
||||||
"docs": [],
|
|
||||||
}
|
|
||||||
|
|
||||||
for dep in pyproject.get("project", {}).get("dependencies", []):
|
|
||||||
deps["runtime"].append(dep)
|
|
||||||
|
|
||||||
dep_groups = pyproject.get("dependency-groups", {})
|
|
||||||
|
|
||||||
if "tests" in dep_groups:
|
|
||||||
for dep in dep_groups["tests"]:
|
|
||||||
if isinstance(dep, str):
|
|
||||||
deps["tests"].append(dep)
|
|
||||||
|
|
||||||
if "lints" in dep_groups:
|
|
||||||
for dep in dep_groups["lints"]:
|
|
||||||
if isinstance(dep, str):
|
|
||||||
deps["lints"].append(dep)
|
|
||||||
|
|
||||||
if "types" in dep_groups:
|
|
||||||
for dep in dep_groups["types"]:
|
|
||||||
if isinstance(dep, str):
|
|
||||||
deps["types"].append(dep)
|
|
||||||
|
|
||||||
if "docs" in dep_groups:
|
|
||||||
for dep in dep_groups["docs"]:
|
|
||||||
if isinstance(dep, str):
|
|
||||||
deps["docs"].append(dep)
|
|
||||||
|
|
||||||
return deps
|
|
||||||
|
|
||||||
|
|
||||||
def get_available_commands() -> list[dict[str, str]]:
|
|
||||||
commands = [
|
|
||||||
{"cmd": "uv sync", "desc": "Install dependencies"},
|
|
||||||
{"cmd": "uv run python -m app.main", "desc": "Start development server"},
|
|
||||||
{
|
|
||||||
"cmd": "uv run pytest --cov=app --cov-fail-under=70",
|
|
||||||
"desc": "Run tests with coverage",
|
|
||||||
},
|
|
||||||
{"cmd": "uv run ruff check . --fix", "desc": "Run linters"},
|
|
||||||
{"cmd": "uv run ruff format .", "desc": "Format code"},
|
|
||||||
{
|
|
||||||
"cmd": "uv run isort . --profile black --filter-files",
|
|
||||||
"desc": "Sort imports",
|
|
||||||
},
|
|
||||||
{"cmd": "uv run mypy .", "desc": "Type checking"},
|
|
||||||
{"cmd": "uv run mkdocs build", "desc": "Build documentation"},
|
|
||||||
{"cmd": "uv run mkdocs serve", "desc": "Serve documentation locally"},
|
|
||||||
]
|
|
||||||
return commands
|
|
||||||
|
|
||||||
|
|
||||||
def update_dependencies_section(content: str, deps: dict[str, list[str]]) -> str:
|
|
||||||
section_pattern = r"(## Dependencies\n.*?)(\n## |\Z)"
|
|
||||||
|
|
||||||
deps_text = "## Dependencies\n\n"
|
|
||||||
|
|
||||||
if deps["runtime"]:
|
|
||||||
deps_text += "### Runtime\n"
|
|
||||||
for dep in sorted(deps["runtime"]):
|
|
||||||
deps_text += f"- {dep}\n"
|
|
||||||
deps_text += "\n"
|
|
||||||
|
|
||||||
if deps["tests"]:
|
|
||||||
deps_text += "### Development\n"
|
|
||||||
deps_text += "- **Tests**: " + ", ".join(sorted(deps["tests"])) + "\n"
|
|
||||||
if deps["lints"]:
|
|
||||||
deps_text += "- **Lint**: " + ", ".join(sorted(deps["lints"])) + "\n"
|
|
||||||
if deps["types"]:
|
|
||||||
deps_text += "- **Types**: " + ", ".join(sorted(deps["types"])) + "\n"
|
|
||||||
if deps["docs"]:
|
|
||||||
deps_text += "- **Docs**: " + ", ".join(sorted(deps["docs"])) + "\n"
|
|
||||||
|
|
||||||
deps_text += "\n"
|
|
||||||
|
|
||||||
replacement = f"{deps_text}\\2"
|
|
||||||
return re.sub(section_pattern, replacement, content, flags=re.DOTALL)
|
|
||||||
|
|
||||||
|
|
||||||
def update_commands_section(content: str, commands: list[dict[str, str]]) -> str:
|
|
||||||
section_pattern = r"(## Available Commands\n.*?\|.*?\n\|---\|.*?\n)(.*?)(\n## |\Z)"
|
|
||||||
|
|
||||||
commands_table = "| Command | Description |\n|---------|-------------|\n"
|
|
||||||
for cmd in commands:
|
|
||||||
commands_table += f"| `{cmd['cmd']}` | {cmd['desc']} |\n"
|
|
||||||
|
|
||||||
commands_table += "\n"
|
|
||||||
|
|
||||||
replacement = f"\\1{commands_table}\\3"
|
|
||||||
return re.sub(section_pattern, replacement, content, flags=re.DOTALL)
|
|
||||||
|
|
||||||
|
|
||||||
def update_changelog_section(content: str, changelog: str) -> str:
|
|
||||||
section_pattern = r"(## Changelog\n)(.*?)(\Z)"
|
|
||||||
|
|
||||||
replacement = f"\\1\n{changelog}\n\\3"
|
|
||||||
return re.sub(section_pattern, replacement, content, flags=re.DOTALL)
|
|
||||||
|
|
||||||
|
|
||||||
def update_readme(check_only: bool = False) -> bool:
|
|
||||||
readme_path = get_project_root() / "README.md"
|
|
||||||
|
|
||||||
if not readme_path.exists():
|
|
||||||
print("README.md not found")
|
|
||||||
return False
|
|
||||||
|
|
||||||
content = readme_path.read_text()
|
|
||||||
original_content = content
|
|
||||||
|
|
||||||
pyproject = get_pyproject()
|
|
||||||
commits = get_commits_since_tag(get_last_tag())
|
|
||||||
deps = get_dependencies(pyproject)
|
|
||||||
commands = get_available_commands()
|
|
||||||
|
|
||||||
version = get_last_tag() or "v0.1.0"
|
|
||||||
changelog = format_changelog(commits, version)
|
|
||||||
|
|
||||||
content = update_changelog_section(content, changelog)
|
|
||||||
content = update_dependencies_section(content, deps)
|
|
||||||
content = update_commands_section(content, commands)
|
|
||||||
|
|
||||||
if check_only:
|
|
||||||
needs_update = content != original_content
|
|
||||||
if needs_update:
|
|
||||||
print("README.md needs update")
|
|
||||||
else:
|
|
||||||
print("README.md is up to date")
|
|
||||||
return needs_update
|
|
||||||
|
|
||||||
if content != original_content:
|
|
||||||
readme_path.write_text(content)
|
|
||||||
print("README.md updated successfully")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
print("No changes needed")
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
import sys
|
|
||||||
|
|
||||||
check_only = "--check" in sys.argv
|
|
||||||
|
|
||||||
updated = update_readme(check_only=check_only)
|
|
||||||
|
|
||||||
if check_only and updated:
|
|
||||||
sys.exit(1)
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
Reference in New Issue
Block a user