Гайд по разработке 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¶
- Создайте Pull Request на GitHub
- Опишите изменения в описании PR
- Добавьте скриншоты (если UI изменения)
- Дождитесь code review от коллег
- Внесите правки по комментариям
- После одобрения - 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:
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:
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)¶
- Создать
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
- Push в GitHub
- Подключить репозиторий в Render
- Deploy автоматически
Frontend (Vercel)¶
Performance Tips¶
Backend¶
-
Индексы на часто запрашиваемых полях
-
Используйте async везде
-
Кэшируйте тяжелые запросы
Frontend¶
-
Code splitting
-
Мемоизация
-
Виртуализация длинных списков
Полезные команды¶
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¶
- Всегда используйте type hints в Python
- Пишите тесты для критической логики
- Делайте частые коммиты с понятными сообщениями
- Code review обязателен перед merge
- Не коммитьте secrets в git
- Используйте async/await везде в Python
- Мемоизируйте тяжелые вычисления в React
- Следуйте принципу DRY (Don't Repeat Yourself)
Готовы к разработке? Переходите к docs/api.md для изучения API endpoints.