Skip to content
Merged
12 changes: 11 additions & 1 deletion src/dstack/_internal/core/models/projects.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from datetime import datetime
from typing import List, Optional
from typing import List, Optional, Union

from pydantic import UUID4

Expand Down Expand Up @@ -28,6 +28,16 @@ class Project(CoreModel):
is_public: bool = False


class ProjectsInfoList(CoreModel):
total_count: Optional[int] = None
projects: List[Project]


# For backward compatibility with 0.20 clients, endpoints return `List[Project]` if `total_count` is None.
# TODO: Replace with ProjectsInfoList in 0.21.
ProjectsInfoListOrProjectsList = Union[List[Project], ProjectsInfoList]


class ProjectHookConfig(CoreModel):
"""
This class can be inherited to extend the project creation configuration passed to the hooks.
Expand Down
12 changes: 11 additions & 1 deletion src/dstack/_internal/core/models/users.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import enum
from datetime import datetime
from typing import Optional
from typing import List, Optional, Union

from pydantic import UUID4

Expand Down Expand Up @@ -42,6 +42,16 @@ class UserWithCreds(User):
ssh_private_key: Optional[str] = None


class UsersInfoList(CoreModel):
total_count: Optional[int] = None
users: List[User]


# For backward compatibility with 0.20 clients, endpoints return `List[User]` if `total_count` is None.
# TODO: Replace with UsersInfoList in 0.21.
UsersInfoListOrUsersList = Union[List[User], UsersInfoList]


class UserHookConfig(CoreModel):
"""
This class can be inherited to extend the user creation configuration passed to the hooks.
Expand Down
16 changes: 12 additions & 4 deletions src/dstack/_internal/server/routers/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession

from dstack._internal.core.models.projects import Project
from dstack._internal.core.models.projects import Project, ProjectsInfoListOrProjectsList
from dstack._internal.server.db import get_session
from dstack._internal.server.models import ProjectModel, UserModel
from dstack._internal.server.schemas.projects import (
Expand Down Expand Up @@ -36,14 +36,14 @@
)


@router.post("/list", response_model=List[Project])
@router.post("/list", response_model=ProjectsInfoListOrProjectsList)
async def list_projects(
body: Optional[ListProjectsRequest] = None,
session: AsyncSession = Depends(get_session),
user: UserModel = Depends(Authenticated()),
):
"""
Returns projects visible to the user, sorted by ascending `created_at`.
Returns projects visible to the user.

Returns all accessible projects (member projects for regular users, all non-deleted
projects for global admins, plus public projects if `include_not_joined` is `True`).
Expand All @@ -55,7 +55,15 @@ async def list_projects(
body = ListProjectsRequest()
return CustomORJSONResponse(
await projects.list_user_accessible_projects(
session=session, user=user, include_not_joined=body.include_not_joined
session=session,
user=user,
include_not_joined=body.include_not_joined,
return_total_count=body.return_total_count,
name_pattern=body.name_pattern,
prev_created_at=body.prev_created_at,
prev_id=body.prev_id,
limit=body.limit,
ascending=body.ascending,
)
)

Expand Down
32 changes: 28 additions & 4 deletions src/dstack/_internal/server/routers/users.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
from typing import List
from typing import Optional

from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession

from dstack._internal.core.errors import ResourceNotExistsError
from dstack._internal.core.models.users import User, UserWithCreds
from dstack._internal.core.models.users import User, UsersInfoListOrUsersList, UserWithCreds
from dstack._internal.server.db import get_session
from dstack._internal.server.models import UserModel
from dstack._internal.server.schemas.users import (
CreateUserRequest,
DeleteUsersRequest,
GetUserRequest,
ListUsersRequest,
RefreshTokenRequest,
UpdateUserRequest,
)
Expand All @@ -28,12 +29,35 @@
)


@router.post("/list", response_model=List[User])
@router.post("/list", response_model=UsersInfoListOrUsersList)
async def list_users(
body: Optional[ListUsersRequest] = None,
session: AsyncSession = Depends(get_session),
user: UserModel = Depends(Authenticated()),
):
return CustomORJSONResponse(await users.list_users_for_user(session=session, user=user))
"""
Returns users visible to the user, sorted by descending `created_at`.

