Skip to content

Commit

Permalink
Merge branch 'develop' into feature/trycatchdocker
Browse files Browse the repository at this point in the history
  • Loading branch information
strickvl committed Mar 12, 2024
2 parents f5b31a1 + 001ca32 commit 01a595e
Show file tree
Hide file tree
Showing 19 changed files with 604 additions and 71 deletions.
5 changes: 1 addition & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,4 @@ zenml_tutorial/
mlstacks_reset.sh

.local/
# PLEASE KEEP THIS LINE AT THE EOF: never include here src/zenml/zen_server/dashboard, since it is affecting release flow


dashboard/
# PLEASE KEEP THIS LINE AT THE EOF: never include here src/zenml/zen_server/dashboard, since it is affecting release flow
4 changes: 2 additions & 2 deletions examples/quickstart/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Along the way we will also show you how to:

You can use Google Colab to see ZenML in action, no signup / installation required!

<a href="https://colab.research.google.com/github/zenml-io/zenml/blob/main/examples/quickstart/run.ipynb" target="_parent"><img src="https://colab.research.google.com/assets/colab-badge.svg" alt="Open In Colab"/></a>
<a href="https://colab.research.google.com/github/zenml-io/zenml/blob/main/examples/quickstart/quickstart.ipynb" target="_parent"><img src="https://colab.research.google.com/assets/colab-badge.svg" alt="Open In Colab"/></a>

## :computer: Run Locally

Expand Down Expand Up @@ -208,4 +208,4 @@ The best way to get a production ZenML instance up and running with all batterie
Also, make sure to join our <a href="https://zenml.io/slack" target="_blank">
<img width="15" src="https://cdn3.iconfinder.com/data/icons/logos-and-brands-adobe/512/306_Slack-512.png" alt="Slack"/>
<b>Slack Community</b>
</a> to become part of the ZenML family!
</a> to become part of the ZenML family!
75 changes: 71 additions & 4 deletions src/zenml/cli/user_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,15 +120,27 @@ def list_users(ctx: click.Context, **kwargs: Any) -> None:
required=False,
type=str,
)
@click.option(
"--is_admin",
is_flag=True,
help=(
"Whether the user should be an admin. If not specified, the user will "
"be a regular user."
),
required=False,
default=False,
)
def create_user(
user_name: str,
password: Optional[str] = None,
is_admin: bool = False,
) -> None:
"""Create a new user.
Args:
user_name: The name of the user to create.
password: The password of the user to create.
is_admin: Whether the user should be an admin.
"""
client = Client()
if not password:
Expand All @@ -146,7 +158,9 @@ def create_user(
)

try:
new_user = client.create_user(name=user_name, password=password)
new_user = client.create_user(
name=user_name, password=password, is_admin=is_admin
)

