Hallo zusammen,
für ein Projekt soll regelmäßig ausgewertet werden, welche Issues in einem Github Repo bestimmte Labels tragen, wann die gesetzt und entfernt wurden, usw.
Mein Ansatz wäre, dies über Github Actions zu realisieren, weil die automatisch angestoßen werden können, sobald irgendein Issue irgendein Label erhält.
Im Folgenden würde ich dann ein Python Skript laufen lassen, das die ganze detaillierte Logik enthält und die relevanten Daten zusammenfasst und aufbereitet.
Zur Anzeige und Speicherung der Daten bin ich noch unentschlossen. Vielleicht schreibe ich einfach alles in ein Google Docs? Das wäre ggf. für manuelle Bearbeitung praktisch.
Alternativ könnte ich mir die Schnittstelle sparen, sondern die Daten strukturiert ins Repo speichern, und außerdem direkt bei Github Pages als Webseite darstellen und hosten lassen.
Jetzt kenne ich mich nicht so gut aus, welche Python Bibliotheken sich da so anbieten, um tabellarische Daten vernünftig anzuzeigen. Pages kann wohl nur normales HTML / CSS / JS.
Hat jemand schon mal ähnliche Projekte gehabt und wie würdet ihr vorgehen?
Github Issues auswerten
MorgenGrauen: 1 Welt, 8 Rassen, 13 Gilden, >250 Abenteuer, >5000 Waffen & Rüstungen,
>7000 NPC, >16000 Räume, >200 freiwillige Programmierer, nur Text, viel Spaß, seit 1992.
>7000 NPC, >16000 Räume, >200 freiwillige Programmierer, nur Text, viel Spaß, seit 1992.
Ich versuche jetzt, die relevanten Informationen einfach per Github API zu sammeln und benutze dafür das Paket PyGithub. Gefällt mir bisher sehr gut!
MorgenGrauen: 1 Welt, 8 Rassen, 13 Gilden, >250 Abenteuer, >5000 Waffen & Rüstungen,
>7000 NPC, >16000 Räume, >200 freiwillige Programmierer, nur Text, viel Spaß, seit 1992.
>7000 NPC, >16000 Räume, >200 freiwillige Programmierer, nur Text, viel Spaß, seit 1992.
Das mit den Labels ist nicht das eigentliche Ziel, sondern nur ein hoffentlich ausreichender Weg dorthin.
Eigentlich soll ausgewertet werden, wieviel Geld wann für Bounties auf einzelne Issues ausgegeben wurde.
Dabei gibt es einige interessante Labels, bspw. "bounty-20", usw. wenn 20 Dollar an Bounty ausgelobt wurden, und "bounty-paid", wenn das jemand erfolgreich erfüllt hatte.
Der Prozess ist also meist: Issues identifizieren, die zum Bounty werden sollen, Label "bounty-100" osä. dran pappen, warten, Ergebnis prüfen, dann ggf. "bounty-paid" dran.
Manchmal wartet man aber so lange, und niemand erfüllt die Aufgabe. Dann kann es sein, dass der Bounty nachträglich noch erhöht wird, bspw. von 20 auf 100 Dollar.
Für die Labels bedeutet das, dass "bounty-20" entfernt und "bounty-100" ergänzt wurde. All das kann ich mit der Github API gut auslesen, wann welches Label dran kam.
Ein bisschen habe ich gerätselt, wie ich darstellen soll, dass ein Bounty über seine Lebenszeit mehrere Höhen inne hatte. Für mich sind das nun einfach 2 verschiedene Bounties.
Um die ganzen Daten eines Bounty zu verwalten, habe ich eine Klasse erstellt. Damit die auch ins Set passt, mussten einige __ Funktionen her. Das hab ich noch nie gemacht.
Die Schnittstelle zu Google Sheets habe ich nicht implementiert, bzw. wird vom Code einfach eine quasi Tabelle als Text ausgegeben und dann per Copy/Paste weitertransportiert.
Ein Stück weit ist unklar, ob ich den Code jetzt weiter erweitern kann und soll, um nicht nur historische sondern auch neue Daten zukünftig zu sammeln und zu ergänzen.
Hintergrund ist, dass die Bounties nun von Algora Bot verwaltet werden, und dieser fügt nur immer dasselbe Label "Bounty" hinzu ohne den Wert. Der Wert steht dafür in irgendeinem Kommentar. Dafür hat Algora eine eigene API, die man anzapfen könnte...
Hier ist der Code soweit:
Danke für die Aufmerksamkeit!
Eigentlich soll ausgewertet werden, wieviel Geld wann für Bounties auf einzelne Issues ausgegeben wurde.
Dabei gibt es einige interessante Labels, bspw. "bounty-20", usw. wenn 20 Dollar an Bounty ausgelobt wurden, und "bounty-paid", wenn das jemand erfolgreich erfüllt hatte.
Der Prozess ist also meist: Issues identifizieren, die zum Bounty werden sollen, Label "bounty-100" osä. dran pappen, warten, Ergebnis prüfen, dann ggf. "bounty-paid" dran.
Manchmal wartet man aber so lange, und niemand erfüllt die Aufgabe. Dann kann es sein, dass der Bounty nachträglich noch erhöht wird, bspw. von 20 auf 100 Dollar.
Für die Labels bedeutet das, dass "bounty-20" entfernt und "bounty-100" ergänzt wurde. All das kann ich mit der Github API gut auslesen, wann welches Label dran kam.
Ein bisschen habe ich gerätselt, wie ich darstellen soll, dass ein Bounty über seine Lebenszeit mehrere Höhen inne hatte. Für mich sind das nun einfach 2 verschiedene Bounties.
Um die ganzen Daten eines Bounty zu verwalten, habe ich eine Klasse erstellt. Damit die auch ins Set passt, mussten einige __ Funktionen her. Das hab ich noch nie gemacht.
Die Schnittstelle zu Google Sheets habe ich nicht implementiert, bzw. wird vom Code einfach eine quasi Tabelle als Text ausgegeben und dann per Copy/Paste weitertransportiert.
Ein Stück weit ist unklar, ob ich den Code jetzt weiter erweitern kann und soll, um nicht nur historische sondern auch neue Daten zukünftig zu sammeln und zu ergänzen.
Hintergrund ist, dass die Bounties nun von Algora Bot verwaltet werden, und dieser fügt nur immer dasselbe Label "Bounty" hinzu ohne den Wert. Der Wert steht dafür in irgendeinem Kommentar. Dafür hat Algora eine eigene API, die man anzapfen könnte...
Hier ist der Code soweit:
Code: Alles auswählen
from github import Github
from github import Auth
from os import environ
COOL_REPO = "Mudlet/Mudlet"
COOL_EVENTS = ["labeled"]
COOL_LABELS = ["bounty-paid", "bounty-20", "bounty-30", "bounty-50", "bounty-80", "bounty-100", "bounty-120", "bounty-200" ]
try:
GITHUB_ACCESS_TOKEN = environ["GITHUB_ACCESS_TOKEN"]
except KeyError:
raise(ValueError("Token not available!"))
class Bounty():
def __init__(self, number, reward = 0, title = ""):
self.number = number
self.reward = reward
if title == "":
self.title = "Title unknown"
else:
self.title = title
self.status = "plan"
self.url = f"https://github.com/Mudlet/Mudlet/issues/{self.number}"
self.start_date = ""
self.end_date = ""
def publish(self, start_date, reward):
self.start_date = start_date
self.reward = reward
self.status = "open"
def close(self, end_date, status = "abort"):
self.end_date = end_date
self.status = status
def pay(self, end_date):
self.end_date = end_date
self.status = "paid"
def __str__(self):
link_string = f'=HYPERLINK("{self.url}";"{self.title}")'
return "\t".join((str(self.number),
link_string,
self.status,
str(self.reward),
str(self.start_date),
str(self.end_date)))
def __repr__(self):
return self.__str__()
def __eq__(self, other):
result = (self.number == other.number) and \
(self.start_date == other.start_date)
return result
def __ne__(self, other):
result = (self.number != other.number) or \
(self.start_date != other.start_date)
return result
def __lt__(self, other):
result = (self.number < other.number) and \
(self.start_date < other.start_date)
return result
def __le__(self, other):
result = (self.number <= other.number) and \
(self.start_date <= other.start_date)
return result
def __gt__(self, other):
result = (self.number > other.number) and \
(self.start_date > other.start_date)
return result
def __ge__(self, other):
result = (self.number >= other.number) and \
(self.start_date >= other.start_date)
return result
def __hash__(self):
return hash((self.number, self.start_date))
def is_no_pull_request(issue):
return issue.pull_request is None
def gather_cool_issues(repo):
cool_issues = set()
for label in COOL_LABELS:
cool_issues.update(repo.get_issues(
labels = [label], state = "all"))
cool_issues = set(c for c in cool_issues if is_no_pull_request(c))
return cool_issues
def parse_issue(issue):
bounty = Bounty(issue.number, title = issue.title)
bounties = {bounty}
for event in [e for e in issue.get_timeline() if e.event in COOL_EVENTS]:
label_name = event.raw_data['label']['name']
if label_name not in COOL_LABELS:
continue
if label_name == "bounty-paid":
bounty.pay(end_date = event.created_at)
continue
# Now the label must either be "bounty-20", or "bounty-30", etc.
reward = label_name.split("-")[1]
if bounty.status == "open":
# This bounty was already published before, now changed to a different reward.
# Then we will close the original bounty, and create a new bounty with the new reward.
bounty.close(end_date = event.created_at, status = "rise")
bounty = Bounty(issue.number, title = issue.title)
bounties.add(bounty)
bounty.publish(start_date = event.created_at, reward = reward)
return bounties
def main():
auth = Auth.Token(GITHUB_ACCESS_TOKEN)
github = Github(auth=auth)
repo = github.get_repo(COOL_REPO)
cool_issues = gather_cool_issues(repo)
bounties = set()
for issue in cool_issues:
bounties.update(parse_issue(issue))
for line in sorted(bounties):
print(line)
if __name__ == "__main__":
main()
MorgenGrauen: 1 Welt, 8 Rassen, 13 Gilden, >250 Abenteuer, >5000 Waffen & Rüstungen,
>7000 NPC, >16000 Räume, >200 freiwillige Programmierer, nur Text, viel Spaß, seit 1992.
>7000 NPC, >16000 Räume, >200 freiwillige Programmierer, nur Text, viel Spaß, seit 1992.
- __blackjack__
- User
- Beiträge: 13122
- Registriert: Samstag 2. Juni 2018, 10:21
- Wohnort: 127.0.0.1
- Kontaktdaten:
Wenn `str()` und `repr()` gleich sein sollen braucht man nur `__repr__()` implementieren, denn `object.__str__()` benutzt `__repr__()`. Allerdings entspricht das da nicht den Konventionen von `__repr__()`: Entweder etwas das man als Python-Ausdruck auswerten könnte um den gleichen Wert zu bekommen, oder etwas in ”spitze Klammern” eingefasst, was sich zur Fehlersuche eignet.
Von den ganzen Vergleichsfunktionen muss man nicht alle selbst implementieren wenn man die Klasse mit `functools.total_ordering` dekoriert.
Von den ganzen Vergleichsfunktionen muss man nicht alle selbst implementieren wenn man die Klasse mit `functools.total_ordering` dekoriert.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Guter Hinweis. Den Unterschied zwischen __str__ und __repr__ hatte ich noch nicht so verinnerlicht.
Dieses total_ordering ist neumodischer Python 3 Schnickschnack. Sogar das offizielle Python Tutorial empfiehlt es bei Gelegenheit wegzulassen, weil es Dinge verlangsamen und kompliziertere Stacktraces ergeben könnte.
Naja, immerhin spart es etwas Tipparbeit und größtenteils identischen Code zu wiederholen. Habe ich mal testweise eingebaut und einige Vergleichsfunktionen gestrichen. Dann landen wir hier (getestet):
Was mich selbst noch bisschen störte, ist diese Übergabe von mehreren Bounties als Set. Ich würde gerne immer genau ein Bounty pro Issue zurückbekommen. Dann könnte ich auch die "parse_issue" Funktion weiter auftrennen und vielleicht die Schleife in eine andere Funktion auslagern. Aber ich habe es nicht geschafft. Ich würde vermutlich der Bounty Klasse gerne beibringen, was sie zu tun hat, wenn ihr Reward erhöht wird, aber es geht nicht, weil ja dann ein Bounty geschlossen und ein anderes neu eröffnet wird. Das geht also über das einzelne Exemplar hinaus. Alternativ müsste ich plötzlich innerhalb einer Bounty Klasse mehrere Termine merken, ab wann welcher Reward gesetzt wird, damit zumindest die __str__ Ausgabe dann wieder mehrzeilig erfolgen kann. Da fehlte mir noch ein schönes Designmuster.
Dieses total_ordering ist neumodischer Python 3 Schnickschnack. Sogar das offizielle Python Tutorial empfiehlt es bei Gelegenheit wegzulassen, weil es Dinge verlangsamen und kompliziertere Stacktraces ergeben könnte.
Naja, immerhin spart es etwas Tipparbeit und größtenteils identischen Code zu wiederholen. Habe ich mal testweise eingebaut und einige Vergleichsfunktionen gestrichen. Dann landen wir hier (getestet):
Code: Alles auswählen
from functools import total_ordering
from github import Github
from github import Auth
from os import environ
COOL_REPO = "Mudlet/Mudlet"
COOL_EVENTS = ["labeled"]
COOL_LABELS = ["bounty-paid", "bounty-20", "bounty-30", "bounty-50", "bounty-80", "bounty-100", "bounty-120", "bounty-200" ]
try:
GITHUB_ACCESS_TOKEN = environ["GITHUB_ACCESS_TOKEN"]
except KeyError:
raise(ValueError("Token not available!"))
@total_ordering
class Bounty():
def __init__(self, number, reward = 0, title = ""):
self.number = number
self.reward = reward
if title == "":
self.title = "Title unknown"
else:
self.title = title
self.status = "plan"
self.url = f"https://github.com/Mudlet/Mudlet/issues/{self.number}"
self.start_date = ""
self.end_date = ""
def publish(self, start_date, reward):
self.start_date = start_date
self.reward = reward
self.status = "open"
def close(self, end_date, status = "abort"):
self.end_date = end_date
self.status = status
def pay(self, end_date):
self.end_date = end_date
self.status = "paid"
def __str__(self):
link_string = f'=HYPERLINK("{self.url}";"{self.title}")'
return "\t".join((
str(self.number),
link_string,
self.status,
str(self.reward),
str(self.start_date),
str(self.end_date)))
def __repr__(self):
return "Bounty(%r, %r, %r)" % (self.number, self.reward, self.title)
def __eq__(self, other):
result = (self.number == other.number) and \
(self.start_date == other.start_date)
return result
def __lt__(self, other):
result = (self.number < other.number) and \
(self.start_date < other.start_date)
return result
def __hash__(self):
return hash((self.number, self.start_date))
def is_no_pull_request(issue):
return issue.pull_request is None
def gather_cool_issues(repo):
cool_issues = set()
for label in COOL_LABELS:
cool_issues.update(repo.get_issues(
labels = [label], state = "all"))
cool_issues = set(c for c in cool_issues if is_no_pull_request(c))
return cool_issues
def parse_issue(issue):
bounty = Bounty(issue.number, title = issue.title)
bounties = {bounty}
for event in [e for e in issue.get_timeline() if e.event in COOL_EVENTS]:
label_name = event.raw_data['label']['name']
if label_name not in COOL_LABELS:
continue
if label_name == "bounty-paid":
bounty.pay(end_date = event.created_at)
continue
# Now the label must either be "bounty-20", or "bounty-30", etc.
reward = label_name.split("-")[1]
if bounty.status == "open":
# This bounty was already published before, now changed to a different reward.
# Then we will close the original bounty, and create a new bounty with the new reward.
bounty.close(end_date = event.created_at, status = "rise")
bounty = Bounty(issue.number, title = issue.title)
bounties.add(bounty)
bounty.publish(start_date = event.created_at, reward = reward)
return bounties
def main():
auth = Auth.Token(GITHUB_ACCESS_TOKEN)
github = Github(auth=auth)
repo = github.get_repo(COOL_REPO)
cool_issues = gather_cool_issues(repo)
bounties = set()
for issue in cool_issues:
bounties.update(parse_issue(issue))
for line in sorted(bounties):
print(line)
if __name__ == "__main__":
main()
MorgenGrauen: 1 Welt, 8 Rassen, 13 Gilden, >250 Abenteuer, >5000 Waffen & Rüstungen,
>7000 NPC, >16000 Räume, >200 freiwillige Programmierer, nur Text, viel Spaß, seit 1992.
>7000 NPC, >16000 Räume, >200 freiwillige Programmierer, nur Text, viel Spaß, seit 1992.
Wortwörtlich meine Gedanken wenn ich mal wieder irgendwelche Neuigkeiten in Python 3 entdecke. 'total_ordering' ist natürlich durch die jeweils aktuelle Entdeckung zu ersetzen und nicht alles neue in Python ist nur Schnickschnack. Es gibt z.B. auch Dinge bei denen ich mir denke: Warum erst jetzt, warum hatten wir das nicht schon viel früher in Python?
Wenn es über einzelne Exemplare hinaus geht, dann denke ich gleich an einen Manager... eine Klasse die intern das eine Set mit den Bounty-Instanzen verwaltet. Ich glaube "Composite" müsste das Stichwort sein...Kebap hat geschrieben: ↑Dienstag 16. Januar 2024, 22:00 Ich würde vermutlich der Bounty Klasse gerne beibringen, was sie zu tun hat, wenn ihr Reward erhöht wird, aber es geht nicht, weil ja dann ein Bounty geschlossen und ein anderes neu eröffnet wird. Das geht also über das einzelne Exemplar hinaus. Alternativ müsste ich plötzlich innerhalb einer Bounty Klasse mehrere Termine merken, ab wann welcher Reward gesetzt wird, damit zumindest die __str__ Ausgabe dann wieder mehrzeilig erfolgen kann. Da fehlte mir noch ein schönes Designmuster.
Ja, das war tatsächlich eher als Scherz formuliert.
MorgenGrauen: 1 Welt, 8 Rassen, 13 Gilden, >250 Abenteuer, >5000 Waffen & Rüstungen,
>7000 NPC, >16000 Räume, >200 freiwillige Programmierer, nur Text, viel Spaß, seit 1992.
>7000 NPC, >16000 Räume, >200 freiwillige Programmierer, nur Text, viel Spaß, seit 1992.
OK, einen Monat später möchte ich die Auswertung erneut durchführen und mit den letzten Ergebnissen abgleichen.
Die hätte ich dazu vermutlich in einem lesbaren Format speichern sollen und nicht bloß per Copy/Paste manuell weiterverarbeiten.
Was empfiehlt sich da so?
Die hätte ich dazu vermutlich in einem lesbaren Format speichern sollen und nicht bloß per Copy/Paste manuell weiterverarbeiten.
Was empfiehlt sich da so?
MorgenGrauen: 1 Welt, 8 Rassen, 13 Gilden, >250 Abenteuer, >5000 Waffen & Rüstungen,
>7000 NPC, >16000 Räume, >200 freiwillige Programmierer, nur Text, viel Spaß, seit 1992.
>7000 NPC, >16000 Räume, >200 freiwillige Programmierer, nur Text, viel Spaß, seit 1992.
Ich frage mal anders, weil ich dem Problem so ähnlich schon in manchen Projekten begegnet bin.
Irgendwie fehlt mir da noch ein richtiger Ansatz oder eine übliche Vorgehensweise oder ich weiß nicht.
Vermutlich könnten Datenbanken dabei helfen? Aber das ist eigentlich nicht der Kern meiner Frage.
Ich will heute den Stand von X Daten herunterladen, und morgen auch nochmal diese Daten herunterladen.
Aber vielleicht sind es inzwischen mehr Daten geworden, oder die alten haben sich verändert, usw.
Womöglich haben sich auch die lokalen Daten zwischenzeitlich verändert, aber die entfernten nicht.
Dann muss man abgleichen, was neu ist, und beibehalten werden soll. Vermutlich gibt es noch mehr zu beachten.
Das beste Vorgehen an der Stelle ist mir aber noch nicht klar. Hoffentlich erfinde ich nicht das Rad nochmal neu.
Irgendwie fehlt mir da noch ein richtiger Ansatz oder eine übliche Vorgehensweise oder ich weiß nicht.
Vermutlich könnten Datenbanken dabei helfen? Aber das ist eigentlich nicht der Kern meiner Frage.
Ich will heute den Stand von X Daten herunterladen, und morgen auch nochmal diese Daten herunterladen.
Aber vielleicht sind es inzwischen mehr Daten geworden, oder die alten haben sich verändert, usw.
Womöglich haben sich auch die lokalen Daten zwischenzeitlich verändert, aber die entfernten nicht.
Dann muss man abgleichen, was neu ist, und beibehalten werden soll. Vermutlich gibt es noch mehr zu beachten.
Das beste Vorgehen an der Stelle ist mir aber noch nicht klar. Hoffentlich erfinde ich nicht das Rad nochmal neu.
MorgenGrauen: 1 Welt, 8 Rassen, 13 Gilden, >250 Abenteuer, >5000 Waffen & Rüstungen,
>7000 NPC, >16000 Räume, >200 freiwillige Programmierer, nur Text, viel Spaß, seit 1992.
>7000 NPC, >16000 Räume, >200 freiwillige Programmierer, nur Text, viel Spaß, seit 1992.