test: sweep every tool through the MCP server against a seeded Gitea
This commit is contained in:
1 parent
22c8157ca4
commit
303bd9aa21
7 files changed
+1759
-1
No files matched your search
@@ -0,0 +1,445 @@
|
||||
"""Seed data for the all-tools sweep.
|
||||
|
||||
:func:`seed` creates the resources the sweep's arguments point at (through the Gitea API,
|
||||
not through the tools under test) and returns the context dictionary. Everything created
|
||||
here is fake data in a disposable instance.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import secrets
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
||||
|
||||
from tests.integration.harness import GiteaInstance, HarnessError
|
||||
|
||||
Ctx = dict[str, Any]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Small helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def b64(text: str | bytes) -> str:
|
||||
"""Base64 text of ``text``."""
|
||||
raw = text.encode("utf-8") if isinstance(text, str) else text
|
||||
return base64.b64encode(raw).decode("ascii")
|
||||
|
||||
|
||||
def ssh_public_key() -> str:
|
||||
"""A fresh ed25519 public key in OpenSSH format (throwaway)."""
|
||||
key = Ed25519PrivateKey.generate().public_key()
|
||||
raw = key.public_bytes(serialization.Encoding.OpenSSH, serialization.PublicFormat.OpenSSH)
|
||||
return raw.decode("ascii")
|
||||
|
||||
|
||||
# A 1x1 transparent PNG, for the avatar endpoints.
|
||||
PNG_1X1 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Seeding
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class Seeder:
|
||||
"""Creates the sweep's seed data through the Gitea API (not through the tools)."""
|
||||
|
||||
def __init__(self, gitea: GiteaInstance, user: str, user_token: str) -> None:
|
||||
self.gitea = gitea
|
||||
self.user = user
|
||||
self.user_token = user_token
|
||||
|
||||
def call(
|
||||
self,
|
||||
method: str,
|
||||
path: str,
|
||||
*,
|
||||
token: str | None = None,
|
||||
ok: tuple[int, ...] = (200, 201, 202, 204),
|
||||
**kwargs: Any,
|
||||
) -> Any:
|
||||
"""Call the API, require a status in ``ok`` and return the JSON body (or None)."""
|
||||
response = self.gitea.api(method, path, token=token, **kwargs)
|
||||
if response.status_code not in ok:
|
||||
raise HarnessError(
|
||||
f"seed {method} {path} failed: HTTP {response.status_code}: {response.text[:200]}"
|
||||
)
|
||||
if response.content and "json" in response.headers.get("content-type", ""):
|
||||
return response.json()
|
||||
return None
|
||||
|
||||
def put_file(self, repo: str, path: str, text: str, branch: str = "main") -> dict[str, Any]:
|
||||
"""Create a file through the contents API."""
|
||||
owner = self.gitea.admin_login
|
||||
body = {"content": b64(text), "message": f"add {path}", "branch": branch}
|
||||
result: dict[str, Any] = self.call(
|
||||
"POST", f"/repos/{owner}/{repo}/contents/{path}", json=body
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
WORKFLOW = """\
|
||||
name: sweep
|
||||
on: [push, workflow_dispatch]
|
||||
jobs:
|
||||
hello:
|
||||
runs-on: aegis-sweep-nonexistent
|
||||
steps:
|
||||
- run: echo hello
|
||||
"""
|
||||
|
||||
|
||||
def seed(gitea: GiteaInstance, user: str, user_token: str) -> Ctx:
|
||||
"""Create the sweep's seed data and return the context the arguments are built from."""
|
||||
s = Seeder(gitea, user, user_token)
|
||||
admin = gitea.admin_login
|
||||
sfx = secrets.token_hex(3)
|
||||
c: Ctx = {"admin": admin, "user": user, "owner": admin, "sfx": sfx}
|
||||
c["admin_password"] = gitea.admin_password
|
||||
|
||||
# A second user plus throwaway users for the destructive admin tools.
|
||||
c["user2"] = gitea.create_user(f"sw-u2-{sfx}")[0]
|
||||
c["user3"] = gitea.create_user(f"sw-u3-{sfx}")[0]
|
||||
c["del_user"] = gitea.create_user(f"sw-del-{sfx}")[0]
|
||||
c["rename_user"] = gitea.create_user(f"sw-ren-{sfx}")[0]
|
||||
|
||||
# Organization with a team and a repository.
|
||||
c["org"] = org = f"sw-org-{sfx}"
|
||||
s.call("POST", "/orgs", json={"username": org, "visibility": "public"})
|
||||
c["org_repo"] = f"sw-orgrepo-{sfx}"
|
||||
s.call("POST", f"/orgs/{org}/repos", json={"name": c["org_repo"], "auto_init": True})
|
||||
team = s.call(
|
||||
"POST",
|
||||
f"/orgs/{org}/teams",
|
||||
json={
|
||||
"name": "sweepers",
|
||||
"permission": "write",
|
||||
"includes_all_repositories": False,
|
||||
"units": ["repo.code", "repo.issues", "repo.pulls"],
|
||||
},
|
||||
)
|
||||
c["team_id"], c["team_name"] = team["id"], team["name"]
|
||||
team2 = s.call(
|
||||
"POST",
|
||||
f"/orgs/{org}/teams",
|
||||
json={"name": "sweepers2", "permission": "read", "units": ["repo.code"]},
|
||||
)
|
||||
c["team2_name"] = team2["name"]
|
||||
s.call("PUT", f"/teams/{c['team_id']}/members/{user}")
|
||||
s.call("PUT", f"/teams/{c['team_id']}/repos/{org}/{c['org_repo']}")
|
||||
s.call("PUT", f"/orgs/{org}/public_members/{user}")
|
||||
s.call("PUT", f"/orgs/{org}/blocks/{c['user2']}")
|
||||
c["org_label"] = s.call(
|
||||
"POST", f"/orgs/{org}/labels", json={"name": "org-bug", "color": "#ff0000"}
|
||||
)["id"]
|
||||
c["org_hook"] = s.call(
|
||||
"POST",
|
||||
f"/orgs/{org}/hooks",
|
||||
json={"type": "gitea", "config": {"url": "http://127.0.0.1:9/h", "content_type": "json"}},
|
||||
)["id"]
|
||||
s.call("PUT", f"/orgs/{org}/actions/secrets/ORG_SECRET", json={"data": "org-secret-value"})
|
||||
s.call("POST", f"/orgs/{org}/actions/variables/ORG_VAR", json={"value": "1"})
|
||||
c["org_issue_id"] = s.call(
|
||||
"POST", f"/repos/{org}/{c['org_repo']}/issues", json={"title": "org issue"}
|
||||
)["id"]
|
||||
_project(s, c, "org", f"/orgs/{org}/projects")
|
||||
|
||||
# Throwaway org and repository for the destructive tools.
|
||||
c["del_org"] = f"sw-delorg-{sfx}"
|
||||
s.call("POST", "/orgs", json={"username": c["del_org"]})
|
||||
c["del_org2"] = f"sw-delorg2-{sfx}"
|
||||
s.call("POST", "/orgs", json={"username": c["del_org2"]})
|
||||
c["rename_org"] = f"sw-renorg-{sfx}"
|
||||
s.call("POST", "/orgs", json={"username": c["rename_org"]})
|
||||
c["del_repo"] = f"sw-delrepo-{sfx}"
|
||||
gitea.create_repo(admin, c["del_repo"])
|
||||
c["xfer_repo"] = f"sw-xfer-{sfx}"
|
||||
gitea.create_repo(admin, c["xfer_repo"])
|
||||
c["template_repo"] = f"sw-tpl-{sfx}"
|
||||
gitea.create_repo(admin, c["template_repo"])
|
||||
s.call("PATCH", f"/repos/{admin}/{c['template_repo']}", json={"template": True})
|
||||
|
||||
# The main seed repository.
|
||||
c["repo"] = repo = f"sw-repo-{sfx}"
|
||||
created = gitea.create_repo(admin, repo)
|
||||
c["repo_id"] = created["id"]
|
||||
base = f"/repos/{admin}/{repo}"
|
||||
branch = s.call("GET", f"{base}/branches/main")
|
||||
c["sha"] = branch["commit"]["id"]
|
||||
s.put_file(repo, ".editorconfig", "root = true\n")
|
||||
s.put_file(repo, "docs/a.txt", "alpha\n")
|
||||
s.put_file(repo, ".gitea/workflows/sweep.yml", WORKFLOW)
|
||||
c["sha"] = s.call("GET", f"{base}/branches/main")["commit"]["id"]
|
||||
s.call("PUT", f"{base}/collaborators/{user}", json={"permission": "write"})
|
||||
s.call("PUT", f"{base}/collaborators/{c['user2']}", json={"permission": "write"})
|
||||
s.call("PUT", f"{base}/topics", json={"topics": ["sweep", "seed"]})
|
||||
s.call("PUT", f"{base}/topics/extra")
|
||||
# A fork (for merge-upstream), a pull mirror and push-mirror targets, all from this instance.
|
||||
local = f"http://127.0.0.1:3000/{admin}"
|
||||
c["fork_repo"] = f"sw-fork-{sfx}"
|
||||
s.call("POST", f"{base}/forks", json={"organization": org, "name": c["fork_repo"]})
|
||||
c["mirror_repo"] = f"sw-mirror-{sfx}"
|
||||
s.call(
|
||||
"POST",
|
||||
"/repos/migrate",
|
||||
json={
|
||||
"clone_addr": f"{local}/{c['template_repo']}.git",
|
||||
"repo_name": c["mirror_repo"],
|
||||
"service": "git",
|
||||
"mirror": True,
|
||||
"mirror_interval": "8h0m0s",
|
||||
},
|
||||
)
|
||||
c["push_target"] = f"sw-ptarget-{sfx}"
|
||||
gitea.create_repo(admin, c["push_target"], auto_init=False)
|
||||
c["push_target2"] = f"sw-ptarget2-{sfx}"
|
||||
gitea.create_repo(admin, c["push_target2"], auto_init=False)
|
||||
c["push_mirror"] = s.call(
|
||||
"POST",
|
||||
f"{base}/push_mirrors",
|
||||
json={
|
||||
"remote_address": f"{local}/{c['push_target']}.git",
|
||||
"remote_username": admin,
|
||||
"remote_password": gitea.admin_password,
|
||||
"interval": "8h0m0s",
|
||||
"sync_on_commit": False,
|
||||
},
|
||||
)["remote_name"]
|
||||
s.call(
|
||||
"POST",
|
||||
f"{base}/statuses/{c['sha']}",
|
||||
json={"state": "success", "context": "ci/sweep"},
|
||||
)
|
||||
s.call("POST", f"{base}/tags", json={"tag_name": "v0.0.1", "target": "main"})
|
||||
annotated = s.call(
|
||||
"POST", f"{base}/tags", json={"tag_name": "v0.0.3", "target": "main", "message": "note"}
|
||||
)
|
||||
c["tag_sha"] = annotated["id"]
|
||||
for name in ("feat", "feat2", "feat3", "rn", "del-branch"):
|
||||
s.call(
|
||||
"POST", f"{base}/branches", json={"new_branch_name": name, "old_branch_name": "main"}
|
||||
)
|
||||
s.put_file(repo, "feat.txt", "feature\n", branch="feat")
|
||||
s.put_file(repo, "feat2.txt", "feature two\n", branch="feat2")
|
||||
s.put_file(repo, "feat3.txt", "feature three\n", branch="feat3")
|
||||
s.call("POST", f"{base}/branch_protections", json={"rule_name": "release/*"})
|
||||
c["protection"] = "release/*"
|
||||
s.call(
|
||||
"POST",
|
||||
f"{base}/tag_protections",
|
||||
json={"name_pattern": "v9*", "whitelist_usernames": [admin]},
|
||||
)
|
||||
c["tag_protection"] = s.call("GET", f"{base}/tag_protections")[0]["id"]
|
||||
s.put_file(repo, "to-delete.txt", "bye\n")
|
||||
s.put_file(repo, "to-update.txt", "before\n")
|
||||
c["delete_file_sha"] = s.call("GET", f"{base}/contents/to-delete.txt")["sha"]
|
||||
c["update_file_sha"] = s.call("GET", f"{base}/contents/to-update.txt")["sha"]
|
||||
c["blob_sha"] = s.call("GET", f"{base}/contents/README.md")["sha"]
|
||||
c["page_name"] = "Home"
|
||||
c["cron_task"] = "cleanup_hook_task_table"
|
||||
|
||||
# Issues: 1 main, 2 dependency target, 3 pinned, 4/5 more targets.
|
||||
for number, title in enumerate(("main", "dependency", "pinned", "target four", "target five")):
|
||||
issue = s.call("POST", f"{base}/issues", json={"title": f"Sweep {title}", "body": "x"})
|
||||
assert issue["number"] == number + 1
|
||||
c["issue"], c["issue2"], c["issue3"], c["issue4"], c["issue5"] = 1, 2, 3, 4, 5
|
||||
c["issue1_id"] = s.call("GET", f"{base}/issues/1")["id"]
|
||||
c["comment"] = s.call("POST", f"{base}/issues/1/comments", json={"body": "one"})["id"]
|
||||
c["comment2"] = s.call("POST", f"{base}/issues/1/comments", json={"body": "two"})["id"]
|
||||
c["label"] = s.call("POST", f"{base}/labels", json={"name": "bug", "color": "#d73a4a"})["id"]
|
||||
s.call("POST", f"{base}/issues/1/labels", json={"labels": [c["label"]]})
|
||||
c["milestone"] = s.call("POST", f"{base}/milestones", json={"title": "m1"})["id"]
|
||||
c["milestone_name"] = "m1"
|
||||
s.call("POST", f"{base}/issues/1/assignees", json={"assignees": [admin]})
|
||||
s.call("PUT", f"{base}/issues/1/subscriptions/{user}")
|
||||
s.call("POST", f"{base}/issues/1/reactions", json={"content": "+1"})
|
||||
s.call("POST", f"{base}/issues/comments/{c['comment']}/reactions", json={"content": "+1"})
|
||||
s.call("POST", f"{base}/issues/1/dependencies", json={"index": 2, "owner": admin, "repo": repo})
|
||||
s.call("POST", f"{base}/issues/1/blocks", json={"index": 3, "owner": admin, "repo": repo})
|
||||
# A comment by another user notifies the admin (author and assignee of issue 1).
|
||||
s.call("POST", f"{base}/issues/1/comments", token=user_token, json={"body": "ping"})
|
||||
s.call("POST", f"{base}/issues/3/pin")
|
||||
s.call("POST", f"{base}/issues/2/stopwatch/start", token=user_token)
|
||||
time_entry = s.call("POST", f"{base}/issues/1/times", json={"time": 60})
|
||||
c["time_id"] = time_entry["id"]
|
||||
attachment = s.call(
|
||||
"POST",
|
||||
f"{base}/issues/1/assets",
|
||||
files={"attachment": ("issue.txt", b"issue attachment", "text/plain")},
|
||||
params={"name": "issue.txt"},
|
||||
)
|
||||
c["issue_attachment"] = attachment["id"]
|
||||
attachment = s.call(
|
||||
"POST",
|
||||
f"{base}/issues/comments/{c['comment']}/assets",
|
||||
files={"attachment": ("comment.txt", b"comment attachment", "text/plain")},
|
||||
params={"name": "comment.txt"},
|
||||
)
|
||||
c["comment_attachment"] = attachment["id"]
|
||||
|
||||
# Pull requests: 6 is the main one, 7 is merged by the sweep.
|
||||
pr = s.call("POST", f"{base}/pulls", json={"title": "Sweep PR", "base": "main", "head": "feat"})
|
||||
c["pr"] = pr["number"]
|
||||
pr2 = s.call(
|
||||
"POST", f"{base}/pulls", json={"title": "Sweep PR two", "base": "main", "head": "feat2"}
|
||||
)
|
||||
c["pr2"] = pr2["number"]
|
||||
pr_path = f"{base}/pulls/{c['pr']}"
|
||||
review = s.call(
|
||||
"POST",
|
||||
f"{pr_path}/reviews",
|
||||
token=user_token,
|
||||
json={
|
||||
"event": "REQUEST_CHANGES",
|
||||
"body": "please change",
|
||||
"comments": [{"path": "feat.txt", "new_position": 1, "body": "line note"}],
|
||||
},
|
||||
)
|
||||
c["review"] = review["id"]
|
||||
c["review_comment"] = s.call("GET", f"{pr_path}/reviews/{review['id']}/comments")[0]["id"]
|
||||
c["review2"] = s.call(
|
||||
"POST", f"{pr_path}/reviews", token=user_token, json={"event": "COMMENT", "body": "fyi"}
|
||||
)["id"]
|
||||
c["pending_review"] = s.call(
|
||||
"POST", f"{pr_path}/reviews", token=user_token, json={"body": "draft"}
|
||||
)["id"]
|
||||
s.call(
|
||||
"POST", f"{pr_path}/requested_reviewers", json={"reviewers": [c["user2"]]}, ok=(201, 422)
|
||||
)
|
||||
|
||||
# Release with an asset (and a second release removed by tag).
|
||||
release = s.call(
|
||||
"POST", f"{base}/releases", json={"tag_name": "v1.0.0", "target_commitish": "main"}
|
||||
)
|
||||
c["release"], c["release_tag"] = release["id"], "v1.0.0"
|
||||
asset = s.call(
|
||||
"POST",
|
||||
f"{base}/releases/{release['id']}/assets",
|
||||
files={"attachment": ("rel.bin", b"release bytes", "application/octet-stream")},
|
||||
params={"name": "rel.bin"},
|
||||
)
|
||||
c["release_asset"] = asset["id"]
|
||||
s.call("POST", f"{base}/releases", json={"tag_name": "v2.0.0", "target_commitish": "main"})
|
||||
c["release_tag2"] = "v2.0.0"
|
||||
|
||||
# Hooks, keys, secrets, variables, wiki, projects.
|
||||
hook_body = {"type": "gitea", "config": {"url": "http://127.0.0.1:9/h", "content_type": "json"}}
|
||||
c["repo_hook"] = s.call("POST", f"{base}/hooks", json=hook_body)["id"]
|
||||
s.call(
|
||||
"PATCH",
|
||||
f"{base}/hooks/git/pre-receive",
|
||||
json={"content": "#!/bin/sh\nexit 0\n"},
|
||||
ok=(200,),
|
||||
)
|
||||
c["git_hook"] = "pre-receive"
|
||||
c["user_hook"] = s.call("POST", "/user/hooks", json=hook_body)["id"]
|
||||
c["admin_hook"] = s.call("POST", "/admin/hooks", json=hook_body)["id"]
|
||||
c["deploy_key"] = s.call(
|
||||
"POST", f"{base}/keys", json={"title": f"deploy-{sfx}", "key": ssh_public_key()}
|
||||
)["id"]
|
||||
c["user_key"] = s.call(
|
||||
"POST", "/user/keys", json={"title": f"mine-{sfx}", "key": ssh_public_key()}
|
||||
)["id"]
|
||||
c["admin_user_key"] = s.call(
|
||||
"POST", f"/admin/users/{user}/keys", json={"title": f"k-{sfx}", "key": ssh_public_key()}
|
||||
)["id"]
|
||||
s.call("PUT", f"{base}/actions/secrets/REPO_SECRET", json={"data": "repo-secret-value"})
|
||||
c["user_secret"] = f"USER_SECRET_{sfx.upper()}"
|
||||
c["user_var"] = f"USER_VAR_{sfx.upper()}"
|
||||
s.call("PUT", f"/user/actions/secrets/{c['user_secret']}", json={"data": "user-secret-value"})
|
||||
s.call("POST", f"{base}/actions/variables/REPO_VAR", json={"value": "1"})
|
||||
s.call("POST", f"/user/actions/variables/{c['user_var']}", json={"value": "1"})
|
||||
s.call(
|
||||
"POST",
|
||||
f"{base}/wiki/new",
|
||||
json={"title": "Home", "content_base64": b64("# home\n"), "message": "home"},
|
||||
)
|
||||
_project(s, c, "repo", f"{base}/projects")
|
||||
_project(s, c, "user", "/user/projects")
|
||||
c["oauth_app"] = s.call(
|
||||
"POST",
|
||||
"/user/applications/oauth2",
|
||||
json={"name": f"sweep-{sfx}", "redirect_uris": ["http://127.0.0.1:1/cb"]},
|
||||
)["id"]
|
||||
|
||||
# Current user: email, follow, star, watch, block.
|
||||
c["email"] = f"sweep-{sfx}@example.test"
|
||||
s.call("POST", "/user/emails", json={"emails": [c["email"]]})
|
||||
s.call("PUT", f"/user/following/{user}")
|
||||
s.call("PUT", f"/user/starred/{admin}/{repo}")
|
||||
s.call("PUT", f"{base}/subscription")
|
||||
s.call("PUT", f"/user/blocks/{c['user2']}")
|
||||
# A throwaway access token of the admin (basic auth), deleted by name.
|
||||
token_name = f"sweep-{sfx}"
|
||||
s.call(
|
||||
"POST",
|
||||
f"/users/{admin}/tokens",
|
||||
auth=(admin, gitea.admin_password),
|
||||
json={"name": token_name, "scopes": ["read:user"]},
|
||||
)
|
||||
c["throwaway_token"] = token_name
|
||||
|
||||
# A generic package.
|
||||
c["pkg"] = pkg = f"sweep-pkg-{sfx}"
|
||||
upload = gitea.request(
|
||||
"PUT", f"/api/packages/{admin}/generic/{pkg}/1.0.0/payload.bin", content=b"package"
|
||||
)
|
||||
if upload.status_code != 201:
|
||||
raise HarnessError(f"seed package upload failed: HTTP {upload.status_code}")
|
||||
c["pkg_version"] = "1.0.0"
|
||||
c["pkg2"] = f"sweep-pkg2-{sfx}"
|
||||
upload = gitea.request(
|
||||
"PUT", f"/api/packages/{admin}/generic/{c['pkg2']}/1.0.0/payload.bin", content=b"package"
|
||||
)
|
||||
if upload.status_code != 201:
|
||||
raise HarnessError(f"seed package upload failed: HTTP {upload.status_code}")
|
||||
|
||||
# The workflow run created by the push of the workflow file (queued, no runner).
|
||||
c["run"], c["job_id"] = _find_run(s, base)
|
||||
c["notification"] = _find_notification(s, admin)
|
||||
return c
|
||||
|
||||
|
||||
def _project(s: Seeder, c: Ctx, scope: str, path: str) -> None:
|
||||
"""Create a project with two extra columns; store ids as ``<scope>_project/_column...``."""
|
||||
project = s.call("POST", path, json={"title": f"{scope} project"})["id"]
|
||||
c[f"{scope}_project"] = project
|
||||
ids: dict[str, int] = {}
|
||||
for title in ("todo", "done"):
|
||||
ids[title] = s.call("POST", f"{path}/{project}/columns", json={"title": title})["id"]
|
||||
c[f"{scope}_column"], c[f"{scope}_column2"] = ids["todo"], ids["done"]
|
||||
columns = s.call("GET", f"{path}/{project}/columns")
|
||||
c[f"{scope}_columns"] = [column["id"] for column in columns]
|
||||
|
||||
|
||||
def _find_run(s: Seeder, base: str) -> tuple[Any, Any]:
|
||||
"""Return ``(run id, job id)`` of the first workflow run, or ``(1, 1)`` if none appeared."""
|
||||
deadline = time.monotonic() + 10
|
||||
while time.monotonic() < deadline:
|
||||
runs = s.call("GET", f"{base}/actions/runs")
|
||||
entries = runs.get("workflow_runs") or []
|
||||
if entries:
|
||||
run_id = entries[0]["id"]
|
||||
jobs = s.call("GET", f"{base}/actions/runs/{run_id}/jobs")
|
||||
job_entries = jobs.get("jobs") or []
|
||||
if job_entries:
|
||||
return run_id, job_entries[0]["id"]
|
||||
time.sleep(0.5)
|
||||
return 1, 1
|
||||
|
||||
|
||||
def _find_notification(s: Seeder, admin: str) -> Any:
|
||||
"""Return the id of a notification of the admin if one exists, else 1."""
|
||||
deadline = time.monotonic() + 5
|
||||
while time.monotonic() < deadline:
|
||||
listing = s.call("GET", "/notifications", params={"all": "true"})
|
||||
if listing:
|
||||
return listing[0]["id"]
|
||||
time.sleep(0.5)
|
||||
return 1
|
||||
Reference in new issue
Block a user