From 88eeec57e3edcb675cefdb633607184064edf7e5 Mon Sep 17 00:00:00 2001 From: romeonicholas Date: Mon, 12 Feb 2024 07:20:15 +0100 Subject: [PATCH] feat: Add descriptions, examples, and validations to pydantic fields --- backend/capellacollab/core/metadata.py | 45 ++++++-- backend/capellacollab/core/models.py | 60 ++++++++-- backend/capellacollab/events/models.py | 38 +++++-- backend/capellacollab/notices/models.py | 25 +++-- backend/capellacollab/projects/models.py | 106 +++++++++++++----- .../projects/toolmodels/models.py | 99 +++++++++++----- .../tools/integrations/models.py | 30 +++-- backend/capellacollab/users/models.py | 59 +++++++--- backend/capellacollab/users/tokens/models.py | 41 +++++-- 9 files changed, 375 insertions(+), 128 deletions(-) diff --git a/backend/capellacollab/core/metadata.py b/backend/capellacollab/core/metadata.py index a61cef9033..d11fdb3276 100644 --- a/backend/capellacollab/core/metadata.py +++ b/backend/capellacollab/core/metadata.py @@ -4,7 +4,7 @@ import typing as t import fastapi -import pydantic +from pydantic import BaseModel, ConfigDict, Field from sqlalchemy import orm import capellacollab @@ -14,19 +14,40 @@ from capellacollab.settings.configuration import models as config_models -class Metadata(pydantic.BaseModel): - model_config = pydantic.ConfigDict(from_attributes=True) +class Metadata(BaseModel): + model_config = ConfigDict(from_attributes=True) - version: str - privacy_policy_url: str | None - imprint_url: str | None - provider: str | None - authentication_provider: str | None - environment: str | None + version: str = Field( + description="The version of the application", examples=["1.0.0"] + ) + privacy_policy_url: str | None = Field( + description="The URL to the privacy policy", + examples=["https://example.com/privacy-policy"], + ) + imprint_url: str | None = Field( + description="The URL to the imprint", + examples=["https://example.com/imprint"], + ) + provider: str | None = Field( + description="The application provider", + examples=["DB InfraGO AG"], + ) + authentication_provider: str | None = Field( + description="The authentication provider", examples=["OAuth2"] + ) + environment: str | None = Field( + description="The application environment", examples=["test"] + ) - host: str | None - port: str | None - protocol: str | None + host: str | None = Field( + description="The host of the application", examples=["localhost"] + ) + port: str | None = Field( + description="The port of the application", examples=["4200"] + ) + protocol: str | None = Field( + description="The protocol of the application", examples=["https"] + ) router = fastapi.APIRouter() diff --git a/backend/capellacollab/core/models.py b/backend/capellacollab/core/models.py index a7743d36af..cd3620f98c 100644 --- a/backend/capellacollab/core/models.py +++ b/backend/capellacollab/core/models.py @@ -3,21 +3,59 @@ import typing as t -import pydantic +from pydantic import BaseModel, Field T = t.TypeVar("T") -class Message(pydantic.BaseModel): - err_code: str | None = None - title: str | None = None - reason: str | tuple | None = None - technical: str | None = None - - -class ResponseModel(pydantic.BaseModel): - warnings: list[Message] | None = None - errors: list[Message] | None = None +class Message(BaseModel): + err_code: str | None = Field( + description="The HTTP response status code", + examples=[ + "422 Unprocessable Content", + ], + ) + title: str | None = Field( + description="The error title", + examples=["Repository deletion failed partially."], + ) + reason: str | tuple | None = Field( + description="The user friendly error description", + examples=["The TeamForCapella server is not reachable."], + ) + technical: str | None = Field( + description="The technical developer error description", + examples=["TeamForCapella returned status code {e.status_code}"], + ) + + +class ResponseModel(BaseModel): + warnings: list[Message] | None = Field( + description="The list of warning message objects", + examples=[ + [ + { + "err_code": "422 Unprocessable Content", + "title": "Repository deletion failed partially.", + "reason": "The TeamForCapella server is not reachable.", + "technical": "TeamForCapella returned status code {e.status_code}", + } + ] + ], + ) + errors: list[Message] | None = Field( + description="The list of error message objects", + examples=[ + [ + { + "err_code": "422 Unprocessable Content", + "title": "Repository deletion failed partially.", + "reason": "TeamForCapella returned an error when deleting the repository.", + "technical": "TeamForCapella returned status code {e.status_code}", + } + ] + ], + ) class PayloadResponseModel(ResponseModel, t.Generic[T]): diff --git a/backend/capellacollab/events/models.py b/backend/capellacollab/events/models.py index b74186bfe9..b7b2d898c9 100644 --- a/backend/capellacollab/events/models.py +++ b/backend/capellacollab/events/models.py @@ -5,8 +5,8 @@ import enum import typing as t -import pydantic import sqlalchemy as sa +from pydantic import BaseModel, ConfigDict, Field, field_serializer from sqlalchemy import orm from capellacollab.core import database @@ -33,17 +33,35 @@ class EventType(enum.Enum): ASSIGNED_ROLE_USER = "AssignedRoleUser" -class BaseHistoryEvent(pydantic.BaseModel): - model_config = pydantic.ConfigDict(from_attributes=True) +class BaseHistoryEvent(BaseModel): + model_config = ConfigDict(from_attributes=True) - user: users_models.User - executor: users_models.User | None = None - project: projects_models.Project | None = None - execution_time: datetime.datetime - event_type: EventType - reason: str | None = None + user: users_models.User = Field( + description="The user affected by an event", + examples=[{"id": 2, "name": "John Doe", "role": "user"}], + ) + executor: users_models.User | None = Field( + description="The user who executed an event", + examples=[{"id": 1, "name": "Joe Manager", "role": "admin"}], + ) + project: projects_models.Project | None = Field( + description="The project affected by an event", + examples=[{"id": 1, "name": "Project A"}], + ) + execution_time: datetime.datetime = Field( + description="The time an event was executed", + examples=["2021-01-01T12:00:00Z"], + ) + event_type: EventType = Field( + description="The type of event executed", examples=["CreatedUser"] + ) + reason: str | None = Field( + description="The rationale provided by the executor of an event", + examples=["New hire"], + max_length=255, + ) - _validate_execution_time = pydantic.field_serializer("execution_time")( + _validate_execution_time = field_serializer("execution_time")( core_pydantic.datetime_serializer ) diff --git a/backend/capellacollab/notices/models.py b/backend/capellacollab/notices/models.py index ef38002985..5711ce53a5 100644 --- a/backend/capellacollab/notices/models.py +++ b/backend/capellacollab/notices/models.py @@ -3,7 +3,7 @@ import enum -import pydantic +from pydantic import BaseModel, ConfigDict, Field from sqlalchemy import orm from capellacollab.core import database @@ -19,12 +19,23 @@ class NoticeLevel(enum.Enum): ALERT = "alert" -class CreateNoticeRequest(pydantic.BaseModel): - model_config = pydantic.ConfigDict(from_attributes=True) - - level: NoticeLevel - title: str - message: str +class CreateNoticeRequest(BaseModel): + model_config = ConfigDict(from_attributes=True) + + level: NoticeLevel = Field( + description="The severity or indication level of a notice", + examples=["info"], + ) + title: str = Field( + description="The title of a notice", + examples=["Planned Maintenance 13.09.2021"], + ) + message: str = Field( + description="The message body of a notice", + examples=[ + "The site will be unavailable from 7:00 until 14:00 on 13.09.2021." + ], + ) class NoticeResponse(CreateNoticeRequest): diff --git a/backend/capellacollab/projects/models.py b/backend/capellacollab/projects/models.py index be808655c6..7b39e118b8 100644 --- a/backend/capellacollab/projects/models.py +++ b/backend/capellacollab/projects/models.py @@ -6,7 +6,7 @@ import enum import typing as t -import pydantic +from pydantic import BaseModel, ConfigDict, Field, field_validator from sqlalchemy import orm # Import required for sqlalchemy @@ -18,10 +18,16 @@ from capellacollab.projects.users.models import ProjectUserAssociation -class UserMetadata(pydantic.BaseModel): - leads: int - contributors: int - subscribers: int +class UserMetadata(BaseModel): + leads: int = Field( + description="The number of users with the manager role in a project" + ) + contributors: int = Field( + description="The number of non-manager users with write access in a project" + ) + subscribers: int = Field( + description="The number of non-manager users with read access in a project" + ) class Visibility(enum.Enum): @@ -34,18 +40,39 @@ class ProjectType(enum.Enum): TRAINING = "training" -class Project(pydantic.BaseModel): - model_config = pydantic.ConfigDict(from_attributes=True) +class Project(BaseModel): + model_config = ConfigDict(from_attributes=True) - name: str - slug: str - description: str | None = None - visibility: Visibility - type: ProjectType - users: UserMetadata - is_archived: bool + name: str = Field( + description="The name of a project", + examples=["Automated Coffee Experiences"], + max_length=255, + ) + slug: str = Field( + description="The slug derived from the name of a project", + examples=["automated-coffee-experiences"], + ) + description: str | None = Field( + description="The description of a project", + examples=["Models for exploring automated coffee experiences."], + ) + visibility: Visibility = Field( + description="The visibility of a project to users within the Collab Manager", + examples=["private"], + ) + type: ProjectType = Field( + description="The type of project (general or training)", + examples=["general"], + ) + users: UserMetadata = Field( + description="The metadata of users in a project", + examples=[{"leads": 1, "contributors": 2, "subscribers": 3}], + ) + is_archived: bool = Field( + description="The archive status of a project", examples=[False] + ) - @pydantic.field_validator("users", mode="before") + @field_validator("users", mode="before") @classmethod def transform_users(cls, data: t.Any): if isinstance(data, UserMetadata): @@ -87,18 +114,47 @@ def transform_users(cls, data: t.Any): return data -class PatchProject(pydantic.BaseModel): - name: str | None = None - description: str | None = None - visibility: Visibility | None = None - type: ProjectType | None = None - is_archived: bool | None = None +class PatchProject(BaseModel): + name: str | None = Field( + description="The name of a project provided for patching", + examples=["Robotic Coffee Experiences"], + max_length=255, + ) + description: str | None = Field( + description="The description of a project provided for patching", + examples=["Models for exploring robotic coffee experiences."], + max_length=1500, + ) + visibility: Visibility | None = Field( + description="The visibility of a project provided for patching", + examples=["private"], + ) + type: ProjectType | None = Field( + description="The type of project (general or training) provided for patching", + examples=["private"], + ) + is_archived: bool | None = Field( + description="The archive status of a project provided for patching", + examples=[True], + ) -class PostProjectRequest(pydantic.BaseModel): - name: str - description: str | None = None - visibility: Visibility = Visibility.PRIVATE +class PostProjectRequest(BaseModel): + name: str = Field( + description="The name of a project provided at creation", + examples=["Automated Coffee Experiences"], + max_length=255, + ) + description: str | None = Field( + description="The description of a project provided at creation", + examples=["Models for exploring automated coffee experiences."], + max_length=1500, + ) + visibility: Visibility = Field( + default=Visibility.PRIVATE, + description="The visibility of a project provided at creation", + examples=["private"], + ) class DatabaseProject(database.Base): diff --git a/backend/capellacollab/projects/toolmodels/models.py b/backend/capellacollab/projects/toolmodels/models.py index 0ab55fd171..71498c7d39 100644 --- a/backend/capellacollab/projects/toolmodels/models.py +++ b/backend/capellacollab/projects/toolmodels/models.py @@ -7,8 +7,8 @@ import enum import typing as t -import pydantic import sqlalchemy as sa +from pydantic import BaseModel, ConfigDict, Field from sqlalchemy import orm from capellacollab.core import database @@ -42,22 +42,49 @@ class EditingMode(enum.Enum): GIT = "git" -class PostCapellaModel(pydantic.BaseModel): - name: str - description: str | None = None - tool_id: int +class PostCapellaModel(BaseModel): + name: str = Field( + description="The name of a model provided at creation", + examples=["Coffee Machine"], + max_length=255, + ) + description: str | None = Field( + description="The description of a model provided at creation", + examples=["A model of a coffee machine."], + max_length=1500, + ) + tool_id: int = Field( + description="The model tool ID for a model provided at creation", + ) -class PatchCapellaModel(pydantic.BaseModel): - name: str | None = None - description: str | None = None - version_id: int | None = None - nature_id: int | None = None - project_slug: str | None = None - display_order: int | None = None +class PatchCapellaModel(BaseModel): + name: str | None = Field( + description="An optional new name for a model provided for patching", + examples=["Espresso Machine"], + max_length=255, + ) + description: str | None = Field( + description="An optional new description for a model provided for patching", + examples=["A model of an espresso machine."], + max_length=1500, + ) + version_id: int | None = Field( + description="An optional model version ID for a model provided for patching", + ) + nature_id: int | None = Field( + description="An optional nature ID for a model provided for patching", + ) + project_slug: str | None = Field( + description="An optional project slug for a model for patching, derived from the model name provided by the model update request", + examples=["espresso-machine"], + ) + display_order: int | None = Field( + description="An optional display order index for a model in the Project Overview provided for patching", + ) -class ToolDetails(pydantic.BaseModel): +class ToolDetails(BaseModel): version_id: int nature_id: int @@ -111,18 +138,40 @@ class DatabaseCapellaModel(database.Base): ) -class CapellaModel(pydantic.BaseModel): - model_config = pydantic.ConfigDict(from_attributes=True) +class CapellaModel(BaseModel): + model_config = ConfigDict(from_attributes=True) - id: int - slug: str - name: str - description: str - display_order: int | None - tool: tools_models.ToolBase - version: tools_models.ToolVersionBase | None = None - nature: tools_models.ToolNatureBase | None = None - git_models: list[GitModel] | None = None - t4c_models: list[T4CModel] | None = None + id: int = Field(description="The unique ID of a model", examples=[1]) + slug: str = Field( + description="The unique slug of a model", examples=["coffee-machine"] + ) + name: str = Field( + description="The name of a model", + examples=["Coffee Machine"], + max_length=255, + ) + description: str = Field( + description="The description of a model", + examples=["A model of a coffee machine."], + max_length=1500, + ) + display_order: int | None = Field( + description="The display order index of a model in the Project Overview", + ) + tool: tools_models.ToolBase = Field( + description="The id, name, and tool integrations of a tool" + ) + version: tools_models.ToolVersionBase | None = Field( + description="The id, name, and recommended or deprecated states of a tool version" + ) + nature: tools_models.ToolNatureBase | None = Field( + description="The id and name of the model's tool nature" + ) + git_models: list[GitModel] | None = Field( + description="A list of git models associated with the model" + ) + t4c_models: list[T4CModel] | None = Field( + description="A list of T4C models associated with the model" + ) restrictions: restrictions_models.ToolModelRestrictions | None = None diff --git a/backend/capellacollab/tools/integrations/models.py b/backend/capellacollab/tools/integrations/models.py index 9615d5663b..c34b02d639 100644 --- a/backend/capellacollab/tools/integrations/models.py +++ b/backend/capellacollab/tools/integrations/models.py @@ -5,8 +5,8 @@ import typing as t -import pydantic import sqlalchemy as sa +from pydantic import BaseModel, ConfigDict, Field from sqlalchemy import orm from capellacollab.core import database @@ -15,18 +15,28 @@ from capellacollab.tools.models import DatabaseTool -class ToolIntegrations(pydantic.BaseModel): - model_config = pydantic.ConfigDict(from_attributes=True) +class ToolIntegrations(BaseModel): + model_config = ConfigDict(from_attributes=True) - t4c: bool - pure_variants: bool - jupyter: bool + t4c: bool = Field(description="Status of the tool integration with T4C") + pure_variants: bool = Field( + description="Status of the tool integration with Pure Variants" + ) + jupyter: bool = Field( + description="Status of the tool integration with Jupyter" + ) -class PatchToolIntegrations(pydantic.BaseModel): - t4c: bool | None = None - pure_variants: bool | None = None - jupyter: bool | None = None +class PatchToolIntegrations(BaseModel): + t4c: bool | None = Field( + description="Indicator of whether the tool is integrated with T4C provided for patching" + ) + pure_variants: bool | None = Field( + description="Indicator of whether the tool is integrated with Pure Variants for patching" + ) + jupyter: bool | None = Field( + description="Indicator of whether the tool is integrated with Jupyter for patching" + ) class DatabaseToolIntegrations(database.Base): diff --git a/backend/capellacollab/users/models.py b/backend/capellacollab/users/models.py index d1ffef652e..435580566d 100644 --- a/backend/capellacollab/users/models.py +++ b/backend/capellacollab/users/models.py @@ -7,7 +7,7 @@ import enum import typing as t -import pydantic +from pydantic import BaseModel, ConfigDict, Field, field_serializer from sqlalchemy import orm from capellacollab.core import database @@ -25,35 +25,62 @@ class Role(enum.Enum): ADMIN = "administrator" -class BaseUser(pydantic.BaseModel): - model_config = pydantic.ConfigDict(from_attributes=True) +class BaseUser(BaseModel): + model_config = ConfigDict(from_attributes=True) - name: str - role: Role + name: str = Field( + description="The name of a user", examples=["John Doe"], max_length=50 + ) + role: Role = Field( + description="The application-level role of a user", examples=["user"] + ) class User(BaseUser): id: int - created: datetime.datetime | None = None - last_login: datetime.datetime | None = None + created: datetime.datetime | None = Field( + description="The time a user was created", + examples=["2021-01-01T12:00:00Z"], + ) + last_login: datetime.datetime | None = Field( + description="The time a user last logged in", + examples=["2021-01-01T12:00:00Z"], + ) - _validate_created = pydantic.field_serializer("created")( + _validate_created = field_serializer("created")( core_pydantic.datetime_serializer ) - _validate_last_login = pydantic.field_serializer("last_login")( + _validate_last_login = field_serializer("last_login")( core_pydantic.datetime_serializer ) -class PatchUserRoleRequest(pydantic.BaseModel): - role: Role - reason: str +class PatchUserRoleRequest(BaseModel): + role: Role = Field( + description="The application-level role of a user provided for patching", + examples=["admin"], + ) + reason: str = Field( + description="The rationale provided for patching a user's role", + examples=["User transfered to support team"], + ) -class PostUser(pydantic.BaseModel): - name: str - role: Role - reason: str +class PostUser(BaseModel): + name: str = Field( + description="The name of a user provided at creation", + examples=["superuser@hotmail.com"], + max_length=50, + ) + role: Role = Field( + description="The application-level role of a user provided at creation", + examples=["admin"], + ) + reason: str = Field( + description="The rationale provided for creating a user", + examples=["New hire"], + max_length=255, + ) class DatabaseUser(database.Base): diff --git a/backend/capellacollab/users/tokens/models.py b/backend/capellacollab/users/tokens/models.py index 38345a8403..207d10f216 100644 --- a/backend/capellacollab/users/tokens/models.py +++ b/backend/capellacollab/users/tokens/models.py @@ -3,8 +3,8 @@ import datetime import typing as t -import pydantic import sqlalchemy as sa +from pydantic import BaseModel, ConfigDict, Field from sqlalchemy import orm from capellacollab.core import database @@ -13,24 +13,41 @@ from capellacollab.users.models import DatabaseUser -class UserToken(pydantic.BaseModel): - model_config = pydantic.ConfigDict(from_attributes=True) +class UserToken(BaseModel): + model_config = ConfigDict(from_attributes=True) - id: int - user_id: int - hash: str - expiration_date: datetime.date - description: str + id: int = Field(description="The ID of the token") + user_id: int = Field(description="The user ID of the token creator") + hash: str = Field( + description="The automatically generated hash of the token" + ) + expiration_date: datetime.date = Field( + description="The user-provided expiration date of the token", + examples=["2022-01-01"], + ) + description: str = Field( + description="The user-provided description of the token", + examples=["Weekly automations"], + ) source: str class UserTokenWithPassword(UserToken): - password: str + password: str = Field( + description="The static token password generated at token creation", + examples=["collabmanager_1234567890"], + ) -class PostToken(pydantic.BaseModel): - expiration_date: datetime.datetime - description: str +class PostToken(BaseModel): + expiration_date: datetime.datetime = Field( + description="The expiration date of the token provided at creation", + examples=["2022-01-01"], + ) + description: str = Field( + description="The description of the token provided at creation", + examples=["Weekly automations"], + ) source: str