From 87315d5a4058e83eb5b37f96920cf456f502045c Mon Sep 17 00:00:00 2001 From: Reid 'arrdem' McKenzie <me@arrdem.com> Date: Thu, 27 Jul 2023 16:34:13 -0600 Subject: [PATCH 1/2] Update requirements --- tools/python/requirements.in | 1 + tools/python/requirements_lock.txt | 88 ++++++++++++++++-------------- 2 files changed, 48 insertions(+), 41 deletions(-) diff --git a/tools/python/requirements.in b/tools/python/requirements.in index f48906d..7546ca0 100644 --- a/tools/python/requirements.in +++ b/tools/python/requirements.in @@ -53,3 +53,4 @@ toml unify yamllint yaspin +pytimeparse diff --git a/tools/python/requirements_lock.txt b/tools/python/requirements_lock.txt index 5c93185..e2c15b9 100644 --- a/tools/python/requirements_lock.txt +++ b/tools/python/requirements_lock.txt @@ -1,25 +1,26 @@ -aiohttp==3.8.4 +aiohttp==3.8.5 aiohttp-basicauth==1.0.0 aiosignal==1.3.1 -aiosql==8.0 +aiosql==9.0 alabaster==0.7.13 -async-lru==2.0.2 +annotated-types==0.5.0 +async-lru==2.0.4 async-timeout==4.0.2 attrs==23.1.0 autocommand==2.2.2 -autoflake==2.1.1 +autoflake==2.2.0 Babel==2.12.1 beautifulsoup4==4.12.2 -black==23.3.0 +black==23.7.0 blinker==1.6.2 build==0.10.0 cachetools==5.3.1 -certifi==2023.5.7 -charset-normalizer==3.1.0 +certifi==2023.7.22 +charset-normalizer==3.2.0 cheroot==10.0.0 CherryPy==18.8.0 -click==8.1.3 -colored==1.4.4 +click==8.1.6 +colored==2.2.3 commonmark==0.9.1 coverage==7.2.7 decorator==5.1.1 @@ -28,53 +29,54 @@ docutils==0.20.1 ExifRead==3.0.0 flake8==6.0.0 Flask==2.3.2 -frozenlist==1.3.3 -hypothesis==6.75.9 +frozenlist==1.4.0 +hypothesis==6.82.0 ibis==3.2.0 icmplib==3.0.3 idna==3.4 imagesize==1.4.1 -inflect==6.0.4 +inflect==7.0.0 iniconfig==2.0.0 isort==5.12.0 itsdangerous==2.1.2 -jaraco.collections==4.2.0 +jaraco.collections==4.3.0 jaraco.context==4.3.0 -jaraco.functools==3.7.0 -jaraco.text @ git+https://github.com/arrdem/jaraco.text.git@0dd8d0b25a93c3fad896f3a92d11caff61ff273d +jaraco.functools==3.8.0 +jaraco.text==3.11.1 jedi==0.18.2 Jinja2==3.1.2 -jsonschema==4.17.3 -jsonschema-spec==0.1.4 -lark==1.1.5 +jsonschema==4.18.4 +jsonschema-spec==0.2.3 +jsonschema-specifications==2023.7.1 +lark==1.1.7 lazy-object-proxy==1.9.0 libsass==0.22.0 livereload==2.6.3 -lxml==4.9.2 -Markdown==3.4.3 +lxml==4.9.3 +Markdown==3.4.4 MarkupSafe==2.1.3 mccabe==0.7.0 -meraki==1.33.0 +meraki==1.34.0 mirakuru==2.5.1 mistune==2.0.5 -more-itertools==9.1.0 +more-itertools==10.0.0 multidict==6.0.4 mypy-extensions==1.0.0 octorest==0.4 -openapi-schema-validator==0.4.4 -openapi-spec-validator==0.5.6 +openapi-schema-validator==0.6.0 +openapi-spec-validator==0.6.0 packaging==23.1 parso==0.8.3 pathable==0.4.3 pathspec==0.11.1 picobox==3.0.0 pip==23.1.2 -pip-tools==6.13.0 -platformdirs==3.5.1 -pluggy==1.0.0 -port-for==0.6.3 -portend==3.1.0 -prompt-toolkit==3.0.38 +pip-tools==7.1.0 +platformdirs==3.9.1 +pluggy==1.2.0 +port-for==0.7.1 +portend==3.2.0 +prompt-toolkit==3.0.39 proquint==0.2.1 psutil==5.9.5 psycopg==3.1.9 @@ -83,30 +85,34 @@ pudb==2022.1.3 py==1.11.0 pycodestyle==2.10.0 pycryptodome==3.18.0 -pydantic==1.10.8 +pydantic==2.1.1 +pydantic_core==2.4.0 pyflakes==3.0.1 Pygments==2.15.1 pyproject_hooks==1.0.0 pyrsistent==0.19.3 -pytest==7.3.1 +pytest==7.4.0 pytest-cov==4.1.0 pytest-postgresql==5.0.0 pytest-pudb==0.7.0 pytest-timeout==2.1.0 +pytimeparse==1.1.8 pytz==2023.3 -PyYAML==6.0 +PyYAML==6.0.1 recommonmark==0.7.1 -redis==4.5.5 +redis==4.6.0 +referencing==0.29.3 requests==2.31.0 retry==0.9.2 rfc3339-validator==0.1.4 -setuptools==67.7.2 +rpds-py==0.9.2 +setuptools==68.0.0 six==1.16.0 smbus2==0.4.2 snowballstemmer==2.2.0 sortedcontainers==2.4.0 soupsieve==2.4.1 -Sphinx==7.0.1 +Sphinx==7.1.1 sphinx_mdinclude==0.5.3 sphinxcontrib-applehelp==1.0.4 sphinxcontrib-devhelp==1.0.2 @@ -117,19 +123,19 @@ sphinxcontrib-openapi==0.8.1 sphinxcontrib-programoutput==0.17 sphinxcontrib-qthelp==1.0.3 sphinxcontrib-serializinghtml==1.1.5 -tempora==5.2.2 +tempora==5.5.0 termcolor==2.3.0 toml==0.10.2 tornado==6.3.2 -typing_extensions==4.6.3 +typing_extensions==4.7.1 unify==0.5 untokenize==0.1.1 -urllib3==2.0.2 +urllib3==2.0.4 urwid==2.1.2 urwid-readline==0.13 wcwidth==0.2.6 -websocket-client==1.5.2 -Werkzeug==2.3.4 +websocket-client==1.6.1 +Werkzeug==2.3.6 wheel==0.40.0 yamllint==1.32.0 yarl==1.9.2 From 64c46222008c35816e555af62709624f02ca5513 Mon Sep 17 00:00:00 2001 From: Reid 'arrdem' McKenzie <me@arrdem.com> Date: Thu, 27 Jul 2023 16:34:26 -0600 Subject: [PATCH 2/2] Create the unnotifier --- projects/gh-unnotifier/BUILD | 9 ++ .../src/python/ghunnotif/__main__.py | 125 ++++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 projects/gh-unnotifier/BUILD create mode 100644 projects/gh-unnotifier/src/python/ghunnotif/__main__.py diff --git a/projects/gh-unnotifier/BUILD b/projects/gh-unnotifier/BUILD new file mode 100644 index 0000000..a820dab --- /dev/null +++ b/projects/gh-unnotifier/BUILD @@ -0,0 +1,9 @@ +py_project( + name = "gh-unnotifier", + main = "src/python/ghunnotif/__main__.py", + main_deps = [ + py_requirement("click"), + py_requirement("requests"), + py_requirement("pytimeparse"), + ] +) diff --git a/projects/gh-unnotifier/src/python/ghunnotif/__main__.py b/projects/gh-unnotifier/src/python/ghunnotif/__main__.py new file mode 100644 index 0000000..d4a7e44 --- /dev/null +++ b/projects/gh-unnotifier/src/python/ghunnotif/__main__.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 + +import os +from pathlib import Path +import tomllib +from typing import Optional +from datetime import datetime, timedelta, timezone +from time import sleep + +import click +import requests +import pytimeparse + +class Client(object): + BASE = "https://api.github.com" + HEADERS = {"Accept": "application/vnd.github+json"} + + def __init__(self, token): + self._token = token + self._session = requests.Session() + self._session.headers["Authorization"] = f"Bearer {token}" + + def get_notifications(self, page=1): + resp = self._session.get(f"{self.BASE}/notifications", headers=self.HEADERS, params={"page": page, "all": "false"}) + resp.raise_for_status() + return resp.json() + + def get_all_notifications(self): + page = 1 + while True: + results = self.get_notifications(page=page) + if not results: + return + yield from results + page += 1 + + def read(self, notif): + return self._session.patch(notif["url"]).raise_for_status() + + def unsubscribe(self, notif): + return self._session.delete(notif["subscription_url"]).raise_for_status() + + def get_pr(self, + url: Optional[str] = None, + repo: Optional[str] = None, + id: Optional[int] = None): + url = url or f"{self.BASE}/{repo}/pulls/{id}" + resp = self._session.get(url, headers=self.HEADERS) + resp.raise_for_status() + return resp.json() + + +@click.group() +def cli(): + pass + + +@cli.command() +@click.option("--config", "config_path", type=Path, default=lambda: Path(os.getenv("BUILD_WORKSPACE_DIRECTORY", "")) / "projects/gh-unnotifier/config.toml") +def oneshot(config_path: Path): + with open(config_path, "rb") as fp: + config = tomllib.load(fp) + + client = Client(config["gh-unnotifier"]["api_key"]) + for notif in client.get_all_notifications(): + subject = notif["subject"] + pr = None + if "/pulls/" in subject["url"]: + pr = client.get_pr(url=subject["url"]) + if pr["state"] == "closed": + client.read(notif) + client.unsubscribe(notif) + + print("Resolved", notif["id"]) + continue + + print(notif["id"], notif["subscription_url"], notif["reason"], notif["subject"], pr) + + +def parse_seconds(text: str) -> timedelta: + return timedelta(seconds=pytimeparse.parse(text)) + + +@cli.command() +@click.option("--config", "config_path", type=Path, default=lambda: Path(os.getenv("BUILD_WORKSPACE_DIRECTORY", "")) / "projects/gh-unnotifier/config.toml") +@click.option("--schedule", "schedule", default="15 seconds", type=parse_seconds) +def maintain(config_path: Path, schedule: timedelta): + with open(config_path, "rb") as fp: + config = tomllib.load(fp) + + client = Client(config["gh-unnotifier"]["api_key"]) + mark = None + while True: + try: + prev = mark + mark = datetime.now(timezone.utc) + tick = mark + schedule + assert tick - schedule == mark + for notif in client.get_all_notifications(): + subject = notif["subject"] + + # Don't waste time on notifications which haven't changed since the last scrub + updated_at = datetime.fromisoformat(notif["updated_at"]) + if prev and updated_at < prev: + continue + + pr = None + if "/pulls/" in subject["url"]: + pr = client.get_pr(url=subject["url"]) + if pr["state"] == "closed": + client.read(notif) + client.unsubscribe(notif) + + click.echo(f"Resolved {notif['id']}") + continue + + click.echo("Napping...") + sleep((tick - datetime.now(timezone.utc)).total_seconds()) + + except KeyboardInterrupt: + break + + +if __name__ == "__main__": + cli()