
    5jr                        d Z ddlZddlZddlZddlZddlZddlZddlmZmZm	Z	 ddl
mZ ddlmZ 	 ddlZddlZddlZ ee      j+                         j,                  Z eej0                  j3                  ded	z              Z eej0                  j3                  d
edz              Zedz  Zedz  Zedz  ZdZdZ dddZ!dBdede"fdZ#dBde$deddfdZ%de"de&fdZ'de"de&dede(fdZ)de$fdZ*d ed!e+fd"Z,de$d ede"d!e+de(f
d#Z-dede"fd$Z.de"fd%Z/de$de"fd&Z0de$de"fd'Z1de$de"fd(Z2d)e&de&fd*Z3d+ed,ede&fd-Z4de$d+ed,ede&fd.Z5de&fd/Z6d0e&de&fd1Z7dddd2de$d3e$d+ed,ede&f
d4Z8d5 Z9dd6d7de$d ed8e(d9ejt                  d:e&d;e(de$fd<Z;d=e"d ed8e(d9ejt                  de$f
d>Z<dede"fd?Z=dCd@Z>e?dAk(  r e>        yy# e$ r!Z ej&                  de d       Y dZ[dZ[ww xY w)Du  
scada_report_scheduler.py — Scheduled SCADA report execution + email delivery
(DVI-486, SCADA View Reporting ST4).

Part of SCADA View Reporting (DVI-474). A standalone, systemd-timer driven CLI
that runs once per invocation, scans the saved report schedules
(``scada_report_schedules.json``, authored by ST3 / DVI-484), generates the XLSX
for every schedule that is *due*, persists it under ``reports/``, and — when the
schedule opts in — emails it as a base64 file attachment via Microsoft Graph.

Modeled on ``notification_scheduler.py``: it imports the small self-contained
engine modules (``scada_report`` for report/XLSX generation, ``notifications``
for the Graph send) and never imports app.py, which starts background loops at
import time and is unsuitable for a one-shot job.

Due-date / cadence model
------------------------
A schedule's ``cadence`` drives BOTH the report's data window and how often it
fires. The window always ends on *yesterday* (the last complete log day):

    cadence       interval   data window (run date = today)
    last_week     7 days     [today-7,  today-1]
    last_month    30 days    [today-30, today-1]
    custom        N days     [today-N,  today-1]   (N = schedule.custom_days)

There is no separate firing anchor stored on the schedule, so the run log
(``scada_report_log.json``) doubles as the last-run tracker: a schedule is due
when it has never run, or when its most recent successful run is at least
``interval`` days old. First invocation after a schedule is created runs it
immediately.

Idempotency
-----------
File-based, mirroring the ``notification_log.json`` pattern but in its own
``scada_report_log.json`` (the report run shape differs from notification
sends). The idempotency key is ``(schedule_id, run_date)`` — a schedule runs at
most once per calendar day, so a second invocation the same day is a no-op. A
run is recorded unless the email was attempted and failed transiently (so a
failed send is retried on the next daily pass); permission/config issues
(no recipients, oversized attachment) are recorded to avoid daily regeneration.

Attachment size guard
---------------------
Graph's simple sendMail caps total message size near 4 MB. If a generated XLSX
exceeds ``MAX_ATTACHMENT_BYTES`` (3 MB) the report is still persisted to
``reports/`` but the email is skipped with a warning (never silently
truncated).

Recipient model (DVI-487 — final shape)
---------------------------------------
The Settings → Notifications pane drives two global lists plus per-schedule
toggles. Recipients are OR-merged and de-duplicated by lowercased email from:

  1. ``schedule["recipients"]``                 — per-schedule list, included
                                                  when ``schedule["notify"]``
                                                  (User notifications) is on
  2. ``scada_enforced_recipients.json``         — global enforced list, always
                                                  included (every report)
  3. ``scada_external_recipients.json``         — global external list, included
                                                  only when ``schedule["external"]``
                                                  is on

Each global file may be a flat JSON list ``["a@b.com", ...]`` or an object
``{"recipients": ["a@b.com", ...]}``. Missing files resolve to no recipients.
A report is emailed when ``notify`` OR ``external`` is set; if neither is on the
report is persisted only. When sending but no recipients resolve, the email is
skipped with a warning.

Invocation contract (for the systemd timer in services/)
--------------------------------------------------------
  * Command:           <venv>/bin/python /var/www/html/togen/scada_report_scheduler.py
  * Working directory: /var/www/html/togen   (so ``import scada_report`` /
                       ``import notifications`` resolve and the JSON state files
                       are found alongside them)
  * Required env vars: AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_TENANT_ID
                       (Graph fetch + send), AZURE_MAIL_FROM (send-from address)
  * Optional env vars: SCADA_MAILBOX, SCADA_REPORT_SCHEDULES, SCADA_REPORTS_DIR
  * Cadence:           run daily; the per-schedule cadence decides what is due.
  * Exit code:         0 on a clean scan (including "nothing due"); non-zero only
                       on an unexpected error (e.g. unreadable schedules file).

Usage:
    python scada_report_scheduler.py [--dry-run] [--date YYYY-MM-DD]
                                     [--schedules PATH] [--log-level LEVEL]

    --dry-run    Generate + persist reports and resolve recipients, but do not
                 send email or write the run log.
    --date       Override "today" (for testing); defaults to the system date.
    --schedules  Override the schedules JSON path.
    N)datedatetime	timedelta)Path)quotez!Cannot import a required module: z
