├── .gitignore ├── CHANGELOG.md ├── MANIFEST.in ├── Makefile ├── README.md ├── ghtop ├── __init__.py ├── all_rich.py ├── ghtop.py └── richext.py ├── settings.ini └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | docs 2 | _nbdev.py 3 | log.txt 4 | *.bak 5 | .gitattributes 6 | .last_checked 7 | .gitconfig 8 | *.bak 9 | *.log 10 | *~ 11 | ~* 12 | _tmp* 13 | tmp* 14 | tags 15 | 16 | # Byte-compiled / optimized / DLL files 17 | __pycache__/ 18 | *.py[cod] 19 | *$py.class 20 | 21 | # C extensions 22 | *.so 23 | 24 | # Distribution / packaging 25 | .Python 26 | env/ 27 | build/ 28 | develop-eggs/ 29 | dist/ 30 | downloads/ 31 | eggs/ 32 | .eggs/ 33 | lib/ 34 | lib64/ 35 | parts/ 36 | sdist/ 37 | var/ 38 | wheels/ 39 | *.egg-info/ 40 | .installed.cfg 41 | *.egg 42 | 43 | # PyInstaller 44 | # Usually these files are written by a python script from a template 45 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 46 | *.manifest 47 | *.spec 48 | 49 | # Installer logs 50 | pip-log.txt 51 | pip-delete-this-directory.txt 52 | 53 | # Unit test / coverage reports 54 | htmlcov/ 55 | .tox/ 56 | .coverage 57 | .coverage.* 58 | .cache 59 | nosetests.xml 60 | coverage.xml 61 | *.cover 62 | .hypothesis/ 63 | 64 | # Translations 65 | *.mo 66 | *.pot 67 | 68 | # Django stuff: 69 | *.log 70 | local_settings.py 71 | 72 | # Flask stuff: 73 | instance/ 74 | .webassets-cache 75 | 76 | # Scrapy stuff: 77 | .scrapy 78 | 79 | # Sphinx documentation 80 | docs/_build/ 81 | 82 | # PyBuilder 83 | target/ 84 | 85 | # Jupyter Notebook 86 | .ipynb_checkpoints 87 | 88 | # pyenv 89 | .python-version 90 | 91 | # celery beat schedule file 92 | celerybeat-schedule 93 | 94 | # SageMath parsed files 95 | *.sage.py 96 | 97 | # dotenv 98 | .env 99 | 100 | # virtualenv 101 | .venv 102 | venv/ 103 | ENV/ 104 | 105 | # Spyder project settings 106 | .spyderproject 107 | .spyproject 108 | 109 | # Rope project settings 110 | .ropeproject 111 | 112 | # mkdocs documentation 113 | /site 114 | 115 | # mypy 116 | .mypy_cache/ 117 | 118 | .vscode 119 | *.swp 120 | 121 | # osx generated files 122 | .DS_Store 123 | .DS_Store? 124 | .Trashes 125 | ehthumbs.db 126 | Thumbs.db 127 | .idea 128 | 129 | # pytest 130 | .pytest_cache 131 | 132 | # tools/trust-doc-nbs 133 | docs_src/.last_checked 134 | 135 | # symlinks to fastai 136 | docs_src/fastai 137 | tools/fastai 138 | 139 | # link checker 140 | checklink/cookies.txt 141 | 142 | # .gitconfig is now autogenerated 143 | .gitconfig 144 | 145 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Release notes 2 | 3 | 4 | 5 | ## 0.0.4 6 | 7 | - Add CLI flags for filtering by type, repo, etc 8 | - Use ghapi.event parallel event stream 9 | 10 | ## 0.0.3 11 | 12 | - failed to handle stored token 13 | - missing deps 14 | 15 | ## 0.0.1 16 | 17 | - Initial release 18 | 19 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include settings.ini 2 | include LICENSE 3 | include README.md 4 | recursive-exclude * __pycache__ 5 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .ONESHELL: 2 | SHELL := /bin/bash 3 | SRC = $(wildcard nbs/*.ipynb) 4 | 5 | all: ghtop docs 6 | 7 | ghtop: $(SRC) 8 | nbdev_build_lib 9 | touch ghtop 10 | 11 | sync: 12 | nbdev_update_lib 13 | 14 | docs_serve: docs 15 | cd docs && bundle exec jekyll serve 16 | 17 | docs: $(SRC) 18 | nbdev_build_docs 19 | touch docs 20 | 21 | release: pypi 22 | nbdev_bump_version 23 | 24 | conda_release: 25 | fastrelease_conda_package 26 | 27 | pypi: dist 28 | twine upload --repository pypi dist/* 29 | 30 | dist: clean 31 | python setup.py sdist bdist_wheel 32 | 33 | clean: 34 | rm -rf dist 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ghtop 2 | > See what's happening on GitHub in real time (also helpful if you need to use up your API quota as quickly as possible). 3 | 4 | 5 | `ghtop` provides a number of views of all current public activity from all users across the entire GitHub platform. (Note that GitHub delays all events by five minutes.) 6 | 7 | 8 | 9 | ## Install 10 | 11 | Either `pip install ghtop` or `conda install -c fastai ghtop`. 12 | 13 | ## How to use 14 | 15 | Run `ghtop -h` to view the help: 16 | 17 | ``` 18 | $ ghtop -h 19 | usage: ghtop [-h] [--include_bots] [--types TYPES] [--pause PAUSE] [--filt {users,repo,org}] [--filtval FILTVAL] {tail,quad,users,simple} 20 | 21 | positional arguments: 22 | {tail,quad,users,simple} Operation mode to run 23 | 24 | optional arguments: 25 | -h, --help show this help message and exit 26 | --include_bots Include bots (there's a lot of them!) (default: False) 27 | --types TYPES Comma-separated types of event to include (e.g PushEvent) (default: ) 28 | --pause PAUSE Number of seconds to pause between requests to the GitHub api (default: 0.4) 29 | --filt {users,repo,org} Filtering method 30 | --filtval FILTVAL Value to filter by (for `repo` use format `owner/repo`) 31 | ``` 32 | 33 | There are 4 views you can choose: `ghtop simple`, `ghtop tail`, `ghtop quad`, or `ghtop users`. Each are shown and described below. All views have the following options: 34 | 35 | - `--include_bots`: By default events that appear to be from bots are excluded. Add this flag to include them 36 | - `--types TYPES`: Optional comma-separated list of event types to include (defaults to all types). For a full list of types, see the GitHub [event types docs](https://docs.github.com/en/free-pro-team@latest/developers/webhooks-and-events/github-event-types) 37 | - `--pause PAUSE`: Number of seconds to pause between requests to the GitHub api (default: 0.4). It is helpful to adjust this number if you want to get events more or less frequently. For example, if you are filtering all events by an org, then you likely want to pause for a longer period of time than the default. 38 | - `--filt` and `--filtval`: Optionally filter events to just those from one of: `user`, `repo`, or `org`, depending on `filt`. `filtval` is the value to filter by. See the [GitHub docs](https://docs.github.com/en/free-pro-team@latest/rest/reference/activity#list-public-events) for details on the public event API calls used. 39 | 40 | **Important note**: while running, `ghtop` will make about 5 API calls per second. GitHub has a quota of 5000 calls per hour. When there are 1000 calls left, `ghtop` will show a warning on every call. 41 | 42 | ### ghtop simple 43 | 44 | A simple dump to your console of all events as they happen. 45 | 46 | 47 | 48 | ### ghtop tail 49 | 50 | Like `simple`, but only includes releases, issues and PRs (open, close, and comment events). A summary of the frequency of different kind of events along with sparklines are shown at the top of the screen. 51 | 52 | 53 | 54 | ### ghtop quad 55 | 56 | The same information as `tail`, but in a split window showing separately PRs, issues, pushes, and releases. 57 | 58 | 59 | 60 | ### ghtop users 61 | 62 | A summary of activity for the most active current users. 63 | 64 | 65 | 66 | ---- 67 | 68 | Shared under the MIT license with ♥ by @nat 69 | -------------------------------------------------------------------------------- /ghtop/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.0.6" 2 | -------------------------------------------------------------------------------- /ghtop/all_rich.py: -------------------------------------------------------------------------------- 1 | from rich import print as pr 2 | from rich.align import * 3 | from rich.bar import * 4 | from rich.color import * 5 | from rich.columns import * 6 | from rich.console import * 7 | from rich.emoji import * 8 | from rich.highlighter import * 9 | from rich.live import * 10 | from rich.logging import * 11 | from rich.markdown import * 12 | from rich.markup import * 13 | from rich.measure import * 14 | from rich.padding import * 15 | from rich.panel import * 16 | from rich.pretty import * 17 | from rich.progress_bar import * 18 | from rich.progress import * 19 | from rich.prompt import * 20 | from rich.protocol import * 21 | from rich.rule import * 22 | from rich.segment import * 23 | from rich.spinner import * 24 | from rich.status import * 25 | from rich.style import * 26 | from rich.styled import * 27 | from rich.syntax import * 28 | from rich.table import * 29 | from rich.text import * 30 | from rich.theme import * 31 | from rich.traceback import * 32 | from fastcore.all import * 33 | 34 | @delegates(Style) 35 | def text(s, maxlen=None, **kwargs): 36 | "Create a styled `Text` object" 37 | if maxlen: s = truncstr(s, maxlen=maxlen) 38 | return Text(s, style=Style(**kwargs)) 39 | 40 | @delegates(Style) 41 | def segment(s, maxlen=None, space=' ', **kwargs): 42 | "Create a styled `Segment` object" 43 | if maxlen: s = truncstr(s, maxlen=maxlen, space=space) 44 | return Segment(s, style=Style(**kwargs)) 45 | 46 | class Segments(list): 47 | def __init__(self, options): self.w = options.max_width 48 | 49 | @property 50 | def chars(self): return sum(o.cell_length for o in self) 51 | def txtlen(self, pct): return min((self.w-self.chars)*pct, 999) 52 | 53 | @delegates(segment) 54 | def add(self, x, maxlen=None, pct=None, **kwargs): 55 | if pct: maxlen = math.ceil(self.txtlen(pct)) 56 | self.append(segment(x, maxlen=maxlen, **kwargs)) 57 | 58 | @delegates(Table) 59 | def _grid(box=None, padding=0, collapse_padding=True, pad_edge=False, expand=False, show_header=False, show_edge=False, **kwargs): 60 | return Table(padding=padding, pad_edge=pad_edge, expand=expand, collapse_padding=collapse_padding, 61 | box=box, show_header=show_header, show_edge=show_edge, **kwargs) 62 | 63 | @delegates(_grid) 64 | def grid(items, expand=True, no_wrap=True, **kwargs): 65 | g = _grid(expand=expand, **kwargs) 66 | for c in items[0]: g.add_column(no_wrap=no_wrap, justify='center') 67 | for i in items: g.add_row(*i) 68 | return g 69 | 70 | Color = str_enum('Color', *ANSI_COLOR_NAMES) 71 | 72 | class Deque(deque): 73 | def __rich__(self): return RenderGroup(*(filter(None, self))) 74 | 75 | @delegates() 76 | class FixedPanel(Panel, GetAttr): 77 | _default='renderable' 78 | def __init__(self, height, **kwargs): 79 | super().__init__(Deque([' ']*height, maxlen=height), **kwargs) 80 | 81 | @delegates(Style) 82 | def add(self, s:str, **kwargs): 83 | "Add styled `s` to panel" 84 | self.append(text(s, **kwargs)) 85 | -------------------------------------------------------------------------------- /ghtop/ghtop.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | __all__ = ['get_sparklines', 'ETYPES', 'term', 'tdim', 'limit_cb', 'Events', 'print_event', 'pct_comp', 'tail_events', 4 | 'watch_users', 'quad_logs', 'simple', 'main'] 5 | 6 | 7 | import sys, signal, shutil, os, json, emoji, enlighten 8 | from dashing import * 9 | from collections import defaultdict 10 | from warnings import warn 11 | from itertools import islice 12 | 13 | from fastcore.utils import * 14 | from fastcore.foundation import * 15 | from fastcore.script import * 16 | from ghapi.all import * 17 | from .richext import * 18 | from .all_rich import (Console, Color, FixedPanel, box, Segments, Live, 19 | grid, ConsoleOptions, Progress, BarColumn, Spinner, Table) 20 | 21 | ETYPES=PushEvent,PullRequestEvent,IssuesEvent,ReleaseEvent 22 | 23 | def get_sparklines(): 24 | s1 = ESpark('Push', 'magenta', [PushEvent], mx=30) 25 | s2 = ESpark('PR', 'yellow', [PullRequestEvent, PullRequestReviewCommentEvent, PullRequestReviewEvent], mx=8) 26 | s3 = ESpark('Issues', 'green', [IssueCommentEvent,IssuesEvent], mx=6) 27 | s4 = ESpark('Releases', 'blue', [ReleaseEvent], mx=0.4) 28 | s5 = ESpark('All Events', 'orange', mx=45) 29 | 30 | return Stats([s1,s2,s3,s4,s5], store=5, span=5, spn_lbl='5/s', show_freq=True) 31 | 32 | 33 | term = Terminal() 34 | 35 | tdim = L(os.popen('stty size', 'r').read().split()) 36 | if not tdim: theight,twidth = 15,15 37 | else: theight,twidth = tdim.map(lambda x: max(int(x)-4, 15)) 38 | 39 | 40 | def _exit(msg): 41 | print(msg, file=sys.stderr) 42 | sys.exit() 43 | 44 | 45 | def limit_cb(rem,quota): 46 | "Callback to warn user when close to using up hourly quota" 47 | w='WARNING '*7 48 | if rem < 1000: print(f"{w}\nRemaining calls: {rem} out of {quota}\n{w}", file=sys.stderr) 49 | 50 | 51 | Events = dict( 52 | IssuesEvent_closed=('⭐', 'closed', noop), 53 | IssuesEvent_opened=('📫', 'opened', noop), 54 | IssueCommentEvent=('💬', 'commented on', term.white), 55 | PullRequestEvent_opened=('✨', 'opened a pull request', term.yellow), 56 | PullRequestEvent_closed=('✔', 'closed a pull request', term.green), 57 | ) 58 | 59 | 60 | def _to_log(e): 61 | login,repo,pay = e.actor.login,e.repo.name,e.payload 62 | typ = e.type + (f'_{pay.action}' if e.type in ('PullRequestEvent','IssuesEvent') else '') 63 | emoji,msg,color = Events.get(typ, [0]*3) 64 | if emoji: 65 | xtra = '' if e.type == "PullRequestEvent" else f' issue # {pay.issue.number}' 66 | d = try_attrs(pay, "pull_request", "issue") 67 | return color(f'{emoji} {login} {msg}{xtra} on repo {repo[:20]} ("{d.title[:50]}...")') 68 | elif e.type == "ReleaseEvent": return f'🚀 {login} released {e.payload.release.tag_name} of {repo}' 69 | 70 | 71 | def print_event(e, counter): 72 | res = _to_log(e) 73 | if res: print(res) 74 | elif counter and e.type == "PushEvent": [counter.update() for c in e.payload.commits] 75 | elif e.type == "SecurityAdvisoryEvent": print(term.blink("SECURITY ADVISORY")) 76 | 77 | 78 | def pct_comp(api): return int(((5000-int(api.limit_rem)) / 5000) * 100) 79 | 80 | 81 | def tail_events(evt, api): 82 | "Print events from `fetch_events` along with a counter of push events" 83 | p = FixedPanel(theight, box=box.HORIZONTALS, title='ghtop') 84 | s = get_sparklines() 85 | g = grid([[s], [p]]) 86 | with Live(g): 87 | for e in evt: 88 | s.add_events(e) 89 | s.update_prog(pct_comp(api)) 90 | p.append(e) 91 | g = grid([[s], [p]]) 92 | 93 | 94 | def _user_grid(): 95 | g = Table.grid(expand=True) 96 | g.add_column(justify="left") 97 | for i in range(4): g.add_column(justify="center") 98 | g.add_row("", "", "", "", "") 99 | g.add_row("User", "Events", "PRs", "Issues", "Pushes") 100 | return g 101 | 102 | 103 | def watch_users(evts, api): 104 | "Print a table of the users with the most events" 105 | users,users_events = defaultdict(int),defaultdict(lambda: defaultdict(int)) 106 | 107 | with Live() as live: 108 | s = get_sparklines() 109 | while True: 110 | for x in islice(evts, 10): 111 | users[x.actor.login] += 1 112 | users_events[x.actor.login][x.type] += 1 113 | s.add_events(x) 114 | 115 | ig = _user_grid() 116 | sorted_users = sorted(users.items(), key=lambda o: (o[1],o[0]), reverse=True) 117 | for u in sorted_users[:theight]: 118 | data = (*u, *itemgetter('PullRequestEvent','IssuesEvent','PushEvent')(users_events[u[0]])) 119 | ig.add_row(*L(data).map(str)) 120 | 121 | s.update_prog(pct_comp(api)) 122 | g = grid([[s], [ig]]) 123 | live.update(g) 124 | 125 | 126 | def _panelDict2Grid(pd): 127 | ispush,ispr,isiss,isrel = pd.values() 128 | return grid([[ispush,ispr],[isiss,isrel]], width=twidth) 129 | 130 | 131 | def quad_logs(evts, api): 132 | "Print 4 panels, showing most recent issues, commits, PRs, and releases" 133 | pd = {o:FixedPanel(height=(theight//2)-1, 134 | width=(twidth//2)-1, 135 | box=box.HORIZONTALS, 136 | title=camel2words(remove_suffix(o.__name__,'Event'))) for o in ETYPES} 137 | p = _panelDict2Grid(pd) 138 | s = get_sparklines() 139 | g = grid([[s], [p]]) 140 | with Live(g): 141 | for e in evts: 142 | s.add_events(e) 143 | s.update_prog(pct_comp(api)) 144 | typ = type(e) 145 | if typ in pd: pd[typ].append(e) 146 | p = _panelDict2Grid(pd) 147 | g = grid([[s], [p]]) 148 | 149 | 150 | def simple(evts, api): 151 | for ev in evts: print(f"{ev.actor.login} {ev.type} {ev.repo.name}") 152 | 153 | 154 | def _get_token(): 155 | path = Path.home()/".ghtop_token" 156 | if path.is_file(): 157 | try: return path.read_text().strip() 158 | except: _exit("Error reading token") 159 | else: token = github_auth_device() 160 | path.write_text(token) 161 | return token 162 | 163 | 164 | def _signal_handler(sig, frame): 165 | if sig != signal.SIGINT: return 166 | print(term.exit_fullscreen(),term.clear(),term.normal) 167 | sys.exit(0) 168 | 169 | _funcs = dict(tail=tail_events, quad=quad_logs, users=watch_users, simple=simple) 170 | _filts = str_enum('_filts', 'users', 'repo', 'org') 171 | _OpModes = str_enum('_OpModes', *_funcs) 172 | 173 | @call_parse 174 | def main(mode: Param("Operation mode to run", _OpModes), 175 | include_bots: Param("Include bots (there's a lot of them!)", store_true)=False, 176 | types: Param("Comma-separated types of event to include (e.g PushEvent)", str)='', 177 | pause: Param("Number of seconds to pause between requests to the GitHub api", float)=0.4, 178 | filt: Param("Filtering method", _filts)=None, 179 | filtval: Param("Value to filter by (for `repo` use format `owner/repo`)", str)=None): 180 | signal.signal(signal.SIGINT, _signal_handler) 181 | types = types.split(',') if types else None 182 | if filt and not filtval: _exit("Must pass `filter_value` if passing `filter_type`") 183 | if filtval and not filt: _exit("Must pass `filter_type` if passing `filter_value`") 184 | kwargs = {filt:filtval} if filt else {} 185 | api = GhApi(limit_cb=limit_cb, token=_get_token()) 186 | evts = api.fetch_events(types=types, incl_bot=include_bots, pause=float(pause), **kwargs) 187 | _funcs[mode](evts, api) -------------------------------------------------------------------------------- /ghtop/richext.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | __all__ = ['console', 'EProg', 'ESpark', 'SpkMap', 'Stats', 'colors', 'colors2'] 4 | 5 | 6 | import time,random 7 | from collections import defaultdict 8 | from typing import List 9 | from collections import deque, OrderedDict, namedtuple 10 | from .all_rich import (Console, Color, FixedPanel, box, Segments, Live, 11 | grid, ConsoleOptions, Progress, BarColumn, Spinner) 12 | from ghapi.event import * 13 | from fastcore.all import * 14 | console = Console() 15 | 16 | 17 | 18 | class EProg: 19 | "Progress bar with a heading `hdg`." 20 | def __init__(self, hdg='Quota', width=10): 21 | self.prog = Progress(BarColumn(bar_width=width), "[progress.percentage]{task.percentage:>3.0f}%") 22 | self.task = self.prog.add_task("",total=100, visible=False) 23 | store_attr() 24 | def update(self, completed): self.prog.update(self.task, completed=completed) 25 | def __rich_console__(self, console: Console, options: ConsoleOptions): 26 | self.prog.update(self.task, visible=True) 27 | yield grid([["Quota"], [self.prog.get_renderable()]], width=self.width+2, expand=False) 28 | 29 | 30 | 31 | class ESpark(EventTimer): 32 | "An `EventTimer` that displays a sparkline with a heading `nm`." 33 | def __init__(self, nm:str, color:str, ghevts=None, store=5, span=.2, mn=0, mx=None, stacked=True, show_freq=False): 34 | super().__init__(store=store, span=span) 35 | self.ghevts=L(ghevts) 36 | store_attr('nm,color,store,span,mn,mx,stacked,show_freq') 37 | 38 | def _spark(self): 39 | data = L(list(self.hist)+[self.freq] if self.show_freq else self.hist) 40 | num = f'{self.freq:.1f}' if self.freq < 10 else f'{self.freq:.0f}' 41 | return f"[{self.color}]{num} {sparkline(data, mn=self.mn, mx=self.mx)}[/]" 42 | 43 | def upd_hist(self, store, span): super().__init__(store=store, span=span) 44 | 45 | def _nm(self): return f"[{self.color}] {self.nm}[/]" 46 | 47 | def __rich_console__(self, console: Console, options: ConsoleOptions): 48 | yield grid([[self._nm()], [self._spark()]]) if self.stacked else f'{self._nm()} {self._spark()}' 49 | 50 | def add_events(self, evts): 51 | evts = L([evts]) if isinstance(evts, dict) else L(evts) 52 | if self.ghevts: evts.map(lambda e: self.add(1) if type(e) in L(self.ghevts) else noop) 53 | else: self.add(len(evts)) 54 | 55 | __repr__ = basic_repr('nm,color,ghevts,store,span,stacked,show_freq,ylim') 56 | 57 | 58 | class SpkMap: 59 | "A Group of `ESpark` instances." 60 | def __init__(self, spks:List[ESpark]): store_attr() 61 | 62 | @property 63 | def evcounts(self): return dict([(s.nm, s.events) for s in self.spks]) 64 | 65 | def update_params(self, store:int=None, span:float=None, stacked:bool=None, show_freq:bool=None): 66 | for s in self.spks: 67 | s.upd_hist(store=ifnone(store,s.store), span=ifnone(span,s.span)) 68 | s.stacked = ifnone(stacked,s.stacked) 69 | s.show_freq = ifnone(show_freq,s.show_freq) 70 | 71 | def add_events(self, evts:GhEvent): 72 | "Update `SpkMap` sparkline historgrams with events." 73 | evts = L([evts]) if isinstance(evts, dict) else L(evts) 74 | for s in self.spks: s.add_events(evts) 75 | 76 | def __rich_console__(self, console: Console, options: ConsoleOptions): yield grid([self.spks]) 77 | __repr__ = basic_repr('spks') 78 | 79 | 80 | 81 | 82 | class Stats(SpkMap): 83 | "Renders a group of `ESpark` along with a spinner and progress bar that are dynamically sized." 84 | def __init__(self, spks:List[ESpark], store=None, span=None, stacked=None, show_freq=None, max_width=console.width-5, spin:str='earth', spn_lbl="/min"): 85 | super().__init__(spks) 86 | self.update_params(store=store, span=span, stacked=stacked, show_freq=show_freq) 87 | store_attr() 88 | self.spn = Spinner(spin) 89 | self.slen = len(spks) * max(15, store*2) 90 | self.plen = max(store, 10) # max(max_width-self.slen-15, 15) 91 | self.progbar = EProg(width=self.plen) 92 | 93 | def get_spk(self): return grid([self.spks], width=min(console.width-15, self.slen), expand=False) 94 | 95 | def get_spinner(self): return grid([[self.spn], [self.spn_lbl]]) 96 | 97 | def update_prog(self, pct_complete:int=None): self.progbar.update(pct_complete) if pct_complete else noop() 98 | 99 | def __rich_console__(self, console: Console, options: ConsoleOptions): 100 | yield grid([[self.get_spinner(), self.get_spk(), grid([[self.progbar]], width=self.plen+5) ]], width=self.max_width) 101 | 102 | 103 | 104 | @patch 105 | def __rich_console__(self:GhEvent, console, options): 106 | res = Segments(options) 107 | kw = {'color': colors[self.type]} 108 | res.add(f'{self.emoji} ') 109 | res.add(self.actor.login, pct=0.25, bold=True, **kw) 110 | res.add(self.description, pct=0.5, **kw) 111 | res.add(self.repo.name, pct=0.5 if self.text else 1, space = ': ' if self.text else '', italic=True, **kw) 112 | if self.text: 113 | clean_text = self.text.replace('\n', ' ').replace('\n', ' ') 114 | res.add (f'"{clean_text}"', pct=1, space='', **kw) 115 | res.add('\n') 116 | return res 117 | 118 | 119 | colors = dict( 120 | PushEvent=None, CreateEvent=Color.red, IssueCommentEvent=Color.green, WatchEvent=Color.yellow, 121 | PullRequestEvent=Color.blue, PullRequestReviewEvent=Color.magenta, PullRequestReviewCommentEvent=Color.cyan, 122 | DeleteEvent=Color.bright_red, ForkEvent=Color.bright_green, IssuesEvent=Color.bright_magenta, 123 | ReleaseEvent=Color.bright_blue, MemberEvent=Color.bright_yellow, CommitCommentEvent=Color.bright_cyan, 124 | GollumEvent=Color.white, PublicEvent=Color.turquoise4) 125 | 126 | colors2 = dict( 127 | PushEvent=None, CreateEvent=Color.dodger_blue1, IssueCommentEvent=Color.tan, WatchEvent=Color.steel_blue1, 128 | PullRequestEvent=Color.deep_pink1, PullRequestReviewEvent=Color.slate_blue1, PullRequestReviewCommentEvent=Color.tan, 129 | DeleteEvent=Color.light_pink1, ForkEvent=Color.orange1, IssuesEvent=Color.medium_violet_red, 130 | ReleaseEvent=Color.green1, MemberEvent=Color.orchid1, CommitCommentEvent=Color.tan, 131 | GollumEvent=Color.sea_green1, PublicEvent=Color.magenta2) -------------------------------------------------------------------------------- /settings.ini: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | lib_name = ghtop 3 | user = fastai 4 | author = natfriedman 5 | author_email = j@fast.ai 6 | copyright = Nat Friedman 7 | description = See what is happening on GitHub in real time 8 | keywords = python 9 | version = 0.0.6 10 | min_python = 3.6 11 | audience = Developers 12 | language = English 13 | license = mit 14 | status = 2 15 | requirements = emoji enlighten py-dashing fastcore ghapi>0.1.9 rich 16 | console_scripts = ghtop=ghtop.ghtop:main 17 | branch = master 18 | custom_sidebar = False 19 | host = github 20 | nbs_path = nbs 21 | doc_path = docs 22 | doc_host = https://ghtop.fast.ai 23 | doc_baseurl = / 24 | git_url = https://github.com/fastai/ghtop/tree/master/ 25 | lib_path = ghtop 26 | title = ghtop 27 | bare = True 28 | 29 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from pkg_resources import parse_version 2 | from configparser import ConfigParser 3 | import setuptools 4 | assert parse_version(setuptools.__version__)>=parse_version('36.2') 5 | 6 | # note: all settings are in settings.ini; edit there, not here 7 | config = ConfigParser(delimiters=['=']) 8 | config.read('settings.ini') 9 | cfg = config['DEFAULT'] 10 | 11 | cfg_keys = 'version description keywords author author_email'.split() 12 | expected = cfg_keys + "lib_name user branch license status min_python audience language".split() 13 | for o in expected: assert o in cfg, "missing expected setting: {}".format(o) 14 | setup_cfg = {o:cfg[o] for o in cfg_keys} 15 | 16 | licenses = { 17 | 'mit': ('MIT','OSI Approved :: MIT License'), 18 | } 19 | statuses = [ '1 - Planning', '2 - Pre-Alpha', '3 - Alpha', 20 | '4 - Beta', '5 - Production/Stable', '6 - Mature', '7 - Inactive' ] 21 | py_versions = '2.0 2.1 2.2 2.3 2.4 2.5 2.6 2.7 3.0 3.1 3.2 3.3 3.4 3.5 3.6 3.7 3.8'.split() 22 | 23 | requirements = cfg.get('requirements','').split() 24 | lic = licenses[cfg['license']] 25 | min_python = cfg['min_python'] 26 | 27 | setuptools.setup( 28 | name = cfg['lib_name'], 29 | license = lic[0], 30 | classifiers = [ 31 | 'Development Status :: ' + statuses[int(cfg['status'])], 32 | 'Intended Audience :: ' + cfg['audience'].title(), 33 | 'License :: ' + lic[1], 34 | 'Natural Language :: ' + cfg['language'].title(), 35 | ] + ['Programming Language :: Python :: '+o for o in py_versions[py_versions.index(min_python):]], 36 | url = cfg['git_url'], 37 | packages = setuptools.find_packages(), 38 | include_package_data = True, 39 | install_requires = requirements, 40 | dependency_links = cfg.get('dep_links','').split(), 41 | python_requires = '>=' + cfg['min_python'], 42 | long_description = open('README.md').read(), 43 | long_description_content_type = 'text/markdown', 44 | zip_safe = False, 45 | entry_points = { 'console_scripts': cfg.get('console_scripts','').split() }, 46 | **setup_cfg) 47 | 48 | --------------------------------------------------------------------------------