diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..c73e0bd --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,329 @@ +# Руководство для контрибьюторов netsleuth + +Спасибо за интерес к проекту netsleuth! Это руководство поможет вам начать вносить свой вклад. + +## 🚀 Быстрый старт + +### 1. Установка зависимостей + +```bash +# Клонируйте репозиторий +git clone https://github.com/netsleuth/netsleuth.git +cd netsleuth + +# Установите uv (менеджер пакетов) +curl -LsSf https://astral.sh/uv/install.sh | sh + +# Синхронизируйте зависимости +uv sync --extra tcptrace --extra ndt7 + +# Установите pre-commit хуки +uv run pre-commit install +``` + +### 2. Запуск тестов + +```bash +# Все тесты +uv run pytest + +# Тесты с покрытием +uv run pytest --cov=src/netsleuth --cov-report=term-missing + +# Конкретный тест +uv run pytest tests/test_cli.py -v +``` + +### 3. Линтинг и форматирование + +```bash +# Проверка стиля +uv run flake8 src/netsleuth + +# Авто-форматирование (ruff) +uv run ruff format src/netsleuth + +# Проверка типов +uv run mypy src/netsleuth +``` + +--- + +## 📁 Структура проекта + +``` +netsleuth/ +├── src/netsleuth/ # Исходный код +│ ├── cli.py # CLI интерфейс (typer) +│ ├── config.py # Конфигурация и настройки +│ ├── models.py # Датаклассы и модели данных +│ ├── bgp.py # BGP и ASN информация +│ ├── geoip.py # Геолокация IP +│ ├── latency.py # Измерение задержек +│ ├── path.py # Traceroute и анализ пути +│ ├── speed.py # Speedtest и bufferbloat +│ ├── reputation.py # Проверка IP репутации +│ ├── exporter.py # Генерация отчётов +│ └── interpret.py # Интерпретация результатов +├── tests/ # Тесты +├── .github/workflows/ # CI/CD пайплайны +└── docs/ # Документация +``` + +--- + +## 🔧 Как добавить новый источник геоданных + +### Пример: Добавление GeoIP провайдера + +1. **Создайте функцию в `geoip.py`:** + +```python +async def fetch_geolocation_new_provider(ip: str) -> dict[str, Any]: + """Получает геоданные от нового провайдера. + + Args: + ip: IP адрес для поиска + + Returns: + Dict с ключами: country, city, org, asn + + Raises: + httpx.HTTPError: Если API недоступен + """ + async with httpx.AsyncClient() as client: + response = await client.get( + f"https://api.newprovider.com/geo/{ip}", + timeout=5.0 + ) + response.raise_for_status() + return parse_geolocation_response(response.json()) +``` + +2. **Добавьте парсер ответа:** + +```python +def parse_geolocation_response(payload: dict) -> dict[str, Any]: + """Извлекает поля из ответа API.""" + return { + "country": payload.get("country_name"), + "city": payload.get("city"), + "org": payload.get("org"), + "asn": payload.get("asn"), + } +``` + +3. **Интегрируйте в основной пайплайн:** + +В `cli.py` найдите `gather_identity()` и добавьте вызов: + +```python +geo_new = await fetch_geolocation_new_provider(ip) +``` + +4. **Напишите тест:** + +Создайте `tests/test_geoip_new_provider.py`: + +```python +import pytest +from unittest.mock import AsyncMock, patch + +@pytest.mark.asyncio +async def test_fetch_geolocation_new_provider(httpx_mock): + httpx_mock.add_response( + url="https://api.newprovider.com/geo/8.8.8.8", + json={"country_name": "US", "city": "Mountain View"} + ) + + from netsleuth.geoip import fetch_geolocation_new_provider + result = await fetch_geolocation_new_provider("8.8.8.8") + + assert result["country"] == "US" + assert result["city"] == "Mountain View" +``` + +--- + +## 🧪 Как написать тест для новой пробы + +### Шаблон теста + +```python +"""Тесты для модуля .""" + +import pytest +from unittest.mock import patch, MagicMock + +from netsleuth. import + + +class TestFunctionName: + """Тесты для .""" + + @pytest.mark.asyncio + async def test_success_case(self, httpx_mock): + """Тестирует успешный сценарий.""" + # Arrange + httpx_mock.add_response(...) + + # Act + result = await (...) + + # Assert + assert result.expected_field == "expected_value" + + @pytest.mark.asyncio + async def test_error_handling(self): + """Тестирует обработку ошибок.""" + with pytest.raises(ExpectedError): + await (invalid_input) + + @pytest.mark.asyncio + async def test_edge_case_empty_input(self): + """Тестирует граничный случай.""" + result = await ("") + assert result is None +``` + +### Мокирование HTTP запросов + +```python +@pytest.mark.asyncio +async def test_with_httpx_mock(httpx_mock): + """httpx_mock автоматически мокирует httpx запросы.""" + + # Добавляем ожидаемый запрос + httpx_mock.add_response( + url="https://api.example.com/data", + method="GET", + status_code=200, + json={"key": "value"} + ) + + # Вызываем функцию + result = await fetch_data() + + # Проверяем что запрос был сделан + request = httpx_mock.get_request() + assert request.url == "https://api.example.com/data" +``` + +--- + +## ✅ Чеклист перед Pull Request + +Перед отправкой PR убедитесь: + +- [ ] Все тесты проходят: `uv run pytest` +- [ ] Покрытие не уменьшилось (минимум 60%) +- [ ] Линтер не находит ошибок: `uv run flake8 src/netsleuth` +- [ ] Типизация корректна: `uv run mypy src/netsleuth` +- [ ] Код отформатирован: `uv run ruff format src/netsleuth` +- [ ] Добавлены docstrings для публичных функций +- [ ] Добавлены тесты для нового функционала +- [ ] Обновлена документация (если нужно) +- [ ] Pre-commit хуки проходят: `uv run pre-commit run --all-files` + +--- + +## 📝 Стиль кода + +### Именование + +- **Функции**: `snake_case` (`fetch_geolocation`) +- **Классы**: `PascalCase` (`SpeedResult`) +- **Константы**: `UPPER_CASE` (`MAX_RETRIES`) +- **Приватные функции**: `_prefix` (`_parse_response`) + +### Docstrings + +Используйте Google style: + +```python +def calculate_latency(samples: list[float]) -> float: + """Вычисляет среднюю задержку из списка семплов. + + Args: + samples: Список задержек в миллисекундах + + Returns: + Средняя задержка, или 0.0 если список пуст + + Raises: + ValueError: Если есть отрицательные значения + """ + if not samples: + return 0.0 + + if any(s < 0 for s in samples): + raise ValueError("Latency cannot be negative") + + return sum(samples) / len(samples) +``` + +### Type hints + +Все публичные функции должны иметь аннотации типов: + +```python +from typing import Any, Optional + +def process_ip(ip: str, timeout: float = 5.0) -> Optional[dict[str, Any]]: + ... +``` + +--- + +## 🐛 Отладка + +### Логирование + +Включите дебаг логирование: + +```bash +netsleuth --watch --verbose +``` + +### Отладка тестов + +```bash +# Запустить с выводом print() +uv run pytest -s tests/test_file.py::test_name + +# Остановиться на первой ошибке +uv run pytest -x tests/ + +# Запустить последний упавший тест +uv run pytest --lf +``` + +--- + +## 📞 Вопросы и поддержка + +- **GitHub Issues**: Для багов и фич https://github.com/netsleuth/netsleuth/issues +- **Discussions**: Для вопросов https://github.com/netsleuth/netsleuth/discussions + +--- + +## 🎯 Идеи для контрибьюшена + +### Начинающим + +- [ ] Добавить docstrings к функциям без документации +- [ ] Исправить опечатки в README +- [ ] Добавить тесты для существующих функций +- [ ] Улучшить сообщения об ошибках + +### Продвинутым + +- [ ] Добавить новый источник геоданных +- [ ] Реализовать кэширование API запросов +- [ ] Оптимизировать производительность NetsetIndex +- [ ] Добавить поддержку IPv6 в traceroute +- [ ] Создать GUI для результатов + +--- + +Спасибо за ваш вклад! 🎉