xcpcio 0.63.4__py3-none-any.whl → 0.63.6__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.
Potentially problematic release.
This version of xcpcio might be problematic. Click here for more details.
- xcpcio/__version__.py +4 -1
- xcpcio/ccs/__init__.py +2 -2
- xcpcio/ccs/api_server/__init__.py +9 -0
- xcpcio/ccs/api_server/dependencies.py +48 -0
- xcpcio/ccs/api_server/routes/__init__.py +50 -0
- xcpcio/ccs/api_server/routes/access.py +20 -0
- xcpcio/ccs/api_server/routes/awards.py +38 -0
- xcpcio/ccs/api_server/routes/clarifications.py +42 -0
- xcpcio/ccs/api_server/routes/contests.py +74 -0
- xcpcio/ccs/api_server/routes/general.py +32 -0
- xcpcio/ccs/api_server/routes/groups.py +41 -0
- xcpcio/ccs/api_server/routes/judgement_types.py +41 -0
- xcpcio/ccs/api_server/routes/judgements.py +40 -0
- xcpcio/ccs/api_server/routes/languages.py +41 -0
- xcpcio/ccs/api_server/routes/organizations.py +82 -0
- xcpcio/ccs/api_server/routes/problems.py +77 -0
- xcpcio/ccs/api_server/routes/runs.py +40 -0
- xcpcio/ccs/api_server/routes/submissions.py +82 -0
- xcpcio/ccs/api_server/routes/teams.py +82 -0
- xcpcio/ccs/api_server/server.py +83 -0
- xcpcio/ccs/api_server/services/__init__.py +9 -0
- xcpcio/ccs/api_server/services/contest_service.py +408 -0
- {xcpcio-0.63.4.dist-info → xcpcio-0.63.6.dist-info}/METADATA +4 -1
- xcpcio-0.63.6.dist-info/RECORD +33 -0
- xcpcio-0.63.4.dist-info/RECORD +0 -13
- {xcpcio-0.63.4.dist-info → xcpcio-0.63.6.dist-info}/WHEEL +0 -0
- {xcpcio-0.63.4.dist-info → xcpcio-0.63.6.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any, Dict, List, Optional
|
|
3
|
+
|
|
4
|
+
from fastapi import APIRouter, Path, Query
|
|
5
|
+
|
|
6
|
+
from ...model import Run, Runs
|
|
7
|
+
from ..dependencies import ContestServiceDep
|
|
8
|
+
|
|
9
|
+
router = APIRouter()
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@router.get(
|
|
14
|
+
"/contests/{contest_id}/runs",
|
|
15
|
+
summary="Get Runs",
|
|
16
|
+
description="Get all test case runs, optionally filtered by judgement",
|
|
17
|
+
response_model=Runs,
|
|
18
|
+
)
|
|
19
|
+
async def get_runs(
|
|
20
|
+
contest_id: str = Path(..., description="Contest identifier"),
|
|
21
|
+
judgement_id: Optional[str] = Query(None, description="Filter runs by judgement ID"),
|
|
22
|
+
service: ContestServiceDep = None,
|
|
23
|
+
) -> List[Dict[str, Any]]:
|
|
24
|
+
"""Get all test case runs, optionally filtered by judgement"""
|
|
25
|
+
return service.get_runs(contest_id, judgement_id)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@router.get(
|
|
29
|
+
"/contests/{contest_id}/runs/{run_id}",
|
|
30
|
+
summary="Get Run",
|
|
31
|
+
description="Get specific test case run information",
|
|
32
|
+
response_model=Run,
|
|
33
|
+
)
|
|
34
|
+
async def get_run(
|
|
35
|
+
contest_id: str = Path(..., description="Contest identifier"),
|
|
36
|
+
run_id: str = Path(..., description="Run identifier"),
|
|
37
|
+
service: ContestServiceDep = None,
|
|
38
|
+
) -> Dict[str, Any]:
|
|
39
|
+
"""Get specific test case run information"""
|
|
40
|
+
return service.get_run(contest_id, run_id)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any, Dict, List, Optional
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, HTTPException, Query
|
|
6
|
+
from fastapi import Path as FastAPIPath
|
|
7
|
+
from fastapi.responses import FileResponse
|
|
8
|
+
|
|
9
|
+
from ...model import Submission, Submissions
|
|
10
|
+
from ..dependencies import ContestServiceDep
|
|
11
|
+
|
|
12
|
+
router = APIRouter()
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@router.get(
|
|
17
|
+
"/contests/{contest_id}/submissions",
|
|
18
|
+
summary="Get Submissions",
|
|
19
|
+
description="Get all submissions, optionally filtered by team or problem",
|
|
20
|
+
response_model=Submissions,
|
|
21
|
+
)
|
|
22
|
+
async def get_submissions(
|
|
23
|
+
contest_id: str = FastAPIPath(..., description="Contest identifier"),
|
|
24
|
+
team_id: Optional[str] = Query(None, description="Filter submissions by team ID"),
|
|
25
|
+
problem_id: Optional[str] = Query(None, description="Filter submissions by problem ID"),
|
|
26
|
+
service: ContestServiceDep = None,
|
|
27
|
+
) -> List[Dict[str, Any]]:
|
|
28
|
+
"""Get all submissions, optionally filtered by team or problem"""
|
|
29
|
+
return service.get_submissions(contest_id, team_id, problem_id)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@router.get(
|
|
33
|
+
"/contests/{contest_id}/submissions/{submission_id}",
|
|
34
|
+
summary="Get Submission",
|
|
35
|
+
description="Get specific submission information",
|
|
36
|
+
response_model=Submission,
|
|
37
|
+
)
|
|
38
|
+
async def get_submission(
|
|
39
|
+
contest_id: str = FastAPIPath(..., description="Contest identifier"),
|
|
40
|
+
submission_id: str = FastAPIPath(..., description="Submission identifier"),
|
|
41
|
+
service: ContestServiceDep = None,
|
|
42
|
+
) -> Dict[str, Any]:
|
|
43
|
+
"""Get specific submission information"""
|
|
44
|
+
return service.get_submission(contest_id, submission_id)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@router.get(
|
|
48
|
+
"/contests/{contest_id}/submissions/{submission_id}/files",
|
|
49
|
+
summary="Get Submission Files",
|
|
50
|
+
description="Get files for a specific submission",
|
|
51
|
+
response_class=FileResponse,
|
|
52
|
+
)
|
|
53
|
+
async def get_submission_files(
|
|
54
|
+
contest_id: str = FastAPIPath(..., description="Contest identifier"),
|
|
55
|
+
submission_id: str = FastAPIPath(..., description="Submission identifier"),
|
|
56
|
+
service: ContestServiceDep = None,
|
|
57
|
+
) -> FileResponse:
|
|
58
|
+
"""Get submission files"""
|
|
59
|
+
service.validate_contest_id(contest_id)
|
|
60
|
+
|
|
61
|
+
# Get submission from indexed data
|
|
62
|
+
submission = service.submissions_by_id.get(submission_id)
|
|
63
|
+
if not submission:
|
|
64
|
+
raise HTTPException(status_code=404, detail=f"Submission {submission_id} not found")
|
|
65
|
+
|
|
66
|
+
# Expected href pattern for this endpoint
|
|
67
|
+
expected_href = f"contests/{contest_id}/submissions/{submission_id}/files"
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
files: List[Dict] = submission["files"]
|
|
71
|
+
for file_info in files:
|
|
72
|
+
href = file_info["href"]
|
|
73
|
+
if href == expected_href:
|
|
74
|
+
filename = file_info["filename"]
|
|
75
|
+
submission_file: Path = service.contest_package_dir / "submissions" / submission_id / filename
|
|
76
|
+
if submission_file.exists():
|
|
77
|
+
mime_type = file_info["mime"]
|
|
78
|
+
return FileResponse(path=submission_file, media_type=mime_type, filename=filename)
|
|
79
|
+
except Exception as e:
|
|
80
|
+
raise HTTPException(
|
|
81
|
+
status_code=404, detail=f"Submission files not found. [submission_id={submission_id}] [err={e}]"
|
|
82
|
+
)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any, Dict, List, Optional
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, HTTPException, Query
|
|
6
|
+
from fastapi import Path as FastAPIPath
|
|
7
|
+
from fastapi.responses import FileResponse
|
|
8
|
+
|
|
9
|
+
from ...model import (
|
|
10
|
+
Team,
|
|
11
|
+
Teams,
|
|
12
|
+
)
|
|
13
|
+
from ..dependencies import ContestServiceDep
|
|
14
|
+
|
|
15
|
+
router = APIRouter()
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@router.get(
|
|
20
|
+
"/contests/{contest_id}/teams",
|
|
21
|
+
summary="Get Teams",
|
|
22
|
+
description="Get all teams, optionally filtered by group",
|
|
23
|
+
response_model=Teams,
|
|
24
|
+
)
|
|
25
|
+
async def get_teams(
|
|
26
|
+
contest_id: str = FastAPIPath(..., description="Contest identifier"),
|
|
27
|
+
group_id: Optional[str] = Query(None, description="Filter teams by group ID"),
|
|
28
|
+
service: ContestServiceDep = None,
|
|
29
|
+
) -> List[Dict[str, Any]]:
|
|
30
|
+
"""Get all teams, optionally filtered by group"""
|
|
31
|
+
return service.get_teams(contest_id, group_id)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@router.get(
|
|
35
|
+
"/contests/{contest_id}/teams/{team_id}",
|
|
36
|
+
summary="Get Team",
|
|
37
|
+
description="Get specific team information",
|
|
38
|
+
response_model=Team,
|
|
39
|
+
)
|
|
40
|
+
async def get_team(
|
|
41
|
+
contest_id: str = FastAPIPath(..., description="Contest identifier"),
|
|
42
|
+
team_id: str = FastAPIPath(..., description="Team identifier"),
|
|
43
|
+
service: ContestServiceDep = None,
|
|
44
|
+
) -> Dict[str, Any]:
|
|
45
|
+
"""Get specific team information"""
|
|
46
|
+
return service.get_team(contest_id, team_id)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@router.get(
|
|
50
|
+
"/contests/{contest_id}/teams/{team_id}/photo",
|
|
51
|
+
summary="Get Team Photo",
|
|
52
|
+
description="Get photo file for a specific team",
|
|
53
|
+
response_class=FileResponse,
|
|
54
|
+
)
|
|
55
|
+
async def get_team_photo(
|
|
56
|
+
contest_id: str = FastAPIPath(..., description="Contest identifier"),
|
|
57
|
+
team_id: str = FastAPIPath(..., description="Team identifier"),
|
|
58
|
+
service: ContestServiceDep = None,
|
|
59
|
+
) -> FileResponse:
|
|
60
|
+
"""Get team photo file"""
|
|
61
|
+
service.validate_contest_id(contest_id)
|
|
62
|
+
|
|
63
|
+
# Get team from indexed data
|
|
64
|
+
team = service.teams_by_id.get(team_id)
|
|
65
|
+
if not team:
|
|
66
|
+
raise HTTPException(status_code=404, detail=f"Team {team_id} not found")
|
|
67
|
+
|
|
68
|
+
# Expected href pattern for this endpoint
|
|
69
|
+
expected_href = f"contests/{contest_id}/teams/{team_id}/photo"
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
photos = team["photo"]
|
|
73
|
+
for photo in photos:
|
|
74
|
+
href = photo["href"]
|
|
75
|
+
if href == expected_href:
|
|
76
|
+
filename = ["filename"]
|
|
77
|
+
photo_file: Path = service.contest_package_dir / "teams" / team_id / filename
|
|
78
|
+
if photo_file.exists():
|
|
79
|
+
mime_type = photo["mime"]
|
|
80
|
+
return FileResponse(path=photo_file, media_type=mime_type, filename=filename)
|
|
81
|
+
except Exception as e:
|
|
82
|
+
raise HTTPException(status_code=404, detail=f"Photo not found. [team_id={team_id}] [err={e}]")
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Contest API Server
|
|
3
|
+
|
|
4
|
+
Main server class implementing the Contest API specification using modern FastAPI architecture.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import uvicorn
|
|
11
|
+
from fastapi import FastAPI
|
|
12
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
13
|
+
|
|
14
|
+
from xcpcio.__version__ import __version__
|
|
15
|
+
|
|
16
|
+
from .dependencies import configure_dependencies
|
|
17
|
+
from .routes import create_router
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ContestAPIServer:
|
|
23
|
+
"""
|
|
24
|
+
Contest API Server implementing the Contest API specification.
|
|
25
|
+
|
|
26
|
+
This server provides REST API endpoints for contest data based on
|
|
27
|
+
the Contest Package format and Contest API specification.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, contest_package_dir: Path):
|
|
31
|
+
"""
|
|
32
|
+
Initialize the Contest API Server.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
contest_packages: Dictionary mapping contest_id to contest package directory
|
|
36
|
+
"""
|
|
37
|
+
self.contest_package_dir = contest_package_dir
|
|
38
|
+
|
|
39
|
+
# Configure dependency injection for multi-contest mode
|
|
40
|
+
# This might need adjustment based on how dependencies work
|
|
41
|
+
configure_dependencies(contest_package_dir)
|
|
42
|
+
|
|
43
|
+
# Create FastAPI application
|
|
44
|
+
self.app = FastAPI(
|
|
45
|
+
title="Contest API Server",
|
|
46
|
+
description="REST API for Contest Control System specifications",
|
|
47
|
+
version=__version__,
|
|
48
|
+
docs_url="/docs",
|
|
49
|
+
redoc_url="/redoc",
|
|
50
|
+
openapi_url="/openapi.json",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Add CORS middleware
|
|
54
|
+
self.app.add_middleware(
|
|
55
|
+
CORSMiddleware,
|
|
56
|
+
allow_origins=["*"],
|
|
57
|
+
allow_credentials=True,
|
|
58
|
+
allow_methods=["*"],
|
|
59
|
+
allow_headers=["*"],
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Include all routes
|
|
63
|
+
router = create_router()
|
|
64
|
+
self.app.include_router(router)
|
|
65
|
+
|
|
66
|
+
def run(self, host: str = "0.0.0.0", port: int = 8000, reload: bool = True, log_level: str = "info"):
|
|
67
|
+
"""
|
|
68
|
+
Run the contest API server.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
host: Host to bind to
|
|
72
|
+
port: Port to bind to
|
|
73
|
+
reload: Enable auto-reload for development
|
|
74
|
+
log_level: Log level (debug, info, warning, error, critical)
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
logger.info("Starting Contest API Server...")
|
|
78
|
+
logger.info(f"Contest package dir: {self.contest_package_dir}")
|
|
79
|
+
logger.info(f"API will be available at: http://{host}:{port}")
|
|
80
|
+
logger.info(f"Interactive docs at: http://{host}:{port}/docs")
|
|
81
|
+
logger.info(f"ReDoc at: http://{host}:{port}/redoc")
|
|
82
|
+
|
|
83
|
+
uvicorn.run(self.app, host=host, port=port, reload=reload, log_level=log_level)
|