speechwire-mcp 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- speechwire_mcp/__init__.py +37 -0
- speechwire_mcp/__main__.py +5 -0
- speechwire_mcp/client.py +411 -0
- speechwire_mcp/judges/__init__.py +23 -0
- speechwire_mcp/judges/client.py +424 -0
- speechwire_mcp/judges/parsers.py +533 -0
- speechwire_mcp/login/__init__.py +6 -0
- speechwire_mcp/login/client.py +65 -0
- speechwire_mcp/login/parsers.py +189 -0
- speechwire_mcp/parsing_helpers.py +75 -0
- speechwire_mcp/results/__init__.py +3 -0
- speechwire_mcp/results/client.py +48 -0
- speechwire_mcp/results/parsers.py +310 -0
- speechwire_mcp/rooms/__init__.py +11 -0
- speechwire_mcp/rooms/client.py +83 -0
- speechwire_mcp/rooms/parsers.py +372 -0
- speechwire_mcp/schematics/__init__.py +6 -0
- speechwire_mcp/schematics/client.py +49 -0
- speechwire_mcp/schematics/parsers.py +243 -0
- speechwire_mcp/server.py +802 -0
- speechwire_mcp/structure/__init__.py +9 -0
- speechwire_mcp/structure/client.py +61 -0
- speechwire_mcp/structure/parsers.py +249 -0
- speechwire_mcp/teams/__init__.py +7 -0
- speechwire_mcp/teams/client.py +87 -0
- speechwire_mcp/teams/parsers.py +395 -0
- speechwire_mcp-0.1.0.dist-info/METADATA +93 -0
- speechwire_mcp-0.1.0.dist-info/RECORD +31 -0
- speechwire_mcp-0.1.0.dist-info/WHEEL +4 -0
- speechwire_mcp-0.1.0.dist-info/entry_points.txt +2 -0
- speechwire_mcp-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""speechwire-mcp: Model Context Protocol server for SpeechWire tournament management."""
|
|
2
|
+
|
|
3
|
+
from speechwire_mcp.client import SpeechWireClient, SpeechWireAuthError
|
|
4
|
+
from speechwire_mcp.judges import (
|
|
5
|
+
get_judge_list,
|
|
6
|
+
get_judge_contact,
|
|
7
|
+
get_judge_availability,
|
|
8
|
+
get_judge_school,
|
|
9
|
+
)
|
|
10
|
+
from speechwire_mcp.login import get_accounts, get_tournaments
|
|
11
|
+
from speechwire_mcp.rooms import get_room_counts, get_room_list, get_room_usage
|
|
12
|
+
from speechwire_mcp.schematics import get_schematic_events, get_round_schematic
|
|
13
|
+
from speechwire_mcp.structure import get_groupings, get_timeslots
|
|
14
|
+
from speechwire_mcp.teams import get_team_list, get_team_entries, get_hybrid_entries
|
|
15
|
+
from speechwire_mcp.results import get_tab_sheet
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"SpeechWireClient",
|
|
19
|
+
"SpeechWireAuthError",
|
|
20
|
+
"get_judge_list",
|
|
21
|
+
"get_judge_contact",
|
|
22
|
+
"get_judge_availability",
|
|
23
|
+
"get_judge_school",
|
|
24
|
+
"get_accounts",
|
|
25
|
+
"get_tournaments",
|
|
26
|
+
"get_room_counts",
|
|
27
|
+
"get_room_list",
|
|
28
|
+
"get_room_usage",
|
|
29
|
+
"get_schematic_events",
|
|
30
|
+
"get_round_schematic",
|
|
31
|
+
"get_groupings",
|
|
32
|
+
"get_timeslots",
|
|
33
|
+
"get_team_list",
|
|
34
|
+
"get_team_entries",
|
|
35
|
+
"get_hybrid_entries",
|
|
36
|
+
"get_tab_sheet",
|
|
37
|
+
]
|
speechwire_mcp/client.py
ADDED
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import TypeVar, Callable
|
|
3
|
+
import logging
|
|
4
|
+
import requests
|
|
5
|
+
from bs4 import BeautifulSoup
|
|
6
|
+
from requests.exceptions import RequestException
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
T = TypeVar("T")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ClientState(Enum):
|
|
14
|
+
"""Authentication state machine for the SpeechWire client."""
|
|
15
|
+
|
|
16
|
+
UNAUTHENTICATED = "unauthenticated"
|
|
17
|
+
LOGGED_IN = "logged_in"
|
|
18
|
+
ACCOUNT_SELECTED = "account_selected"
|
|
19
|
+
TOURNAMENT_ACTIVE = "tournament_active"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SpeechWireAuthError(Exception):
|
|
23
|
+
"""Raised when authentication fails (bad credentials, missing pages, etc.)."""
|
|
24
|
+
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SpeechWireSelectionRequired(Exception):
|
|
29
|
+
"""Raised when multiple options are available and user must choose.
|
|
30
|
+
|
|
31
|
+
Parameters
|
|
32
|
+
----------
|
|
33
|
+
message : str
|
|
34
|
+
Human-readable explanation.
|
|
35
|
+
options : list[dict]
|
|
36
|
+
Available choices the caller should present to the user.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, message: str, options: list[dict]):
|
|
40
|
+
super().__init__(message)
|
|
41
|
+
self.options = options
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class SpeechWireClient:
|
|
45
|
+
"""Authenticated HTTP client for the SpeechWire tournament management system.
|
|
46
|
+
|
|
47
|
+
Handles a phased authentication flow (login → account → tournament) with
|
|
48
|
+
automatic session renewal on expiry. Account, circuit, and tournament IDs
|
|
49
|
+
are optional — when omitted the client enters discovery mode and can
|
|
50
|
+
auto-select when exactly one option exists.
|
|
51
|
+
|
|
52
|
+
Parameters
|
|
53
|
+
----------
|
|
54
|
+
email : str
|
|
55
|
+
SpeechWire account email.
|
|
56
|
+
password : str
|
|
57
|
+
SpeechWire account password.
|
|
58
|
+
account_id : str | None
|
|
59
|
+
Numeric account identifier. Discovered if omitted.
|
|
60
|
+
circuit_id : str | None
|
|
61
|
+
Numeric circuit identifier. Discovered if omitted.
|
|
62
|
+
tournament_id : str | None
|
|
63
|
+
Numeric tournament identifier. Discovered if omitted.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
email: str,
|
|
69
|
+
password: str,
|
|
70
|
+
account_id: str | None = None,
|
|
71
|
+
circuit_id: str | None = None,
|
|
72
|
+
tournament_id: str | None = None,
|
|
73
|
+
):
|
|
74
|
+
self.email = email
|
|
75
|
+
self.password = password
|
|
76
|
+
self.account_id = account_id
|
|
77
|
+
self.circuit_id = circuit_id
|
|
78
|
+
self.tournament_id = tournament_id
|
|
79
|
+
self.session = requests.Session()
|
|
80
|
+
self.state = ClientState.UNAUTHENTICATED
|
|
81
|
+
self._reauthenticating = False
|
|
82
|
+
|
|
83
|
+
# ------------------------------------------------------------------
|
|
84
|
+
# Phased auth methods
|
|
85
|
+
# ------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
def login(self) -> None:
|
|
88
|
+
"""Phase A — POST credentials and establish a logged-in session.
|
|
89
|
+
|
|
90
|
+
Transitions state to ``LOGGED_IN``.
|
|
91
|
+
"""
|
|
92
|
+
try:
|
|
93
|
+
self.session.cookies.clear()
|
|
94
|
+
self.state = ClientState.UNAUTHENTICATED
|
|
95
|
+
|
|
96
|
+
resp = self.session.post(
|
|
97
|
+
"https://www.speechwire.com/c-login.php",
|
|
98
|
+
data={
|
|
99
|
+
"teamemail": self.email,
|
|
100
|
+
"password": self.password,
|
|
101
|
+
"mode": "account",
|
|
102
|
+
"tournid": "",
|
|
103
|
+
"Submit": "Log in",
|
|
104
|
+
},
|
|
105
|
+
)
|
|
106
|
+
resp.raise_for_status()
|
|
107
|
+
body_lower = resp.text.lower()
|
|
108
|
+
if "incorrect" in body_lower or "invalid" in body_lower:
|
|
109
|
+
raise SpeechWireAuthError(
|
|
110
|
+
"Login rejected — SpeechWire reported invalid credentials"
|
|
111
|
+
)
|
|
112
|
+
self.state = ClientState.LOGGED_IN
|
|
113
|
+
logger.debug("Logged in as %s", self.email)
|
|
114
|
+
except RequestException as e:
|
|
115
|
+
raise SpeechWireAuthError(f"Login failed: {e}") from e
|
|
116
|
+
|
|
117
|
+
def select_account(self, account_id: str | int) -> None:
|
|
118
|
+
"""Phase B — select an account by its numeric ID.
|
|
119
|
+
|
|
120
|
+
Parameters
|
|
121
|
+
----------
|
|
122
|
+
account_id : str | int
|
|
123
|
+
The ``selectaccountid`` value from the account-select page.
|
|
124
|
+
|
|
125
|
+
Transitions state to ``ACCOUNT_SELECTED`` and stores the chosen ID.
|
|
126
|
+
"""
|
|
127
|
+
try:
|
|
128
|
+
resp = self.session.get(
|
|
129
|
+
"https://www.speechwire.com/c-account-select.php",
|
|
130
|
+
params={"selectaccountid": str(account_id)},
|
|
131
|
+
)
|
|
132
|
+
resp.raise_for_status()
|
|
133
|
+
self.account_id = str(account_id)
|
|
134
|
+
self.state = ClientState.ACCOUNT_SELECTED
|
|
135
|
+
logger.info("Selected account %s", account_id)
|
|
136
|
+
except RequestException as e:
|
|
137
|
+
raise SpeechWireAuthError(f"Account selection failed: {e}") from e
|
|
138
|
+
|
|
139
|
+
def select_tournament(self, tournament_id: str | int, circuit_id: str | int) -> None:
|
|
140
|
+
"""Phase C — select a tournament and activate the session.
|
|
141
|
+
|
|
142
|
+
Parameters
|
|
143
|
+
----------
|
|
144
|
+
tournament_id : str | int
|
|
145
|
+
Numeric tournament identifier.
|
|
146
|
+
circuit_id : str | int
|
|
147
|
+
Numeric circuit identifier.
|
|
148
|
+
|
|
149
|
+
Transitions state to ``TOURNAMENT_ACTIVE`` and stores the chosen IDs.
|
|
150
|
+
"""
|
|
151
|
+
try:
|
|
152
|
+
resp = self.session.post(
|
|
153
|
+
"https://www.speechwire.com/c-circuit-tournaments.php",
|
|
154
|
+
data={
|
|
155
|
+
"tournid": str(tournament_id),
|
|
156
|
+
"Submit": "Log in",
|
|
157
|
+
"mode": "tournjump",
|
|
158
|
+
"circuitid": str(circuit_id),
|
|
159
|
+
},
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Activate tournament session via the redirect link
|
|
163
|
+
soup = BeautifulSoup(resp.text, "html.parser")
|
|
164
|
+
link = soup.find(
|
|
165
|
+
"a",
|
|
166
|
+
href=lambda x: x and "njc=" in x and f"tournid={tournament_id}" in x,
|
|
167
|
+
)
|
|
168
|
+
if not link:
|
|
169
|
+
raise SpeechWireAuthError("Could not find tournament activation link")
|
|
170
|
+
|
|
171
|
+
self.session.get(
|
|
172
|
+
link["href"],
|
|
173
|
+
headers={"Referer": "https://www.speechwire.com/"},
|
|
174
|
+
)
|
|
175
|
+
self.tournament_id = str(tournament_id)
|
|
176
|
+
self.circuit_id = str(circuit_id)
|
|
177
|
+
self.state = ClientState.TOURNAMENT_ACTIVE
|
|
178
|
+
logger.info(
|
|
179
|
+
"Activated tournament %s (circuit %s)",
|
|
180
|
+
tournament_id,
|
|
181
|
+
circuit_id,
|
|
182
|
+
)
|
|
183
|
+
except RequestException as e:
|
|
184
|
+
raise SpeechWireAuthError(f"Tournament selection failed: {e}") from e
|
|
185
|
+
|
|
186
|
+
# ------------------------------------------------------------------
|
|
187
|
+
# Discovery helpers
|
|
188
|
+
# ------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
def discover_accounts(self) -> list[dict]:
|
|
191
|
+
"""Fetch the list of accounts available after login.
|
|
192
|
+
|
|
193
|
+
Returns
|
|
194
|
+
-------
|
|
195
|
+
list[dict]
|
|
196
|
+
Each dict has ``account_id`` (int) and ``name`` (str).
|
|
197
|
+
"""
|
|
198
|
+
from speechwire_mcp.login.parsers import parse_account_list_html
|
|
199
|
+
|
|
200
|
+
resp = self.get("https://www.speechwire.com/c-account-select.php")
|
|
201
|
+
return parse_account_list_html(resp.text)
|
|
202
|
+
|
|
203
|
+
def discover_tournaments(self) -> list[dict]:
|
|
204
|
+
"""Fetch the list of tournaments for the selected account.
|
|
205
|
+
|
|
206
|
+
Returns
|
|
207
|
+
-------
|
|
208
|
+
list[dict]
|
|
209
|
+
Each dict has ``tournament_id``, ``circuit_id``, ``name``,
|
|
210
|
+
and optionally ``date``.
|
|
211
|
+
"""
|
|
212
|
+
from speechwire_mcp.login.parsers import parse_tournament_list_html
|
|
213
|
+
|
|
214
|
+
resp = self.get("https://www.speechwire.com/c-circuit-tournaments.php")
|
|
215
|
+
return parse_tournament_list_html(resp.text)
|
|
216
|
+
|
|
217
|
+
# ------------------------------------------------------------------
|
|
218
|
+
# Auto-select private helpers
|
|
219
|
+
# ------------------------------------------------------------------
|
|
220
|
+
|
|
221
|
+
def _auto_select_account(self) -> None:
|
|
222
|
+
"""Discover accounts and auto-select if exactly one exists."""
|
|
223
|
+
accounts = self.discover_accounts()
|
|
224
|
+
if len(accounts) == 1:
|
|
225
|
+
self.select_account(accounts[0]["account_id"])
|
|
226
|
+
logger.info("Auto-selected sole account: %s", accounts[0].get("name"))
|
|
227
|
+
elif len(accounts) == 0:
|
|
228
|
+
raise SpeechWireAuthError("No accounts found for this login. Verify your credentials.")
|
|
229
|
+
else:
|
|
230
|
+
raise SpeechWireSelectionRequired(
|
|
231
|
+
"Multiple accounts available — please select one.",
|
|
232
|
+
options=accounts,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
# ------------------------------------------------------------------
|
|
237
|
+
# Convenience wrappers
|
|
238
|
+
# ------------------------------------------------------------------
|
|
239
|
+
|
|
240
|
+
def ensure_tournament_session(self) -> None:
|
|
241
|
+
"""Reach ``TOURNAMENT_ACTIVE`` state, auto-discovering where possible.
|
|
242
|
+
|
|
243
|
+
If IDs were provided up front this behaves like the original
|
|
244
|
+
``_authenticate()``. Otherwise it discovers and auto-selects when
|
|
245
|
+
a single option exists, raising ``SpeechWireSelectionRequired``
|
|
246
|
+
when the caller must choose.
|
|
247
|
+
"""
|
|
248
|
+
if self.state == ClientState.TOURNAMENT_ACTIVE:
|
|
249
|
+
return
|
|
250
|
+
self._authenticate()
|
|
251
|
+
|
|
252
|
+
def _authenticate(self) -> None:
|
|
253
|
+
"""Full authentication convenience wrapper.
|
|
254
|
+
|
|
255
|
+
Logs in, selects account (by ID or auto-discover), then selects
|
|
256
|
+
tournament (by ID or auto-discover).
|
|
257
|
+
"""
|
|
258
|
+
self.login()
|
|
259
|
+
|
|
260
|
+
if self.account_id:
|
|
261
|
+
self.select_account(self.account_id)
|
|
262
|
+
else:
|
|
263
|
+
self._auto_select_account()
|
|
264
|
+
|
|
265
|
+
if self.tournament_id and self.circuit_id:
|
|
266
|
+
self.select_tournament(self.tournament_id, self.circuit_id)
|
|
267
|
+
|
|
268
|
+
# ------------------------------------------------------------------
|
|
269
|
+
# HTTP helpers
|
|
270
|
+
# ------------------------------------------------------------------
|
|
271
|
+
|
|
272
|
+
def _looks_like_expired_session(self, resp: requests.Response) -> bool:
|
|
273
|
+
"""Detect whether a response indicates the session has expired.
|
|
274
|
+
|
|
275
|
+
Checks for two indicators:
|
|
276
|
+
- Password input field (login page marker)
|
|
277
|
+
- "Select the tournament" text, but only when the URL is NOT the
|
|
278
|
+
tournament discovery page where that text appears legitimately
|
|
279
|
+
"""
|
|
280
|
+
if 'type="password"' in resp.text or "type='password'" in resp.text:
|
|
281
|
+
return True
|
|
282
|
+
if "Select the tournament" in resp.text and "c-circuit-tournaments.php" not in resp.url:
|
|
283
|
+
return True
|
|
284
|
+
return False
|
|
285
|
+
|
|
286
|
+
def get(self, url: str, params: dict | None = None) -> requests.Response:
|
|
287
|
+
"""GET a manage.speechwire.com page with session-expiry handling.
|
|
288
|
+
|
|
289
|
+
If the response looks like an expired session (login page redirect
|
|
290
|
+
or unexpected tournament-selection text), the client re-authenticates
|
|
291
|
+
and retries the request once. A guard prevents infinite recursion
|
|
292
|
+
when ``_authenticate`` itself issues GETs (e.g. account discovery).
|
|
293
|
+
"""
|
|
294
|
+
resp = self.session.get(url, params=params)
|
|
295
|
+
if self._looks_like_expired_session(resp) and not self._reauthenticating:
|
|
296
|
+
self._reauthenticating = True
|
|
297
|
+
try:
|
|
298
|
+
self._authenticate()
|
|
299
|
+
finally:
|
|
300
|
+
self._reauthenticating = False
|
|
301
|
+
resp = self.session.get(url, params=params)
|
|
302
|
+
return resp
|
|
303
|
+
|
|
304
|
+
def post(self, url: str, data: dict) -> requests.Response:
|
|
305
|
+
"""POST form data with session-expiry handling.
|
|
306
|
+
|
|
307
|
+
If the response looks like an expired session, re-authenticates
|
|
308
|
+
and retries the POST once.
|
|
309
|
+
|
|
310
|
+
Parameters
|
|
311
|
+
----------
|
|
312
|
+
url : str
|
|
313
|
+
Target URL on manage.speechwire.com.
|
|
314
|
+
data : dict
|
|
315
|
+
Form data to submit.
|
|
316
|
+
"""
|
|
317
|
+
resp = self.session.post(url, data=data)
|
|
318
|
+
if self._looks_like_expired_session(resp) and not self._reauthenticating:
|
|
319
|
+
self._reauthenticating = True
|
|
320
|
+
try:
|
|
321
|
+
self._authenticate()
|
|
322
|
+
finally:
|
|
323
|
+
self._reauthenticating = False
|
|
324
|
+
resp = self.session.post(url, data=data)
|
|
325
|
+
return resp
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _fetch_and_parse(
|
|
329
|
+
client: SpeechWireClient | None,
|
|
330
|
+
url: str,
|
|
331
|
+
parser: Callable[[str], T],
|
|
332
|
+
default: T,
|
|
333
|
+
context: str = "",
|
|
334
|
+
params: dict | None = None,
|
|
335
|
+
) -> T:
|
|
336
|
+
"""Fetch a page and parse it, returning *default* on any failure.
|
|
337
|
+
|
|
338
|
+
Parameters
|
|
339
|
+
----------
|
|
340
|
+
client : SpeechWireClient | None
|
|
341
|
+
The authenticated client instance.
|
|
342
|
+
url : str
|
|
343
|
+
URL to fetch.
|
|
344
|
+
parser : Callable[[str], T]
|
|
345
|
+
Pure function that converts HTML text into the desired data shape.
|
|
346
|
+
default : T
|
|
347
|
+
Value returned when fetching or parsing fails.
|
|
348
|
+
context : str
|
|
349
|
+
Human-readable label for log messages.
|
|
350
|
+
params : dict | None
|
|
351
|
+
Optional query parameters passed to ``requests.Session.get``.
|
|
352
|
+
"""
|
|
353
|
+
if not client:
|
|
354
|
+
logger.error("No SpeechWire client provided")
|
|
355
|
+
return default
|
|
356
|
+
|
|
357
|
+
try:
|
|
358
|
+
resp = client.get(url, params=params)
|
|
359
|
+
resp.raise_for_status()
|
|
360
|
+
except Exception:
|
|
361
|
+
logger.error("Failed to fetch %s", context)
|
|
362
|
+
return default
|
|
363
|
+
|
|
364
|
+
try:
|
|
365
|
+
return parser(resp.text)
|
|
366
|
+
except Exception:
|
|
367
|
+
logger.error("Failed to parse %s", context)
|
|
368
|
+
return default
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _post_and_parse(
|
|
372
|
+
client: SpeechWireClient | None,
|
|
373
|
+
url: str,
|
|
374
|
+
data: dict,
|
|
375
|
+
parser: Callable[[str], T],
|
|
376
|
+
default: T,
|
|
377
|
+
context: str = "",
|
|
378
|
+
) -> T:
|
|
379
|
+
"""POST form data and parse the response, returning *default* on failure.
|
|
380
|
+
|
|
381
|
+
Parameters
|
|
382
|
+
----------
|
|
383
|
+
client : SpeechWireClient | None
|
|
384
|
+
The authenticated client instance.
|
|
385
|
+
url : str
|
|
386
|
+
URL to POST to.
|
|
387
|
+
data : dict
|
|
388
|
+
Form data to submit.
|
|
389
|
+
parser : Callable[[str], T]
|
|
390
|
+
Pure function that converts HTML text into the desired data shape.
|
|
391
|
+
default : T
|
|
392
|
+
Value returned when posting or parsing fails.
|
|
393
|
+
context : str
|
|
394
|
+
Human-readable label for log messages.
|
|
395
|
+
"""
|
|
396
|
+
if not client:
|
|
397
|
+
logger.error("No SpeechWire client provided")
|
|
398
|
+
return default
|
|
399
|
+
|
|
400
|
+
try:
|
|
401
|
+
resp = client.post(url, data=data)
|
|
402
|
+
resp.raise_for_status()
|
|
403
|
+
except Exception:
|
|
404
|
+
logger.error("Failed to post %s", context)
|
|
405
|
+
return default
|
|
406
|
+
|
|
407
|
+
try:
|
|
408
|
+
return parser(resp.text)
|
|
409
|
+
except Exception:
|
|
410
|
+
logger.error("Failed to parse %s response", context)
|
|
411
|
+
return default
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from speechwire_mcp.judges.client import (
|
|
2
|
+
get_judge_list,
|
|
3
|
+
get_judge_contact,
|
|
4
|
+
get_judge_availability,
|
|
5
|
+
get_judge_school,
|
|
6
|
+
add_judge,
|
|
7
|
+
get_judge_types,
|
|
8
|
+
update_judge_email,
|
|
9
|
+
update_judge_availability,
|
|
10
|
+
update_judge_school,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"get_judge_list",
|
|
15
|
+
"get_judge_contact",
|
|
16
|
+
"get_judge_availability",
|
|
17
|
+
"get_judge_school",
|
|
18
|
+
"add_judge",
|
|
19
|
+
"get_judge_types",
|
|
20
|
+
"update_judge_email",
|
|
21
|
+
"update_judge_availability",
|
|
22
|
+
"update_judge_school",
|
|
23
|
+
]
|