Простой тайм-трекер на Python 3

С проблемой учета затраченного на работу времени для отчета перед заказчиком знаком каждый фрилансер. Да и не только он: как правило, в мало-мальски серьезных IT-конторах контроль сотрудников на предмет их занятости в рабочее время производится в обязательном порядке.

В конторе, конечно же, проблемой для непосредственного исполнителя это не является - решение уже есть, и нас просто обязывают его использовать. То же касается и фриланс-бирж. Практически на каждой крупной механизм тайм-трекинга так или иначе реализован. Другое дело, когда вы не любите биржи и работу в офисе с печеньками так же, как не люблю их я, но дух фриланса вам идейно близок. Тут уж вся сладость ответственности за организацию своего рабочего процесса полностью накладывается на нас самих.

В свое время вопрос "учета для отчета" встал передо мной неожиданно. И потребовал себя решить в максимально короткие сроки. Когда ничего из найденных готовых решений меня не обрадовало, а надо было вот уже вчера, ничего не оставалось, как сесть и написать свое. Конечно же на Python. Забавно, что тогда  был проект на PHP. Хотя и не забавно может, не на PHP ж скрипты писать. Прикольно другое: написанный тогда в спешке "на коленке" как одноразовое решение простецкий тайм-трекер используется мною до сих пор и достаточно часто. Несмотря на то, что использование подобных инструментов предполагает достаточно высокую степень доверия со стороны клиента.

Функционал и использование

Концепции

  • Скрипт предполагает работу внутри git-репозитория. Т.е., если ваш проект гитом не инициализирован, то работать скрипт вежливо откажется.
  • Файл с тайм-треком используется для всего проекта один единственный. Хранится будет в корневой директории вашего проекта. Корневой с точки зрения git`а (читайте параграф выше).
  • Данные в файл пишутся в формате CSV:
DateStartTimeEndTimeCommentHours
число мес. год
Время начала временного периода
Время окончания
КомментарийОтработанное время (в часах)


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

$ tt Minutes Сomments

Где:

  • tt - у  меня альяс вызова скрипта;
  • Minutes - количество отработанных за текущий логируемый отчетный период минут;
  • Comments - комментарии к текущему отчетному периоду (можно не указывать, тогда будут подставлены "по-умолчательные");

Если нужна статистика по уже наработанному/заработанному, то вызываем скрипт с опцией -s

$ tt -s

Код скрипта

Подключаем нужные модули и задаем константы с глобальными настройками.

#!/usr/bin/env python3
  
import sys
import time
import os
import csv
from subprocess import check_output, CalledProcessError
from functools import lru_cache
  
  
# Задаем константы для имени файла с треком,
# почасового рейта и валюту расчета.
FILE_NAME = 'timetracking.csv'
HOUR_RATE = 1000
CURRENCY = 'RUB'

Пишем функцию обработки введенных аргументом минут.

def getOffset():
    '''
    Валидируем введенное значение отработанных минут.
    Если не введены - просим предоставить.
    '''
    try:
        minutes = sys.argv[1]
        if not minutes.isdigit():
            raise IndexError
        else:
            minutes = int(minutes)
    except IndexError:
        while True:
            try:
                minutes = int(
                    input("Нужно задать количество отработанных минут: "))
                break
            except ValueError:
                print("Оп-п-па! Введена ни разу не цифра. Пробуем заново...")
    return minutes

Получаем корневой каталог.

# Проверяем, что мы внутри гит-репозитория,
# и если да, то вычисляем и отдаем путь к корневой директории репозитория.
# С кэшем (ltu_cache) отработать должно быстрее.
@lru_cache(maxsize=1)
def getGitRoot():
    ''' Возвращаем абсолютный путь к корневой директории гит-репозитория '''
    try:
        base = check_output('git rev-parse --show-toplevel', shell=True)
    except CalledProcessError:
        raise IOError(
            'Текущая директория не является гит-репозиторием!\nДосвидания.')
    return base.decode('utf-8').strip()

Функция для получения статистики на основе данных в файле учета времени.

def getStats(filename, col_index=4):
    '''
    Получаем статистику по затраченному времени и 
    заработанному лаве.
    '''
    try:
        with open(filename, 'r') as f:
            reader = csv.reader(f)
            headerline = next(reader)
            hours = 0
            for row in reader:
                hours += float(row[col_index])
            sum = hours * HOUR_RATE
            # "Копейки" не учитываем.
            print('Затрачено {} часов | Заработано {} {}'.format(
                round(hours, 2), int(sum), CURRENCY))
    except FileNotFoundError:
        sys.exit('Файл с треком не найден!')

Точка входа.

def main():
    # Проверяем сперва в репозитории ли мы у Гита.
    # Если да - получаем путь к корневой папке репозитория.
    try:
        rootDir = getGitRoot()
        filename = os.path.join(rootDir, FILE_NAME)
    except OSError as err:
        # Если это не гит-репозиторий, то выходим нафиг.
        message = "OS error: {0}".format(err)
        sys.exit(message)
  
    # Если первым аргументом скрипта '-s', то возвращаем статистику
    # по отработанному и выходим.
    try:
        if sys.argv[1] == '-s':
            return getStats(filename)
    except IndexError:
        pass
  
    # Получаем колл-во отработанных минут
    # и дату/время начала текущего рабочего периода.
    offset = getOffset()
    startDateTime = time.localtime(time.time() - (offset * 60))
  
    # Подготавливаем данные для записи в файл.
    logDate = time.strftime('%d %b %Y', startDateTime)
    logStartTime = time.strftime('%H:%M', startDateTime)
    logEndTime = time.strftime('%H:%M', time.localtime())
    # Комментарий к записи о проделанной работе.
    # Если не введен аргументом - пишем дефолтный.
    logComment = 'См. коммит.' if len(sys.argv) < 3 \
        else '"' + ' '.join(sys.argv[2:]) + '"'
    logHours = '%.1f' % (offset / 60)
  
    data = [logDate, logStartTime, logEndTime, logComment, logHours]
    new = os.path.isfile(filename)
    
    with open(filename, 'a+') as f:
        if not new:
            f.write('Date,Start,End,Comment,Hour(s)\n')
        f.write(','.join(data) + '\n')
if __name__ == '__main__':
    main()

Сорцы на GitHub.