From fc1740e6aa122032d64ddcfc6d291c2a62fc9b17 Mon Sep 17 00:00:00 2001 From: "bozo.kopic" Date: Fri, 1 Apr 2022 23:48:44 +0200 Subject: atom feed --- VERSION | 2 +- src_py/boxhatter/backend.py | 30 +++++++++++++- src_py/boxhatter/main.py | 4 +- src_py/boxhatter/server.py | 7 +++- src_py/boxhatter/ui.py | 99 +++++++++++++++++++++++++++++++++++---------- 5 files changed, 116 insertions(+), 26 deletions(-) diff --git a/VERSION b/VERSION index 0c62199..ee1372d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2.1 +0.2.2 diff --git a/src_py/boxhatter/backend.py b/src_py/boxhatter/backend.py index ad83851..1a6ad11 100644 --- a/src_py/boxhatter/backend.py +++ b/src_py/boxhatter/backend.py @@ -1,6 +1,7 @@ from pathlib import Path import sqlite3 import typing +import uuid from hat import aio @@ -17,15 +18,27 @@ async def create(db_path: Path backend.async_group.spawn(aio.call_on_cancel, backend._executor, _ext_close, backend._db) + try: + backend._server_uuid = await backend._executor(_ext_get_server_uuid, + backend._db) + + except BaseException: + await aio.uncancellable(backend.async_close()) + raise + return backend class Backend(aio.Resource): @property - def async_group(self): + def async_group(self) -> aio.Group: return self._async_group + @property + def server_uuid(self) -> uuid.UUID: + return self._server_uuid + async def get_commits(self, repo: typing.Optional[str], statuses: typing.Optional[typing.Set[common.Status]], @@ -60,6 +73,9 @@ def _ext_create(db_path): try: db.executescript(r""" PRAGMA journal_mode = OFF; + CREATE TABLE IF NOT EXISTS server_uuid ( + uuid TEXT + ); CREATE TABLE IF NOT EXISTS commits ( repo TEXT, hash TEXT, @@ -83,6 +99,18 @@ def _ext_close(db): db.close() +def _ext_get_server_uuid(db): + cur = db.execute("SELECT * FROM server_uuid") + row = cur.fetchone() + if row: + return uuid.UUID(row[0]) + server_uuid = uuid.uuid4() + cmd = ("INSERT INTO server_uuid VALUES (:uuid)") + args = {'uuid': str(server_uuid)} + db.execute(cmd, args) + return server_uuid + + def _ext_get_commits(db, repo, statuses, order): cmd = "SELECT * FROM commits" where = [] diff --git a/src_py/boxhatter/main.py b/src_py/boxhatter/main.py index 7a16963..6f23bda 100644 --- a/src_py/boxhatter/main.py +++ b/src_py/boxhatter/main.py @@ -23,7 +23,7 @@ user_config_dir: Path = Path(appdirs.user_config_dir('boxhatter')) user_data_dir: Path = Path(appdirs.user_data_dir('boxhatter')) default_conf_path: Path = user_config_dir / 'server.yaml' -default_db_path: Path = user_data_dir / 'boxhatter.db' +default_db_path: Path = user_data_dir / 'server.db' @click.group() @@ -123,7 +123,7 @@ def execute(action: str, "(default $XDG_CONFIG_HOME/boxhatter/server.yaml)") @click.option('--db', default=default_db_path, metavar='PATH', type=Path, help="sqlite database path " - "(default $XDG_CONFIG_HOME/boxhatter/boxhatter.db") + "(default $XDG_CONFIG_HOME/boxhatter/server.db") def server(host: str, port: int, conf: Path, diff --git a/src_py/boxhatter/server.py b/src_py/boxhatter/server.py index 7fbabde..ddaf7ca 100644 --- a/src_py/boxhatter/server.py +++ b/src_py/boxhatter/server.py @@ -8,6 +8,7 @@ import subprocess import sys import time import typing +import uuid from hat import aio from hat import json @@ -59,9 +60,13 @@ async def create(conf: json.Data, class Server(aio.Resource): @property - def async_group(self): + def async_group(self) -> aio.Group: return self._async_group + @property + def server_uuid(self) -> uuid.UUID: + return self._backend.server_uuid + @property def repos(self) -> typing.Set[str]: return self._repos diff --git a/src_py/boxhatter/ui.py b/src_py/boxhatter/ui.py index 2cf174d..bea4782 100644 --- a/src_py/boxhatter/ui.py +++ b/src_py/boxhatter/ui.py @@ -1,5 +1,7 @@ from pathlib import Path import datetime +import time +import uuid from hat import aio import aiohttp.web @@ -23,8 +25,10 @@ async def create(host: str, get_routes = ( aiohttp.web.get(path, handler) for path, handler in ( ('/', ui._process_get_root), + ('/feed', ui._process_get_feed), ('/repo/{repo}', ui._process_get_repo), - ('/repo/{repo}/commit/{commit}', ui._process_get_commit))) + ('/repo/{repo}/commit/{commit}', ui._process_get_commit), + ('/repo/{repo}/feed', ui._process_get_feed))) post_routes = ( aiohttp.web.post(path, handler) for path, handler in ( ('/repo/{repo}/run', ui._process_post_run), @@ -56,7 +60,7 @@ async def create(host: str, class UI(aio.Resource): @property - def async_group(self): + def async_group(self) -> aio.Group: return self._async_group async def _process_get_root(self, request): @@ -64,7 +68,7 @@ class UI(aio.Resource): body = (f'{_generate_repos(self._server.repos)}\n' f'{_generate_commits(commits)}') - return _create_html_response('Box Hatter', body) + return _create_html_response('Box Hatter', body, '/feed') async def _process_get_repo(self, request): repo = self._get_repo(request) @@ -73,14 +77,26 @@ class UI(aio.Resource): title = f'Box Hatter - {repo}' body = (f'{_generate_commits(commits)}\n' f'{_generate_run(repo)}') - return _create_html_response(title, body) + feed_url = f'/repo/{repo}/feed' + return _create_html_response(title, body, feed_url) async def _process_get_commit(self, request): commit = await self._get_commit(request) title = f'Box Hatter - {commit.repo}/{commit.hash}' body = _generate_commit(commit) - return _create_html_response(title, body) + feed_url = f'/repo/{commit.repo}/feed' + return _create_html_response(title, body, feed_url) + + async def _process_get_feed(self, request): + repo = (self._get_repo(request) if 'repo' in request.match_info + else None) + commits = await self._server.get_commits(repo) + + title = 'All repositories' if repo is None else f'Repository {repo}' + text = _generate_feed(self._server.server_uuid, title, commits) + return aiohttp.web.Response(content_type='application/atom+xml', + text=text) async def _process_post_run(self, request): repo = self._get_repo(request) @@ -123,9 +139,20 @@ class UI(aio.Resource): return commit -def _create_html_response(title, body): - text = _html_template.format(title=title, - body=body) +def _create_html_response(title, body, feed_url): + text = (f'\n' + f'\n' + f'\n' + f'\n' + f'\n' # NOQA + f'{title}\n' + f'\n' # NOQA + f'\n' + f'\n' + f'\n' + f'{body}\n' + f'\n' + f'\n') return aiohttp.web.Response(content_type='text/html', text=text) @@ -217,16 +244,46 @@ def _format_time(t): return datetime.datetime.fromtimestamp(t).strftime("%Y-%m-%d %H:%M:%S") -_html_template = r""" - - - - -{title} - - - -{body} - - -""" +def format_feed_time(t): + dt = datetime.datetime.utcfromtimestamp(t) + return dt.isoformat(timespec='seconds') + 'Z' + + +def _generate_feed(server_uuid, title, commits): + + def get_entry_uuid(commit): + return uuid.uuid5(server_uuid, f'{commit.repo}/{commit.hash}') + + def get_entry_title(commit): + return f'{commit.repo} - {commit.hash}' + + def get_entry_link(commit): + return f'/repo/{commit.repo}/commit/{commit.hash}' + + def get_entry_content(commit): + return (f'Status: {commit.status.name}\n' + f'Output:\n{commit.output}') + + feed_updated = max((commit.change for commit in commits), + default=int(time.time())) + + entries = '\n'.join( + f'\n' + f'urn:uuid:{get_entry_uuid(commit)}\n' + f'{get_entry_title(commit)}\n' + f'\n' + f'{format_feed_time(commit.change)}\n' + f'{get_entry_content(commit)}\n' + f'' + for commit in commits) + + return (f'\n' + f'\n' + f'{title}\n' + f'urn:uuid:{server_uuid}\n' + f'\n' + f'boxhatter\n' + f'\n' + f'{format_feed_time(feed_updated)}\n' + f'{entries}\n' + f'\n') -- cgit v1.2.3-70-g09d2