"""
Pydantic models of the account data that the REST api's responses include.
A resource that a user owns, e.g. an LLMClient or a plugin, is returned with its owner, as
:class:`AccountApiUserProfile`: the owner's username and email, and their account's number. It is
the output of :class:`smarter.apps.account.serializers.UserProfileSerializer`, which the REST api
returned before it was modeled, and which clients already read.
The accounts' REST api (``/api/v1/accounts/``), which only superusers may call, returns accounts
(:class:`AccountApiAccountDetail`), users (:class:`AccountApiUserDetail`), user profiles
(:class:`AccountApiProfile`) and account contacts (:class:`AccountApiContact`): the outputs of
:class:`smarter.apps.account.serializers.AccountSerializer`, ``UserSerializer`` and
``AccountContactSerializer``, which clients already read.
The models forbid extra fields: the api returns exactly what they declare.
"""
from datetime import datetime
from typing import TYPE_CHECKING, Optional, Union
from pydantic import BaseModel, ConfigDict, Field, JsonValue
from pydantic.alias_generators import to_camel
if TYPE_CHECKING:
from smarter.apps.account.models import Account, AccountContact
from smarter.apps.account.models import User as UserModel
from smarter.apps.account.models import UserProfile
MetaDataApiAnnotations = Optional[Union[list[dict[str, JsonValue]], dict[str, JsonValue]]]
"""
``MetaDataModel.annotations``: a list of key-value pairs, e.g. ``[{"smarter.sh/purpose": "example"}]``, or,.
in legacy rows, an object.
"""
[docs]
class AccountApiModel(BaseModel):
"""
Base class of the account contract's models.
The api's JSON is camelCase, e.g. ``accountNumber``. The models' fields are snake_case, and
their camelCase aliases are what they validate and dump.
"""
model_config = ConfigDict(
extra="forbid",
alias_generator=to_camel,
validate_by_name=True,
validate_by_alias=True,
serialize_by_alias=True,
)
[docs]
class AccountApiUser(AccountApiModel):
"""A user, as the owner of a resource: a :class:`django.contrib.auth.models.User`'s username and email."""
username: str = Field(..., description="User.username.")
email: Optional[str] = Field(None, description="User.email.")
[docs]
@classmethod
def from_model(cls, user: "UserModel") -> "AccountApiUser":
"""
The contract's model of a user.
:param user: The row.
:type user: django.contrib.auth.models.User
:returns: The model.
:rtype: AccountApiUser
"""
return cls(username=user.username, email=user.email)
[docs]
class AccountApiAccount(AccountApiModel):
"""An account, as the owner of a resource: a :class:`smarter.apps.account.models.Account`'s number."""
account_number: str = Field(..., description="Account.account_number, e.g. 1234-5678-9012.")
[docs]
@classmethod
def from_model(cls, account: "Account") -> "AccountApiAccount":
"""
The contract's model of an account.
:param account: The row.
:type account: smarter.apps.account.models.Account
:returns: The model.
:rtype: AccountApiAccount
"""
return cls(account_number=account.account_number)
[docs]
class AccountApiUserProfile(AccountApiModel):
"""The owner of a resource: a :class:`smarter.apps.account.models.UserProfile`'s user and account."""
user: AccountApiUser
account: AccountApiAccount
[docs]
@classmethod
def from_model(cls, user_profile: "UserProfile") -> "AccountApiUserProfile":
"""
The contract's model of a resource's owner.
:param user_profile: The row, whose ``user`` and ``account`` should be fetched with it.
:type user_profile: smarter.apps.account.models.UserProfile
:returns: The model.
:rtype: AccountApiUserProfile
"""
return cls(
user=AccountApiUser.from_model(user_profile.user),
account=AccountApiAccount.from_model(user_profile.account),
)
[docs]
class AccountApiAccountDetail(AccountApiModel):
"""An account: a :class:`smarter.apps.account.models.Account`."""
id: int = Field(..., description="The row's primary key.")
hashed_id: str = Field(..., description="The account's hashed id, e.g. rMTAwMDAwMQx.")
created_at: Optional[datetime] = Field(None, description="TimestampedModel.created_at.")
updated_at: Optional[datetime] = Field(None, description="TimestampedModel.updated_at.")
name: str = Field(..., description="MetaDataModel.name.")
description: Optional[str] = Field(None, description="MetaDataModel.description.")
version: Optional[str] = Field(None, description="MetaDataModel.version, e.g. 1.0.0.")
tags: list[str] = Field(default_factory=list, description="MetaDataModel.tags, by name.")
annotations: MetaDataApiAnnotations = Field(None, description="MetaDataModel.annotations.")
account_number: Optional[str] = Field(None, description="Account.account_number, e.g. 1234-5678-9012.")
is_default_account: bool = Field(..., description="Account.is_default_account.")
company_name: str = Field(..., description="Account.company_name.")
phone_number: Optional[str] = Field(None, description="Account.phone_number.")
address1: Optional[str] = Field(None, description="Account.address1.")
address2: Optional[str] = Field(None, description="Account.address2.")
city: Optional[str] = Field(None, description="Account.city.")
state: Optional[str] = Field(None, description="Account.state.")
postal_code: Optional[str] = Field(None, description="Account.postal_code.")
country: Optional[str] = Field(None, description="Account.country.")
language: Optional[str] = Field(None, description="Account.language, e.g. EN.")
timezone: Optional[str] = Field(None, description="Account.timezone, e.g. America/Denver.")
currency: Optional[str] = Field(None, description="Account.currency, e.g. USD.")
is_active: bool = Field(..., description="Account.is_active.")
[docs]
@classmethod
def from_model(cls, account: "Account") -> "AccountApiAccountDetail":
"""
The contract's model of an account.
:param account: The row.
:type account: smarter.apps.account.models.Account
:returns: The model.
:rtype: AccountApiAccountDetail
"""
return cls(
id=account.pk,
hashed_id=account.hashed_id,
created_at=account.created_at,
updated_at=account.updated_at,
name=account.name,
description=account.description,
version=account.version,
tags=[tag.name for tag in account.tags.all()],
annotations=account.annotations,
account_number=account.account_number,
is_default_account=account.is_default_account,
company_name=account.company_name,
phone_number=account.phone_number,
address1=account.address1,
address2=account.address2,
city=account.city,
state=account.state,
postal_code=account.postal_code,
country=account.country,
language=account.language,
timezone=account.timezone,
currency=account.currency,
is_active=account.is_active,
)
[docs]
class AccountApiUserDetail(AccountApiModel):
"""A user: a :class:`django.contrib.auth.models.User`, without its password and permissions."""
id: int = Field(..., description="The row's primary key.")
username: str = Field(..., description="User.username.")
first_name: str = Field("", description="User.first_name.")
last_name: str = Field("", description="User.last_name.")
email: str = Field("", description="User.email.")
is_staff: bool = Field(..., description="User.is_staff: whether the user administers their account.")
is_superuser: bool = Field(..., description="User.is_superuser: whether the user administers the platform.")
[docs]
@classmethod
def from_model(cls, user: "UserModel") -> "AccountApiUserDetail":
"""
The contract's model of a user.
:param user: The row.
:type user: django.contrib.auth.models.User
:returns: The model.
:rtype: AccountApiUserDetail
"""
return cls(
id=user.pk,
username=user.username,
first_name=user.first_name,
last_name=user.last_name,
email=user.email,
is_staff=user.is_staff,
is_superuser=user.is_superuser,
)
[docs]
class AccountApiProfile(AccountApiModel):
"""A user's membership of an account: a :class:`smarter.apps.account.models.UserProfile`."""
id: int = Field(..., description="The row's primary key.")
hashed_id: str = Field(..., description="The user profile's hashed id.")
created_at: Optional[datetime] = Field(None, description="TimestampedModel.created_at.")
updated_at: Optional[datetime] = Field(None, description="TimestampedModel.updated_at.")
name: str = Field(..., description="MetaDataModel.name.")
description: Optional[str] = Field(None, description="MetaDataModel.description.")
version: Optional[str] = Field(None, description="MetaDataModel.version, e.g. 1.0.0.")
tags: list[str] = Field(default_factory=list, description="MetaDataModel.tags, by name.")
annotations: MetaDataApiAnnotations = Field(None, description="MetaDataModel.annotations.")
user: AccountApiUserDetail = Field(..., description="UserProfile.user.")
account: AccountApiAccount = Field(..., description="UserProfile.account.")
profile_image_url: Optional[str] = Field(None, description="UserProfile.profile_image_url.")
is_test: bool = Field(..., description="UserProfile.is_test: whether a test created it.")
[docs]
@classmethod
def from_model(cls, user_profile: "UserProfile") -> "AccountApiProfile":
"""
The contract's model of a user profile.
:param user_profile: The row, whose ``user`` and ``account`` should be fetched with it.
:type user_profile: smarter.apps.account.models.UserProfile
:returns: The model.
:rtype: AccountApiProfile
"""
return cls(
id=user_profile.pk,
hashed_id=user_profile.hashed_id,
created_at=user_profile.created_at,
updated_at=user_profile.updated_at,
name=user_profile.name,
description=user_profile.description,
version=user_profile.version,
tags=[tag.name for tag in user_profile.tags.all()],
annotations=user_profile.annotations,
user=AccountApiUserDetail.from_model(user_profile.user),
account=AccountApiAccount.from_model(user_profile.account),
profile_image_url=user_profile.profile_image_url,
is_test=user_profile.is_test,
)
ACCOUNT_API_MODELS = (AccountApiAccountDetail, AccountApiUserDetail, AccountApiProfile, AccountApiContact)
"""The models whose JSON Schemas the Smarter api's OpenAPI document includes."""
__all__ = [
"ACCOUNT_API_MODELS",
"AccountApiAccount",
"AccountApiAccountDetail",
"AccountApiContact",
"AccountApiModel",
"AccountApiProfile",
"AccountApiUser",
"AccountApiUserDetail",
"AccountApiUserProfile",
"MetaDataApiAnnotations",
]