
    iJ                        S r SSKrSSKrSSKrSSKrSSKJrJrJr  SSKJ	r	  \R                  " S5      r\	" \5      R                  5       R                  R                  r\S-  rSr " S S	5      rS
\4S jrSqS
\4S jrg)ut  
Oura Ring API v2 client.

Pulls sleep, HRV, steps, exercise, heart rate, and readiness data
directly from the Oura API — no Health Connect zip needed.

Bill wears his Oura ring, data syncs to Oura cloud, this module
pulls it via API and transforms it into dashboard-data.json format.

Docs: https://cloud.ouraring.com/v2/docs
Auth: Personal Access Token (Bearer token)
    N)datedatetime	timedelta)Pathzoura-api.oura-token.jsonzhttps://api.ouraring.comc                   ,   \ rS rSrSrSS\4S jjr\S\4S j5       r	S\4S jr
SS	\S
\S\4S jjrSS\S\S\4S jjrSS\S\4S jjrS\S\4S jrSS\S\4S jjrSS\S\4S jjrS S\S\S\4S jjrS!S\S\4S jjrS!S\S\4S jjrS\4S jrSrg)"
OuraClient   zOura Ring API v2 client.Naccess_tokenc                 N    U=(       d    U R                  5       U l        S U l        g N)_load_token_token
_available)selfr   s     //Users/bsyrros/clawd/whatsapp-agent/oura_api.py__init__OuraClient.__init__"   s    "8d&6&6&8    returnc                    U R                   cs  U R                  (       d(  SU l         [        R                  S5        U R                   $  U R	                  S5        SU l         [        R                  S5        U R                   $ U R                   $ ! [
         ao  nS[        U5      ;   d  S[        U5      ;   a  [        R                  S	U 35         SnAgSU l         [        R                  S
U 35         SnAU R                   $ SnAff = f)z/Check if Oura API is configured and accessible.NFz)Oura API: NOT CONFIGURED (no token found)z /v2/usercollection/personal_infoTz"Oura API: ACTIVE (token validated)CERTIFICATE_VERIFY_FAILEDSSLu/   Oura API: SSL error (will retry next call) — u   Oura API: UNAVAILABLE — )r   r   loggerinfo_api_get	Exceptionstrwarning)r   es     r   	availableOuraClient.available&   s     ??";;"'GH 
EMM"DE&*DOKK DE t ! E2c!f<Q)XYZX['\]$&+DONN%?s#CDDEs   -B 
D6DDDc                    [         R                  " SS5      R                  5       nU(       a$  [        R	                  S[        U5       S35        U$ [        R                  5       (       a|   [        R                  " [        R                  5       5      nUR                  SS5      R                  5       nU(       a+  [        R	                  S[         S[        U5       S35        U$  [        R                  " 5       S-  S-  nUR                  5       (       ac  U[        :w  aY   [        R                  " UR                  5       5      nUR                  SS5      nU(       a  [        R	                  SU 35        U$  [        R	                  S[         SU S35        g! [         a)  n[        R                  S[         S	U 35         S
nANS
nAff = f! [         a%  n[        R                  SU S	U 35         S
nANS
nAff = f)z#Load access token from file or env.OURA_ACCESS_TOKEN z Oura token loaded from env var (z chars)r   zOura token loaded from z (zFailed to load Oura token from : Nclawdr   z Oura token loaded from fallback z"No Oura token found (checked env, z, ))osgetenvstripr   r   len
TOKEN_PATHexistsjsonloads	read_textgetr   r   r   home)r   tokendatar    	home_paths        r   r   OuraClient._load_token<   s    		-r288:KK:3u:,gNOL Tzz*"6"6"894::<KK"9*RE
|SZ [\ L  IIK'),>>	)z"9Szz)"5"5"784KK"B9+ NO L  	8BykQRST!  T!@BqcRSST  S!@2aSQRRSs2   +A9F !AG 
G$GG
G?G::G?endpointparamsc                 z   [         U-   n[        R                  " UUSU R                   3SS.SS9nUR                  S:X  aU  [
        R                  SU R                  SS	  S
U R                  SS  35        UR                  5         UR                  5       $ UR                  S:X  a5  [
        R                  S5        UR                  5         UR                  5       $ UR                  S:w  aB  [
        R                  SUR                   SUR                  SS  35        UR                  5         UR                  5       $ )z#Make a GET request to the Oura API.zBearer zapplication/json)AuthorizationAccept   )r9   headerstimeouti  u&   Oura API 401: Unauthorized — token: N   z...i  u*   Oura API: Rate limited — try again later   zOura API error r&   )BASE_URLrequestsr2   r   status_coder   errorraise_for_statusr   textr/   )r   r8   r9   urlresps        r   r   OuraClient._api_get^   s&   !||#*4;;-!8, 
 s"LLA$++bq/ARRUVZVaVabdbeVfUghi!!# yy{ $NNGH!!#
 yy{	 $LL?4+;+;*<Btyy#>OPQ!!#yy{r   
start_dateend_datec                 .   U(       d  [         R                  " 5       nU(       d  U[        SS9-
  nU R                  SUR	                  5       UR	                  5       S.5      nUR                  S/ 5      nU(       d  g[        US S9nU R                  U5      $ )	zL
Get sleep data. Returns the most recent night's sleep
in dashboard format.
   days/v2/usercollection/sleeprL   rM   r5   Nc                 &    U R                  SS5      $ )Ntotal_sleep_durationr   )r2   )ss    r   <lambda>&OuraClient.get_sleep.<locals>.<lambda>   s    1551G+Kr   )key)r   todayr   r   	isoformatr2   max_transform_sleep)r   rL   rM   r5   sessionsmains         r   	get_sleepOuraClient.get_sleepz   s    
 zz|H!I1$55J}}7$..0 **,:
 
 88FB' 8!KL$$T**r   rQ   c                    [         R                  " 5       nU[        US9-
  nU R                  SUR	                  5       UR	                  5       S.5      n0 nUR                  S/ 5       HP  nUR                  SS5      nUR                  SS5      S	-  nU(       d  M3  Xu;  d
  XU   :  d  MB  [        US
5      XW'   MR     [        [        UR                  5       5      5      $ )z(Get sleep durations for the last N days.rP   rR   rS   r5   dayr%   rU   r     rO   )
r   rZ   r   r   r[   r2   rounddictsorteditems)	r   rQ   endstartr5   by_daterV   rc   hourss	            r   get_sleep_seriesOuraClient.get_sleep_series   s    jjliT**}}7//+:
  &"%A%%r"CEE0!4t;Es*ecl.B$UA	 & F7==?+,,r   sleepc           
         UR                  SS5      nUR                  SS5      nSnSn U(       a5  [        R                  " UR                  SS5      5      R	                  S5      nU(       a5  [        R                  " UR                  SS5      5      R	                  S5      n[        UR                  SS5      S	-  S
5      n[        UR                  SS5      S-  5      [        UR                  SS5      S-  5      [        UR                  SS5      S-  5      [        UR                  SS5      S-  5      S.nUR                  S5      nUc8  UR                  S0 5      n	[        U	[        5      (       a  U	R                  S5      nUR                  S[        R                  " 5       R                  5       5      UUUSUU(       a  [        US
5      S.$ SS.$ ! [
        [        4 a     GNEf = f)u3   Transform Oura sleep response → dashboard format.bedtime_startr%   bedtime_endZ+00:00%H:%MrU   r   rd   rO   deep_sleep_duration<   rem_sleep_durationlight_sleep_duration
awake_time)deep_minrem_min	light_min	awake_minaverage_hrvNhrv
mean_rmssdrc   Oura)r   bedtime	wake_timetotal_hourssourcestagesavg_hrv)r2   r   fromisoformatreplacestrftime
ValueError	TypeErrorre   
isinstancerf   r   rZ   r[   )
r   ro   bedtime_strwaketime_strr   r   r   r   r   hrv_datas
             r   r]   OuraClient._transform_sleep   s   ii4yy3 		"001D1DS(1ST]]^ef$22<3G3GX3VW``ahi	 EII&<a@4GK eii(=qABFGUYY';Q?"DEuyy)?CbHIuyyq9B>?	
 ))M*?yy+H(D))",,|4 IIeTZZ\%;%;%=>"&,3uWa(
 	
 :>
 	
+ I& 		s   A8G G('G(c                    [         R                  " 5       nU[        US9-
  nU R                  SUR	                  5       UR	                  5       S.5      n0 nUR                  S/ 5       H<  nUR                  SS5      nUR                  SS5      nU(       d  M0  US:  d  M8  XU'   M>     [        [        UR                  5       5      5      $ )	z*Get daily step counts for the last N days.rP   z!/v2/usercollection/daily_activityrS   r5   rc   r%   stepsr   )	r   rZ   r   r   r[   r2   rf   rg   rh   )	r   rQ   ri   rj   r5   r   day_datarc   counts	            r   get_daily_activityOuraClient.get_daily_activity   s    jjliT**}}@//+C
 
 ,H,,ub)CLL!,Esuqy"c
	 - F5;;=)**r   c           	      p   [         R                  " 5       nU[        US9-
  n/ n U R                  SUR	                  5       UR	                  5       S.5      nUR                  S/ 5       H-  nU R                  USS9nU(       d  M  UR                  U5        M/     [        R                  S[        UR                  S/ 5      5       S35         U R                  SUR	                  5       UR	                  5       S.5      nUR                  S/ 5       H-  n	U R                  U	SS9nU(       d  M  UR                  U5        M/     [        R                  S[        UR                  S/ 5      5       S35        [        5       n
/ nU H?  nUS    SUS    SUS    3nX;  d  M  U
R                  U5        UR                  U5        MA     UR                  S SS9  US
S $ ! [         a#  n[        R                  S	U 35         S
nAGN6S
nAff = f! [         a"  n[        R                  SU 35         S
nANS
nAff = f)zHGet exercise/workout sessions from both sessions and workouts endpoints.rP   z/v2/usercollection/sessionsrS   r5   session)r   zOura sessions endpoint: z entrieszOura sessions endpoint failed: Nz/v2/usercollection/workoutworkoutzOura workout endpoint: zOura workout endpoint failed: r   _timeduration_minc                     U S   U S   4$ )Nr   r    )xs    r   rW   )OuraClient.get_sessions.<locals>.<lambda>  s    1V9ai"8r   T)rY   reverse
   )r   rZ   r   r   r[   r2   _parse_exercise_entryappendr   r   r,   r   r   setaddsort)r   rQ   ri   rj   	exercisesr5   rV   parsedr    wseenuniqueexrY   s                 r   get_sessionsOuraClient.get_sessions   s   jjliT**		B==!>#oo/MMOA D XXfb)33Ai3H6$$V, * KK23txx7K3L2MXVW
	A==!=#oo/MMO@ D XXfb)33Ai3H6$$V, * KK1#dhhvr6J2K1LHUV
 uBZL"V*Qr./A.BCCb!	  	8$Gcr{9  	BNN<QC@AA	B  	ANN;A3?@@	As?   AG AG AH	 +AH	 
H#HH	
H5H00H5entryr   c                 R   UR                  SS5      n [        R                  " UR                  SS5      5      nUR                  SS5      nSnU(       aE   [        R                  " UR                  SS5      5      n[        Xt-
  R                  5       S-  5      nUR                  S	UR                  S
S5      5      n0 SS_SS_SS_SS_SS_SS_SS_SS_SS_SS_S S!_S"S#_S$S%_SS&_S'S(_S)S_S*S+_S,S0En	U	R                  XR                  S-S.5      R                  5       5      n
UR                  S/5      =(       d    UR                  S05      =(       d    U
nUR                  S15      UR                  S25      U
UUS3.$ ! [        [
        4 a     gf = f! [        [
        4 a     GNf = f)4z7Parse a session or workout entry into dashboard format.start_datetimer%   rs   rt   Nend_datetimer   rw   typeactivityothercyclingBikingrunningRunningwalkingWalkinghikingHikingswimmingSwimmingyogaYogastrength_trainingzStrength TraininghiitHIIT
elliptical
EllipticaldancingDancingpilatesPilates
stretching
Stretching
meditation
MeditationOtherindoor_cyclingSpinningoutdoor_cyclingindoor_running	Treadmilloutdoor_runningr    labelmoodz%Y-%m-%dru   )r   r   r   namer   )
r2   r   r   r   r   r   re   total_secondstitler   )r   r   r   	start_strstart_dtend_strr   end_dtactivity_typetype_mapetyper   s               r   r    OuraClient._parse_exercise_entry  s&   II.3		--i.?.?X.NOH
 ))NB/!//X0NO$f&7%F%F%H2%MN
 		&%))J*HI	
x	
!*I	
7@)	
h	
 *J	
8>	
  !4	
 7=f	
 ,		
 )29		

 y	

 #/	
 ,	
 )0	
 j	
 +<X	
 k	
 ,=i	
 ],A,A#s,K,Q,Q,ST yy!?UYYv%6?% %%j1%%g.(
 	
= I& 		 	* s$   &E< AF <FFF&%F&c                    [         R                  " 5       nU[        US9-
  nU R                  SUR	                  5       UR	                  5       S.5      nUR                  S/ 5      nU(       d  gU Vs/ s H  nSU;   d  M  US   PM     nnU(       d  g[        [        U5      [        U5      -  S5      [        U5      [        U5      [        U5      S.$ s  snf )	zGet heart rate data summary.rP   z/v2/usercollection/heartraterS   r5   NbpmrO   )avgminr\   readings)r   rZ   r   r   r[   r2   re   sumr,   r   r\   )r   rQ   ri   rj   r5   r   rbpmss           r   get_heart_rateOuraClient.get_heart_rateE  s    jjliT**}};//+>
 
 88FB'"*9(Qeqj%(9 TSY.2t9t9D		
 	
	 :s   1
C?	Cc                 L   [         R                  " 5       nU[        US9-
  nU R                  SUR	                  5       UR	                  5       S.5      nUR                  S/ 5      nU(       d  gUS   nUR                  S5      UR                  S5      UR                  S	0 5      S
.$ )zGet daily readiness score.rP   z"/v2/usercollection/daily_readinessrS   r5   Nscorerc   contributors)r   r   r   )r   rZ   r   r   r[   r2   )r   rQ   ri   rj   r5   entrieslatests          r   get_readinessOuraClient.get_readiness`  s    jjliT**}}A//+D
 
 ((62&ZZ(JJu%"JJ~r:
 	
r   c                    0 n/ n[         R                  " 5       n U R                  5       nU(       a?  XAS'   U R                  S5      nU(       a  XQS   S'   [        R                  SUS    S35         U R                  S5      nU(       a7  XsR                  5       S.US'   [        R                  S[        U5       S35         U R                  S5      nU(       a7  XR                  5       S.US'   [        R                  S[        U5       S35         U R                  5       n	U	(       a   XS'   [        R                  SU	S    S35         U R                  5       n
U
(       a+  XS'   [        R                  S U
R                  S!5       35        U(       a(  X!S$'   [        R                  S%[        U5       S&U 35        U$ ! [         a6  nUR                  SU 35        [        R                  SU 3S	S
9   SnAGNSnAff = f! [         a6  nUR                  SU 35        [        R                  SU 3S	S
9   SnAGNzSnAff = f! [         a6  nUR                  SU 35        [        R                  SU 3S	S
9   SnAGNmSnAff = f! [         a6  nUR                  SU 35        [        R                  SU 3S	S
9   SnAGNxSnAff = f! [         a6  nUR                  S"U 35        [        R                  S#U 3S	S
9   SnAGNxSnAff = f)'u   
Pull all available health data from Oura.
Returns dict in dashboard-ready format.
Each metric is independent — if one fails, others still return.
Also stores errors in result["_errors"] for debugging.
ro      	series14dzOura sleep: r   hzsleep: zOura sleep fetch failed: T)exc_infoN   )dailyupdatedr   zOura steps: z dayszsteps: zOura steps fetch failed: )recentr   exercisezOura exercise: 	 sessionsz
exercise: zOura exercise fetch failed: 
heart_ratezOura HR: avg r   z bpmzheart_rate: zOura heart rate fetch failed: 	readinesszOura readiness: r   zreadiness: zOura readiness fetch failed: _errorszOura fetch completed with z	 errors: )r   rZ   r`   rm   r   r   r   r   r   r   r[   r,   r   r   r   r2   )r   resulterrorsrZ   ro   seriesr    r   r   hrr  s              r   fetch_all_health_data OuraClient.fetch_all_health_dataw  s    


	KNN$E"'w..r2397OK0l5+?*@BC	K++A.E,1oo>O"Pwl3u:,e<=	N))!,I09ooFW%Xz"oc)n-=YGH	P$$&B')|$mBuI;d;<	O**,I&/{#.y}}W/E.FGH
  &9NN7F}IfXVWa  	KMMGA3-(NN6qc:TNJ	K  	KMMGA3-(NN6qc:TNJ	K  	NMMJqc*+NN9!=NM	N  	PMML,-NN;A3?$NO	P  	OMMKs+,NN:1#>NN	Os{   AF= 2AH  AI 7J 
AK	 =
G=+G88G= 
I 
+H;;I 
J+I>>J
K+KK	
L	+LL	)r   r   r   )NN)r   )r   )r   )rO   )__name__
__module____qualname____firstlineno____doc__r   r   propertyboolr!   r   rf   r   r   r`   intrm   r]   r   listr   r   r   r   r  __static_attributes__r   r   r   r	   r	      s
   "S  4  * S  D d d 8+D +4 +4 +.-S -$ -(*
d *
t *
\+s +4 +*. .T .`)
4 )
 )
T )
Z
3 
t 
6
# 
d 
.Dt Dr   r	   r   c                    ^^	 [        5       nUR                  (       d  g UR                  5       m	T	R                  S	/ 5      nT	(       d(  U(       a   S
R                  S USS  5       5      nSU 3$ g/ mUU	4S jnU R                  U5        SSR                  S T 5       5      -   n[        R                  U5        U$ ! [         a6  n[	        U5      nSU;   d  SU;   a  SUSS  3s SnA$ SUSS  3s SnA$ SnAff = f)zM
Pull data from Oura API and update the dashboard.
Returns a summary string.
z6Oura API not configured. Run setup_oura_auth.py first.r   r   u9   Oura API SSL error — try: pip3 install certifi
Detail: Nx   zOura API error:    r  z; c              3   >   #    U  H  n[        U5      S S v   M     g 7f)NP   )r   ).0r    s     r   	<genexpr>#ingest_oura_data.<locals>.<genexpr>  s     #J9IACF3BK9Is      zOura API calls failed:
zNo data returned from Oura API.c                 ,  > ST;   a#  TS   U S'   TR                  STS   S    S35        ST;   aW  TS   U S'   TS   S   R                  [        R                  " 5       R	                  5       S5      nTR                  SUS	 S
35        ST;   a,  TS   U S'   TR                  S[        TS   S   5       S35        ST;   a/  TS   U S'   TR                  STS   R                  SS5       35        [        R                  " 5       R	                  5       U S'   g )Nro   zSleep: r   r   r   r   r   zSteps: ,z todayr  z
Exercise: r  r  r  zReadiness: r   ?r   )r   r2   r   rZ   r[   r,   )	dashboardtotal_todaychangesr5   s     r   updater!ingest_oura_data.<locals>.updater  s'    d?!%gIgNNWT']=%A$B!DE d?!%gIgw-044TZZ\5K5K5MqQKNNW[O6:; $($4Ij!NNZD,<X,F(G'H	RS $%)+%6Ik"NN[k):)>)>w)L(MNO JJL224	&r   zOura data updated:

c              3   ,   #    U  H
  nS U 3v   M     g7f)u     • Nr   )r  cs     r   r  r    s     0Ow!6!ws   )
r	   r   r  r   r   popjoin_read_and_writer   r   )
dashboard_managerclientr    errfetch_errorserr_summaryr&  summaryr%  r5   s
           @@r   ingest_oura_datar4    s   
 \F ==G.++- 88Ir*L))#Jbq9I#JJK-k];;0G54 %%g.$tyy0Ow0O'OOG
KKN_  .!f&#-#OPSTXUXPY{[[!#ds)--	.s)   B= =
C=C8%C=+C82C=8C=c                  0    [         c
  [        5       q [         $ )zGet singleton OuraClient.)_clientr	   r   r   r   
get_clientr7    s     ,Nr   )r  r)   r/   loggingrD   r   r   r   pathlibr   	getLoggerr   __file__resolveparent	_BASE_DIRr-   rC   r	   r   r4  r6  r7  r   r   r   <module>r?     s    
    . . 			:	& N""$++22	++
 &\ \~=3 =B J r   