5
5
"""
6
6
7
7
import os
8
+ import sys
8
9
import asyncio
9
10
import aiohttp
10
11
from rich import print
@@ -138,6 +139,12 @@ async def get_org_members(
138
139
139
140
members .extend (page_members )
140
141
142
+ tasks = [check_user_admin (session , org , member ["login" ]) for member in members ]
143
+ admin_statuses = await asyncio .gather (* tasks )
144
+
145
+ for member , is_owner in zip (members , admin_statuses ):
146
+ member ["is_owner" ] = is_owner
147
+
141
148
# Cache the results
142
149
cache [cache_key ] = members # Using __setitem__ instead of set()
143
150
print (f"[green]Cached { len (members )} members for { org } [/green]" )
@@ -225,12 +232,39 @@ def clear_cache() -> None:
225
232
print (f"[red]Error clearing cache: { str (e )} [/red]" )
226
233
227
234
235
+ async def check_user_admin (
236
+ session : aiohttp .ClientSession , org : str , username : str
237
+ ) -> bool :
238
+ url = f"https://api.github.com/orgs/{ org } /memberships/{ username } "
239
+ async with session .get (url , headers = headers ) as response :
240
+ if response .status == 404 :
241
+ return False
242
+ elif response .status != 200 :
243
+ print (
244
+ f"[red]Error fetching membership for { username } in { org } : { response .status } [/red]"
245
+ )
246
+ return False
247
+ return (await response .json ())["role" ] == "admin"
248
+
249
+
228
250
async def main (orgs , debug : bool , timelimit_days : int ):
229
251
"""Main execution function."""
230
252
# Show cache status
231
253
print (f"[blue]Cache directory: { CACHE_DIR } (size: { get_cache_size ()} )[/blue]" )
232
254
print (f"[blue]Cache contains { len (cache )} items[/blue]" )
233
255
256
+ # check who the current user is
257
+ async with aiohttp .ClientSession () as session :
258
+ async with session .get (
259
+ "https://api.github.com/user" , headers = headers
260
+ ) as response :
261
+ if response .status == 200 :
262
+ user_data = await response .json ()
263
+ current_user = user_data ["login" ]
264
+ print (f"[blue]Current user: { current_user } [/blue]" )
265
+ else :
266
+ sys .exit (f"[red]Error fetching user data: { response .status } [/red]" )
267
+
234
268
async with aiohttp .ClientSession () as session :
235
269
# Check rate limit
236
270
async with session .get (
@@ -255,12 +289,15 @@ async def main(orgs, debug: bool, timelimit_days: int):
255
289
256
290
# Get all members from all orgs
257
291
all_members = {}
292
+ org_owners = {}
258
293
for org in orgs :
259
294
members = await get_org_members (session , org , debug )
260
295
for member in members :
261
296
if member ["login" ] not in all_members :
262
297
all_members [member ["login" ]] = []
263
298
all_members [member ["login" ]].append (org )
299
+ if member ["is_owner" ]:
300
+ org_owners .setdefault (org , []).append (member ["login" ])
264
301
265
302
# Get activity for each user
266
303
tasks = []
@@ -284,31 +321,47 @@ async def main(orgs, debug: bool, timelimit_days: int):
284
321
)
285
322
for org in orgs :
286
323
print (f"[bold]{ org } [/bold]" )
324
+ is_admin = await check_user_admin (session , org , current_user )
325
+ if is_admin :
326
+ if debug :
327
+ print (f" [green]{ current_user } is an admin in { org } [/green]" )
328
+ else :
329
+ print (
330
+ f" [yellow]{ current_user } is not an admin in { org } , list of users will be incomplete (limited to public membership)[/yellow]"
331
+ )
287
332
n_active = 0
288
333
n_inactive = 0
289
334
for username , last_activity , user_orgs in sorted (
290
335
user_activities ,
291
- key = lambda x : (x [1 ], x [0 ])
292
- if x [1 ] is not None
293
- else (datetime .fromtimestamp (0 ).replace (tzinfo = timezone .utc ), x [0 ]),
336
+ key = lambda x : (
337
+ (x [1 ], x [0 ])
338
+ if x [1 ] is not None
339
+ else (datetime .fromtimestamp (0 ).replace (tzinfo = timezone .utc ), x [0 ])
340
+ ),
294
341
reverse = True ,
295
342
):
296
343
if org not in user_orgs :
297
344
continue
298
- if last_activity is not None and last_activity > (datetime .now ().replace (tzinfo = timezone .utc ) - timedelta (days = timelimit_days )):
345
+ if last_activity is not None and last_activity > (
346
+ datetime .now ().replace (tzinfo = timezone .utc )
347
+ - timedelta (days = timelimit_days )
348
+ ):
299
349
n_active += 1
300
350
continue
301
351
n_inactive += 1
302
352
last_activity_ago = (
303
- humanize .naturaltime (datetime .now (last_activity .tzinfo ) - last_activity )
353
+ humanize .naturaltime (
354
+ datetime .now (last_activity .tzinfo ) - last_activity
355
+ )
304
356
if last_activity
305
357
else "[red]never[/red]"
306
358
)
307
359
orgs_str = ", " .join (user_orgs )
308
- print (
309
- f" { username :<20} : Last activity { last_activity_ago } "
310
- )
311
- print (f" Found [red]{ n_inactive } inactive[/red] and [green]{ n_active } active[/green] users in { org } with last activity more recent than { timelimit_days } days." )
360
+ u_owner = " (owner)" if username in org_owners .get (org , []) else ""
361
+ print (f" { username + u_owner :<20} : Last activity { last_activity_ago } " )
362
+ print (
363
+ f" Found [red]{ n_inactive } inactive[/red] and [green]{ n_active } active[/green] users in { org } with last activity more recent than { timelimit_days } days."
364
+ )
312
365
313
366
314
367
if __name__ == "__main__" :
@@ -318,7 +371,6 @@ async def main(orgs, debug: bool, timelimit_days: int):
318
371
)
319
372
parser .add_argument ("--debug" , action = "store_true" , help = "Show debug information" )
320
373
321
-
322
374
parser .add_argument (
323
375
"--timelimit-days" ,
324
376
type = int ,
@@ -333,7 +385,6 @@ async def main(orgs, debug: bool, timelimit_days: int):
333
385
)
334
386
args = parser .parse_args ()
335
387
336
-
337
388
if args .clear_cache :
338
389
clear_cache ()
339
390
0 commit comments