Run this script from the togen app directory (the one containing scada_report.py and notifications.py), e.g. WorkingDirectory=/var/www/html/togen.SCADA_REPORT_SCHEDULESzscada_report_schedules.jsonSCADA_REPORTS_DIRreportszscada_report_log.jsonzscada_enforced_recipients.jsonzscada_external_recipients.jsoni  0 zAapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheet      )	last_week
last_monthpathreturnc                     | xs t         } | j                         sg S 	 t        j                  | j	                               }t        |t              r|S g S # t        j                  t        f$ r g cY S w xY w)zFLoad the report run log (list of run records). [] when absent/invalid.)	RUN_LOG_FILEis_filejsonloads	read_text
isinstancelistJSONDecodeErrorOSError)r   datas     -/var/www/html/togen/scada_report_scheduler.pyload_run_logr      sg    <D<<>	zz$..*+!$-t525  '* 	s   4A A A32A3recordc                    |xs t         }t        |      }|j                  |        	 |j                  t	        j
                  |d             y # t        $ r( t        j                  d      j                  d|       Y y w xY w)N   )indentscada_report_schedulerzFailed to persist %s)
r   r   append
write_textr   dumpsr   logging	getLogger	exception)r   r   logs      r   _append_run_logr*      sl    <D
t
CJJv*

3q12 *23=="D	**s   &A .B ?B r)   schedule_idc                     d}| D ]\  }|j                  d      |k7  r|j                  d      dk(  r-	 t        j                  |j                  dd            }|||kD  s[|}^ |S # t        t        f$ r Y rw xY w)u%  Most recent *scheduled* run_date (date) for a schedule, or None.

    Manual (Test / run-now) runs are tagged ``source:"manual"`` and are excluded
    so a manual run — which sends no email (DVI-1164 D3) — never counts as the
    day's scheduled delivery and thus never suppresses it.
    Nr+   sourcemanualrun_date )getr   fromisoformat
ValueError	TypeError)r)   r+   latestentryds        r   _last_run_dater8      s     F 
99]#{299X(*	""599Z#<=A >QZF
 M	 I& 		s   %A&&A87A8r/   c                 P    |j                         t        fd| D              S )zTrue if a *scheduled* run for this schedule already landed on ``run_date``.
    Manual runs are ignored (see ``_last_run_date``).c              3      K   | ]D  }|j                  d       k(  xr* |j                  d      k(  xr |j                  d      dk7   F yw)r+   r/   r-   r.   N)r1   ).0eisor+   s     r   	<genexpr>z_ran_on.<locals>.<genexpr>   sV       uu]#{2 /quuZ7HC7O /55?h./ s   A
