Предисловие
В бытность работы аналитиком у меня и моих коллег была практически ежедневная необходимость рассчитывать сроки поставок по доработкам. Задача стояла например такая: рассчитать дату поставки доработки начиная с завтра + 40 рабочих дней. За время работы и руководства отделом аналитики автоматизировать данную функцию руки не дошли, но сейчас решил исправиться, тем более что это замечательный и простой проект, который поможет новичкам ознакомиться с основными конструкциями Python.
Чтобы не откладывать ознакомление с данным модулем просто наберите в командной строке:
pip install prod-cal
Гарантирую что проект будет работать на Python 2.7 и Windows 7, т. к. на этой конфигурации он разрабатывался.
Как собирать пакеты и выкладывать в PyPi я описывать не буду, есть достаточно подробные статьи на эту тему, скажу только что с этой задачей может справиться и новичок, так что если Вы подумывали сделать новый модуль, то не откладывайте это в долгий ящик в этом нет ничего сложного.
Главная цель данной статьи разобрать устройство данного модуля и наметить перспективы для его развития сообществом.
Чтобы не плодить календарей в моём календаре можно использовать все методы стандартного модуля calendar.Calendar.
Состав проекта
После установки проект будет доступен в C:\Python27\Lib\site-packages\prodcal, если вы устанавливали пакет в виртуальное окружение, то ищите его в: <домашний каталог вирт. окружения>\Lib\site-packages\prodcal
Проект можно вообще не устанавливать а скачать его напрямую с сайта PyPi. После чего распаковать и использовать код непосредственно в своём проекте.
Проект состоит из следующих файлов (все с расширением *.py):
- config — описывает информацию о поддерживаемых календарях и о календаре выбранном по умолчанию
- service — файл со вспомогательными функциями, вроде приведения типов и т.п., некоторые функции из этого файла мы разберём ниже
- holidays — файл содержит реализацию основного и пока единственного класса ProdCal
- каталог prodcals — содержит наборы календарей и файл prod_dict, который содержит реализацию класса ProdDict (о нём также ниже)
from procal import ProdCal
my_first_prod_cal = ProdCal()
# Проверяем праздничный день 1 мая
my_first_prod_cal.is_work_day(2016, 5, 1)
# Проверяем рабочий день
my_first_prod_cal.is_work_day(2016, 4, 1)
# Проверяем выходной день
my_first_prod_cal.is_work_day(2016, 4, 2)
# Проверяем перенос празничного дня (рабочий день)
my_first_prod_cal.is_work_day(2016, 2, 20)
# Передаём сразу объект даты
my_first_prod_cal.is_work_day(date(2016, 5, 1)
# Передаём в качестве аргумента строку (today - сегодня)
my_first_prod_cal.is_work_day('today')
# Передаём в качестве аргумента строку (yesterday - вчера)
my_first_prod_cal.is_work_day('yesterday')
# Передаём в качестве аргумента строку (tomorrow - завтра)
my_first_prod_cal.is_work_day('tomorrow')
# Проверяем количество рабочих дней в различных месяцах
my_first_prod_cal.count_work_days([2016, 4, 1], [2016, 4, 30])
my_first_prod_cal.count_work_days([2016, 5, 1], [2016, 5, 31])
my_first_prod_cal.count_work_days([2016, 6, 1], [2016, 6, 30])
# Передаём сразу в формате даты и времени
my_first_prod_cal.count_work_days(date(2016, 4, 1), date(2016, 4, 30))
my_first_prod_cal.count_work_days(date(2016, 5, 1), date(2016, 5, 31))
my_first_prod_cal.count_work_days(date(2016, 6, 1), date(2016, 6, 30))
# Передаём дату начала ввиде текста (today, yesterday, tomorrow)
my_first_prod_cal.count_work_days('today', date(2016, 4, 30))
my_first_prod_cal.count_work_days('yesterday', date(2016, 4, 30))
my_first_prod_cal.count_work_days('tomorrow', date(2016, 4, 30))
# Передаём в качестве конечной даты количество дней от даты начала (включительно)
my_first_prod_cal.count_work_days([2016, 4, 1], 30)
my_first_prod_cal.count_work_days('today', 30)
# Проверяем количество выходных дней в различных месяцах
my_first_prod_cal.count_holidays([2016, 4, 1], [2016, 4, 30])
my_first_prod_cal.count_holidays([2016, 5, 1], [2016, 5, 31])
my_first_prod_cal.count_holidays([2016, 6, 1], [2016, 6, 30])
# Передаём сразу в формате даты и времени
my_first_prod_cal.count_holidays(date(2016, 4, 1), date(2016, 4, 30))
my_first_prod_cal.count_holidays(date(2016, 5, 1), date(2016, 5, 31))
my_first_prod_cal.count_holidays(date(2016, 6, 1), date(2016, 6, 30))
# Передаём дату начала ввиде текста (today, yesterday, tomorrow)
my_first_prod_cal.count_holidays('today', date(2016, 4, 30))
my_first_prod_cal.count_holidays('yesterday', date(2016, 4, 30))
my_first_prod_cal.count_holidays('tomorrow', date(2016, 4, 30))
# Передаём в качестве конечной даты количество дней от даты начала (включительно)
my_first_prod_cal.count_holidays([2016, 4, 1], 30)
my_first_prod_cal.count_holidays('today', 30)
# Рассчитываем конечную дату по рабочим дням
my_first_prod_cal.get_date_by_work_days([2016, 4, 1], 21))
my_first_prod_cal.get_date_by_work_days('today', 21)
Реализация
Структура производственного календаря
Все производственные календари находятся в подкаталоге prodcals в виде отдельных файлов. Формат названия файла соотв. буквенному коду страны по ISO в нижнем регистре. Например, росс. производственный календарь находится в файле ru.py.
Файл содержит два словаря: NON_WORK_DAY_DICT и WORK_DAY_DICT, они имеют одинаковую структуру, первый словарь описывает нерабочие дни (праздничные), а второй описывает переносы рабочих дней на выходные. Словари не содержат указания на «стандартные» нерабочие дни субботу и воскресенье.
Календарь описывают два вложенных словаря: в год вкладываются месяцы, значением месяца является список дней.
Для удобства работы с календарём был сделан отдельный класс ProdDict (унаследован от стандартного словаря) в котором реализован метод is_value, который возвращает True или False в зависимости от наличия в словаре переданного значения. На вход данный класс принимает только даты. Реализация класса ProdDict описана в файле prod_dict (расположен в подкаталоге prodcals).
Реализация класса ProdCal
Данный класс может быть создан и без указания каких-либо аргументов, в этом случае будет использован календарь по умолчанию (российский). Если требуется указать какой календарь использовать, то необходимо передать именованный аргумент locale=<значение>, где значение — это код страны по ISO в любом регистре. Пример для создания производственного календаря Украины:
from prodcal import ProdCal
my_prod_cal = ProdCal(locale='UA')
В настоящий момент поддерживаются календари следующих стран: Беларусь, Грузия, Казахстан, Россия, Украина.
Методы класса ProdCalis_work_day
Вход: дата, список (с int), кортеж аргументов, строка (поддерживает только: 'today', tomorrow', 'yesterday')
Выход: bool
Описание: проверяет заданную дату на предмет того рабочий ли сегодня день.
Примечание: для удобства в этом и всех других методах реализована возможность передавать в качестве аргументов даты в удобном формате, как это реализовано описано в разделе, описывающим сервисные функции.
count_work_days, count_holidays
Вход: дата начала, дата окончания (периода), формат дат описан выше.
Выход: int
Описание: подсчитывает количество рабочих дней в заданном периоде (в случае count_work_days), а в случае count_holidays количество выходных дней.
get_date_by_work_days
Вход: дата начала, int
Выход: date
Описание: вычисляет конечную дату по заданному числу рабочих дней.
Описание сервисных функций
Напомню, что сервисные функции находятся в файле service.py.
Простейшая функция get_date_today преобразует переданное значение в необходимую дату, реализация самая незатейливая (пытливым умам предлагаю переписать под более эффективную конструкцию, например выбор из словаря).
def get_date_today(day):
today = datetime.today().date()
if 'today' == day:
return today
elif 'yesterday' == day:
return today - timedelta(days=1)
elif 'tomorrow' == day:
return today + timedelta(days=1)
raise ValueError('Unknown string format', day)
Магия возможности использования дат в различных форматах (если так корректно выражаться) реализована в функции cast.
Реализация функции castdef cast(start_date, end_date):
if isinstance(start_date, (tuple, list)) and isinstance(end_date, (tuple, list)):
start_date, end_date = date(*start_date), date(*end_date)
if isinstance(start_date, str):
start_date = get_date_today(start_date)
elif isinstance(start_date, (tuple, list)):
start_date = date(*start_date)
if isinstance(end_date, (tuple, list)):
end_date = date(*end_date)
elif isinstance(end_date, int):
end_date = calc_days_by_int(start_date, end_date)
if isinstance(start_date, date) and isinstance(end_date, date):
pass
else:
raise ValueError("Unknown format for parse")
Вся идея очень простая, проверяем тип переданных аргументов и приводим всё к дате и возвращаем её. Если не разобрались бросаем исключение.
Ещё интересным местом является функция get_prodcals, которая по переданному значению подгружает из подкаталога prodcals нужный календарь. Возможность этого обеспечивается с помощью функции import_module() из стандартной библиотеки importlib, которая интерпретирует переданную строку как путь к модулю. Например: import_module('prodcal.prodcals.ru') эквивалентно from prodcals import ru. Главный смысл использования этой функции в том, чтобы не указывать явно какие календари загружать, что несколько облегчает дальнейшую поддержку.
Поддержка новых календарей
Поддержка новых календарей обеспечивается с помощью добавления в файл config.py данных о новых календарях, написании тестов и загрузки календаря в подкаталог prodcals. Кроме этого делать больше ничего не нужно.
Планы на развитие
Если уж взялся за какое-то дело, то нужно решать его глобально: обеспечить поддержку всех производственных календарей в мире.
Также планируется добавить ряд новых функций, например: расчёт даты и времени по переданным часам, написать тесты совместимости с Python3 и поправить некоторые ошибки.
Для русскоязычных пользователей данная статья может выступать в роли документации по модулю, а вот для остальных придётся сделать отдельную документацию.
Для всех кто хотел бы поучаствовать в развитии этого и проекта доступен этот репозиторий.
Благодарность
Помимо меня в этом проекте участвует Аркадий Аристов из Челябинска, за что ему большое спасибо!
is_work_day
Вход: дата, список (с int), кортеж аргументов, строка (поддерживает только: 'today', tomorrow', 'yesterday')
Выход: bool
Описание: проверяет заданную дату на предмет того рабочий ли сегодня день.
Примечание: для удобства в этом и всех других методах реализована возможность передавать в качестве аргументов даты в удобном формате, как это реализовано описано в разделе, описывающим сервисные функции.
count_work_days, count_holidays
Вход: дата начала, дата окончания (периода), формат дат описан выше.
Выход: int
Описание: подсчитывает количество рабочих дней в заданном периоде (в случае count_work_days), а в случае count_holidays количество выходных дней.
get_date_by_work_days
Вход: дата начала, int
Выход: date
Описание: вычисляет конечную дату по заданному числу рабочих дней.
Описание сервисных функций
Напомню, что сервисные функции находятся в файле service.py.
Простейшая функция get_date_today преобразует переданное значение в необходимую дату, реализация самая незатейливая (пытливым умам предлагаю переписать под более эффективную конструкцию, например выбор из словаря).
def get_date_today(day):
today = datetime.today().date()
if 'today' == day:
return today
elif 'yesterday' == day:
return today - timedelta(days=1)
elif 'tomorrow' == day:
return today + timedelta(days=1)
raise ValueError('Unknown string format', day)
Магия возможности использования дат в различных форматах (если так корректно выражаться) реализована в функции cast.
def cast(start_date, end_date):
if isinstance(start_date, (tuple, list)) and isinstance(end_date, (tuple, list)):
start_date, end_date = date(*start_date), date(*end_date)
if isinstance(start_date, str):
start_date = get_date_today(start_date)
elif isinstance(start_date, (tuple, list)):
start_date = date(*start_date)
if isinstance(end_date, (tuple, list)):
end_date = date(*end_date)
elif isinstance(end_date, int):
end_date = calc_days_by_int(start_date, end_date)
if isinstance(start_date, date) and isinstance(end_date, date):
pass
else:
raise ValueError("Unknown format for parse")
Вся идея очень простая, проверяем тип переданных аргументов и приводим всё к дате и возвращаем её. Если не разобрались бросаем исключение.
Ещё интересным местом является функция get_prodcals, которая по переданному значению подгружает из подкаталога prodcals нужный календарь. Возможность этого обеспечивается с помощью функции import_module() из стандартной библиотеки importlib, которая интерпретирует переданную строку как путь к модулю. Например: import_module('prodcal.prodcals.ru') эквивалентно from prodcals import ru. Главный смысл использования этой функции в том, чтобы не указывать явно какие календари загружать, что несколько облегчает дальнейшую поддержку.
Поддержка новых календарей
Поддержка новых календарей обеспечивается с помощью добавления в файл config.py данных о новых календарях, написании тестов и загрузки календаря в подкаталог prodcals. Кроме этого делать больше ничего не нужно.
Планы на развитие
Если уж взялся за какое-то дело, то нужно решать его глобально: обеспечить поддержку всех производственных календарей в мире.
Также планируется добавить ряд новых функций, например: расчёт даты и времени по переданным часам, написать тесты совместимости с Python3 и поправить некоторые ошибки.
Для русскоязычных пользователей данная статья может выступать в роли документации по модулю, а вот для остальных придётся сделать отдельную документацию.
Для всех кто хотел бы поучаствовать в развитии этого и проекта доступен этот репозиторий.
Благодарность
Помимо меня в этом проекте участвует Аркадий Аристов из Челябинска, за что ему большое спасибо!
Комментарии (31)
alexws54tk
06.04.2016 18:50+2Поддержу предыдущего оратора по поводу более доступной платформы.
Файл содержит два словаря: NON_WORK_DAY_DICT и WORK_DAY_DICT, они имеют одинаковую структуру, первый словарь описывает нерабочие дни (праздничные), а второй описывает переносы рабочих дней на выходные.
Из какого источника берутся «дефолтные» сведения о праздниках и прочих „выходных“?balamut108
06.04.2016 18:51-2Выходные дни берутся из claendar с помощью метода weekday, а что касается самих данных о празниках, то в интернете полно об этом информации.
alexws54tk
06.04.2016 18:54+2Тоесть по дефолту надо самому «гуглить» праздники на конкретный год в конкретной стране?
balamut108
06.04.2016 21:15-3Да, верно. Потом готовить из них словарь и далее по тем пунктам что я описал, если бы был доступный способ получать актуальные данные смысла бы в данной библиотеке никакого не было.
bromzh
07.04.2016 04:08+2если бы был доступный способ получать актуальные данные смысла бы в данной библиотеке никакого не было.
Когда у меня стояла кубунта 14.04 в ней был виджет календаря, в котором показывались выходные дни (включая праздники) и всякие невыходные праздники, типа дня эколога и др. Откуда-то они берут данные. Увы, в свежей версии (15.10) всё похерили, виджет перестал показывать такое.
Но в целом, я за пару минут гугления нашёл производственный календарь с апи и вот такую штуку.
Я бы сделал так: завёл отдельный репозиторий с данными по странам с их календарями. Достаточно написать скрипт, который бы при запуске качал json с данными или парсил бы сайт с продкалендарями, перегонял бы данные в нужный формат, и обновлял бы модуль в pypi. Версионирование сделать по типу pytz: первое число — год, второе — номер фикса. Добавилась новая страна — увеличиваем вторую цифру. Добавился календарь на следующий год — увеличиваем первую.
В основном же пакете добавить зависимость от вышеописанного пакета с данными и оставить только логику по работе с датами. Таким образом, можно развивать сам календарь независимо от данных локали. И наоборот, фиксить данные календарей по странам можно не затрагивая пакет с логикой.nuklea
07.04.2016 07:22Спасибо за basicdata.ru/api/calend. Давно искал что-то подобное для планирования расходов.
igrishaev
06.04.2016 20:27+4Почему репозиторий требует авторизации? Почему код не в гитхабе? Что-то сделали, а посмотреть нельзя.
balamut108
06.04.2016 21:14-8Иван, потому доступ в этот репозиторий не для всех. Как показывает опыт от кода не гитхабе толка нет, так что решил сделать формат закрытого сообщества (в первую очередь по тому что для тех кому это нужно и интересно не будет являться препядствием личная коммуникация), посмотреть сам код можно и на PyPi и после установки, этому ничто не мешает.
SirEdvin
06.04.2016 21:17+1Как показывает, закрытая платформа отпугнет 90% людей, которые хотя бы глянули бы код. Если бы альтернатив не было бы, то это сработало бы, наверное, а так...
balamut108
06.04.2016 21:20-6Посмотреть сам код можно и на PyPi и после установки, этому ничто не мешает.
Если бы Вы действительно хотели глянуть, то наверно бы глянули, а не тратили время подобные комментарии.vsapronov
07.04.2016 02:58Вы же понимаете, что:
1. Доступ на запись к репозиторию вашего проекта в GitHub'е тоже не для всех.
2. Доступ на чтение есть и на GitHub и через PyPi.
3. Вы вообще пользовались GitHub'ом? Вы понимаете, что все патчи, которые вам не понравятся вы сможете не принимать?
4. Если вы даете исходный код, то, наверное, имеет смысл сделать максимально удобным для всех способом. Какой это способ?
SirEdvin
06.04.2016 21:15+1workdays? workcalendar? Чем они не подошли?
balamut108
06.04.2016 21:21-2Видимо Вы прочитали статью по диагонали и не поняли основной идеи этого модуля.
SirEdvin
06.04.2016 21:29Я не понял, почему в качестве ядра не был выбран один из этих моделей. Была ли на то причина?
sledopit
07.04.2016 00:59+4Но ведь есть dateutil.
Возможности dateutil гораздо шире. И сообщество вокруг проекта немаленькое (33 разработчика, если верить гитхабу).
Там можно не только сформировать расписание выходных/рабочих дней (у вас всё равно это руками фактически задаётся, поэтому тут всё аналогично можно сделать), но и прикрутить туда рабочие часы и высчитать, например, +3 часа к текущему моменту с учётом рабочего времени и выходных.
А также узнать ближайшую рабочую пятницу или первый понедельник следующего месяца и кучу других интересных вещей.balamut108
07.04.2016 04:15Сегодня 6-й день существования проекта не судите строго, что касается других функций, то они будут добавлены по мере развития + в стандартном календаре это есть.
bromzh
Сам код написан довольно весьма плохо:
В каждом файле стоит shebang, зачем?.. А так:
делать вообще плохо.
А помочь даже при желании нельзя: репозиторий недоступен. Выкладывайте на гитхаб/битбакет.
balamut108
Плохо писать и не указывать что плохо.
На главной странице репозитория указано как принять участие.
bromzh
Так я написал что плохо. Могу подробнее:
path = self[day.year][day.month]
— тоже плохо. Потому чтоProdCal().is_work_day(2015, 2, 2)
выдастKeyError: 2015
, а должен что-то другоеProdCal().is_work_day(u'tomorrow')
выдастAttributeError: 'NoneType' object has no attribute 'year'
По приведённой ссылке я увидел вот что:
Т.е. мне нужно пройти собеседование, чтобы как-то помочь? Спасибо, не надо.
Почему бы просто не завести репу в гитхабе? Там есть пулл-реквесты, issues, etc.
balamut108
Спасибо за замечания я их учту в ближайшее время. Что касается формата работы с репозиторием, то как показывает практика для тех кому это интересно и нужно это не является препятствием, т.е. учитывая Ваши компетенции и общий тон комментариев, я уверен практически на 100 что Вы бы в этом проекте не участвовали. Если я не прав, то Вы знаете как со мной связаться.
veveve
Зря вы так остро реагируете на критику. Человек ответил вам достаточно вежливо, комментарий о недостатках кода дал по делу. Обижаться тут не на что, на мой взгляд.
bromzh
Как показывает практика, если бы человек действительно хотел бы развивать проект и вовлечь в него людей, то он бы выложил код в публичный репозиторий. А пока что я вижу в этом посте лишь попытку порекламировать ваш сайт с обучающими курсами, ведь ссылка в статье ведёт нас именно на ваш сайт. Может быть я не прав и ищу скрытый смысл там, где его нет.
Компетенциями давайте не будем меряться. Я на питоне не писал уже давно, может подзабыл что-то. А вот от человека, который пишет на питоне 10 лет и учит ему за деньги других людей ожидаешь как минимум наличия юнит тестов и умения писать библиотеки под обе версии языка. Тем более, что с помощью six в таком некрупном проекте это делается тривиально.
И да, я бы не участвовал в проекте: просто в ближайшем будущем такая библиотека мне точно не пригодятся. А пару issue я уже отписал, но пока только в комменте, так как в вашем репозитории на гитхабе пусто.
К слову, если вы действительно намерены развивать проект: гитхаб же хорош не только сам по себе, но он ещё ценен сервисам с ним интегрирующимися. Например, есть сервис непрерывной интеграции, который может бесплатно запустить задачи для проекта, который хостится на гитхабе. Плюс, травис умеет интегрироваться с tox. Так что при наличии тестов можно было бы при каждом коммите (и пуше в гитхаб) сразу прогонять их на разных версиях языка и автоматически получать отчёт из трависа, что намного бы упростило дальнейшую разработку для всех участвующих.
balamut108
Я обновил ссылку на ГитХаб, идея как раз в развитии, а не в том чтобы сделать готовый продукт и представить его на суд читателей. За 6 дней проекта как мне кажется сделали не мало. До CI тоже руки дойдут. Ваши замечания учёл в течение получаса выложу их. Спасибо.
bziker
Я использую питончик время от времени в довольно простых скриптах, многого не знаю, поясни пожалуйста, почему это плохо?