Skip to content
21 changes: 21 additions & 0 deletions docs/book/getting-started/zenml-pro/projects.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ Projects offer several key benefits:

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.

### Creating a project

To create a new project using the CLI, run the following command:
```bash
zenml project register <NAME>
```

### Setting an active project

After initializing your ZenML repository (`zenml init`), you should set an active project. This is similar to how you set an active stack:
Expand All @@ -35,6 +42,20 @@ This command sets the "default" project as your active project. All subsequent Z
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.
{% endhint %}

### Setting a default project

The default project is something that each user can configure. This project will be automatically set as the active project when you connect
your local Python client to a ZenML Pro workspace.

You can set your default project either when creating a new project or when activating it:
```bash
# Set default project during registration
zenml project register <NAME> --set-default

# Set default project during activation
zenml project set <NAME> --default
```

## Creating and Managing Projects

To create a new project:
Expand Down
39 changes: 36 additions & 3 deletions src/zenml/cli/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,24 +75,32 @@ def list_projects(ctx: click.Context, /, **kwargs: Any) -> None:
required=False,
help="The display name of the project.",
)
@click.option(
"--set-default",
"set_default",
is_flag=True,
help="Set this project as the default project.",
)
@click.argument("project_name", type=str, required=True)
def register_project(
project_name: str,
set_project: bool = False,
display_name: Optional[str] = None,
set_default: bool = False,
) -> None:
"""Register a new project.

Args:
project_name: The name of the project to register.
set_project: Whether to set the project as active.
display_name: The display name of the project.
set_default: Whether to set the project as the default project.
"""
check_zenml_pro_project_availability()
client = Client()
with console.status("Creating project...\n"):
try:
client.create_project(
project = client.create_project(
project_name,
description="",
display_name=display_name,
Expand All @@ -105,26 +113,51 @@ def register_project(
client.set_active_project(project_name)
cli_utils.declare(f"The active project has been set to {project_name}")

if set_default:
client.update_user(
name_id_or_prefix=client.active_user.id,
updated_default_project_id=project.id,
)
cli_utils.declare(
f"The default project has been set to {project.name}"
)


@project.command("set")
@click.argument("project_name_or_id", type=str, required=True)
def set_project(project_name_or_id: str) -> None:
@click.option(
"--default",
"default",
is_flag=True,
help="Set this project as the default project.",
)
def set_project(project_name_or_id: str, default: bool = False) -> None:
"""Set the active project.

Args:
project_name_or_id: The name or ID of the project to set as active.
default: Whether to set the project as the default project.
"""
check_zenml_pro_project_availability()
client = Client()
with console.status("Setting project...\n"):
try:
client.set_active_project(project_name_or_id)
project = client.set_active_project(project_name_or_id)
cli_utils.declare(
f"The active project has been set to {project_name_or_id}"
)
except Exception as e:
cli_utils.error(str(e))

if default:
client.update_user(
name_id_or_prefix=client.active_user.id,
updated_default_project_id=project.id,
)
cli_utils.declare(
f"The default project has been set to {project.name}"
)


@project.command("describe")
@click.argument("project_name_or_id", type=str, required=False)
Expand Down
9 changes: 7 additions & 2 deletions src/zenml/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ def active_project(self) -> "ProjectResponse":
else:
raise RuntimeError(
"No active project is configured. Run "
"`zenml project set PROJECT_NAME` to set the active "
"`zenml project set <NAME>` to set the active "
"project."
)

Expand Down Expand Up @@ -876,6 +876,7 @@ def update_user(
old_password: Optional[str] = None,
updated_is_admin: Optional[bool] = None,
updated_metadata: Optional[Dict[str, Any]] = None,
updated_default_project_id: Optional[UUID] = None,
active: Optional[bool] = None,
) -> UserResponse:
"""Update a user.
Expand All @@ -891,6 +892,7 @@ def update_user(
update.
updated_is_admin: Whether the user should be an admin.
updated_metadata: The new metadata for the user.
updated_default_project_id: The new default project ID for the user.
active: Use to activate or deactivate the user.

Returns:
Expand Down Expand Up @@ -928,6 +930,9 @@ def update_user(
if updated_metadata is not None:
user_update.user_metadata = updated_metadata

if updated_default_project_id is not None:
user_update.default_project_id = updated_default_project_id

return self.zen_store.update_user(
user_id=user.id, user_update=user_update
)
Expand Down Expand Up @@ -1154,7 +1159,7 @@ def active_project(self) -> ProjectResponse:
if not project:
raise RuntimeError(
"No active project is configured. Run "
"`zenml project set PROJECT_NAME` to set the active "
"`zenml project set <NAME>` to set the active "
"project."
)

Expand Down
30 changes: 15 additions & 15 deletions src/zenml/config/global_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ class GlobalConfiguration(BaseModel, metaclass=GlobalConfigMetaClass):
global config.
store: Store configuration.
active_stack_id: The ID of the active stack.
active_project_name: The name of the active project.
active_project_id: The ID of the active project.
"""

user_id: uuid.UUID = Field(default_factory=uuid.uuid4)
Expand All @@ -123,7 +123,7 @@ class GlobalConfiguration(BaseModel, metaclass=GlobalConfigMetaClass):
version: Optional[str] = None
store: Optional[SerializeAsAny[StoreConfiguration]] = None
active_stack_id: Optional[uuid.UUID] = None
active_project_name: Optional[str] = None
active_project_id: Optional[uuid.UUID] = None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see this being used anywhere. Why do you need it ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is used below as an argument to validate_active_config. If we were to keep using the name, we would keep a project with the same name when connecting to a different server, and therefore ignore the default project configured for the user.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you then be okay with replacing active_project_name with active_project_id ? this is in fact how it's done on the client/repo (i.e. zenml init) configuration side of things.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, I was wondering whether we should keep it in there for backwards compatibility somehow, but I'm happy to remove it entirely!


_zen_store: Optional["BaseZenStore"] = None
_active_project: Optional["ProjectResponse"] = None
Expand Down Expand Up @@ -393,15 +393,15 @@ def _sanitize_config(self) -> None:
if ENV_ZENML_SERVER in os.environ:
return
active_project, active_stack = self.zen_store.validate_active_config(
self.active_project_name,
self.active_project_id,
self.active_stack_id,
config_name="global",
)
if active_project:
self.active_project_name = active_project.name
self.active_project_id = active_project.id
self._active_project = active_project
else:
self.active_project_name = None
self.active_project_id = None
self._active_project = None

self.set_active_stack(active_stack)
Expand Down Expand Up @@ -730,7 +730,7 @@ def set_active_project(
Returns:
The project that was set active.
"""
self.active_project_name = project.name
self.active_project_id = project.id
self._active_project = project
# Sanitize the global configuration to reflect the new project
self._sanitize_config()
Expand All @@ -751,35 +751,35 @@ def get_active_project(self) -> "ProjectResponse":
Returns:
The model of the active project.
"""
project_name = self.get_active_project_name()
project_id = self.get_active_project_id()

if self._active_project is not None:
return self._active_project

project = self.zen_store.get_project(
project_name_or_id=project_name,
project_name_or_id=project_id,
)
return self.set_active_project(project)

def get_active_project_name(self) -> str:
"""Get the name of the active project.
def get_active_project_id(self) -> UUID:
"""Get the ID of the active project.

Returns:
The name of the active project.
The ID of the active project.

Raises:
RuntimeError: If the active project is not set.
"""
if self.active_project_name is None:
if self.active_project_id is None:
_ = self.zen_store
if self.active_project_name is None:
if self.active_project_id is None:
raise RuntimeError(
"No project is currently set as active. Please set the "
"active project using the `zenml project set` CLI "
"active project using the `zenml project set <NAME>` CLI "
"command."
)

return self.active_project_name
return self.active_project_id

def get_active_stack_id(self) -> UUID:
"""Get the ID of the active stack.
Expand Down
17 changes: 17 additions & 0 deletions src/zenml/models/v2/core/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,10 @@ class UserUpdate(UserBase, BaseUpdate):
"accounts. Required when updating the password.",
max_length=STR_FIELD_MAX_LENGTH,
)
default_project_id: Optional[UUID] = Field(
default=None,
title="The default project ID for the user.",
)

@model_validator(mode="after")
def user_email_updates(self) -> "UserUpdate":
Expand Down Expand Up @@ -279,6 +283,10 @@ class UserResponseBody(BaseDatedResponseBody):
is_admin: bool = Field(
title="Whether the account is an administrator.",
)
default_project_id: Optional[UUID] = Field(
default=None,
title="The default project ID for the user.",
)


class UserResponseMetadata(BaseResponseMetadata):
Expand Down Expand Up @@ -422,6 +430,15 @@ def user_metadata(self) -> Dict[str, Any]:
"""
return self.get_metadata().user_metadata

@property
def default_project_id(self) -> Optional[UUID]:
"""The `default_project_id` property.

Returns:
the value of the property.
"""
return self.get_body().default_project_id

# Helper methods
@classmethod
def _get_crypt_context(cls) -> "CryptContext":
Expand Down
Loading
Loading