ctfy.sdk.resources.submissions
client.submissions — graded submit, oracle verify, QA challenges.
1"""``client.submissions`` — graded submit, oracle verify, QA challenges.""" 2 3from __future__ import annotations 4 5import builtins 6from typing import Any 7 8from ctfy.core.state.models import SubmissionState 9from ctfy.sdk._helpers import _extract_items, _raise_for_status 10from ctfy.sdk.base import BaseHttpClient 11from ctfy.server.models import QaChallengeView, SubmissionResponse, VerifyAnswerResponse 12 13 14class SubmissionsResource: 15 """Answer submission (graded), oracle verification (no record), and QA.""" 16 17 def __init__(self, http: BaseHttpClient) -> None: 18 self._http = http 19 20 def submit( 21 self, 22 instance_id: str, 23 answer: str | builtins.list[str], 24 *, 25 question_id: str = "flag", 26 ) -> SubmissionResponse: 27 """Submit ``answer`` against ``question_id`` on ``instance_id``. 28 29 The server reads challenge_id, team_id and competition_id off 30 the instance row; the calling user must be on the same team 31 that started this instance (in the same competition). 32 33 ``question_id`` defaults to ``"flag"`` for single-question 34 challenges (the most common case). Multi-question challenges 35 (see :class:`ChallengeInfo.questions`) require an explicit id. 36 ``answer`` is a single string for dynamic / static / 37 single_select questions and a list of strings for multi_select. 38 """ 39 resp = self._http.request( 40 "POST", 41 "/submissions", 42 json={ 43 "instance_id": instance_id, 44 "question_id": question_id, 45 "answer": answer, 46 }, 47 ) 48 _raise_for_status(resp) 49 return SubmissionResponse.model_validate(resp.json()) 50 51 def verify( 52 self, 53 instance_id: str, 54 answer: str | builtins.list[str], 55 *, 56 question_id: str = "flag", 57 ) -> VerifyAnswerResponse: 58 """Server-side answer verification — no submission record. 59 60 Same grader as :meth:`submit` (mode-aware, 61 ``$ANSWER_<UPPER_ID>`` tolerance for dynamic, set-equal for 62 ``multi_select``, etc.) but writes nothing to the audit log. 63 Use for oracle-style probes; for graded competitions use 64 :meth:`submit`. 65 66 Returns :class:`VerifyAnswerResponse` with ``.correct`` and 67 ``.question_id`` (the latter echoed on a correct match). 68 """ 69 resp = self._http.request( 70 "POST", 71 f"/instances/{instance_id}/verify-answer", 72 json={"question_id": question_id, "answer": answer}, 73 ) 74 _raise_for_status(resp) 75 return VerifyAnswerResponse.model_validate(resp.json()) 76 77 def list( 78 self, challenge_id: str = "", offset: int = 0, limit: int = 50 79 ) -> builtins.list[SubmissionState]: 80 """List current team's submissions.""" 81 params: dict[str, Any] = {"offset": offset, "limit": limit} 82 if challenge_id: 83 params["challenge_id"] = challenge_id 84 resp = self._http.request("GET", "/submissions", params=params) 85 _raise_for_status(resp) 86 return _extract_items(resp.json(), SubmissionState) 87 88 def submit_qa( 89 self, 90 challenge_id: str, 91 competition_id: str, 92 answer: str | builtins.list[str], 93 *, 94 question_id: str = "answer", 95 ) -> SubmissionResponse: 96 """Submit ``answer`` against a pure question-answer challenge. 97 98 Unlike :meth:`submit`, QA challenges have no instance — the 99 caller passes ``challenge_id`` + ``competition_id`` explicitly, 100 and the server resolves the team for the competition via the 101 standard auth helper. ``question_id`` defaults to ``"answer"`` 102 (the canonical id for MCQ items produced by ``ctfy-admin 103 challenge import``). 104 """ 105 resp = self._http.request( 106 "POST", 107 "/qa/submissions", 108 json={ 109 "challenge_id": challenge_id, 110 "competition_id": competition_id, 111 "question_id": question_id, 112 "answer": answer, 113 }, 114 ) 115 _raise_for_status(resp) 116 return SubmissionResponse.model_validate(resp.json()) 117 118 def list_qa_challenges(self, competition_id: str) -> builtins.list[QaChallengeView]: 119 """All QA challenges in scope of one competition + team status. 120 121 Returns one :class:`QaChallengeView` per challenge with the 122 calling team's per-question solve / wrong-attempt state 123 pre-joined. The quiz UI renders the full surface from this 124 single response. 125 """ 126 resp = self._http.request( 127 "GET", "/qa/challenges", params={"competition_id": competition_id} 128 ) 129 _raise_for_status(resp) 130 data = resp.json() 131 return [QaChallengeView.model_validate(item) for item in data]
15class SubmissionsResource: 16 """Answer submission (graded), oracle verification (no record), and QA.""" 17 18 def __init__(self, http: BaseHttpClient) -> None: 19 self._http = http 20 21 def submit( 22 self, 23 instance_id: str, 24 answer: str | builtins.list[str], 25 *, 26 question_id: str = "flag", 27 ) -> SubmissionResponse: 28 """Submit ``answer`` against ``question_id`` on ``instance_id``. 29 30 The server reads challenge_id, team_id and competition_id off 31 the instance row; the calling user must be on the same team 32 that started this instance (in the same competition). 33 34 ``question_id`` defaults to ``"flag"`` for single-question 35 challenges (the most common case). Multi-question challenges 36 (see :class:`ChallengeInfo.questions`) require an explicit id. 37 ``answer`` is a single string for dynamic / static / 38 single_select questions and a list of strings for multi_select. 39 """ 40 resp = self._http.request( 41 "POST", 42 "/submissions", 43 json={ 44 "instance_id": instance_id, 45 "question_id": question_id, 46 "answer": answer, 47 }, 48 ) 49 _raise_for_status(resp) 50 return SubmissionResponse.model_validate(resp.json()) 51 52 def verify( 53 self, 54 instance_id: str, 55 answer: str | builtins.list[str], 56 *, 57 question_id: str = "flag", 58 ) -> VerifyAnswerResponse: 59 """Server-side answer verification — no submission record. 60 61 Same grader as :meth:`submit` (mode-aware, 62 ``$ANSWER_<UPPER_ID>`` tolerance for dynamic, set-equal for 63 ``multi_select``, etc.) but writes nothing to the audit log. 64 Use for oracle-style probes; for graded competitions use 65 :meth:`submit`. 66 67 Returns :class:`VerifyAnswerResponse` with ``.correct`` and 68 ``.question_id`` (the latter echoed on a correct match). 69 """ 70 resp = self._http.request( 71 "POST", 72 f"/instances/{instance_id}/verify-answer", 73 json={"question_id": question_id, "answer": answer}, 74 ) 75 _raise_for_status(resp) 76 return VerifyAnswerResponse.model_validate(resp.json()) 77 78 def list( 79 self, challenge_id: str = "", offset: int = 0, limit: int = 50 80 ) -> builtins.list[SubmissionState]: 81 """List current team's submissions.""" 82 params: dict[str, Any] = {"offset": offset, "limit": limit} 83 if challenge_id: 84 params["challenge_id"] = challenge_id 85 resp = self._http.request("GET", "/submissions", params=params) 86 _raise_for_status(resp) 87 return _extract_items(resp.json(), SubmissionState) 88 89 def submit_qa( 90 self, 91 challenge_id: str, 92 competition_id: str, 93 answer: str | builtins.list[str], 94 *, 95 question_id: str = "answer", 96 ) -> SubmissionResponse: 97 """Submit ``answer`` against a pure question-answer challenge. 98 99 Unlike :meth:`submit`, QA challenges have no instance — the 100 caller passes ``challenge_id`` + ``competition_id`` explicitly, 101 and the server resolves the team for the competition via the 102 standard auth helper. ``question_id`` defaults to ``"answer"`` 103 (the canonical id for MCQ items produced by ``ctfy-admin 104 challenge import``). 105 """ 106 resp = self._http.request( 107 "POST", 108 "/qa/submissions", 109 json={ 110 "challenge_id": challenge_id, 111 "competition_id": competition_id, 112 "question_id": question_id, 113 "answer": answer, 114 }, 115 ) 116 _raise_for_status(resp) 117 return SubmissionResponse.model_validate(resp.json()) 118 119 def list_qa_challenges(self, competition_id: str) -> builtins.list[QaChallengeView]: 120 """All QA challenges in scope of one competition + team status. 121 122 Returns one :class:`QaChallengeView` per challenge with the 123 calling team's per-question solve / wrong-attempt state 124 pre-joined. The quiz UI renders the full surface from this 125 single response. 126 """ 127 resp = self._http.request( 128 "GET", "/qa/challenges", params={"competition_id": competition_id} 129 ) 130 _raise_for_status(resp) 131 data = resp.json() 132 return [QaChallengeView.model_validate(item) for item in data]
Answer submission (graded), oracle verification (no record), and QA.
21 def submit( 22 self, 23 instance_id: str, 24 answer: str | builtins.list[str], 25 *, 26 question_id: str = "flag", 27 ) -> SubmissionResponse: 28 """Submit ``answer`` against ``question_id`` on ``instance_id``. 29 30 The server reads challenge_id, team_id and competition_id off 31 the instance row; the calling user must be on the same team 32 that started this instance (in the same competition). 33 34 ``question_id`` defaults to ``"flag"`` for single-question 35 challenges (the most common case). Multi-question challenges 36 (see :class:`ChallengeInfo.questions`) require an explicit id. 37 ``answer`` is a single string for dynamic / static / 38 single_select questions and a list of strings for multi_select. 39 """ 40 resp = self._http.request( 41 "POST", 42 "/submissions", 43 json={ 44 "instance_id": instance_id, 45 "question_id": question_id, 46 "answer": answer, 47 }, 48 ) 49 _raise_for_status(resp) 50 return SubmissionResponse.model_validate(resp.json())
Submit answer against question_id on instance_id.
The server reads challenge_id, team_id and competition_id off the instance row; the calling user must be on the same team that started this instance (in the same competition).
question_id defaults to "flag" for single-question
challenges (the most common case). Multi-question challenges
(see ChallengeInfo.questions) require an explicit id.
answer is a single string for dynamic / static /
single_select questions and a list of strings for multi_select.
52 def verify( 53 self, 54 instance_id: str, 55 answer: str | builtins.list[str], 56 *, 57 question_id: str = "flag", 58 ) -> VerifyAnswerResponse: 59 """Server-side answer verification — no submission record. 60 61 Same grader as :meth:`submit` (mode-aware, 62 ``$ANSWER_<UPPER_ID>`` tolerance for dynamic, set-equal for 63 ``multi_select``, etc.) but writes nothing to the audit log. 64 Use for oracle-style probes; for graded competitions use 65 :meth:`submit`. 66 67 Returns :class:`VerifyAnswerResponse` with ``.correct`` and 68 ``.question_id`` (the latter echoed on a correct match). 69 """ 70 resp = self._http.request( 71 "POST", 72 f"/instances/{instance_id}/verify-answer", 73 json={"question_id": question_id, "answer": answer}, 74 ) 75 _raise_for_status(resp) 76 return VerifyAnswerResponse.model_validate(resp.json())
Server-side answer verification — no submission record.
Same grader as submit() (mode-aware,
$ANSWER_<UPPER_ID> tolerance for dynamic, set-equal for
multi_select, etc.) but writes nothing to the audit log.
Use for oracle-style probes; for graded competitions use
submit().
Returns VerifyAnswerResponse with .correct and
.question_id (the latter echoed on a correct match).
78 def list( 79 self, challenge_id: str = "", offset: int = 0, limit: int = 50 80 ) -> builtins.list[SubmissionState]: 81 """List current team's submissions.""" 82 params: dict[str, Any] = {"offset": offset, "limit": limit} 83 if challenge_id: 84 params["challenge_id"] = challenge_id 85 resp = self._http.request("GET", "/submissions", params=params) 86 _raise_for_status(resp) 87 return _extract_items(resp.json(), SubmissionState)
List current team's submissions.
89 def submit_qa( 90 self, 91 challenge_id: str, 92 competition_id: str, 93 answer: str | builtins.list[str], 94 *, 95 question_id: str = "answer", 96 ) -> SubmissionResponse: 97 """Submit ``answer`` against a pure question-answer challenge. 98 99 Unlike :meth:`submit`, QA challenges have no instance — the 100 caller passes ``challenge_id`` + ``competition_id`` explicitly, 101 and the server resolves the team for the competition via the 102 standard auth helper. ``question_id`` defaults to ``"answer"`` 103 (the canonical id for MCQ items produced by ``ctfy-admin 104 challenge import``). 105 """ 106 resp = self._http.request( 107 "POST", 108 "/qa/submissions", 109 json={ 110 "challenge_id": challenge_id, 111 "competition_id": competition_id, 112 "question_id": question_id, 113 "answer": answer, 114 }, 115 ) 116 _raise_for_status(resp) 117 return SubmissionResponse.model_validate(resp.json())
Submit answer against a pure question-answer challenge.
Unlike submit(), QA challenges have no instance — the
caller passes challenge_id + competition_id explicitly,
and the server resolves the team for the competition via the
standard auth helper. question_id defaults to "answer"
(the canonical id for MCQ items produced by ctfy-admin
challenge import).
119 def list_qa_challenges(self, competition_id: str) -> builtins.list[QaChallengeView]: 120 """All QA challenges in scope of one competition + team status. 121 122 Returns one :class:`QaChallengeView` per challenge with the 123 calling team's per-question solve / wrong-attempt state 124 pre-joined. The quiz UI renders the full surface from this 125 single response. 126 """ 127 resp = self._http.request( 128 "GET", "/qa/challenges", params={"competition_id": competition_id} 129 ) 130 _raise_for_status(resp) 131 data = resp.json() 132 return [QaChallengeView.model_validate(item) for item in data]
All QA challenges in scope of one competition + team status.
Returns one QaChallengeView per challenge with the
calling team's per-question solve / wrong-attempt state
pre-joined. The quiz UI renders the full surface from this
single response.