U
    [Se                     @   s   d dl Zd dlZd dlmZ d dlmZmZmZm	Z	m
Z
 d dlZd dlT d dlmZ d dlmZmZ ejZejZejZG dd dejZejej dS )	    N)	lru_cache)IteratorListNoReturnTupleUnion)*)cmf_calendar)MOrelativedeltac                       sV  e Zd Zeeeee dddZedGeeeeedddZedHeeeee	e d	d
dZ
edIeeeee	e e	e dddZedJeeeeeeee	e dddZedKeeeeeeee	e dddZedLeeeeef dddZedMeeedddZedeee	e f dddZed eeddd ZedNd eeeed"d#d$ZeedOeed ejjf eeed&d'd(Z edPd eeeeef ed)d*d+Z!dQeeed-d.d/Z"ed eeed0d1d2Z#edRd eeed3d4d5Z$d6d7 Z%ee&j'd!d!d8d9d: Z(ed;d< Z)d=d> Z*d?d@ Z+dAdB Z, fdCdDZ- fdEdFZ.  Z/S )SCmfCalendar)
date_startdate_endreturnc                 c   s0   t t||  jd D ]}| t| V  qdS )u   
        Генератор диапазона дат

        Args:
            date_start (Date): начальная дата
            date_end (Date): конечная дата

        Yields:
            Iterator[Date]: дата
           N)rangeintdaysdatetime	timedelta)r   r   i r   ./common/models/cmf_calendar.py_date_range   s    zCmfCalendar._date_rangeN)date	month_daypositionday_of_weekr   c           	         sz   |r2t  j jd }||k r$|} j|d}nDt  }| j j} fdd|D }|dk rn|| n|d }|S )u:  
        Вспомогательный метод для every_month и every_year
        Возвращает дату по указанному дню месяца или по позиции дня недели месяца

        Args:
            date (Date): дата
            month_day (int, optional): день месяца (1-31). Defaults to None.
            position (int, optional): позиция недели месяца. Defaults to None.
                                      (0-первая, 1-вторая, 2-третья, 3-четвертая, 4-последняя)
            day_of_week (int, optional): день недели. Defaults to None.
                                         (0-ПН, 1-ВТ, 2-СР, 3-ЧТ ,4-ПТ ,5-СБ, 6-ВС)

        Returns:
            Date: дата
        r   )dayc                    s$   g | ]}| j  j kr| qS r   )month).0Zweekr   r   r   r   
<listcomp>:   s      z=CmfCalendar._get_day_by_position_in_month.<locals>.<listcomp>   )clZ
monthrangeyearr   replaceZCalendarZmonthdatescalendar)	r   r   r   r   Zdays_in_monthr   calendarweeksZday_of_weeksr   r!   r   _get_day_by_position_in_month    s    z)CmfCalendar._get_day_by_position_in_monthr   )
start_dateend_daterepeatsperiodr   c                 C   sR   g }|dk	rd}| }|rN|dkr*|d8 }n
||kr4qN| | |t|d7 }q|S )u  
        Возвращает список дат для типа повторения "Ежедневно"

        Args:
            start_date (Date): начальная дата
            end_date (Date, optional): конечная дата. Defaults to None.
            repeats (int, optional): количество повторений. Defaults to None.
            period (int, optional): периодичность (каждый N день). Defaults to 1.

            Обязателен один из двух аргументов, либо end_date, либо repeats

        Returns:
            List[Date]: список дат
        NTr   r   )appendr   )r+   r,   r-   r.   r   current_dater   r   r   	every_day?   s    

zCmfCalendar.every_day)r+   r,   r-   r.   weekdaysr   c           	      C   s   g }|dk	rd}| t tdd }|r|D ]N}|t |d }|| krp|dkrV|d8 }n||krfd} qz|| |s* qzq*|t |d7 }q"|S )u]  
        Возвращает список дат для типа повторения "Еженедельно"

        Args:
            start_date (Date): начальная дата
            end_date (Date, optional): конечная дата. Defaults to None.
            repeats (int, optional): количество повторений. Defaults to None.
            period (int, optional): периодичность (каждая N неделя). Defaults to 1.
            weekdays (List[int], optional): список дней недели. Defaults to None.
                                            (0-ПН, 1-ВТ, 2-СР, 3-ЧТ ,4-ПТ ,5-СБ, 6-ВС)

            Обязателен один из двух аргументов, либо end_date, либо repeats

        Returns:
            List[Date]: список дат
        NTr$   weekdayr   F)r)   )r   r
   r0   )	r+   r,   r-   r.   r3   r   r1   r5   r   r   r   r   
