{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#default_exp event"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# The events API\n",
    "\n",
    "> Helpers for getting GitHub API events"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#export\n",
    "from fastcore.utils import *\n",
    "from fastcore.foundation import *\n",
    "from fastcore.meta import *\n",
    "from ghapi.core import *\n",
    "from ghapi.page import *\n",
    "from ghapi.actions import *\n",
    "\n",
    "import time,json,gzip\n",
    "from itertools import islice"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#hide\n",
    "from nbdev.showdoc import show_doc\n",
    "from IPython.display import Markdown\n",
    "import matplotlib.pyplot as plt\n",
    "from collections import Counter"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "api = GhApi()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#export\n",
    "def _list_events(g, username=None, org=None, owner=None, repo=None):\n",
    "    if (username or org or owner) and \\\n",
    "        not (bool(username) ^ bool(org) ^ bool(owner)): raise Exception('Can not pass more than one of username, org, and owner')\n",
    "    if (owner and not repo): owner,repo = repo.split('/')\n",
    "    if owner: return g.list_public_events_for_repo_network,{'owner':owner,'repo':repo}\n",
    "    if org: return g.list_public_org_events,{'org':org}\n",
    "    if username: return g.list_public_events_for_user,{'username':username}\n",
    "    return g.list_public_events,{}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#export\n",
    "def _id2int(x):\n",
    "    x.id = int(x.id)\n",
    "    return x"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#export\n",
    "@patch\n",
    "@delegates(_list_events)\n",
    "def list_events(self:GhApi, per_page=30, page=1, **kwargs):\n",
    "    \"Fetch public events for repo network, org, user, or all\"\n",
    "    oper,kw = _list_events(self.activity, **kwargs)\n",
    "    return oper(per_page=per_page, page=page, **kw).map(_id2int)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#export\n",
    "@patch\n",
    "@delegates(_list_events)\n",
    "def list_events_parallel(self:GhApi, per_page=30, n_pages=8, **kwargs):\n",
    "    \"Fetch as many events from `list_events` in parallel as available\"\n",
    "    oper,kw = _list_events(self.activity, **kwargs)\n",
    "    return pages(oper, n_pages, per_page=per_page, **kw).concat().map(_id2int)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`list_events` and `list_events_parallel` support the following:\n",
    "\n",
    "|Events from|Example|\n",
    "|:-|:-|\n",
    "|Organization|`api.list_events_parallel(org='fastai')`\n",
    "|User|`api.list_events_parallel(username='jph00')`\n",
    "|Repository network|`api.list_events_parallel(owner='fastai', repo='fastcore')`\n",
    "|All public|`api.list_events_parallel()`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#export\n",
    "_bot_re = re.compile('b[o0]t')\n",
    "def _want_evt(o, types, incl_bot):\n",
    "    if not incl_bot and _bot_re.search(nested_attr(o, 'actor.login') or ''): return False\n",
    "    if types and o.type not in types: return False\n",
    "    return True"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#export\n",
    "class GhEvent(AttrDict):\n",
    "    \"Class for events returned from `fetch_events\"\n",
    "    pass\n",
    "\n",
    "for o in Event: exec(f'class {snake2camel(o)}Event(GhEvent):pass')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/markdown": [
       "<h4 id=\"GhEvent\" class=\"doc_header\"><code>class</code> <code>GhEvent</code><a href=\"\" class=\"source_link\" style=\"float:right\">[source]</a></h4>\n",
       "\n",
       "> <code>GhEvent</code>() :: `AttrDict`\n",
       "\n",
       "Class for events returned from `fetch_events"
      ],
      "text/plain": [
       "<IPython.core.display.Markdown object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "show_doc(GhEvent, title_level=4)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "['PageBuildEvent', 'ContentReferenceEvent', 'RepositoryImportEvent', 'CreateEvent', 'WorkflowRunEvent', 'DeleteEvent', 'OrganizationEvent', 'SponsorshipEvent', 'ProjectColumnEvent', 'PushEvent', 'ContextEvent', 'MilestoneEvent', 'ProjectCardEvent', 'ProjectEvent', 'PackageEvent', 'PullRequestEvent', 'RepositoryDispatchEvent', 'TeamAddEvent', 'WorkflowDispatchEvent', 'MemberEvent', 'MetaEvent', 'CodeScanningAlertEvent', 'PublicEvent', 'NeedsEvent', 'CheckRunEvent', 'SecurityAdvisoryEvent', 'PullRequestReviewCommentEvent', 'OrgBlockEvent', 'CommitCommentEvent', 'WatchEvent', 'MarketplacePurchaseEvent', 'StarEvent', 'InstallationRepositoriesEvent', 'CheckSuiteEvent', 'GithubAppAuthorizationEvent', 'TeamEvent', 'StatusEvent', 'RepositoryVulnerabilityAlertEvent', 'PullRequestReviewEvent', 'LabelEvent', 'InstallationEvent', 'ReleaseEvent', 'IssuesEvent', 'RepositoryEvent', 'GollumEvent', 'MembershipEvent', 'DeploymentEvent', 'DeployKeyEvent', 'IssueCommentEvent', 'PingEvent', 'DeploymentStatusEvent', 'ForkEvent', 'ScheduleEvent']\n"
     ]
    }
   ],
   "source": [
    "print([snake2camel(o)+'Event' for o in Event])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#export\n",
    "_all_ = [\n",
    "    'PageBuildEvent', 'ContentReferenceEvent', 'RepositoryImportEvent', 'CreateEvent', 'WorkflowRunEvent', 'DeleteEvent', 'OrganizationEvent',\n",
    "    'SponsorshipEvent', 'ProjectColumnEvent', 'PushEvent', 'ContextEvent', 'MilestoneEvent', 'ProjectCardEvent', 'ProjectEvent', 'PackageEvent',\n",
    "    'PullRequestEvent', 'RepositoryDispatchEvent', 'TeamAddEvent', 'WorkflowDispatchEvent', 'MemberEvent', 'MetaEvent', 'CodeScanningAlertEvent',\n",
    "    'PublicEvent', 'NeedsEvent', 'CheckRunEvent', 'SecurityAdvisoryEvent', 'PullRequestReviewCommentEvent', 'OrgBlockEvent',\n",
    "    'CommitCommentEvent', 'WatchEvent', 'MarketplacePurchaseEvent', 'StarEvent', 'InstallationRepositoriesEvent', 'CheckSuiteEvent',\n",
    "    'GithubAppAuthorizationEvent', 'TeamEvent', 'StatusEvent', 'RepositoryVulnerabilityAlertEvent', 'PullRequestReviewEvent', 'LabelEvent',\n",
    "    'InstallationEvent', 'ReleaseEvent', 'IssuesEvent', 'RepositoryEvent', 'GollumEvent', 'MembershipEvent', 'DeploymentEvent', 'DeployKeyEvent',\n",
    "    'IssueCommentEvent', 'PingEvent', 'DeploymentStatusEvent', 'ForkEvent', 'ScheduleEvent']"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#export\n",
    "def _cast_evt(o): return globals()[o.type](o)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#export\n",
    "@patch\n",
    "@delegates(_list_events)\n",
    "def fetch_events(self:GhApi, n_pages=3, pause=0.4, per_page=30, types=None, incl_bot=False, **kwargs):\n",
    "    \"Generate an infinite stream of events, optionally filtered to `types, with `pause` seconds between requests\"\n",
    "    seen = set()\n",
    "    if types: types=setify(types or None)\n",
    "    g = globals()\n",
    "    while True:\n",
    "        evts = self.list_events_parallel(n_pages=n_pages, per_page=per_page, **kwargs)\n",
    "        new_evts = L(_cast_evt(o) for o in evts if o.id not in seen and _want_evt(o, types, incl_bot))\n",
    "        seen.update(new_evts.attrgot('id'))\n",
    "        yield from new_evts\n",
    "        if pause: time.sleep(pause)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#export\n",
    "def load_sample_events():\n",
    "    \"Load sample events, downloading if needed\"\n",
    "    name = 'sample_evts.json.gz'\n",
    "    url = f'https://raw.githubusercontent.com/fastai/ghapi/master/examples/{name}'\n",
    "    try: path = Path(__file__).parent\n",
    "    except NameError: path = Path()/'examples'\n",
    "    path = path/name\n",
    "    if not path.exists():path.write_bytes(urlread(url, decode=False))\n",
    "    return dict2obj(json.load(open_file(path))).map(_cast_evt)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#export\n",
    "def save_sample_events(n=5000):\n",
    "    \"Save the most recent `n` events as compressed JSON\"\n",
    "    evts = list(islice(api.fetch_events(incl_bot=True), n))\n",
    "    with gzip.open('sample_evts.json.gz', 'wt') as f: json.dump(obj2dict(evts), f)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#export\n",
    "#hide\n",
    "@patch(as_prop=True)\n",
    "def full_type(self:GhEvent):\n",
    "    \"Concatenation of `type` and `payload.action` (if available)\"\n",
    "    act = getattr(self.payload, 'action', '')\n",
    "    return f'{self.type}_{act}' if act else self.type"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "5000 sample events (taken from a single recent period) are available, and are downloaded and cached by `load_sample_events`. `full_type` provides the combination of `type` and `payload.action` (where available) for each event. Here's the frequency of all `full_types` in the sample:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 30784 characters]\n",
      "text/plain": [
       "<Figure size 576x432 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "evts = load_sample_events()\n",
    "x,y = zip(*Counter([o.full_type for o in evts]).most_common())\n",
    "plt.figure(figsize=(8, 6))\n",
    "plt.barh(x,y);"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#export\n",
    "_all_ = ['PageBuildEvent', 'ContentReferenceEvent', 'RepositoryImportEvent', 'CreateEvent', 'WorkflowRunEvent', 'DeleteEvent', 'OrganizationEvent', 'SponsorshipEvent', 'ProjectColumnEvent', 'PushEvent', 'ContextEvent', 'MilestoneEvent', 'ProjectCardEvent', 'ProjectEvent', 'PackageEvent', 'PullRequestEvent', 'RepositoryDispatchEvent', 'TeamAddEvent', 'WorkflowDispatchEvent', 'MemberEvent', 'MetaEvent', 'CodeScanningAlertEvent',     'PublicEvent', 'NeedsEvent', 'CheckRunEvent', 'SecurityAdvisoryEvent', 'PullRequestReviewCommentEvent', 'OrgBlockEvent', 'CommitCommentEvent', 'WatchEvent', 'MarketplacePurchaseEvent', 'StarEvent', 'InstallationRepositoriesEvent', 'CheckSuiteEvent', 'GithubAppAuthorizationEvent', 'TeamEvent', 'StatusEvent', 'RepositoryVulnerabilityAlertEvent', 'PullRequestReviewEvent', 'LabelEvent', 'InstallationEvent', 'ReleaseEvent', 'IssuesEvent', 'RepositoryEvent', 'GollumEvent', 'MembershipEvent', 'DeploymentEvent', 'DeployKeyEvent', 'IssueCommentEvent', 'PingEvent', 'DeploymentStatusEvent', 'ForkEvent', 'ScheduleEvent']"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#export\n",
    "evt_emojis = dict(\n",
    "    PushEvent=                             '⭐',\n",
    "    CreateEvent=                           '🏭',\n",
    "    IssueCommentEvent_created=             '💬',\n",
    "    WatchEvent_started=                    '👀',\n",
    "    PullRequestEvent_closed=               '📪',\n",
    "    PullRequestReviewEvent_created=        '💌',\n",
    "    PullRequestEvent_opened=               '📬',\n",
    "    PullRequestReviewCommentEvent_created= '🗨',\n",
    "    DeleteEvent=                           '✂',\n",
    "    ForkEvent=                             '🍽',\n",
    "    IssuesEvent_opened=                    '🐛',\n",
    "    IssuesEvent_closed=                    '🎁',\n",
    "    ReleaseEvent_published=                '🚀',\n",
    "    MemberEvent_added=                     '💃',\n",
    "    CommitCommentEvent=                    '🎉',\n",
    "    GollumEvent=                           '📚',\n",
    "    PublicEvent=                           '♥',\n",
    "    IssuesEvent_reopened=                  '🔁',\n",
    "    PullRequestEvent_reopened=             '🔁'\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#export\n",
    "def _ref(pay, pre=''): return f'{pre} \"{pay.ref.split(\"/\")[-1]}\"' if pay.ref else ''\n",
    "def _ref_detl(pay): return pay.ref_type + _ref(pay)\n",
    "\n",
    "def _action(self):\n",
    "    pay = self.payload\n",
    "    det = (f'issue #{pay.issue.number} on' if isinstance(self,IssuesEvent) else\n",
    "           f'PR #{pay.number} on' if isinstance(self,PullRequestEvent) else\n",
    "           f'member {pay.member.login} in' if isinstance(self,MemberEvent) else\n",
    "           f'review comment on PR #{pay.pull_request.number} in' if isinstance(self,PullRequestReviewCommentEvent) else\n",
    "           f'comment on issue #{pay.issue.number} in' if isinstance(self,IssueCommentEvent) else\n",
    "           f'release {pay.release.tag_name} in' if isinstance(self,ReleaseEvent) else\n",
    "           'PR review in' if isinstance(self,PullRequestReviewEvent) else\n",
    "           'watching' if isinstance(self,WatchEvent) else '')\n",
    "    if det: return f'{pay.action} {det}'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#export\n",
    "#hide\n",
    "@patch(as_prop=True)\n",
    "def description(self:GhEvent):\n",
    "    \"Description of event\"\n",
    "    act,pay,cls,repo = self.actor,self.payload,type(self),self.repo\n",
    "    res = _action(self)\n",
    "    return res if res else (\n",
    "        f'deleted {_ref_detl(pay)} in' if isinstance(self,DeleteEvent) else\n",
    "        f'created {_ref_detl(pay)} in' if isinstance(self,CreateEvent) else\n",
    "        f'pushed {len(pay.commits)} commits{_ref(pay,\" to\")} in' if isinstance(self,PushEvent) else\n",
    "        f'created commit comment in' if isinstance(self,CommitCommentEvent) else\n",
    "        f'{pay.pages[0].action} wiki page in' if isinstance(self,GollumEvent) else\n",
    "        f'forked' if isinstance(self,ForkEvent) else\n",
    "        f'made public repo ' if isinstance(self,PublicEvent) else\n",
    "        remove_suffix(self.type, \"Event\")\n",
    "    )\n",
    "\n",
    "#export\n",
    "@patch(as_prop=True)\n",
    "def emoji(self:GhEvent):\n",
    "    \"Emoji for event from `evt_emojis`\"\n",
    "    return evt_emojis.get(self.full_type, '❌')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#export\n",
    "#hide\n",
    "described_evts = (PushEvent,CreateEvent,IssueCommentEvent,WatchEvent,PullRequestEvent,PullRequestReviewEvent,PullRequestReviewCommentEvent,\n",
    "                  DeleteEvent,ForkEvent,IssuesEvent,ReleaseEvent,MemberEvent,CommitCommentEvent,GollumEvent,PublicEvent)\n",
    "\n",
    "_text_keys = dict(\n",
    "    CreateEvent = \"description\",\n",
    "    PullRequestEvent = \"pull_request.title\",\n",
    "    PullRequestReviewCommentEvent = \"comment.body\",\n",
    "    ForkEvent = \"forkee.description\",\n",
    "    CommitCommentEvent = \"comment.body\",\n",
    "    PullRequestReviewEvent = \"review.body\",\n",
    "    ReleaseEvent = \"release.body\",\n",
    "    IssuesEvent = \"issue.title\",\n",
    "    IssueCommentEvent = \"comment.body\"\n",
    ")\n",
    "\n",
    "@patch(as_prop=True)\n",
    "def text(self:GhEvent):\n",
    "    \"Text (e.g. body or title) of event, if exists\"\n",
    "    return nested_idx(self.payload, *_text_keys.get(self.type, '').split('.')) or ''"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "You can use the `description`, `text`, and `emoji` properties to display events, e.g:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/markdown": [
       "|Type|Description|\n",
       "|:--|:--|\n",
       "|Push|⭐ **fedya** pushed 1 commits to \"rolling\" in *OpenMandrivaAssociation/g…*|\n",
       "|Delete|✂ **dependabot[bot]** deleted branch \"packit-0.5.0\" in *paketo-buildpacks/occam*|\n",
       "|Create|🏭 **github-classroom[bot]** created repository in *Introduction-to-Programming-OSOWSKI/…*: \"1-5-add-two-numbers-noraahlman23 created by GitHu…\"|\n",
       "|PR |📬 **JerryMacedoCastro** opened PR #1 on *JerryMacedoCastro/tb-arquitetura-js*: \"Input component created\"|\n",
       "|Member|💃 **github-classroom[bot]** added member noraahlman23 in *Introduction-to-Programming-O…*|\n",
       "|Public|♥ **navikt** made public repo  *navikt/syfoinntektsmelding*|\n",
       "|PR  Review Comment|🗨 **fruttasecca** created review comment on PR #97 in *orchest/orchest*: \"Does this make it so that users coming from an ol…\"|\n",
       "|Fork|🍽 **amin-lotf** forked *vinaygaba/Learn-Jetpack-Compose-By-Example*: \"🚀 This project contains various examples that sho…\"|\n",
       "|Commit Comment|🎉 **vercel[bot]** created commint comment in *Samaraferreira/test-frontend*: \"Successfully deployed to the following URLs:  * […\"|\n",
       "|Watch|👀 **ikarius** started watching *jorgebucaran/awsm.fish*|\n",
       "|PR  Review|💌 **fruttasecca** created PR review in *orchest/orchest*|\n",
       "|Release|🚀 **github-actions[bot]** published release v1.1.3 in *vouv/srun*|\n",
       "|Issues|🐛 **efritz** opened issue #16915 on *sourcegraph/sourcegraph*: \"auto-indexing: Add docker/src-cli command wall cl…\"|\n",
       "|Gollum|📚 **TradingToolCrypto** created wiki page in *TradingToolCrypto/TradingTool-Wiki*|\n",
       "|Issue Comment|💬 **jerhard** created comment on issue #2570 in *ls1intum/Artemis*: \"Do you think that spaces (or even white space cha…\"|"
      ],
      "text/plain": [
       "<IPython.core.display.Markdown object>"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "exs = [first(evts, risinstance(o)) for o in described_evts]\n",
    "\n",
    "def _fmt_evt(o):\n",
    "    res = f'{o.emoji} **{o.actor.login}** ' + truncstr(f'{o.description} *{o.repo.name}',60) + '*'\n",
    "    if o.text: res += f': \"{truncstr(o.text, 50)}\"'\n",
    "    return res.replace('\\n',' ')\n",
    "\n",
    "Markdown('|Type|Description|\\n|:--|:--|\\n' +\n",
    "         '\\n'.join(f'|{camel2words(o.type.replace(\"PullRequest\",\"PR \")[:-5])}|{_fmt_evt(o)}|' for o in exs))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Export -"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Converted 00_core.ipynb.\n",
      "Converted 01_actions.ipynb.\n",
      "Converted 02_auth.ipynb.\n",
      "Converted 03_page.ipynb.\n",
      "Converted 04_event.ipynb.\n",
      "Converted 10_cli.ipynb.\n",
      "Converted 50_fullapi.ipynb.\n",
      "Converted 80_tutorial_actions.ipynb.\n",
      "Converted 90_build_lib.ipynb.\n",
      "Converted index.ipynb.\n"
     ]
    }
   ],
   "source": [
    "#hide\n",
    "from nbdev.export import notebook2script\n",
    "notebook2script()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
