
    5jA                        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mZ ddlmZ  eej                  j                  d ee      j                         j                  dz              Z ej                         Zd Zd Zd	 Zd d
Zd Zd!dZd"dZd#dZd Zd Zd!dZ	 d$dZd%dZd Zd&dZd'dZ dddddddddZ!d Z"d Z#d Z$d(dZ% e        y))u   reports_store.py — SQLite store for Report Builder templates (DVI-1255).

Part of the DVI-1252 Report Builder. A *report template* is a reusable,
versioned definition — a set of dashboard-shaped tiles (the chart /
report_table tiles registered by P1, DVI-1254) plus a report-level date-range
parameter and an optional recipient list — that P3 will render into a viewable
report and (later) snapshot to history.

Flask-independent (mirrors scada_store.py / idencia_store.py / assets_store.py)
so app.py, the togen-worker, and tests can all import it. The DB lives
alongside the app's other state files (reports.db next to dashboards.json).

Decision D7-A: this lives in a standalone module (NOT inline in app.py) with a
DVI-848 dual-dir `update.sh` copy step, so the `sys.path` shadow gotcha can
never serve a stale copy.

Schema
------
templates(id, name, owner, definition, version, created_at, updated_at,
    deleted) — one row per template. `definition` is the JSON authoring
    payload (tiles + date_range + recipients). `version` bumps on every
    content save; soft-delete via `deleted` keeps ids stable for any P3 run
    rows that reference them.
template_versions(id, template_id, version, definition, created_at,
    created_by) — append-only history of every saved version, so a template's
    edit trail (and a future "restore this version") survives overwrites.
runs / snapshots — created empty now so P3 (Report Viewer + history +
    snapshots) is additive with no schema migration; no write API here yet.
    N)datetime)PathREPORTS_DB_FILEz
reports.dbc                      t        j                  t        t              d      } | j	                  d       | j	                  d       t         j
                  | _        | S )N   )timeoutzPRAGMA journal_mode=WALzPRAGMA busy_timeout=10000)sqlite3connectstrr   executeRowrow_factoryconns    $/var/www/html/togen/reports_store.py_connectr   /   sB    ??3/<DLL*+LL,-{{DK    c                     t         5  t               5 } | j                  d       | j                  d       | j                  d       | j                  d       | j                  d       | j                  d       | j                  d       | j                  d       d	d	d	       d	d	d	       y	# 1 sw Y   xY w# 1 sw Y   y	xY w)
zECreate tables/indexes if missing. Idempotent; call at import/startup.a  CREATE TABLE IF NOT EXISTS templates (
                   id TEXT PRIMARY KEY,
                   name TEXT NOT NULL,
                   owner TEXT NOT NULL DEFAULT '',
                   definition TEXT NOT NULL,
                   version INTEGER NOT NULL DEFAULT 1,
                   created_at TEXT NOT NULL,
                   updated_at TEXT NOT NULL,
                   deleted INTEGER NOT NULL DEFAULT 0
               )ad  CREATE TABLE IF NOT EXISTS template_versions (
                   id INTEGER PRIMARY KEY AUTOINCREMENT,
                   template_id TEXT NOT NULL,
                   version INTEGER NOT NULL,
                   definition TEXT NOT NULL,
                   created_at TEXT NOT NULL,
                   created_by TEXT NOT NULL DEFAULT ''
               )zkCREATE INDEX IF NOT EXISTS idx_tversions_template
               ON template_versions(template_id, version)aQ  CREATE TABLE IF NOT EXISTS runs (
                   id TEXT PRIMARY KEY,
                   template_id TEXT NOT NULL,
                   template_version INTEGER,
                   ran_at TEXT NOT NULL,
                   ran_by TEXT NOT NULL DEFAULT '',
                   params TEXT,
                   status TEXT
               )zCREATE TABLE IF NOT EXISTS snapshots (
                   id INTEGER PRIMARY KEY AUTOINCREMENT,
                   run_id TEXT NOT NULL,
                   tile_id TEXT,
                   payload TEXT NOT NULL
               )zPCREATE INDEX IF NOT EXISTS idx_snapshots_run
               ON snapshots(run_id)a  CREATE TABLE IF NOT EXISTS schedule_runs (
                   id INTEGER PRIMARY KEY AUTOINCREMENT,
                   template_id TEXT NOT NULL,
                   run_date TEXT NOT NULL,
                   fired_at TEXT NOT NULL,
                   run_id TEXT,
                   emailed INTEGER NOT NULL DEFAULT 0,
                   recipients TEXT,
                   tile_count INTEGER,
                   status TEXT NOT NULL DEFAULT 'ok',
                   error TEXT
               )zlCREATE INDEX IF NOT EXISTS idx_schedule_runs_template
               ON schedule_runs(template_id, run_date)N)_write_lockr   r   r   s    r   init_reports_dbr   7   s    	 C;hj C;D	
	 		 	=	>
 			 		 	'	( 		 	:	;CC; C; C; C; C; C;s#   B8B	B,B8,B5	1B88Cc                  H    t        j                         j                  d      S )Nz%Y-%m-%d %H:%M:%S)r   nowstrftime r   r   _nowr      s    <<>""#677r   c           
          | y | d   | d   | d   | d   | d   | d   t        | d         d}|r	 t        j                  | d	         |d	<   |S |S # t        j                  t        f$ r	 i |d	<   Y |S w xY w)
