Перейти к содержанию

Гайд по разработке Silent Meadow

Development Workflow

1. Создание новой фичи

# Обновить main ветку
git checkout main
git pull origin main

# Создать feature ветку
git checkout -b feature/property-map
# ИЛИ
git checkout -b fix/avito-auth-bug

# Работа над фичей...

# Коммит изменений
git add .
git commit -m "feat: add interactive property map with Yandex Maps"

# Push в remote
git push origin feature/property-map

# Создать Pull Request на GitHub

2. Code Review Process

  1. Создайте Pull Request на GitHub
  2. Опишите изменения в описании PR
  3. Добавьте скриншоты (если UI изменения)
  4. Дождитесь code review от коллег
  5. Внесите правки по комментариям
  6. После одобрения - merge в main

Code Style

Python (Backend)

Форматирование

Используем Black для автоматического форматирования:

# Форматировать все файлы
black app/

# Проверить без изменений
black --check app/

# Конфигурация в pyproject.toml:
[tool.black]
line-length = 100
target-version = ['py311']

Линтинг

Используем Ruff для быстрого линтинга:

# Запустить линтер
ruff check app/

# Автофикс
ruff check --fix app/

# Конфигурация в pyproject.toml:
[tool.ruff]
line-length = 100
select = ["E", "F", "I", "N", "W"]
ignore = ["E501"]

Type Hints

Type hints обязательны для всех функций:

# ✅ Правильно
async def get_property(property_id: UUID) -> Property:
    property = await db.get(Property, property_id)
    if not property:
        raise HTTPException(status_code=404, detail="Property not found")
    return property

# ❌ Неправильно
async def get_property(property_id):
    return await db.get(Property, property_id)

Docstrings

Используем Google style docstrings:

async def calculate_property_price(
    property: Property,
    discount: float = 0.0
) -> int:
    """
    Рассчитывает финальную цену участка с учетом скидки.

    Args:
        property: Объект участка
        discount: Процент скидки (0.0 - 1.0)

    Returns:
        Финальная цена в рублях

    Raises:
        ValueError: Если discount вне диапазона [0, 1]
    """
    if not 0 <= discount <= 1:
        raise ValueError("Discount must be between 0 and 1")

    base_price = property.price
    return int(base_price * (1 - discount))

Naming Conventions

  • Функции и переменные: snake_case
  • Классы: PascalCase
  • Константы: UPPER_SNAKE_CASE
  • Private методы/атрибуты: _leading_underscore
# ✅ Правильно
class PropertyService:
    MAX_PRICE = 10_000_000

    async def get_available_properties(self) -> list[Property]:
        return await self._query_database()

    async def _query_database(self) -> list[Property]:
        ...

TypeScript (Frontend)

Форматирование

Используем Prettier:

# Форматировать все файлы
npm run format

# Проверить
npm run format:check

# Конфигурация в .prettierrc:
{
  "semi": true,
  "singleQuote": false,
  "tabWidth": 2,
  "printWidth": 100
}

Линтинг

Используем ESLint:

# Запустить линтер
npm run lint

# Автофикс
npm run lint:fix

# Конфигурация в .eslintrc.js

TypeScript Strict Mode

Строгий режим TypeScript обязателен:

// tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true
  }
}

Naming Conventions

  • Компоненты: PascalCase (PropertyCard.tsx)
  • Функции и переменные: camelCase
  • Константы: UPPER_SNAKE_CASE
  • Типы и интерфейсы: PascalCase с префиксом I для интерфейсов (опционально)
// ✅ Правильно
interface Property {
  id: string;
  number: string;
  price: number;
}

const PropertyCard: React.FC<{ property: Property }> = ({ property }) => {
  const [isHovered, setIsHovered] = useState(false);

  const formatPrice = (price: number): string => {
    return new Intl.NumberFormat("ru-RU").format(price);
  };

  return <div>{formatPrice(property.price)}</div>;
};

Структура проекта

Backend Structure

backend/
├── app/
│   ├── __init__.py
│   ├── main.py                 # FastAPI app entry point
│   ├── config.py               # Конфигурация (env variables)
│   │
│   ├── models/                 # SQLAlchemy модели
│   │   ├── __init__.py
│   │   ├── base.py             # Base model
│   │   ├── property.py         # Property модель
│   │   ├── house.py            # House модель
│   │   └── avito_ad.py         # AvitoAd модель
│   │
│   ├── schemas/                # Pydantic схемы для валидации
│   │   ├── __init__.py
│   │   ├── property.py         # PropertyCreate, PropertyResponse
│   │   ├── house.py
│   │   └── avito.py
│   │
│   ├── api/                    # API роутеры
│   │   ├── __init__.py
│   │   ├── v1/
│   │   │   ├── __init__.py
│   │   │   ├── properties.py   # /v1/properties endpoints
│   │   │   ├── houses.py       # /v1/houses endpoints
│   │   │   ├── avito.py        # /v1/avito endpoints
│   │   │   └── yandex_maps.py  # /v1/map endpoints
│   │
│   ├── services/               # Бизнес-логика
│   │   ├── __init__.py
│   │   ├── property_service.py
│   │   ├── avito_service.py    # Avito API integration
│   │   ├── telegram_service.py # Telegram парсинг
│   │   └── telegram_crm_bot.py # CRM бот
│   │
│   ├── core/                   # Ядро приложения
│   │   ├── __init__.py
│   │   ├── database.py         # Database session
│   │   ├── security.py         # JWT auth
│   │   └── exceptions.py       # Custom exceptions
│   │
│   ├── scripts/                # CLI скрипты
│   │   ├── import_properties.py
│   │   ├── create_admin.py
│   │   └── telegram_login.py
│   │
│   └── tests/                  # Тесты
│       ├── __init__.py
│       ├── conftest.py         # Pytest fixtures
│       ├── test_properties.py
│       └── test_avito.py
├── alembic/                    # Database migrations
│   ├── versions/
│   └── env.py
├── requirements.txt
├── requirements-dev.txt
└── pytest.ini

