o
    «Ÿ­iD  ã                   @   sœ   d 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	 e 
d¡Ze	eƒ ¡ jjZed ZdZG dd	„ d	ƒZd
efdd„Zdad
efdd„ZdS )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                   @   sô   e Zd ZdZd&defdd„Zedefdd„ƒZdefd	d
„Z	d&dede
de
fdd„Zd'dedede
fdd„Zd(dede
fdd„Zde
de
fdd„Zd)dede
fdd„Zd)dedefdd„Zd*dede
fd d!„Zd*dede
fd"d#„Zde
fd$d%„ZdS )+Ú
OuraClientzOura Ring API v2 client.NÚaccess_tokenc                 C   s   |p|   ¡ | _d | _d S ©N)Ú_load_tokenÚ_tokenÚ
_available)Úselfr   © r   úoura_api.pyÚ__init__"   s   
zOuraClient.__init__Úreturnc              
   C   sÄ   | j du r_| jsd| _ t d¡ | j S z|  d¡ d| _ t d¡ W | j S  ty^ } z.dt|ƒv s8dt|ƒv rGt d	|› ¡ W Y d}~dS d| _ t d
|› ¡ W Y d}~| j S d}~ww | j S )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   Úer   r   r   Ú	available&   s(   

õ
ù€ùzOuraClient.availablec              
   C   sj  t  dd¡ ¡ }|rt dt|ƒ› d¡ |S t ¡ r]z$t 	t 
¡ ¡}| dd¡ ¡ }|r>t dt› dt|ƒ› d¡ |W S W n ty\ } zt dt› d	|› ¡ W Y d
}~nd
}~ww t ¡ d d }| ¡ r§|tkr§zt 	| 
¡ ¡}| dd¡}|rˆt d|› ¡ |W S W n ty¦ } zt d|› d	|› ¡ W Y d
}~nd
}~ww t dt› d|› d¡ dS )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_pathr   r   r   r
   <   s>   þ"€ÿþ"€ÿzOuraClient._load_tokenÚendpointÚparamsc                 C   sÌ   t | }tj||d| j› ddœdd}|jdkr6t d| jdd	… › d
| jdd… › ¡ | ¡  | 
¡ S |jdkrHt d¡ | ¡  | 
¡ S |jdkrbt d|j› d|j	dd… › ¡ | ¡  | 
¡ S )z#Make a GET request to the Oura API.zBearer zapplication/json)ÚAuthorizationÚAccepté   )r0   Ú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Úrequestsr*   r   Ústatus_coder   ÚerrorÚraise_for_statusr   Útextr'   )r   r/   r0   ÚurlÚrespr   r   r   r   ^   s*   
þù

*
ù

ü"zOuraClient._api_getÚ
start_dateÚend_datec                 C   sf   |st  ¡ }|s|tdd }|  d| ¡ | ¡ dœ¡}| dg ¡}|s&dS t|dd„ d	}|  |¡S )
zd
        Get sleep data. Returns the most recent night's sleep
        in dashboard format.
        é   ©Údaysú/v2/usercollection/sleep©rA   rB   r-   Nc                 S   s   |   dd¡S )NÚtotal_sleep_durationr   )r*   )Úsr   r   r   Ú<lambda>Ž   s    z&OuraClient.get_sleep.<locals>.<lambda>)Úkey)r   Útodayr   r   Ú	isoformatr*   ÚmaxÚ_transform_sleep)r   rA   rB   r-   ÚsessionsÚmainr   r   r   Ú	get_sleepz   s   þ
zOuraClient.get_sleepé   rE   c           	      C   s˜   t  ¡ }|t|d }|  d| ¡ | ¡ dœ¡}i }| dg ¡D ]#}| dd¡}| dd¡d	 }|rC||vs<||| krCt|d
ƒ||< q tt| 	¡ ƒƒS )z(Get sleep durations for the last N days.rD   rF   rG   r-   Údayr   rH   r   é  rC   )
r   rL   r   r   rM   r*   ÚroundÚdictÚsortedÚitems)	r   rE   ÚendÚstartr-   Úby_daterI   rT   Úhoursr   r   r   Úget_sleep_series‘   s   þ€zOuraClient.get_sleep_seriesÚsleepc           
   	   C   s@  |  dd¡}|  dd¡}d}d}z|rt | dd¡¡ d¡}|r-t | dd¡¡ d¡}W n ttfy9   Y nw t|  dd¡d	 d
ƒ}t|  dd¡d ƒt|  dd¡d ƒt|  dd¡d ƒt|  dd¡d ƒdœ}|  d¡}|du r…|  di ¡}	t|	t	ƒr…|	  d¡}|  dt
 ¡  ¡ ¡|||d||rœt|d
ƒdœS ddœS )u3   Transform Oura sleep response â†’ dashboard format.Úbedtime_startr   Úbedtime_endÚZú+00:00ú%H:%MrH   r   rU   rC   Údeep_sleep_durationé<   Úrem_sleep_durationÚlight_sleep_durationÚ
awake_time)Údeep_minÚrem_minÚ	light_minÚ	awake_minÚaverage_hrvNÚhrvÚ
mean_rmssdrT   ÚOura)r   ÚbedtimeÚ	wake_timeÚtotal_hoursÚsourceÚstagesÚavg_hrv)r*   r   ÚfromisoformatÚreplaceÚstrftimeÚ
ValueErrorÚ	TypeErrorrV   Ú
isinstancerW   r   rL   rM   )
r   r_   Úbedtime_strÚwaketime_strrr   rs   rt   rv   rw   Úhrv_datar   r   r   rO   ¥   sD   €ÿü


ùùzOuraClient._transform_sleepé   c           	      C   s‚   t  ¡ }|t|d }|  d| ¡ | ¡ dœ¡}i }| dg ¡D ]}| dd¡}| dd¡}|r8|dkr8|||< q tt| ¡ ƒƒS )	z*Get daily step counts for the last N days.rD   z!/v2/usercollection/daily_activityrG   r-   rT   r   Ústepsr   )	r   rL   r   r   rM   r*   rW   rX   rY   )	r   rE   rZ   r[   r-   r‚   Úday_datarT   Úcountr   r   r   Úget_daily_activityÓ   s   þ€zOuraClient.get_daily_activityc                 C   sd  t  ¡ }|t|d }|  d| ¡ | ¡ dœ¡}g }| dg ¡D ]‚}| dd¡}zt | dd¡¡}W n t	t
fy>   Y q w | d	d¡}	d
}
|	rjzt |	 dd¡¡}t||  ¡ d ƒ}
W n t	t
fyi   Y nw | dd¡}dddddddddddddddœ}| || dd¡ ¡ ¡}| | d¡| d ¡|| d!|¡|
d"œ¡ q |jd#d$„ d%d& |d'd(… S ))zGet exercise/workout sessions.rD   z/v2/usercollection/sessionsrG   r-   Ústart_datetimer   rb   rc   Úend_datetimer   rf   ÚtypeÚotherÚBikingÚRunningÚWalkingÚHikingÚSwimmingÚYogazStrength TrainingÚHIITÚ
EllipticalÚDancingÚPilatesÚ
StretchingÚ
MeditationÚOther)ÚcyclingÚrunningÚwalkingÚhikingÚswimmingÚyogaÚstrength_trainingÚhiitÚ
ellipticalÚdancingÚpilatesÚ
stretchingÚ
meditationr‰   Ú_ú z%Y-%m-%drd   Úmood)r   Útimerˆ   ÚnameÚduration_minc                 S   s   | d | d fS )Nr   r§   r   )Úxr   r   r   rJ     s    z)OuraClient.get_sessions.<locals>.<lambda>T)rK   ÚreverseNé
   )r   rL   r   r   rM   r*   r   rx   ry   r{   r|   rV   Útotal_secondsÚtitleÚappendrz   Úsort)r   rE   rZ   r[   r-   Ú	exercisesrI   Ú	start_strÚstart_dtÚend_strr©   Úend_dtÚactivity_typeÚtype_mapÚetyper   r   r   Úget_sessionsè   sR   þÿÿú

û	zOuraClient.get_sessionsrC   c                 C   s†   t  ¡ }|t|d }|  d| ¡ | ¡ dœ¡}| dg ¡}|s"dS dd„ |D ƒ}|s-dS tt|ƒt|ƒ dƒt	|ƒt
|ƒt|ƒd	œS )
zGet heart rate data summary.rD   z/v2/usercollection/heartraterG   r-   Nc                 S   s   g | ]
}d |v r|d  ‘qS )Úbpmr   )Ú.0Úrr   r   r   Ú
<listcomp>,  s    z-OuraClient.get_heart_rate.<locals>.<listcomp>rC   )ÚavgÚminrN   Úreadings)r   rL   r   r   rM   r*   rV   Úsumr$   r¿   rN   )r   rE   rZ   r[   r-   rÀ   Úbpmsr   r   r   Úget_heart_rate  s"   þüzOuraClient.get_heart_ratec                 C   sl   t  ¡ }|t|d }|  d| ¡ | ¡ dœ¡}| dg ¡}|s"dS |d }| d¡| d¡| d	i ¡d
œS )zGet daily readiness score.rD   z"/v2/usercollection/daily_readinessrG   r-   NéÿÿÿÿÚscorerT   Úcontributors)rÅ   r   rÆ   )r   rL   r   r   rM   r*   )r   rE   rZ   r[   r-   ÚentriesÚlatestr   r   r   Úget_readiness9  s   þ
ýzOuraClient.get_readinessc              
   C   sÌ  i }g }t  ¡ }z$|  ¡ }|r+||d< |  d¡}|r ||d d< t d|d › d¡ W n$ tyP } z| d|› ¡ tjd|› d	d
 W Y d}~nd}~ww z|  	d¡}|rm|| 
¡ dœ|d< t dt|ƒ› d¡ W n$ ty’ } z| d|› ¡ tjd|› d	d
 W Y d}~nd}~ww z|  d¡}|r¯|| 
¡ dœ|d< t dt|ƒ› d¡ W n$ tyÔ } z| d|› ¡ tjd|› d	d
 W Y d}~nd}~ww z|  ¡ }	|	rë|	|d< t d|	d › d¡ W n% ty } z| d|› ¡ tjd|› d	d
 W Y d}~nd}~ww z|  ¡ }
|
r)|
|d< t d |
 d!¡› ¡ W n% tyO } z| d"|› ¡ tjd#|› d	d
 W Y d}~nd}~ww |rd||d$< t d%t|ƒ› d&|› ¡ |S )'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.
        r_   rS   Ú	series14dzOura sleep: rt   Úhzsleep: zOura sleep fetch failed: T)Úexc_infoNr   )Ú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   rL   rR   r^   r   r   r   r¯   r   r…   rM   r$   r¹   rÃ   rÉ   r*   )r   ÚresultÚerrorsrL   r_   Úseriesr   r‚   r±   ÚhrrÓ   r   r   r   Úfetch_all_health_dataP  s‚   
€ €þ
€ €þ
€ €þ€ €þ€ €þz OuraClient.fetch_all_health_datar	   )NN)rS   )r   )rC   )Ú__name__Ú
__module__Ú__qualname__Ú__doc__r   r   ÚpropertyÚboolr   r
   rW   r   r   rR   Úintr^   rO   r…   Úlistr¹   rÃ   rÉ   rÙ   r   r   r   r   r      s    ".6r   r   c              
      s
  t ƒ }|jsdS z| ¡ ‰W n6 tyD } z*t|ƒ}d|v s"d|v r1d|dd… › W  Y d}~S d|dd… › W  Y d}~S d}~ww ˆ d	g ¡}ˆsd|rbd
 dd„ |dd… D ƒ¡}d|› S dS g ‰ ‡ ‡fdd„}|  |¡ dd dd„ ˆ D ƒ¡ }t 	|¡ |S )zY
    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: Néx   zOura API error: é–   rÔ   z; c                 s   s     | ]}t |ƒd d… V  qd S )NéP   )r   )r»   r   r   r   r   Ú	<genexpr>°  s   € z#ingest_oura_data.<locals>.<genexpr>é   zOura API calls failed:
zNo data returned from Oura API.c                    sè   dˆv rˆd | d< ˆ   dˆd d › d¡ dˆv r9ˆd | d< ˆd d  t ¡  ¡ d¡}ˆ   d|d	›d
¡ dˆv rRˆd | d< ˆ   dtˆd d ƒ› d¡ dˆv rjˆd | d< ˆ   dˆd  dd¡› ¡ t ¡  ¡ | d< d S )Nr_   zSleep: rt   rË   r‚   rÍ   r   zSteps: ú,z todayrÐ   z
Exercise: rÏ   rÑ   rÓ   zReadiness: rÅ   ú?r   )r¯   r*   r   rL   rM   r$   )Ú	dashboardÚtotal_today©Úchangesr-   r   r   Úupdater¶  s   z!ingest_oura_data.<locals>.updaterzOura data updated:
Ú
c                 s   s    | ]}d |› V  qdS )u     â€¢ Nr   )r»   Úcr   r   r   rå   Ò  s   € )
r   r   rÙ   r   r   ÚpopÚjoinÚ_read_and_writer   r   )Údashboard_managerÚclientr   ÚerrÚfetch_errorsÚerr_summaryrí   Úsummaryr   rë   r   Úingest_oura_data—  s0   €ü


rù   c                   C   s   t du rtƒ a t S )zGet singleton OuraClient.N)Ú_clientr   r   r   r   r   Ú
get_clientÛ  s   rû   )rÝ   r!   r'   Úloggingr:   r   r   r   Úpathlibr   Ú	getLoggerr   Ú__file__ÚresolveÚparentÚ	_BASE_DIRr%   r9   r   r   rù   rú   rû   r   r   r   r   Ú<module>   s"    
  zA