every_weeka   s$    

zCmfCalendar.every_week)r+   r,   r-   r.   r   r   r   r   c                 C   s   g }|dk	rd}| }|rt j||||d}	|	| krX|dkrD|d8 }n
|	|krNq||	 |jd | }
|j|
d  }|
d d }
|j||
d}q|S )u  
        Возвращает список дат для типа повторения "Ежемесячно"
        Расчитывает даты в двух режимах:
            1 - по указанному дню, если установлен параметр month_day
            2 - по позиции дня недели, если установлены параметры position и day_of_week

        Args:
            start_date (Date): начальная дата
            end_date (Date, optional): конечная дата. Defaults to None.
            repeats (int, optional): количество повторений. Defaults to None.
            period (int, optional): периодичность (каждый N месяц). Defaults to 1.
            month_day (int, optional): день месяца (1-31). Defaults to None.
            position (int, optional): позиция недели месяца. Defaults to None.
                                      (0-первая, 1-вторая, 2-третья, 3-четвертая, 4-последняя)
            day_of_week (int, optional): день недели. Defaults to None.
                                         (0-ПН, 1-ВТ, 2-СР, 3-ЧТ ,4-ПТ ,5-СБ, 6-ВС)

            Обязателен один из двух аргументов, либо end_date, либо repeats

        Returns:
            List[Date]: список дат
        NTr   r   r   r      )r&   r   )r   r*   r0   r   r&   r'   )r+   r,   r-   r.   r   r   r   r   r1   r   r   r&   r   r   r   every_month   s(     

zCmfCalendar.every_month)r+   r,   r-   r   r   r   r   r   c           
      C   sz   g }|dk	rd}| j |dd}|rvtj||||d}	|	| krb|dkrN|d8 }n
|	|krXqv||	 |j |jd d}q|S )u  
        Возвращает список дат для типа повторения "Ежегодно"
        Расчитывает даты в двух режимах:
            1 - по указанному дню, если установлены параметры month и month_day
            2 - по позиции дня недели месяца, если установлены параметры month, position и day_of_week

        Args:
            start_date (Date): начальная дата
            end_date (Date, optional): конечная дата. Defaults to None.
            repeats (int, optional): количество повторений. Defaults to None.
            month (int, optional): месяц (1-Янв, 2-Фев, ..., 12-Дек). Defaults to None.
            month_day (int, optional): день месяца (1-31). Defaults to None.
            position (int, optional): позиция недели месяца. Defaults to None.
                                      (0-первая, 1-вторая, 2-третья, 3-четвертая, 4-последняя)
            day_of_week (int, optional): день недели. Defaults to None.
                                         (0-ПН, 1-ВТ, 2-СР, 3-ЧТ ,4-ПТ ,5-СБ, 6-ВС)

            Обязателен один из двух аргументов, либо end_date, либо repeats

        Returns:
            List[Date]: _description_
        NTr   r   r   r7   )r&   )r'   r   r*   r0   r&   )
r+   r,   r-   r   r   r   r   r   r1   r   r   r   r   
every_year   s"     

zCmfCalendar.every_year)	from_timeto_timer   c                 C   sf   t j }t j || } t j ||}t dd| krJ|t jdd7 }| |kr^tddd | |fS )u  
        Нормализует интервал времени и проверяет его на корректность.
        Так как у datetime нет времени 24:00,
        то время окончания 00:00 конвертируется в 00:00 следующего дня,
        как будто это интервал, например, 18:00 - 24:00

        Args:
            from_time (Time, optional): время начала интервала. Defaults to None.
            to_time (Time, optional): время окончания интервала. Defaults to None.

        Returns:
            Tuple[Datetime, Datetime]: дата и время начала, дата и время окончания
        r      )Zhoursuo   Время начала должно быть раньше времени окончания интервалаTabort)r   r   todaycombinetimer   	cmf_alert)r<   r=   rA   r   r   r   normalize_time_interval   s    
