ctfy.sdk.admin_resources.patches

client.admin.patches — the organiser's view of AWD+ defence.

 1"""``client.admin.patches`` — the organiser's view of AWD+ defence."""
 2
 3from __future__ import annotations
 4
 5from ctfy.sdk._helpers import _raise_for_status
 6from ctfy.sdk.base import BaseHttpClient
 7from ctfy.server.models import AdminPatchSubmissionDetail, AdminPatchSubmissionPage
 8
 9
10class AdminPatchesResource:
11    """Read the defence queue, and read one submission's code.
12
13    The two calls are split because the resource they spend differs.
14    :meth:`list` is a competition-wide page and never carries patch
15    bodies — an event accumulates those all week, so a listing that
16    shipped them would read every stored patch to render a table of
17    statuses. :meth:`get` does carry them, one row at a time, which is
18    what keeping the bytes was for: a disputed verdict, or a checker
19    bug found on day two, needs somebody to read what was submitted.
20    """
21
22    def __init__(self, http: BaseHttpClient) -> None:
23        self._http = http
24
25    def list(
26        self,
27        *,
28        competition_id: str = "",
29        team_id: str = "",
30        challenge_id: str = "",
31        status: str = "",
32        offset: int = 0,
33        limit: int = 50,
34    ) -> AdminPatchSubmissionPage:
35        """Defence submissions across every team, newest first.
36
37        ``total`` is counted separately from the page, so
38        ``status="pending"`` answers "is the queue draining" without
39        pulling the rows to find out.
40        """
41        params = {
42            "competition_id": competition_id,
43            "team_id": team_id,
44            "challenge_id": challenge_id,
45            "status": status,
46            "offset": offset,
47            "limit": limit,
48        }
49        resp = self._http.request(
50            "GET", "/admin/patches", params={k: v for k, v in params.items() if v not in ("", 0)}
51        )
52        _raise_for_status(resp)
53        return AdminPatchSubmissionPage.model_validate(resp.json())
54
55    def get(self, submission_id: str) -> AdminPatchSubmissionDetail:
56        """One submission with its code.
57
58        ``files_b64`` is base64 per path rather than text: a patch
59        target may legitimately be binary, and decoding it into a string
60        would corrupt it into something that merely looks like a bad
61        submission.
62        """
63        resp = self._http.request("GET", f"/admin/patches/{submission_id}")
64        _raise_for_status(resp)
65        return AdminPatchSubmissionDetail.model_validate(resp.json())
class AdminPatchesResource:
11class AdminPatchesResource:
12    """Read the defence queue, and read one submission's code.
13
14    The two calls are split because the resource they spend differs.
15    :meth:`list` is a competition-wide page and never carries patch
16    bodies — an event accumulates those all week, so a listing that
17    shipped them would read every stored patch to render a table of
18    statuses. :meth:`get` does carry them, one row at a time, which is
19    what keeping the bytes was for: a disputed verdict, or a checker
20    bug found on day two, needs somebody to read what was submitted.
21    """
22
23    def __init__(self, http: BaseHttpClient) -> None:
24        self._http = http
25
26    def list(
27        self,
28        *,
29        competition_id: str = "",
30        team_id: str = "",
31        challenge_id: str = "",
32        status: str = "",
33        offset: int = 0,
34        limit: int = 50,
35    ) -> AdminPatchSubmissionPage:
36        """Defence submissions across every team, newest first.
37
38        ``total`` is counted separately from the page, so
39        ``status="pending"`` answers "is the queue draining" without
40        pulling the rows to find out.
41        """
42        params = {
43            "competition_id": competition_id,
44            "team_id": team_id,
45            "challenge_id": challenge_id,
46            "status": status,
47            "offset": offset,
48            "limit": limit,
49        }
50        resp = self._http.request(
51            "GET", "/admin/patches", params={k: v for k, v in params.items() if v not in ("", 0)}
52        )
53        _raise_for_status(resp)
54        return AdminPatchSubmissionPage.model_validate(resp.json())
55
56    def get(self, submission_id: str) -> AdminPatchSubmissionDetail:
57        """One submission with its code.
58
59        ``files_b64`` is base64 per path rather than text: a patch
60        target may legitimately be binary, and decoding it into a string
61        would corrupt it into something that merely looks like a bad
62        submission.
63        """
64        resp = self._http.request("GET", f"/admin/patches/{submission_id}")
65        _raise_for_status(resp)
66        return AdminPatchSubmissionDetail.model_validate(resp.json())

Read the defence queue, and read one submission's code.

The two calls are split because the resource they spend differs. list() is a competition-wide page and never carries patch bodies — an event accumulates those all week, so a listing that shipped them would read every stored patch to render a table of statuses. get() does carry them, one row at a time, which is what keeping the bytes was for: a disputed verdict, or a checker bug found on day two, needs somebody to read what was submitted.

AdminPatchesResource(http: ctfy.sdk.base.BaseHttpClient)
23    def __init__(self, http: BaseHttpClient) -> None:
24        self._http = http
def list( self, *, competition_id: str = '', team_id: str = '', challenge_id: str = '', status: str = '', offset: int = 0, limit: int = 50) -> ctfy.server.models.AdminPatchSubmissionPage:
26    def list(
27        self,
28        *,
29        competition_id: str = "",
30        team_id: str = "",
31        challenge_id: str = "",
32        status: str = "",
33        offset: int = 0,
34        limit: int = 50,
35    ) -> AdminPatchSubmissionPage:
36        """Defence submissions across every team, newest first.
37
38        ``total`` is counted separately from the page, so
39        ``status="pending"`` answers "is the queue draining" without
40        pulling the rows to find out.
41        """
42        params = {
43            "competition_id": competition_id,
44            "team_id": team_id,
45            "challenge_id": challenge_id,
46            "status": status,
47            "offset": offset,
48            "limit": limit,
49        }
50        resp = self._http.request(
51            "GET", "/admin/patches", params={k: v for k, v in params.items() if v not in ("", 0)}
52        )
53        _raise_for_status(resp)
54        return AdminPatchSubmissionPage.model_validate(resp.json())

Defence submissions across every team, newest first.

total is counted separately from the page, so status="pending" answers "is the queue draining" without pulling the rows to find out.

def get( self, submission_id: str) -> ctfy.server.models.AdminPatchSubmissionDetail:
56    def get(self, submission_id: str) -> AdminPatchSubmissionDetail:
57        """One submission with its code.
58
59        ``files_b64`` is base64 per path rather than text: a patch
60        target may legitimately be binary, and decoding it into a string
61        would corrupt it into something that merely looks like a bad
62        submission.
63        """
64        resp = self._http.request("GET", f"/admin/patches/{submission_id}")
65        _raise_for_status(resp)
66        return AdminPatchSubmissionDetail.model_validate(resp.json())

One submission with its code.

files_b64 is base64 per path rather than text: a patch target may legitimately be binary, and decoding it into a string would corrupt it into something that merely looks like a bad submission.