Extensions to rich for ghtop.
evts = load_sample_events()
exs = [first(evts, risinstance(o)) for o in described_evts]

Animated Stats

This section outlines how we can display statistics and visualizations such as sparklines and status bars that are animated as events are received.

class EProg[source]

EProg(hdg='Quota', width=10)

Progress bar with a heading hdg.

When you instantiate Eprog the starting progress is set to 0%:

p = EProg()
console.print(p)
   Quota    
━━━━━━━   0%

You can update the progress bar with the update method:

p.update(10)
console.print(p)
   Quota    
━━━━━━  10%

Espark - A sparkline combined with an EventTimer

fastcore's EventTimer calculates frequency metrics aggregated by slices of time specified by the argument span. The EventTimer can produce a sparkline that shows the last n time slices, where n is specified by the parameter store:

class ESpark[source]

ESpark(nm:str, color:str, ghevts=None, store=5, span=0.2, mn=0, mx=None, stacked=True, show_freq=False) :: EventTimer

An EventTimer that displays a sparkline with a heading nm.

from time import sleep
def _randwait(): yield from (sleep(random.random()/200) for _ in range(100))

c = EventTimer(store=5, span=0.03)
for o in _randwait(): c.add(1)

By default nm will be stacked on top of the sparkline. We simulate adding events to ESpark and render the result:

e = ESpark(nm='💌Issue', color='blue', store=5)

def _r(): return random.randint(1,30)

def _sim(e, steps=8, sleep=.2):
    for i in range(steps):
        e.add(_r())
        time.sleep(sleep)

_sim(e)
console.print(e)
                                           💌Issue                                           
                                          27 ▃▃▁▅▇                                           

If you would prefer nm and the sparkline to be on one line instead, you can set stacked to false:

e = ESpark(color='blue', nm='💌Issue', stacked=False)
_sim(e)
console.print(e)
 💌Issue  59 ▇▇▂▅▂

You can optionally specify a list of GhEvent types that will allow you to update sparklines by streaming in events. described_evts has a complete list of options:

described_evts
(ghapi.event.PushEvent,
 ghapi.event.CreateEvent,
 ghapi.event.IssueCommentEvent,
 ghapi.event.WatchEvent,
 ghapi.event.PullRequestEvent,
 ghapi.event.PullRequestReviewEvent,
 ghapi.event.PullRequestReviewCommentEvent,
 ghapi.event.DeleteEvent,
 ghapi.event.ForkEvent,
 ghapi.event.IssuesEvent,
 ghapi.event.ReleaseEvent,
 ghapi.event.MemberEvent,
 ghapi.event.CommitCommentEvent,
 ghapi.event.GollumEvent,
 ghapi.event.PublicEvent)

If ghevts is specified, only events that match the list of the GhEvent types will increment the event counter.

In the below example, the IssueCommentEvent and IssuesEvent are listed, therefore any other event types will not update the event counter:

_pr_evts = evts.filter(risinstance((PullRequestEvent, PullRequestReviewCommentEvent, PullRequestReviewEvent)))
_watch_evts = evts.filter(risinstance((WatchEvent)))


_s = ESpark('Issues', 'blue', [IssueCommentEvent, IssuesEvent], span=5)
_s.add_events(_pr_evts)
_s.add_events(_watch_evts)
test_eq(_s.events, 0)

However, events that match those types will update the event counter accordingly:

_issue_evts = evts.filter(risinstance((IssueCommentEvent, IssuesEvent)))
_s.add_events(_issue_evts)
test_eq(_s.events, len(_issue_evts))

If ghevts is not specified, all events are counted:

_s = ESpark('Issues', 'blue', span=5)
_s.add_events(evts)
test_eq(_s.events, len(evts))

You can also just add one event at a time instead of a list of events:

_s = ESpark('Issues', 'blue', span=5)
_s.add_events(evts[0])
test_eq(_s.events, 1)

Update A Group of Sparklines with SpkMap

class SpkMap[source]

SpkMap(spks:List[ESpark])

A Group of ESpark instances.

You can define a SpkMap instance with a list of ESpark:

s1 = ESpark('Issues', 'green', [IssueCommentEvent, IssuesEvent], span=60)
s2 = ESpark('PR', 'red', [PullRequestEvent, PullRequestReviewCommentEvent, PullRequestReviewEvent], span=60)
s3 = ESpark('Follow', 'blue', [WatchEvent, StarEvent, IssueCommentEvent, IssuesEvent], span=60)
s4 = ESpark('Other', 'red', span=60)