Frontend Structure

frontend/
├── src/
│   ├── main.tsx                # Entry point
│   ├── App.tsx                 # Root component
│   │
│   ├── pages/                  # Страницы (роуты)
│   │   ├── PropertiesPage.tsx  # /properties
│   │   ├── HousesPage.tsx      # /houses
│   │   ├── AvitoAdsPage.tsx    # /avito
│   │   └── LeadsPage.tsx       # /leads
│   │
│   ├── components/             # Переиспользуемые компоненты
│   │   ├── ui/                 # UI примитивы
│   │   │   ├── Button.tsx
│   │   │   ├── Card.tsx
│   │   │   └── Input.tsx
│   │   ├── PropertyCard.tsx
│   │   ├── HouseCard.tsx
│   │   └── YandexMapView.tsx
│   │
│   ├── lib/                    # Утилиты и API клиенты
│   │   ├── api.ts              # Axios instance
│   │   ├── properties-api.ts   # Properties API client
│   │   ├── avito-api.ts        # Avito API client
│   │   └── utils.ts            # Хелперы
│   │
│   ├── hooks/                  # Custom React hooks
│   │   ├── useProperties.ts    # React Query для properties
│   │   ├── useHouses.ts
│   │   └── useAuth.ts
│   │
│   ├── store/                  # Zustand store (если нужно)
│   │   └── authStore.ts
│   │
│   └── types/                  # TypeScript типы
│       ├── property.ts
│       ├── house.ts
│       └── api.ts
├── public/
├── index.html
├── vite.config.ts
├── tailwind.config.js
└── tsconfig.json

Работа с базой данных

Создание модели

# app/models/property.py
from sqlalchemy import Column, String, Integer, Float, DateTime, JSON
from sqlalchemy.dialects.postgresql import UUID
from app.models.base import Base
import uuid
from datetime import datetime

class Property(Base):
    __tablename__ = "properties"

    id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    number = Column(String(50), nullable=False)
    cadastral_number = Column(String(100), unique=True, index=True)
    price = Column(Integer, nullable=False)
    status = Column(String(20), default="available", index=True)
    latitude = Column(Float)
    longitude = Column(Float)
    created_at = Column(DateTime, default=datetime.utcnow)
    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)

Создание миграции

# Автогенерация миграции
alembic revision --autogenerate -m "Add properties table"

# Проверить сгенерированный файл
cat alembic/versions/xxx_add_properties_table.py

# Применить миграцию
alembic upgrade head

# Откатить миграцию
alembic downgrade -1

CRUD операции

# app/services/property_service.py
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.models.property import Property

class PropertyService:
    def __init__(self, db: AsyncSession):
        self.db = db

    async def get_all(self, status: str | None = None) -> list[Property]:
        query = select(Property)
        if status:
            query = query.where(Property.status == status)
        result = await self.db.execute(query)
        return result.scalars().all()

    async def create(self, data: dict) -> Property:
        property = Property(**data)
        self.db.add(property)
        await self.db.commit()
        await self.db.refresh(property)
        return property

Тестирование

Backend Tests (pytest)

# tests/test_properties.py
import pytest
from httpx import AsyncClient
from app.main import app

@pytest.mark.asyncio
async def test_create_property():
    async with AsyncClient(app=app, base_url="http://test") as client:
        response = await client.post(
            "/v1/properties",
            json={
                "number": "502",
                "cadastral_number": "50:18:0090613:670",
                "price": 299000,
                "area_sotok": 10,
                "settlement": "Тихие Луга"
            }
        )
        assert response.status_code == 201
        data = response.json()
        assert data["number"] == "502"
        assert data["price"] == 299000

@pytest.mark.asyncio
async def test_get_properties_filter_by_status():
    async with AsyncClient(app=app, base_url="http://test") as client:
        response = await client.get("/v1/properties?status=available")
        assert response.status_code == 200
        data = response.json()
        assert isinstance(data["items"], list)

Запуск тестов

# Все тесты
pytest