Nidnameownerversion
created_at
updated_atdeleted)r   r   r   r    r!   r"   r#   
definition)booljsonloadsJSONDecodeError	TypeError)rowwith_definitionouts      r   _row_to_templater-      s    
{$iFWy>,','I'C 	# $

3|+< =C J3J $$i0 	# "CJ	#s   A A0/A0c                    dt        j                         j                  dd z   }t               }t	        j
                  |xs i       }t        5  t               5 }|j                  d|| |xs d|||f       |j                  d||||xs df       ddd       ddd       t        |      S # 1 sw Y   xY w# 1 sw Y   t        |      S xY w)z<Insert a new template at version 1. Returns the full record.rpt_N
   zINSERT INTO templates
                   (id, name, owner, definition, version, created_at,
                    updated_at, deleted)
               VALUES (?, ?, ?, ?, 1, ?, ?, 0) zINSERT INTO template_versions
                   (template_id, version, definition, created_at, created_by)
               VALUES (?, 1, ?, ?, ?))
uuiduuid4hexr   r&   dumpsr   r   r   get_template)r   r   r$   tidr   payloadr   s          r   create_templater9      s    
4::<##CR(
(C
&Cjj)r*G	 .hj .D2 $Wc37	9 	) '3,		.. . . . . s$   B77B+B7+B4	0B77C
Fc                     d}|s|dz  }t               5 }|j                  || f      j                         }ddd       t        |      S # 1 sw Y   t              S xY w)z0Full template record (incl. definition) or None.z"SELECT * FROM templates WHERE id=?z AND deleted=0N)r   r   fetchoner-   )r7   include_deletedqr   r*   s        r   r6   r6      s]    ,A		 1tll1sf%..01C  1C  s   "A		Ac                    d}g g }}|s|j                  d       | G|j                  d       |j                  t        |       j                         j                                |r|ddj	                  |      z   z  }|dz  }t               5 }|j                  ||      j                         }ddd       D cg c]  }t        |d	       c}S # 1 sw Y   $xY wc c}w )
zTemplates (metadata only, no definition) newest-updated first. `owner`
    filters to one owner's templates (lower-cased match).zSELECT * FROM templatesz	deleted=0NzLOWER(owner)=?z WHERE z AND z2 ORDER BY updated_at DESC, name COLLATE NOCASE ASCF)r+   )	appendr   striplowerjoinr   r   fetchallr-   )r   r<   r=   condsargsr   rowsrs           r   list_templatesrH      s     	"Ab4E[!%&CJ$$&,,./	Ye,,,	==A	 0t||At$--/0@DE1Q6EE0 0Es   !C8CCr1   c                    t         5  t               5 }|j                  d| f      j                         }|	 ddd       ddd       yt	               }||d   n|}|d   }|Tt        j                  |xs i       }	|d   dz   }|j                  d||	||| f       |j                  d| ||	||xs df       n|j                  d	||| f       ddd       ddd       t        |       S # 1 sw Y   xY w# 1 sw Y   t        |       S xY w)
zPartial update. A definition change bumps `version` and appends a
    template_versions row; a name-only change touches `updated_at` without a
    version bump. Returns the updated record or None if missing/deleted.z0SELECT * FROM templates WHERE id=? AND deleted=0Nr   r       zdUPDATE templates SET name=?, definition=?, version=?,
                       updated_at=? WHERE id=?zINSERT INTO template_versions
                       (template_id, version, definition, created_at,
                        created_by)
                   VALUES (?, ?, ?, ?, ?)r1   z4UPDATE templates SET name=?, updated_at=? WHERE id=?)r   r   r   r;   r   r&   r5   r6   )
r7   r   r$   actorr   r*   r   new_namer    r8   s
             r   update_templaterM      s.    
 &hj &Dll>FHJ 	 ;& & & f"&,3v;Di.!jj!1r2G)nq(GLL27GS#68 LL- gwU[b9; LLF3$&/& &4 5& & &4 s.   C2%C&C2BC&C2&C/	+C22Dc                     t         5  t               5 }|j                  dt               | f      }|j                  dkD  cddd       cddd       S # 1 sw Y   nxY wddd       y# 1 sw Y   yxY w)ztSoft-delete (keeps id stable for any P3 run rows). Returns True if a
    live template was found and marked deleted.zEUPDATE templates SET deleted=1, updated_at=? WHERE id=? AND deleted=0r   N)r   r   r   r   rowcount)r7   r   curs      r   delete_templaterQ      sf     
  hj  Dll"fc], ||a	             s!   A%+A	A%A	A%%A.c                     t               5 }|j                  d| f      j                         }ddd       D cg c]  }|d   |d   |d   d c}S # 1 sw Y   &xY wc c}w )z:Version history (metadata) for one template, newest first.zgSELECT version, created_at, created_by FROM template_versions WHERE template_id=? ORDER BY version DESCNr    r!   
created_by)r    r!   rS   )r   r   rC   )r7   r   rF   rG   s       r   template_versionsrT      s|    	 Lt||8:=AAI 	L
 6:;01 )AlO\?, ; ;	L L;s   "AAAc                     d}| s|dz  }t               5 }|j                  |      j                         d   cd d d        S # 1 sw Y   y xY w)Nz#SELECT COUNT(*) AS n FROM templatesz WHERE deleted=0nr   r   r;   )r<   r=   r   s      r   template_countrX      sJ    -A		 /t||A'')#./ / /s   "A  A	okc                    dt        j                         j                  dd z   }t               }t	        j
                  d|xs i i      }t	        j
                  |xs i       }	t        5  t               5 }
|
j                  d|| |||xs d||xs df       |
j                  d||	f       ddd       ddd       t        |      S # 1 sw Y   xY w# 1 sw Y   t        |      S xY w)	zEPersist a run + its view-model snapshot. Returns the full run record.run_N   
date_rangezINSERT INTO runs
                   (id, template_id, template_version, ran_at, ran_by,
                    params, status)
               VALUES (?, ?, ?, ?, ?, ?, ?)r1   rY   zDINSERT INTO snapshots (run_id, tile_id, payload) VALUES (?, NULL, ?))
r2   r3   r4   r   r&   r5   r   r   r   get_run)template_idtemplate_versionran_by
view_modelr]   statusridr   paramsr8   r   s              r   
create_runrf     s     4::<##CR(
(C
&CZZz'7R89Fjj)r*G	 
hj 
D/ +/flF^t	 	R'N	
 
 3<
 
 
 3<s$   )C46C*CC	
CC$c           	          | y 	 | d   rt        j                  | d         ni }| d   | d   | d   | d   | d   | d   |j	                  d      xs i d	}|r||d
<   |S # t         j                  t        f$ r i }Y Uw xY w)Nre   r   r_   r`   ran_atra   rc   r]   )r   r_   r`   rh   ra   rc   r]   rb   )r&   r'   r(   r)   get)r*   with_view_modelrb   re   r,   s        r   _row_to_runrk   #  s    
{.1(mCM* $i=) 23h-h-h-jj.4"C &LJ   ), s   A A:9A:c                 t   t               5 }|j                  d| f      j                         }|
	 ddd       y|j                  d| f      j                         }ddd       d}	 t        j                  |d         }t        d|      S # 1 sw Y   4xY w# t        j
                  t        f$ r d}Y 7w xY w)z9Full run record including its stored view-model, or None.zSELECT * FROM runs WHERE id=?NzDSELECT payload FROM snapshots WHERE run_id=? ORDER BY id ASC LIMIT 1r8   T)rj   rb   )r   r   r;   r&   r'   r(   r)   rk   )rd   r   r*   snapvms        r   r^   r^   8  s    	 *tll:SFCLLN;* * ||v'xz 		* 
B	DO,B sDR@@* * $$i0 	B	s"   %B!B'B BB76B7c                 .   	 t        dt        t        |      d            }t               5 }|j                  d| |f      j                         }ddd       D cg c]  }t        |       c}S # t        t        f$ r d}Y aw xY w# 1 sw Y   9xY wc c}w )z<Run history (metadata only, no view-model) newest-run first.rJ     d   zLSELECT * FROM runs WHERE template_id=? ORDER BY ran_at DESC, id DESC LIMIT ?N)	maxminintr)   
ValueErrorr   r   rC   rk   r_   limitr   rF   rG   s        r   	list_runsrx   J  s    As3u:s+, 
 -t||4% " #+(* 	-
 %))qKN)) z" - -
 *"   A/ #BB/BBBc                     d}g }| |dz  }|j                  |        t               5 }|j                  ||      j                         d   cd d d        S # 1 sw Y   y xY w)NzSELECT COUNT(*) AS n FROM runsz WHERE template_id=?rV   )r?   r   r   r;   )r_   r=   rE   r   s       r   	run_countr{   X  sa    (AD	##K 	 5t||At$--/45 5 5s   #AA)fired_atrun_idemailed
recipients
tile_countrc   errorc                j   t         5  t               5 }	|	j                  d| ||xs
 t               ||rdndt	        j
                  t        |xs g             t        |xs d      |xs d|xs ddd f	      }
|
j                  cddd       cddd       S # 1 sw Y   nxY wddd       y# 1 sw Y   yxY w)z;Append a scheduled-run record. Returns the new record's id.zINSERT INTO schedule_runs
                   (template_id, run_date, fired_at, run_id, emailed,
                    recipients, tile_count, status, error)
               VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)rJ   r   rY   r1   Ni  )	r   r   r   r   r&   r5   listrt   	lastrowid)r_   run_dater|   r}   r~   r   r   rc   r   r   rP   s              r   record_schedule_runr   h  s     
 	hj 	Dll5 (H$6Qq$**T*2B-C"Dq!6>TEKR$3GIJ }}	 	 	 	 	 	 	s#   B)A/B	B)B	B))B2c                     | y 	 | d   rt        j                  | d         ng }| d   | d   | d   | d   | d   t	        | d         || d   | d	   | d
   xs dd
S # t         j                  t        f$ r g }Y Rw xY w)Nr   r   r_   r   r|   r}   r~   r   rc   r   r1   )
r   r_   r   r|   r}   r~   r   r   rc   r   )r&   r'   r(   r)   r%   )r*   recipss     r   _row_to_schedule_runr   x  s    
{25l2CC-. $i=)
O
Oh-I','h-W#    ), s   A A76A7c                     t               5 }|j                  d| |f      j                         }ddd       |duS # 1 sw Y   duS xY w)u   True if a scheduled run already landed for this template on run_date
    (the per-period fire guard). A prior error also counts — a failed fire is
    retried on the next due day, not re-hammered every minute.zFSELECT 1 FROM schedule_runs WHERE template_id=? AND run_date=? LIMIT 1NrW   )r_   r   r   r*   s       r   schedule_ran_onr     sX     
 ;tll#X.008
 	; d?	; d?s	   #:Ac                     t               5 }|j                  d| f      j                         }ddd       t        |      S # 1 sw Y   t              S xY w)z9Most-recent scheduled-run record for a template, or None.zHSELECT * FROM schedule_runs WHERE template_id=? ORDER BY id DESC LIMIT 1N)r   r   r;   r   )r_   r   r*   s      r   last_schedule_runr     sX    	 Ctll')488@
 	C  $$	C  $$s   "A  Ac                 .   	 t        dt        t        |      d            }t               5 }|j                  d| |f      j                         }ddd       D cg c]  }t        |       c}S # t        t        f$ r d}Y aw xY w# 1 sw Y   9xY wc c}w )zHScheduled-run history (newest first) for the Report Builder health view.rJ   rp   2   zHSELECT * FROM schedule_runs WHERE template_id=? ORDER BY id DESC LIMIT ?N)	rr   rs   rt   r)   ru   r   r   rC   r   rv   s        r   list_schedule_runsr     s    As3u:s+, 
 It||')4e(<>>Fhj 	I .22 #22 z" I I 3ry   )T)F)NF)NNr1   )NrY   )FN)rq   )N)r   )&__doc__r&   osr	   	threadingr2   r   pathlibr   environri   __file__resolveparentr   Lockr   r   r   r   r-   r9   r6   rH   rM   rQ   rT   rX   rf   rk   r^   rx   r{   r   r   r   r   r   r   r   r   <module>r      s   <  	     JJNN$>))+22\ACD
 innE;P8,(!F$B ;/" (,**A$*5  <@ %$1#2 *%
3  r   