z#CmfCalendar.normalize_time_intervalc                 C   s.   | r|sdS t j| |\} }||   d S )u?  
        Вычисляет в минутах интервал времени

        Args:
            from_time (Time, optional): время начала. Defaults to None.
            to_time (Time, optional): время окончания. Defaults to None.

        Returns:
            int: минуты
        r   <   )modelsr   rE   total_seconds)r<   r=   r   r   r   get_interval_minutes  s    z CmfCalendar.get_interval_minuteszmodels.CmfCalendarExclude)	exclusionr   c           	         sb  g }d} j j} jj} j} j} jdkrN jdkrNd}| |||}n jdkrvd}| j||| j j	d}n jdkrd	}| j||| j j
 jd
}n jdkrd}| j|||| j	d}n jdkrd}| j|||| j
 jd}nh jdkr.d} fddtdD }| j|||||d}n, jdkrZ jdkrZd}| ||||}||fS )uw  
        Расчитывает и возвращает исключительные дни и приоритет исключения
        Приоритеты исключений (7-самый высокий, 1-самый низкий):
            7 - Ежедневно (каждый день)
            6 - Ежегодно (в указанный день)
            5 - Ежегодно (по позиции дня недели месяца)
            4 - Ежемесячно (в указанный день)
            3 - Ежемесячно (по позиции дня недели)
            2 - Еженедельно
            1 - Ежедневно (каждый N день)

        Args:
            exclusion (CmfCalendarExclude): исключение

        Returns:
            Tuple[int, List[Date]]: приоритет, список дат
        Nr2   r      Zevery_year_day   )r   r   Zevery_year_week_day   )r   r   r   Zevery_month_dayr#   )r   Zevery_month_week_day   )r   r   r6      c                    s"   g | ]}t  d | dr|qS )r   F)getattr)r    r   rJ   r   r   r"   X  s      z2CmfCalendar.calc_excluded_days.<locals>.<listcomp>)r3   )period_start_datevalueperiod_end_dater.   Zrepeat_timesrepeat_typer2   r;   r   r   Zmonth_week_positionZmonth_day_weekr9   r   r6   )	clsrJ   r   priorityr+   r,   r.   r-   r3   r   rQ   r   calc_excluded_days  sT    

 



 
zCmfCalendar.calc_excluded_days)r(   r   r   c                    s  d}g }|   d  }tjj|ddgdddgdd|gd	d
|ggd}d}|D ]4}| |\}	}
|	dkrlqP||
krP|	|krP|	}|}qP|r|jj}|jj}|dkr|dd |j	D  ntj
j|ddgdd|gd	d
|ggd}|rt|| djdkr|ddg |j}t|| dj}t|| dj}|dkrX| fdd|j	D  tjj||d}|stj||d} |_|j|_|j|_|j|_||_||_||_|  |S )u   
        Пересчет указанного дня календаря

        Args:
            calendar (CmfCalendar): объект календаря
            date (Date): дата

        Returns:
            NoReturn
        Nr   r   intervals.*rU   !=rR   <=rT   >=parentfieldsfilterr   workc                 s   s*   | ]"}|j jd |jjd gV  qdS %H:%MNr<   rS   strftimer=   r    intervalr   r   r   	<genexpr>  s   z*CmfCalendar.recalc_date.<locals>.<genexpr>_typedefaultdefault_workweek.*default_workweek.intervals.*_intervals_total_minutesc                 3   s6   | ].}|j j kr|jjd |jjd gV  qdS rb   day_weekrS   r<   re   r=   rf   r4   r   r   rh     s    r^   r   )r5   rG   CmfCalendarExcludelistrX   exclude_typerS   intervals_total_minutesextend	intervalsCmfCalendarWorkWeekgetrP   load_fieldsdefault_workweekCmfCalendarDayro   r&   r   r   day_typeinterval_total_minutesinterval_jsonsave)rV   r(   r   r   rv   day_num
exclusionsZexclusion_priorityrJ   rW   Zexcluded_daysr|   r}   calendar_dayr   r4   r   recalc_datea  sj    




