mirror of
https://gitverse.ru/kpa39l/fastapi-microservice.git
synced 2026-09-28 21:05:06 +00:00
3.7 KiB
3.7 KiB
md2vk — FastAPI microservice example
Project: VK wall publisher — converts Markdown to VK format_data, publishes via VK API. Pattern: FastAPI + async SQLAlchemy + SQLite + Fernet-encrypted tokens + external API client.
Architecture decisions
| Decision | Choice | Why |
|---|---|---|
| Database | SQLite + aiosqlite | Lightweight, no external deps, fits Docker |
| ORM | SQLAlchemy 2.0 async | Async-first, well-typed, portable |
| Token encryption | Fernet (cryptography) | AES-128-CBC + HMAC, simple API, key from file |
| Auth | API key (sha256 hash) | API key !== VK token, token never leaves encrypted storage |
| Markdown conversion | Custom parser | VK format_data is a niche JSON format, no lib exists |
| HTTP client | httpx | Async, modern, timeouts by default |
| VK API scope | wall only | Minimal permissions — can't read DMs, photos, etc. |
Layer structure
app/config.py → pydantic-settings, reads TOKEN_ENCRYPTION_KEY_FILE
app/security.py → Fernet encrypt/decrypt + sha256 API key hash
app/database.py → create_async_engine, async_sessionmaker, init_db()
app/models.py → User, VkAccount, Publication (SQLAlchemy 2.0 mapped)
app/vk_client.py → VkClient class: wall.post, users.get, check_token()
app/converters/ → markdown_to_vk(): returns list of chunks with FormatItem[]
app/api/schemas.py → 12 Pydantic models (Request/Response)
app/api/deps.py → get_current_user_from_header(), get_user_by_api_key()
app/api/v1.py → 6 endpoints: health, accounts CRUD, publish, convert, publications
app/main.py → FastAPI() + lifespan (init_db on startup)
Key implementation details
Fernet key loading
# app/config.py
@field_validator("fernet_key", mode="before")
@classmethod
def load_key(cls, v: Any, info: ValidationInfo) -> str:
key_file = info.data.get("token_encryption_key_file")
if key_file and Path(key_file).exists():
return Path(key_file).read_text().strip()
raise ValueError("...")
VK format_data converter
The converter parses Markdown AST into VK's format_data JSON which uses offset-based formatting:
{"version": 1, "items": [
{"type": "bold", "offset": 0, "length": 5},
{"type": "link", "offset": 12, "length": 4, "url": "https://vk.com"}
]}
Handles: bold, italic, inline_code, links, #h1-######, > quotes, code blocks, ---.
API key auth dual-mode
- Header mode:
Authorization: Bearer <key>— for GET/DELETE (accounts list, delete) - Body mode:
api_keyfield in POST body — for publish, create account, list publications - Both call
get_user_by_api_key()which does sha256 → query User table
Verification commands from session
# Generate key
python3 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" > secrets/token_encryption_key
# Verify imports
TOKEN_ENCRYPTION_KEY_FILE=secrets/token_encryption_key \
python3 -c "from app import config, database, models, security, vk_client; from app.converters import markdown_to_vk; from app.api import schemas, deps, v1; print('OK')"
# Start server
TOKEN_ENCRYPTION_KEY_FILE=secrets/token_encryption_key \
uvicorn app.main:app --host 0.0.0.0 --port 8000
# Test
curl http://localhost:8000/api/v1/health
curl -X POST http://localhost:8000/api/v1/convert \
-H "Content-Type: application/json" \
-d '{"message_md":"**bold** text"}'
Key counts (from session)
- 9 source files (excluding __init__.py, config, and infrastructure)
- ~422 lines in converter (largest module)
- ~310 lines in API v1 router (6 endpoints)
- 3 ORM models: User, VkAccount, Publication
- 4 infrastructure files: Dockerfile, docker-compose.yml, Makefile, .gitignore