11import logging
2- import sys
32from contextlib import asynccontextmanager
43from datetime import datetime , UTC
54from typing import Annotated , Any
@@ -39,14 +38,20 @@ def transform_card_number_to_mifare(card_number: str):
3938
4039class User (BaseModel ):
4140 uid : Annotated [str , Field (validation_alias = "username" )]
42- mifare_card_ids : Annotated [list [str ], Field (validation_alias = AliasPath ("attributes" , "mifareCardId" ))] = []
43- unique_card_ids : Annotated [list [str ], Field (validation_alias = AliasPath ("attributes" , "uniquecardId" ))] = []
41+ mifare_card_ids : Annotated [
42+ list [str ], Field (validation_alias = AliasPath ("attributes" , "mifareCardId" ))
43+ ] = []
44+ unique_card_ids : Annotated [
45+ list [str ], Field (validation_alias = AliasPath ("attributes" , "uniquecardId" ))
46+ ] = []
4447 membership_expiration : Annotated [
4548 int ,
46- Field (validation_alias = AliasPath ("attributes" , "membershipExpirationTimestamp" ))
49+ Field (
50+ validation_alias = AliasPath ("attributes" , "membershipExpirationTimestamp" )
51+ ),
4752 ]
4853
49- @field_validator ("mifare_card_ids" , "unique_card_ids" , mode = ' plain' )
54+ @field_validator ("mifare_card_ids" , "unique_card_ids" , mode = " plain" )
5055 def use_default_for_missing_cards (cls , v ) -> str :
5156 if v is None :
5257 raise PydanticUseDefault ()
@@ -57,7 +62,7 @@ class Settings(BaseSettings):
5762 authentik_token : str | None = None
5863 authentik_token_file : str | None = None
5964
60- @model_validator (mode = ' after' )
65+ @model_validator (mode = " after" )
6166 def set_token (self ) -> "Settings" :
6267 if self .authentik_token :
6368 return self
@@ -96,24 +101,28 @@ def auth():
96101
97102async def fetch () -> list [User ]:
98103 async with httpx .AsyncClient (
99- headers = {"Authorization" : f"Bearer { config .authentik_token } " },
100- timeout = 30 .0 ,
101- ) as client :
104+ headers = {"Authorization" : f"Bearer { config .authentik_token } " },
105+ timeout = 60 .0 ,
106+ ) as client :
102107 url = (
103108 "https://auth.apps.hskrk.pl/api/v3/core/users/?"
104- " attributes={\ " membershipExpirationTimestamp__gt\ " : 100}&page_size=50"
109+ ' attributes={"membershipExpirationTimestamp__gt": 100}&page_size=50'
105110 )
106111 response = await client .get (url )
107112 parsed_response = response .json ()
108113 results = parsed_response ["results" ]
109- while parsed_response ["pagination" ]["next" ]:
110- response = await client .get (f"{ url } &page={ parsed_response ["pagination" ]["next" ]} " )
111- parsed_response = response .json ()
112- results += parsed_response ["results" ]
113- return [
114- User (** u )
115- for u in results
116- ]
114+ next_page = parsed_response ["pagination" ]["next" ]
115+ while next_page :
116+ try :
117+ response = await client .get (f"{ url } &page={ next_page } " )
118+ parsed_response = response .json ()
119+ results += parsed_response ["results" ]
120+ next_page = parsed_response ["pagination" ]["next" ]
121+ except httpx .ReadTimeout as t :
122+ logging .warning (
123+ f"[HTTP] Timeout reached on fetching users data, page: { next_page } , { t } "
124+ )
125+ return [User (** u ) for u in results ]
117126
118127
119128@repeat_every (seconds = 300 )
@@ -135,30 +144,46 @@ async def fetch_users():
135144 users_by_card = {
136145 ** {
137146 mifare .lower (): user
138- for user in users for mifare in user .mifare_card_ids
147+ for user in users
148+ for mifare in user .mifare_card_ids
139149 },
140150 ** {
141151 transform_card_number_to_unique (mifare ).lower (): user
142- for user in users for mifare in user .mifare_card_ids
152+ for user in users
153+ for mifare in user .mifare_card_ids
143154 },
144155 ** {
145156 unique .lower (): user
146- for user in users for unique in user .unique_card_ids
157+ for user in users
158+ for unique in user .unique_card_ids
147159 },
148160 ** {
149161 transform_card_number_to_mifare (unique ).lower (): user
150- for user in users for unique in user .unique_card_ids
162+ for user in users
163+ for unique in user .unique_card_ids
151164 },
152165 }
153- logging .debug (f"Fetched { len (users )} users ({ len (users_by_card )} cards) from Authentik" )
166+ logging .debug (
167+ f"Fetched { len (users )} users ({ len (users_by_card )} cards) from Authentik"
168+ )
154169
155170
156171@app .get ("/users/-/stats" )
157172async def get_user_stats ():
158173 return {
159- "last_success_run" : users_last_success_run .isoformat ().replace ("+00:00" , "Z" ) if users_last_success_run else None ,
160- "last_failed_run" : users_last_failed_run .isoformat ().replace ("+00:00" , "Z" ) if users_last_failed_run else None ,
161- "last_failed_reason" : str (users_last_failed_reason ) if users_last_failed_reason else None ,
174+ "last_success_run" : (
175+ users_last_success_run .isoformat ().replace ("+00:00" , "Z" )
176+ if users_last_success_run
177+ else None
178+ ),
179+ "last_failed_run" : (
180+ users_last_failed_run .isoformat ().replace ("+00:00" , "Z" )
181+ if users_last_failed_run
182+ else None
183+ ),
184+ "last_failed_reason" : (
185+ str (users_last_failed_reason ) if users_last_failed_reason else None
186+ ),
162187 "users" : {
163188 "count" : len (users ),
164189 },
@@ -178,6 +203,37 @@ async def get_user_by_card(card_id: str):
178203 return user
179204
180205
206+ @app .get ("/user/-/sync" )
207+ async def sync_on_demand ():
208+ global users_last_success_run
209+ current_run = datetime .now (tz = UTC )
210+ diff_seconds = (current_run - users_last_success_run ).seconds
211+ if diff_seconds < 30 :
212+ logging .debug ("Skipping fetching users, data fresh" )
213+ return {
214+ "cached" : True ,
215+ "last_success_run" : (
216+ users_last_success_run .isoformat ().replace ("+00:00" , "Z" )
217+ if users_last_success_run
218+ else None
219+ ),
220+ "users" : {
221+ "count" : len (users ),
222+ },
223+ "last_failed_run" : (
224+ users_last_failed_run .isoformat ().replace ("+00:00" , "Z" )
225+ if users_last_failed_run
226+ else None
227+ ),
228+ }
229+ else :
230+ logging .debug (
231+ "Last successful fetch {diff_seconds}s before. Fetching on demand"
232+ )
233+ fetch_users ()
234+ return get_user_stats ()
235+
236+
181237@app .websocket ("/ws" )
182238async def websocket_endpoint (websocket : WebSocket ):
183239 logging .debug ("[WS] New connection" )
@@ -199,7 +255,9 @@ async def websocket_endpoint(websocket: WebSocket):
199255 {"status" : "error" , "error" : "user not found" }
200256 )
201257 else :
202- logging .info ("[WS] User %s (%s) found" , user .uid , kwargs .get ("card_id" ))
258+ logging .info (
259+ "[WS] User %s (%s) found" , user .uid , kwargs .get ("card_id" )
260+ )
203261 await websocket .send_json (
204262 {"status" : "ok" , "object" : user .model_dump ()}
205263 )
0 commit comments