zCmfCalendar.recalc_dateT)r(   r   r   back_recalcr   c                 C   sh   |dkr$t j   }|jddd}|dkrF|tdd t jdd }| ||D ]}| || qRdS )u$  
        Пересчет дней календаря
        Если интервал не задан, делает пересчет от начала текущего года +2 года, итого 3 года

        Args:
            calendar (CmfCalendar): объект календаря
            date_start (Date, optional): начальная дата
            date_end (Date, optional): конечная дата
            back_recalc (bool, optional): пересчет назад только текущего года
        Nr   r:   rN   )Zyearsr/   )r   rA   r   r'   r   r   r   r   )rV   r(   r   r   r   rA   r   r   r   r   recalc_calendar_day  s    zCmfCalendar.recalc_calendar_dayF)r(   r   detailr   c                 C   s~   t |d}tjjdd|gdd|ggdddgd}t j|d	gd
}|sdtd|dd|j ddd |jj	|j
j	|jj	i dS )u<  
        Получает день календаря

        Args:
            calendar (Union[str, CmfCalendar, CmfType]): календарь
            date (Date): дата
            detail (bool, optional):

        Returns:
            day (dict): информация о дне календаря
        r   Z	parent_id=r   r|   r}   r~   r`   r_   dirtyr_   u	   День z%d.%m.%Yu*    не найден в календаре "uO   ". Попробуйте запустить пересчет календаря.Tr?   )typerv   rt   r   )ZcmfutilZget_obj_id_by_anyrG   r{   rx   Zget_obj_by_idrD   namer|   rS   r~   r}   )rV   r(   r   r   Zcalendar_idr   r   r   r   get_day  s    zCmfCalendar.get_day)r(   r&   r   c                    s   |dkrt j  j}nt|t js.t|t j r4|j}t t|dd}t t|dd}t }|}||kr| |j|j|j	ddg d||< |t j
dd7 }qb|dd	g |j}tjj|d
dgddd|gdd|ggdd|gdd|ggdd|gdd|gggd}| D ]r\}	}
|	 d }t|| dj}t|| dj}||
d< ||
d< |dkrfdd|jD |
d< q|D ]}|jj}||jkrd}g }| d }t|| dj}|dkr|t j
dd7 }q|dkrt|| dj}fdd|jD }||}|r:||d< ||d< ||d< |t j
dd7 }qqtjj|d
dgdddgddd|gdd|ggdd|gdd|ggdd|gdd|ggggd} fdd|D }t|D ]r\}}}d}g }|jj}|dkr |jj}d d |jD }|D ].}	||	}|r||d< ||d< ||d< qqtjj||d
gd!}|D ]`}||jjd}
|
rP|
dg   |
 D ]\}}t||| q|jrP|j d"d# qP| D ]@\}	}
tj||	d$}|
 D ]\}}t||| q|   qdS )%uQ  
        Расчитывает все дни года с учетом рабочих недель и исключений и сохраняет их в БД

        Args:
            calendar (CmfCalendar): объект календаря
            year (Union[str, int, Date, Datetime], optional): год. Defaults to None.
        Nr   r8      r   )ro   r&   r   r   r|   r}   r~   r/   rk   rl   r   rY   ORrR   r\   r[   rT   r]   r   ri   rm   r|   r}   ra   c                    s4   g | ],}|j j kr|jjd |jjd gqS rc   rn   rf   r4   r   r   r"   #  s    z-CmfCalendar.calc_one_year.<locals>.<listcomp>r~   rj   c                    s4   g | ],}|j j kr|jjd |jjd gqS r   rn   rf   r4   r   r   r"   9  s    rU   rZ   c                    s"   g | ]}|j r ||f qS r   )rU   rX   )r    rJ   rV   r   r   r"   V  s   c                 S   s(   g | ] }|j jd |jjd gqS r   rd   rf   r   r   r   r"   _  s   )r^   r&   r_   TZ	only_datarp   )!r   nowr&   
isinstancer   r   dictr5   r   r   r   ry   rz   rG   rw   rr   itemsrP   rS   rv   rR   rT   rx   rq   sortedrs   rt   r{   popsortsetattr
is_changedr   )rV   r(   r&   Zstart_date_yearZend_date_yearZdays_of_yearr1   rz   	workweeksr   Zday_datar   r|   r}   workweekZcurrent_period_daterv   Zday_of_yearr   Zexcluded_dates_ZdatesrJ   Zcalendar_daysr   Z
field_namerS   r   )rV   r5   r   calc_one_year  s    

	










