Skip to content

Commit e37b5af

Browse files
authored
Add the ability to store a default project for a user (#3457)
1 parent 68e8b94 commit e37b5af

File tree

10 files changed

+374
-208
lines changed

10 files changed

+374
-208
lines changed

docs/book/getting-started/zenml-pro/projects.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ Projects offer several key benefits:
2121

2222
Before you can work with projects, you need to be logged into your workspace. If you haven't done this yet, see the [Workspaces](workspaces.md#using-the-cli) documentation for instructions on logging in.
2323

24+
### Creating a project
25+
26+
To create a new project using the CLI, run the following command:
27+
```bash
28+
zenml project register <NAME>
29+
```
30+
2431
### Setting an active project
2532

2633
After initializing your ZenML repository (`zenml init`), you should set an active project. This is similar to how you set an active stack:
@@ -35,6 +42,20 @@ This command sets the "default" project as your active project. All subsequent Z
3542
Best practice is to set your active project right after running `zenml init`, just like you would set an active stack. This ensures all your resources are properly organized within the project.
3643
{% endhint %}
3744

45+
### Setting a default project
46+
47+
The default project is something that each user can configure. This project will be automatically set as the active project when you connect
48+
your local Python client to a ZenML Pro workspace.
49+
50+
You can set your default project either when creating a new project or when activating it:
51+
```bash
52+
# Set default project during registration
53+
zenml project register <NAME> --set-default
54+
55+
# Set default project during activation
56+
zenml project set <NAME> --default
57+
```
58+
3859
## Creating and Managing Projects
3960

4061
To create a new project:

src/zenml/cli/project.py

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,24 +75,32 @@ def list_projects(ctx: click.Context, /, **kwargs: Any) -> None:
7575
required=False,
7676
help="The display name of the project.",
7777
)
78+
@click.option(
79+
"--set-default",
80+
"set_default",
81+
is_flag=True,
82+
help="Set this project as the default project.",
83+
)
7884
@click.argument("project_name", type=str, required=True)
7985
def register_project(
8086
project_name: str,
8187
set_project: bool = False,
8288
display_name: Optional[str] = None,
89+
set_default: bool = False,
8390
) -> None:
8491
"""Register a new project.
8592
8693
Args:
8794
project_name: The name of the project to register.
8895
set_project: Whether to set the project as active.
8996
display_name: The display name of the project.
97+
set_default: Whether to set the project as the default project.
9098
"""
9199
check_zenml_pro_project_availability()
92100
client = Client()
93101
with console.status("Creating project...\n"):
94102
try:
95-
client.create_project(
103+
project = client.create_project(
96104
project_name,
97105
description="",
98106
display_name=display_name,
@@ -105,26 +113,51 @@ def register_project(
105113
client.set_active_project(project_name)
106114
cli_utils.declare(f"The active project has been set to {project_name}")
107115

116+
if set_default:
117+
client.update_user(
118+
name_id_or_prefix=client.active_user.id,
119+
updated_default_project_id=project.id,
120+
)
121+
cli_utils.declare(
122+
f"The default project has been set to {project.name}"
123+
)
124+
108125

109126
@project.command("set")
110127
@click.argument("project_name_or_id", type=str, required=True)
111-
def set_project(project_name_or_id: str) -> None:
128+
@click.option(
129+
"--default",
130+
"default",
131+
is_flag=True,
132+
help="Set this project as the default project.",
133+
)
134+
def set_project(project_name_or_id: str, default: bool = False) -> None:
112135
"""Set the active project.
113136
114137
Args:
115138
project_name_or_id: The name or ID of the project to set as active.
139+
default: Whether to set the project as the default project.
116140
"""
117141
check_zenml_pro_project_availability()
118142
client = Client()
119143
with console.status("Setting project...\n"):
120144
try:
121-
client.set_active_project(project_name_or_id)
145+
project = client.set_active_project(project_name_or_id)
122146
cli_utils.declare(
123147
f"The active project has been set to {project_name_or_id}"
124148
)
125149
except Exception as e:
126150
cli_utils.error(str(e))
127151

152+
if default:
153+
client.update_user(
154+
name_id_or_prefix=client.active_user.id,
155+
updated_default_project_id=project.id,
156+
)
157+
cli_utils.declare(
158+
f"The default project has been set to {project.name}"
159+
)
160+
128161

129162
@project.command("describe")
130163
@click.argument("project_name_or_id", type=str, required=False)

src/zenml/client.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ def active_project(self) -> "ProjectResponse":
235235
else:
236236
raise RuntimeError(
237237
"No active project is configured. Run "
238-
"`zenml project set PROJECT_NAME` to set the active "
238+
"`zenml project set <NAME>` to set the active "
239239
"project."
240240
)
241241

@@ -876,6 +876,7 @@ def update_user(
876876
old_password: Optional[str] = None,
877877
updated_is_admin: Optional[bool] = None,
878878
updated_metadata: Optional[Dict[str, Any]] = None,
879+
updated_default_project_id: Optional[UUID] = None,
879880
active: Optional[bool] = None,
880881
) -> UserResponse:
881882
"""Update a user.
@@ -891,6 +892,7 @@ def update_user(
891892
update.
892893
updated_is_admin: Whether the user should be an admin.
893894
updated_metadata: The new metadata for the user.
895+
updated_default_project_id: The new default project ID for the user.
894896
active: Use to activate or deactivate the user.
895897
896898
Returns:
@@ -928,6 +930,9 @@ def update_user(
928930
if updated_metadata is not None:
929931
user_update.user_metadata = updated_metadata
930932

933+
if updated_default_project_id is not None:
934+
user_update.default_project_id = updated_default_project_id
935+
931936
return self.zen_store.update_user(
932937
user_id=user.id, user_update=user_update
933938
)
@@ -1154,7 +1159,7 @@ def active_project(self) -> ProjectResponse:
11541159
if not project:
11551160
raise RuntimeError(
11561161
"No active project is configured. Run "
1157-
"`zenml project set PROJECT_NAME` to set the active "
1162+
"`zenml project set <NAME>` to set the active "
11581163
"project."
11591164
)
11601165

src/zenml/config/global_config.py

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ class GlobalConfiguration(BaseModel, metaclass=GlobalConfigMetaClass):
113113
global config.
114114
store: Store configuration.
115115
active_stack_id: The ID of the active stack.
116-
active_project_name: The name of the active project.
116+
active_project_id: The ID of the active project.
117117
"""
118118

119119
user_id: uuid.UUID = Field(default_factory=uuid.uuid4)
@@ -123,7 +123,7 @@ class GlobalConfiguration(BaseModel, metaclass=GlobalConfigMetaClass):
123123
version: Optional[str] = None
124124
store: Optional[SerializeAsAny[StoreConfiguration]] = None
125125
active_stack_id: Optional[uuid.UUID] = None
126-
active_project_name: Optional[str] = None
126+
active_project_id: Optional[uuid.UUID] = None
127127

128128
_zen_store: Optional["BaseZenStore"] = None
129129
_active_project: Optional["ProjectResponse"] = None
@@ -393,15 +393,15 @@ def _sanitize_config(self) -> None:
393393
if ENV_ZENML_SERVER in os.environ:
394394
return
395395
active_project, active_stack = self.zen_store.validate_active_config(
396-
self.active_project_name,
396+
self.active_project_id,
397397
self.active_stack_id,
398398
config_name="global",
399399
)
400400
if active_project:
401-
self.active_project_name = active_project.name
401+
self.active_project_id = active_project.id
402402
self._active_project = active_project
403403
else:
404-
self.active_project_name = None
404+
self.active_project_id = None
405405
self._active_project = None
406406

407407
self.set_active_stack(active_stack)
@@ -730,7 +730,7 @@ def set_active_project(
730730
Returns:
731731
The project that was set active.
732732
"""
733-
self.active_project_name = project.name
733+
self.active_project_id = project.id
734734
self._active_project = project
735735
# Sanitize the global configuration to reflect the new project
736736
self._sanitize_config()
@@ -751,35 +751,35 @@ def get_active_project(self) -> "ProjectResponse":
751751
Returns:
752752
The model of the active project.
753753
"""
754-
project_name = self.get_active_project_name()
754+
project_id = self.get_active_project_id()
755755

756756
if self._active_project is not None:
757757
return self._active_project
758758

759759
project = self.zen_store.get_project(
760-
project_name_or_id=project_name,
760+
project_name_or_id=project_id,
761761
)
762762
return self.set_active_project(project)
763763

764-
def get_active_project_name(self) -> str:
765-
"""Get the name of the active project.
764+
def get_active_project_id(self) -> UUID:
765+
"""Get the ID of the active project.
766766
767767
Returns:
768-
The name of the active project.
768+
The ID of the active project.
769769
770770
Raises:
771771
RuntimeError: If the active project is not set.
772772
"""
773-
if self.active_project_name is None:
773+
if self.active_project_id is None:
774774
_ = self.zen_store
775-
if self.active_project_name is None:
775+
if self.active_project_id is None:
776776
raise RuntimeError(
777777
"No project is currently set as active. Please set the "
778-
"active project using the `zenml project set` CLI "
778+
"active project using the `zenml project set <NAME>` CLI "
779779
"command."
780780
)
781781

782-
return self.active_project_name
782+
return self.active_project_id
783783

784784
def get_active_stack_id(self) -> UUID:
785785
"""Get the ID of the active stack.

src/zenml/integrations/kubernetes/orchestrators/kubernetes_orchestrator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ def _validate_local_requirements(stack: "Stack") -> Tuple[bool, str]:
238238
)
239239
if kubernetes_context != active_context:
240240
logger.warning(
241-
f"{msg}the Kubernetes context "
241+
f"{msg}the Kubernetes context " # nosec
242242
f"'{kubernetes_context}' configured for the "
243243
f"Kubernetes orchestrator is not the same as the "
244244
f"active context in the local Kubernetes "

src/zenml/models/v2/core/user.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,10 @@ class UserUpdate(UserBase, BaseUpdate):
203203
"accounts. Required when updating the password.",
204204
max_length=STR_FIELD_MAX_LENGTH,
205205
)
206+
default_project_id: Optional[UUID] = Field(
207+
default=None,
208+
title="The default project ID for the user.",
209+
)
206210

207211
@model_validator(mode="after")
208212
def user_email_updates(self) -> "UserUpdate":
@@ -279,6 +283,10 @@ class UserResponseBody(BaseDatedResponseBody):
279283
is_admin: bool = Field(
280284
title="Whether the account is an administrator.",
281285
)
286+
default_project_id: Optional[UUID] = Field(
287+
default=None,
288+
title="The default project ID for the user.",
289+
)
282290

283291

284292
class UserResponseMetadata(BaseResponseMetadata):
@@ -422,6 +430,15 @@ def user_metadata(self) -> Dict[str, Any]:
422430
"""
423431
return self.get_metadata().user_metadata
424432

433+
@property
434+
def default_project_id(self) -> Optional[UUID]:
435+
"""The `default_project_id` property.
436+
437+
Returns:
438+
the value of the property.
439+
"""
440+
return self.get_body().default_project_id
441+
425442
# Helper methods
426443
@classmethod
427444
def _get_crypt_context(cls) -> "CryptContext":

0 commit comments

Comments
 (0)