idegym-api 0.8.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.
idegym/api/__init__.py ADDED
@@ -0,0 +1,9 @@
1
+ from importlib.metadata import PackageNotFoundError, version
2
+
3
+ try:
4
+ __version__ = version("idegym-api")
5
+ except PackageNotFoundError:
6
+ __version__ = "0.0.0.dev0"
7
+
8
+ if __version__ == "0.0.0.dev0":
9
+ __version__ = "latest"
idegym/api/auth.py ADDED
@@ -0,0 +1,21 @@
1
+ from base64 import b64encode
2
+ from typing import Optional
3
+
4
+ from pydantic import BaseModel, Field, SecretStr
5
+
6
+
7
+ class BasicAuth(BaseModel):
8
+ """Basic HTTP authentication credentials. Both fields are excluded from serialization."""
9
+
10
+ username: Optional[str] = Field(default=None, exclude=True)
11
+ password: Optional[SecretStr] = Field(default=None, exclude=True)
12
+
13
+ @property
14
+ def base64(self) -> Optional[str]:
15
+ """Returns Base64-encoded 'username:password' string, or None if username is not set."""
16
+ if not self.username:
17
+ return None
18
+ password = self.password.get_secret_value() if self.password else ""
19
+ value = self.username + ":" + password
20
+ encoded = value.encode()
21
+ return b64encode(encoded).decode()
idegym/api/config.py ADDED
@@ -0,0 +1,186 @@
1
+ from os.path import abspath, join
2
+ from tempfile import gettempdir
3
+ from typing import Optional
4
+
5
+ from idegym.api.auth import BasicAuth
6
+ from idegym.api.data import DataSize
7
+ from idegym.api.type import Duration, HttpUrl, IPvAddress, LogLevelName
8
+ from pydantic import BaseModel, Field, field_validator
9
+
10
+
11
+ class ServerConfig(BaseModel):
12
+ host: IPvAddress = Field(default="0.0.0.0")
13
+ port: int = Field(ge=0, le=65535, default=8000)
14
+ response_buffer_size: DataSize = Field(ge=0, default=DataSize(mb=8))
15
+ shutdown_delay: Duration = Field(default=Duration(seconds=30))
16
+
17
+
18
+ class LoggingConfig(BaseModel):
19
+ level: LogLevelName = Field(default="INFO")
20
+ json_format: bool = Field(default=False)
21
+ file_path: str = Field(default=join(gettempdir(), "idegym.log"))
22
+ max_file_size: DataSize = Field(ge=0, default=DataSize(mb=10))
23
+ max_file_count: int = Field(description="Number of log file backups to keep", ge=0, default=5)
24
+
25
+ @field_validator("file_path")
26
+ def validate_file_path(cls, value: str) -> str:
27
+ path = value.strip() if value else None
28
+ if not path:
29
+ field = cls.__pydantic_fields__["file_path"]
30
+ return field.default
31
+ else:
32
+ return abspath(path)
33
+
34
+
35
+ class ProjectConfig(BaseModel):
36
+ path: str = Field(default=".project")
37
+ archive: Optional[str] = Field(default=None)
38
+
39
+ @field_validator("path")
40
+ def validate_path(cls, value: str) -> str:
41
+ path = value.strip() if value else None
42
+ if not path:
43
+ field = cls.__pydantic_fields__["path"]
44
+ return field.default
45
+ else:
46
+ return abspath(path)
47
+
48
+ @field_validator("archive")
49
+ def validate_archive_path(cls, value: Optional[str]) -> Optional[str]:
50
+ path = value.strip() if value else None
51
+ if not path:
52
+ field = cls.__pydantic_fields__["archive"]
53
+ return field.default
54
+ else:
55
+ return abspath(path)
56
+
57
+
58
+ class DatabaseConfig(BaseModel):
59
+ host: str = Field(default="localhost")
60
+ port: str = Field(default="5432")
61
+ user: str = Field(default="postgres")
62
+ password: str = Field(default="postgres")
63
+ db: str = Field(default="idegym")
64
+ clean_database: bool = Field(description="Drop and recreate all tables on startup", default=False)
65
+
66
+ @property
67
+ def url(self) -> str:
68
+ return f"postgresql+asyncpg://{self.user}:{self.password}@{self.host}:{self.port}/{self.db}"
69
+
70
+
71
+ class SQLAlchemyConfig(BaseModel):
72
+ pool_size: int = Field(ge=0, default=20)
73
+ max_overflow: int = Field(ge=0, default=5)
74
+ pool_recycle: int = Field(ge=-1, default=1800, description="Connection recycling interval in seconds")
75
+ pool_timeout: int = Field(gt=0, default=1200, description="Connection acquisition timeout in seconds")
76
+ pool_pre_ping: bool = Field(default=True)
77
+
78
+
79
+ class AsyncioConfig(BaseModel):
80
+ debug: bool = Field(default=False)
81
+ dump_interval: int = Field(description="Interval in seconds between asyncio task dumps", ge=1, default=300)
82
+
83
+
84
+ class ResourcesConfig(BaseModel):
85
+ default_cpu_request: float = Field(description="Default CPU cores per environment", ge=0, default=1.0)
86
+ default_ram_request: float = Field(description="Default RAM per environment in GB", ge=0, default=2.0)
87
+
88
+
89
+ class ConnectionLimitsConfig(BaseModel):
90
+ max_connections_or_asyncio_tasks: int = Field(
91
+ description="The maximum number of concurrent connections that may be established or asyncio tasks in uvicorn.",
92
+ ge=1,
93
+ default=1500,
94
+ )
95
+ unhealthy_connections_or_asyncio_tasks: int = Field(
96
+ description="The maximum number of concurrent connections that"
97
+ " may be established or asyncio tasks in uvicorn after which orchestrator becomes unhealthy.",
98
+ ge=1,
99
+ default=1000,
100
+ )
101
+ max_keepalive_connections: int = Field(
102
+ description="Allow the connection pool to maintain keep-alive connections below this point."
103
+ "Should be less than or equal to `max_connections`",
104
+ ge=1,
105
+ default=20,
106
+ )
107
+ keepalive_expiry: float = Field(
108
+ description="Time limit on idle keep-alive connections in seconds.", ge=1.0, default=5.0
109
+ )
110
+
111
+
112
+ class TracingConfig(BaseModel):
113
+ endpoint: Optional[HttpUrl] = Field(description="OTLP HTTP endpoint for traces", default=None)
114
+ timeout: float = Field(description="Timeout for sending traces in seconds", ge=0, default=10)
115
+ auth: BasicAuth = Field(default_factory=BasicAuth)
116
+
117
+ @property
118
+ def enabled(self) -> bool:
119
+ return bool(self.endpoint)
120
+
121
+
122
+ class OTELConfig(BaseModel):
123
+ service_name: Optional[str] = Field(default=None)
124
+ tracing: TracingConfig = Field(default_factory=TracingConfig)
125
+ attributes: dict[str, str] = Field(description="Extra attributes added to all spans", default_factory=dict)
126
+
127
+
128
+ class WatcherConfig(BaseModel):
129
+ cleanup_interval: Duration = Field(default=Duration(seconds=60))
130
+ inactive_timeout: Duration = Field(
131
+ description="Inactivity timeout after which idle servers/clients are cleaned up",
132
+ default=Duration(minutes=10),
133
+ )
134
+ finished_timeout: Duration = Field(
135
+ description="How long to keep finished servers before deleting them",
136
+ default=Duration(minutes=5),
137
+ )
138
+ request_max_age: Duration = Field(
139
+ description="Maximum age of request records to retain",
140
+ default=Duration(days=14),
141
+ )
142
+ request_stale: Duration = Field(
143
+ description="Age after which IN_PROGRESS requests are marked as finished",
144
+ default=Duration(hours=24),
145
+ )
146
+
147
+
148
+ class OrchestratorConfig(BaseModel):
149
+ host: IPvAddress = Field(default="0.0.0.0")
150
+ port: int = Field(ge=0, le=65535, default=8000)
151
+ workers: int = Field(description="Number of uvicorn worker processes", ge=1, default=1)
152
+ prometheus_multiproc_dir: str = Field(
153
+ description="Directory for Prometheus multiprocess metric files",
154
+ default=join(gettempdir(), "idegym", "prometheus"),
155
+ )
156
+ database: DatabaseConfig = Field(default_factory=DatabaseConfig)
157
+ sqlalchemy: SQLAlchemyConfig = Field(default_factory=SQLAlchemyConfig)
158
+ asyncio: AsyncioConfig = Field(default_factory=AsyncioConfig)
159
+ resources: ResourcesConfig = Field(default_factory=ResourcesConfig)
160
+ watcher: WatcherConfig = Field(default_factory=WatcherConfig)
161
+ client_request_timeout: float = Field(
162
+ description="Client request read timeout in seconds",
163
+ default=60.0 * 60, # 1 hour
164
+ )
165
+ connection_limits: ConnectionLimitsConfig = Field(default_factory=ConnectionLimitsConfig)
166
+ enable_fifo_server_reuse: bool = Field(
167
+ description="Enable FIFO queue for server reuse to ensure fair provisioning",
168
+ default=False,
169
+ )
170
+
171
+ @field_validator("prometheus_multiproc_dir")
172
+ def validate_prometheus_multiproc_dir(cls, value: str) -> str:
173
+ path = value.strip() if value else None
174
+ if not path:
175
+ field = cls.__pydantic_fields__["prometheus_multiproc_dir"]
176
+ return field.default
177
+ else:
178
+ return abspath(path)
179
+
180
+
181
+ class Config(BaseModel):
182
+ server: ServerConfig = Field(default_factory=ServerConfig)
183
+ logging: LoggingConfig = Field(default_factory=LoggingConfig)
184
+ project: ProjectConfig = Field(default_factory=ProjectConfig)
185
+ otel: OTELConfig = Field(default_factory=OTELConfig)
186
+ orchestrator: OrchestratorConfig = Field(default_factory=OrchestratorConfig)
idegym/api/ctx.py ADDED
@@ -0,0 +1,7 @@
1
+ from contextvars import ContextVar
2
+ from typing import Optional
3
+
4
+ from structlog.contextvars import STRUCTLOG_KEY_PREFIX
5
+
6
+ task_id_context_var: ContextVar[Optional[str]] = ContextVar(STRUCTLOG_KEY_PREFIX + "task_id", default=None)
7
+ task_name_context_var: ContextVar[Optional[str]] = ContextVar(STRUCTLOG_KEY_PREFIX + "task_name", default=None)
idegym/api/data.py ADDED
@@ -0,0 +1,201 @@
1
+ from enum import IntEnum, auto
2
+ from functools import total_ordering
3
+ from re import Pattern, compile
4
+ from typing import Any, ClassVar, Union
5
+
6
+ from pydantic import GetCoreSchemaHandler, GetJsonSchemaHandler
7
+ from pydantic.json_schema import JsonSchemaValue
8
+ from pydantic_core import core_schema
9
+
10
+
11
+ class BinaryScaleEnum(IntEnum):
12
+ @staticmethod
13
+ def _generate_next_value_(name: str, start: int, count: int, last_values: list[Any]) -> int:
14
+ return 2 ** (count * 10)
15
+
16
+
17
+ class DataUnit(BinaryScaleEnum):
18
+ B = auto()
19
+ KB = auto()
20
+ MB = auto()
21
+ GB = auto()
22
+ TB = auto()
23
+ PB = auto()
24
+ EB = auto()
25
+ ZB = auto()
26
+ YB = auto()
27
+
28
+ @property
29
+ def bytes(self) -> int:
30
+ return self.value
31
+
32
+
33
+ @total_ordering
34
+ class DataSize:
35
+ PATTERN: ClassVar[Pattern] = compile(r"^([+\-]?\d+)(B|[EGKMPTYZ]B)?$")
36
+
37
+ def __init__(
38
+ self,
39
+ b: int = 0,
40
+ kb: int = 0,
41
+ mb: int = 0,
42
+ gb: int = 0,
43
+ tb: int = 0,
44
+ pb: int = 0,
45
+ eb: int = 0,
46
+ zb: int = 0,
47
+ yb: int = 0,
48
+ ):
49
+ args = {
50
+ DataUnit.KB: kb,
51
+ DataUnit.MB: mb,
52
+ DataUnit.GB: gb,
53
+ DataUnit.TB: tb,
54
+ DataUnit.PB: pb,
55
+ DataUnit.EB: eb,
56
+ DataUnit.ZB: zb,
57
+ DataUnit.YB: yb,
58
+ }
59
+ self._bytes = b + sum(size.bytes * amount for size, amount in args.items() if amount)
60
+
61
+ @classmethod
62
+ def parse(cls, value: str, unit: DataUnit = DataUnit.B) -> "DataSize":
63
+ try:
64
+ normalized = value.replace(" ", "").upper()
65
+ matcher = cls.PATTERN.match(normalized)
66
+ if not matcher:
67
+ raise ValueError(f"'{value}' does not match data size pattern")
68
+ amount, suffix = matcher.groups()
69
+ unit: DataUnit = DataUnit[suffix] if suffix else unit
70
+ return cls(b=unit.bytes * int(amount))
71
+ except Exception as ex:
72
+ raise ValueError(f"'{value}' is not a valid data size") from ex
73
+
74
+ @property
75
+ def negative(self) -> bool:
76
+ return self.bytes < 0
77
+
78
+ @property
79
+ def bytes(self) -> int:
80
+ return self._bytes
81
+
82
+ @property
83
+ def kilobytes(self) -> int:
84
+ return self.bytes // DataUnit.KB.bytes
85
+
86
+ @property
87
+ def megabytes(self) -> int:
88
+ return self.bytes // DataUnit.MB.bytes
89
+
90
+ @property
91
+ def gigabytes(self) -> int:
92
+ return self.bytes // DataUnit.GB.bytes
93
+
94
+ @property
95
+ def terabytes(self) -> int:
96
+ return self.bytes // DataUnit.TB.bytes
97
+
98
+ @property
99
+ def petabytes(self) -> int:
100
+ return self.bytes // DataUnit.PB.bytes
101
+
102
+ @property
103
+ def exabytes(self) -> int:
104
+ return self.bytes // DataUnit.EB.bytes
105
+
106
+ @property
107
+ def zettabytes(self) -> int:
108
+ return self.bytes // DataUnit.ZB.bytes
109
+
110
+ @property
111
+ def yottabytes(self) -> int:
112
+ return self.bytes // DataUnit.YB.bytes
113
+
114
+ def __eq__(self, other: Union["DataSize", int, str, None]) -> bool:
115
+ if self is other:
116
+ return True
117
+ match other:
118
+ case DataSize():
119
+ return self.bytes == other.bytes
120
+ case int():
121
+ parsed = DataSize(b=other)
122
+ return self.bytes == parsed.bytes
123
+ case str():
124
+ parsed = DataSize.parse(value=other)
125
+ return self.bytes == parsed.bytes
126
+ case _:
127
+ return NotImplemented
128
+
129
+ def __str__(self) -> str:
130
+ return f"{self.bytes}B"
131
+
132
+ def __repr__(self) -> str:
133
+ return f"{self.__class__.__name__}(bytes={self.bytes})"
134
+
135
+ def __hash__(self) -> int:
136
+ return hash(self.bytes)
137
+
138
+ def __lt__(self, other: Union["DataSize", int, str]) -> bool:
139
+ match other:
140
+ case DataSize():
141
+ return self.bytes < other.bytes
142
+ case int():
143
+ parsed = DataSize(b=other)
144
+ return self.bytes < parsed.bytes
145
+ case str():
146
+ parsed = DataSize.parse(value=other)
147
+ return self.bytes < parsed.bytes
148
+ case _:
149
+ return NotImplemented
150
+
151
+ @classmethod
152
+ def __get_validators__(cls):
153
+ yield cls.validate
154
+
155
+ @classmethod
156
+ def validate(cls, value: Union["DataSize", int, str]) -> "DataSize":
157
+ match value:
158
+ case cls():
159
+ return value
160
+ case int():
161
+ return cls(b=value)
162
+ case str():
163
+ return cls.parse(value=value)
164
+ case _:
165
+ raise ValueError(f"Cannot convert {value} to {cls.__name__}")
166
+
167
+ @classmethod
168
+ def __get_pydantic_core_schema__(
169
+ cls,
170
+ _source_type: Any,
171
+ _handler: GetCoreSchemaHandler,
172
+ ) -> core_schema.CoreSchema:
173
+ return core_schema.union_schema(
174
+ [
175
+ core_schema.is_instance_schema(cls),
176
+ core_schema.chain_schema(
177
+ [
178
+ core_schema.int_schema(),
179
+ core_schema.no_info_plain_validator_function(cls.validate),
180
+ ]
181
+ ),
182
+ core_schema.chain_schema(
183
+ [
184
+ core_schema.str_schema(),
185
+ core_schema.no_info_plain_validator_function(cls.parse),
186
+ ]
187
+ ),
188
+ ]
189
+ )
190
+
191
+ @classmethod
192
+ def __get_pydantic_json_schema__(
193
+ cls,
194
+ _core_schema: core_schema.CoreSchema,
195
+ _handler: GetJsonSchemaHandler,
196
+ ) -> JsonSchemaValue:
197
+ return {
198
+ "type": "string",
199
+ "description": "Data size value (e.g. '5MB', '1GB')",
200
+ "examples": ["512B", "10KB", "5MB", "1GB", "2TB"],
201
+ }
idegym/api/docker.py ADDED
@@ -0,0 +1,71 @@
1
+ from enum import StrEnum
2
+ from typing import Any, Optional
3
+
4
+ from idegym.api.data import DataSize
5
+ from pydantic import BaseModel, Field, model_serializer, model_validator
6
+ from pydantic_core.core_schema import SerializerFunctionWrapHandler
7
+
8
+
9
+ class BaseImage(StrEnum):
10
+ DEBIAN = "debian-bookworm-20250520-slim"
11
+ UBUNTU = "ubuntu-jammy-20250530"
12
+
13
+ DEFAULT = DEBIAN
14
+
15
+
16
+ class ContainerConfig(BaseModel):
17
+ cpu_period: Optional[int] = Field(
18
+ description="Length of a CPU period in microseconds",
19
+ default=None,
20
+ gt=0,
21
+ )
22
+ cpu_quota: Optional[int] = Field(
23
+ description="Microseconds of CPU time that the container can get in a CPU period",
24
+ default=None,
25
+ gt=0,
26
+ )
27
+ cpu_rt_period: Optional[int] = Field(
28
+ description="Limit CPU real-time period in microseconds",
29
+ default=None,
30
+ gt=0,
31
+ )
32
+ cpu_rt_runtime: Optional[int] = Field(
33
+ description="Limit CPU real-time runtime in microseconds",
34
+ default=None,
35
+ gt=0,
36
+ )
37
+ cpu_shares: Optional[int] = Field(
38
+ description="CPU shares (relative weight)",
39
+ default=None,
40
+ gt=0,
41
+ )
42
+ memory: Optional[DataSize] = Field(
43
+ description="Memory limit",
44
+ ge="6MB",
45
+ default=None,
46
+ )
47
+ memory_reservation: Optional[DataSize] = Field(
48
+ description="Memory soft limit",
49
+ ge="6MB",
50
+ default=None,
51
+ )
52
+ memory_swappiness: Optional[int] = Field(
53
+ description="Tune a container's memory swappiness behavior",
54
+ default=None,
55
+ ge=0,
56
+ le=100,
57
+ )
58
+
59
+ @model_serializer(mode="wrap")
60
+ def serialize_model(self, nxt: SerializerFunctionWrapHandler) -> dict[str, Any]:
61
+ return {
62
+ key: (str(value) if key in ("memory", "memory_reservation") and value is not None else value)
63
+ for key, value in nxt(self).items()
64
+ }
65
+
66
+ @model_validator(mode="after")
67
+ def validate_cpu_constraints(self):
68
+ if self.cpu_rt_runtime is not None and self.cpu_rt_period is not None:
69
+ if self.cpu_rt_runtime > self.cpu_rt_period:
70
+ raise ValueError("'cpu_rt_runtime' must be less than or equal to 'cpu_rt_period'")
71
+ return self
idegym/api/download.py ADDED
@@ -0,0 +1,41 @@
1
+ from typing import Optional
2
+
3
+ from idegym.api.type import AuthType, HttpUrl
4
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
5
+
6
+
7
+ class Authorization(BaseModel):
8
+ """Authorization credentials. Both fields are excluded from serialization."""
9
+
10
+ type: Optional[AuthType] = Field(
11
+ default=None,
12
+ exclude=True,
13
+ repr=False,
14
+ )
15
+ token: Optional[str] = Field(
16
+ default=None,
17
+ exclude=True,
18
+ repr=False,
19
+ )
20
+
21
+ model_config = ConfigDict(frozen=True)
22
+
23
+ @model_validator(mode="after")
24
+ def validate_auth(self):
25
+ if self.type is not None and self.token is None:
26
+ raise ValueError("Authorization token is required when type is specified")
27
+ return self
28
+
29
+
30
+ class ArchiveDescriptor(BaseModel):
31
+ name: str = Field(pattern=r"^.+\.(?:zip|tar\.gz)$")
32
+ url: HttpUrl
33
+
34
+ model_config = ConfigDict(frozen=True)
35
+
36
+
37
+ class DownloadRequest(BaseModel):
38
+ descriptor: ArchiveDescriptor
39
+ auth: Authorization = Field(default_factory=Authorization)
40
+
41
+ model_config = ConfigDict(frozen=True)
@@ -0,0 +1,10 @@
1
+ class IdeGYMException(Exception):
2
+ pass
3
+
4
+
5
+ class InspectionsNotReadyException(IdeGYMException):
6
+ pass
7
+
8
+
9
+ class ResourceDeletionFailedException(IdeGYMException):
10
+ """Raised when one or more Kubernetes resources fail to be deleted."""
idegym/api/git.py ADDED
@@ -0,0 +1,103 @@
1
+ from enum import StrEnum
2
+
3
+ from idegym.api.download import ArchiveDescriptor
4
+ from pydantic import BaseModel, ConfigDict, Field
5
+
6
+
7
+ class GitServer(StrEnum):
8
+ GITHUB = "github.com"
9
+ GITLAB = "gitlab.com"
10
+ HUGGING_FACE_DATASETS = "huggingface.co/datasets"
11
+ HUGGING_FACE_MODELS = "huggingface.co"
12
+
13
+
14
+ class GitRepository(BaseModel):
15
+ server: GitServer
16
+ owner: str = Field(min_length=1)
17
+ name: str = Field(min_length=1)
18
+
19
+ model_config = ConfigDict(frozen=True)
20
+
21
+ @property
22
+ def qualified_name(self) -> str:
23
+ return f"{self.owner}/{self.name}"
24
+
25
+ @property
26
+ def url(self) -> str:
27
+ return f"https://{self.server}/{self.qualified_name}.git"
28
+
29
+ def head(self) -> "GitRepositorySnapshot":
30
+ return self.at("HEAD")
31
+
32
+ def at(self, reference: str) -> "GitRepositorySnapshot":
33
+ return GitRepositorySnapshot(repository=self, reference=reference)
34
+
35
+ @classmethod
36
+ def parse(cls, url: str) -> "GitRepository":
37
+ if not url.startswith("https://"):
38
+ raise ValueError("URL must start with 'https://'")
39
+ value = url.removeprefix("https://").removesuffix(".git")
40
+ for server in GitServer:
41
+ if value.startswith(server):
42
+ path = value.removeprefix(server)
43
+ _, owner, name, *other = path.split("/")
44
+ if len(other) != 0:
45
+ raise ValueError(f"URL must be in the format 'https://{server}/<owner>/<name>.git', got '{url}'")
46
+ return cls(owner=owner, name=name, server=server)
47
+ raise ValueError(f"Invalid URL: '{url}'")
48
+
49
+
50
+ class GitRepositorySnapshot(BaseModel):
51
+ repository: GitRepository
52
+ reference: str = Field(
53
+ description="Branch, tag, commit hash, or HEAD",
54
+ pattern=r"^(HEAD|([\w\-.]+)(/[\w\-.]+)?|refs/(heads|tags|remotes)/[\w\-./]+)$",
55
+ default="HEAD",
56
+ )
57
+
58
+ model_config = ConfigDict(frozen=True)
59
+
60
+ @property
61
+ def filename(self) -> str:
62
+ return f"{self.repository.name}-{self.reference}.tar.gz"
63
+
64
+ def descriptor(self) -> ArchiveDescriptor:
65
+ url = self.repository.url.removesuffix(".git")
66
+ match self.repository.server:
67
+ case GitServer.GITHUB:
68
+ url += f"/archive/{self.reference}.tar.gz"
69
+ case GitServer.GITLAB:
70
+ url += f"/-/archive/{self.reference}/{self.repository.name}-{self.reference}.tar.gz"
71
+ case _:
72
+ # Downloading entire repositories from HuggingFace is not supported!
73
+ raise NotImplementedError(f"Unsupported server: '{self.repository.server}'")
74
+ return ArchiveDescriptor(name=self.filename, url=url)
75
+
76
+ def resource(self, path: str) -> "GitRepositoryResource":
77
+ return GitRepositoryResource(snapshot=self, path=path)
78
+
79
+
80
+ class GitRepositoryResource(BaseModel):
81
+ snapshot: GitRepositorySnapshot
82
+ path: str = Field(description="Relative path within the repository")
83
+
84
+ model_config = ConfigDict(frozen=True)
85
+
86
+ @property
87
+ def filename(self) -> str:
88
+ *_, filename = self.path.split("/")
89
+ return filename
90
+
91
+ def descriptor(self) -> ArchiveDescriptor:
92
+ url = self.snapshot.repository.url.removesuffix(".git")
93
+ match self.snapshot.repository.server:
94
+ case GitServer.GITHUB:
95
+ url = url.replace(GitServer.GITHUB, "raw.githubusercontent.com")
96
+ url += f"/{self.snapshot.reference}/{self.path}"
97
+ case GitServer.GITLAB:
98
+ url += f"/-/raw/{self.snapshot.reference}/{self.path}"
99
+ case GitServer.HUGGING_FACE_DATASETS | GitServer.HUGGING_FACE_MODELS:
100
+ url += f"/resolve/{self.snapshot.reference}/{self.path}"
101
+ case _:
102
+ raise NotImplementedError(f"Unsupported server: '{self.snapshot.repository.server}'")
103
+ return ArchiveDescriptor(name=self.filename, url=url)
idegym/api/health.py ADDED
@@ -0,0 +1,8 @@
1
+ from typing import Optional
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+
6
+ class HealthCheckResponse(BaseModel):
7
+ status: str = Field(description="Service health status, e.g. 'healthy'")
8
+ message: Optional[str] = Field(default=None, description="Optional message with details")
@@ -0,0 +1,28 @@
1
+ from json import dumps as dump_json
2
+ from typing import Any, Optional
3
+
4
+ from idegym.api.download import DownloadRequest
5
+ from idegym.utils.hashing import md5
6
+ from pydantic import BaseModel, ConfigDict, Field
7
+
8
+
9
+ class ImageBuildSpec(BaseModel):
10
+ name: Optional[str] = None
11
+ request: Optional[DownloadRequest] = Field(default=None, description="Optional project download request")
12
+ dockerfile_content: str = Field(description="Fully rendered Dockerfile content", min_length=1)
13
+ labels: dict[str, str] = Field(default_factory=dict, description="Image labels")
14
+ context_path: str = Field(default=".", description="Docker build context path")
15
+ platforms: list[str] = Field(default_factory=list, description="Build target platforms")
16
+ runtime_class_name: str = Field(default="gvisor", description="Kubernetes runtime class name")
17
+ resources: Optional[dict[str, Any]] = Field(default=None, description="Build resources")
18
+
19
+ model_config = ConfigDict(extra="forbid")
20
+
21
+ def image_version(self) -> str:
22
+ identifiers = []
23
+ if self.request is not None:
24
+ identifiers.append(dump_json(self.request.descriptor.model_dump(mode="json"), sort_keys=True))
25
+ identifiers.append(dump_json(self.labels, sort_keys=True))
26
+ identifiers.append(self.context_path)
27
+ identifiers.append(self.dockerfile_content)
28
+ return md5(*identifiers)
File without changes
@@ -0,0 +1,34 @@
1
+ from idegym.api.orchestrator.jobs import JobPollResult
2
+ from pydantic import BaseModel, Field, field_validator, model_validator
3
+ from yaml import YAMLError
4
+ from yaml import safe_load as parse_yaml
5
+
6
+
7
+ class BuildFromYamlRequest(BaseModel):
8
+ namespace: str = Field(default="idegym")
9
+ yaml_content: str = Field(description="YAML content containing image build job definitions")
10
+
11
+ @field_validator("yaml_content")
12
+ def validate_yaml_content(cls, value: str) -> str:
13
+ try:
14
+ parse_yaml(value)
15
+ return value
16
+ except YAMLError as ex:
17
+ raise ValueError("Yaml content is not valid") from ex
18
+
19
+
20
+ class BuildFromYamlResponse(BaseModel):
21
+ job_names: list[str] = Field(description="Names of started Kubernetes build jobs")
22
+
23
+
24
+ class BuildJobsSummary(BaseModel):
25
+ total_jobs: int = Field(ge=0)
26
+ failed_jobs: int = Field(ge=0)
27
+ total_time: str = Field(description="Total elapsed time, e.g. '12.34s'")
28
+ jobs_results: list[JobPollResult]
29
+
30
+ @model_validator(mode="after")
31
+ def validate_jobs_results(self):
32
+ if not len(self.jobs_results) == self.total_jobs:
33
+ raise ValueError("Number of jobs results does not match total jobs")
34
+ return self
@@ -0,0 +1,67 @@
1
+ from enum import StrEnum
2
+ from typing import Optional
3
+ from uuid import UUID
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class AvailabilityStatus(StrEnum):
9
+ ALIVE = "ALIVE"
10
+ FINISHED = "FINISHED"
11
+ REUSED = "REUSED"
12
+ FAILED_TO_START = "FAILED_TO_START"
13
+ STOPPED = "STOPPED"
14
+ KILLED = "KILLED"
15
+ CRASHED = "CRASHED"
16
+ DELETION_FAILED = "DELETION_FAILED"
17
+ RESTART_FAILED = "RESTART_FAILED"
18
+
19
+ @property
20
+ def is_terminal(self) -> bool:
21
+ """Returns True if this status represents a terminal state."""
22
+ return self in {
23
+ AvailabilityStatus.FAILED_TO_START,
24
+ AvailabilityStatus.KILLED,
25
+ AvailabilityStatus.STOPPED,
26
+ AvailabilityStatus.CRASHED,
27
+ AvailabilityStatus.DELETION_FAILED,
28
+ AvailabilityStatus.RESTART_FAILED,
29
+ }
30
+
31
+
32
+ class RegisterClientRequest(BaseModel):
33
+ name: str = Field(description="Human-readable name for the client")
34
+ nodes_count: int = Field(default=0, ge=0, description="Number of nodes to spin up for the client")
35
+ namespace: str = Field(default="idegym")
36
+
37
+
38
+ class SendClientHeartbeatRequest(BaseModel):
39
+ client_id: UUID
40
+ availability: AvailabilityStatus = Field(default=AvailabilityStatus.ALIVE)
41
+
42
+
43
+ class StopClientRequest(BaseModel):
44
+ client_id: UUID
45
+ namespace: str = Field(default="idegym")
46
+
47
+
48
+ class StopClientResponse(BaseModel):
49
+ operation_id: Optional[int] = Field(default=None, description="Async operation ID to poll for client stop status")
50
+
51
+
52
+ class FinishClientRequest(BaseModel):
53
+ client_id: UUID
54
+ namespace: str = Field(default="idegym")
55
+
56
+
57
+ class RegisteredClientResponse(BaseModel):
58
+ id: UUID
59
+ name: str = Field(default=None)
60
+ nodes_count: int = Field(ge=0)
61
+ namespace: str
62
+ last_heartbeat_time: int = Field(description="Epoch milliseconds")
63
+ availability: str
64
+ created_at: int = Field(description="Epoch milliseconds")
65
+ operation_id: Optional[int] = Field(
66
+ default=None, description="Async operation ID to poll for client registration status"
67
+ )
@@ -0,0 +1,21 @@
1
+ from typing import Optional
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+
6
+ class JobStatusResponse(BaseModel):
7
+ id: int = Field(default=0)
8
+ job_name: str
9
+ status: str
10
+ created_at: int = Field(default=0, description="Epoch milliseconds")
11
+ updated_at: int = Field(default=0, description="Epoch milliseconds")
12
+ details: Optional[str] = Field(default=None)
13
+ tag: Optional[str] = Field(default=None)
14
+ request_id: Optional[str] = Field(default=None)
15
+
16
+
17
+ class JobPollResult(BaseModel):
18
+ job_name: str
19
+ status: str
20
+ tag: Optional[str] = Field(default=None)
21
+ details: Optional[str] = Field(default=None)
@@ -0,0 +1,58 @@
1
+ from enum import StrEnum
2
+ from typing import Optional
3
+ from uuid import UUID
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class AsyncOperationType(StrEnum):
9
+ REGISTER_CLIENT_WITH_NODES = "REGISTER_CLIENT_WITH_NODES"
10
+ START_SERVER = "START_SERVER"
11
+ RESTART_SERVER = "RESTART_SERVER"
12
+ STOP_SERVER = "STOP_SERVER"
13
+ STOP_CLIENT = "STOP_CLIENT"
14
+ FORWARD_REQUEST = "FORWARD_REQUEST"
15
+
16
+
17
+ class AsyncOperationStatus(StrEnum):
18
+ SCHEDULED = "SCHEDULED"
19
+ IN_PROGRESS = "IN_PROGRESS"
20
+ FAILED = "FAILED"
21
+ SUCCEEDED = "SUCCEEDED"
22
+ CANCELLED = "CANCELLED"
23
+ FINISHED_BY_WATCHER = "FINISHED_BY_WATCHER"
24
+
25
+
26
+ class AsyncOperationStatusResponse(BaseModel):
27
+ id: int
28
+ request_type: str
29
+ status: str
30
+
31
+ request: Optional[str] = Field(default=None, description="Original request payload (JSON)")
32
+ result: Optional[str] = Field(default=None, description="Operation result payload (JSON or string)")
33
+
34
+ client_id: Optional[UUID] = Field(default=None)
35
+ server_id: Optional[int] = Field(default=None)
36
+
37
+ orchestrator_pod: Optional[str] = Field(default=None)
38
+
39
+ scheduled_at: int = Field(description="Epoch milliseconds")
40
+ started_at: Optional[int] = Field(default=None, description="Epoch milliseconds")
41
+ finished_at: Optional[int] = Field(default=None, description="Epoch milliseconds")
42
+
43
+
44
+ class ForwardRequestPayload(BaseModel):
45
+ method: str
46
+ path: str
47
+ headers: dict[str, str] = Field(default_factory=dict, description="Sanitized request headers")
48
+ body: Optional[str] = Field(default=None, description="Request body as text, if present")
49
+ target_url: str = Field(description="Resolved target URL to forward the request to")
50
+ server_id: int
51
+
52
+
53
+ class ForwardRequestResponse(BaseModel):
54
+ async_operation_id: Optional[int] = Field(default=None, description="Async operation ID to poll for request status")
55
+
56
+ status_code: Optional[int] = Field(default=None)
57
+ headers: Optional[dict[str, str]] = Field(default_factory=dict, description="Sanitized response headers")
58
+ body: Optional[str] = Field(default=None)
@@ -0,0 +1,95 @@
1
+ from enum import StrEnum
2
+ from typing import Any, Optional
3
+ from uuid import UUID
4
+
5
+ from idegym.api.type import KubernetesNodeSelector, KubernetesObjectName, OCIImageName
6
+ from pydantic import BaseModel, Field
7
+
8
+
9
+ class ServerReuseStrategy(StrEnum):
10
+ NONE = "NONE"
11
+ RESTART = "RESTART"
12
+ RESET = "RESET"
13
+ CHECKPOINT = "CHECKPOINT"
14
+
15
+
16
+ class ServerKind(StrEnum):
17
+ IDEGYM = "idegym"
18
+ OPENENV = "openenv"
19
+
20
+
21
+ class StartServerRequest(BaseModel):
22
+ client_id: UUID
23
+ namespace: str = Field(default="idegym")
24
+ image_tag: OCIImageName
25
+ server_name: KubernetesObjectName = Field(default="default-idegym-server")
26
+ runtime_class_name: Optional[str] = Field(default=None)
27
+ run_as_root: bool = Field(default=False)
28
+ service_port: int = Field(default=80, ge=0, le=65535)
29
+ container_port: int = Field(default=8000, ge=0, le=65535)
30
+ resources: Optional[dict[str, Any]] = Field(default=None, description="Kubernetes resource requirements")
31
+ node_selector: Optional[KubernetesNodeSelector] = Field(default=None)
32
+ server_start_wait_timeout_in_seconds: int = Field(
33
+ default=60, description="Seconds to wait for server readiness", ge=0
34
+ )
35
+ reuse_strategy: ServerReuseStrategy = Field(
36
+ default=ServerReuseStrategy.RESET,
37
+ description="What to do with an existing matching server instead of starting a new one",
38
+ )
39
+ server_kind: ServerKind = Field(default=ServerKind.IDEGYM, description="Server type: idegym or openenv")
40
+
41
+
42
+ class StopServerRequest(BaseModel):
43
+ client_id: UUID
44
+ namespace: str = Field(default="idegym")
45
+ server_id: int
46
+
47
+
48
+ class FinishServerRequest(BaseModel):
49
+ client_id: UUID
50
+ namespace: str = Field(default="idegym")
51
+ server_id: int
52
+
53
+
54
+ class RestartServerRequest(BaseModel):
55
+ client_id: UUID
56
+ namespace: str = Field(default="idegym")
57
+ server_id: int
58
+ server_start_wait_timeout_in_seconds: int = Field(
59
+ default=60, description="Seconds to wait for server readiness after restart", ge=0
60
+ )
61
+
62
+
63
+ class StartServerResponse(BaseModel):
64
+ namespace: str
65
+ client_id: UUID
66
+ operation_id: Optional[int] = Field(default=None, description="Async operation ID to poll for server start status")
67
+ server_id: Optional[int] = Field(default=None)
68
+ server_name: Optional[str] = Field(default=None, description="Logical server name as provided in the request")
69
+ generated_name: Optional[str] = Field(default=None, description="Generated Kubernetes resource name")
70
+ service_name: Optional[str] = Field(default=None, description="Kubernetes Service name for the server")
71
+ image_tag: Optional[str] = Field(default=None)
72
+ need_to_reset: bool = Field(default=False, description="True if the reused server requires a project reset")
73
+
74
+
75
+ class ErrorResponse(BaseModel):
76
+ status_code: Optional[int] = Field(default=None)
77
+ headers: Optional[dict[str, str]] = Field(default_factory=dict, description="Sanitized response headers")
78
+ body: Optional[str] = Field(default=None)
79
+
80
+
81
+ class ServerActionResponse(BaseModel):
82
+ server_name: str
83
+ message: str
84
+ operation_id: Optional[int] = Field(default=None, description="Async operation ID to poll for server action status")
85
+
86
+
87
+ class ServerRequestResponse(BaseModel):
88
+ id: UUID
89
+ server_id: int
90
+ request: str = Field(description="Original request payload or summary")
91
+ path: Optional[str] = Field(default=None)
92
+ started_at: int = Field(description="Epoch milliseconds", ge=0)
93
+ result: Optional[str] = Field(default=None)
94
+ finished_at: Optional[int] = Field(default=None, description="Epoch milliseconds", ge=0)
95
+ status: str
idegym/api/paths.py ADDED
@@ -0,0 +1,45 @@
1
+ from enum import StrEnum
2
+
3
+ API_BASE_PATH = "/api"
4
+
5
+
6
+ class APIPath(StrEnum):
7
+ pass
8
+
9
+
10
+ class ActuatorPath(APIPath):
11
+ HEALTH = "/health"
12
+ LOG = "/log"
13
+ METRICS = "/metrics"
14
+ SHUTDOWN = "/shutdown"
15
+
16
+
17
+ class OpenenvPath(APIPath):
18
+ """Path constants for OpenEnv servers deployed alongside IdeGYM without the IdeGYM API."""
19
+
20
+ HEALTH = "/health"
21
+
22
+
23
+ class FSPath(APIPath):
24
+ LIST_DIRECTORY = "/fs/ls"
25
+ READ_FILE = "/fs/cat"
26
+ CREATE_FILE = "/fs/touch"
27
+ CREATE_DIRECTORY = "/fs/mkdir"
28
+ DELETE_PATH = "/fs/rm"
29
+
30
+
31
+ class ProjectPath(APIPath):
32
+ RESET = "/project/reset"
33
+
34
+
35
+ class RewardsPath(APIPath):
36
+ SETUP = "/rewards/setup"
37
+ COMPILATION = "/rewards/compilation"
38
+ TEST = "/rewards/test"
39
+
40
+
41
+ class ToolsPath(APIPath):
42
+ BASH = "/tools/bash"
43
+ CREATE_FILE = "/tools/file/create"
44
+ EDIT_FILE = "/tools/file/edit"
45
+ PATCH_FILE = "/tools/file/patch"
File without changes
@@ -0,0 +1,14 @@
1
+ from idegym.api.status import Status
2
+ from pydantic import BaseModel, Field
3
+
4
+
5
+ class ResetRequest(BaseModel):
6
+ timeout: float = Field(default=600.0, description="Timeout for the reset operation in seconds")
7
+ graceful_termination_timeout: float = Field(
8
+ default=2.0, description="Timeout in seconds for graceful process termination"
9
+ )
10
+
11
+
12
+ class ResetResult(BaseModel):
13
+ status: Status
14
+ output: str
File without changes
@@ -0,0 +1,19 @@
1
+ from idegym.api.status import Status
2
+ from pydantic import BaseModel, Field
3
+
4
+
5
+ class CompilationRequest(BaseModel):
6
+ compilation_script: str = Field(default="./gradlew assemble")
7
+ timeout: float = Field(default=600.0, description="Timeout for the compilation in seconds")
8
+ graceful_termination_timeout: float = Field(
9
+ default=2.0, description="Timeout in seconds for graceful process termination"
10
+ )
11
+
12
+
13
+ class CompilationResult(BaseModel):
14
+ status: Status
15
+ output: str
16
+
17
+
18
+ class CompilationError(BaseModel):
19
+ message: str
@@ -0,0 +1,18 @@
1
+ from typing import Optional
2
+
3
+ from idegym.api.status import Status
4
+ from pydantic import BaseModel, Field
5
+
6
+
7
+ class InspectionRequest(BaseModel):
8
+ files_to_analyze: Optional[list[str]] = Field(default=None)
9
+ timeout: float = Field(default=600.0, description="Timeout for the analysis in seconds")
10
+
11
+
12
+ class InspectionResult(BaseModel):
13
+ status: Status
14
+ output: str = Field(description="Analysis output, or error message if analysis failed", default="")
15
+
16
+
17
+ class InspectionError(BaseModel):
18
+ message: str
@@ -0,0 +1,19 @@
1
+ from idegym.api.status import Status
2
+ from pydantic import BaseModel, Field
3
+
4
+
5
+ class SetupRequest(BaseModel):
6
+ setup_check_script: str = Field(default="true")
7
+ timeout: float = Field(default=600.0, description="Timeout for the setup in seconds")
8
+ graceful_termination_timeout: float = Field(
9
+ default=2.0, description="Timeout in seconds for graceful process termination"
10
+ )
11
+
12
+
13
+ class SetupResult(BaseModel):
14
+ status: Status
15
+ output: str
16
+
17
+
18
+ class SetupError(BaseModel):
19
+ message: str
@@ -0,0 +1,26 @@
1
+ from idegym.api.status import Status
2
+ from pydantic import BaseModel, Field
3
+
4
+
5
+ class TestRequest(BaseModel):
6
+ test_script: str = Field(default="python -m pytest --junitxml=test-results.xml")
7
+ timeout: float = Field(default=600.0, description="Timeout for the test in seconds")
8
+ graceful_termination_timeout: float = Field(
9
+ default=2.0, description="Timeout in seconds for graceful process termination"
10
+ )
11
+
12
+
13
+ class TestScores(BaseModel):
14
+ total: int = Field(ge=0)
15
+ passed: int = Field(ge=0)
16
+ failed: int = Field(ge=0)
17
+ skipped: int = Field(ge=0)
18
+
19
+
20
+ class TestReport(BaseModel):
21
+ status: Status
22
+ scores: TestScores
23
+
24
+
25
+ class TestError(BaseModel):
26
+ message: str
idegym/api/status.py ADDED
@@ -0,0 +1,7 @@
1
+ from enum import StrEnum
2
+
3
+
4
+ class Status(StrEnum):
5
+ SUCCESS = "success"
6
+ FAILURE = "failure"
7
+ IN_PROGRESS = "in_progress"
File without changes
@@ -0,0 +1,19 @@
1
+ from pydantic import BaseModel, Field
2
+
3
+
4
+ class BashCommandRequest(BaseModel):
5
+ command: str = Field(description="Command to execute as a bash script")
6
+ timeout: float = Field(default=600.0, description="Timeout for the command execution in seconds")
7
+ graceful_termination_timeout: float = Field(
8
+ default=2.0, description="Timeout in seconds for graceful process termination"
9
+ )
10
+
11
+
12
+ class BashCommandResponse(BaseModel):
13
+ stdout: str
14
+ stderr: str
15
+ exit_code: int
16
+
17
+
18
+ class BashCommandErrorResponse(BaseModel):
19
+ message: str
@@ -0,0 +1,26 @@
1
+ from idegym.api.status import Status
2
+ from pydantic import BaseModel, Field
3
+
4
+
5
+ class FileRequest(BaseModel):
6
+ file_path: str
7
+
8
+
9
+ class CreateFileRequest(FileRequest):
10
+ content: str
11
+
12
+
13
+ class EditFileRequest(FileRequest):
14
+ start_line: int = Field(ge=0)
15
+ end_line: int = Field(ge=0)
16
+ new_content: str
17
+
18
+
19
+ class PatchFileRequest(FileRequest):
20
+ """Applies a unified diff patch generated by `diff_patch.compute_diff`."""
21
+
22
+ patch: str
23
+
24
+
25
+ class FileResult(BaseModel):
26
+ status: Status
idegym/api/type.py ADDED
@@ -0,0 +1,56 @@
1
+ from datetime import timedelta
2
+ from typing import Annotated, Literal, Type, TypeAlias
3
+
4
+ from pydantic import AnyHttpUrl, BeforeValidator, IPvAnyAddress, StringConstraints, TypeAdapter
5
+
6
+ ipv_address_adapter = TypeAdapter(IPvAnyAddress)
7
+ http_url_adapter = TypeAdapter(AnyHttpUrl)
8
+
9
+ HttpUrl = Annotated[str, BeforeValidator(lambda value: str(http_url_adapter.validate_python(value)))]
10
+ IPvAddress = Annotated[str, BeforeValidator(lambda value: str(ipv_address_adapter.validate_python(value)))]
11
+
12
+ # https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#rfc-1035-label-names
13
+ KubernetesObjectName = Annotated[
14
+ str,
15
+ StringConstraints(
16
+ min_length=1,
17
+ max_length=63,
18
+ # language=regexp
19
+ pattern="^[a-z]([-a-z0-9]*[a-z0-9])?$",
20
+ ),
21
+ ]
22
+ # https://github.com/opencontainers/distribution-spec/blob/main/spec.md#workflow-categories
23
+ OCIImageName = Annotated[
24
+ str,
25
+ StringConstraints(
26
+ min_length=1,
27
+ max_length=383, # The maximum length is set to 255 + 128
28
+ # language=regexp
29
+ pattern="^[a-z0-9._/:@-]+$",
30
+ ),
31
+ ]
32
+ # https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#syntax-and-character-set
33
+ KubernetesLabelKey = Annotated[
34
+ str,
35
+ StringConstraints(
36
+ min_length=1,
37
+ max_length=317, # 253 (prefix) + 1 (/) + 63 (name)
38
+ # language=regexp
39
+ pattern=r"^([a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?/)?[a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?$",
40
+ ),
41
+ ]
42
+ KubernetesLabelValue = Annotated[
43
+ str,
44
+ StringConstraints(
45
+ max_length=63,
46
+ # language=regexp
47
+ pattern=r"^([a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?)?$",
48
+ ),
49
+ ]
50
+
51
+
52
+ KubernetesNodeSelector: TypeAlias = dict[KubernetesLabelKey, KubernetesLabelValue]
53
+ AuthType: Type[str] = Literal["Basic", "Bearer", "Token"]
54
+ Duration: TypeAlias = timedelta
55
+ LogLevel: TypeAlias = int
56
+ LogLevelName: Type[str] = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.4
2
+ Name: idegym-api
3
+ Version: 0.8.0
4
+ Summary: IdeGYM API
5
+ Author-email: Daniil Mironov <daniil.mironov@jetbrains.com>, Ozren Dabić <ozren.dabic@jetbrains.com>, Vladimir Poliakov <vladimir.poliakov@jetbrains.com>, Vseslav Kasatskii <vseslav.kasatskii@jetbrains.com>
6
+ Requires-Python: >=3.12
7
+ Requires-Dist: idegym-common-utils>=0.8.0
8
+ Requires-Dist: pydantic>=2.10.6
9
+ Requires-Dist: pyyaml>=6.0.2
10
+ Requires-Dist: structlog>=25.4.0
@@ -0,0 +1,33 @@
1
+ idegym/api/__init__.py,sha256=jQ8BJhwmXkWE6I6fVaU6bcAWeoZEnDaSL7AE3mYOSns,227
2
+ idegym/api/auth.py,sha256=vCzMWcYlbpioEHOqS6Hu_Fwyude83n0BwKYaLBLVBYA,771
3
+ idegym/api/config.py,sha256=udYkygaGtqBb7J-yk5Ei7_JRa6OfFe0Ckb8iNx7gM2I,7403
4
+ idegym/api/ctx.py,sha256=3RTOSaZvaAunZe1UPF36KwmW22L9sba98TuA6ogeAe4,340
5
+ idegym/api/data.py,sha256=Dk7rBglat28fWRey_YDWcu6HS9xvoDDbPRe9i7DLSM4,5730
6
+ idegym/api/docker.py,sha256=aZgnnTbaK4uEnr1URK7YBi_pVcFWGdBAwOrp1Tb70_E,2221
7
+ idegym/api/download.py,sha256=sBVk569b16CO8Vs7vxpOSyqrVWJzHMgQGkEF833fnYY,1076
8
+ idegym/api/exceptions.py,sha256=7V4xt_4MtqtLFV43PTBib69kr-uXZGxgqPKfzqMIAv4,240
9
+ idegym/api/git.py,sha256=CDJ879GzImiZo5r5-hhJ7HfEPw7wz8E-Oe0JQ2bGICM,3865
10
+ idegym/api/health.py,sha256=jmhn8JPdVlTQDVoLLzElyL-Z254E-svDwRzzeF3nuJ0,278
11
+ idegym/api/image_build.py,sha256=Nch0zhWuSza9DURdi9flOuxGmu8y1q7_9tPHyEATmc0,1385
12
+ idegym/api/paths.py,sha256=8S232AexPiVLy_27HrMs6fWvvFzMTfloyf2KS2w5Sek,879
13
+ idegym/api/status.py,sha256=Hu3ZggQIqhh1TyvLt7o5wXBakvHCUupKpbD2Ihkz3IE,130
14
+ idegym/api/type.py,sha256=fI8E8vAI9p23j31L-sZMye2VN9QXLHe88ab6Ta8Tjos,1950
15
+ idegym/api/orchestrator/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
+ idegym/api/orchestrator/build.py,sha256=aBh_77A_Zst_PCSXwqVtt5ezjYVI8nGywsRnsXdMOOg,1219
17
+ idegym/api/orchestrator/clients.py,sha256=7-NTzuEeAk9kJlwgpVW-F7ZjlH0Y2RoaFTr28srEQAs,2043
18
+ idegym/api/orchestrator/jobs.py,sha256=UCrGuzSqt5NF8_QP2KA7Srs_PPz_Wx4SXFGEbIalVVU,624
19
+ idegym/api/orchestrator/operations.py,sha256=XH67IeV4_gjDJOy1O8edPzWWBSvEGKlDl3THgBWXVXA,2063
20
+ idegym/api/orchestrator/servers.py,sha256=8i_ke27p9JhXlvbqfjwhrm-KxzxGiBtuErc_SvQukF4,3603
21
+ idegym/api/project/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
+ idegym/api/project/reset.py,sha256=7XJakz35TCNTkPwSleED-wp7umh-Cwf9SgcldjtMe2A,417
23
+ idegym/api/rewards/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
+ idegym/api/rewards/compilation.py,sha256=pAchlZPQ-K3qv7xOXUs2DDCbTgi6HevWoD8JaGDshhw,545
25
+ idegym/api/rewards/inspection.py,sha256=YZUsjH-eaUF4k064Z9qsfl0LaY3jN4Z0lzvMXatN8so,512
26
+ idegym/api/rewards/setup.py,sha256=omz4mMvb2NmTN_mzcUKH7GQOS_m1AU2Hodx6C0os4EM,507
27
+ idegym/api/rewards/test.py,sha256=t8ztRcETGcnvoWdm1C2S1rwM9JjuTLxVZwxttFaVGsE,694
28
+ idegym/api/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
29
+ idegym/api/tools/bash.py,sha256=_Hjlljk1BwEwueQVPNvdmtyG-vpEtjK7URe86xuR2j8,550
30
+ idegym/api/tools/file.py,sha256=JkdktteXmnJE0TLnZyZjLSA-6LKP1eHAtOoj_2fi1Og,492
31
+ idegym_api-0.8.0.dist-info/METADATA,sha256=njU_6ixGDVfOWWia8lvK-u3cQ5lSfA0n0Y88vtoRB5c,442
32
+ idegym_api-0.8.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
33
+ idegym_api-0.8.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any