zCmfCalendar.calc_one_yearr   )
back_yearsforcer   c                 C   s   |dk s|dkrt ddd | ddg | js8|s8dS tj j}|rP|| n|}|t| j }t||D ]&}t	j
| | tt|d	d
| _qlt	j
j  d| _| jdd dS )u  
        Пересчитывает дни календаря.
        Пересчет начинается от текущего года вперед на количество лет,
        указанных в параметре calc_num_years календаря.
        Если в параметре back_years указано положительное число, то пересчет начнется с прошлых лет.
        Например:
            back_years = 3
            calc_num_years = 5
            текущий год - 2023
            Будут пересчитаны года с 2020 по 2027 включительно

        Args:
            force (bool, optional): принудительный пересчет дней календаря. Defaults to False.
            back_years (int, optional): количество прошлых лет. Defaults to 0.
        r   
   u@   Параметр back_years должен быть от 0 до 10Tr?   r   calc_num_yearsNr8   r   Fr   )rD   ry   r   r   r   r&   r   r   r   rG   r   r   r   Z
calc_untilr   cache_clearr   )selfr   r   Zcurrent_yearZ	from_yearZto_yearr&   r   r   r   recalc_calendar  s    
zCmfCalendar.recalc_calendar)r(   from_dtto_dtr   c                 C   s  dd }d}t t jt|jpdd}| }| }||kr| ||}	|	d dkrl|t jdd7 }q8|	d	 D ]}
t j |
d d
 }t j |
d d
 }t j j|||d}t j j|||d}| t ddkr|t jdd7 }|||||}||7 }qt|t jdd7 }q8|S )u  
        Получает продолжительность рабочего времени в минутах из указанного диапазона дат

        Args:
            calendar (CmfCalendar): объект календаря
            from_dt (datetime): дата и время начала диапазона
            to_dt (datetime): дата и время конца диапазона

        Returns:
            minutes_sum (int): сумма минут
        c                 S   s8   t | |}t||}||k r0t||  d S dS d S )NrF   r   )maxminr   rH   )r   r   interval_startinterval_endstartendr   r   r   	intersect  s
    

z3CmfCalendar.get_duration_minutes.<locals>.intersectr   Zsecondsr   ra   r   r/   rv   rc   tzinfo)	r   timezoner   r   r   r   strptimerC   rB   )rV   r(   r   r   r   Zminutes_sumtzr1   r,   r   rg   r<   r=   r   r   Zinterr   r   r   get_duration_minutes  s*    	

z CmfCalendar.get_duration_minutes)r(   r   durationr   c                 C   s  |s|S t t jt|jpdd}d}|dk r<d}t|}| }|dkrl| ||}|sf|d ntt|d }|D ]}	t j 	|	d d
 }
t j 	|	d d
 }t j j||
|d}t j j|||d}|
 t 
ddkr|t jdd	7 }|st||n|}|rt||n|}|||k r:t||  d
 nd8 }|dkrz qPqz|t j|s`dndd	7 }qD|r|t jt|d }n|t jt|d }|S )u$  
        Рассчитывает дату и время на основе длительности от указанной даты и времени

        Args:
            calendar (CmfCalendar): объект календаря
            from_dt (Datetime): дата и время от которой производится расчет
            duration (int, optional): длительность в минутах. Defaults to 0.

        Returns:
            to_dt (Datetime): рассчитанная дата и время
        r   r   FTrv   rc   r   r   r/   rF   r$   )Zminutes)r   r   r   r   absr   r   rr   reversedr   rC   rB   r   r   rH   )rV   r(   r   r   r   reverser1   r   rv   rg   r<   r=   r   r   r   r   r   r   r   r   get_date_by_duration  s8    
&z CmfCalendar.get_date_by_durationc              	   C   s^   t jjdddgdddgdddgdd| ggddd	d
dddgd}|D ]}|j  |  qBdS )u   
        Обработчик смотрит только активные циклы (не на триггерной паузе)

        Zbreachedr   FalseZpausedZ	stop_timeZNULLzsla_goal.calendarZsla_goalcodeZwithin_calendar_hoursZpause_interval_stop_timeZpause_interval_start_timelast_time_updater   N)rG   ZCmfSDeskSlaCyclerr   r   Zset_nowr   )r   Zcyclescycler   r   r   calendar_handler  s    
 
zCmfCalendar.calendar_handler)Z	only_onceZsystem_taskc               	   O   s6   t jjddddddddgd	}|D ]}t| q"d S )
Nrz   zdefault_workweek.intervalsz#default_workweek.intervals.day_weekr   z$default_workweek.intervals.from_timez"default_workweek.intervals.to_timer   r   r   )rG   r   rr   r   )_args_kwargsZ	calendarsr(   r   r   r   celery_hourly_hook%  s    zCmfCalendar.celery_hourly_hookc                 C   s   t | j d S )N)Zcmf_deferred_taskr   r   r   r   r   hourly_hook8  s    zCmfCalendar.hourly_hookc                 C   s   d| _ tjj  dS )u   
        Помечает, что календарь необходимо пересчитать и сбрасывает кэш дней
        TN)r   rG   r   r   r   r   r   r   r   _set_as_dirty<  s    zCmfCalendar._set_as_dirtyc                 C   s   | j jsdS | j stddd d| j d}tjjdd| jgd	d
dggd}|rtd|_ |jdd d|j d| }t| dS )uX   
        Изменяет флаг "Календарь по умолчанию"
        Nu   Для сброса флага "Календарь по умолчанию" вы должны установить его у любого другого календаря.Tr?   uf   Во всех новых проектах будет использоваться календарь ""idrZ   
is_defaultr   )r`   Fr   u^   Сбросили флаг "Календарь по умолчанию" у календаря "z". )	r   r   rD   r   rG   r   rx   r   r   )r   msgZcurrent_default_calendarr   r   r   _set_is_defaultD  s"    
zCmfCalendar._set_is_defaultc                 C   s    | j jrd| _ | jjrd| _dS )u}   
        Исправляет нулевые значения, если приходит null выставляет 0
        r   N)r   Zis_nullr   r   r   r   r   fix_null_value]  s    zCmfCalendar.fix_null_valuec                    s  | j rvt| dd rtjj| jdddgd}|j| _|j| _|jD ]h}|	 }| |_