A)	isoformatany)r)   r+   r/   r=   s    ` @r   _ran_onrA      s-     


C       schedulec                     | j                  d      xs dj                         }|t        v r	t        |   S |dk(  r$	 t        | j                  d            }|dk\  r|S dS y# t        t
        f$ r Y yw xY w)z}Firing interval / window length in days for a schedule, or None if the
    cadence is unrecognized or custom_days is invalid.cadencer0   customcustom_daysN   )r1   stripCADENCE_INTERVAL_DAYSintr4   r3   )rC   rE   ns      r   _interval_daysrM      s     ||I&,"335G''$W--(	HLL/0A Fq$$ :& 		s   A A10A1todayintervalc                 L    | t        d      z
  }|t        |dz
        z
  }||fS )z<Trailing data window ending yesterday: (date_from, date_to).rH   )days)r   )rN   rO   date_to	date_froms       r   _window_forrT      s/    iQ''G)A66IgrB   c                     | j                  d      }|syt        |||      ryt        ||      }|y||z
  j                  |k\  S )zA schedule is due when it hasn't run today and its last successful run is
    at least ``interval`` days old (or it has never run).idFT)r1   rA   r8   rQ   )rC   rN   r)   rO   sidlasts         r   is_duerY      sP     ,,t
CsC#s#D|DL(**rB   c                    | j                         sg S 	 t        j                  | j                               }t        |t              r|j                  dg       }t        |t              sg S |D cg c]&  }t        |t              s|j                         s%|( c}S # t        j                  t
        f$ r g cY S w xY wc c}w )N
recipients)r   r   r   r   r   r   r   dictr1   r   strrI   )r   r   r<   s      r   _read_recipient_filer^      s    <<>	zz$..*+ $xxb)dD!	@!z!S1aggiA@@   '* 	 As#   #B /B>B>B>B;:B;c                     g t               }}| D ]i  }t        |t              s|j                         j	                         }|s5||vs:|j                  |       |j                  |j                                k |S N)setr   r]   rI   loweraddr#   )addrsorderedseenaddrnorms        r   _deduperi      sk    TG )$$zz|!!#D$HHTNNN4::<() NrB   c                     g }| j                  d      r2| j                  d      }t        |t              r|j                  |       |j                  t	        t
                     t        |      S )zInternal audience emailed at generation time (no approval): the
    per-schedule list when ``notify`` is on + the global enforced list.notifyr[   )r1   r   r   extendr^   ENFORCED_RECIPIENTS_FILEri   )rC   rd   per_schedules      r   resolve_internal_recipientsro     sV     E||H||L1lD)LL&	LL%&>?@5>rB   c                 X    | j                  d      sg S t        t        t                    S )u   External audience — the global external list, included only when the
    schedule's ``external`` toggle is on. Gated behind approval (DVI-1102 F4).external)r1   ri   r^   EXTERNAL_RECIPIENTS_FILErC   s    r   resolve_external_recipientsrt     s'     <<
#	'(@ABBrB   c                 B    t        t        |       t        |       z         S )zFull OR-merged recipient list (internal + external). Retained for callers
    that want the combined audience; the scheduler itself sends the two groups
    separately so external delivery can be held for approval.)ri   ro   rt   rs   s    r   resolve_recipientsrv     s&     .x80:; < <rB   namec                 ~    t        j                  dd| xs dj                               }|j                  d      xs dS )Nz[^A-Za-z0-9._-]+-report)resubrI   )rw   ss     r   _slugr~   '  s6    
"C$*:()A)A)CDA773<#8#rB   rS   rR   c                 .    d } ||        d ||       S )Nc                 \    | j                    d| j                   d| j                  d       S )N.z%y)monthdaystrftime)r7   s    r   fmtz_friendly_range.<locals>.fmt-  s+    ''!AEE7!AJJt$4#566rB   z -  )rS   rR   r   s      r   _friendly_ranger   ,  s!    7)nSW//rB   c                 H    d| j                  dd       dt        ||       dS )NzKiln Metrics Report: rw   Reportz ())r1   r   )rC   rS   rR   s      r   _email_subjectr   2  s4    #HLL$B#C D	734A7 8rB   c                  f    t         j                  j                  d      xs dj                  d      S )z?Public Togen base URL for building the in-app viewer deep link.TOGEN_BASE_URLzhttps://togen.icastinc.com/)osenvironr1   rstripr   rB   r   	_base_urlr   7  s(    JJNN+, ,+VVC[9rB   out_namec                 h    t                dt        |        }dj                  t        |            S )u  A "View report online" button linking into the in-app Report Viewer.

    Deep-links to ``?open_scada_report=<file>`` which the frontend handles by
    opening the SCADA tool and the persisted-report viewer modal. The route is
    SSO-gated, so this is for the internal audience only (DVI-1165 D2) — the
    scheduler never adds it to the external send (external gets the attachment
    only, via the approvals path).
    z/?open_scada_report=aM  <div style="margin:18px 0 2px;"><a href="{link}" style="display:inline-block;background:#0077A8;color:#ffffff;text-decoration:none;font-size:14px;font-weight:600;padding:10px 20px;border-radius:8px;">View report online</a><p style="margin:8px 0 0;font-size:12px;color:#9ca3af;">Opens the report in Togen (sign-in required).</p></div>)link)r   r   format_esc)r   r   s     r   _viewer_cta_htmlr   =  s9     k].uX.?@D	B f$t*frB   criteria_summaryr   optsrz   c          
      J   |xs t        j                         }| j                  d      xs g }|rdj                  |      nd}d| j                  dd      fd|j	                          d|j	                          fd	|fd
t        |j                  dd            fg}	dj                  d |	D              }
t        j                  |||      }d|
z   dz   |z   }d}|j                  d      r|rt        |      }d}t        j                  t        t        | ||            |d||      S )zInternal report email body: intro + detail table + (opt) Attention
    summary block + (opt) viewer link CTA. The summary/link are gated by the
    ``report_email`` settings in ``opts`` (DVI-1165).sensors, zAll sensorsSchedulerw   r0   z
Date rangeu    – SensorsRows	row_countr   c              3   l   K   | ],  \  }}d j                  t        |      t        |             . yw)z<tr><td style="padding:4px 12px 4px 0;color:#6b7280;font-size:13px;white-space:nowrap;vertical-align:top;">{lbl}</td><td style="padding:4px 0;color:#111827;font-size:13px;">{val}</td></tr>)lblvalN)r   r   )r;   r   r   s      r   r>   z_email_html.<locals>.<genexpr>_  s>      K :Bc	 49$s)4	5Ks   24z<p style="margin:0;font-size:15px;line-height:1.6;">Your scheduled Kiln Metrics report is attached as an Excel workbook (one tab per day).</p><table role="presentation" cellpadding="0" cellspacing="0" style="margin:12px 0 4px;">z</table>include_viewer_linkz<hr style="margin:8px 0 14px;border:none;border-top:1px solid #e5e7eb;"><p style="font-size:12px;color:#9ca3af;text-align:center;margin:0;line-height:1.6;">You are receiving this because you are a recipient of this scheduled Kiln Metrics report.</p>zKiln Metrics Report)title	body_htmlsubtitlecta_htmlfooter_html)scada_reportreport_email_settingsr1   joinr?   r]   build_report_email_summaryr   notificationsrender_email_shellr   r   )rC   rz   rS   rR   r   r   r   r   sensors_txtrowsrow_htmlsummary_htmlr   r   r   s                  r   _email_htmlr   Q  s^   
 7<557Dll9%+G(/$))G$]K	X\\&"-.	)--/0g6G6G6I5JKL	K 	VZZQ/01	D ww K FJ	K KH
  :: $(L	& )1		1 4>		>
 	  Hxx%&8#H-	2 
 ++>(Iw?@&;{4 4rB   c                 x    t        |       j                  dd      j                  dd      j                  dd      S )N&z&amp;<z&lt;>z&gt;)r]   replace)values    r   r   r   {  s1    JWS'"773#7V8LNrB   T)r-   
send_emaildry_runloggerr-   r   c                x
   | j                  d      }| j                  dd      }t        |       }t        ||      \  }	}
|||j                         |	j                         |
j                         ddg g d|t	        j
                         j                         dd}t        j                  | j                  d            }|j                  |	|
| j                  d	      xs d
      }|d   r1|j                  d||d          dt        |d         z   |d<   d|d<   |S |d   |d<   |j                  |      }|j                  ||      }|j                         }t        j                  dd       t!        |       d|	j                          d|
j                          d}t        |z  }	 |j#                  |       ||d<   |j%                  d|||d   t'        |             	 t        j,                  ||||||j.                  |	j                         |
j                         t	        j
                         j                         ||d       |s|j%                  d|       d|d<   |S t3        |       }t5        |       }||d<   ||d<   | j                  d       s%| j                  d!      s|j%                  d"|       |S |s|s|j1                  d#|       d$|d<   |S t'        |      t6        kD  r(|j1                  d%|t'        |      t6               d&|d<   |S t        j8                         }t;        | |	|
      }t=        | ||	|
|||'      }t?        |	|
       d}|r|g }|r&|jA                  d(| d)d*jC                  |              |r#|jA                  d+d*jC                  |              |j%                  d,|d-jC                  |      xs d.       d/|d<   |S d}|r]tE        jF                  |||||tH        0      }|r|j%                  d1||t'        |             n|j                  d2|       d3|d<   d|d<   |S |s|rd4nd$|d<   |S tK        jL                  ||j.                  |	r|	j                         nd
|
r|
j                         nd
|d   ||||d5|d67      }|d8   d   |d9<   |d:   r4|j%                  d;|t'        |      t'        |d<                |rd=nd>|d<   |S |j1                  d?|       |rd@ndA|d<   |S # t(        $ r! |j+                  d|       d|d<   d|d<   |cY S w xY w# t(        $ r |j1                  d|       Y w xY w)Bu~  Generate + persist a schedule's report and (optionally) email it.

    Returns a run record dict (also the shape appended to the run log). The
    ``recorded`` key tells the caller whether to persist it: a transiently failed
    send is not recorded so it retries on the next pass.

    ``source`` tags the run in the log — ``"schedule"`` for a timer-driven pass,
    ``"manual"`` for a Test / run-now (DVI-1164). ``send_email`` gates all email:
    a manual Test run (D3) sets it False so the report is generated, persisted and
    recorded (openable in the viewer) but nothing is emailed and no external
    approval is enqueued.
    rV   rw   r0   r   skipped_notify_offT)r+   schedule_namer/   rS   rR   r   email_statusr[   external_recipientsreport_filer-   recorded_atrecordedreport_typer   Nerrorz$schedule %r: report build failed: %serror:r   Fr   r   )parentsexist_ok_z.xlsxr   z)schedule %r: wrote %s (%d rows, %d bytes)z%schedule %r: failed to persist reportpersist_failed)r+   r   r   rS   rR   generated_atr-   r   )metazCschedule %r: failed to persist view-model JSON (report still saved)uF   schedule %r: manual run — report persisted + recorded, no email (D3)manual_no_emailr[   r   rk   rq   u=   schedule %r: notifications off — report persisted, no emailuJ   schedule %r: notifications on but no recipients resolved — email skippedskipped_no_recipientsu[   schedule %r: XLSX %d bytes exceeds %d-byte limit — email skipped (report still persisted)skipped_oversizedr   zemail z to internal r   zenqueue external approval for z[DRY-RUN] schedule %r: would %sz; z
do nothingr   )filenamecontentcontent_typez3schedule %r: emailed %s to %d internal recipient(s)u@   schedule %r: internal email send FAILED — will retry next passsend_failedsentrC   	scheduler)report_namer   rS   rR   r   r   attach_namesubjectr[   r-   r+   requested_byrequestapproval_idnotifiedzWschedule %r: external send held for approval (%d recipient(s)); notified %d approver(s)	approverszsent+pending_approvalpending_approvalu   schedule %r: external send held for approval but NO approver is configured — report will NOT reach external recipients until an approver is set in Kiln Metrics → Report Notificationsz!sent+pending_approval_no_approverpending_approval_no_approver)'r1   rM   rT   r?   r   nowr   get_report_typebuildr   r]   r   workbookgetvalueREPORTS_DIRmkdirr~   write_bytesinfolenr   r(   write_report_view_modelrV   warningro   rt   MAX_ATTACHMENT_BYTESr   r   r   r   r#   r   r   send_email_with_attachmentXLSX_CONTENT_TYPEscada_report_approvalsenqueue_external_send)rC   rN   r   r   r-   r   rW   rw   rO   rS   rR   r   rtyperz   summarybufr   r   out_pathinternalrq   
email_optsr   htmlr   partsinternal_okresults                               r   run_scheduler     s    ,,t
C<<#Dh'H$UH5Iw OO%((*$$&,!||~//1F& ((m)DEE[[GX\\)-D-LMFg;T6'?S!)Cw,@!@~"z -F; $$V,G
..
)C<<>D dT2+a	 3 3 56a8I8I8K7LERHX%H	T" (}?(F;$7T	D5,,fg!$t!&(224$..0"*,,.":":"<"8=	>   $%)	+!2~*84H*84H#F<$,F !LL"hll:&>S	H +,0	2!8~
4y'' @SY(<	> "5~
 335JXy':GxG(/(&(D %Y89?KLL6+mDIIh<O;PQRLL9$))H:M9NOP5tIIe$4	6!*~ K#>>gt $=NP KKMk3x=: LL %&*,%2F>"!&F:M
 +39P~#99ehh+4)%%'$'.!!#D%8XsFF #9-d3F=j A#h-VK-@)A	C >F"9(: 	~ M 	 . 04	5 JR"E(F 	~M}  @$G!1~"z	$  5 ./3	55s%   $7S- A(T -'TTT98T9	schedulesc                   t               }t        |       dddddd}| D ])  }t        |t              r|j	                  d      s|dxx   dz  cc<   3t        |      }|/|j                  d|j	                  d             |dxx   dz  cc<   ot        ||||      s|dxx   dz  cc<   |d	xx   dz  cc<   t        ||||
      }|j	                  dd      j                  d      s|dxx   dz  cc<   |j	                  d      dk(  r|dxx   dz  cc<   |j                  dd      s
|rt        |       |j                  |       , |S )z.Run every due schedule. Returns summary stats.r   )r   due	generatedemailedskipped_not_dueinvalidrV   r  rH   u(   schedule %r: invalid cadence — skippedrw   r  r  r   r   r   r0   )r   persistr  r   r  r   T)r   r   r   r\   r1   rM   r   rY   r   
startswithpopr*   r#   )	r   rN   r   r   r)   statsrC   rO   r   s	            r   scanr  G  sQ    .Ci.aA?E  (D)d1C)!!(+NNE#<</1)!hsH5#$)$ehwvNzz."-889NO+!#::n%/)!::j$'F#JJv/2 LrB   c                     | j                         sg S t        j                  | j                               }t	        |t
              st        d|        |D cg c]  }t	        |t              s| c}S c c}w )Nz#schedules file is not a JSON list: )existsr   r   r   r   r   r3   r\   )r   r   r}   s      r   load_schedulesr  j  s_    ;;=	::dnn&'DdD!>tfEFF3!z!T2A333s   A5.A5c            
         t        j                  dt         j                        } | j                  dt	        t
              d       | j                  dd d       | j                  dd	d
       | j                  ddg dd       | j                         }t        j                  |j                  ddt        j                         t        j                  d      }|j                  r!	 t        j                  |j                        }n"t%        j&                         j                         }t)        |j*                        }	 t-        |      }|j5                  d|j7                         |j8                  rdnd       t;        ||j8                  |      }|j5                  d|d   |d   |d    |d!   |d"   |d#          y # t        $ r4 |j!                  d|j                         t        j"                  d       Y w xY w# t.        t        t0        j2                  f$ r2}|j!                  d||       t        j"                  d       Y d }~d }~ww xY w)$Nz9Run due SCADA report schedules and email the XLSX results)descriptionformatter_classz--schedulesz#Path to scada_report_schedules.json)defaulthelpz--datez1Override today's date as YYYY-MM-DD (for testing)z	--dry-run
store_truezEGenerate + persist reports but do not send email or write the run log)actionr  z--log-levelINFO)DEBUGr  WARNINGERRORzLogging verbosity)r  choicesr  z'%(asctime)s %(levelname)-8s %(message)sz%Y-%m-%dT%H:%M:%S)levelr   datefmtstreamr"   z'Invalid --date %r (expected YYYY-MM-DD)r    zCould not read schedules %s: %srH   zScanning %s (today=%s%s)z	, DRY-RUNr0   r  z_Scan complete: %d schedule(s) | due=%d generated=%d emailed=%d | skipped: not_due=%d invalid=%dr   r  r  r  r  r  )argparseArgumentParserArgumentDefaultsHelpFormatteradd_argumentr]   DEFAULT_SCHEDULES_PATH
parse_argsr&   basicConfig	log_levelsysstdoutr'   r   r2   r3   r   exitr   r   r   r   r  r   r   r   r   r?   r   r  )parserargsr   rN   schedules_pathr   excr  s           r   mainr.  s  s   $$O >>F s3I/JB  D
$P  R
L4  5 v E0  2 Dnn8#zz	 78Fyy	&&tyy1E
 ##%$..)N">2	
 KK*NEOO<M#||5E4<<GE
KK	)kE%L%*<eI>N %	"2	4!  	LLBDIINHHQK	 Z!5!56 6Ls*   )F? G? ?:G<;G<?I(I

I__main__r`   )r   N)@__doc__r  r   r&   r   r{   r'  r   r   r   pathlibr   urllib.parser   r   r   r   ImportErrorr-  r)  __file__resolveparent	_BASE_DIRr   r1   r#  r   r   rm   rr   r   r   rJ   r   r   r\   r*   r]   r8   boolrA   rM   rK   rT   rY   r^   ri   ro   rt   rv   r~   r   r   r   r   r   r   Loggerr   r  r  r.  __name__r   rB   r   <module>r;     sj  Yv    	 	 
 . .  
! N""$++	JJNN+Y9V-VWY 2::>>"5y97LMN22$'GG $'GG  '  H  '(r: 	t 	t 	*D * * * 3 , C 4 D T t s +T +$ +T +S +T +$At A A	d 		$ 	4 	C$ C4 C< <$ <$ $ $
0t 0d 0s 08T 8d 8T 8c 8
93 9s s * %)4d'4$ '4 '4 '4 '4GJ'4TN 8B$(@4 @ @$ @ @14@!@-1@F D    4   $( F4 4$ 404f zF w  CHH
+C5 10 	0 s   F> >G$GG$