sm = SpkMap([s1,s2,s3,s4])

We haven't added any events to SpkMap so the event count will be zero for all sparklines:

sm.evcounts
{'Issues': 0, 'PR': 0, 'Follow': 0, 'Other': 0}

In the above example, Issue events update both the Issues and Follow sparklines, as well as the Other sparkline which doesn't have any GhEvent type filters so it counts all events:

sm.add_events(_issue_evts)
test_eq(sm.evcounts['Issues'], len(_issue_evts))
test_eq(sm.evcounts['Follow'], len(_issue_evts))
test_eq(sm.evcounts['Other'], len(_issue_evts))

sm.evcounts
{'Issues': 80, 'PR': 0, 'Follow': 80, 'Other': 80}

You can also just add one event at a time:

sm.add_events(_pr_evts[0])
test_eq(sm.evcounts['PR'], 1)
test_eq(sm.evcounts['Other'], len(_issue_evts)+1)

It may be desirable to make certain attributes of the sparklines the same so the group can look consistent. For example, by default sparklines are set to stacked=True, which means the labels are on top:

console.print(sm)
         Issues                   PR                   Follow                  Other         
         4339                    52                    4016                    3923          

We can update stack=False for the entire group with the update_params method:

sm.update_params(stacked=False)
console.print(sm)
      Issues  0.0              PR  0.0              Follow  0.0             Other  0.0       
sm.update_params(stacked=True, span=.1, store=8)
def _sim(s):
    with Live(s) as live:
        for i in range(200):
            s.add_events(evts[:random.randint(0,500)])
            time.sleep(random.randint(0,10)/100)
_sim(sm)
console.print(sm.spks[0])
                                            Issues                                           
                                        128 ▂▁▅▅▁▇▁▁                                         

Stats - Sparklines, Progress bars and Counts Combined

We may want to combine sparklines (with ESpark), spinners, and progress bars (with EProg) to display organized information concerning an event stream. Stats helps you create, group, display and update these elements together.

class Stats[source]

Stats(spks:List[ESpark], store=None, span=None, stacked=None, show_freq=None, max_width=88, spin:str='earth', spn_lbl='/min') :: SpkMap

Renders a group of ESpark along with a spinner and progress bar that are dynamically sized.

Instantiate Stats with a list of Espark instances. The parameters: store, span, and stacked allow you to set or override properties of underlying sparklines for consistency.

s1 = ESpark('Issues', 'green', [IssueCommentEvent, IssuesEvent])
s2 = ESpark('PR', 'red', [PullRequestEvent, PullRequestReviewCommentEvent, PullRequestReviewEvent])
s3 = ESpark('Follow', 'blue', [WatchEvent, StarEvent])
s4 = ESpark('Other', 'red')

s = Stats([s1,s2,s3,s4], store=5, span=.1, stacked=True)
console.print(s)
 🌍       Issues           PR           Follow         Other               Quota        
/min       0.0            0.0            0.0            0.0             ━━━━━━━   0%    

You can add events to update counters and sparklines just like SpkMap:

s.add_events(evts)
console.print(s)
 🌍       Issues           PR           Follow         Other               Quota        
/min      2614           3254           1061           30513            ━━━━━━━   0%    

You can update the progress bar with the update_prog method:

s.update_prog(50)
console.print(s)
 🌍       Issues           PR           Follow         Other               Quota        
/min      1847           2316            760           21981            ━━━╸━━━  50%    

Here is what this looks like when animated using Live:

def _sim_spark(s):
    with Live(s) as live:
        for i in range(101):
            s.update_prog(i)
            s.add_events(evts[:random.randint(0,500)])
            time.sleep(random.randint(0,10)/100)

s.update_params(span=1, show_freq=True)
_sim_spark(s)

Event Panel

Display GitHub events in a FixedPanel, which is a frame of fixed height that displays streaming data.

GhEvent.__rich_console__[source]

GhEvent.__rich_console__(console, options)

