comparison repo_local_status.py @ 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
author drewp@bigasterisk.com
date Sun, 09 Jan 2022 00:21:41 -0800
parents hg_status.py@a4778c56cc03
children b59912649fc4
comparison
equal deleted inserted replaced
17:a4778c56cc03 18:6f38aa08408d
1 """
2 configured hg dirs and settings -> rdf graph
3 """
4 import datetime
5 import json
6 import time
7 import traceback
8 from dataclasses import dataclass, field
9 from pathlib import Path
10 from typing import Dict, Optional, Tuple
11
12 import cyclone.httpserver
13 import cyclone.sse
14 import cyclone.web
15 import docopt
16 import treq
17 import tzlocal
18 from cycloneerr import PrettyErrorHandler
19 from dateutil.parser import parse
20 from dateutil.tz import tzlocal
21 from prometheus_client.exposition import generate_latest
22 from prometheus_client.registry import REGISTRY
23 from ruamel.yaml import YAML
24 from standardservice.logsetup import log, verboseLogging
25 from twisted.internet import reactor
26 from twisted.internet.defer import inlineCallbacks, returnValue
27 from twisted.internet.utils import _UnexpectedErrorOutput, getProcessOutput
28
29 from patch_cyclone_sse import patchCycloneSse
30 patchCycloneSse()
31
32 githubOwner = 'drewp'
33
34
35 @inlineCallbacks
36 def runHg(cwd, args):
37 if args[0] not in ['push']:
38 args.extend(['-T', 'json'])
39 j = yield getProcessOutput('/usr/local/bin/hg', args, path=cwd)
40 returnValue(json.loads(j) if j else None)
41
42
43 @dataclass
44 class Repo:
45 path: Path
46 github: bool
47 _cache: Dict[str, Tuple[float, object]] = field(default_factory=dict)
48
49 def _isStale(self, group) -> Optional[object]:
50 now = time.time()
51 if group not in self._cache:
52 return True
53 if now > self._cache[group][0] + 86400:
54 return True
55 print('fresh')
56 return False
57
58 def _save(self, group, obj):
59 now = time.time()
60 self._cache[group] = (now, obj)
61
62 def _get(self, group):
63 print('get')
64 return self._cache[group][1]
65
66 @inlineCallbacks
67 def getStatus(self):
68 if self._isStale('status'):
69 try:
70 statusResp = yield runHg(self.path, ['status'])
71 except Exception as e:
72 status = {'error': repr(e)}
73 else:
74 unknowns = len([row for row in statusResp if row['status'] == '?'])
75 status = {'unknown': unknowns, 'changed': len(statusResp) - unknowns}
76 self._save('status', status)
77 returnValue(self._get('status'))
78
79 @inlineCallbacks
80 def getLatestHgCommit(self):
81 if self._isStale('log'):
82 rows = yield runHg(self.path, ['log', '--limit', '1'])
83 commit = rows[0]
84 sec = commit['date'][0]
85 t = datetime.datetime.fromtimestamp(sec, tzlocal())
86 self._save('log', {'email': commit['user'], 't': t.isoformat(), 'message': commit['desc']})
87 returnValue(self._get('log'))
88
89 @inlineCallbacks
90 def getLatestGithubCommit(self):
91 if self._isStale('github'):
92 resp = yield treq.get(f'https://api.github.com/repos/{githubOwner}/{self.path.name}/commits?per_page=1',
93 timeout=5,
94 headers={
95 'User-agent': 'reposync by github.com/drewp',
96 'Accept': 'application/vnd.github.v3+json'
97 })
98 ret = yield treq.json_content(resp)
99 commit = ret[0]['commit']
100 t = parse(commit['committer']['date']).astimezone(tzlocal()).isoformat()
101 self._save('github', {'email': commit['committer']['email'], 't': t, 'message': commit['message']})
102 returnValue(self._get('github'))
103
104 @inlineCallbacks
105 def clearGithubMaster(self):
106 '''bang(pts/13):/tmp/reset% git init
107 Initialized empty Git repository in /tmp/reset/.git/
108 then github set current to a new branch called 'clearing' with https://developer.github.com/v3/repos/#update-a-repository
109 bang(pts/13):/tmp/reset% git remote add origin git@github.com:drewp/href.git
110 bang(pts/13):/tmp/reset% git push origin :master
111 To github.com:drewp/href.git
112 - [deleted] master
113 maybe --set-upstream origin
114 bang(pts/13):/tmp/reset% git remote set-branches origin master
115 ?
116 then push
117 then github setdefault to master
118 then github delete clearing
119 '''
120
121 @inlineCallbacks
122 def pushToGithub(self):
123 if not self.github:
124 raise ValueError
125 yield runHg(self.path, ['bookmark', '--rev', 'default', 'master'])
126 out = yield runHg(self.path, ['push', f'git+ssh://git@github.com/{githubOwner}/{self.path.name}.git'])
127 print(f'out fompushh {out}')
128
129
130 class GithubSync(PrettyErrorHandler, cyclone.web.RequestHandler):
131
132 @inlineCallbacks
133 def post(self):
134 try:
135 path = self.get_argument('repo')
136 repo = [r for r in self.settings.repos if str(r.path) == path][0]
137 yield repo.pushToGithub()
138 except Exception:
139 traceback.print_exc()
140 raise
141
142
143 class Statuses(cyclone.sse.SSEHandler):
144
145 def update(self, key, data):
146 self.sendEvent(json.dumps({'key': key, 'update': data}).encode('utf8'))
147
148 def bind(self):
149 self.toProcess = self.settings.repos[:]
150 reactor.callLater(0, self.runOne)
151
152 @inlineCallbacks
153 def runOne(self):
154 if not self.toProcess:
155 print('done')
156 return
157 repo = self.toProcess.pop(0)
158
159 try:
160 update = {'path': str(repo.path), 'github': repo.github, 'status': (yield repo.getStatus()), 'hgLatest': (yield repo.getLatestHgCommit())}
161 if repo.github:
162 update['githubLatest'] = (yield repo.getLatestGithubCommit())
163 self.update(str(repo.path), update)
164 except Exception:
165 log.warn(f'not reporting on {repo}')
166 traceback.print_exc()
167 reactor.callLater(0, self.runOne)
168
169
170 class Metrics(cyclone.web.RequestHandler):
171
172 def get(self):
173 self.add_header('content-type', 'text/plain')
174 self.write(generate_latest(REGISTRY))
175
176
177 def main():
178 args = docopt.docopt('''
179 Usage:
180 hg_status.py [options]
181
182 Options:
183 -v, --verbose more logging
184 ''')
185 verboseLogging(args['--verbose'])
186
187 # import sys
188 # sys.path.append('/usr/lib/python3/dist-packages')
189 # import OpenSSL
190
191 yaml = YAML(typ='safe')
192 config = yaml.load(open('config.yaml'))
193 repos = [Repo(Path(row['dir']), row['github']) for row in config['hg_repos']]
194
195 class Application(cyclone.web.Application):
196
197 def __init__(self):
198 handlers = [
199 (r"/()", cyclone.web.StaticFileHandler, {
200 'path': '.',
201 'default_filename': 'index.html'
202 }),
203 (r'/build/(bundle\.js)', cyclone.web.StaticFileHandler, {
204 'path': './build/'
205 }),
206 (r'/status/events', Statuses),
207 (r'/githubSync', GithubSync),
208 (r'/metrics', Metrics),
209 ]
210 cyclone.web.Application.__init__(
211 self,
212 handlers,
213 repos=repos,
214 debug=args['--verbose'],
215 template_path='.',
216 )
217
218 reactor.listenTCP(10001, Application())
219 reactor.run()
220
221
222 if __name__ == '__main__':
223 main()