← Back to team overview

sts-sponsors team mailing list archive

[Merge] ~ack/maas-site-manager:schema-models-package into maas-site-manager:main

 

Alberto Donato has proposed merging ~ack/maas-site-manager:schema-models-package into maas-site-manager:main.

Commit message:
move db-related models to the db package, and API-related ones to user_api

This also drops some unused schemas


Requested reviews:
  MAAS Committers (maas-committers)

For more details, see:
https://code.launchpad.net/~ack/maas-site-manager/+git/site-manager/+merge/442380
-- 
Your team MAAS Committers is requested to review the proposed merge of ~ack/maas-site-manager:schema-models-package into maas-site-manager:main.
diff --git a/backend/msm/db/models.py b/backend/msm/db/models.py
new file mode 100644
index 0000000..a04bc3e
--- /dev/null
+++ b/backend/msm/db/models.py
@@ -0,0 +1,63 @@
+from datetime import datetime
+from uuid import UUID
+
+from pydantic import (
+    BaseModel,
+    EmailStr,
+    Field,
+    SecretStr,
+)
+
+from ..schema import TimeZone
+
+
+class SiteData(BaseModel):
+    """Data for a site."""
+
+    allocated_machines: int
+    deployed_machines: int
+    ready_machines: int
+    error_machines: int
+    last_seen: datetime
+
+
+class Site(BaseModel):
+    """A MAAS installation."""
+
+    id: int
+    name: str
+    city: str | None
+    country: str | None = Field(min_length=2, max_length=2)
+    latitude: str | None
+    longitude: str | None
+    note: str | None
+    region: str | None
+    street: str | None
+    timezone: TimeZone | None
+    url: str
+    stats: SiteData | None
+
+
+class Token(BaseModel):
+    """A registration token for a site."""
+
+    id: int
+    value: UUID
+    site_id: int | None
+    expired: datetime
+    created: datetime
+
+
+class User(BaseModel):
+    """A user."""
+
+    id: int
+    email: EmailStr = Field(title="email@xxxxxxxxxxx")
+    full_name: str
+
+
+class UserWithPassword(User):
+    """A user with its password."""
+
+    # use password.get_secret_value() to retrieve the value
+    password: SecretStr = Field(min_length=8, max_length=100)
diff --git a/backend/msm/db/queries.py b/backend/msm/db/queries.py
index ff5df47..9248559 100644
--- a/backend/msm/db/queries.py
+++ b/backend/msm/db/queries.py
@@ -20,21 +20,23 @@ from sqlalchemy import (
 )
 from sqlalchemy.ext.asyncio import AsyncSession
 
-from ..schema import (
-    MAX_PAGE_SIZE,
-    Site as SiteSchema,
-    Token as TokenSchema,
-    UserWithPassword as UserPWSchema,
-)
+from ..schema import MAX_PAGE_SIZE
 from ._tables import (
     Site,
     SiteData,
     Token,
     User,
 )
+from .models import (
+    Site as SiteSchema,
+    Token as TokenSchema,
+    UserWithPassword as UserWithPasswordSchema,
+)
 
 
-async def get_user(session: AsyncSession, email: str) -> UserPWSchema | None:
+async def get_user(
+    session: AsyncSession, email: str
+) -> UserWithPasswordSchema | None:
     """
     Gets a user by its unique identifier: their email
     """
@@ -50,7 +52,7 @@ async def get_user(session: AsyncSession, email: str) -> UserPWSchema | None:
     )
     if result := await session.execute(stmt):
         if user := result.one_or_none():
-            return UserPWSchema(**user._asdict())
+            return UserWithPasswordSchema(**user._asdict())
     return None
 
 
diff --git a/backend/msm/schema/__init__.py b/backend/msm/schema/__init__.py
index 21e6316..114792b 100644
--- a/backend/msm/schema/__init__.py
+++ b/backend/msm/schema/__init__.py
@@ -1,18 +1,6 @@
 """API schema definitions."""
 