d|_|jdd |d	g |jD ]}|	 }||_
|jdd qz|jsD|jsD|| _qD|jD ]P}|	 }	| |	_
|	jdd |d	g |jD ]}|	 }|	|_
|jdd qqnntj  j}
ttj|
  | _tj| d
d}tdD ]}t|d| dd qH|jdd || _|   |   t| jdkrtddd | jj r| !  t" j||S )Ntemplater   zworkweeks.*zexclusions.*)r   r_   FTr   rY   u   По умолчанию)r^   r   rK   r   ri   Zweekendr   u\   Количество лет расчета не должно быть больше 10 летr?   )#Zis_newrP   rG   r   rx   r   r   r   r   Zcloner^   systemr   ry   rv   rR   rT   rz   r   r   r   Z
astimezoner   r   Z	utcoffsetrH   rw   r   r   r   r   rD   r   r   super)r   argskwargsZtemplate_calendarZtemplate_workweekZnew_workweekZtemplate_intervalZnew_intervalZtemplate_exclusionZnew_exclusionr   r   r   	__class__r   r   r   g  sV    




zCmfCalendar.savec                    sh   |  dg | jrtddd tjj| dgd}|rZddd	 |D }td
| dd t j||S )Nr   u   Нельзя удалить календарь по умолчанию. Сначала назначьте любой другой календарь, как календарь по умолчанию.Tr?   r   )r(   r_   z, c                 S   s   g | ]}d |j  d qS )r   )r   )r    Zprojectr   r   r   r"     s     z&CmfCalendar.delete.<locals>.<listcomp>uk   Невозможно удалить календарь, он используется в проектах: )	ry   r   rD   rG   Z
CmfProjectrr   joinr   delete)r   r   r   ZprojectsZprojects_strr   r   r   r     s    
zCmfCalendar.delete)NNN)NNr   )NNr   N)NNr   NNN)NNNNNN)NN)NN)NNT)F)N)r   F)r   )0__name__
__module____qualname__staticmethodDater   r   r   r*   r   r2   r6   r9   r;   Timer   DatetimerE   rI   classmethodrX   r   r   boolr   r   r   strZcmfr_   ZCmfTyper   r   r   r   r   r   r   Z
celery_appZtaskr   r   r   r   r   r   r   __classcell__r   r   r   r   r      s            !  
 *          3          /   
AM         !   &7   4

=r   )r(   r%   r   	functoolsr   typingr   r   r   r   r   ZpytzZcmf.includeZcommon.fieldsr	   Zdateutil.relativedeltar
   r   rC   r   r   r   r   r   ZAPPZHOOK_CRON_HOURLYr0   r   r   r   r   r   <module>   s&          *