{ "cells": [ { "cell_type": "code", "execution_count": 52, "metadata": {}, "outputs": [], "source": [ "#default_exp ghtop" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# ghtop API\n", "\n", "> API details" ] }, { "cell_type": "code", "execution_count": 53, "metadata": {}, "outputs": [], "source": [ "#export\n", "import time, 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", "\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": 54, "metadata": {}, "outputs": [], "source": [ "#export\n", "term = Terminal()\n", "logfile = Path(\"log.txt\")" ] }, { "cell_type": "code", "execution_count": 55, "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": "code", "execution_count": 56, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "First copy your one-time code: \u001b[33m0F24-0D10\u001b[m\n", "Then visit https://github.com/login/device in your browser, and paste the code when prompted.\n", "Waiting for authorization...Authentication not complete!\n" ] } ], "source": [ "github_auth_device('n',n_polls=0)" ] }, { "cell_type": "code", "execution_count": 57, "metadata": {}, "outputs": [], "source": [ "#export\n", "def _exit(msg):\n", " print(msg, file=sys.stderr)\n", " sys.exit()" ] }, { "cell_type": "code", "execution_count": 60, "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": 81, "metadata": {}, "outputs": [], "source": [ "#export\n", "def _get_api():\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 GhApi(limit_cb=limit_cb, token=token)\n", "\n", "api = _get_api()" ] }, { "cell_type": "code", "execution_count": 62, "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", " time.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": 63, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'ForkEvent'" ] }, "execution_count": 63, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ev = next(fetch_events())\n", "ev.type" ] }, { "cell_type": "code", "execution_count": 64, "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": 65, "metadata": {}, "outputs": [], "source": [ "#export\n", "seen = set()" ] }, { "cell_type": "code", "execution_count": 66, "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": 67, "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": 68, "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": 69, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\u001b[33m✨ TingluoHuang opened a pull request on repo actions-canary/ForkP (\"PR from Fork...\")\u001b[m\n", "\u001b[33m✨ danielSanchez98 opened a pull request on repo danielSanchez98/soci (\"Social Media Dashboard Dark Mode...\")\u001b[m\n", "⭐ StephSako closed issue # 15 on repo StephSako/PingBracke (\"Dynamic bracket according to poules/players number...\")\n", "\u001b[33m✨ ZigZag48 opened a pull request on repo ZigZag48/QuizApp (\"header...\")\u001b[m\n", "\u001b[32m✔ joshfriend closed a pull request on repo pyenv/pyenv (\"Docker config for testing python-build...\")\u001b[m\n", "📫 ltabis opened issue # 11 on repo ltabis/KMU-CG1-mesh- (\"Renderer options...\")\n", "\u001b[33m✨ cindie-fff opened a pull request on repo virtualgoodsdealer/v (\"Updated language in submission page from web show ...\")\u001b[m\n", "\u001b[33m✨ notrabe opened a pull request on repo notrabe/node-db4-pro (\"....\")\u001b[m\n", "\u001b[33m✨ jamescbaldwin opened a pull request on repo jamescbaldwin/PROJEC (\"Nihal...\")\u001b[m\n", "\u001b[32m✔ AngelFQC closed a pull request on repo mozillaperu/particip (\"update poll...\")\u001b[m\n", "\u001b[32m✔ 19ATF72 closed a pull request on repo 19ATF72/AAA-project (\"updating git ignore...\")\u001b[m\n", "⭐ rlorenzo closed issue # 138 on repo ucla/moodle-mod_zoom (\"PHP catchable fatal error...\")\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": 70, "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": 71, "metadata": {}, "outputs": [], "source": [ "#export\n", "def batch_events(size, types=None):\n", " \"Generate an infinite stream of batches of events of `size` optionally filtered to `types`\"\n", " while True: yield islice(fetch_events(types=types), size)" ] }, { "cell_type": "code", "execution_count": 72, "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", " \"Print a table of the users with the most events\"\n", " users,users_events = defaultdict(int),defaultdict(lambda: defaultdict(int))\n", " for xs in batch_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": 73, "metadata": {}, "outputs": [], "source": [ "#export\n", "def _push_to_log(e): return f\"{e.actor.login} pushed {len(e.payload.commits)} commits to repo {e.repo.name}\"\n", "def _logwin(title,color): return Log(title=title,border_color=2,color=color)\n", "\n", "def quad_logs():\n", " \"Print 4 panels, showing most recent issues, commits, PRs, and releases\"\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[1].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 batch_events(10, types=d):\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": 74, "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": 80, "metadata": {}, "outputs": [], "source": [ "#export\n", "def _signal_handler(sig, frame):\n", " if sig != signal.SIGINT: return\n", " print(term.exit_fullscreen(),term.clear(),term.normal)\n", " sys.exit(0)\n", "\n", "_funcs = dict(tail=tail_events, quad=quad_logs, users=watch_users, simple=simple)\n", "_OpModes = str_enum('_OpModes', *_funcs)\n", "\n", "@call_parse\n", "def main(mode: Param(\"Operation mode to run\", _OpModes)):\n", " signal.signal(signal.SIGINT, _signal_handler)\n", " _funcs[mode]()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.7" }, "toc": { "base_numbering": 1, "nav_menu": {}, "number_sections": false, "sideBar": true, "skip_h1_title": true, "title_cell": "Table of Contents", "title_sidebar": "Contents", "toc_cell": false, "toc_position": {}, "toc_section_display": true, "toc_window_display": false } }, "nbformat": 4, "nbformat_minor": 4 }