{ "cells": [ { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#default_exp ghtop" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# ghtop\n", "\n", "> I cannot believe this library name is not already taken on pypi." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#export\n", "import sys, signal, shutil, os, json, enlighten, emoji, blessed\n", "from dashing import *\n", "from collections import defaultdict\n", "from warnings import warn\n", "from itertools import islice\n", "from time import sleep\n", "\n", "from fastcore.utils import *\n", "from fastcore.foundation import *\n", "from fastcore.script import *\n", "from ghapi.all import *" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#export\n", "term = Terminal()\n", "logfile = Path(\"log.txt\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#export\n", "def github_auth_device(wb='', n_polls=9999):\n", " \"Authenticate with GitHub, polling up to `n_polls` times to wait for completion\"\n", " auth = GhDeviceAuth()\n", " print(f\"First copy your one-time code: {term.yellow}{auth.user_code}{term.normal}\")\n", " print(f\"Then visit {auth.verification_uri} in your browser, and paste the code when prompted.\")\n", " if not wb: wb = input(\"Shall we try to open the link for you? [y/n] \")\n", " if wb[0].lower()=='y': auth.open_browser()\n", "\n", " print (\"Waiting for authorization...\", end='')\n", " token = auth.wait(lambda: print('.', end=''), n_polls=n_polls)\n", " if not token: return print('Authentication not complete!')\n", " print(\"Authenticated with GitHub\")\n", " return token" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "When we run this we'll be shown a URL to visit and a code to enter in order to authenticate. Normally we'll be prompted to open a browser, and the function will wait for authentication to complete -- for demonstrating here we'll skip both of these steps:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "```py\n", "_token = github_auth_device('n')\n", "```\n", "\n", "> ![image.png](00_ghtop_files/att_00000.png)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#export\n", "def _exit(msg):\n", " print(msg, file=sys.stderr)\n", " sys.exit()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#export\n", "def _get_token():\n", " path = Path.home()/\".ghtop_token\"\n", " if path.is_file():\n", " try: return path.read_text().strip()\n", " except: _exit(\"Error reading token\")\n", "\n", " token = github_auth_device()\n", " path.write_text(token)\n", " return token" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#export\n", "token = _get_token()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#exports\n", "def limit_cb(rem,quota):\n", " \"Callback to warn user when close to using up hourly quota\"\n", " w='WARNING '*7\n", " if rem < 1000: print(f\"{w}\\nRemaining calls: {rem} out of {quota}\\n{w}\", file=sys.stderr)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "When creating `GhApi` we can pass a callback which will be called after each API operation. In this case, we use it to warn the user when their quota is getting low." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#exports\n", "api = GhApi(limit_cb=limit_cb, token=token)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#export\n", "def fetch_events(types=None):\n", " \"Generate an infinite stream of events optionally filtered to `types`\"\n", " while True:\n", " yield from (o for o in api.activity.list_public_events() if not types or o.type in types)\n", " sleep(0.2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "`api.activity.list_public_events` returns 30 events at a time, and `fetch_events` turns that into an infinite length stream. We can take a look at the most recent event to see what type it is:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'PushEvent'" ] }, "execution_count": null, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ev = next(fetch_events())\n", "ev.type" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#export\n", "Events = dict(\n", " IssuesEvent_closed=('⭐', 'closed', noop),\n", " IssuesEvent_opened=('📫', 'opened', noop),\n", " IssueCommentEvent=('💬', 'commented on', term.white),\n", " PullRequestEvent_opened=('✨', 'opened a pull request', term.yellow),\n", " PullRequestEvent_closed=('✔', 'closed a pull request', term.green),\n", ")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#export\n", "seen = set()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#export\n", "def _to_log(e):\n", " login,repo,pay = e.actor.login,e.repo.name,e.payload\n", " typ = e.type + (f'_{pay.action}' if e.type in ('PullRequestEvent','IssuesEvent') else '')\n", " emoji,msg,color = Events.get(typ, [0]*3)\n", " if emoji:\n", " xtra = '' if e.type == \"PullRequestEvent\" else f' issue # {pay.issue.number}'\n", " d = try_attrs(pay, \"pull_request\", \"issue\")\n", " return color(f'{emoji} {login} {msg}{xtra} on repo {repo[:20]} (\"{d.title[:50]}...\")')\n", " elif e.type == \"ReleaseEvent\": return f'🚀 {login} released {e.payload.release.tag_name} of {repo}'" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#export\n", "def print_event(e, commits_counter):\n", " if e.id in seen: return\n", " seen.add(e.id)\n", " login = e.actor.login\n", " if \"bot\" in login or \"b0t\" in login: return # Don't print bot activity (there is a lot!)\n", "\n", " res = _to_log(e)\n", " if res: print(res)\n", " elif e.type == \"PushEvent\": [commits_counter.update() for c in e.payload.commits]\n", " elif e.type == \"SecurityAdvisoryEvent\": print(term.blink(\"SECURITY ADVISORY\"))" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#hide\n", "seen.clear()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can pretty print a selection of event types using `print_event` (note that `print_event` filters out most bot activity), eg:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\u001b[32m✔ Yfill closed a pull request on repo Yfill/event-hub (\"chore(deps): update typescript-eslint monorepo to ...\")\u001b[m\n", "\u001b[33m✨ olblak opened a pull request on repo olblak/charts (\"[updatecli] Update Docker Image version to plugin-...\")\u001b[m\n", "📫 yxuco opened issue # 117 on repo project-flogo/cli (\"Specify module name and package versions when crea...\")\n", "\u001b[32m✔ sylvestre closed a pull request on repo uutils/coreutils (\"refactor(mv): move to clap & add tests...\")\u001b[m\n", "\u001b[33m✨ hhtong opened a pull request on repo dwave-examples/facto (\"Add code owner...\")\u001b[m\n", "⭐ doitsujin closed issue # 1848 on repo doitsujin/dxvk (\"Far Cry 5 and Far Cry New Dawn...\")\n", "\u001b[33m✨ markanator opened a pull request on repo IAM-Teams-Mental-Hea (\"✨ added framer motion to page navigations...\")\u001b[m\n", "🚀 tothlevente released v1.5.2 of tothlevente/advent-calendar\n", "📫 lauracosorio opened issue # 2 on repo Campus-Advisors/camp (\"Module 1.2 Assignment...\")\n", "\u001b[32m✔ Shinmera closed a pull request on repo Shinmera/float-featu (\"fix abcl...\")\u001b[m\n", "\u001b[33m✨ Valdnet opened a pull request on repo nextcloud/documentat (\"Reposition the photo...\")\u001b[m\n", "\u001b[33m✨ tebjan opened a pull request on repo vvvv/VL.Stride (\"TextureFX Lumakey...\")\u001b[m\n" ] } ], "source": [ "gen = fetch_events(types=('IssuesEvent','ReleaseEvent','PullRequestEvent'))\n", "for e in islice(gen, 30): print_event(e,None)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#export\n", "def tail_events():\n", " \"Print events from `fetch_events` along with a counter of push events\"\n", " manager = enlighten.get_manager()\n", " commits = manager.counter(desc='Commits', unit='commits', color='green')\n", " for ev in fetch_events(): print_event(ev, commits)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#export\n", "def _pr_row(*its): print(f\"{its[0]: <30} {its[1]: <6} {its[2]: <5} {its[3]: <6} {its[4]: <7}\")\n", "def watch_users():\n", " users,users_events = defaultdict(int),defaultdict(lambda: defaultdict(int))\n", " for xs in chunked(fetch_events(), 10):\n", " for x in xs:\n", " users[x.actor.login] += 1\n", " users_events[x.actor.login][x.type] += 1\n", "\n", " print(term.clear())\n", " _pr_row(\"User\", \"Events\", \"PRs\", \"Issues\", \"Pushes\")\n", " sorted_users = sorted(users.items(), key=lambda o: (o[1],o[0]), reverse=True)\n", " for u in sorted_users[:20]:\n", " _pr_row(*u, *itemgetter('PullRequestEvent','IssuesEvent','PushEvent')(users_events[u[0]]))" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#export\n", "def _push_to_log(e):\n", " return f\"{e.actor.login} pushed {len(e.payload.commits)} commits to repo {e.repo.name}\"" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#export\n", "def _logwin(title,color): return Log(title=title,border_color=2,color=color)\n", "def quad_logs():\n", " term.enter_fullscreen()\n", " ui = HSplit(VSplit(_logwin('Issues', color=7), _logwin('Commits' , color=3)),\n", " VSplit(_logwin('Pull Requests', color=4), _logwin('Releases', color=5)))\n", "\n", " issues,commits,prs,releases = all_items = ui.items[0].items+ui.items[0].items\n", " for o in all_items: o.append(\" \")\n", "\n", " d = dict(PushEvent=commits, IssuesEvent=issues, IssueCommentEvent=issues, PullRequestEvent=prs, ReleaseEvent=releases)\n", " for xs in chunked(fetch_events(types=d), 10):\n", " for x in xs:\n", " f = [_to_log,_push_to_log][x.type == 'PushEvent']\n", " d[x.type].append(f(x)[:95])\n", " ui.display()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#export\n", "def simple():\n", " for ev in fetch_events(): print(f\"{ev.actor.login} {ev.type} {ev.repo.name}\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#export\n", "def _signal_handler(sig, frame):\n", " if sig != signal.SIGINT: return\n", " print(term.exit_fullscreen())\n", " print(term.clear())\n", " print(term.normal)\n", " sys.exit(0)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#export\n", "def _help(): _exit(\"Usage: ghtop \")\n", "if __name__ == '__main__' and not IN_NOTEBOOK:\n", " if len(sys.argv) < 2: _help()\n", " signal.signal(signal.SIGINT, _signal_handler)\n", " dict(tail=tail_events, quad=quad_logs, users=watch_users, simple=simple\n", " ).get(sys.argv[1],_help)()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" } }, "nbformat": 4, "nbformat_minor": 4 }