~sircmpwn/hub.sr.ht

95513e13243162c6f773f105194382facbde0ce4 — Drew DeVault a month ago 840a8b8
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.
1 files changed, 169 insertions(+), 170 deletions(-)

M hubsrht/services.py
M hubsrht/services.py => hubsrht/services.py +169 -170
@@ 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(