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 CHANGED
@@ -1 +1,4 @@
1
- __version__ = "0.63.4"
1
+ # This file is auto-generated by Hatchling. As such, do not:
2
+ # - modify
3
+ # - track in version control e.g. be sure to add to .gitignore
4
+ __version__ = VERSION = '0.63.6'
xcpcio/ccs/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
- from . import contest_archiver, model
1
+ from . import api_server, contest_archiver, model
2
2
 
3
- __all__ = [model, contest_archiver]
3
+ __all__ = [model, contest_archiver, api_server]
@@ -0,0 +1,9 @@
1
+ """
2
+ Contest API Server Package
3
+
4
+ Modern FastAPI-based implementation of the Contest API specification.
5
+ """
6
+
7
+ from .server import ContestAPIServer
8
+
9
+ __all__ = [ContestAPIServer]
@@ -0,0 +1,48 @@
1
+ """
2
+ FastAPI Dependencies
3
+
4
+ Dependency injection system for Contest API Server.
5
+ """
6
+
7
+ from pathlib import Path
8
+ from typing import Annotated
9
+
10
+ from fastapi import Depends
11
+
12
+ from .services.contest_service import ContestService
13
+
14
+ # Global contest service instance cache
15
+ _contest_service_instance = None
16
+
17
+
18
+ def get_contest_service() -> ContestService:
19
+ """
20
+ Dependency that provides ContestService instance.
21
+
22
+ This is configured by the main server class and cached globally.
23
+
24
+ Returns:
25
+ ContestService instance
26
+
27
+ Raises:
28
+ RuntimeError: If service not configured
29
+ """
30
+ global _contest_service_instance
31
+ if _contest_service_instance is None:
32
+ raise RuntimeError("ContestService not configured. Call configure_dependencies() first.")
33
+ return _contest_service_instance
34
+
35
+
36
+ def configure_dependencies(contest_package_dir: Path) -> None:
37
+ """
38
+ Configure the dependency injection system.
39
+
40
+ Args:
41
+ contest_package_dir: Path to contest package directory
42
+ """
43
+ global _contest_service_instance
44
+ _contest_service_instance = ContestService(contest_package_dir)
45
+
46
+
47
+ # Type alias for dependency injection
48
+ ContestServiceDep = Annotated[ContestService, Depends(get_contest_service)]
@@ -0,0 +1,50 @@
1
+ """
2
+ Routes package for Contest API Server
3
+
4
+ Contains all API route definitions organized by functionality.
5
+ """
6
+
7
+ from fastapi import APIRouter
8
+
9
+ from . import (
10
+ access,
11
+ awards,
12
+ clarifications,
13
+ contests,
14
+ general,
15
+ groups,
16
+ judgement_types,
17
+ judgements,
18
+ languages,
19
+ organizations,
20
+ problems,
21
+ runs,
22
+ submissions,
23
+ teams,
24
+ )
25
+
26
+
27
+ def create_router() -> APIRouter:
28
+ """Create and configure the main API router"""
29
+ router = APIRouter(prefix="/api")
30
+
31
+ # Include all route modules
32
+ router.include_router(access.router, tags=["Access"])
33
+ router.include_router(awards.router, tags=["Awards"])
34
+ router.include_router(clarifications.router, tags=["Clarifications"])
35
+ router.include_router(contests.router, tags=["Contests"])
36
+ router.include_router(general.router, tags=["General"])
37
+ router.include_router(groups.router, tags=["Groups"])
38
+ router.include_router(judgements.router, tags=["Judgements"])
39
+ router.include_router(judgement_types.router, tags=["Judgement types"])
40
+ router.include_router(languages.router, tags=["Languages"])
41
+ router.include_router(organizations.router, tags=["Organizations"])
42
+ router.include_router(problems.router, tags=["Problems"])
43
+ router.include_router(runs.router, tags=["Runs"])
44
+ router.include_router(submissions.router, tags=["Submissions"])
45
+ router.include_router(teams.router, tags=["Teams"])
46
+
47
+ return router
48
+
49
+
50
+ __all__ = ["create_router"]
@@ -0,0 +1,20 @@
1
+ import logging
2
+ from typing import Any, Dict
3
+
4
+ from fastapi import APIRouter
5
+
6
+ from ..dependencies import ContestServiceDep
7
+
8
+ router = APIRouter()
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ @router.get(
13
+ "/contests/{contest_id}/access",
14
+ summary="Get Access Information",
15
+ description="Get access capabilities and visible endpoints for current client",
16
+ response_model=Dict[str, Any],
17
+ )
18
+ async def get_access(contest_id: str, service: ContestServiceDep) -> Dict[str, Any]:
19
+ """Get access information for the current client"""
20
+ return service.get_access_info(contest_id)
@@ -0,0 +1,38 @@
1
+ import logging
2
+ from typing import Any, Dict, List
3
+
4
+ from fastapi import APIRouter, Path
5
+
6
+ from ...model import Award, Awards
7
+ from ..dependencies import ContestServiceDep
8
+
9
+ router = APIRouter()
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ @router.get(
14
+ "/contests/{contest_id}/awards",
15
+ summary="Get Awards",
16
+ description="Get all awards in the contest",
17
+ response_model=Awards,
18
+ )
19
+ async def get_awards(
20
+ contest_id: str = Path(..., description="Contest identifier"), service: ContestServiceDep = None
21
+ ) -> List[Dict[str, Any]]:
22
+ """Get all awards"""
23
+ return service.get_awards(contest_id)
24
+
25
+
26
+ @router.get(
27
+ "/contests/{contest_id}/awards/{award_id}",
28
+ summary="Get Award",
29
+ description="Get specific award information",
30
+ response_model=Award,
31
+ )
32
+ async def get_award(
33
+ contest_id: str = Path(..., description="Contest identifier"),
34
+ award_id: str = Path(..., description="Award identifier"),
35
+ service: ContestServiceDep = None,
36
+ ) -> Dict[str, Any]:
37
+ """Get specific award information"""
38
+ return service.get_award(contest_id, award_id)
@@ -0,0 +1,42 @@
1
+ import logging
2
+ from typing import Any, Dict, List, Optional
3
+
4
+ from fastapi import APIRouter, Path, Query
5
+
6
+ from ...model import Clarification, Clarifications
7
+ from ..dependencies import ContestServiceDep
8
+
9
+ router = APIRouter()
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ @router.get(
14
+ "/contests/{contest_id}/clarifications",
15
+ summary="Get Clarifications",
16
+ description="Get all clarifications, optionally filtered",
17
+ response_model=Clarifications,
18
+ )
19
+ async def get_clarifications(
20
+ contest_id: str = Path(..., description="Contest identifier"),
21
+ from_team_id: Optional[str] = Query(None, description="Filter by sender team ID (empty string for null)"),
22
+ to_team_id: Optional[str] = Query(None, description="Filter by recipient team ID (empty string for null)"),
23
+ problem_id: Optional[str] = Query(None, description="Filter by problem ID (empty string for null)"),
24
+ service: ContestServiceDep = None,
25
+ ) -> List[Dict[str, Any]]:
26
+ """Get all clarifications, optionally filtered"""
27
+ return service.get_clarifications(contest_id, from_team_id, to_team_id, problem_id)
28
+
29
+
30
+ @router.get(
31
+ "/contests/{contest_id}/clarifications/{clarification_id}",
32
+ summary="Get Clarification",
33
+ description="Get specific clarification information",
34
+ response_model=Clarification,
35
+ )
36
+ async def get_clarification(
37
+ contest_id: str = Path(..., description="Contest identifier"),
38
+ clarification_id: str = Path(..., description="Clarification identifier"),
39
+ service: ContestServiceDep = None,
40
+ ) -> Dict[str, Any]:
41
+ """Get specific clarification information"""
42
+ return service.get_clarification(contest_id, clarification_id)
@@ -0,0 +1,74 @@
1
+ import logging
2
+ from pathlib import Path
3
+ from typing import Any, Dict, List
4
+
5
+ from fastapi import APIRouter, HTTPException
6
+ from fastapi import Path as FastAPIPath
7
+ from fastapi.responses import FileResponse
8
+
9
+ from ...model import Contest, State
10
+ from ..dependencies import ContestServiceDep
11
+
12
+ router = APIRouter()
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ @router.get("/contests", summary="Get Contests", description="Get list of all contests", response_model=List[Contest])
17
+ async def get_contests(service: ContestServiceDep) -> List[Dict[str, Any]]:
18
+ """Get all contests"""
19
+ return service.get_contests()
20
+
21
+
22
+ @router.get(
23
+ "/contests/{contest_id}",
24
+ summary="Get Contest",
25
+ description="Get specific contest information",
26
+ response_model=Contest,
27
+ )
28
+ async def get_contest(
29
+ contest_id: str = FastAPIPath(..., description="Contest identifier"), service: ContestServiceDep = None
30
+ ) -> Dict[str, Any]:
31
+ """Get specific contest information"""
32
+ return service.get_contest(contest_id)
33
+
34
+
35
+ @router.get(
36
+ "/contests/{contest_id}/state",
37
+ summary="Get Contest State",
38
+ description="Get current contest state (started, ended, frozen, etc.)",
39
+ response_model=State,
40
+ )
41
+ async def get_state(
42
+ contest_id: str = FastAPIPath(..., description="Contest identifier"), service: ContestServiceDep = None
43
+ ) -> Dict[str, Any]:
44
+ """Get contest state"""
45
+ return service.get_contest_state(contest_id)
46
+
47
+
48
+ @router.get(
49
+ "/contests/{contest_id}/contest/banner",
50
+ summary="Get Contest Banner",
51
+ description="Get banner image for the contest",
52
+ response_class=FileResponse,
53
+ )
54
+ async def get_contest_banner(
55
+ contest_id: str = FastAPIPath(..., description="Contest identifier"), service: ContestServiceDep = None
56
+ ) -> FileResponse:
57
+ """Get contest banner file"""
58
+ service.validate_contest_id(contest_id)
59
+
60
+ # Expected href pattern for this endpoint
61
+ expected_href = f"contests/{contest_id}/contest/banner"
62
+
63
+ try:
64
+ banners = service.contest_data.get("banner", [])
65
+ for banner in banners:
66
+ href = banner["href"]
67
+ if href == expected_href:
68
+ filename = banner["filename"]
69
+ banner_file: Path = service.contest_package_dir / "contest" / filename
70
+ if banner_file.exists():
71
+ mime_type = banner["mime"]
72
+ return FileResponse(path=banner_file, media_type=mime_type, filename=filename)
73
+ except Exception as e:
74
+ raise HTTPException(status_code=404, detail=f"Banner not found. [contest_id={contest_id}] [err={e}]")
@@ -0,0 +1,32 @@
1
+ import logging
2
+ from datetime import datetime
3
+ from typing import Any, Dict
4
+
5
+ from fastapi import APIRouter
6
+
7
+ from ..dependencies import ContestServiceDep
8
+
9
+ router = APIRouter()
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ @router.get(
14
+ "/",
15
+ summary="API Information",
16
+ description="Get API version and provider information",
17
+ response_model=Dict[str, Any],
18
+ )
19
+ async def get_api_info(service: ContestServiceDep) -> Dict[str, Any]:
20
+ """Get API information and provider details"""
21
+ return service.get_api_info()
22
+
23
+
24
+ @router.get("/health", summary="Health Check", description="Check server health status", response_model=Dict[str, Any])
25
+ async def health_check(service: ContestServiceDep) -> Dict[str, Any]:
26
+ """Health check endpoint"""
27
+ return {
28
+ "status": "healthy",
29
+ "timestamp": datetime.now().isoformat(),
30
+ "contest_package": str(service.contest_package_dir),
31
+ "api_version": "2023-06",
32
+ }
@@ -0,0 +1,41 @@
1
+ import logging
2
+ from typing import Any, Dict, List
3
+
4
+ from fastapi import APIRouter, Path
5
+
6
+ from ...model import (
7
+ Group,
8
+ Groups,
9
+ )
10
+ from ..dependencies import ContestServiceDep
11
+
12
+ router = APIRouter()
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ @router.get(
17
+ "/contests/{contest_id}/groups",
18
+ summary="Get Groups",
19
+ description="Get all team groups in the contest",
20
+ response_model=Groups,
21
+ )
22
+ async def get_groups(
23
+ contest_id: str = Path(..., description="Contest identifier"), service: ContestServiceDep = None
24
+ ) -> List[Dict[str, Any]]:
25
+ """Get all groups"""
26
+ return service.get_groups(contest_id)
27
+
28
+
29
+ @router.get(
30
+ "/contests/{contest_id}/groups/{group_id}",
31
+ summary="Get Group",
32
+ description="Get specific group information",
33
+ response_model=Group,
34
+ )
35
+ async def get_group(
36
+ contest_id: str = Path(..., description="Contest identifier"),
37
+ group_id: str = Path(..., description="Group identifier"),
38
+ service: ContestServiceDep = None,
39
+ ) -> Dict[str, Any]:
40
+ """Get specific group information"""
41
+ return service.get_group(contest_id, group_id)
@@ -0,0 +1,41 @@
1
+ import logging
2
+ from typing import Any, Dict, List
3
+
4
+ from fastapi import APIRouter, Path
5
+
6
+ from ...model import (
7
+ JudgementType,
8
+ JudgementTypes,
9
+ )
10
+ from ..dependencies import ContestServiceDep
11
+
12
+ router = APIRouter()
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ @router.get(
17
+ "/contests/{contest_id}/judgement-types",
18
+ summary="Get Judgement Types",
19
+ description="Get all judgement types available in the contest",
20
+ response_model=JudgementTypes,
21
+ )
22
+ async def get_judgement_types(
23
+ contest_id: str = Path(..., description="Contest identifier"), service: ContestServiceDep = None
24
+ ) -> List[Dict[str, Any]]:
25
+ """Get all judgement types"""
26
+ return service.get_judgement_types(contest_id)
27
+
28
+
29
+ @router.get(
30
+ "/contests/{contest_id}/judgement-types/{judgement_type_id}",
31
+ summary="Get Judgement Type",
32
+ description="Get specific judgement type information",
33
+ response_model=JudgementType,
34
+ )
35
+ async def get_judgement_type(
36
+ contest_id: str = Path(..., description="Contest identifier"),
37
+ judgement_type_id: str = Path(..., description="Judgement type identifier"),
38
+ service: ContestServiceDep = None,
39
+ ) -> Dict[str, Any]:
40
+ """Get specific judgement type information"""
41
+ return service.get_judgement_type(contest_id, judgement_type_id)
@@ -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 Judgement, Judgements
7
+ from ..dependencies import ContestServiceDep
8
+
9
+ router = APIRouter()
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ @router.get(
14
+ "/contests/{contest_id}/judgements",
15
+ summary="Get Judgements",
16
+ description="Get all judgements, optionally filtered by submission",
17
+ response_model=Judgements,
18
+ )
19
+ async def get_judgements(
20
+ contest_id: str = Path(..., description="Contest identifier"),
21
+ submission_id: Optional[str] = Query(None, description="Filter judgements by submission ID"),
22
+ service: ContestServiceDep = None,
23
+ ) -> List[Dict[str, Any]]:
24
+ """Get all judgements, optionally filtered by submission"""
25
+ return service.get_judgements(contest_id, submission_id)
26
+
27
+
28
+ @router.get(
29
+ "/contests/{contest_id}/judgements/{judgement_id}",
30
+ summary="Get Judgement",
31
+ description="Get specific judgement information",
32
+ response_model=Judgement,
33
+ )
34
+ async def get_judgement(
35
+ contest_id: str = Path(..., description="Contest identifier"),
36
+ judgement_id: str = Path(..., description="Judgement identifier"),
37
+ service: ContestServiceDep = None,
38
+ ) -> Dict[str, Any]:
39
+ """Get specific judgement information"""
40
+ return service.get_judgement(contest_id, judgement_id)
@@ -0,0 +1,41 @@
1
+ import logging
2
+ from typing import Any, Dict, List
3
+
4
+ from fastapi import APIRouter, Path
5
+
6
+ from ...model import (
7
+ Language,
8
+ Languages,
9
+ )
10
+ from ..dependencies import ContestServiceDep
11
+
12
+ router = APIRouter()
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ @router.get(
17
+ "/contests/{contest_id}/languages",
18
+ summary="Get Languages",
19
+ description="Get all programming languages available for submission",
20
+ response_model=Languages,
21
+ )
22
+ async def get_languages(
23
+ contest_id: str = Path(..., description="Contest identifier"), service: ContestServiceDep = None
24
+ ) -> List[Dict[str, Any]]:
25
+ """Get all programming languages"""
26
+ return service.get_languages(contest_id)
27
+
28
+
29
+ @router.get(
30
+ "/contests/{contest_id}/languages/{language_id}",
31
+ summary="Get Language",
32
+ description="Get specific language information",
33
+ response_model=Language,
34
+ )
35
+ async def get_language(
36
+ contest_id: str = Path(..., description="Contest identifier"),
37
+ language_id: str = Path(..., description="Language identifier"),
38
+ service: ContestServiceDep = None,
39
+ ) -> Dict[str, Any]:
40
+ """Get specific language information"""
41
+ return service.get_language(contest_id, language_id)
@@ -0,0 +1,82 @@
1
+ import logging
2
+ from pathlib import Path
3
+ from typing import Any, Dict, List
4
+
5
+ from fastapi import APIRouter, HTTPException
6
+ from fastapi import Path as FastAPIPath
7
+ from fastapi.responses import FileResponse
8
+
9
+ from ...model import (
10
+ Organization,
11
+ Organizations,
12
+ )
13
+ from ..dependencies import ContestServiceDep
14
+
15
+ router = APIRouter()
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ @router.get(
20
+ "/contests/{contest_id}/organizations",
21
+ summary="Get Organizations",
22
+ description="Get all organizations in the contest",
23
+ response_model=Organizations,
24
+ )
25
+ async def get_organizations(
26
+ contest_id: str = FastAPIPath(..., description="Contest identifier"), service: ContestServiceDep = None
27
+ ) -> List[Dict[str, Any]]:
28
+ """Get all organizations"""
29
+ return service.get_organizations(contest_id)
30
+
31
+
32
+ @router.get(
33
+ "/contests/{contest_id}/organizations/{organization_id}",
34
+ summary="Get Organization",
35
+ description="Get specific organization information",
36
+ response_model=Organization,
37
+ )
38
+ async def get_organization(
39
+ contest_id: str = FastAPIPath(..., description="Contest identifier"),
40
+ organization_id: str = FastAPIPath(..., description="Organization identifier"),
41
+ service: ContestServiceDep = None,
42
+ ) -> Dict[str, Any]:
43
+ """Get specific organization information"""
44
+ return service.get_organization(contest_id, organization_id)
45
+
46
+
47
+ @router.get(
48
+ "/contests/{contest_id}/organizations/{organization_id}/logo",
49
+ summary="Get Organization Logo",
50
+ description="Get logo file for a specific organization",
51
+ response_class=FileResponse,
52
+ )
53
+ async def get_organization_logo(
54
+ contest_id: str = FastAPIPath(..., description="Contest identifier"),
55
+ organization_id: str = FastAPIPath(..., description="Organization identifier"),
56
+ service: ContestServiceDep = None,
57
+ ) -> FileResponse:
58
+ """Get organization logo file"""
59
+ service.validate_contest_id(contest_id)
60
+
61
+ # Get organization from indexed data
62
+ org = service.organizations_by_id.get(organization_id)
63
+ if not org:
64
+ raise HTTPException(status_code=404, detail=f"Organization {organization_id} not found")
65
+
66
+ # Expected href pattern for this endpoint
67
+ expected_href = f"contests/{contest_id}/organizations/{organization_id}/logo"
68
+
69
+ try:
70
+ logos = org["logo"]
71
+ for logo in logos:
72
+ href = logo["href"]
73
+ if href == expected_href:
74
+ filename = logo["filename"]
75
+ logo_file: Path = service.contest_package_dir / "organizations" / organization_id / filename
76
+ if logo_file.exists():
77
+ mime_type = logo["mime"]
78
+ return FileResponse(path=logo_file, media_type=mime_type, filename=filename)
79
+ except Exception as e:
80
+ raise HTTPException(
81
+ status_code=404, detail=f"Logo file not found. [organization_id={organization_id}] [err={e}]"
82
+ )
@@ -0,0 +1,77 @@
1
+ import logging
2
+ from pathlib import Path
3
+ from typing import Any, Dict, List
4
+
5
+ from fastapi import APIRouter, HTTPException
6
+ from fastapi import Path as FastAPIPath
7
+ from fastapi.responses import FileResponse
8
+
9
+ from ...model import Problem, Problems
10
+ from ..dependencies import ContestServiceDep
11
+
12
+ router = APIRouter()
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ @router.get(
17
+ "/contests/{contest_id}/problems",
18
+ summary="Get Problems",
19
+ description="Get all problems in the contest",
20
+ response_model=Problems,
21
+ )
22
+ async def get_problems(
23
+ contest_id: str = FastAPIPath(..., description="Contest identifier"), service: ContestServiceDep = None
24
+ ) -> List[Dict[str, Any]]:
25
+ """Get all problems in the contest"""
26
+ return service.get_problems(contest_id)
27
+
28
+
29
+ @router.get(
30
+ "/contests/{contest_id}/problems/{problem_id}",
31
+ summary="Get Problem",
32
+ description="Get specific problem information",
33
+ response_model=Problem,
34
+ )
35
+ async def get_problem(
36
+ contest_id: str = FastAPIPath(..., description="Contest identifier"),
37
+ problem_id: str = FastAPIPath(..., description="Problem identifier"),
38
+ service: ContestServiceDep = None,
39
+ ) -> Dict[str, Any]:
40
+ """Get specific problem information"""
41
+ return service.get_problem(contest_id, problem_id)
42
+
43
+
44
+ @router.get(
45
+ "/contests/{contest_id}/problems/{problem_id}/statement",
46
+ summary="Get Problem Statement",
47
+ description="Get problem statement file",
48
+ response_class=FileResponse,
49
+ )
50
+ async def get_problem_statement(
51
+ contest_id: str = FastAPIPath(..., description="Contest identifier"),
52
+ problem_id: str = FastAPIPath(..., description="Problem identifier"),
53
+ service: ContestServiceDep = None,
54
+ ) -> FileResponse:
55
+ """Get problem statement file"""
56
+ service.validate_contest_id(contest_id)
57
+
58
+ # Get problem from indexed data
59
+ problem: Dict = service.problems_by_id.get(problem_id)
60
+ if not problem:
61
+ raise HTTPException(status_code=404, detail=f"Problem {problem_id} not found")
62
+
63
+ # Expected href pattern for this endpoint
64
+ expected_href = f"contests/{contest_id}/problems/{problem_id}/statement"
65
+
66
+ try:
67
+ statements = problem.get("statement", [])
68
+ for statement in statements:
69
+ href = statement["href"]
70
+ filename = statement["filename"]
71
+ if href == expected_href and filename:
72
+ statement_file: Path = service.contest_package_dir / "problems" / problem_id / filename
73
+ if statement_file.exists():
74
+ mime_type = statement["mime"]
75
+ return FileResponse(path=statement_file, media_type=mime_type, filename=filename)
76
+ except Exception as e:
77
+ raise HTTPException(status_code=404, detail=f"Statement not found. [problem_id={problem_id}] [err={e}]")