Skip to content

Andrew-Pyanzin/evateam-api-tools

Repository files navigation

EvaTeam API — Инструменты для работы с задачами

Оглавление


Описание

Набор Python-скриптов для автоматизации работы с задачами в системе EvaTeam через JSON-RPC API. Позволяет:

  • Массово искать и фильтровать задачи проекта по тегам
  • Выгружать списки задач в файлы с URL-ссылками
  • Классифицировать задачи по платформам (iOS / Android / MBE)
  • Автоматически списывать время на задачи с распределением по платформам
  • Получать информацию о текущем пользователе
  • Подсчитывать количество задач в проекте
  • Выполнять аудит задач на наличие/отсутствие обязательных тегов

Требования

  • Python 3.8+
  • Доступ к корпоративной сети (или VPN) для подключения к серверу EvaTeam
  • API-токен EvaTeam (см. раздел Получение API-токена)

Установка и настройка

1. Установка зависимостей

pip install requests python-dotenv

2. Получение API-токена

  1. Авторизоваться в EvaTeam по адресу вашего инстанса (например, https://eva.your-company.ru)
  2. Перейти в Настройки профиляAPI-токены (или ПрофильИнтеграции)
  3. Создать новый токен с правами на чтение/запись задач
  4. Скопировать сгенерированный токен

Внимание: Никогда не коммитьте токен в репозиторий. Храните его только в .env файле, который добавлен в .gitignore.

3. Настройка окружения (.env)

Создайте файл .env в директории со скриптами:

# ========================================
# НАСТРОЙКИ ПОДКЛЮЧЕНИЯ К EVATEAM
# ========================================

# URL вашего инстанса EvaTeam (без слэша в конце)
EVA_BASE=https://eva.your-company.ru

# <<< ВСТАВЬТЕ ВАШ API-ТОКЕН СЮДА >>>
EVA_TOKEN=your_api_token_here

Где взять значения:

Переменная Описание Пример
EVA_BASE URL-адрес вашего инстанса EvaTeam https://eva.your-company.ru
EVA_TOKEN Персональный API-токен (см. п.2) eyJhbGciOiJIUzI1Ni...

Расположение .env файла:

Скрипт Где ищет .env
find_tasks_simple.py В той же директории, где лежит скрипт
find_tasks_v2.py В той же директории, где лежит скрипт
find_tasks_with_tag.py В той же директории, где лежит скрипт
find_tasks_without_service_tag.py На 4 уровня выше (в корне проекта)
add_time_tracking.py На 4 уровня выше (в корне проекта)

Если .env не найден в ожидаемом месте, скрипт выведет ошибку. Некоторые скрипты (find_tasks_without_service_tag.py) предложат ввести токен интерактивно.


Описание API EvaTeam

Формат запросов (JSON-RPC 2.2)

EvaTeam использует JSON-RPC 2.2 протокол. Каждый запрос отправляется как POST на эндпоинт:

POST {EVA_BASE}/api/?m={method_name}

Тело запроса:

{
    "jsonrpc": "2.2",
    "method": "CmfTask.list",
    "callid": "уникальный-uuid",
    "kwargs": {
        "filter": [["code", "LIKE", "MAPPS-%"]],
        "fields": ["code", "name", "tags"],
        "slice": [0, 100],
        "order_by": ["code"]
    }
}

Поля запроса:

Поле Тип Описание
jsonrpc string Версия протокола, всегда "2.2"
method string Название вызываемого метода API
callid string Уникальный UUID для идентификации запроса
kwargs object Именованные аргументы метода
args array Позиционные аргументы метода (используется в CmfTask.get)

Ответ API:

{
    "jsonrpc": "2.2",
    "result": [ ... ],
    "error": null
}
  • result — данные ответа (массив задач, объект задачи и т.д.)
  • errornull при успехе или объект с полем message при ошибке

Аутентификация

Все запросы требуют Bearer-токен в заголовке:

Authorization: Bearer <<ВАШ_EVA_TOKEN>>
Content-Type: application/json

Примечание: SSL-верификация отключена (verify=False) для работы с корпоративными сертификатами. Если ваш инстанс EvaTeam использует публичный SSL-сертификат, можно включить верификацию для повышения безопасности.

Основные методы API

Метод Описание Тип аргументов Параметры
CmfTask.list Получить список задач kwargs filter, fields, slice, order_by
CmfTask.get Получить одну задачу по ID args args=[task_id], kwargs.fields
CmfTask.count Подсчитать количество задач kwargs filter
User.getCurrentUser Информация о текущем пользователе kwargs {} (пустой)
CmfTimeTrackerHistory.create Добавить запись о времени kwargs task, time_spent, date, comment
CmfTask.addTime Добавить время (альтернативный метод) kwargs task, time_spent, date, comment
CmfTask.logTime Логировать время (альтернативный метод) kwargs task, time_spent, date, comment
TimeTracker.create Создать запись трекера (альтернативный метод) kwargs task, time_spent, date, comment
TimeEntry.create Создать запись времени (альтернативный метод) kwargs task, time_spent, date, comment

Важно: Для списания времени доступно несколько методов API. Рабочий метод зависит от версии и конфигурации вашего инстанса EvaTeam. Скрипт add_time_tracking.py автоматически перебирает все 5 методов и использует первый работающий.

Параметры фильтрации

Фильтры передаются как массив условий. Каждое условие — массив из трёх элементов: [поле, оператор, значение].

Оператор Описание Пример
= Точное совпадение ["code", "=", "MAPPS-100"]
LIKE Поиск по шаблону (% — любые символы) ["code", "LIKE", "MAPPS-%"]
>= Больше или равно ["cmf_created_at", ">=", "2025-01-01"]
<= Меньше или равно ["cmf_created_at", "<=", "2025-12-31"]

Комбинирование фильтров (логическое И):

filters = [
    ["code", "LIKE", "MAPPS-%"],
    ["cmf_created_at", ">=", "2025-12-01"],
    ["cmf_created_at", "<=", "2025-12-31"]
]

Пагинация (slice)

Параметр slice принимает массив [offset, limit]:

  • [0, 100] — первые 100 записей
  • [100, 100] — записи со 101 по 200
  • [0, 1000] — попытка получить до 1000 записей за раз

Ограничение: API может возвращать максимум ~200-1000 записей за один запрос в зависимости от конфигурации сервера. Используйте пагинацию для получения всех данных.

Доступные поля задач

Поле Тип Описание
code string Код задачи (например, MAPPS-123)
name string Название задачи
tags array Список тегов (объекты с полями code, name)
id string Внутренний UUID задачи (формат CmfTask:UUID)
cmf_created_at datetime Дата создания задачи
responsible object Ответственный за задачу
parent string Код родительской задачи/проекта
lists string Название списка/спринта, к которому относится задача

Структура тегов в ответе API

Теги могут приходить в двух форматах:

Формат 1 — объект:

{
    "tags": [
        {"code": "25-026178_сервисы", "name": "25-026178_сервисы"},
        {"code": "ios", "name": "iOS"}
    ]
}

Формат 2 — строка (в некоторых версиях API):

{
    "tags": ["25-026178_сервисы", "ios"]
}

Все скрипты обрабатывают оба формата через проверку isinstance(tag, dict).


Все сценарии использования (Use Cases)

UC-1. Подсчёт общего количества задач проекта

Цель: Узнать, сколько задач существует в проекте, до загрузки данных.

Метод API: CmfTask.count

total_count = call_api("CmfTask.count", {
    "filter": [["code", "LIKE", "MAPPS-%"]],
})

Применяется в: find_tasks_simple.py

Примечание: Результат может быть числом или списком (зависит от версии API). Скрипт обрабатывает оба случая:

if isinstance(total_count, list):
    total_count = len(total_count)

UC-2. Загрузка всех задач проекта (простая пагинация)

Цель: Загрузить все задачи проекта, обходя лимит API на количество записей.

Алгоритм:

  1. Запросить первую порцию (batch) задач с slice=[0, batch_size]
  2. Если получено batch_size записей — есть ещё данные, увеличить offset
  3. Повторять, пока не получим меньше batch_size записей или пустой ответ
  4. Защита от бесконечного цикла: лимит на максимальный offset
all_tasks = []
batch_size = 100
offset = 0

while True:
    batch = call_api("CmfTask.list", {
        "filter": [["code", "LIKE", "MAPPS-%"]],
        "fields": ["code", "name", "tags"],
        "slice": [offset, batch_size],
        "order_by": ["-cmf_created_at"]
    })

    if not batch:
        break

    all_tasks.extend(batch)

    if len(batch) < batch_size:
        break

    offset += batch_size

    # Защита от бесконечного цикла
    if offset > 100000:
        break

Применяется в: find_tasks_simple.py, find_tasks_without_service_tag.py


UC-3. Загрузка всех задач проекта (запрос по диапазонам кодов)

Цель: Обойти ограничение пагинации, когда API не возвращает все записи через обычный offset.

Алгоритм:

  1. Попытка загрузить одним запросом с большим лимитом slice=[0, 1000]
  2. Если получено меньше ожидаемого — переключиться на стратегию по диапазонам
  3. Разбить задачи по диапазонам номеров: MAPPS-1..200, MAPPS-200..400 и т.д.
  4. Запросить каждый диапазон отдельно
  5. Объединить и дедуплицировать результаты
# Стратегия 1: один запрос
batch = call_api("CmfTask.list", {
    "filter": [["code", "LIKE", "MAPPS-%"]],
    "fields": ["code", "name", "tags"],
    "slice": [0, 1000],
    "order_by": ["code"]
})

# Стратегия 2 (fallback): по диапазонам
if len(batch) < expected_total:
    ranges = [(1, 200), (200, 400), (400, 600), (600, 800), (800, 1000), (1000, 1200)]
    for start, end in ranges:
        range_batch = call_api("CmfTask.list", {
            "filter": [
                ["code", ">=", f"MAPPS-{start}"],
                ["code", "<=", f"MAPPS-{end}"]
            ],
            "fields": ["code", "name", "tags"],
            "slice": [0, 200]
        })
        all_tasks.extend(range_batch)

Применяется в: find_tasks_v2.py

Когда использовать: Если обычная пагинация (UC-2) возвращает неполные данные.


UC-4. Дедупликация задач после загрузки

Цель: Убрать дубликаты задач, которые могут появиться при загрузке по перекрывающимся диапазонам.

seen_codes = set()
unique_tasks = []
for task in all_tasks:
    code = task.get('code')
    if code not in seen_codes:
        seen_codes.add(code)
        unique_tasks.append(task)
all_tasks = unique_tasks

Применяется в: find_tasks_v2.py


UC-5. Поиск задач БЕЗ определённого тега

Цель: Найти все задачи проекта, у которых отсутствует указанный тег. Используется для аудита — убедиться, что все задачи помечены обязательным тегом.

Алгоритм:

  1. Загрузить все задачи проекта (UC-2 или UC-3)
  2. Для каждой задачи проверить наличие тега (по полю code или name)
  3. Собрать задачи, где тег не найден
service_tag = "25-026178_сервисы"  # <<< Замените на нужный код тега

tasks_without_tag = []
for task in all_tasks:
    tags = task.get('tags', [])
    has_tag = False

    for tag in tags:
        if isinstance(tag, dict):
            tag_code = tag.get('code')
            tag_name = tag.get('name')
            if tag_code == service_tag or tag_name == service_tag:
                has_tag = True
                break
        else:
            if str(tag) == service_tag:
                has_tag = True
                break

    if not has_tag:
        tasks_without_tag.append(task)

Применяется в: find_tasks_simple.py, find_tasks_v2.py, find_tasks_without_service_tag.py


UC-6. Поиск задач С определённым тегом

Цель: Найти все задачи проекта, которые содержат указанный тег. Используется для инвентаризации — получить список задач определённой категории.

Алгоритм: Аналогичен UC-5, но собираются задачи, где тег найден.

tasks_with_tag = []
for task in all_tasks:
    tags = task.get('tags', [])
    has_tag = False

    for tag in tags:
        if isinstance(tag, dict):
            tag_code = tag.get('code')
            tag_name = tag.get('name')
            if tag_code == service_tag or tag_name == service_tag:
                has_tag = True
                break
        else:
            if str(tag) == service_tag:
                has_tag = True
                break

    if has_tag:
        tasks_with_tag.append(task)

Применяется в: find_tasks_with_tag.py


UC-7. Поиск задач по дате создания (диапазон дат)

Цель: Получить задачи, созданные в определённый период (например, за конкретный месяц).

tasks = call_api("CmfTask.list", {
    "filter": [
        ["code", "LIKE", "MAPPS-%"],
        ["cmf_created_at", ">=", "2025-12-01"],   # <<< Начало периода
        ["cmf_created_at", "<=", "2025-12-31"]     # <<< Конец периода
    ],
    "fields": ["code", "name", "tags", "id", "cmf_created_at"],
    "slice": [0, 500]
})

Применяется в: add_time_tracking.py (функция get_december_tasks)


UC-8. Поиск задач по родительскому проекту

Цель: Получить задачи, принадлежащие определённому проекту по его коду (ID родительского объекта).

tasks = call_api("CmfTask.list", {
    "filter": [
        ["parent", "=", "KBB-000251"],            # <<< Код проекта
        ["cmf_created_at", ">=", "2025-12-01"],
        ["cmf_created_at", "<=", "2025-12-31"]
    ],
    "fields": ["code", "name", "tags", "id", "cmf_created_at"],
    "slice": [0, 500]
})

Применяется в: add_time_tracking.py (стратегия 1 внутри get_december_tasks)


UC-9. Поиск задач по названию спринта/списка

Цель: Получить задачи из определённого спринта или списка по его имени.

tasks = call_api("CmfTask.list", {
    "filter": [
        ["lists", "LIKE", "%Sprint 2%"],           # <<< Имя спринта
        ["cmf_created_at", ">=", "2025-12-01"],
        ["cmf_created_at", "<=", "2025-12-31"]
    ],
    "fields": ["code", "name", "tags", "id", "cmf_created_at"],
    "slice": [0, 500]
})

Применяется в: add_time_tracking.py (стратегия 2 внутри get_december_tasks)


UC-10. Классификация задач по платформам (iOS / Android / MBE)

Цель: Разделить задачи на группы по целевой платформе на основе тегов.

Алгоритм:

  1. Для каждой задачи извлечь все теги
  2. Привести названия тегов к нижнему регистру
  3. Проверить наличие ключевых слов: ios, android, mbe, backend
  4. Распределить в соответствующие группы
ios_tasks = []
android_tasks = []
mbe_tasks = []
other_tasks = []

for task in all_tasks:
    task_tags = task.get("tags", [])
    tag_names = []

    for tag in task_tags:
        if isinstance(tag, dict):
            tag_names.append(tag.get("name", "").lower())
        else:
            tag_names.append(str(tag).lower())

    is_ios = any("ios" in t for t in tag_names)
    is_android = any("android" in t for t in tag_names)
    is_mbe = any("mbe" in t or "backend" in t for t in tag_names)

    if is_ios:
        ios_tasks.append(task)
    elif is_android:
        android_tasks.append(task)
    elif is_mbe:
        mbe_tasks.append(task)
    else:
        other_tasks.append(task)

Применяется в: add_time_tracking.py (функция get_december_tasks)

Настройка: Для добавления новых платформ — добавьте новые условия any(...) и списки.


UC-11. Получение информации о текущем пользователе

Цель: Проверить, что токен валиден, и узнать, от чьего имени будут выполняться действия.

result = call_api("User.getCurrentUser", kwargs={})
user_name = result.get("name", "Неизвестно")
print(f"Пользователь: {user_name}")

Применяется в: add_time_tracking.py (начало main())


UC-12. Списание времени на конкретную задачу

Цель: Добавить запись о затраченном времени к одной задаче.

Особенность: API может поддерживать разные методы для записи времени в зависимости от версии. Скрипт последовательно пробует 5 методов:

methods_to_try = [
    "CmfTimeTrackerHistory.create",
    "CmfTask.addTime",
    "CmfTask.logTime",
    "TimeTracker.create",
    "TimeEntry.create"
]

kwargs = {
    "task": task_id,         # ID задачи (UUID)
    "time_spent": 2.5,       # Часы (float)
    "date": "2025-12-15",    # Дата в формате YYYY-MM-DD
    "comment": "Описание"    # Комментарий (опционально)
}

for method in methods_to_try:
    try:
        result = call_api(method, kwargs=kwargs)
        break  # Метод сработал
    except Exception:
        continue  # Пробуем следующий

Применяется в: add_time_tracking.py (метод add_time_to_task)


UC-13. Массовое равномерное распределение часов по задачам

Цель: Распределить общее количество часов (например, 176 за месяц) равномерно между задачами, сгруппированными по платформам.

Алгоритм:

  1. Общее количество часов делится поровну между платформами: часов_на_платформу = total / 3
  2. Часы каждой платформы делятся поровну между задачами: часов_на_задачу = часов_на_платформу / количество_задач
  3. Для каждой задачи вызывается UC-12
Пример: 176 часов, 3 платформы
├── iOS (20 задач):     58.67 часов → 2.93 часа на задачу
├── Android (15 задач): 58.67 часов → 3.91 часа на задачу
└── MBE (10 задач):     58.67 часов → 5.87 часа на задачу

Применяется в: add_time_tracking.py (функция distribute_hours)

Настройка:

# Изменить количество часов:
distribute_hours(176, tasks_by_platform, dry_run=True)  # <<< 176 → ваше значение

# Изменить список платформ:
platforms = ["ios", "android", "mbe"]  # <<< Добавьте или уберите платформы

UC-14. Dry Run — предпросмотр изменений без применения

Цель: Показать, какие изменения будут выполнены, без фактической записи данных в EvaTeam.

Алгоритм:

  1. По умолчанию скрипт запускается в режиме dry run
  2. Выполняется вся логика: загрузка задач, распределение часов, вывод отчёта
  3. Но вызовы API на запись времени не выполняются
  4. Для фактического применения — передать флаг --apply
# Предпросмотр (безопасно):
python add_time_tracking.py

# Применить:
python add_time_tracking.py --apply

Применяется в: add_time_tracking.py


UC-15. Генерация URL-ссылок на задачи

Цель: Сформировать прямые ссылки на задачи в веб-интерфейсе EvaTeam.

Формат URL:

{EVA_BASE}/tasks/{TASK_CODE}
task_code = task.get('code')  # например, "MAPPS-14"
url = f"{BASE_URL}/tasks/{task_code}"
# Результат: https://eva.your-company.ru/tasks/MAPPS-14

Применяется в: find_tasks_with_tag.py, find_tasks_without_service_tag.py


UC-16. Экспорт результатов в текстовый файл

Цель: Сохранить результаты поиска в файл для дальнейшего использования, отчётности или передачи коллегам.

Формат файла (задачи с тегом):

Задачи С тегом '25-026178_сервисы'
Всего: 32

1. MAPPS-14: iOS. Favorites. Добавить в кнопке новый action для emptyPage
   https://eva.your-company.ru/tasks/MAPPS-14

2. MAPPS-56: iOS. Live-Activities. Корректировки в моделях
   https://eva.your-company.ru/tasks/MAPPS-56

Формат файла (задачи без тега — расширенный):

Задачи без тега '25-026178_сервисы'
Дата: 2025-12-15 14:30:00
Всего: 330 задач
======================================================================

1. MAPPS-1
   Название: iOS. R&D Фикса ошибок Яндекс Карт
   Теги: ios, bugs
   URL: https://eva.your-company.ru/tasks/MAPPS-1

Применяется в: Все скрипты поиска задач

Выходные файлы:

  • tasks_with_service_tag.txt — задачи с тегом
  • tasks_without_service_tag.txt — задачи без тега

UC-17. Сортировка задач по числовой части кода

Цель: Отсортировать задачи по номеру (MAPPS-1, MAPPS-2, ..., MAPPS-1000) вместо лексикографической сортировки.

tasks.sort(key=lambda x: int(x['code'].split('-')[1]) if '-' in x['code'] else 0)

Применяется в: find_tasks_v2.py, find_tasks_with_tag.py


UC-18. Получение одной задачи по ID

Цель: Получить детальную информацию об одной конкретной задаче.

task = call_api("CmfTask.get", args=["MAPPS-123"], kwargs={
    "fields": ["code", "name", "tags", "id", "responsible"]
})

Метод API: CmfTask.get с позиционным аргументом args=[task_id]

Применяется в: add_time_tracking.py (метод get_task класса EvaTeamClient)


UC-19. Интерактивный ввод токена (fallback)

Цель: Если токен не найден в .env, предложить пользователю ввести его вручную в консоли.

if not self.token:
    print("API токен не найден в переменных окружения.")
    print("Введите токен вручную (или нажмите Ctrl+C для выхода):")
    self.token = input("EVA_TOKEN: ").strip()

    if not self.token:
        raise ValueError("API token не может быть пустым")

Применяется в: find_tasks_without_service_tag.py (класс EvaTeamClient)


UC-20. Каскадный поиск задач (несколько стратегий)

Цель: Если один способ поиска не дал результатов, автоматически попробовать альтернативные.

Алгоритм (из add_time_tracking.py):

  1. Стратегия 1: Поиск по коду родительского проекта (parent = "KBB-000251")
  2. Стратегия 2 (fallback): Поиск по имени спринта (lists LIKE "%Sprint 2%")
  3. Стратегия 3 (fallback): Поиск по префиксу кода (code LIKE "MAPPS-%")
all_tasks = []

# Стратегия 1
try:
    tasks = client.get_tasks(filters=[["parent", "=", project_code], ...])
    all_tasks.extend(tasks)
except Exception:
    pass

# Стратегия 2 (если стратегия 1 не дала результатов)
if not all_tasks:
    try:
        tasks = client.get_tasks(filters=[["lists", "LIKE", "%Sprint 2%"], ...])
        all_tasks.extend(tasks)
    except Exception:
        pass

# Стратегия 3 (если стратегии 1 и 2 не дали результатов)
if not all_tasks:
    try:
        tasks = client.get_tasks(filters=[["code", "LIKE", "MAPPS-%"], ...])
        all_tasks.extend(tasks)
    except Exception:
        pass

Применяется в: add_time_tracking.py (функция get_december_tasks)


Скрипты — подробное описание

find_tasks_simple.py

Назначение: Поиск всех задач проекта без указанного тега (базовая версия).

Запуск:

python find_tasks_simple.py

Алгоритм:

  1. Подсчитывает общее количество задач через CmfTask.count (UC-1)
  2. Загружает все задачи порциями по 100 с пагинацией по offset (UC-2)
  3. Защита от бесконечного цикла: лимит offset > 100000
  4. Фильтрует задачи без целевого тега (UC-5)
  5. Выводит список в консоль
  6. Сохраняет в tasks_without_service_tag.txt (UC-16)

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

service_tag = "25-026178_сервисы"  # <<< Код тега
["code", "LIKE", "MAPPS-%"]       # <<< Префикс проекта

Где ищет .env: в директории скрипта


find_tasks_v2.py

Назначение: Улучшенная версия поиска задач без тега с двумя стратегиями загрузки и дедупликацией.

Запуск:

python find_tasks_v2.py

Алгоритм:

  1. Стратегия 1: Один запрос с slice=[0, 1000] (UC-3, попытка 1)
  2. Стратегия 2 (fallback): Если загружено менее 330 задач — переключение на запрос по диапазонам кодов: MAPPS-1..200, MAPPS-200..400, ..., MAPPS-1000..1200 (UC-3, попытка 2)
  3. Дедупликация по коду задачи (UC-4)
  4. Фильтрация без тега (UC-5)
  5. Сортировка по числовой части кода (UC-17)
  6. Сохранение в файл (UC-16)

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

service_tag = "25-026178_сервисы"           # <<< Код тега
["code", "LIKE", "MAPPS-%"]                 # <<< Префикс проекта
if len(all_tasks) < 330:                     # <<< Порог для fallback
ranges = [(1, 200), (200, 400), ...]         # <<< Диапазоны кодов

Где ищет .env: в директории скрипта


find_tasks_with_tag.py

Назначение: Поиск всех задач проекта С указанным тегом. Генерирует список с URL-ссылками.

Запуск:

python find_tasks_with_tag.py

Алгоритм:

  1. Загружает все задачи одним запросом slice=[0, 1000] (UC-3)
  2. Фильтрует задачи с целевым тегом (UC-6)
  3. Сортирует по числовой части кода (UC-17)
  4. Генерирует URL-ссылки для каждой задачи (UC-15)
  5. Выводит список с URL в консоль
  6. Сохраняет в tasks_with_service_tag.txt (UC-16)

Формат вывода:

1. MAPPS-14: iOS. Favorites. Добавить в кнопке новый action для emptyPage
   🔗 https://eva.your-company.ru/tasks/MAPPS-14

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

service_tag = "25-026178_сервисы"  # <<< Код тега
["code", "LIKE", "MAPPS-%"]       # <<< Префикс проекта

Где ищет .env: в директории скрипта


find_tasks_without_service_tag.py

Назначение: Полная OOP-версия поиска задач без тега с классом EvaTeamClient, подробным выводом и интерактивным вводом токена.

Запуск:

python find_tasks_without_service_tag.py

Алгоритм:

  1. Инициализация клиента EvaTeamClient — если токен не найден, запрашивает интерактивно (UC-19)
  2. Загрузка всех задач порциями по 100 (до 10000 задач) (UC-2)
  3. Фильтрация без тега с проверкой по code и name (UC-5)
  4. Подробный вывод: код задачи, название, список текущих тегов
  5. Генерация URL в файле (UC-15)
  6. Сохранение с меткой времени (UC-16)

Отличия от других версий:

  • OOP-архитектура (класс EvaTeamClient)
  • Интерактивный ввод токена как fallback
  • Расширенные поля: responsible, cmf_created_at
  • Подробный вывод текущих тегов задачи (для понимания, какие теги уже есть)
  • Дата и время генерации в выходном файле
  • URL-ссылки в выходном файле

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

service_tag = "25-026178_сервисы"  # <<< Код тега
["code", "LIKE", "MAPPS-%"]       # <<< Префикс проекта

Где ищет .env: на 4 уровня выше от скрипта (в корне проекта)


add_time_tracking.py

Назначение: Автоматическое распределение и списание рабочих часов по задачам проекта с разбивкой по платформам.

Запуск:

# Предпросмотр (dry run) — изменения НЕ применяются
python add_time_tracking.py

# Применить списание часов
python add_time_tracking.py --apply

Полный алгоритм:

  1. Проверка наличия EVA_TOKEN — если нет, вывод инструкции по настройке
  2. Получение информации о текущем пользователе (UC-11)
  3. Каскадный поиск задач проекта за указанный период (UC-20):
    • Стратегия 1: по коду родительского проекта (UC-8)
    • Стратегия 2: по имени спринта (UC-9)
    • Стратегия 3: по префиксу кода задач (UC-7)
  4. Классификация задач по платформам на основе тегов (UC-10)
  5. Равномерное распределение часов (UC-13):
    • Общие часы делятся поровну между платформами
    • Часы каждой платформы делятся поровну между задачами
  6. Dry Run (UC-14): показывает, что будет сделано
  7. Apply (при флаге --apply): выполняет списание через API с fallback по методам (UC-12)
  8. Итоговая статистика: обработано / ошибки / часы по каждой платформе

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

project_code = "KBB-000251"                  # <<< Код проекта
["cmf_created_at", ">=", "2025-12-01"]       # <<< Начало периода
["cmf_created_at", "<=", "2025-12-31"]       # <<< Конец периода
distribute_hours(176, ...)                    # <<< Количество часов
platforms = ["ios", "android", "mbe"]         # <<< Список платформ
comment = "Списание времени Sprint 2 weeks"  # <<< Комментарий к записи

Где ищет .env: на 4 уровня выше от скрипта (в корне проекта)


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

eva-team/
├── .env                              # Файл с ключами (НЕ коммитить!)
│                                     # Содержит EVA_BASE и EVA_TOKEN
├── README.md                         # Данная инструкция
│
├── find_tasks_simple.py              # UC-1, UC-2, UC-5, UC-16
│                                     # Простой поиск задач без тега
│                                     # Пагинация по offset, count
│
├── find_tasks_v2.py                  # UC-3, UC-4, UC-5, UC-16, UC-17
│                                     # Улучшенный поиск с дедупликацией
│                                     # Две стратегии загрузки
│
├── find_tasks_with_tag.py            # UC-3, UC-6, UC-15, UC-16, UC-17
│                                     # Поиск задач с тегом + URL-ссылки
│
├── find_tasks_without_service_tag.py # UC-2, UC-5, UC-15, UC-16, UC-18, UC-19
│                                     # Полная OOP-версия поиска без тега
│                                     # Интерактивный ввод токена
│
├── add_time_tracking.py              # UC-7..UC-14, UC-20
│                                     # Массовое списание времени
│                                     # Каскадный поиск + платформы
│                                     # Dry run + apply
│
├── tasks_with_service_tag.txt        # Выходной файл: задачи С тегом
└── tasks_without_service_tag.txt     # Выходной файл: задачи БЕЗ тега

Примеры кода

Базовый клиент API (минимальный)

Минимальный код для работы с API EvaTeam (функциональный стиль):

import requests
import uuid
import os
from dotenv import load_dotenv

load_dotenv()  # загружает .env

BASE_URL = os.getenv('EVA_BASE')   # <<< Из .env
TOKEN = os.getenv('EVA_TOKEN')     # <<< Из .env

import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

session = requests.Session()
session.headers.update({
    "Authorization": f"Bearer {TOKEN}",   # <<< Токен из .env
    "Content-Type": "application/json"
})

def call_api(method, kwargs=None, args=None):
    payload = {
        "jsonrpc": "2.2",
        "method": method,
        "callid": str(uuid.uuid4()),
    }
    if kwargs:
        payload["kwargs"] = kwargs
    if args:
        payload["args"] = args

    response = session.post(
        f"{BASE_URL}/api/?m={method}",
        json=payload,
        verify=False  # для корпоративных сертификатов
    )
    result = response.json()

    if result.get("error"):
        raise Exception(f"API Error: {result['error']}")

    return result.get("result", [])

OOP-клиент (класс EvaTeamClient)

Полнофункциональный клиент с обработкой ошибок и fallback-ом для токена:

import requests
import uuid
import os
from typing import List, Dict, Optional
from dotenv import load_dotenv

load_dotenv()

class EvaTeamClient:
    def __init__(self, base_url: Optional[str] = None, token: Optional[str] = None):
        self.base_url = base_url or os.getenv('EVA_BASE')    # <<< Из .env
        self.token = token or os.getenv('EVA_TOKEN')          # <<< Из .env

        if not self.token:
            # Fallback: интерактивный ввод
            print("API токен не найден. Введите токен вручную:")
            self.token = input("EVA_TOKEN: ").strip()
            if not self.token:
                raise ValueError("API token не может быть пустым")

        self.session = requests.Session()
        self.session.headers.update({
            "Authorization": f"Bearer {self.token}",
            "Content-Type": "application/json"
        })

        import urllib3
        urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

    def call_method(self, method: str, kwargs: Optional[Dict] = None,
                    args: Optional[List] = None) -> Dict:
        payload = {
            "jsonrpc": "2.2",
            "method": method,
            "callid": str(uuid.uuid4()),
        }
        if kwargs:
            payload["kwargs"] = kwargs
        if args:
            payload["args"] = args

        response = self.session.post(
            f"{self.base_url}/api/?m={method}",
            json=payload,
            verify=False
        )

        if response.status_code != 200:
            raise Exception(f"API Error: {response.status_code} - {response.text}")

        result = response.json()
        if result.get("error"):
            raise Exception(f"API Error: {result['error'].get('message', result['error'])}")

        return result

    def get_tasks(self, filters=None, fields=None, slice_params=None, order_by=None):
        kwargs = {}
        if filters:
            kwargs["filter"] = filters
        if fields:
            kwargs["fields"] = fields
        if slice_params:
            kwargs["slice"] = slice_params
        if order_by:
            kwargs["order_by"] = order_by
        result = self.call_method("CmfTask.list", kwargs=kwargs)
        return result.get("result", [])

    def get_task(self, task_id: str, fields=None):
        kwargs = {"fields": fields} if fields else {}
        result = self.call_method("CmfTask.get", args=[task_id], kwargs=kwargs)
        return result.get("result", {})

    def get_user_info(self):
        result = self.call_method("User.getCurrentUser", kwargs={})
        return result.get("result", {})

Фильтрация задач по тегам (универсальная)

Универсальная функция, обрабатывающая оба формата тегов (объект и строка):

def filter_tasks_by_tag(tasks, tag_code, has_tag=True):
    """
    Фильтрация задач по наличию/отсутствию тега.

    Args:
        tasks: список задач из API
        tag_code: код тега для проверки
        has_tag: True — задачи С тегом, False — БЕЗ тега
    """
    result = []
    for task in tasks:
        tags = task.get('tags', [])
        found = False

        for tag in tags:
            if isinstance(tag, dict):
                # Формат объекта: {"code": "...", "name": "..."}
                tag_val_code = tag.get('code')
                tag_val_name = tag.get('name')
                if tag_val_code == tag_code or tag_val_name == tag_code:
                    found = True
                    break
            else:
                # Формат строки: "tag_code"
                if str(tag) == tag_code:
                    found = True
                    break

        if found == has_tag:
            result.append(task)

    return result

Пагинация с защитой от бесконечного цикла

def load_all_tasks(project_prefix="MAPPS", batch_size=100, max_offset=100000):
    """Загрузить все задачи проекта с пагинацией и защитой"""
    all_tasks = []
    offset = 0

    while offset < max_offset:
        batch = call_api("CmfTask.list", {
            "filter": [["code", "LIKE", f"{project_prefix}-%"]],
            "fields": ["code", "name", "tags"],
            "slice": [offset, batch_size],
            "order_by": ["code"]
        })

        if not batch:
            break

        all_tasks.extend(batch)
        print(f"  Загружено: {len(all_tasks)}")

        if len(batch) < batch_size:
            break

        offset += batch_size

    return all_tasks

Загрузка по диапазонам кодов

Альтернативный метод загрузки, когда стандартная пагинация даёт неполные данные:

def load_tasks_by_ranges(project_prefix="MAPPS", range_step=200, max_code=1200):
    """Загрузить задачи по диапазонам кодов"""
    all_tasks = []
    ranges = [(i, i + range_step) for i in range(1, max_code, range_step)]

    for start, end in ranges:
        batch = call_api("CmfTask.list", {
            "filter": [
                ["code", ">=", f"{project_prefix}-{start}"],
                ["code", "<=", f"{project_prefix}-{end}"]
            ],
            "fields": ["code", "name", "tags"],
            "slice": [0, range_step]
        })

        if batch:
            all_tasks.extend(batch)
            print(f"  Диапазон {project_prefix}-{start}..{end}: +{len(batch)}")

    # Дедупликация
    seen = set()
    unique = []
    for task in all_tasks:
        code = task.get('code')
        if code not in seen:
            seen.add(code)
            unique.append(task)

    return unique

Классификация по платформам через теги

def classify_by_platform(tasks):
    """Разделить задачи по платформам на основе тегов"""
    result = {"ios": [], "android": [], "mbe": [], "other": []}

    for task in tasks:
        tag_names = []
        for tag in task.get("tags", []):
            if isinstance(tag, dict):
                tag_names.append(tag.get("name", "").lower())
            else:
                tag_names.append(str(tag).lower())

        if any("ios" in t for t in tag_names):
            result["ios"].append(task)
        elif any("android" in t for t in tag_names):
            result["android"].append(task)
        elif any("mbe" in t or "backend" in t for t in tag_names):
            result["mbe"].append(task)
        else:
            result["other"].append(task)

    return result

Списание времени с fallback по методам API

def add_time_to_task(task_id, hours, date=None, comment=None):
    """
    Добавить время к задаче, перебирая доступные методы API.
    Разные версии EvaTeam поддерживают разные методы.
    """
    from datetime import datetime

    methods = [
        "CmfTimeTrackerHistory.create",
        "CmfTask.addTime",
        "CmfTask.logTime",
        "TimeTracker.create",
        "TimeEntry.create"
    ]

    kwargs = {
        "task": task_id,
        "time_spent": hours,
        "date": date or datetime.now().strftime("%Y-%m-%d"),
    }
    if comment:
        kwargs["comment"] = comment

    for method in methods:
        try:
            return call_api(method, kwargs=kwargs)
        except Exception:
            continue

    raise Exception("Ни один метод API для записи времени не сработал")

Возможные проблемы и решения

Проблема Причина Решение
Токен не найден Нет .env файла или переменной EVA_TOKEN Создайте .env по инструкции. Проверьте расположение (у разных скриптов разные пути)
SSL: CERTIFICATE_VERIFY_FAILED Корпоративный SSL-сертификат Убедитесь, что в коде verify=False. При необходимости установите корневой сертификат
API Error: 401 Невалидный или просроченный токен Сгенерируйте новый токен в настройках EvaTeam
API Error: 403 Недостаточно прав Запросите права на чтение/запись задач у администратора
Загружается мало задач Ограничение пагинации API Используйте find_tasks_v2.py (загрузка по диапазонам кодов)
Дубликаты в результатах Перекрывающиеся диапазоны при загрузке Добавьте дедупликацию (UC-4)
Кодировка в Windows Некорректное отображение кириллицы Скрипты содержат фикс: io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
Не удалось списать время Метод API не поддерживается Скрипт add_time_tracking.py автоматически пробует 5 методов (UC-12)
Нет задач в результатах каскадного поиска Неверный код проекта, даты или имя спринта Проверьте параметры в get_december_tasks(). Попробуйте другие стратегии поиска
CmfTask.count возвращает список вместо числа Разные версии API Обрабатывайте оба случая: if isinstance(result, list): count = len(result)
ConnectionError Нет доступа к серверу EvaTeam Проверьте VPN/корпоративную сеть и EVA_BASE в .env
JSONDecodeError Сервер вернул не-JSON ответ Проверьте EVA_BASE (должен быть без слэша в конце), доступность сервера

Расширение функциональности

Добавление нового скрипта

Шаблон для создания нового скрипта:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Описание скрипта"""

import sys, io
if sys.platform == "win32":
    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
    sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')

import requests, uuid, os
from pathlib import Path
from dotenv import load_dotenv

# Загрузка переменных окружения
load_dotenv(Path(__file__).resolve().parent / '.env')

BASE_URL = os.getenv('EVA_BASE')   # <<< Берётся из .env
TOKEN = os.getenv('EVA_TOKEN')     # <<< Берётся из .env

if not TOKEN:
    print("Токен не найден! Установите EVA_TOKEN в файле .env")
    exit(1)

import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

session = requests.Session()
session.headers.update({
    "Authorization": f"Bearer {TOKEN}",
    "Content-Type": "application/json"
})

def call_api(method, kwargs=None):
    payload = {
        "jsonrpc": "2.2",
        "method": method,
        "callid": str(uuid.uuid4()),
    }
    if kwargs:
        payload["kwargs"] = kwargs
    response = session.post(f"{BASE_URL}/api/?m={method}", json=payload, verify=False)
    result = response.json()
    if result.get("error"):
        print(f"API Error: {result['error']}")
        return []
    return result.get("result", [])

# ========================================
# ВАШ КОД ЗДЕСЬ
# ========================================

if __name__ == "__main__":
    pass

Полезные дополнительные сценарии

  • Массовое добавление тегов — используя метод обновления задачи через API
  • Экспорт задач в Excel — подключить библиотеку openpyxl
  • Отчёты по времени — агрегация списаний по платформам/исполнителям
  • Интеграция с Telegram-ботом — уведомления о задачах без тегов
  • Автоматический запуск по расписанию — через cron (Linux/Mac) или Планировщик задач (Windows)
  • Сравнение списков — найти задачи, которые были/не были в предыдущем выгруженном списке
  • Экспорт в CSV — для загрузки в Google Sheets / Excel
  • Валидация тегов — проверить, что у каждой задачи есть хотя бы один тег из обязательного набора

Краткая шпаргалка

# 1. Создать .env файл
cat > .env << 'EOF'
EVA_BASE=https://eva.your-company.ru
EVA_TOKEN=<<< ВСТАВЬТЕ ВАШ ТОКЕН СЮДА >>>
EOF

# 2. Установить зависимости
pip install requests python-dotenv

# 3. Найти задачи без тега (простая версия)
python find_tasks_simple.py

# 4. Найти задачи без тега (с дедупликацией и диапазонами)
python find_tasks_v2.py

# 5. Найти задачи без тега (полная OOP-версия с деталями)
python find_tasks_without_service_tag.py

# 6. Найти задачи с тегом + ссылки
python find_tasks_with_tag.py

# 7. Списать время — предпросмотр (безопасно, ничего не меняет)
python add_time_tracking.py

# 8. Списать время — применить изменения
python add_time_tracking.py --apply

About

Python scripts for automating EvaTeam task management via JSON-RPC API

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages