dstack 0.19.1__py3-none-any.whl → 0.19.2__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.
Files changed (35) hide show
  1. dstack/_internal/cli/commands/metrics.py +138 -0
  2. dstack/_internal/cli/commands/stats.py +5 -119
  3. dstack/_internal/cli/main.py +2 -0
  4. dstack/_internal/core/backends/base/compute.py +3 -0
  5. dstack/_internal/core/backends/base/models.py +7 -7
  6. dstack/_internal/core/backends/configurators.py +9 -0
  7. dstack/_internal/core/backends/models.py +8 -0
  8. dstack/_internal/core/backends/nebius/__init__.py +0 -0
  9. dstack/_internal/core/backends/nebius/backend.py +16 -0
  10. dstack/_internal/core/backends/nebius/compute.py +270 -0
  11. dstack/_internal/core/backends/nebius/configurator.py +74 -0
  12. dstack/_internal/core/backends/nebius/models.py +108 -0
  13. dstack/_internal/core/backends/nebius/resources.py +222 -0
  14. dstack/_internal/core/errors.py +14 -0
  15. dstack/_internal/core/models/backends/base.py +2 -0
  16. dstack/_internal/proxy/lib/schemas/model_proxy.py +3 -3
  17. dstack/_internal/server/background/tasks/process_instances.py +12 -7
  18. dstack/_internal/server/routers/prometheus.py +5 -0
  19. dstack/_internal/server/security/permissions.py +19 -1
  20. dstack/_internal/server/statics/index.html +1 -1
  21. dstack/_internal/server/statics/{main-4a0fe83e84574654e397.js → main-bcb3228138bc8483cc0b.js} +7268 -125
  22. dstack/_internal/server/statics/{main-4a0fe83e84574654e397.js.map → main-bcb3228138bc8483cc0b.js.map} +1 -1
  23. dstack/_internal/server/statics/{main-da9f8c06a69c20dac23e.css → main-c0bdaac8f1ea67d499eb.css} +1 -1
  24. dstack/_internal/utils/event_loop.py +30 -0
  25. dstack/version.py +1 -1
  26. {dstack-0.19.1.dist-info → dstack-0.19.2.dist-info}/METADATA +27 -11
  27. {dstack-0.19.1.dist-info → dstack-0.19.2.dist-info}/RECORD +35 -26
  28. tests/_internal/server/background/tasks/test_process_instances.py +4 -2
  29. tests/_internal/server/routers/test_backends.py +116 -0
  30. tests/_internal/server/routers/test_prometheus.py +21 -0
  31. tests/_internal/utils/test_event_loop.py +18 -0
  32. {dstack-0.19.1.dist-info → dstack-0.19.2.dist-info}/LICENSE.md +0 -0
  33. {dstack-0.19.1.dist-info → dstack-0.19.2.dist-info}/WHEEL +0 -0
  34. {dstack-0.19.1.dist-info → dstack-0.19.2.dist-info}/entry_points.txt +0 -0
  35. {dstack-0.19.1.dist-info → dstack-0.19.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,74 @@
1
+ import json
2
+
3
+ from nebius.aio.service_error import RequestError
4
+
5
+ from dstack._internal.core.backends.base.configurator import (
6
+ BackendRecord,
7
+ Configurator,
8
+ raise_invalid_credentials_error,
9
+ )
10
+ from dstack._internal.core.backends.nebius import resources
11
+ from dstack._internal.core.backends.nebius.backend import NebiusBackend
12
+ from dstack._internal.core.backends.nebius.models import (
13
+ AnyNebiusBackendConfig,
14
+ NebiusBackendConfig,
15
+ NebiusBackendConfigWithCreds,
16
+ NebiusConfig,
17
+ NebiusCreds,
18
+ NebiusServiceAccountCreds,
19
+ NebiusStoredConfig,
20
+ )
21
+ from dstack._internal.core.models.backends.base import BackendType
22
+
23
+
24
+ class NebiusConfigurator(Configurator):
25
+ TYPE = BackendType.NEBIUS
26
+ BACKEND_CLASS = NebiusBackend
27
+
28
+ def validate_config(self, config: NebiusBackendConfigWithCreds, default_creds_enabled: bool):
29
+ assert isinstance(config.creds, NebiusServiceAccountCreds)
30
+ try:
31
+ sdk = resources.make_sdk(config.creds)
32
+ available_regions = set(resources.get_region_to_project_id_map(sdk))
33
+ except (ValueError, RequestError) as e:
34
+ raise_invalid_credentials_error(
35
+ fields=[["creds"]],
36
+ details=str(e),
37
+ )
38
+ if invalid_regions := set(config.regions or []) - available_regions:
39
+ raise_invalid_credentials_error(
40
+ fields=[["regions"]],
41
+ details=(
42
+ f"Configured regions {invalid_regions} do not exist in this Nebius tenancy."
43
+ " Omit `regions` to use all regions or select some of the available regions:"
44
+ f" {available_regions}"
45
+ ),
46
+ )
47
+
48
+ def create_backend(
49
+ self, project_name: str, config: NebiusBackendConfigWithCreds
50
+ ) -> BackendRecord:
51
+ return BackendRecord(
52
+ config=NebiusStoredConfig(
53
+ **NebiusBackendConfig.__response__.parse_obj(config).dict()
54
+ ).json(),
55
+ auth=NebiusCreds.parse_obj(config.creds).json(),
56
+ )
57
+
58
+ def get_backend_config(
59
+ self, record: BackendRecord, include_creds: bool
60
+ ) -> AnyNebiusBackendConfig:
61
+ config = self._get_config(record)
62
+ if include_creds:
63
+ return NebiusBackendConfigWithCreds.__response__.parse_obj(config)
64
+ return NebiusBackendConfig.__response__.parse_obj(config)
65
+
66
+ def get_backend(self, record: BackendRecord) -> NebiusBackend:
67
+ config = self._get_config(record)
68
+ return NebiusBackend(config=config)
69
+
70
+ def _get_config(self, record: BackendRecord) -> NebiusConfig:
71
+ return NebiusConfig.__response__(
72
+ **json.loads(record.config),
73
+ creds=NebiusCreds.parse_raw(record.auth),
74
+ )
@@ -0,0 +1,108 @@
1
+ from typing import Annotated, Literal, Optional, Union
2
+
3
+ from pydantic import Field, root_validator
4
+
5
+ from dstack._internal.core.backends.base.models import fill_data
6
+ from dstack._internal.core.models.common import CoreModel
7
+
8
+
9
+ class NebiusServiceAccountCreds(CoreModel):
10
+ type: Annotated[Literal["service_account"], Field(description="The type of credentials")] = (
11
+ "service_account"
12
+ )
13
+ service_account_id: Annotated[str, Field(description="Service account ID")]
14
+ public_key_id: Annotated[str, Field(description="ID of the service account public key")]
15
+ private_key_file: Annotated[
16
+ Optional[str], Field(description=("Path to the service account private key"))
17
+ ] = None
18
+ private_key_content: Annotated[
19
+ str,
20
+ Field(
21
+ description=(
22
+ "Content of the service account private key. When configuring via"
23
+ " `server/config.yml`, it's automatically filled from `private_key_file`."
24
+ " When configuring via UI, it has to be specified explicitly."
25
+ )
26
+ ),
27
+ ]
28
+
29
+
30
+ class NebiusServiceAccountFileCreds(CoreModel):
31
+ type: Annotated[Literal["service_account"], Field(description="The type of credentials")] = (
32
+ "service_account"
33
+ )
34
+ service_account_id: Annotated[str, Field(description="Service account ID")]
35
+ public_key_id: Annotated[str, Field(description="ID of the service account public key")]
36
+ private_key_file: Annotated[
37
+ Optional[str], Field(description=("Path to the service account private key"))
38
+ ] = None
39
+ private_key_content: Annotated[
40
+ Optional[str],
41
+ Field(
42
+ description=(
43
+ "Content of the service account private key. When configuring via"
44
+ " `server/config.yml`, it's automatically filled from `private_key_file`."
45
+ " When configuring via UI, it has to be specified explicitly."
46
+ )
47
+ ),
48
+ ] = None
49
+
50
+ @root_validator
51
+ def fill_data(cls, values):
52
+ return fill_data(
53
+ values, filename_field="private_key_file", data_field="private_key_content"
54
+ )
55
+
56
+
57
+ AnyNebiusCreds = NebiusServiceAccountCreds
58
+ NebiusCreds = AnyNebiusCreds
59
+ AnyNebiusFileCreds = NebiusServiceAccountFileCreds
60
+
61
+
62
+ class NebiusBackendConfig(CoreModel):
63
+ """
64
+ The backend config used in the API, server/config.yml, `NebiusConfigurator`.
65
+ It also serves as a base class for other backend config models.
66
+ Should not include creds.
67
+ """
68
+
69
+ type: Annotated[
70
+ Literal["nebius"],
71
+ Field(description="The type of backend"),
72
+ ] = "nebius"
73
+ regions: Annotated[
74
+ Optional[list[str]],
75
+ Field(description="The list of Nebius regions. Omit to use all regions"),
76
+ ] = None
77
+
78
+
79
+ class NebiusBackendConfigWithCreds(NebiusBackendConfig):
80
+ """
81
+ Same as `NebiusBackendConfig` but also includes creds.
82
+ """
83
+
84
+ creds: Annotated[AnyNebiusCreds, Field(description="The credentials")]
85
+
86
+
87
+ class NebiusBackendFileConfigWithCreds(NebiusBackendConfig):
88
+ creds: AnyNebiusFileCreds = Field(description="The credentials")
89
+
90
+
91
+ AnyNebiusBackendConfig = Union[NebiusBackendConfig, NebiusBackendConfigWithCreds]
92
+
93
+
94
+ class NebiusStoredConfig(NebiusBackendConfig):
95
+ """
96
+ The backend config used for config parameters in the DB.
97
+ Can extend `NebiusBackendConfig` with additional parameters.
98
+ """
99
+
100
+ pass
101
+
102
+
103
+ class NebiusConfig(NebiusStoredConfig):
104
+ """
105
+ The backend config used by `NebiusBackend` and `NebiusCompute`.
106
+ """
107
+
108
+ creds: AnyNebiusCreds
@@ -0,0 +1,222 @@
1
+ import logging
2
+ import time
3
+ from collections.abc import Container as ContainerT
4
+ from collections.abc import Generator
5
+ from contextlib import contextmanager
6
+ from tempfile import NamedTemporaryFile
7
+
8
+ from nebius.aio.authorization.options import options_to_metadata
9
+ from nebius.aio.operation import Operation as SDKOperation
10
+ from nebius.aio.service_error import RequestError, StatusCode
11
+ from nebius.aio.token.renewable import OPTION_RENEW_REQUEST_TIMEOUT, OPTION_RENEW_SYNCHRONOUS
12
+ from nebius.api.nebius.common.v1 import Operation, ResourceMetadata
13
+ from nebius.api.nebius.compute.v1 import (
14
+ AttachedDiskSpec,
15
+ CreateDiskRequest,
16
+ CreateInstanceRequest,
17
+ DeleteDiskRequest,
18
+ DeleteInstanceRequest,
19
+ DiskServiceClient,
20
+ DiskSpec,
21
+ ExistingDisk,
22
+ GetInstanceRequest,
23
+ Instance,
24
+ InstanceServiceClient,
25
+ InstanceSpec,
26
+ IPAddress,
27
+ NetworkInterfaceSpec,
28
+ PublicIPAddress,
29
+ ResourcesSpec,
30
+ SourceImageFamily,
31
+ )
32
+ from nebius.api.nebius.iam.v1 import (
33
+ ListProjectsRequest,
34
+ ListTenantsRequest,
35
+ ProjectServiceClient,
36
+ TenantServiceClient,
37
+ )
38
+ from nebius.api.nebius.vpc.v1 import ListSubnetsRequest, Subnet, SubnetServiceClient
39
+ from nebius.sdk import SDK
40
+
41
+ from dstack._internal.core.backends.nebius.models import NebiusServiceAccountCreds
42
+ from dstack._internal.core.errors import BackendError, NoCapacityError
43
+ from dstack._internal.utils.event_loop import DaemonEventLoop
44
+
45
+ #
46
+ # Guidelines on using the Nebius SDK:
47
+ #
48
+ # Do not use Request.wait() or other sync SDK methods, they suffer from deadlocks.
49
+ # Instead, use async methods and await them with LOOP.await_()
50
+ LOOP = DaemonEventLoop()
51
+ # Pass a timeout to all methods to avoid infinite waiting
52
+ REQUEST_TIMEOUT = 10
53
+ # Pass REQUEST_MD to all methods to avoid infinite retries in case of invalid credentials
54
+ REQUEST_MD = options_to_metadata(
55
+ {
56
+ OPTION_RENEW_SYNCHRONOUS: "true",
57
+ OPTION_RENEW_REQUEST_TIMEOUT: "5",
58
+ }
59
+ )
60
+
61
+ # disables log messages about errors such as invalid creds or expired timeouts
62
+ logging.getLogger("nebius").setLevel(logging.CRITICAL)
63
+
64
+
65
+ @contextmanager
66
+ def wrap_capacity_errors() -> Generator[None, None, None]:
67
+ try:
68
+ yield
69
+ except RequestError as e:
70
+ if e.status.code == StatusCode.RESOURCE_EXHAUSTED or "Quota limit exceeded" in str(e):
71
+ raise NoCapacityError(e)
72
+ raise
73
+
74
+
75
+ @contextmanager
76
+ def ignore_errors(status_codes: ContainerT[StatusCode]) -> Generator[None, None, None]:
77
+ try:
78
+ yield
79
+ except RequestError as e:
80
+ if e.status.code not in status_codes:
81
+ raise
82
+
83
+
84
+ def make_sdk(creds: NebiusServiceAccountCreds) -> SDK:
85
+ with NamedTemporaryFile("w") as f:
86
+ f.write(creds.private_key_content)
87
+ f.flush()
88
+ return SDK(
89
+ service_account_private_key_file_name=f.name,
90
+ service_account_public_key_id=creds.public_key_id,
91
+ service_account_id=creds.service_account_id,
92
+ )
93
+
94
+
95
+ def wait_for_operation(
96
+ op: SDKOperation[Operation],
97
+ timeout: float,
98
+ interval: float = 1,
99
+ ) -> None:
100
+ # Re-implementation of SDKOperation.wait() to avoid https://github.com/nebius/pysdk/issues/74
101
+ deadline = time.monotonic() + timeout
102
+ while not op.done():
103
+ if time.monotonic() + interval > deadline:
104
+ raise TimeoutError(f"Operation {op.id} wait timeout")
105
+ time.sleep(interval)
106
+ LOOP.await_(op.update(timeout=REQUEST_TIMEOUT, metadata=REQUEST_MD))
107
+
108
+
109
+ def get_region_to_project_id_map(sdk: SDK) -> dict[str, str]:
110
+ tenants = LOOP.await_(
111
+ TenantServiceClient(sdk).list(
112
+ ListTenantsRequest(), timeout=REQUEST_TIMEOUT, metadata=REQUEST_MD
113
+ )
114
+ )
115
+ if len(tenants.items) != 1:
116
+ raise ValueError(f"Expected to find 1 tenant, found {(len(tenants.items))}")
117
+ projects = LOOP.await_(
118
+ ProjectServiceClient(sdk).list(
119
+ ListProjectsRequest(parent_id=tenants.items[0].metadata.id, page_size=999),
120
+ timeout=REQUEST_TIMEOUT,
121
+ metadata=REQUEST_MD,
122
+ )
123
+ )
124
+ result = {}
125
+ for project in projects.items:
126
+ if project.metadata.name == f"default-project-{project.status.region}":
127
+ result[project.status.region] = project.metadata.id
128
+ return result
129
+
130
+
131
+ def get_default_subnet(sdk: SDK, project_id: str) -> Subnet:
132
+ subnets = LOOP.await_(
133
+ SubnetServiceClient(sdk).list(
134
+ ListSubnetsRequest(parent_id=project_id, page_size=999),
135
+ timeout=REQUEST_TIMEOUT,
136
+ metadata=REQUEST_MD,
137
+ )
138
+ )
139
+ for subnet in subnets.items:
140
+ if subnet.metadata.name.startswith("default-subnet"):
141
+ return subnet
142
+ raise BackendError(f"Could not find default subnet in project {project_id}")
143
+
144
+
145
+ def create_disk(
146
+ sdk: SDK, name: str, project_id: str, size_mib: int, image_family: str
147
+ ) -> SDKOperation[Operation]:
148
+ client = DiskServiceClient(sdk)
149
+ request = CreateDiskRequest(
150
+ metadata=ResourceMetadata(
151
+ name=name,
152
+ parent_id=project_id,
153
+ ),
154
+ spec=DiskSpec(
155
+ size_mebibytes=size_mib,
156
+ type=DiskSpec.DiskType.NETWORK_SSD,
157
+ source_image_family=SourceImageFamily(image_family=image_family),
158
+ ),
159
+ )
160
+ with wrap_capacity_errors():
161
+ return LOOP.await_(client.create(request, timeout=REQUEST_TIMEOUT, metadata=REQUEST_MD))
162
+
163
+
164
+ def delete_disk(sdk: SDK, disk_id: str) -> None:
165
+ LOOP.await_(
166
+ DiskServiceClient(sdk).delete(
167
+ DeleteDiskRequest(id=disk_id), timeout=REQUEST_TIMEOUT, metadata=REQUEST_MD
168
+ )
169
+ )
170
+
171
+
172
+ def create_instance(
173
+ sdk: SDK,
174
+ name: str,
175
+ project_id: str,
176
+ user_data: str,
177
+ platform: str,
178
+ preset: str,
179
+ disk_id: str,
180
+ subnet_id: str,
181
+ ) -> SDKOperation[Operation]:
182
+ client = InstanceServiceClient(sdk)
183
+ request = CreateInstanceRequest(
184
+ metadata=ResourceMetadata(
185
+ name=name,
186
+ parent_id=project_id,
187
+ ),
188
+ spec=InstanceSpec(
189
+ cloud_init_user_data=user_data,
190
+ resources=ResourcesSpec(platform=platform, preset=preset),
191
+ boot_disk=AttachedDiskSpec(
192
+ attach_mode=AttachedDiskSpec.AttachMode.READ_WRITE,
193
+ existing_disk=ExistingDisk(id=disk_id),
194
+ ),
195
+ network_interfaces=[
196
+ NetworkInterfaceSpec(
197
+ name="dstack-default-interface",
198
+ subnet_id=subnet_id,
199
+ ip_address=IPAddress(),
200
+ public_ip_address=PublicIPAddress(static=True),
201
+ )
202
+ ],
203
+ ),
204
+ )
205
+ with wrap_capacity_errors():
206
+ return LOOP.await_(client.create(request, timeout=REQUEST_TIMEOUT, metadata=REQUEST_MD))
207
+
208
+
209
+ def get_instance(sdk: SDK, instance_id: str) -> Instance:
210
+ return LOOP.await_(
211
+ InstanceServiceClient(sdk).get(
212
+ GetInstanceRequest(id=instance_id), timeout=REQUEST_TIMEOUT, metadata=REQUEST_MD
213
+ )
214
+ )
215
+
216
+
217
+ def delete_instance(sdk: SDK, instance_id: str) -> SDKOperation[Operation]:
218
+ return LOOP.await_(
219
+ InstanceServiceClient(sdk).delete(
220
+ DeleteInstanceRequest(id=instance_id), timeout=REQUEST_TIMEOUT, metadata=REQUEST_MD
221
+ )
222
+ )
@@ -102,6 +102,20 @@ class PlacementGroupInUseError(ComputeError):
102
102
  pass
103
103
 
104
104
 
105
+ class NotYetTerminated(ComputeError):
106
+ """
107
+ Used by Compute.terminate_instance to signal that instance termination is not complete
108
+ and the method should be called again after some time to continue termination.
109
+ """
110
+
111
+ def __init__(self, details: str) -> None:
112
+ """
113
+ Args:
114
+ details: some details about the termination status
115
+ """
116
+ return super().__init__(details)
117
+
118
+
105
119
  class CLIError(DstackError):
106
120
  pass
107
121
 
@@ -12,6 +12,7 @@ class BackendType(str, enum.Enum):
12
12
  DATACRUNCH (BackendType): DataCrunch
13
13
  KUBERNETES (BackendType): Kubernetes
14
14
  LAMBDA (BackendType): Lambda Cloud
15
+ NEBIUS (BackendType): Nebius AI Cloud
15
16
  OCI (BackendType): Oracle Cloud Infrastructure
16
17
  RUNPOD (BackendType): Runpod Cloud
17
18
  TENSORDOCK (BackendType): TensorDock Marketplace
@@ -29,6 +30,7 @@ class BackendType(str, enum.Enum):
29
30
  LAMBDA = "lambda"
30
31
  LOCAL = "local"
31
32
  REMOTE = "remote" # TODO: replace for LOCAL
33
+ NEBIUS = "nebius"
32
34
  OCI = "oci"
33
35
  RUNPOD = "runpod"
34
36
  TENSORDOCK = "tensordock"
@@ -57,11 +57,11 @@ class ChatCompletionsResponse(CoreModel):
57
57
 
58
58
 
59
59
  class ChatCompletionsChunk(CoreModel):
60
- id: str
60
+ id: Optional[str] = None
61
61
  choices: List[ChatCompletionsChunkChoice]
62
- created: int
62
+ created: Optional[int] = None
63
63
  model: str
64
- system_fingerprint: str = ""
64
+ system_fingerprint: Optional[str] = ""
65
65
  object: Literal["chat.completion.chunk"] = "chat.completion.chunk"
66
66
 
67
67
 
@@ -39,7 +39,7 @@ from dstack._internal.core.backends.remote.provisioning import (
39
39
  from dstack._internal.core.consts import DSTACK_SHIM_HTTP_PORT
40
40
 
41
41
  # FIXME: ProvisioningError is a subclass of ComputeError and should not be used outside of Compute
42
- from dstack._internal.core.errors import BackendError, ProvisioningError
42
+ from dstack._internal.core.errors import BackendError, NotYetTerminated, ProvisioningError
43
43
  from dstack._internal.core.models.backends.base import BackendType
44
44
  from dstack._internal.core.models.fleets import InstanceGroupPlacement
45
45
  from dstack._internal.core.models.instances import (
@@ -846,12 +846,17 @@ async def _terminate(instance: InstanceModel) -> None:
846
846
  instance.first_termination_retry_at = get_current_datetime()
847
847
  instance.last_termination_retry_at = get_current_datetime()
848
848
  if _next_termination_retry_at(instance) < _get_termination_deadline(instance):
849
- logger.warning(
850
- "Failed to terminate instance %s. Will retry. Error: %r",
851
- instance.name,
852
- e,
853
- exc_info=not isinstance(e, BackendError),
854
- )
849
+ if isinstance(e, NotYetTerminated):
850
+ logger.debug(
851
+ "Instance %s termination in progress: %s", instance.name, e
852
+ )
853
+ else:
854
+ logger.warning(
855
+ "Failed to terminate instance %s. Will retry. Error: %r",
856
+ instance.name,
857
+ e,
858
+ exc_info=not isinstance(e, BackendError),
859
+ )
855
860
  return
856
861
  logger.error(
857
862
  "Failed all attempts to terminate instance %s."
@@ -1,3 +1,4 @@
1
+ import os
1
2
  from typing import Annotated
2
3
 
3
4
  from fastapi import APIRouter, Depends
@@ -6,12 +7,16 @@ from sqlalchemy.ext.asyncio import AsyncSession
6
7
 
7
8
  from dstack._internal.server import settings
8
9
  from dstack._internal.server.db import get_session
10
+ from dstack._internal.server.security.permissions import OptionalServiceAccount
9
11
  from dstack._internal.server.services import prometheus
10
12
  from dstack._internal.server.utils.routers import error_not_found
11
13
 
14
+ _auth = OptionalServiceAccount(os.getenv("DSTACK_PROMETHEUS_AUTH_TOKEN"))
15
+
12
16
  router = APIRouter(
13
17
  tags=["prometheus"],
14
18
  default_response_class=PlainTextResponse,
19
+ dependencies=[Depends(_auth)],
15
20
  )
16
21
 
17
22
 
@@ -1,4 +1,4 @@
1
- from typing import Tuple
1
+ from typing import Annotated, Optional, Tuple
2
2
 
3
3
  from fastapi import Depends, HTTPException, Security
4
4
  from fastapi.security import HTTPBearer
@@ -99,6 +99,24 @@ class ProjectMember:
99
99
  return await get_project_member(session, project_name, token.credentials)
100
100
 
101
101
 
102
+ class OptionalServiceAccount:
103
+ def __init__(self, token: Optional[str]) -> None:
104
+ self._token = token
105
+
106
+ async def __call__(
107
+ self,
108
+ token: Annotated[
109
+ Optional[HTTPAuthorizationCredentials], Security(HTTPBearer(auto_error=False))
110
+ ],
111
+ ) -> None:
112
+ if self._token is None:
113
+ return
114
+ if token is None:
115
+ raise error_forbidden()
116
+ if token.credentials != self._token:
117
+ raise error_invalid_token()
118
+
119
+
102
120
  async def get_project_member(
103
121
  session: AsyncSession, project_name: str, token: str
104
122
  ) -> Tuple[UserModel, ProjectModel]:
@@ -1,3 +1,3 @@
1
1
  <!doctype html><html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><title>dstack</title><meta name="description" content="Get GPUs at the best prices and availability from a wide range of providers. No cloud account of your own is required.
2
2
  "/><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" rel="stylesheet"><meta name="og:title" content="dstack"><meta name="og:type" content="article"><meta name="og:image" content="/splash_thumbnail.png"><meta name="og:description" content="Get GPUs at the best prices and availability from a wide range of providers. No cloud account of your own is required.
3
- "><link rel="icon" type="image/x-icon" href="/assets/favicon.ico"><link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon-16x16.png"><link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon-32x32.png"><link rel="icon" type="image/png" sizes="48x48" href="/assets/favicon-48x48.png"><link rel="manifest" href="/assets/manifest.webmanifest"><meta name="mobile-web-app-capable" content="yes"><meta name="theme-color" content="#fff"><meta name="application-name" content="dstackai"><link rel="apple-touch-icon" sizes="57x57" href="/assets/apple-touch-icon-57x57.png"><link rel="apple-touch-icon" sizes="60x60" href="/assets/apple-touch-icon-60x60.png"><link rel="apple-touch-icon" sizes="72x72" href="/assets/apple-touch-icon-72x72.png"><link rel="apple-touch-icon" sizes="76x76" href="/assets/apple-touch-icon-76x76.png"><link rel="apple-touch-icon" sizes="114x114" href="/assets/apple-touch-icon-114x114.png"><link rel="apple-touch-icon" sizes="120x120" href="/assets/apple-touch-icon-120x120.png"><link rel="apple-touch-icon" sizes="144x144" href="/assets/apple-touch-icon-144x144.png"><link rel="apple-touch-icon" sizes="152x152" href="/assets/apple-touch-icon-152x152.png"><link rel="apple-touch-icon" sizes="167x167" href="/assets/apple-touch-icon-167x167.png"><link rel="apple-touch-icon" sizes="180x180" href="/assets/apple-touch-icon-180x180.png"><link rel="apple-touch-icon" sizes="1024x1024" href="/assets/apple-touch-icon-1024x1024.png"><meta name="apple-mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"><meta name="apple-mobile-web-app-title" content="dstackai"><link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-640x1136.png"><link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-1136x640.png"><link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-750x1334.png"><link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-1334x750.png"><link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1125x2436.png"><link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2436x1125.png"><link rel="apple-touch-startup-image" media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1170x2532.png"><link rel="apple-touch-startup-image" media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2532x1170.png"><link rel="apple-touch-startup-image" media="(device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1179x2556.png"><link rel="apple-touch-startup-image" media="(device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2556x1179.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-828x1792.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-1792x828.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1242x2688.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2688x1242.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1242x2208.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2208x1242.png"><link rel="apple-touch-startup-image" media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1284x2778.png"><link rel="apple-touch-startup-image" media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2778x1284.png"><link rel="apple-touch-startup-image" media="(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1290x2796.png"><link rel="apple-touch-startup-image" media="(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2796x1290.png"><link rel="apple-touch-startup-image" media="(device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1488x2266.png"><link rel="apple-touch-startup-image" media="(device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2266x1488.png"><link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1536x2048.png"><link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2048x1536.png"><link rel="apple-touch-startup-image" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1620x2160.png"><link rel="apple-touch-startup-image" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2160x1620.png"><link rel="apple-touch-startup-image" media="(device-width: 820px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1640x2160.png"><link rel="apple-touch-startup-image" media="(device-width: 820px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2160x1640.png"><link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1668x2388.png"><link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2388x1668.png"><link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1668x2224.png"><link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2224x1668.png"><link rel="apple-touch-startup-image" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-2048x2732.png"><link rel="apple-touch-startup-image" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2732x2048.png"><meta name="msapplication-TileColor" content="#fff"><meta name="msapplication-TileImage" content="/assets/mstile-144x144.png"><meta name="msapplication-config" content="/assets/browserconfig.xml"><link rel="yandex-tableau-widget" href="/assets/yandex-browser-manifest.json"><script defer="defer" src="/main-4a0fe83e84574654e397.js"></script><link href="/main-da9f8c06a69c20dac23e.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div class="b-page-header" id="header"></div><div id="root"></div></body></html>
3
+ "><link rel="icon" type="image/x-icon" href="/assets/favicon.ico"><link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon-16x16.png"><link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon-32x32.png"><link rel="icon" type="image/png" sizes="48x48" href="/assets/favicon-48x48.png"><link rel="manifest" href="/assets/manifest.webmanifest"><meta name="mobile-web-app-capable" content="yes"><meta name="theme-color" content="#fff"><meta name="application-name" content="dstackai"><link rel="apple-touch-icon" sizes="57x57" href="/assets/apple-touch-icon-57x57.png"><link rel="apple-touch-icon" sizes="60x60" href="/assets/apple-touch-icon-60x60.png"><link rel="apple-touch-icon" sizes="72x72" href="/assets/apple-touch-icon-72x72.png"><link rel="apple-touch-icon" sizes="76x76" href="/assets/apple-touch-icon-76x76.png"><link rel="apple-touch-icon" sizes="114x114" href="/assets/apple-touch-icon-114x114.png"><link rel="apple-touch-icon" sizes="120x120" href="/assets/apple-touch-icon-120x120.png"><link rel="apple-touch-icon" sizes="144x144" href="/assets/apple-touch-icon-144x144.png"><link rel="apple-touch-icon" sizes="152x152" href="/assets/apple-touch-icon-152x152.png"><link rel="apple-touch-icon" sizes="167x167" href="/assets/apple-touch-icon-167x167.png"><link rel="apple-touch-icon" sizes="180x180" href="/assets/apple-touch-icon-180x180.png"><link rel="apple-touch-icon" sizes="1024x1024" href="/assets/apple-touch-icon-1024x1024.png"><meta name="apple-mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"><meta name="apple-mobile-web-app-title" content="dstackai"><link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-640x1136.png"><link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-1136x640.png"><link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-750x1334.png"><link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-1334x750.png"><link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1125x2436.png"><link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2436x1125.png"><link rel="apple-touch-startup-image" media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1170x2532.png"><link rel="apple-touch-startup-image" media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2532x1170.png"><link rel="apple-touch-startup-image" media="(device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1179x2556.png"><link rel="apple-touch-startup-image" media="(device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2556x1179.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-828x1792.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-1792x828.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1242x2688.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2688x1242.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1242x2208.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2208x1242.png"><link rel="apple-touch-startup-image" media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1284x2778.png"><link rel="apple-touch-startup-image" media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2778x1284.png"><link rel="apple-touch-startup-image" media="(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1290x2796.png"><link rel="apple-touch-startup-image" media="(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2796x1290.png"><link rel="apple-touch-startup-image" media="(device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1488x2266.png"><link rel="apple-touch-startup-image" media="(device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2266x1488.png"><link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1536x2048.png"><link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2048x1536.png"><link rel="apple-touch-startup-image" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1620x2160.png"><link rel="apple-touch-startup-image" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2160x1620.png"><link rel="apple-touch-startup-image" media="(device-width: 820px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1640x2160.png"><link rel="apple-touch-startup-image" media="(device-width: 820px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2160x1640.png"><link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1668x2388.png"><link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2388x1668.png"><link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1668x2224.png"><link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2224x1668.png"><link rel="apple-touch-startup-image" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-2048x2732.png"><link rel="apple-touch-startup-image" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2732x2048.png"><meta name="msapplication-TileColor" content="#fff"><meta name="msapplication-TileImage" content="/assets/mstile-144x144.png"><meta name="msapplication-config" content="/assets/browserconfig.xml"><link rel="yandex-tableau-widget" href="/assets/yandex-browser-manifest.json"><script defer="defer" src="/main-bcb3228138bc8483cc0b.js"></script><link href="/main-c0bdaac8f1ea67d499eb.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div class="b-page-header" id="header"></div><div id="root"></div></body></html>