mirror of
https://github.com/Horizontal42/netsleuth.git
synced 2026-11-11 01:02:10 +03:00
docs: добавить руководство для контрибьюторов (CONTRIBUTING.md)
This commit is contained in:
parent
2c547bd763
commit
ad19f76249
1 changed files with 81 additions and 257 deletions
338
CONTRIBUTING.md
338
CONTRIBUTING.md
|
|
@ -1,20 +1,20 @@
|
||||||
# Руководство для контрибьюторов netsleuth
|
# Руководство для контрибьюторов Netsleuth
|
||||||
|
|
||||||
Спасибо за интерес к проекту netsleuth! Это руководство поможет вам начать вносить свой вклад.
|
Спасибо за интерес к проекту! Это руководство поможет вам начать вносить свой вклад.
|
||||||
|
|
||||||
## 🚀 Быстрый старт
|
## 🚀 Быстрый старт
|
||||||
|
|
||||||
### 1. Установка зависимостей
|
### 1. Настройка окружения
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Клонируйте репозиторий
|
# Клонируйте репозиторий
|
||||||
git clone https://github.com/netsleuth/netsleuth.git
|
git clone https://github.com/egorovandrey/netsleuth.git
|
||||||
cd netsleuth
|
cd netsleuth
|
||||||
|
|
||||||
# Установите uv (менеджер пакетов)
|
# Установите uv (если ещё не установлен)
|
||||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||||
|
|
||||||
# Синхронизируйте зависимости
|
# Создайте виртуальное окружение и установите зависимости
|
||||||
uv sync --extra tcptrace --extra ndt7
|
uv sync --extra tcptrace --extra ndt7
|
||||||
|
|
||||||
# Установите pre-commit хуки
|
# Установите pre-commit хуки
|
||||||
|
|
@ -28,10 +28,10 @@ uv run pre-commit install
|
||||||
uv run pytest
|
uv run pytest
|
||||||
|
|
||||||
# Тесты с покрытием
|
# Тесты с покрытием
|
||||||
uv run pytest --cov=src/netsleuth --cov-report=term-missing
|
uv run pytest --cov=src/netsleuth --cov-report=html
|
||||||
|
|
||||||
# Конкретный тест
|
# Конкретный тест
|
||||||
uv run pytest tests/test_cli.py -v
|
uv run pytest tests/test_exporter.py -v
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Линтинг и форматирование
|
### 3. Линтинг и форматирование
|
||||||
|
|
@ -40,290 +40,114 @@ uv run pytest tests/test_cli.py -v
|
||||||
# Проверка стиля
|
# Проверка стиля
|
||||||
uv run flake8 src/netsleuth
|
uv run flake8 src/netsleuth
|
||||||
|
|
||||||
# Авто-форматирование (ruff)
|
# Форматирование кода
|
||||||
uv run ruff format src/netsleuth
|
uv run black src/netsleuth tests
|
||||||
|
|
||||||
# Проверка типов
|
# Проверка типов
|
||||||
uv run mypy src/netsleuth
|
uv run mypy src/netsleuth --ignore-missing-imports
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
## 📝 Как добавить новый источник геоданных
|
||||||
|
|
||||||
## 📁 Структура проекта
|
1. Создайте новый файл в `src/netsleuth/geo/` (например, `new_provider.py`)
|
||||||
|
2. Реализуйте интерфейс:
|
||||||
```
|
|
||||||
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
|
```python
|
||||||
async def fetch_geolocation_new_provider(ip: str) -> dict[str, Any]:
|
from typing import Optional
|
||||||
"""Получает геоданные от нового провайдера.
|
from dataclasses import dataclass
|
||||||
|
|
||||||
Args:
|
@dataclass
|
||||||
ip: IP адрес для поиска
|
class GeoResult:
|
||||||
|
country: Optional[str] = None
|
||||||
Returns:
|
city: Optional[str] = None
|
||||||
Dict с ключами: country, city, org, asn
|
asn: Optional[int] = None
|
||||||
|
org: Optional[str] = None
|
||||||
Raises:
|
|
||||||
httpx.HTTPError: Если API недоступен
|
async def fetch_geo(ip: str) -> Optional[GeoResult]:
|
||||||
"""
|
"""Получить геоданные для IP."""
|
||||||
async with httpx.AsyncClient() as client:
|
# Ваша реализация
|
||||||
response = await client.get(
|
pass
|
||||||
f"https://api.newprovider.com/geo/{ip}",
|
|
||||||
timeout=5.0
|
|
||||||
)
|
|
||||||
response.raise_for_status()
|
|
||||||
return parse_geolocation_response(response.json())
|
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Добавьте парсер ответа:**
|
3. Добавьте импорт в `src/netsleuth/geo/__init__.py`
|
||||||
|
4. Напишите тесты в `tests/test_new_provider.py`
|
||||||
|
5. Обновите документацию
|
||||||
|
|
||||||
```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
|
```python
|
||||||
import pytest
|
import pytest
|
||||||
from unittest.mock import AsyncMock, patch
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_fetch_geolocation_new_provider(httpx_mock):
|
async def test_my_probe():
|
||||||
httpx_mock.add_response(
|
# Arrange
|
||||||
url="https://api.newprovider.com/geo/8.8.8.8",
|
with patch('netsleuth.my_probe.httpx.get') as mock_get:
|
||||||
json={"country_name": "US", "city": "Mountain View"}
|
mock_get.return_value = {"status": "ok"}
|
||||||
)
|
|
||||||
|
|
||||||
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
|
|
||||||
"""Тесты для модуля <module_name>."""
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from unittest.mock import patch, MagicMock
|
|
||||||
|
|
||||||
from netsleuth.<module> import <function_name>
|
|
||||||
|
|
||||||
|
|
||||||
class TestFunctionName:
|
|
||||||
"""Тесты для <function_name>."""
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_success_case(self, httpx_mock):
|
|
||||||
"""Тестирует успешный сценарий."""
|
|
||||||
# Arrange
|
|
||||||
httpx_mock.add_response(...)
|
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
result = await <function_name>(...)
|
result = await run_probe("8.8.8.8")
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert result.expected_field == "expected_value"
|
assert result.status == "ok"
|
||||||
|
mock_get.assert_called_once()
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_error_handling(self):
|
|
||||||
"""Тестирует обработку ошибок."""
|
|
||||||
with pytest.raises(ExpectedError):
|
|
||||||
await <function_name>(invalid_input)
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_edge_case_empty_input(self):
|
|
||||||
"""Тестирует граничный случай."""
|
|
||||||
result = await <function_name>("")
|
|
||||||
assert result is None
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Мокирование HTTP запросов
|
**Важно:**
|
||||||
|
- Все HTTP-запросы должны быть замокированы
|
||||||
```python
|
- Используйте фикстуры из `tests/conftest.py`
|
||||||
@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
|
## ✅ Чеклист перед Pull Request
|
||||||
|
|
||||||
Перед отправкой PR убедитесь:
|
- [ ] Код отформатирован (`black src/netsleuth tests`)
|
||||||
|
- [ ] Нет нарушений flake8 (`flake8 src/netsleuth`)
|
||||||
- [ ] Все тесты проходят: `uv run pytest`
|
- [ ] Все тесты проходят (`pytest`)
|
||||||
- [ ] Покрытие не уменьшилось (минимум 60%)
|
- [ ] Покрытие не уменьшилось (минимум 60%)
|
||||||
- [ ] Линтер не находит ошибок: `uv run flake8 src/netsleuth`
|
|
||||||
- [ ] Типизация корректна: `uv run mypy src/netsleuth`
|
|
||||||
- [ ] Код отформатирован: `uv run ruff format src/netsleuth`
|
|
||||||
- [ ] Добавлены docstrings для публичных функций
|
- [ ] Добавлены docstrings для публичных функций
|
||||||
- [ ] Добавлены тесты для нового функционала
|
|
||||||
- [ ] Обновлена документация (если нужно)
|
- [ ] Обновлена документация (если нужно)
|
||||||
- [ ] Pre-commit хуки проходят: `uv run pre-commit run --all-files`
|
- [ ] Pre-commit хуки пройдены
|
||||||
|
|
||||||
---
|
## 🏗️ Архитектура проекта
|
||||||
|
|
||||||
## 📝 Стиль кода
|
```
|
||||||
|
src/netsleuth/
|
||||||
### Именование
|
├── cli.py # Точка входа, CLI аргументы
|
||||||
|
├── config.py # Конфигурация и настройки
|
||||||
- **Функции**: `snake_case` (`fetch_geolocation`)
|
├── models.py # Датаклассы для данных
|
||||||
- **Классы**: `PascalCase` (`SpeedResult`)
|
├── probes/ # Модули проб (speed, latency, etc.)
|
||||||
- **Константы**: `UPPER_CASE` (`MAX_RETRIES`)
|
├── geo/ # Геоданные и ASN lookup
|
||||||
- **Приватные функции**: `_prefix` (`_parse_response`)
|
├── reputation.py # Проверка по чёрным спискам
|
||||||
|
├── interpret.py # Интерпретация результатов
|
||||||
### Docstrings
|
└── exporter.py # Экспорт в Markdown/JSON
|
||||||
|
|
||||||
Используйте 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
|
Основная статья: [`ARCHITECTURE.ru.md`](ARCHITECTURE.ru.md)
|
||||||
|
|
||||||
Все публичные функции должны иметь аннотации типов:
|
## 🐛 Сообщение об ошибках
|
||||||
|
|
||||||
```python
|
При создании issue укажите:
|
||||||
from typing import Any, Optional
|
1. Версию Python (`python --version`)
|
||||||
|
2. Версию netsleuth (`netsleuth --version`)
|
||||||
|
3. ОС и версию
|
||||||
|
4. Команду запуска
|
||||||
|
5. Полный вывод ошибки
|
||||||
|
6. Скриншот (если применимо)
|
||||||
|
|
||||||
def process_ip(ip: str, timeout: float = 5.0) -> Optional[dict[str, Any]]:
|
## 💡 Идеи для улучшений
|
||||||
...
|
|
||||||
```
|
Ищем помощь в следующих областях:
|
||||||
|
- [ ] Поддержка новых протоколов (QUIC, HTTP/3)
|
||||||
|
- [ ] Графический интерфейс (TUI/GUI)
|
||||||
|
- [ ] Интеграция с Prometheus/Grafana
|
||||||
|
- [ ] Поддержка IPv6-only хостов
|
||||||
|
- [ ] Локализация на другие языки
|
||||||
|
|
||||||
|
## 📞 Контакты
|
||||||
|
|
||||||
|
- GitHub Issues: [egorovandrey/netsleuth/issues](https://github.com/egorovandrey/netsleuth/issues)
|
||||||
|
- Telegram: @your_channel (если есть)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🐛 Отладка
|
**Спасибо за ваш вклад!** 🎉
|
||||||
|
|
||||||
### Логирование
|
|
||||||
|
|
||||||
Включите дебаг логирование:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
netsleuth <target> --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 для результатов
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Спасибо за ваш вклад! 🎉
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue