11"""Admin functionality."""
22
33import calendar
4+ import random
45from datetime import UTC , date , datetime , timedelta
56
67import requests
@@ -32,6 +33,13 @@ def __init__(self, docs, key, total_key):
3233 except KeyError , StopIteration :
3334 self .total = ""
3435
36+ def has_counts (self , ndays = 28 ) -> bool :
37+ """True when every doc of the last `ndays` carries this key. A key only some of them
38+ have is one the stats script started writing recently, and a series read from it would
39+ cover part of the window while claiming all of it."""
40+ docs = self .docs [- ndays :]
41+ return bool (docs ) and all (self .key in doc for doc in docs )
42+
3543 def get_counts (self , ndays = 28 , times = False ):
3644 """Returns the stats for last n days as an array useful for
3745 plotting. i.e. an array of [x, y] tuples where y is the value
@@ -117,6 +125,11 @@ class LoanStats(Stats):
117125 loans, so the fallback (e.g. local dev) reports zero.
118126 """
119127
128+ def has_counts (self , ndays = 28 ) -> bool :
129+ if get_ol_env ().LOCAL_DEV or not _get_loan_counts_from_graphite (ndays ):
130+ return Stats .has_counts (self , ndays )
131+ return True
132+
120133 def get_counts (self , ndays = 28 , times = False ):
121134 # Let dev.openlibrary.org show the true state of things
122135 if get_ol_env ().LOCAL_DEV :
@@ -155,6 +168,9 @@ def _get_visitor_counts_from_graphite(ndays: int = 28) -> list[list[int]]:
155168
156169
157170class VisitorStats (Stats ):
171+ def has_counts (self , ndays : int = 28 ) -> bool :
172+ return bool (_get_visitor_counts_from_graphite (ndays ))
173+
158174 def get_counts (self , ndays : int = 28 , times : bool = False ) -> list [tuple [int , int ]]:
159175 visitors = _get_visitor_counts_from_graphite (ndays )
160176 # Flip the order, convert timestamp to msec, and convert count==None to zero
@@ -180,12 +196,14 @@ def _get_count_docs(ndays):
180196def get_stats (ndays = 30 , use_mock_data = False ):
181197 """Returns the stats for the past `ndays`"""
182198 if use_mock_data :
183- return mock_get_stats ()
199+ return mock_get_stats (ndays )
184200 docs = _get_count_docs (ndays )
185201 return {
186202 "human_edits" : Stats (docs , "human_edits" , "human_edits" ),
187203 "bot_edits" : Stats (docs , "bot_edits" , "bot_edits" ),
204+ "catalog_edits" : Stats (docs , "catalog_edits" , "catalog_edits" ),
188205 "lists" : Stats (docs , "lists" , "total_lists" ),
206+ "list_edits" : Stats (docs , "list_edits" , "list_edits" ),
189207 "visitors" : VisitorStats (docs , "visitors" , "visitors" ),
190208 "loans" : LoanStats (docs , "loans" , "loans" ),
191209 "members" : Stats (docs , "members" , "total_members" ),
@@ -214,38 +232,32 @@ async def get_unique_logins_since(since_days=30):
214232 return count or 0
215233
216234
217- def mock_get_stats ():
218- keyNames = [
219- "human_edits" ,
220- "bot_edits" ,
221- "lists" ,
222- "visitors" ,
223- "loans" ,
224- "members" ,
225- "works" ,
226- "editions" ,
227- "ebooks" ,
228- "covers" ,
229- "authors" ,
230- "subjects" ,
231- ]
232- mockKeyValues = [[(1 + x ) * y for x in range (len (keyNames ))] for y in range (28 )][::- 1 ]
233-
234- docs = [dict (zip (keyNames , mockKeyValues [x ])) for x in range (len (mockKeyValues ))]
235- today = date .today ()
236- for x in range (28 ):
237- docs [x ]["_key" ] = (today - timedelta (days = x + 1 )).strftime ("counts-%Y-%m-%d" )
238- return {
239- "human_edits" : Stats (docs , "human_edits" , "human_edits" ),
240- "bot_edits" : Stats (docs , "bot_edits" , "bot_edits" ),
241- "lists" : Stats (docs , "lists" , "total_lists" ),
242- "visitors" : Stats (docs , "visitors" , "visitors" ),
243- "loans" : Stats (docs , "loans" , "loans" ),
244- "members" : Stats (docs , "members" , "total_members" ),
245- "works" : Stats (docs , "works" , "total_works" ),
246- "editions" : Stats (docs , "editions" , "total_editions" ),
247- "ebooks" : Stats (docs , "ebooks" , "total_ebooks" ),
248- "covers" : Stats (docs , "covers" , "total_covers" ),
249- "authors" : Stats (docs , "authors" , "total_authors" ),
250- "subjects" : Stats (docs , "subjects" , "total_subjects" ),
235+ def mock_get_stats (ndays = 30 ):
236+ """Daily counts shaped like production's for local dev: a weekend dip, some noise and a
237+ gentle climb, at roughly production's scale. Seeded per key so a reload doesn't redraw."""
238+ daily_scale = {
239+ "human_edits" : 12_000 ,
240+ "bot_edits" : 20_000 ,
241+ "catalog_edits" : 3_000 ,
242+ "lists" : 150 ,
243+ "list_edits" : 1_200 ,
244+ "visitors" : 5_800_000 ,
245+ "loans" : 10_000 ,
246+ "members" : 7_000 ,
247+ "works" : 3_000 ,
248+ "editions" : 5_000 ,
249+ "ebooks" : 100 ,
250+ "covers" : 2_000 ,
251+ "authors" : 1_500 ,
252+ "subjects" : 500 ,
251253 }
254+ today = date .today ()
255+ days = [today - timedelta (days = n ) for n in range (ndays , 0 , - 1 )]
256+ docs = [{"_key" : day .strftime ("counts-%Y-%m-%d" )} for day in days ]
257+ for key , scale in daily_scale .items ():
258+ rng = random .Random (key )
259+ for i , (day , doc ) in enumerate (zip (days , docs , strict = True )):
260+ weekend = 0.85 if day .weekday () >= 5 else 1.0
261+ climb = 0.85 + 0.3 * i / max (ndays - 1 , 1 )
262+ doc [key ] = round (scale * weekend * climb * rng .uniform (0.92 , 1.08 ))
263+ return {key : Stats (docs , key , f"total_{ key } " ) for key in daily_scale }
0 commit comments