cli_utils.declare(f"Created user '{new_user.name}'.")
except EntityExistsError as err:
Expand All @@ -162,8 +176,7 @@ def create_user(

@user.command(
"update",
help="Update user information through the cli. All attributes "
"except for password can be updated through the cli.",
help="Update user information through the cli.",
)
@click.argument("user_name_or_id", type=str, required=True)
@click.option(
Expand Down Expand Up @@ -191,26 +204,80 @@ def create_user(
required=False,
help="New user email.",
)
@click.option(
"--password",
"-p",
"updated_password",
type=str,
required=False,
help="New user password.",
)
@click.option(
"--admin",
"-a",
"make_admin",
is_flag=True,
required=False,
default=None,
help="Whether the user should be an admin.",
)
@click.option(
"--user",
"-u",
"make_user",
is_flag=True,
required=False,
default=None,
help="Whether the user should be a regular user.",
)
def update_user(
user_name_or_id: str,
updated_name: Optional[str] = None,
updated_full_name: Optional[str] = None,
updated_email: Optional[str] = None,
updated_password: Optional[str] = None,
make_admin: Optional[bool] = None,
make_user: Optional[bool] = None,
) -> None:
"""Create a new user.
"""Update an existing user.
Args:
user_name_or_id: The name of the user to create.
updated_name: The name of the user to create.
updated_full_name: The name of the user to create.
updated_email: The name of the user to create.
updated_password: The name of the user to create.
make_admin: Whether the user should be an admin.
make_user: Whether the user should be a regular user.
"""
if make_admin is not None and make_user is not None:
cli_utils.error(
"Cannot set both --admin and --user flags as these are mutually exclusive."
)
try:
current_user = Client().get_user(
user_name_or_id, allow_name_prefix_match=False
)
if current_user.is_admin and make_user:
confirmation = cli_utils.confirmation(
f"Currently user `{current_user.name}` is an admin. Are you sure you want to make them a regular user?"
)
if not confirmation:
cli_utils.declare("User update canceled.")
return

updated_is_admin = None
if make_admin is True:
updated_is_admin = True
elif make_user is True:
updated_is_admin = False
Client().update_user(
name_id_or_prefix=user_name_or_id,
updated_name=updated_name,
updated_full_name=updated_full_name,
updated_email=updated_email,
updated_password=updated_password,
updated_is_admin=updated_is_admin,
)
except (KeyError, IllegalOperationError) as err:
cli_utils.error(str(err))
Expand Down
14 changes: 13 additions & 1 deletion src/zenml/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -691,18 +691,22 @@ def create_user(
self,
name: str,
password: Optional[str] = None,
is_admin: bool = False,
) -> UserResponse:
"""Create a new user.
Args:
name: The name of the user.
password: The password of the user. If not provided, the user will
be created with empty password.
is_admin: Whether the user should be an admin.
Returns:
The model of the created user.
"""
user = UserRequest(name=name, password=password or None)
user = UserRequest(
name=name, password=password or None, is_admin=is_admin
)
user.active = (
password != "" if self.zen_store.type != StoreType.REST else True
)
Expand Down Expand Up @@ -801,6 +805,8 @@ def update_user(
updated_email: Optional[str] = None,
updated_email_opt_in: Optional[bool] = None,
updated_hub_token: Optional[str] = None,
updated_password: Optional[str] = None,
updated_is_admin: Optional[bool] = None,
) -> UserResponse:
"""Update a user.
Expand All @@ -811,6 +817,8 @@ def update_user(
updated_email: The new email of the user.
updated_email_opt_in: The new email opt-in status of the user.
updated_hub_token: Update the hub token
updated_password: The new password of the user.
updated_is_admin: Whether the user should be an admin.
Returns:
The updated user.
Expand All @@ -830,6 +838,10 @@ def update_user(
user_update.email_opted_in = updated_email_opt_in
if updated_hub_token is not None:
user_update.hub_token = updated_hub_token
if updated_password is not None:
user_update.password = updated_password
if updated_is_admin is not None:
user_update.is_admin = updated_is_admin

return self.zen_store.update_user(
user_id=user.id, user_update=user_update
Expand Down
1 change: 1 addition & 0 deletions src/zenml/models/v2/core/service_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ def to_user_model(self) -> "UserResponse":
email_opted_in=False,
created=self.created,
updated=self.updated,
is_admin=False,
),
metadata=UserResponseMetadata(
description=self.description,
Expand Down
114 changes: 78 additions & 36 deletions src/zenml/models/v2/core/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
)
from uuid import UUID

from pydantic import Field, root_validator
from pydantic import BaseModel, Field, root_validator

from zenml.constants import STR_FIELD_MAX_LENGTH
from zenml.models.v2.base.base import (
Expand All @@ -35,40 +35,23 @@
BaseRequest,
BaseResponseMetadata,
BaseResponseResources,
BaseZenModel,
)
from zenml.models.v2.base.filter import AnyQuery, BaseFilter
from zenml.models.v2.base.update import update_model

if TYPE_CHECKING:
from passlib.context import CryptContext

from zenml.models.v2.base.filter import AnySchema

# ------------------ Request Model ------------------

# ------------------ Base Model ------------------

class UserRequest(BaseRequest):
"""Request model for users."""

# Analytics fields for user request models
ANALYTICS_FIELDS: ClassVar[List[str]] = [
"name",
"full_name",
"active",
"email_opted_in",
]
class UserBase(BaseModel):
"""Base model for users."""

# Fields
name: str = Field(
title="The unique username for the account.",
max_length=STR_FIELD_MAX_LENGTH,
)
full_name: str = Field(
default="",
title="The full name for the account owner. Only relevant for user "
"accounts.",
max_length=STR_FIELD_MAX_LENGTH,
)

email: Optional[str] = Field(
default=None,
title="The email address associated with the account.",
Expand Down Expand Up @@ -99,17 +82,6 @@ class UserRequest(BaseRequest):
default=None,
title="The external user ID associated with the account.",
)
active: bool = Field(default=False, title="Whether the account is active.")

class Config:
"""Pydantic configuration class."""

# Validate attributes when assigning them
validate_assignment = True

# Forbid extra attributes to prevent unexpected behavior
extra = "forbid"
underscore_attrs_are_private = True

@classmethod
def _get_crypt_context(cls) -> "CryptContext":
Expand Down Expand Up @@ -165,13 +137,71 @@ def generate_activation_token(self) -> str:
return self.activation_token


# ------------------ Request Model ------------------


class UserRequest(UserBase, BaseRequest):
"""Request model for users."""

# Analytics fields for user request models
ANALYTICS_FIELDS: ClassVar[List[str]] = [
"name",
"full_name",
"active",
"email_opted_in",
]

name: str = Field(
title="The unique username for the account.",
max_length=STR_FIELD_MAX_LENGTH,
)
full_name: str = Field(
default="",
title="The full name for the account owner. Only relevant for user "
"accounts.",
max_length=STR_FIELD_MAX_LENGTH,
)
is_admin: bool = Field(
title="Whether the account is an administrator.",
)
active: bool = Field(default=False, title="Whether the account is active.")

class Config:
"""Pydantic configuration class."""

# Validate attributes when assigning them
validate_assignment = True

# Forbid extra attributes to prevent unexpected behavior
extra = "forbid"
underscore_attrs_are_private = True


# ------------------ Update Model ------------------


@update_model
class UserUpdate(UserRequest):
class UserUpdate(UserBase, BaseZenModel):
"""Update model for users."""

name: Optional[str] = Field(
title="The unique username for the account.",
max_length=STR_FIELD_MAX_LENGTH,
default=None,
)
full_name: Optional[str] = Field(
default=None,
title="The full name for the account owner. Only relevant for user "
"accounts.",
max_length=STR_FIELD_MAX_LENGTH,
)
is_admin: Optional[bool] = Field(
default=None,
title="Whether the account is an administrator.",
)
active: Optional[bool] = Field(
default=None, title="Whether the account is active."
)

@root_validator
def user_email_updates(cls, values: Dict[str, Any]) -> Dict[str, Any]:
"""Validate that the UserUpdateModel conforms to the email-opt-in-flow.
Expand Down Expand Up @@ -231,6 +261,9 @@ class UserResponseBody(BaseDatedResponseBody):
is_service_account: bool = Field(
title="Indicates whether this is a service account or a user account."
)
is_admin: bool = Field(
title="Whether the account is an administrator.",
)


class UserResponseMetadata(BaseResponseMetadata):
Expand Down Expand Up @@ -340,6 +373,15 @@ def is_service_account(self) -> bool:
"""
return self.get_body().is_service_account

@property
def is_admin(self) -> bool:
"""The `is_admin` property.
Returns:
Whether the user is an admin.
"""
return self.get_body().is_admin

@property
def email(self) -> Optional[str]:
"""The `email` property.
Expand Down
1 change: 1 addition & 0 deletions src/zenml/models/v2/misc/external_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class ExternalUserModel(BaseModel):
id: UUID
email: str
name: Optional[str] = None
is_admin: bool = False

class Config:
"""Pydantic configuration."""
Expand Down
Loading

0 comments on commit 01a595e

Please sign in to comment.