Admins see all non-deleted users. Non-admins only see themselves.

The results are paginated. To get the next page, pass `created_at` and `id` of
the last user from the previous page as `prev_created_at` and `prev_id`.
"""
if body is None:
# For backward compatibility
body = ListUsersRequest()
return CustomORJSONResponse(
await users.list_users_for_user(
session=session,
user=user,
return_total_count=body.return_total_count,
name_pattern=body.name_pattern,
prev_created_at=body.prev_created_at,
prev_id=body.prev_id,
limit=body.limit,
ascending=body.ascending,
)
)


@router.post("/get_my_user", response_model=UserWithCreds)
Expand Down
6 changes: 3 additions & 3 deletions src/dstack/_internal/server/schemas/fleets.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@


class ListFleetsRequest(CoreModel):
project_name: Optional[str]
project_name: Optional[str] = None
only_active: bool = False
prev_created_at: Optional[datetime]
prev_id: Optional[UUID]
prev_created_at: Optional[datetime] = None
prev_id: Optional[UUID] = None
limit: int = Field(100, ge=0, le=100)
ascending: bool = False

Expand Down
40 changes: 38 additions & 2 deletions src/dstack/_internal/server/schemas/projects.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from typing import Annotated, List
from datetime import datetime
from typing import Annotated, List, Optional
from uuid import UUID

from pydantic import Field

Expand All @@ -8,8 +10,42 @@

class ListProjectsRequest(CoreModel):
include_not_joined: Annotated[
bool, Field(description="Include public projects where user is not a member")
bool, Field(description="Include public projects where user is not a member.")
] = True
return_total_count: Annotated[
bool, Field(description="Return `total_count` with the total number of projects.")
] = False
name_pattern: Annotated[
Optional[str],
Field(
description="Include only projects with the name containing `name_pattern`.",
regex="^[a-zA-Z0-9-_]*$",
),
] = None
prev_created_at: Annotated[
Optional[datetime],
Field(
description="Paginate projects by specifying `created_at` of the last (first) project in previous batch for descending (ascending)."
),
] = None
prev_id: Annotated[
Optional[UUID],
Field(
description=(
"Paginate projects by specifying `id` of the last (first) project in previous batch for descending (ascending)."
" Must be used together with `prev_created_at`."
)
),
] = None
limit: Annotated[
int, Field(ge=0, le=2000, description="Limit number of projects returned.")
] = 2000
ascending: Annotated[
bool,
Field(
description="Return projects sorted by `created_at` in ascending order. Defaults to descending."
),
] = False


class CreateProjectRequest(CoreModel):
Expand Down
48 changes: 47 additions & 1 deletion src/dstack/_internal/server/schemas/users.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,55 @@
from typing import List, Optional
from datetime import datetime
from typing import Annotated, List, Optional
from uuid import UUID

from pydantic import Field

from dstack._internal.core.models.common import CoreModel
from dstack._internal.core.models.users import GlobalRole


class ListUsersRequest(CoreModel):
return_total_count: Annotated[
bool, Field(description="Return `total_count` with the total number of users.")
] = False
name_pattern: Annotated[
Optional[str],
Field(
description="Include only users with the name containing `name_pattern`.",
regex="^[a-zA-Z0-9-_]*$",
),
] = None
prev_created_at: Annotated[
Optional[datetime],
Field(
description=(
"Paginate users by specifying `created_at` of the last (first) user in previous "
"batch for descending (ascending)."
)
),
] = None
prev_id: Annotated[
Optional[UUID],
Field(
description=(
"Paginate users by specifying `id` of the last (first) user in previous batch "
"for descending (ascending). Must be used together with `prev_created_at`."
)
),
] = None
limit: Annotated[int, Field(ge=0, le=2000, description="Limit number of users returned.")] = (
2000
)
ascending: Annotated[
bool,
Field(
description=(
"Return users sorted by `created_at` in ascending order. Defaults to descending."
)
),
] = False


class GetUserRequest(CoreModel):
username: str

Expand Down
Loading