Skip to content

Commit 60cb706

Browse files
authored
Merge pull request #101 from Carreau/more-activity
Misc updates (manual activity dates for users)
2 parents 20d7c96 + 1c88cf4 commit 60cb706

File tree

4 files changed

+82
-18
lines changed

4 files changed

+82
-18
lines changed

last_user_activity.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# User : inviter : manual activity YYYY-MM : note
2+
JamiesHQ:fperez :2016-01:Hired admin at BIDS
3+
4+
jhamrick:carreau:2016-01:Wrote nbgrader and was a regular member of the team at bids.
5+
mpacer :carreau:2016-01:was a regular member of the team at bids on nbconvert
6+

tools/all_repos.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ def get_packages(url):
5252

5353

5454
default_orgs = [
55-
"binder-examples",
55+
# "binder-examples",
5656
"binderhub-ci-repos",
5757
"ipython",
5858
"jupyter",
@@ -66,7 +66,7 @@ def get_packages(url):
6666
"jupyter-standards",
6767
"jupyter-widgets",
6868
"jupyter-xeus",
69-
"jupytercon",
69+
# "jupytercon",
7070
"jupyterhub",
7171
"jupyterlab",
7272
"voila-dashboards",

tools/last_user_activity.py

Lines changed: 71 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,20 @@
44
It implements disk-based caching to minimize API requests and respect rate limits.
55
"""
66

7+
import argparse
8+
import asyncio
79
import os
10+
import pathlib
811
import sys
9-
import asyncio
10-
import aiohttp
11-
from rich import print
12-
from datetime import datetime, timezone, timedelta
13-
import humanize
12+
from datetime import datetime, timedelta, timezone
1413
from itertools import count
14+
from pathlib import Path
15+
from typing import Dict, List, Optional
16+
17+
import aiohttp
1518
import diskcache
16-
import pathlib
17-
from typing import Optional, List, Dict
18-
import argparse
19+
import humanize
20+
from rich import print
1921

2022
default_orgs = [
2123
"binder-examples",
@@ -81,6 +83,7 @@ def get(self, key, default=None, retry=False):
8183

8284

8385
# Configure DiskCache in the current directory
86+
# todo: auto clear after ~24 hours
8487
CACHE_DIR = "github_cache"
8588
cache = DateTimeCache(CACHE_DIR)
8689

@@ -247,12 +250,34 @@ async def check_user_admin(
247250
return (await response.json())["role"] == "admin"
248251

249252

250-
async def main(orgs, debug: bool, timelimit_days: int):
253+
async def main(orgs, debug: bool, timelimit_days: int, config_file: str):
251254
"""Main execution function."""
252255
# Show cache status
253256
print(f"[blue]Cache directory: {CACHE_DIR} (size: {get_cache_size()})[/blue]")
254257
print(f"[blue]Cache contains {len(cache)} items[/blue]")
255-
258+
now = datetime.now(timezone.utc)
259+
manual_users = {}
260+
for line in Path(config_file).read_text().splitlines():
261+
if line.startswith("#") or not line.strip():
262+
continue
263+
user, inviter, date_last_activity, reason = [x.strip() for x in line.split(":")]
264+
manual_users[user] = {
265+
"manual_last_activity": datetime.strptime(
266+
date_last_activity, "%Y-%m"
267+
).replace(tzinfo=timezone.utc),
268+
"last_activity_reason": reason,
269+
"inviter": inviter,
270+
}
271+
if not manual_users[user]["last_activity_reason"]:
272+
print(
273+
f"[yellow]Warning: No last_activity_reason for {user['username']}, skipping[/yellow]"
274+
)
275+
continue
276+
if now < manual_users[user]["manual_last_activity"]:
277+
print(
278+
f"[red]Warning: manual_last_activity for {user['username']} is in the future, skipping[/red]"
279+
)
280+
continue
256281
# check who the current user is
257282
async with aiohttp.ClientSession() as session:
258283
async with session.get(
@@ -296,7 +321,7 @@ async def main(orgs, debug: bool, timelimit_days: int):
296321
if member["login"] not in all_members:
297322
all_members[member["login"]] = []
298323
all_members[member["login"]].append(org)
299-
if member["is_owner"]:
324+
if member.get("is_owner", None):
300325
org_owners.setdefault(org, []).append(member["login"])
301326

302327
# Get activity for each user
@@ -312,16 +337,25 @@ async def main(orgs, debug: bool, timelimit_days: int):
312337
for (username, _), last_activity in zip(tasks, results):
313338
if last_activity is not None:
314339
assert isinstance(last_activity, datetime), last_activity
340+
341+
if last_activity is None:
342+
last_activity = manual_users.get(username, {}).get(
343+
"manual_last_activity", None
344+
)
315345
user_activities.append(
316346
(
317347
username,
318348
last_activity if last_activity is not None else None,
319349
all_members[username],
320350
)
321351
)
322-
for org in orgs:
352+
353+
admin_check_tasks = [
354+
check_user_admin(session, org, current_user) for org in orgs
355+
]
356+
admin_check_results = await asyncio.gather(*admin_check_tasks)
357+
for org, is_admin in zip(orgs, admin_check_results):
323358
print(f"[bold]{org}[/bold]")
324-
is_admin = await check_user_admin(session, org, current_user)
325359
if is_admin:
326360
if debug:
327361
print(f" [green]{current_user} is an admin in {org}[/green]")
@@ -347,6 +381,8 @@ async def main(orgs, debug: bool, timelimit_days: int):
347381
- timedelta(days=timelimit_days)
348382
):
349383
n_active += 1
384+
if debug:
385+
print(f" [green]{username}[/green] is active in {org}")
350386
continue
351387
n_inactive += 1
352388
last_activity_ago = (
@@ -356,9 +392,22 @@ async def main(orgs, debug: bool, timelimit_days: int):
356392
if last_activity
357393
else "[red]never[/red]"
358394
)
359-
orgs_str = ", ".join(user_orgs)
360395
u_owner = " (owner)" if username in org_owners.get(org, []) else ""
361-
print(f" {username+u_owner:<20}: Last activity {last_activity_ago}")
396+
inviter = manual_users.get(username, {}).get("inviter", None)
397+
if inviter:
398+
inviter = f"[green]@{inviter}[/green]"
399+
else:
400+
inviter = "[red]unknown[/red]"
401+
reason = manual_users.get(username, {}).get(
402+
"last_activity_reason", None
403+
)
404+
if reason:
405+
reason = f"[green]{reason}[/green]"
406+
else:
407+
reason = "[red]unknown[/red]"
408+
print(
409+
f" {username + u_owner:<20}: Last activity {last_activity_ago}: reason: {reason}, inviter: {inviter}"
410+
)
362411
print(
363412
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."
364413
)
@@ -370,6 +419,12 @@ async def main(orgs, debug: bool, timelimit_days: int):
370419
"--clear-cache", action="store_true", help="Clear the cache before running"
371420
)
372421
parser.add_argument("--debug", action="store_true", help="Show debug information")
422+
parser.add_argument(
423+
"--config-file",
424+
type=str,
425+
default="last_user_activity.json",
426+
help="Path to the config file (default: last_user_activity.json)",
427+
)
373428

374429
parser.add_argument(
375430
"--timelimit-days",
@@ -388,4 +443,4 @@ async def main(orgs, debug: bool, timelimit_days: int):
388443
if args.clear_cache:
389444
clear_cache()
390445

391-
asyncio.run(main(args.orgs, args.debug, args.timelimit_days))
446+
asyncio.run(main(args.orgs, args.debug, args.timelimit_days, args.config_file))

tools/tide.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ def get_packages(url):
2525

2626
if "A required part of this site couldn’t load" in response.text:
2727
print(f"Fastly is blocking us for {url}. Status code: 403")
28+
print(
29+
"You can try `Array.from(document.querySelectorAll('h3')).map(h3 => h3.innerText).join(' ');`, from js console when viewing a page from a browser and use the `--packages` option."
30+
)
2831
exit(1)
2932

3033
# Parse the HTML content

0 commit comments

Comments
 (0)