
    5j                     ^   d 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
 Zd Zd Zd Zd ZedfdZeddfdZd Zd Zd ZddZd Z e        y)u6  
powerbi_store.py — SQLite cache + snapshot store for Power BI data (DVI-1243).

Togen reads the Power BI REST API live-first (org service principal,
client-credentials) and falls back to this local store so dashboard tiles and
Auditor reports never go blank when Power BI is slow, throttled, or down —
the same board-approved hybrid design as the Idencia service (D4 mirrors
DVI-1226 D2). The background vocab sync (org scope) and every successful live
query (any scope) write through here.

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

Schema
------
cache(scope, cache_key, payload, fetched_at) — last-good JSON result per
    (connection scope, query key). Scope is "org" today (DVI-1243 D1-A: org
    service principal only); "user:<email>" is reserved for the deferred
    per-user delegated-access follow-up (D1-B) so cache entries stay
    scope-keyed from day one. UPSERT on (scope, cache_key); this is a
    latest-value store, not history.
snapshots(run_ts, scope, kind, payload) — append-only rows written by each
    sync run (workspace/report/dataset inventory counts, dataset refresh
    outcomes), the over-time data source for Auditor Reporting (P3). Kept
    separate from cache so trends survive cache overwrites.
meta(key, value) — sync health bookkeeping (last run, error, counts) for the
    Admin → Services → Power BI pane, mirroring the SCADA collector-health
    pattern.
    N)datetime)PathPOWERBI_DB_FILEz
powerbi.dborgc                      t        j                  t        t              d      } | j	                  d       | j	                  d       | S )N   )timeoutzPRAGMA journal_mode=WALzPRAGMA busy_timeout=10000)sqlite3connectstrr   executeconns    $/var/www/html/togen/powerbi_store.py_connectr   1   s6    ??3/<DLL*+LL,-K    c                      t         5  t               5 } | 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 cache (
                   scope TEXT NOT NULL,
                   cache_key TEXT NOT NULL,
                   payload TEXT NOT NULL,
                   fetched_at TEXT NOT NULL,
                   UNIQUE(scope, cache_key)
               )a  CREATE TABLE IF NOT EXISTS snapshots (
                   id INTEGER PRIMARY KEY AUTOINCREMENT,
                   run_ts TEXT NOT NULL,
                   scope TEXT NOT NULL,
                   kind TEXT NOT NULL,
                   payload TEXT NOT NULL
               )zZCREATE INDEX IF NOT EXISTS idx_snapshots_kind_ts
               ON snapshots(kind, run_ts)zyCREATE TABLE IF NOT EXISTS meta (
                   key TEXT PRIMARY KEY,
                   value TEXT
               )N)_write_lockr   r   r   s    r   init_powerbi_dbr   8   s    	 hj D	 		 	-	. 		)     s#   A4AA(A4(A1	-A44A=c                  H    t        j                         j                  d      S )Nz%Y-%m-%d %H:%M:%S)r   nowstrftime r   r   _nowr   U   s    <<>""#677r   c                 ^    dt        | xs d      j                         j                         z   S )zScope key for a personal connection ("user:<email>", lowercased).
    Reserved for the D1-B per-user delegated-access follow-up.zuser: )r   striplower)emails    r   
user_scoper    Y   s*     S"%++-33555r   c           
          t         5  t               5 }|j                  d| |t        j                  |      t               f       ddd       ddd       y# 1 sw Y   xY w# 1 sw Y   yxY w)zYStore the last-good payload for (scope, key). payload is any
    JSON-serializable value.zINSERT INTO cache (scope, cache_key, payload, fetched_at)
               VALUES (?, ?, ?, ?)
               ON CONFLICT(scope, cache_key)
               DO UPDATE SET payload=excluded.payload,
                             fetched_at=excluded.fetched_atN)r   r   r   jsondumpsr   )scope	cache_keypayloadr   s       r   	cache_putr'   a   sa     
 =hj =D?
 Itzz'2DF;	== = = = = =s"   A!2AA!A	A!!A*c                    t               5 }|j                  d| |f      j                         }ddd       sy	 t        j                  |d         |d   dS # 1 sw Y   +xY w# t        j
                  t        f$ r Y yw xY w)z|Return {"payload": ..., "fetched_at": "..."} or None when never
    cached. Corrupt rows read as a miss rather than raising.zCSELECT payload, fetched_at FROM cache WHERE scope=? AND cache_key=?Nr      )r&   
