architect/_archive/2025-11-09-marketplace-old/marketplace-mvp/DEVELOPMENT-STANDARDS.md

DEVELOPMENT STANDARDS - Дополнительные аспекты стандартизации

Дополнение к STYLE-GUIDE.md
Версия: 1.0
Дата: 2025-11-08


ВАЖНО: Почему CSS на каждой странице?

Streamlit-специфика:
- Каждая страница = отдельный Python процесс
- CSS не сохраняется между страницами
- load_css() должен вызываться на каждой странице

Альтернатива (не рекомендуется):
- Можно через .streamlit/config.toml но там ограниченные возможности
- Наш подход: переиспользуемая функция load_css()


1. GIT WORKFLOW

1.1 Структура коммитов

Типы коммитов:

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.

1.2 Branching Strategy

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

1.3 .gitignore

Обязательно игнорировать:

# Секреты
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

2. CODE REVIEW

2.1 Checklist для reviewer

Функциональность:
- [ ] Код делает то, что заявлено
- [ ] Нет очевидных багов
- [ ] Обработаны edge cases
- [ ] Добавлены тесты

Качество:
- [ ] Читаемый код
- [ ] Нет дублирования
- [ ] Следует стиль-гайду
- [ ] Адекватные названия переменных

Безопасность:
- [ ] Нет SQL injection
- [ ] Нет XSS уязвимостей
- [ ] Валидация пользовательского ввода
- [ ] Секреты не в коде

Performance:
- [ ] Нет N+1 запросов
- [ ] Использованы индексы БД
- [ ] Оптимальные алгоритмы
- [ ] Кеширование где нужно

2.2 Как оставлять комментарии

Типы комментариев:

💡 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!

3. DATABASE

3.1 Миграции

Alembic - обязательно:

# Создать миграцию
alembic revision --autogenerate -m "add channel table"

# Применить
alembic upgrade head

# Откатить
alembic downgrade -1

Правила:
- Всегда через миграции, НИКОГДА напрямую в БД
- Review миграций перед применением
- Тестировать миграцию и откат
- Миграции должны быть idempotent

3.2 Запросы

Плохо (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() для подсчета
- Индексы на часто используемых полонах

3.3 Транзакции

Паттерн:

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()

4. API INTEGRATION

4.1 Retry Logic

Обязательно для внешних 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()

4.2 Rate Limiting

Уважать лимиты 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():
    ...

4.3 Timeout

Всегда указывать 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

5. CONFIGURATION MANAGEMENT

5.1 Уровни конфигурации

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

5.2 Секреты

НИКОГДА в коде:

# ❌ ОЧЕНЬ ПЛОХО
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"]

6. ERROR MESSAGES

6.1 Для пользователя

Плохо:

Error: NoneType object has no attribute 'id'
Exception in line 147
Database connection failed

Хорошо:

❌ Не удалось загрузить каналы
Попробуйте обновить страницу или обратитесь к администратору.

⚠️ Подключение к Ozon API не удалось
Проверьте правильность Client ID и API ключа в настройках.

✅ Канал успешно создан
Теперь вы можете импортировать заказы из Ozon.

6.2 Логирование

Уровни детализации:

# Для пользователя
st.error("❌ Не удалось создать канал")

# В логи - детали
logging.error(
    f"Failed to create channel: {e}",
    extra={
        "user_id": user.id,
        "client_id": client_id,
        "error_type": type(e).__name__
    }
)

7. DATA VALIDATION

7.1 Pydantic Models

Использовать для валидации:

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}")

8. BACKGROUND JOBS

8.1 Celery для async задач

Когда нужно:
- Импорт большого количества заказов
- Отправка 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})")

9. MONITORING & ALERTING

9.1 Health Checks

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()
        }
    )

9.2 Метрики (Prometheus)

Что собирать:

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()

10. DOCUMENTATION

10.1 README.md структура

# Название проекта

Краткое описание (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

10.2 API Documentation

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
    """
    ...

11. DEPENDENCY MANAGEMENT

11.1 requirements.txt

Разделять:

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

11.2 Версии

Фиксировать major.minor:

# ❌ Плохо - может сломаться
streamlit

# ⚠️ Осторожно - может быть несовместимо
streamlit>=1.28.0

# ✅ Хорошо
streamlit==1.28.0

# ✅ Тоже хорошо - minor updates ok
streamlit>=1.28.0,<1.29.0

12. ENVIRONMENT-SPECIFIC

12.1 Настройки по окружениям

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"

13. КОМАНДЫ (Makefile)

Создать 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

Применяйте эти стандарты во ВСЕХ новых фичах!