p = FixedPanel(15, box=box.HORIZONTALS, title='ghtop')
for e in evts[:163]: p.append(e)
p
 ────────────────────────────────────────── ghtop ────────────────────────────────────────── 
  ⭐  diddledan pushed 1 commits to "master" in diddlesnaps/openttdveigarbot pushed 1 commits to "main" in veigarbot/veigarbot.github.ioFromDarkHell pushed 1 commits to "main" in FromDarkHell/Simulstream                    
  🏭  pg45 created branch "add-headers" in pg45/markdown-portfolio                           
  💬  ljwagerfield created comment on issue #743 in lukeautry/tsoa: "@rudfoss did you find…KwameTaylor pushed 1 commits to "main" in SpotiScryers/SpotiScry                       
  👀  okeeffdp started watching alshedivat/al-folio                                          
  📬  JlchavezG opened PR #43 on JlchavezG/Psbg: "Se agregan notificaciones"                 
  👀  Lit3r4lly started watching 0xgalz/Virtuailor                                           
  🏭  Nabil1907 created repository in Nabil1907/Node-js-ProjectsNotWhoYoureThinkingOf pushed 1 commits to "main" in NotWhoYoureThinkingOf/fb-clone     
  🏭  thoriqkemal created repository in thoriqkemal/tig: "tig tig"WrathfulSpatula pushed 1 commits to "master" in WrathfulSpatula/OpenRelativityalinapopaqb pushed 1 commits to "master" in alinkamalvinka/cdond-c3-projectstarterLombiqBot pushed 0 commits to "MatteoPiovanelli…Lombiq/Orchard                         
 ─────────────────────────────────────────────────────────────────────────────────────────── 

Using grid with FixedPanel

We can use grid to arrange multiple FixedPanel instances in rows and columns. Below is an example of how two FixedPanel instances can be arranged in a row:

