Дополнение к STYLE-GUIDE.md
Версия: 1.0
Дата: 2025-11-08
Streamlit-специфика:
- Каждая страница = отдельный Python процесс
- CSS не сохраняется между страницами
- load_css() должен вызываться на каждой странице
Альтернатива (не рекомендуется):
- Можно через .streamlit/config.toml но там ограниченные возможности
- Наш подход: переиспользуемая функция load_css()
Типы коммитов:
feat: Новая функциональность
fix: Исправление бага
docs: Документация
style: Форматирование, отступы (не CSS!)
refactor: Рефакторинг без изменения функциональности
test: Добавление тестов
chore: Обновление зависимостей, конфигов
Формат сообщения:
<type>(<scope>): <subject>
<body>
<footer>
Примеры:
feat(channels): add channel management page
- Add channel list with sorting
- Add channel detail page with actions
- Implement API test button
Closes #123
fix(auth): correct password hashing
Password was stored in plaintext due to missing bcrypt call.
Now properly hashed with bcrypt.
refactor(table): extract compact table component
Moved table logic to components/table.py for reusability.
Main branches:
- main - production
- develop - development
Feature branches:
feature/channel-management
feature/order-import
bugfix/auth-redirect
hotfix/critical-security
Правила:
- Создавать feature branch от develop
- Merge request требует review
- Проходить все тесты
- Удалять branch после merge
Обязательно игнорировать:
# Секреты
config.yaml
.env
*.key
*.pem
# Базы данных
*.db
*.sqlite
*.sqlite3
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
venv/
ENV/
# Streamlit
.streamlit/secrets.toml
# IDE
.vscode/
.idea/
*.swp
*.swo
# Logs
logs/
*.log
# OS
.DS_Store
Thumbs.db
Функциональность:
- [ ] Код делает то, что заявлено
- [ ] Нет очевидных багов
- [ ] Обработаны edge cases
- [ ] Добавлены тесты
Качество:
- [ ] Читаемый код
- [ ] Нет дублирования
- [ ] Следует стиль-гайду
- [ ] Адекватные названия переменных
Безопасность:
- [ ] Нет SQL injection
- [ ] Нет XSS уязвимостей
- [ ] Валидация пользовательского ввода
- [ ] Секреты не в коде
Performance:
- [ ] Нет N+1 запросов
- [ ] Использованы индексы БД
- [ ] Оптимальные алгоритмы
- [ ] Кеширование где нужно
Типы комментариев:
💡 Suggestion: можно улучшить
❓ Question: не понятно
⚠️ Warning: потенциальная проблема
🐛 Bug: точно баг
✨ Praise: хорошо сделано
Примеры:
# ❓ Question: почему здесь используется time.sleep()
# вместо asyncio.sleep()?
# 💡 Suggestion: можно использовать list comprehension
result = []
for item in items:
if item.active:
result.append(item.name)
# Лучше так:
result = [item.name for item in items if item.active]
# ⚠️ Warning: этот код выполняется для КАЖДОЙ строки
# в таблице. При 10000 строк это 10000 запросов к БД!
for order in orders:
customer = db.query(Customer).get(order.customer_id) # N+1!
Alembic - обязательно:
# Создать миграцию
alembic revision --autogenerate -m "add channel table"
# Применить
alembic upgrade head
# Откатить
alembic downgrade -1
Правила:
- Всегда через миграции, НИКОГДА напрямую в БД
- Review миграций перед применением
- Тестировать миграцию и откат
- Миграции должны быть idempotent
Плохо (N+1):
channels = session.query(Channel).all()
for channel in channels:
entity = session.query(LegalEntity).get(channel.entity_id) # N+1!
print(f"{channel.name} - {entity.name}")
Хорошо (JOIN):
channels = session.query(Channel).join(LegalEntity).all()
for channel in channels:
print(f"{channel.name} - {channel.entity.name}")
Использовать:
- joinedload() для eager loading
- filter() вместо filter_by() для сложных условий
- func.count() для подсчета
- Индексы на часто используемых полонах
Паттерн:
session = get_session()
try:
# Несколько операций
channel = Channel(...)
session.add(channel)
warehouse = Warehouse(channel_id=channel.id, ...)
session.add(warehouse)
# Все или ничего
session.commit()
except Exception:
session.rollback()
raise
finally:
session.close()
Обязательно для внешних API:
from tenacity import retry, stop_after_attempt, wait_exponential
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=4, max=10)
)
def call_ozon_api():
response = requests.post(...)
response.raise_for_status()
return response.json()
Уважать лимиты API:
import time
from collections import deque
class RateLimiter:
def __init__(self, max_calls, period):
self.max_calls = max_calls
self.period = period
self.calls = deque()
def __call__(self, func):
def wrapper(*args, **kwargs):
now = time.time()
# Удаляем старые вызовы
while self.calls and self.calls[0] < now - self.period:
self.calls.popleft()
# Ждем если превышен лимит
if len(self.calls) >= self.max_calls:
sleep_time = self.period - (now - self.calls[0])
time.sleep(sleep_time)
self.calls.append(time.time())
return func(*args, **kwargs)
return wrapper
# Использование: max 100 запросов в минуту
@RateLimiter(max_calls=100, period=60)
def call_api():
...
Всегда указывать timeout:
# ❌ Плохо - может висеть вечно
response = requests.get(url)
# ✅ Хорошо
response = requests.get(url, timeout=10)
# ✅ Еще лучше - разные timeout для connect и read
response = requests.get(url, timeout=(3, 10)) # connect=3s, read=10s
1. Код (defaults):
# core/config.py
APP_NAME = "Marketplace MVP"
PAGE_SIZE = 50
CACHE_TTL = 3600
2. Environment файл (.env):
DATABASE_URL=postgresql://user:pass@localhost/db
API_TIMEOUT=30
DEBUG=true
3. Environment variables (production):
export DATABASE_URL="postgresql://..."
export APP_MODE="production"
Приоритет: ENV vars > .env file > defaults
НИКОГДА в коде:
# ❌ ОЧЕНЬ ПЛОХО
API_KEY = "sk_live_abc123xyz"
DB_PASSWORD = "mypassword123"
Правильно:
# ✅ Хорошо
import os
API_KEY = os.getenv("OZON_API_KEY")
if not API_KEY:
raise ValueError("OZON_API_KEY not set")
Для Streamlit:
# .streamlit/secrets.toml (НЕ в git!)
[ozon]
api_key = "sk_live_abc123xyz"
client_id = "123456"
# В коде:
import streamlit as st
api_key = st.secrets["ozon"]["api_key"]
Плохо:
Error: NoneType object has no attribute 'id'
Exception in line 147
Database connection failed
Хорошо:
❌ Не удалось загрузить каналы
Попробуйте обновить страницу или обратитесь к администратору.
⚠️ Подключение к Ozon API не удалось
Проверьте правильность Client ID и API ключа в настройках.
✅ Канал успешно создан
Теперь вы можете импортировать заказы из Ozon.
Уровни детализации:
# Для пользователя
st.error("❌ Не удалось создать канал")
# В логи - детали
logging.error(
f"Failed to create channel: {e}",
extra={
"user_id": user.id,
"client_id": client_id,
"error_type": type(e).__name__
}
)
Использовать для валидации:
from pydantic import BaseModel, validator, Field
class ChannelCreate(BaseModel):
client_id: str = Field(..., min_length=6, max_length=50)
api_key: str = Field(..., min_length=20)
channel_name: str = Field(..., min_length=1, max_length=200)
@validator('client_id')
def client_id_alphanumeric(cls, v):
if not v.isalnum():
raise ValueError('must be alphanumeric')
return v
@validator('api_key')
def api_key_not_test(cls, v):
if v.startswith('test_'):
raise ValueError('test API keys not allowed in production')
return v
# Использование
try:
channel_data = ChannelCreate(
client_id=input_client_id,
api_key=input_api_key,
channel_name=input_name
)
except ValidationError as e:
st.error(f"❌ Ошибка валидации: {e}")
Когда нужно:
- Импорт большого количества заказов
- Отправка email уведомлений
- Генерация отчетов
- Синхронизация с внешними API
Структура:
tasks/
├── __init__.py
├── celery_app.py
├── orders.py # Задачи с заказами
├── sync.py # Синхронизация
└── reports.py # Генерация отчетов
Пример:
# tasks/orders.py
from celery_app import celery
@celery.task
def import_orders_from_ozon(channel_id):
"""Импорт заказов из Ozon (может занять долго)"""
channel = db.query(Channel).get(channel_id)
api = OzonAPI(channel.client_id, channel.api_key)
# Импорт 10000 заказов - долго
orders = api.get_all_orders()
for order_data in orders:
order = Order.from_ozon_data(order_data)
db.add(order)
db.commit()
return len(orders)
# В Streamlit
if st.button("Импортировать заказы"):
task = import_orders_from_ozon.delay(channel.id)
st.info(f"⏳ Импорт запущен (task_id: {task.id})")
Endpoint:
@app.get("/health")
def health_check():
checks = {
"database": check_database(),
"ozon_api": check_ozon_api(),
"redis": check_redis()
}
all_ok = all(checks.values())
status_code = 200 if all_ok else 503
return JSONResponse(
status_code=status_code,
content={
"status": "healthy" if all_ok else "unhealthy",
"checks": checks,
"version": VERSION,
"timestamp": datetime.now().isoformat()
}
)
Что собирать:
from prometheus_client import Counter, Histogram, Gauge
# Счетчики
orders_imported = Counter('orders_imported_total', 'Total orders imported')
api_errors = Counter('api_errors_total', 'API errors', ['api_name', 'error_type'])
# Гистограммы (latency)
api_latency = Histogram('api_request_duration_seconds', 'API request latency')
# Gauge (текущее значение)
active_channels = Gauge('active_channels', 'Number of active channels')
# Использование
with api_latency.time():
response = call_ozon_api()
orders_imported.inc()
api_errors.labels(api_name='ozon', error_type='timeout').inc()
# Название проекта
Краткое описание (1-2 предложения)
## Требования
- Python 3.10+
- PostgreSQL 13+
- Redis 6+
## Установка
\`\`\`bash
git clone ...
cd project
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
\`\`\`
## Конфигурация
Скопировать `.env.example` в `.env` и заполнить:
\`\`\`
DATABASE_URL=...
OZON_API_KEY=...
\`\`\`
## Запуск
\`\`\`bash
streamlit run app.py
\`\`\`
## Тестирование
\`\`\`bash
pytest
\`\`\`
## Deployment
См. DEPLOYMENT.md
## Contributing
См. CONTRIBUTING.md
Swagger/OpenAPI для API endpoints:
from fastapi import FastAPI
from fastapi.openapi.utils import get_openapi
app = FastAPI(
title="Marketplace API",
description="API for marketplace integration",
version="1.0.0"
)
@app.get("/channels/{channel_id}", tags=["channels"])
def get_channel(channel_id: int):
"""
Получить канал по ID
Returns:
Channel object
"""
...
Разделять:
requirements/
├── base.txt # Основные зависимости
├── dev.txt # Для разработки
├── test.txt # Для тестирования
└── prod.txt # Для production
base.txt:
streamlit==1.28.0
sqlalchemy==2.0.23
bcrypt==4.1.2
dev.txt:
-r base.txt
black==23.11.0
flake8==6.1.0
pytest==7.4.3
Фиксировать major.minor:
# ❌ Плохо - может сломаться
streamlit
# ⚠️ Осторожно - может быть несовместимо
streamlit>=1.28.0
# ✅ Хорошо
streamlit==1.28.0
# ✅ Тоже хорошо - minor updates ok
streamlit>=1.28.0,<1.29.0
config.py:
import os
ENV = os.getenv("APP_ENV", "development")
if ENV == "production":
DEBUG = False
DATABASE_POOL_SIZE = 20
CACHE_TTL = 3600
LOG_LEVEL = "WARNING"
elif ENV == "staging":
DEBUG = True
DATABASE_POOL_SIZE = 10
CACHE_TTL = 600
LOG_LEVEL = "INFO"
else: # development
DEBUG = True
DATABASE_POOL_SIZE = 5
CACHE_TTL = 60
LOG_LEVEL = "DEBUG"
Создать Makefile для частых операций:
.PHONY: install run test lint format clean
install:
pip install -r requirements.txt
run:
streamlit run app.py
test:
pytest tests/
lint:
flake8 .
black --check .
format:
black .
isort .
clean:
find . -type d -name __pycache__ -exec rm -rf {} +
find . -type f -name '*.pyc' -delete
migrate:
alembic upgrade head
migrate-create:
alembic revision --autogenerate -m "$(message)"
Использование:
make install
make run
make test
Уже есть в STYLE-GUIDE.md:
✅ UI/UX дизайн
✅ Naming conventions
✅ Error handling
✅ Performance
✅ Security
Добавлено в этом документе:
✅ Git workflow
✅ Code review
✅ Database (миграции, запросы)
✅ API integration (retry, rate limit, timeout)
✅ Configuration management
✅ Error messages
✅ Data validation
✅ Background jobs
✅ Monitoring & alerting
✅ Documentation
✅ Dependency management
✅ Environment-specific config
✅ Makefile commands
Применяйте эти стандарты во ВСЕХ новых фичах!