-from ._models import (
-    CreateTokensRequest,
-    CreateTokensResponse,
-    JSONWebToken,
-    JSONWebTokenData,
-    PaginatedSites,
-    PaginatedTokens,
-    Site,
-    Token,
-    User,
-    UserLoginRequest,
-    UserWithPassword,
-)
+from ._fields import TimeZone
 from ._pagination import (
     MAX_PAGE_SIZE,
     PaginatedResults,
@@ -21,19 +9,9 @@ from ._pagination import (
 )
 
 __all__ = [
-    "CreateTokensRequest",
-    "CreateTokensResponse",
-    "JSONWebToken",
-    "JSONWebTokenData",
     "MAX_PAGE_SIZE",
     "PaginatedResults",
-    "PaginatedSites",
-    "PaginatedTokens",
     "PaginationParams",
-    "Site",
-    "Token",
-    "User",
-    "UserLoginRequest",
-    "UserWithPassword",
     "pagination_params",
+    "TimeZone",
 ]
diff --git a/backend/msm/schema/_fields.py b/backend/msm/schema/_fields.py
new file mode 100644
index 0000000..9e0e830
--- /dev/null
+++ b/backend/msm/schema/_fields.py
@@ -0,0 +1,5 @@
+import pytz
+from strenum import StrEnum
+
+# Enum with timezones accepted by pytz.
+TimeZone = StrEnum("TimeZone", pytz.all_timezones)
diff --git a/backend/msm/schema/_models.py b/backend/msm/schema/_models.py
deleted file mode 100644
index 1362f63..0000000
--- a/backend/msm/schema/_models.py
+++ /dev/null
@@ -1,168 +0,0 @@
-from datetime import (
-    datetime,
-    timedelta,
-)
-from uuid import UUID
-
-from pydantic import (
-    BaseModel,
-    EmailStr,
-    SecretStr,
-)
-from pydantic.fields import Field
-import pytz
-from strenum import StrEnum
-
-from ._pagination import PaginatedResults
-
-# Enum with timezones accepted by pytz.
-TimeZone = StrEnum("TimeZone", pytz.all_timezones)
-
-
-class ReadUser(BaseModel):
-    """
-    A MAAS Site Manager User
-    We never want to sent the password (hash) around
-    """
-
-    email: EmailStr = Field(title="email@xxxxxxxxxxx")
-    full_name: str
-
-
-class User(ReadUser):
-    """
-    To read a user from the DB it comes with an ID
-    """
-
-    id: int
-
-
-class UserWithPassword(User):
-    """
-    To create a user we need a password as well.
-    """
-
-    # use password.get_secret_value() to retrieve the value
-    password: SecretStr = Field(min_length=8, max_length=100)
-
-
-class UserLoginRequest(BaseModel):
-    """User login details."""
-
-    username: str
-    password: str
-
-
-class CreateSite(BaseModel):
-    """
-    A MAAS installation
-    """
-
-    name: str
-    city: str | None
-    country: str | None = Field(min_length=2, max_length=2)
-    latitude: str | None
-    longitude: str | None
-    note: str | None
-    region: str | None
-    street: str | None
-    timezone: TimeZone | None  # type: ignore
-    url: str
-    # TODO: we will need to add tags
-
-
-class SiteData(BaseModel):
-    """Data for a site"""
-
-    allocated_machines: int
-    deployed_machines: int
-    ready_machines: int
-    error_machines: int
-    last_seen: datetime
-
-
-class Site(CreateSite):
-    """
-    Site persisted to the database
-    """
-
-    id: int
-    stats: SiteData | None
-
-
-class CreateSiteData(SiteData):
-    """Site data"""
-
-    site_id: int
-
-
-class PaginatedSites(PaginatedResults):
-    items: list[Site]
-
-
-class SiteWithData(Site):
-
-    """
-    A site, together with its SiteData
-    """
-
-    id: int
-    site_data: SiteData
-
-
-class CreateToken(BaseModel):
-    """
-    To create a token a value and an expiration
-    time need to be generated
-    """
-
-    site_id: int | None
-    value: UUID
-    expired: datetime
-    created: datetime
-
-
-class Token(CreateToken):
-    """
-    A token persisted to the database
-    """
-
-    id: int
-
-
-class JSONWebToken(BaseModel):
-    """
-    A JSON Web Token for authenticating users.
-    """
-
-    access_token: str
-    token_type: str
-
-
-class JSONWebTokenData(BaseModel):
-    """
-    The payload data for a JWT Token
-    """
-
-    email: str
-
-
-class PaginatedTokens(PaginatedResults):
-    items: list[Token]
-
-
-class CreateTokensRequest(BaseModel):
-    """
-    Request to create one or more tokens, with a certain validity,
-    expressed in seconds.
-    """
-
-    count: int = 1
-    duration: timedelta
-
-
-class CreateTokensResponse(BaseModel):
-    """List of created tokens, along with their duration."""
-
-    expired: datetime
-    tokens: list[UUID]
diff --git a/backend/msm/user_api/_base.py b/backend/msm/user_api/_handlers.py
similarity index 98%
rename from backend/msm/user_api/_base.py
rename to backend/msm/user_api/_handlers.py
index 1f4d1f1..c1dcd0a 100644
--- a/backend/msm/user_api/_base.py
+++ b/backend/msm/user_api/_handlers.py
@@ -13,16 +13,10 @@ from ..db import (
     db_session,
     queries,
 )
+from ..db.models import User
 from ..schema import (
-    CreateTokensRequest,
-    CreateTokensResponse,
-    JSONWebToken,
-    PaginatedSites,
-    PaginatedTokens,
     pagination_params,
     PaginationParams,
-    User,
-    UserLoginRequest,
 )
 from ..settings import SETTINGS
 from ._forms import (
@@ -34,6 +28,14 @@ from ._jwt import (
     create_access_token,
     get_authenticated_user,
 )
+from ._schema import (
+    CreateTokensRequest,
+    CreateTokensResponse,
+    JSONWebToken,
+    PaginatedSites,
+    PaginatedTokens,
+    UserLoginRequest,
+)
 
 
 async def root() -> dict[str, str]:
diff --git a/backend/msm/user_api/_jwt.py b/backend/msm/user_api/_jwt.py
index 8f7472e..ecf2a26 100644
--- a/backend/msm/user_api/_jwt.py
+++ b/backend/msm/user_api/_jwt.py
@@ -21,14 +21,10 @@ from passlib.context import CryptContext
 from sqlalchemy.ext.asyncio import AsyncSession
 
 from ..db import db_session
-
-# from ..db import db_session
+from ..db.models import UserWithPassword
 from ..db.queries import get_user
-from ..schema import (
-    JSONWebTokenData,
-    UserWithPassword,
-)
 from ..settings import SETTINGS
+from ._schema import JSONWebTokenData
 
 pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
 oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
diff --git a/backend/msm/user_api/_schema.py b/backend/msm/user_api/_schema.py
new file mode 100644
index 0000000..ee6c3f8
--- /dev/null
+++ b/backend/msm/user_api/_schema.py
@@ -0,0 +1,62 @@
+from datetime import (
+    datetime,
+    timedelta,
+)
+from uuid import UUID
+
+from pydantic import BaseModel
+
+from ..db.models import (
+    Site,
+    Token,
+)
+from ..schema import PaginatedResults
+
+
+class CreateTokensRequest(BaseModel):
+    """
+    Request to create one or more tokens, with a certain validity,
+    expressed in seconds.
+    """
+
+    count: int = 1
+    duration: timedelta
+
+
+class CreateTokensResponse(BaseModel):
+    """List of created tokens, along with their duration."""
+
+    expired: datetime
+    tokens: list[UUID]
+
+
+class PaginatedSites(PaginatedResults):
+    items: list[Site]
+
+
+class PaginatedTokens(PaginatedResults):
+    items: list[Token]
+
+
+class UserLoginRequest(BaseModel):
+    """User login request schema."""
+
+    username: str
+    password: str
+
+
+class JSONWebToken(BaseModel):
+    """
+    A JSON Web Token for authenticating users.
+    """
+
+    access_token: str
+    token_type: str
+
+
+class JSONWebTokenData(BaseModel):
+    """
+    The payload data for a JWT Token
+    """
+
+    email: str
diff --git a/backend/msm/user_api/_setup.py b/backend/msm/user_api/_setup.py
index e058f20..6efd588 100644
--- a/backend/msm/user_api/_setup.py
+++ b/backend/msm/user_api/_setup.py
@@ -4,7 +4,7 @@ from typing import AsyncGenerator
 from fastapi import FastAPI
 from fastapi.middleware.cors import CORSMiddleware
 
-from . import _base
+from . import _handlers
 from .. import PACKAGE
 from ..db import Database
 from ..settings import SETTINGS
@@ -36,12 +36,16 @@ def create_app(db_dsn: str | None = None) -> FastAPI:
         allow_headers=["*"],
     )
     app.state.db = db
-    app.router.add_api_route("/", _base.root, methods=["GET"])
-    app.router.add_api_route("/sites", _base.sites, methods=["GET"])
-    app.router.add_api_route("/tokens", _base.tokens, methods=["GET"])
-    app.router.add_api_route("/tokens", _base.tokens_post, methods=["POST"])
+    app.router.add_api_route("/", _handlers.root, methods=["GET"])
+    app.router.add_api_route("/sites", _handlers.sites, methods=["GET"])
+    app.router.add_api_route("/tokens", _handlers.tokens, methods=["GET"])
     app.router.add_api_route(
-        "/login", _base.login_for_access_token, methods=["POST"]
+        "/tokens", _handlers.tokens_post, methods=["POST"]
+    )
+    app.router.add_api_route(
+        "/login", _handlers.login_for_access_token, methods=["POST"]
+    )
+    app.router.add_api_route(
+        "/users/me", _handlers.read_users_me, methods=["GET"]
     )
-    app.router.add_api_route("/users/me", _base.read_users_me, methods=["GET"])
     return app

Follow ups