p = FixedPanel(15, box=box.HORIZONTALS, title='ghtop')
for e in exs: p.append(e)
grid([[p,p]])
 ─────────────────── ghtop ───────────────────  ────────────────── ghtop ─────────────────── 
  ⭐  BeckhamL pushed 1 commi…BeckhamL/leetc…BeckhamL pushed 1 commi…BeckhamL/leet…  
  🏭  admmonito…created branch…admmon…"This …    🏭  admmonito…created branc…admmon…"This …  
  💬  Holzhaus created commen…mixxxdj…"I reo…    💬  Holzhaus created commen…mixxxd…"I reo…  
  👀  mikalacki…started watchi…microg/GmsCore    👀  mikalacki…started watch…microg/GmsCore  
  📪  Didier-D-…closed PR #3 o…Didier…"Bump …    📪  Didier-D-…closed PR #3 …Didier…"Bump …  
  💌  kadirselc…created PR rev…turkdevops/no…    💌  kadirselc…created PR re…turkdevops/no…  
  🗨  mobinmob created review …void-li…"Οκ 👍"    🗨  mobinmob created review…void-li…"Οκ 👍"heehee3 deleted branch …heehee3/Proyect…heehee3 deleted branch …heehee3/Proyec…  
  🍽  Tubbz-alt forked fortran-lan…"Fortran w…    🍽  Tubbz-alt forked fortran-la…"Fortran w…  
  🐛  oulasvirt…opened issue #…oulasv…"Title"    🐛  oulasvirt…opened issue …oulasv…"Title"  
  🚀  github-ac…published rele…Anuken/Mindus…    🚀  github-ac…published rel…Anuken/Mindus…  
  💃  laurofilh…added member b…laurofilho96/…    💃  laurofilh…added member …laurofilho96/…  
  🎉  vercel[bo…created commit…LCinde…"Succe…    🎉  vercel[bo…created commi…LCinde…"Succe…  
  📚  adrianggc edited wiki pa…adrianggc/Die…    📚  adrianggc edited wiki p…adrianggc/Die…  
 ─────────────────────────────────────────────  ──────────────────────────────────────────── 

Here is another example of a four FixedPanel instances arranged in two rows and two columns:

types = IssueCommentEvent,IssuesEvent,PullRequestEvent,PullRequestReviewEvent
ps = {o:FixedPanel(15, box=box.HORIZONTALS, title=camel2words(remove_suffix(o.__name__,'Event'))) for o in types}
for k,v in ps.items(): v.extend(evts.filter(risinstance(k)))
isc,iss,prs,prrs = ps.values()
grid([[isc,iss],[prs,prrs]], width=110)
 ─────────────────── Issue Comment ───────────────────  ────────────────────── Issues ─────────────────────── 
  💬  sanskritbsc…created comment o…sanskrit…"fixed …    🐛  github-lear…opened issue #1 o…pg45/mar…"Gettin…  
  💬  github-lear…created comment o…pfxsys/g…"## Ste…    🐛  NillerMedDi…opened issue #3 o…gigabit1…"[Reque…  
  💬  moezzineb created comment on…flutter/…"Voila :…    🐛  lucasfarias…opened issue #2 o…lucasfar…"Issue …  
  💬  JustSlone created comment on…microsof…"So read…    🐛  jabolopes opened issue #44 o…lafriks/…"Possibl…  
  💬  codecov[bot…created comment o…open-mml…"# [Cod…    🐛  aisaioop opened issue #1598…aisaioop/…"北京通州  
  💬  mightybart created comment o…floriank…"Current…    🐛  jonathannag…opened issue #23 …navikt/m…"Access…  
  💬  Joulinar created comment on…MichaIng/…"Hi,
 
 …      🐛  aisaioop opened issue #1599…aisaioop/…"上海黄浦  
  💬  9mm created comment on is…rubycdp/fe…"@route h…    🐛  helmrich opened issue #27 o…vinceliui…"Spotify…  
  💬  dependabot[…created comment o…herzliya…"Looks …    🐛  github-acti…opened issue #204…Sakzsee/…"Error …  
  💬  codepope created comment on…superfly/…"That's …    🐛  aisaioop opened issue #1600…aisaioop/…"北京门头  
  💬  poelzi created comment on …mixxxdj/m…"@Be-ing …    🎁  glutamate closed issue #466 …saltcorn…"Forgott…  
  💬  ethindp created comment on …rust-osde…"I disag…    🐛  autocode-ap…opened issue #793…imamandr…"https:…  
  💬  michaelforn…created comment o…oasislin…"Thanks…    🎁  sanskritbsc…closed issue #3 o…sanskrit…"someth…  
  💬  stale[bot] created comment o…ironhack…"This pu…    🐛  aisaioop opened issue #1601…aisaioop/…"上海宝山  
  💬  awolf78 created comment on …ImpulseRC…"If you …    🐛  slingamn opened issue #1455…oragono/o…"split m…  
 ─────────────────────────────────────────────────────  ───────────────────────────────────────────────────── 
 ─────────────────── Pull Request ────────────────────  ──────────────── Pull Request Review ──────────────── 
  📪  alexerlands…closed PR #5 on alexerlan…"Bump in…                                                         
  📬  constancefe…opened PR #85 on lucienwa…"week 5 …                                                         
  📬  jyshangguan opened PR #1 on jyshanggu…"add ima…    💌  kadirselcuk created PR review…turkdevops/node    
  📪  Ryukishi closed PR #2 on adibhanna/…"Feature/p…    💌  mobinmob created PR review …void-linux/void-pa…  
  📪  ahocevar closed PR #11811 o…openlayer…"Better …    💌  swatso2020 created PR review…swatso2020/Projec…  
  📬  snyk-bot opened PR #84 on stelthdroi…"[Snyk] S…    💌  github-lear…created PR review…theaioat…"## Ste…  
  📪  mennovanemm…closed PR #27 on Softimis…"Ship Mo…    💌  ASchwad created PR review i…reaviz/reaflow       
  📪  Chramox closed PR #4 on Chramox/tyt…"Update"       💌  francinaPon…created PR review…UB-ES-2020-A/Gru…  
  📪  gabriel-hah…closed PR #128 on gabriel-…"Bump @…    💌  aaronPeruga created PR review…UB-ES-2020-A/Gru…  
  📬  distantnati…opened PR #1103 o…getkirby…"Remove…    💌  ahocevar created PR review …openlayer…"Thanks,…  
  📬  pull[bot] opened PR #534 on antosubas…"[pull] …    💌  nedbat created PR review i…nedbat/scriv          
  📪  yakirgot closed PR #1 on yakirgot/s…"chore(dep…    💌  nedbat created PR review i…nedbat/scriv          
  📪  mergify[bot…closed PR #4 on spbu-codi…"Исправл…    💌  mennovanemm…created PR review…Softimistic/Proj…  
  🔁  stringcode8…reopened PR #2051…MetaMask…"[1984]…    💌  i-stam created PR review i…Synthetixio/synthet…  
  📪  dependabot[…closed PR #3 on herzliya-…"Bump in…    💌  gmolinga created PR review …UB-ES-2020-A/Grup-…  
 ─────────────────────────────────────────────────────  ─────────────────────────────────────────────────────