# С покрытием
pytest --cov=app tests/

# Конкретный файл
pytest tests/test_properties.py

# Конкретный тест
pytest tests/test_properties.py::test_create_property

# С verbose output
pytest -v

# Остановить на первой ошибке
pytest -x

Frontend Tests (Vitest)

// src/components/PropertyCard.test.tsx
import { render, screen } from "@testing-library/react";
import { PropertyCard } from "./PropertyCard";

describe("PropertyCard", () => {
  it("renders property information", () => {
    const property = {
      id: "1",
      number: "502",
      price: 299000,
      status: "available",
    };

    render(<PropertyCard property={property} />);

    expect(screen.getByText("502")).toBeInTheDocument();
    expect(screen.getByText("299 000 ₽")).toBeInTheDocument();
  });
});

Запуск тестов

# Все тесты
npm test

# Watch mode
npm test -- --watch

# С покрытием
npm test -- --coverage

# E2E тесты (Playwright)
npm run test:e2e

Git Conventions

Commit Messages

Используем Conventional Commits:

<type>(<scope>): <subject>

<body>

<footer>

Types: - feat: новая функциональность - fix: исправление бага - docs: изменения в документации - style: форматирование кода (не влияет на логику) - refactor: рефакторинг кода - test: добавление тестов - chore: обновление зависимостей, конфигурации

Примеры:

git commit -m "feat(properties): add filtering by price range"
git commit -m "fix(avito): handle OAuth token expiration"
git commit -m "docs(api): update properties endpoint documentation"
git commit -m "refactor(services): extract Avito API client to separate service"

Branch Naming

feature/property-map-integration
fix/avito-authentication-bug
refactor/telegram-service
docs/setup-instructions

Debugging

Backend Debugging (VS Code)

Создайте .vscode/launch.json:

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Python: FastAPI",
      "type": "python",
      "request": "launch",
      "module": "uvicorn",
      "args": ["app.main:app", "--reload", "--port", "8000"],
      "jinja": true,
      "justMyCode": false
    }
  ]
}

Frontend Debugging

// Используйте React DevTools
console.log("Property data:", property);

// Или debugger
debugger;

// Browser console:
// window.__REDUX_DEVTOOLS_EXTENSION__ для Redux

Environment Variables

Development

# .env.development
DATABASE_URL=postgresql+asyncpg://localhost/silent_meadow_dev
DEBUG=True
LOG_LEVEL=DEBUG

Production

# .env.production
DATABASE_URL=postgresql+asyncpg://prod-db/silent_meadow
DEBUG=False
LOG_LEVEL=WARNING
SENTRY_DSN=https://...

Загрузка env

# app/config.py
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    database_url: str
    debug: bool = False
    log_level: str = "INFO"

    class Config:
        env_file = ".env"

settings = Settings()

Деплой

Backend (Render.com)

  1. Создать render.yaml:
services:
  - type: web
    name: silent-meadow-api
    env: python
    buildCommand: pip install -r requirements.txt
    startCommand: uvicorn app.main:app --host 0.0.0.0 --port $PORT
    envVars:
      - key: DATABASE_URL
        fromDatabase:
          name: silent-meadow-db
          property: connectionString
  1. Push в GitHub
  2. Подключить репозиторий в Render
  3. Deploy автоматически

Frontend (Vercel)

# Установить Vercel CLI
npm i -g vercel

# Deploy
vercel

# Production deploy
vercel --prod

Performance Tips

Backend

  1. Индексы на часто запрашиваемых полях

    cadastral_number = Column(String, unique=True, index=True)
    status = Column(String, index=True)
    

  2. Используйте async везде

    async def get_property(property_id: UUID) -> Property:
        return await db.get(Property, property_id)
    

  3. Кэшируйте тяжелые запросы

    @cache(ttl=300)
    async def get_all_properties():
        ...
    

Frontend

  1. Code splitting

    const PropertiesPage = lazy(() => import("./pages/PropertiesPage"));
    

  2. Мемоизация

    const expensiveCalculation = useMemo(() => calculatePrice(property), [property]);
    

  3. Виртуализация длинных списков

    import { useVirtualizer } from "@tanstack/react-virtual";
    


Полезные команды

Backend

# Запустить dev сервер
uvicorn app.main:app --reload

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

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

# Запустить тесты
pytest

# Форматирование
black app/ && ruff check --fix app/

# Type checking
mypy app/

Frontend

# Dev server
npm run dev

# Build
npm run build

# Preview
npm run preview

# Lint & format
npm run lint && npm run format

# Type check
npm run type-check

Best Practices

  1. Всегда используйте type hints в Python
  2. Пишите тесты для критической логики
  3. Делайте частые коммиты с понятными сообщениями
  4. Code review обязателен перед merge
  5. Не коммитьте secrets в git
  6. Используйте async/await везде в Python
  7. Мемоизируйте тяжелые вычисления в React
  8. Следуйте принципу DRY (Don't Repeat Yourself)

Готовы к разработке? Переходите к docs/api.md для изучения API endpoints.