Mercurial > code > home > repos > reposync
annotate repo_github_status.py @ 21:cb71722bb75c
use pipenv
author | drewp@bigasterisk.com |
---|---|
date | Tue, 29 Mar 2022 21:13:48 -0700 |
parents | b59912649fc4 |
children |
rev | line source |
---|---|
18
6f38aa08408d
starting over: make a web page that draws a streamed graph from collector, with plans for services to scrape the data that collector will subscribe to
drewp@bigasterisk.com
parents:
diff
changeset
|
1 """ |
6f38aa08408d
starting over: make a web page that draws a streamed graph from collector, with plans for services to scrape the data that collector will subscribe to
drewp@bigasterisk.com
parents:
diff
changeset
|
2 repos from config.yaml that are at github -> rdf data |
19 | 3 """ |
4 import datetime | |
5 from pathlib import Path | |
6 from typing import Set, Tuple | |
7 | |
8 import cyclone.web | |
9 import docopt | |
10 import treq | |
11 from background_loop import loop_forever_async | |
12 from dateutil.parser import parse | |
13 from dateutil.tz import tzlocal | |
14 from patchablegraph import (CycloneGraphEventsHandler, CycloneGraphHandler, PatchableGraph) | |
15 from prometheus_client import Counter, Gauge | |
16 from prometheus_client.exposition import generate_latest | |
17 from prometheus_client.registry import REGISTRY | |
18 from rdfdb.currentstategraphapi import CurrentStateGraphApi | |
19 from rdfdb.patch import Patch | |
20 from rdflib import RDF, Literal, Namespace, URIRef | |
21 from rdflib.term import Identifier | |
22 from ruamel.yaml import YAML | |
23 from standardservice.logsetup import log, verboseLogging | |
24 from twisted.internet import reactor | |
25 from twisted.internet.defer import inlineCallbacks | |
26 | |
27 Quad = Tuple[Identifier, Identifier, Identifier, Identifier] | |
28 Triple = Tuple[Identifier, Identifier, Identifier] | |
29 | |
30 githubOwner = 'drewp' | |
31 | |
32 EX = Namespace('http://example.com/') # todo | |
33 GITHUB_SYNC = Gauge('github_sync', 'syncs to github') | |
34 GITHUB_CALLS = Counter('github_calls', 'http calls to github') | |
35 | |
36 | |
37 # merge this into setToGraph | |
38 def replaceContext(pg: PatchableGraph, ctx: URIRef, newTriples: Set[Triple]): | |
39 prevCtxStmts = set((s, p, o, g.identifier) for s, p, o, g in pg._graph.quads((None, None, None, ctx))) | |
40 | |
41 currentStmts: Set[Quad] = set() | |
42 for tri in newTriples: | |
43 currentStmts.add(tri + (ctx,)) | |
44 | |
45 p = Patch(delQuads=prevCtxStmts.difference(currentStmts), addQuads=currentStmts.difference(prevCtxStmts)) | |
46 | |
47 pg.patch(p) | |
48 | |
49 | |
50 class Metrics(cyclone.web.RequestHandler): | |
51 | |
52 def get(self): | |
53 self.add_header('content-type', 'text/plain') | |
54 self.write(generate_latest(REGISTRY)) | |
55 | |
56 | |
57 class Index(cyclone.web.RequestHandler): | |
58 | |
59 def get(self, *args): | |
60 self.add_header('content-type', 'text/html') | |
61 self.write('''<!DOCTYPE html> | |
62 <html> | |
63 <head> | |
64 <title>repo_github_status</title> | |
65 </head> | |
66 <body> | |
67 <a href="graph/localRepos">graph</a> | |
68 </body> | |
69 </html>''') | |
70 | |
71 | |
72 @inlineCallbacks | |
73 def updateOne(graph: PatchableGraph, repo: URIRef, ghrepo: URIRef, shortRepoName: str): | |
74 log.info(f'getting update from github for {repo}') | |
75 | |
76 writeGhRepo(graph, repo, ghrepo) | |
77 | |
78 commitsUrl = f'https://api.github.com/repos/{githubOwner}/{shortRepoName}/commits?per_page=1' | |
79 GITHUB_CALLS.inc() | |
80 resp = yield treq.get(commitsUrl, | |
81 timeout=5, | |
82 headers={ | |
83 'User-agent': 'reposync by github.com/drewp', | |
84 'Accept': 'application/vnd.github.v3+json' | |
85 }) | |
86 ret = yield treq.json_content(resp) | |
87 | |
88 if len(ret) < 1: | |
89 raise ValueError(f"no commits on {commitsUrl}") | |
90 log.info(f'{repo=} {ret[0]=}') | |
91 | |
92 author = writeAuthor(graph, ret[0]['author']) | |
93 writeCommit(graph, ghrepo, ret[0], author) | |
94 | |
95 | |
96 def writeGhRepo(graph, repo, ghrepo): | |
97 replaceContext(graph, URIRef(ghrepo + '/config'), { | |
21 | 98 (repo, RDF.type, EX['Repo']), |
19 | 99 (repo, EX['githubRepo'], ghrepo), |
100 (ghrepo, RDF.type, EX['GithubRepo']), | |
101 }) | |
102 | |
103 | |
104 def writeCommit(graph, ghrepo, row, author): | |
105 new: Set[Triple] = set() | |
106 | |
107 commit = row['commit'] | |
108 latest = URIRef(commit['url']) | |
109 new.add((ghrepo, EX['latestCommit'], latest)) | |
110 | |
111 new.add((latest, RDF.type, EX['GithubCommit'])) | |
112 | |
113 t = parse(commit['author']['date']).astimezone(tzlocal()).isoformat() | |
114 new.add((latest, EX['created'], Literal(t))) | |
115 | |
116 new.add((latest, EX['creator'], author)) | |
117 new.add((author, EX['foafMail'], Literal(commit['committer']['email']))) | |
118 new.add((latest, EX['commitMessage'], Literal(commit['message']))) | |
119 new.add((latest, EX['sha'], Literal(row['sha']))) | |
120 for p in row['parents']: | |
121 new.add((latest, EX['parent'], Literal(p['url']))) | |
122 | |
123 replaceContext(graph, ghrepo, new) | |
124 | |
125 | |
126 def writeAuthor(graph: PatchableGraph, author: dict) -> URIRef: | |
127 uri = URIRef(author['url']) | |
128 replaceContext( | |
129 graph, uri, { | |
130 (uri, RDF.type, EX['GithubAuthor']), | |
131 (uri, EX['login'], Literal(author['login'])), | |
132 (uri, EX['avatar'], URIRef(author['avatar_url'])), | |
133 }) | |
134 return uri | |
135 | |
136 | |
137 @inlineCallbacks | |
138 def update(graph, repos): | |
139 for shortRepoName in repos: | |
140 uri = URIRef(f'http://bigasterisk.com/repo/{shortRepoName}') | |
141 ghrepo = URIRef(uri + '/github') | |
142 | |
143 now = datetime.datetime.now(tzlocal()) | |
144 if lastReadBefore(graph, ghrepo, now, datetime.timedelta(hours=24)): | |
145 yield updateOne(graph, uri, ghrepo, shortRepoName) | |
146 graph.patchObject(EX['githubUpdates'], ghrepo, EX['lastRead'], newObject=Literal(now)) | |
147 | |
148 | |
149 def lastReadBefore(graph, ghrepo, now, ago): | |
150 with graph.currentState() as g: | |
151 lastRead = g.value(ghrepo, EX['lastRead']) | |
152 return lastRead is None or lastRead.toPython() < now - ago | |
153 | |
154 | |
155 def githubRepoForPath(p: Path) -> str: | |
156 return p.name | |
157 | |
158 | |
159 def main(): | |
160 args = docopt.docopt(''' | |
161 Usage: | |
162 repo_github_status.py [options] | |
163 | |
164 Options: | |
165 -v, --verbose more logging | |
166 ''') | |
167 verboseLogging(args['--verbose']) | |
168 | |
169 yaml = YAML(typ='safe') | |
170 config = yaml.load(open('config.yaml')) | |
171 repos = [githubRepoForPath(Path(row['dir'])) for row in config['hg_repos'] if row['github']] | |
172 | |
173 log.info(f'{repos=}') | |
174 | |
175 class PG2(PatchableGraph, CurrentStateGraphApi): | |
176 pass | |
177 | |
178 graph = PG2() | |
179 | |
180 loop_forever_async(lambda first: update(graph, repos), 10, GITHUB_SYNC) | |
181 | |
182 class Application(cyclone.web.Application): | |
183 | |
184 def __init__(self): | |
185 handlers = [ | |
186 (r"/()", Index), | |
20 | 187 (r'/graph/githubRepos', CycloneGraphHandler, { |
19 | 188 'masterGraph': graph |
189 }), | |
20 | 190 (r'/graph/githubRepos/events', CycloneGraphEventsHandler, { |
19 | 191 'masterGraph': graph, |
192 }), | |
193 (r'/metrics', Metrics), | |
194 ] | |
195 cyclone.web.Application.__init__( | |
196 self, | |
197 handlers, | |
198 debug=args['--verbose'], | |
199 ) | |
200 | |
201 reactor.listenTCP(8000, Application(), interface='::') | |
202 reactor.run() | |
203 | |
204 | |
205 if __name__ == '__main__': | |
206 main() |