From 95513e13243162c6f773f105194382facbde0ce4 Mon Sep 17 00:00:00 2001 From: Drew DeVault Date: Thu, 25 Jul 2024 14:11:20 +0200 Subject: [PATCH] services.py: further GraphQL refactoring This refines the approach to make it more general and updates all git.sr.ht usage to be routed through GraphQL, save webhooks which will be addressed later. --- hubsrht/services.py | 339 ++++++++++++++++++++++---------------------- 1 file changed, 169 insertions(+), 170 deletions(-) diff --git a/hubsrht/services.py b/hubsrht/services.py index 457c843..76bf568 100644 --- a/hubsrht/services.py +++ b/hubsrht/services.py @@ -6,9 +6,10 @@ from abc import ABC from flask import url_for from markupsafe import Markup, escape from srht.api import ensure_webhooks, encrypt_request_authorization, get_results -from srht.graphql import gql_time -from srht.markdown import markdown, sanitize from srht.config import get_origin, cfg +from srht.graphql import gql_time, exec_gql +from srht.markdown import markdown, sanitize +from typing import Any, Callable _gitsrht = get_origin("git.sr.ht", default=None) _gitsrht_api = cfg("git.sr.ht", "api-origin", default=None) or _gitsrht @@ -47,8 +48,40 @@ def try_html_readme(session, prefix, user, repo_name): raise Exception(r.text) class SrhtService(ABC): - def __init__(self): + def __init__(self, site): self.session = requests.Session() + self.site = site + + def exec(self, user, query, valid=None, **kwargs): + return exec_gql(self.site, query, user=user, valid=valid, **kwargs) + + def enumerate(self, user, + query: str, + collector: Callable[[Any], Any], + **kwargs): + """Enumerate a cursor-driven list of resources from GraphQL. + + Parameters: + - user: the user to authenticate as + - query: the GraphQL query to execute + - collector: function which extracts the cursor object from the GraphQL + response + - kwargs: variables for GraphQL query. "cursor" is added to this by + enumerate. + """ + + items = [] + cursor = None + + while True: + r = self.exec(user, query, cursor=cursor, **kwargs) + result = collector(r) + items.extend(result["results"]) + cursor = result["cursor"] + if cursor is None: + break + + return items def post(self, user, valid, url, payload): r = self.session.post(url, @@ -78,41 +111,27 @@ class SrhtService(ABC): class GitService(SrhtService): def __init__(self): - super().__init__() + super().__init__("git.sr.ht") def get_repos(self, user): - get_repos_query = """ - query GetRepos($cursor: Cursor) { - me { - repositories(cursor: $cursor) { - results { - id - name - updated - owner { - canonicalName + repos = self.enumerate(user, """ + query GetRepos($cursor: Cursor) { + me { + repositories(cursor: $cursor) { + results { + id + name + updated + owner { + canonicalName + } } + cursor } - cursor } } - } - """ - - repos = [] - cursor = None - while True: - r = self.post(user, None, f"{_gitsrht_api}/query", { - "query": get_repos_query, - "variables": { - "cursor": cursor, - }, - }) - result = r["data"]["me"]["repositories"] - repos.extend(result["results"]) - cursor = result["cursor"] - if cursor is None: - break + """, + collector=lambda result: result["me"]["repositories"]) for repo in repos: repo["updated"] = gql_time(repo["updated"]) @@ -120,58 +139,47 @@ class GitService(SrhtService): return repos def get_repo(self, user, repo_name): - get_repo_query = """ - query GetRepo($repoName: String!) { - me { - repository(name: $repoName) { - id - name - description - visibility + resp = self.exec(user, """ + query GetRepo($repoName: String!) { + me { + repository(name: $repoName) { + id + name + description + visibility + } } } - } - """ - r = self.post(user, None, f"{_gitsrht_api}/query", { - "query": get_repo_query, - "variables": { - "repoName": repo_name, - }, - }) - return r["data"]["me"]["repository"] + """, repoName=repo_name) + return resp["me"]["repository"] def get_readme(self, user, repo_name, repo_url): - readme_query = """ - query Readme($username: String!, $repoName: String!) { - user(username: $username) { - repository(name: $repoName) { - html: readme - md: path(path: "README.md") { ...textData } - markdown: path(path: "README.markdown") { ...textData } - plaintext: path(path: "README") { ...textData } + resp = self.exec(user, """ + query Readme($username: String!, $repo_name: String!) { + user(username: $username) { + repository(name: $repo_name) { + html: readme + md: path(path: "README.md") { ...textData } + markdown: path(path: "README.markdown") { ...textData } + plaintext: path(path: "README") { ...textData } + } } } - } - fragment textData on TreeEntry { - object { - ... on TextBlob { - text + fragment textData on TreeEntry { + object { + ... on TextBlob { + text + } } } - } - """ - r = self.post(user, None, f"{_gitsrht_api}/query", { - "query": readme_query, - "variables": { - "username": user.username, - "repoName": repo_name, - }, - }) - if not r["data"]["user"]["repository"]: + """, + username=user.username, repo_name=repo_name) + + if not resp["user"]["repository"]: raise Exception("git.sr.ht returned no repository: " + json.dumps(r, indent=1)) - repo = r["data"]["user"]["repository"] + repo = resp["user"]["repository"] content = repo["html"] if content: @@ -181,7 +189,8 @@ class GitService(SrhtService): if content: blob_prefix = repo_url + "/blob/HEAD/" rendered_prefix = repo_url + "/tree/HEAD/" - html = markdown(content["object"]["text"], link_prefix=[rendered_prefix, blob_prefix]) + html = markdown(content["object"]["text"], + link_prefix=[rendered_prefix, blob_prefix]) return Markup(html) content = repo["plaintext"] @@ -192,141 +201,122 @@ class GitService(SrhtService): return None def get_manifests(self, user, repo_name): - manifests_query = """ - query Manifests($username: String!, $repoName: String!) { - user(username: $username) { - repository(name: $repoName) { - multiple: path(path:".builds") { - object { - ... on Tree { - entries { - results { - name - object { ... on TextBlob { text } } + resp = self.exec(user, """ + query Manifests($username: String!, $repo_name: String!) { + user(username: $username) { + repository(name: $repo_name) { + multiple: path(path:".builds") { + object { + ... on Tree { + entries { + results { + name + object { ... on TextBlob { text } } + } + } } } + }, + singleYML: path(path:".build.yml") { + object { + ... on TextBlob { text } + } + }, + singleYAML: path(path:".build.yaml") { + object { + ... on TextBlob { text } + } } } - }, - singleYML: path(path:".build.yml") { - object { - ... on TextBlob { text } - } - }, - singleYAML: path(path:".build.yaml") { - object { - ... on TextBlob { text } - } } } - } - } - """ - r = self.post(user, None, f"{_gitsrht_api}/query", { - "query": manifests_query, - "variables": { - "username": user.username, - "repoName": repo_name, - }, - }) - if not r["data"]["user"]["repository"]: + """, username=user.username, repo_name=repo_name) + + if not resp["user"]["repository"]: raise Exception(f"git.sr.ht did not find repo {repo_name} (requesting on behalf of {user.username})\n" + json.dumps(r, indent=1)) + repo = resp["user"]["repository"] + manifests = dict() - if r["data"]["user"]["repository"]["multiple"]: - for ent in r["data"]["user"]["repository"]["multiple"]["object"]\ - ["entries"]["results"]: + + if repo["multiple"]: + for ent in repo["multiple"]["object"]["entries"]["results"]: if not ent["object"]: continue manifests[ent["name"]] = ent["object"]["text"] - elif r["data"]["user"]["repository"]["singleYAML"]: - manifests[".build.yaml"] = r["data"]["user"]["repository"]["singleYAML"]\ - ["object"]["text"] - elif r["data"]["user"]["repository"]["singleYML"]: - manifests[".build.yml"] = r["data"]["user"]["repository"]["singleYML"]\ - ["object"]["text"] + elif repo["singleYML"]: + manifests[".build.yml"] = repo["singleYML"]["object"]["text"] + elif repo["singleYAML"]: + manifests[".build.yaml"] = repo["singleYAML"]["object"]["text"] else: return None + return manifests def log(self, user, repo, old, new): - query = """ - query Log($username: String!, $repo: String!, $from: String!) { - user(username: $username) { - repository(name: $repo) { - log(from: $from) { - results { - id - message - author { - name + resp = self.exec(user, """ + query Log($username: String!, $repo: String!, $from: String!) { + user(username: $username) { + repository(name: $repo) { + log(from: $from) { + results { + id + message + author { + name + } } } } } } - } - """ - r = self.post(user, None, f"{_gitsrht_api}/query", { - "query": query, - "variables": { + """, **{ "username": repo.owner.username, "repo": repo.name, "from": new, - } - }) + }) + commits = [] - for c in r["data"]["user"]["repository"]["log"]["results"]: + for c in resp["user"]["repository"]["log"]["results"]: if c["id"] == old: break commits.append(c) return commits def create_repo(self, user, valid, visibility): - query = """ - mutation CreateRepo( - $name: String!, - $description: String, - $visibility: Visibility!) { - createRepository(name: $name, - description: $description, - visibility: $visibility) { - id, name, description, visibility - } - } - """ name = valid.require("name") description = valid.require("description") if not valid.ok: return None - r = self.post(user, None, f"{_gitsrht_api}/query", { - "query": query, - "variables": { - "name": name, - "visibility": visibility.value.upper(), - "description": description, + + resp = self.exec(user, """ + mutation CreateRepo( + $name: String!, + $description: String, + $visibility: Visibility!) { + createRepository(name: $name, + description: $description, + visibility: $visibility) { + id, name, description, visibility + } } - }) - if not r["data"] or not r["data"]["createRepository"]: - for error in r["errors"]: - valid.error(error["message"]) + """, + name=name, + description=description, + visibility=visibility.value, + valid=valid) + + if not valid.ok: return None - repo = r["data"]["createRepository"] - repo["visibility"] = repo["visibility"].lower() - return r["data"]["createRepository"] + + return resp["createRepository"] def delete_repo(self, user, repo_id): - query = """ - mutation DeleteRepo($id: Int!) { - deleteRepository(id: $id) { id } - } - """ - self.post(user, None, f"{_gitsrht_api}/query", { - "query": query, - "variables": { - "id": repo_id, - }, - }) + self.exec(user, """ + mutation DeleteRepo($repo_id: Int!) { + deleteRepository(id: $repo_id) { id } + } + """, repo_id=repo_id) def ensure_user_webhooks(self, user): config = { @@ -362,7 +352,7 @@ class GitService(SrhtService): class HgService(SrhtService): def __init__(self): - super().__init__() + super().__init__("hg.sr.ht") def get_repos(self, user): return get_results(f"{_hgsrht}/api/repos", user) @@ -423,6 +413,9 @@ class HgService(SrhtService): pass # nbd, upstream was presumably deleted class ListService(SrhtService): + def __init__(self): + super().__init__("lists.sr.ht") + def get_lists(self, user): return get_results(f"{_listsrht}/api/lists", user) @@ -508,6 +501,9 @@ class ListService(SrhtService): return r["data"]["updateTool"]["id"] class TodoService(SrhtService): + def __init__(self): + super().__init__("todo.sr.ht") + def get_trackers(self, user): return get_results(f"{_todosrht}/api/trackers", user) @@ -631,6 +627,9 @@ class TodoService(SrhtService): self.put(user, None, url, payload) class BuildService(SrhtService): + def __init__(self): + super().__init__("builds.sr.ht") + def submit_build(self, user, manifest, note, tags, execute=True, valid=None, visibility=None): query = """ mutation SubmitBuild( -- 2.45.2