fetched_atr   r   fetchoner"   loadsJSONDecodeError	TypeError)r$   r%   r   rows       r   	cache_getr1   n   s     
 +tll-I  !)
 	+
 ::c!f-SVDD+ +   ), s   #AA$ A!$B ?B c                     t               5 }|j                  d| f      j                         }ddd       D cg c]  }|d   |d   d c}S # 1 sw Y   "xY wc c}w )zDList cached keys for a scope with their freshness (admin surfacing).zHSELECT cache_key, fetched_at FROM cache WHERE scope=? ORDER BY cache_keyNr   r)   )r%   r*   )r   r   fetchall)r$   r   rowsrs       r   
cache_keysr6   ~   sf    	 8t||"$)8--5XZ 	8 >BB!A$ad3BB	8 8 Cs   "AAAc                     t         5  t               5 }|j                  d|xs
 t               || t	        j
                  |      f       ddd       ddd       y# 1 sw Y   xY w# 1 sw Y   yxY w)z4Append one snapshot row (payload JSON-serializable).zHINSERT INTO snapshots (run_ts, scope, kind, payload) VALUES (?, ?, ?, ?)N)r   r   r   r   r"   r#   )kindr&   r$   run_tsr   s        r   add_snapshotr:      sk    	 Bhj BD#tvudDJJw,?@	BB B B B B Bs"   A%6AA%A"	A%%A.  c           	         t        dt        t        |xs d      d            }d}| |g}|r|dz  }|j                  |       |dz  }|j                  |       t	               5 }|j                  ||      j                         }ddd       g }t              D ]-  \  }	}
	 |j                  |	t        j                  |
      d       / |S # 1 sw Y   HxY w# t        j                  t        f$ r Y Yw xY w)	u{   Snapshot rows for one kind, oldest→newest (chart-ready). ``since`` is
    an inclusive "YYYY-MM-DD HH:MM:SS" lower bound.r)   r;   i  z>SELECT run_ts, payload FROM snapshots WHERE kind=? AND scope=?z AND run_ts>=?z& ORDER BY run_ts DESC, id DESC LIMIT ?N)r9   r&   )maxminintappendr   r   r3   reversedr"   r-   r.   r/   )r8   r$   sincelimitqargsr   r4   outr9   r&   s              r   	snapshotsrG      s     3s5<C($/0E
%A%=D	E	11AKK	 0t||At$--/0
C#D> 	JJ&TZZ5HIJ
 J0 0 $$i0 		s   "!C
'C
CC21C2c                      t               5 } | j                  d      j                         d   cd d d        S # 1 sw Y   y xY w)NzSELECT COUNT(*) FROM snapshotsr   )r   r   r,   r   s    r   snapshot_countrI      s=    	 Lt||<=FFHKL L Ls	   "7A c           	          t         5  t               5 }|j                  d| t        j                  |      f       d d d        d d d        y # 1 sw Y   xY w# 1 sw Y   y xY w)Nz_INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value)r   r   r   r"   r#   )keyvaluer   s      r   set_metarM      sX    	 &hj &DC$**U#$	&& & & & & &s!   A(AAA	AA c                     t               5 }|j                  d| f      j                         }d d d        sy 	 t        j                  |d         S # 1 sw Y   %xY w# t        j
                  t        f$ r Y y w xY w)Nz"SELECT value FROM meta WHERE key=?r   r+   )rK   r   r0   s      r   get_metarO      s    	 Etll03&::B(* 	E zz#a&!!E E   ), s   "AA AA98A9c                     | t               |xs d|xs i d}|rt        |      dd |d<   t        d|       | dk(  rt        d|       yy)	zCRecord one sync run's outcome for the admin pane. status: ok|error.r   )statusatactorcountsNr;   error	last_syncoklast_sync_ok)r   r   rM   )rQ   rS   rU   rT   recs        r   record_syncrZ      sT    46EKR\r#C5z$3'G[#~% r   c                  0    t        d      t        d      dS )zC{"last": ..., "last_ok": ...} for the Admin pane (None when never).rV   rX   )lastlast_ok)rO   r   r   r   sync_healthr^      s    [)h~6NOOr   )r   NN)__doc__r"   osr
   	threadingr   pathlibr   environget__file__resolveparentr   Lockr   	ORG_SCOPEr   r   r   r    r'   r1   r6   r:   rG   rI   rM   rO   rZ   r^   r   r   r   <module>rj      s   >  	    JJNN$>))+22\ACD
 inn	:86
= C '0 B $4s .L&	&P
  r   