Initial commit: Hermes skill fastapi-microservice

This commit is contained in:
estorozhenko
2026-09-06 13:51:25 +00:00
commit fb163f07d4
2 changed files with 261 additions and 0 deletions
+90
View File
@@ -0,0 +1,90 @@
# 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
```python
# 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:
```json
{"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](url), #h1-######, > quotes, ```code blocks```, ---.
### API key auth dual-mode
- **Header mode**: `Authorization: Bearer <key>` — for GET/DELETE (accounts list, delete)
- **Body mode**: `api_key` field 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
```bash
# 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