├── .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 |
--------------------------------------------------------------------------------