Skip to content

snap saas base_wiki

abner G Jacobsen edited this page May 3, 2025 · 1 revision

Generated on: 2025-05-03 13:44:06

Table of Contents

Introduction

Related Files

  • README.md

Related Pages

Related topics: Development Environment Setup, Data Model (SQLAlchemy ORM)

Introduction to Snap SAAS Base

Purpose and Functionality

The snap-saas-base repository serves as a Python base module designed to accelerate the development of Software as a Service (SAAS) applications. It provides a collection of pre-built components, configurations, and tools, promoting standardization, streamlining development, and ensuring code quality.

Core Features

  • Base Models: Includes abstract base classes for common SAAS entities like users, organizations, workspaces, and API keys, facilitating database interactions.
  • Development Environment: Offers pre-configured development environments using Docker and VS Code Dev Containers, ensuring consistency across development teams.
  • Development Tools: Integrates with tools like OpenCommit, gpt4docstrings, and aider to automate tasks such as commit message generation, documentation, and code modification.
  • Conventional Commits: Enforces the Conventional Commits standard for automated versioning and changelog generation.

Overall Architecture

The snap-saas-base module provides the foundational building blocks for a multi-tenant SAAS application. It defines the core data models and relationships, and provides tools and configurations to simplify the development process.

graph TD
    subgraph Core
        User[User Model]
        Organization[Organization Model]
        Workspace[Workspace Model]
        WorkspaceMember[WorkspaceMember Model]
        WorkspaceKv[WorkspaceKv Model]
        ApiKey[WorkspaceApiKey Model]
        ChatMessage[ChatMessage Model]
        Chat[Chat Model]
    end
    subgraph Tools
        OpenCommit[OpenCommit]
        gpt4docstrings[gpt4docstrings]
        Aider[Aider]
        Commitizen[Commitizen]
    end
    subgraph DevEnv
        Codespaces[GitHub Codespaces]
        DevContainers[Dev Containers]
        PyCharm[PyCharm]
        Terminal[Terminal]
    end

    Core --> Tools
    Core --> DevEnv
    linkStyle default fill:#f9f,stroke:#333,stroke-width:2px
Loading

Usage Instructions

  1. Installation: Add the package as a dependency to your project using poetry add snap-saas-base.
  2. Development Environment: Choose one of the supported development environments: GitHub Codespaces, Dev Containers, PyCharm, or Terminal.
  3. Configuration: Configure the development environment and tools as described in the "Contributing" section of the README.md file.
  4. Development: Use the provided base models and tools to build your SAAS application.

Code Examples

Adding the Package

poetry add snap-saas-base

This command adds snap-saas-base as a dependency to your pyproject.toml file.

Base Model Example

The snap-saas-base module provides abstract base classes for common SAAS entities. For example, the User model:

# src/snap_saas_base/models/user.py
import sqlalchemy as sa
import sqlalchemy.orm as so
import uuid6
from sqlalchemy.sql import func

from snap_saas_base.models.base_model import AbstractModel


class User(AbstractModel):
    """A class used to represent a User.

    This class inherits from the AbstractModel class and represents a user in a database.
    It includes information about the user such as their username, email, and password.

    Attributes
    ----------
    __tablename__ : str
        The name of the table in the database.
    username : so.Mapped[str]
        The username of the user. This field is not nullable.
    full_name : so.Mapped[str]
        The full name of the user. This field is nullable.
    email : so.Mapped[str]
        The email of the user. This field is not nullable and must be unique.
    hashed_password : so.Mapped[str]
        The hashed password of the user. This field is not nullable.
    is_active : so.Mapped[bool]
        A boolean indicating whether the user is active. This field is not nullable and defaults to True.
    is_superuser : so.Mapped[bool]
        A boolean indicating whether the user is a superuser. This field is not nullable and defaults to False.
    created_at : so.Mapped[datetime]
        The datetime the user was created. This field is not nullable and defaults to the current time.
    updated_at : so.Mapped[datetime]
        The datetime the user was last updated. This field is not nullable and defaults to the current time.

    Methods
    -------
    as_dict:
        Returns the user as a dictionary.
    __init__(*args, **kwargs):
        Initializes the user. If no id is provided, a unique id is generated.
    """

    __tablename__ = "users"
    username: so.Mapped[str] = so.mapped_column(nullable=False)
    full_name: so.Mapped[str] = so.mapped_column(nullable=True)
    email: so.Mapped[str] = so.mapped_column(nullable=False, unique=True)
    hashed_password: so.Mapped[str] = so.mapped_column(nullable=False)
    is_active: so.Mapped[bool] = so.mapped_column(nullable=False, default=True)
    is_superuser: so.Mapped[bool] = so.mapped_column(nullable=False, default=False)
    created_at: so.Mapped[datetime] = so.mapped_column(
        sa.DateTime(timezone=True), server_default=func.now()
    )
    updated_at: so.Mapped[datetime] = so.mapped_column(
        sa.DateTime(timezone=True), onupdate=func.now(), server_default=func.now()
    )

    def __init__(self, *args: str, **kwargs: Any) -> None:
        """Initializes the user. If no id is provided, a unique id is generated."""
        if not kwargs.get("id", None):
            kwargs["id"] = str(uuid6.uuid7())
        super().__init__(*args, **kwargs)

    @property
    def as_dict(self) -> dict:
        """Returns the user as a dictionary."""
        return {c.name: getattr(self, c.name) for c in self.__table__.columns}

This model inherits from AbstractModel, providing a base for database entities with common fields like id, created_at, and updated_at. It also includes fields specific to a user, such as username, email, and hashed_password.

Setting up a Development Environment

The README.md file provides detailed instructions for setting up a development environment using various tools. For example, to set up a Dev Container with VS Code:

  1. Install VS Code and the Dev Containers extension.

  2. Click on the "Open in Dev Containers" badge in the README.md or use the following link:

    Open in Dev Containers

  3. This will clone the repository in a container volume and create a Dev Container with VS Code.

Contributing

The README.md file also outlines the contribution process, including guidelines for using Conventional Commits, running tests, and managing dependencies.


Development Environment Setup

Related Files

  • README.md
  • TOOLS.md
  • Dockerfile
  • docker-compose.yml
  • .devcontainer/devcontainer.json

Related Pages

Related topics: Introduction, Development Workflow and Tools

Development Environment Setup

This page describes setting up the development environment for the snapenv/snap-saas-base project. The environment is designed to be consistent across different platforms using Docker, Docker Compose, and VS Code Dev Containers.

Purpose and Functionality

The development environment aims to provide a reproducible and isolated workspace for developers. It encapsulates all necessary dependencies, tools, and configurations, eliminating inconsistencies and simplifying the setup process. Key features include:

  • Automated dependency management using Poetry.
  • Pre-configured development tools (e.g., OpenCommit, gpt4docstrings, aider, cz-conventional-gitmoji).
  • Support for multiple development workflows (GitHub Codespaces, VS Code Dev Containers, PyCharm, Terminal).
  • Consistent environment across different operating systems.

Key Files

The following files are essential for setting up the development environment:

  • README.md: Provides a high-level overview of the project and instructions for setting up the development environment.
  • TOOLS.md: Lists recommended development tools and their configurations.
  • Dockerfile: Defines the Docker image for the development container.
  • docker-compose.yml: Defines the services that make up the development environment, including the devcontainer and dev services.
  • .devcontainer/devcontainer.json: Configures the VS Code Dev Container, specifying settings, extensions, and post-create commands.

Setup Instructions

Prerequisites

Before setting up the development environment, ensure you have the following prerequisites installed:

  1. Git with SSH: Configure Git to use SSH for authentication with GitHub. Refer to the README.md for detailed instructions.
    cat << EOF >> ~/.ssh/config
    Host *
      AddKeysToAgent yes
      IgnoreUnknown UseKeychain
      UseKeychain yes
    EOF
  2. Docker: Install Docker Desktop and enable Compose V2. Linux users may need to configure UID/GID mapping.
  3. VS Code or PyCharm: Install your preferred IDE. For VS Code, install the Dev Containers extension.
  4. OpenCommit: Install OpenCommit globally using npm and configure it with your OpenAI API key and preferred settings.
    npm install -g opencommit
    oco config set OCO_OPENAI_API_KEY=<your_api_key>
    oco config set OCO_MODEL=gpt-4
    oco config set OCO_EMOJI=true
  5. gpt4docstrings, aider, cz-conventional-gitmoji: Install these tools using Poetry within the development environment.

Using Dev Containers (VS Code)

The recommended way to set up the development environment is by using VS Code Dev Containers:

  1. Clone the repository to your local machine.
  2. Open the project in VS Code.
  3. If prompted, click "Reopen in Container" or use the command palette (Ctrl/Cmd + Shift + P) and select "Dev Containers: Reopen in Container".
  4. VS Code will build the Docker image (based on Dockerfile) and start the dev service defined in docker-compose.yml.
  5. The .devcontainer/devcontainer.json file configures VS Code to use the container, install extensions, and run post-create commands.

Using Docker Compose (Terminal)

Alternatively, you can set up the development environment using Docker Compose from the terminal:

  1. Clone the repository to your local machine.
  2. Open a terminal in the project directory.
  3. Start the dev service in detached mode:
    docker compose up --detach dev
  4. Open a shell prompt inside the running container:
    docker compose exec dev zsh

File Details

README.md

The README.md file serves as the entry point for developers. It provides a brief overview of the project, setup instructions, and links to other relevant documentation.

TOOLS.md

The TOOLS.md file lists recommended development tools and their configurations. It guides developers on setting up tools like OpenCommit and gpt4docstrings to improve code quality and productivity.

Dockerfile

The Dockerfile defines the Docker image for the development container. It specifies the base image, installs system-level dependencies, configures the user environment, and sets up the necessary tools.

# Dockerfile

ARG PYTHON_VERSION=3.11
ARG UID=1000
ARG GID=1000

FROM python:${PYTHON_VERSION}-slim-buster AS base

# Install dependencies
RUN apt-get update && apt-get install --no-install-recommends -y \
    git \
    zsh \
    curl \
    jq \
    && rm -rf /var/lib/apt/lists/*

# Create user
ARG USERNAME=user
RUN useradd -m -u ${UID} -g ${GID} ${USERNAME}
USER ${USERNAME}
WORKDIR /home/${USERNAME}

# Install poetry
RUN curl -sSL https://install.python-poetry.org | python3 -

ENV PATH="/home/${USERNAME}/.local/bin:${PATH}"

FROM base AS dev

# Copy project files
COPY --chown=user . /workspaces/snap-saas-base/

WORKDIR /workspaces/snap-saas-base

# Install dependencies
RUN poetry install

# Install git hooks
RUN mkdir -p .git/hooks/
COPY --chown=user .githooks/* .git/hooks/

# Set git config
RUN git config --global --add safe.directory /workspaces/snap-saas-base

# Set entrypoint
ENTRYPOINT ["/bin/zsh"]

docker-compose.yml

The docker-compose.yml file defines the services that make up the development environment. It specifies the Dockerfile to use, mounts volumes for code and configuration, sets environment variables, and configures the entrypoint for the dev service.

version: "3.9"

services:

  devcontainer:
    build:
      context: .
      target: dev
      args:
        PYTHON_VERSION: ${PYTHON_VERSION:-3.11}
        UID: ${UID:-1000}
        GID: ${GID:-1000}
    environment:
      - POETRY_PYPI_TOKEN_PYPI
    volumes:
      - ..:/workspaces
      - command-history-volume:/home/user/.history/

  dev:
    extends: devcontainer
    stdin_open: true
    tty: true
    entrypoint: []
    command:
      [
        "sh",
        "-c",
        "sudo chown user $$SSH_AUTH_SOCK && cp --update /opt/build/poetry/poetry.lock /workspaces/snap-saas-base/ && mkdir -p /workspaces/snap-saas-base/.git/hooks/ && cp --update /opt/build/git/* /workspaces/snap-saas-base/.git/hooks/ && zsh"
      ]
    environment:
      - POETRY_PYPI_TOKEN_PYPI
      - SSH_AUTH_SOCK=/run/host-services/ssh-auth.sock
    volumes:
      - ~/.gitconfig:/etc/gitconfig
      - ~/.ssh/known_hosts:/home/user/.ssh/known_hosts
      - ${SSH_AGENT_AUTH_SOCK:-/run/host-services/ssh-auth.sock}:/run/host-services/ssh-auth.sock
    profiles:
      - dev

volumes:
  command-history-volume:

.devcontainer/devcontainer.json

The .devcontainer/devcontainer.json file configures the VS Code Dev Container. It specifies the Docker Compose service to use (dev), mounts, extensions to install, and post-create commands. This file allows VS Code to seamlessly integrate with the Docker-based development environment. While the full content is not provided, it typically includes:

  • name: A descriptive name for the Dev Container.
  • dockerComposeFile: Specifies the docker-compose.yml file.
  • service: Specifies the service to connect to (e.g., dev).
  • workspaceFolder: Specifies the workspace folder inside the container.
  • extensions: An array of VS Code extensions to install.
  • postCreateCommand: A command to run after the container is created.

How it Fits into the Architecture

The development environment provides an isolated and consistent workspace for developing the Python base module. It encapsulates the operating system, Python version, project dependencies (managed by Poetry), and development tools. This isolation ensures that development work doesn't interfere with the host system and that the environment is easily reproducible for all contributors and CI/CD pipelines.

Development Environment Workflow

Here's a sequence diagram illustrating the workflow of setting up and using the development environment:

sequenceDiagram
    participant User
    participant VSCode
    participant DockerCompose
    participant DevContainer

    User->>VSCode: Open project in VSCode
    VSCode->>DockerCompose: Reopen in Container
    DockerCompose->>DevContainer: Build and start dev service
    DevContainer-->>DockerCompose: Container running
    VSCode->>DevContainer: Attach to container
    VSCode->>DevContainer: Install extensions
    DevContainer-->>VSCode: Ready for development
    User->>DevContainer: Develop code
Loading

Usage within the Environment

Once inside the Dev Container (either via VS Code or terminal), you can interact with the project using the pre-installed tools and dependencies. For example:

  • Run Poe tasks: poe
  • Add dependencies: poetry add <package>
  • Update dependencies: poetry update
  • Create commits with gitmoji: cz --name cz_gitmoji commit
  • Bump version and update changelog: cz --name cz_gitmoji bump --changelog

Data Model (SQLAlchemy ORM)

Related Files

  • src/snap_saas_base/models/__init__.py
  • src/snap_saas_base/models/base_model.py
  • src/snap_saas_base/models/user.py
  • src/snap_saas_base/models/organization.py
  • src/snap_saas_base/models/workspace.py
  • src/snap_saas_base/models/chat.py
  • tests/test_model_user.py
  • tests/test_model_organization.py
  • tests/test_model_workspace.py
  • tests/test_model_chat.py

Related Pages

Related topics: Introduction, Data Schemas (Pydantic)

Data Model (SQLAlchemy ORM)

This document describes the data model implemented using SQLAlchemy ORM in the snapenv/snap-saas-base repository. It covers the purpose, functionality, relationships, and usage of the data models defined in the src/snap_saas_base/models directory.

Purpose and Functionality

The SQLAlchemy ORM models define the structure of the database tables and their relationships, allowing the application to interact with the database using Python objects. This abstraction simplifies data access and manipulation, improving code readability and maintainability.

Core Components

The data model consists of the following key components:

  • Base Model: src/snap_saas_base/models/base_model.py
  • User: src/snap_saas_base/models/user.py
  • Organization: src/snap_saas_base/models/organization.py
  • Workspace: src/snap_saas_base/models/workspace.py
  • Chat: src/snap_saas_base/models/chat.py

Base Model (AbstractModel)

The AbstractModel class serves as the foundation for all other models. It defines common attributes such as id, created_at, and updated_at.

# src/snap_saas_base/models/base_model.py
import sqlalchemy as sa
import sqlalchemy.orm as so
import uuid6
from datetime import datetime

db_naming_convention = {
    "ix": "ix_%(column_0_label)s",
    "uq": "uq_%(table_name)s_%(column_0_N_name)s",
    "ck": "ck_%(table_name)s_%(constraint_name)s",
    "fk": "fk_%(table_name)s_%(column_0_N_name)s_%(referred_table_name)s",
    "pk": "pk_%(table_name)s",
}

class AbstractModel(so.DeclarativeBase):
    __abstract__ = True
    metadata: sa.MetaData = sa.MetaData(naming_convention=db_naming_convention) # type: ignore

    id: so.Mapped[str] = so.mapped_column(nullable=False, default=uuid6.uuid7, primary_key=True)
    created_at: so.Mapped[datetime] = so.mapped_column(
        sa.DateTime(timezone=False),
        nullable=False,
        default=datetime.utcnow,
    )
    updated_at: so.Mapped[datetime] = so.mapped_column(
        sa.DateTime(timezone=False),
        nullable=False,
        default=datetime.utcnow,
    )
  • __abstract__ = True: Marks this class as abstract, meaning it won't be directly mapped to a database table.
  • metadata: Configures the naming conventions for database constraints.
  • id: A primary key column of type string, defaulting to a UUID generated by uuid6.uuid7.
  • created_at: A timestamp column that automatically records the creation time.
  • updated_at: A timestamp column that automatically updates on each modification.

User Model

The User model represents a user in the system.

# src/snap_saas_base/models/user.py
import sqlalchemy as sa
import sqlalchemy.orm as so
import uuid6

from snap_saas_base.models.base_model import AbstractModel

# https://github.com/sqlalchemy/sqlalchemy/discussions/6165


class User(AbstractModel):
    """
    A class used to represent a User in the system.

    ...

    Attributes
    ----------
    __tablename__ : str
        a string representing the name of the table in the database
    username : so.Mapped[str]
        a string representing the username of the user, cannot be null
    provider : so.Mapped[str]
        a string representing the provider of the user, default is "local"
    email : so.Mapped[str]
        a string representing the email of the user
    cell_phone : so.Mapped[str]
        a string representing the cell phone number of the user
    full_name : so.Mapped[str]
        a string representing the full name of the user, cannot be null
    hashed_password : so.Mapped[str]
        a string representing the hashed password of the user
    is_verified : so.Mapped[bool]
        a boolean indicating whether the user is verified, default is False
    is_active : so.Mapped[bool]
        a boolean indicating whether the user is active, default is True
    is_superuser : so.Mapped[bool]
        a boolean indicating whether the user is a superuser, default is False
    phone_verified : so.Mapped[bool]
        a boolean indicating whether the user's phone is verified, default is False

    Methods
    -------
    as_dict:
        Returns the user's attributes as a dictionary
    __init__(*args, **kwargs):
        Initializes the workspace. If no id is provided, a 

Key attributes:

  • username: User's username.
  • email: User's email address.
  • full_name: User's full name.
  • hashed_password: Hashed password for authentication.
  • is_verified: Flag indicating if the user's email is verified.
  • is_active: Flag indicating if the user account is active.
  • is_superuser: Flag indicating if the user has superuser privileges.

Organization Model

The Organization model represents an organization.

# src/snap_saas_base/models/organization.py
import sqlalchemy as sa
import sqlalchemy.orm as so
import uuid6

from snap_saas_base.models.base_model import AbstractModel
from snap_saas_base.models.user import User

# https://github.com/sqlalchemy/sqlalchemy/discussions/6165


class Organization(AbstractModel):
    """A class used to represent an Organization in the database.

    This class is a subclass of the AbstractModel class and represents the structure of the 'organizations' table in the database.

    Attributes
    ----------
    __tablename__ : str
        The name of the table in the database.
    name : so.Mapped[str]
        The name of the organization. This field cannot be null.
    slug : so.Mapped[str]
        The slug of the organization. This field cannot be null and must be unique.
    bucket : so.Mapped[str]
        The bucket of the organization. This field cannot be null.
    created_by : so.Mapped[str]
        The user who created the organization. This field cannot be null and is a foreign key referencing the 'users' table.
    created_by_member: so.Mapped[User]
        And object referencing the user that created the Organization
    revoke_link : so.Mapped[bool]
        A boolean indicating whether the link to the organization has been revoked. The default value is False.
    org_member : so.WriteOnlyMapped["OrgMember"]
        A relationship to the OrgMember class, back_populates to 'org', with cascade options set to 'all, delete-orphan'.

    Methods
    -------
    as_dict:
        Returns a dictionary representation of the Organization instance.
    __init__(*args, **kwargs):
        Initializes the workspace. If no id is provided, a unique id is generated.
    """

    __tablename__ = "organizations"

    name: so.Mapped[str] = so.mapped_column(nullable=False)
    slug: so.Mapped[str] = so.mapped_column(nullable=False, unique=True)
    bucket: so.Mapped[str] = 

Key attributes:

  • name: Organization's name.
  • slug: A unique identifier for the organization.
  • bucket: Storage bucket associated with the organization.
  • created_by: Foreign key referencing the User who created the organization.

The OrgMember model represents the many-to-many relationship between users and organizations.

Workspace Model

The Workspace model represents a workspace within an organization.

# src/snap_saas_base/models/workspace.py
from datetime import datetime
from typing import Any

import sqlalchemy as sa
import sqlalchemy.orm as so
import uuid6
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.sql import func

from snap_saas_base.models.base_model import AbstractModel
from snap_saas_base.models.user import User

# https://github.com/sqlalchemy/sqlalchemy/discussions/6165


class Workspace(AbstractModel):
    """A class used to represent a Workspace.

    This class inherits from the AbstractModel class and represents a workspace in a database.
    It includes information about the workspace such as its name, slug, bucket and  organization id.

    Attributes
    ----------
    __tablename__ : str
        The name of the table in the database.
    name : so.Mapped[str]
        The name of the workspace. This field is not nullable.
    slug : so.Mapped[str]
        The slug of the workspace. This field is not nullable.
    bucket : so.Mapped[str]
        The bucket of the workspace. This field is nullable.
    org_id : so.Mapped[str]
        The organization id of the workspace. This field is not nullable and is a foreign key referencing the id of the organization.
    org : so.Mapped["Organization"]
        an object representing the organization the workspace belongs to.
    __table_args__ : tuple
        A tuple containing a unique constraint for the combination of org_id and slug.

    Methods
    -------
    as_dict:
        Returns the workspace as a dictionary.
    __init__(*args, **kwargs):
        Initializes the workspace. If no id is provided, a unique id is generated.
    """

    __tablename__ = "workspaces"
    name: so.Mapped[str] = so.mapped_column(nullable=False)
    slug: so.Mapped[str] = so.mapped_column(nullable=False)
    bucket: so.Mapped[str] = so.mapped_column(nullable=True)
    org_id: so.Mapped[str] = so.mapped_column(
        sa.ForeignKey("organizations.id", 

Key attributes:

  • name: Workspace's name.
  • slug: A unique identifier for the workspace within an organization.
  • bucket: Storage bucket associated with the workspace.
  • org_id: Foreign key referencing the Organization to which the workspace belongs.

The WorkspaceMember model represents the many-to-many relationship between users and workspaces. The WorkspaceKv model is used for storing key-value pairs specific to a workspace.

Chat Model

The Chat model represents a chat session within a workspace.

# src/snap_saas_base/models/chat.py
from datetime import datetime
from typing import Any

import sqlalchemy as sa
import sqlalchemy.orm as so
import uuid6
from sqlalchemy.dialects.postgresql import JSONB

from snap_saas_base.models.base_model import AbstractModel
from snap_saas_base.models.workspace import Workspace

# https://github.com/sqlalchemy/sqlalchemy/discussions/6165


class Chat(AbstractModel):
    # Chats Table.
    __tablename__ = "chats"

    workspace_id: so.Mapped[str] = so.mapped_column(
        sa.ForeignKey("workspaces.id", ondelete="CASCADE"), nullable=False
    )
    workspace: so.Mapped["Workspace"] = so.relationship("Workspace", uselist=False, lazy="raise")
    channel: so.Mapped[str] = so.mapped_column(nullable=False)
    channel_plugin: so.Mapped[str] = so.mapped_column(nullable=False)
    channel_id: so.Mapped[str] = so.mapped_column(nullable=False)
    channel_session_id: so.Mapped[str] = so.mapped_column(nullable=False)
    channel_contact_uid: so.Mapped[str] = so.mapped_column(nullable=False, index=True)
    subject: so.Mapped[dict[str, Any]] = so.mapped_column(JSONB, nullable=False)
    agi_id: so.Mapped[str] = so.mapped_column(nullable=False, index=True)
    status: so.Mapped[int] = so.mapped_column(nullable=False)
    state: so.Mapped[dict[str, Any]] = so.mapped_column(JSONB, nullable=False)
    # contact_id = Column(
    #     String(25), ForeignKey("contact.id", ondelete="RESTRICT", onupdate="CASCADE"), nullable=False
    # )
    contact_id: so.Mapped[str] = so.mapped_column(nullable=False)
    handsoff: so.Mapped[str] = so.mapped_column(nullable=True)
    handsoff_config: so.Mapped[str] = so.mapped_column(nullable=True, index=True)
    handsoff_cid: so.Mapped[str] = so.mapped_column(nullable=True)
    handsoff_data: so.Mapped[dict[str, Any]] = so.mapped_column(JSONB, nullable=False)
    slots: so.Mapped[dict[str, Any]] = so.mapped_column(JSONB, nullable=False)
    # session_data: so.Mapped[dict[str, Any]] = so.mapped_column(JSONB, nullable=False)
    session_metadata: so.Mapped[dict[str, Any]] = so.mapped_column(JSONB, nullable=False)
    history: so.Mapped[dict[str, Any]] = so.mapped_column(JSONB, nullable=False)
    deleted_at: so.Mapped[datetime] = so.mapped_column(nullable=True)

    # messages: so.WriteOnlyMapped["ChatMessage"] = so.relationship(
    #     back_populates="chat", cascade="all, delete-orphan"
    # )
    messages: so.Mapped[list["ChatMessage"]] = so.relationship(lazy="raise", back_populates="chat")
    # messages = so.relationship(
    #     "ChatMessage", back_populates="chat", lazy="raise", passive_deletes=True
    # )

    __table_args__ = (
        sa.UniqueConstraint("workspace_id", "id"),
        sa.UniqueConstraint("workspace_id", "handsoff_config", "handsoff_cid"),
        sa.UniqueConstraint("workspace_id", "channel", "channel_session_id"),
        sa.Index(

Key attributes:

  • workspace_id: Foreign key referencing the Workspace to which the chat belongs.
  • channel: Chat channel.
  • channel_plugin: Plugin used for the chat channel.
  • channel_session_id: Session ID for the chat channel.
  • subject: Subject of the chat.
  • status: Status of the chat.
  • state: State of the chat.

The ChatMessage model represents individual messages within a chat session and has a foreign key relationship to the Chat model.

Model Relationships

The following diagram illustrates the relationships between the models:

graph TD
    User --> OrgMember[OrgMember]
    OrgMember --> Organization[Organization]
    User --> WorkspaceMember[WorkspaceMember]
    WorkspaceMember --> Workspace[Workspace]
    Organization --> Workspace
    Workspace --> WorkspaceKv[WorkspaceKv]
    Workspace --> Chat[Chat]
    Chat --> ChatMessage[ChatMessage]
Loading

Integration with Overall Architecture

The data models are a crucial part of the application's data access layer. They provide an abstraction over the underlying database, allowing other parts of the application to interact with data in a structured and type-safe manner. Services and API endpoints use these models to perform CRUD (Create, Read, Update, Delete) operations on the database.

Setup and Usage

To use these models:

  1. Configure the database connection using SQLAlchemy.
  2. Import the model classes.
  3. Create database sessions to interact with the database.

Example usage (creating a new user):

from sqlalchemy.orm import Session
from snap_saas_base.models.user import User
from sqlalchemy import create_engine

# Replace with your actual database URL
DATABASE_URL = "sqlite:///:memory:"

engine = create_engine(DATABASE_URL)
from snap_saas_base.models.base_model import AbstractModel
AbstractModel.metadata.create_all(engine)


# Create a new user
with Session(engine) as session:
    new_user = User(username="testuser", email="test@example.com", full_name="Test User", hashed_password="password")
    session.add(new_user)
    session.commit()
    print(f"Created user with id: {new_user.id}")

Testing

The tests directory contains unit tests for the models. These tests verify that the models can be instantiated, that their attributes are correctly set, and that basic operations work as expected.

Example from tests/test_model_user.py:

from unittest import TestCase
from snap_saas_base.models.user import User

class UserModelTest(TestCase):
    """Test class for User model."""

    def setUp(self) -> None:
        """Create instance of class to test."""
        print("Setting up model testcase")
        self.model_data = User()

    def test_creation(self) -> None:
        print("Test model object creation")
        assert isinstance(self.model_data.as_dict, dict)
        assert self.model_data.id is not None

These tests help ensure the integrity and reliability of the data model.


Data Schemas (Pydantic)

Related Files

  • src/snap_saas_base/schemas/__init__.py
  • src/snap_saas_base/schemas/user.py
  • tests/test_schema_user.py

Related Pages

Related topics: Data Model (SQLAlchemy ORM)

Data Schemas (Pydantic)

This document describes the Pydantic schemas used for data validation and structuring within the snap-saas-base project. The focus is on the schemas defined in src/snap_saas_base/schemas/user.py and how they are used and tested.

Purpose and Functionality

Pydantic schemas in this project serve the following purposes:

  • Data Validation: Ensure data conforms to specified types and constraints.
  • Serialization/Deserialization: Facilitate conversion of data between Python objects and other formats (e.g., JSON).
  • Data Structure Definition: Provide clear definitions for data models.
  • API Documentation: Enable automated generation of API documentation (especially when used with frameworks like FastAPI).

Files

  • src/snap_saas_base/schemas/__init__.py: Initializes the schemas package. This file is often empty but signifies that the directory should be treated as a Python package.
  • src/snap_saas_base/schemas/user.py: Defines Pydantic schemas for representing user data.
  • tests/test_schema_user.py: Contains unit tests for the user schemas.

User Schemas (src/snap_saas_base/schemas/user.py)

The user.py file defines several Pydantic models for different user-related operations:

  • UserBaseSchema: Base schema with common user attributes.
  • UserInDBBaseSchema: Extends UserBaseSchema with database-related fields (id, password, timestamps).
  • UserCreateSchema: Extends UserBaseSchema for user creation, including a password field.
  • UserUpdateSchema: Extends UserBaseSchema for user updates.
# src/snap_saas_base/schemas/user.py
from datetime import datetime
from pydantic import BaseModel, ConfigDict, EmailStr, Field


class UserBaseSchema(BaseModel):
    username: str = Field(
        ...,
        title="User Login",
        description="Username used in login.",
        examples=["user@domain.com"],
    )
    provider: str = Field(
        ...,
        title="Auth provider",
        description="Authentication provider used in the registration of this user.",
        examples=["Auth0"],
    )
    email: EmailStr = Field(
        ...,
        title="Email",
        description="User email. Must be not used by another existent user.",
        examples=["user@domain.com"],
    )
    cell_phone: str = Field(
        ...,
        title="Mobile Number",
        description="Mobile phone number in full international format.",
        examples=["+5527999884321"],
    )
    full_name: str = Field(
        ...,
        title="Name",
        description="User full name.",
        examples=["John Doe"],
    )
    avatar: str | None = Field(
        ...,
        title="Avatar",
        description="User avatar picture URL.",
        examples=["John Doe"],
    )
    is_verified: bool = Field(
        False,
        title="Verified",
        description="User email verified.",
        examples=[False],
    )
    is_premium: bool = Field(
        False,
        title="Premium",
        description="User is premium.",
        examples=[False],
    )
    is_active: bool = Field(
        True,
        title="Active",
        description="User active.",
        examples=[True],
    )
    is_superuser: bool = Field(
        False,
        title="Superuser",
        description="User is super admin.",
        examples=[False],
    )

    model_config = ConfigDict(
        from_attributes=True,
        validate_assignment=True,
        # allow_population_by_alias=True,
    )


class UserInDBBaseSchema(UserBaseSchema):
    id: str = Field(
        ...,
        title="ID",
        description="Unique ID.",
        examples=["ckasokq6g0000yvxuigfa2agy"],
    )
    password: str | None = Field(
        ...,
        title="Password",
        description="User password.",
        examples=["$2b$12$7ytDry1IsxATsnVwZgLeQOTAkQIPlxCfuNXPM67HQaYSGApzsYXdi"],
    )
    created_at: datetime | None = Field(
        ...,
        title="Create At",
        description="Timestamp when this record was created.",
        examples=["2021-03-02T13:28:54.589000"],
    )
    updated_at: datetime | None = Field(
        ...,
        title="Updated At",
        description="Timestamp when this record was last updated.",
        examples=["2022-08-02T09:48:54.000000"],
    )


# Properties to receive on item creation
class UserCreateSchema(UserBaseSchema):
    password: str | None = Field(
        None,
        title="Password",
        description="Initial plain password.",
        examples=["clic9r1n9000gzqk480ghulbd"],
    )


# Properties to receive on item update
class UserUpdateSchema(UserBaseSchema):
    pass

Usage and Testing

The tests/test_schema_user.py file demonstrates how to create and validate UserBaseSchema instances:

# tests/test_schema_user.py
from unittest import TestCase

from snap_saas_base.schemas.user import UserBaseSchema


class UserBaseSchemaTest(TestCase):
    """Test class for User bae schema."""

    def setUp(self) -> None:
        """Create instance of class to test."""
        print("Setting up schema testcase")
        self.schema_data = {
            "username": "John Doe",
            "provider": "local",
            "email": "john.doe@domain.com",
            "cell_phone": "55279998812345",
            "avatar": "",
            "is_verified": True,
            "is_premium": False,
            "is_active": True,
            "is_superuser": False,
            "password": "SomeGoodPassword",
        }

    def test_creation(self) -> None:
        print("Test model object creation")
        new_obj = UserBaseSchema(**self.schema_data, full_name="John Doe")
        assert isinstance(new_obj, UserBaseSchema)
        assert new_obj.full_name is not None

This test creates a UserBaseSchema object using data in self.schema_data. Pydantic validates the data during object creation.

Architectural Fit

These schemas are typically used in the API layer for:

  1. Request Validation: Validating incoming data from API requests.
  2. Response Serialization: Converting internal data to a format suitable for API responses (e.g., JSON).
  3. Internal Data Structures: Providing type hints and structure for data within the application.

Schema Relationships (Mermaid Diagram)

graph TD
    A[UserBaseSchema] --> B[UserInDBBaseSchema]
    A --> C[UserCreateSchema]
    A --> D[UserUpdateSchema]
Loading

This diagram illustrates the inheritance relationships between the User schemas. UserInDBBaseSchema, UserCreateSchema, and UserUpdateSchema inherit from UserBaseSchema.


Development Workflow and Tools

Related Files

  • README.md
  • TOOLS.md
  • CHANGELOG.md
  • pyproject.toml
  • .pre-commit-config.yaml

Related Pages

Related topics: Development Environment Setup, Testing

Development Workflow and Tools

This document outlines the development workflow and tools used in the snapenv/snap-saas-base repository. It covers dependency management, code quality, commit message conventions, and version control.

Key Files

  • README.md: Provides a general overview of the project, including setup and contribution guidelines.
  • TOOLS.md: Details the tools used in the development process, along with configuration instructions.
  • CHANGELOG.md: Records notable changes for each version release.
  • pyproject.toml: Manages project dependencies and build configurations using Poetry.
  • .pre-commit-config.yaml: Configures pre-commit hooks for automated code quality checks.

Tooling

This project relies on the following tools:

  • Poetry: Manages dependencies and packaging.
  • Pre-commit: Enforces code quality standards.
  • Commitizen: Automates commit message generation and version bumping.
  • cz-conventional-gitmoji: A Commitizen adapter for Conventional Commits with Gitmojis.
  • OpenCommit: AI-powered commit message generation.
  • gpt4docstrings: AI-assisted docstring generation.
  • aider: AI-assisted coding.

File Details

README.md

  • Purpose: Serves as the primary entry point for developers, providing an introduction to the project, installation instructions, and contribution guidelines.

  • Functionality: Guides users on setting up their development environment, installing dependencies, and contributing to the project. It also links to the TOOLS.md file for detailed tool configurations.

  • Integration: Acts as a central hub, linking to other important documentation and resources.

  • Setup/Usage: Includes instructions for setting up SSH, Docker, VS Code/PyCharm, and other development tools.

  • Code Examples:

    poetry add snap-saas-base
    cat << EOF >> ~/.ssh/config
    Host *
      AddKeysToAgent yes
      IgnoreUnknown UseKeychain
      UseKeychain yes
    EOF
    npm install -g opencommit
    oco config set OCO_OPENAI_API_KEY=sk-N...
    oco config set OCO_MODEL=gpt-4
    oco config set OCO_EMOJI=true

TOOLS.md

  • Purpose: Provides detailed information on the development tools used in the project.

  • Functionality: Expands on the tools mentioned in README.md, offering in-depth configuration instructions and usage examples. It covers tools like OpenCommit and supported development environments.

  • Integration: Supplements README.md by providing detailed tool configurations and alternative setup methods.

  • Setup/Usage: Includes configuration commands for tools like OpenCommit.

  • Code Examples:

    opencommit config set OCO_MODEL=gpt-4
    opencommit config set OCO_EMOJI=true
    oco config set OCO_LANGUAGE=en
    opencommit config set OCO_OPENAI_API_KEY=<your_api_key>

pyproject.toml

  • Purpose: Manages project dependencies, build configurations, and tool settings.

  • Functionality: Defines project metadata, dependencies (including main, dev, and test groups), and tool configurations. Poetry uses this file to manage the project's virtual environment and dependencies.

  • Integration: Serves as the central configuration file for Poetry, defining project dependencies and settings.

  • Setup/Usage: Used implicitly by Poetry commands such as poetry install, poetry add, and poetry update.

  • Code Examples:

    [tool.poetry]
    name = "snap-saas-base"
    version = "0.10.2"
    description = "Snap Env (https://snapenv.com) Python base module."
    authors = ["Your Name <you@example.com>"]
    readme = "README.md"
    
    [tool.poetry.dependencies]
    python = "^3.11"
    sqlalchemy = "^2.0.29"
    uuid6 = "^0.4.0"
    pydantic = "^2.7.1"
    
    [tool.poetry.group.dev.dependencies]
    pytest = "^8.2.0"
    pytest-cov = "^5.0.0"
    ruff = "^0.4.3"
    pre-commit = "^3.7.0"
    commitizen = "^3.22.0"
    cz-conventional-gitmoji = "^0.0.5"
    opencommit = "^0.5.8"
    gpt4docstrings = "^0.2.1"
    aider-chat = "^0.2.2"
    
    [build-system]
    requires = ["poetry-core"]
    build-backend = "poetry.core.masonry.api"
    
    [tool.ruff]
    line-length = 120
    target-version = "py311"

.pre-commit-config.yaml

  • Purpose: Configures pre-commit hooks to automatically run code quality checks before commits.

  • Functionality: Defines a series of hooks that are executed before each commit, ensuring code quality, formatting, and style consistency. These hooks can include linters, formatters, and other checks.

  • Integration: Integrates with Git to run pre-commit hooks before each commit.

  • Setup/Usage: Requires installing pre-commit (poetry add --group dev pre-commit) and setting up the hooks in the local Git repository (pre-commit install).

  • Code Examples:

    repos:
      - repo: https://github.com/pre-commit/pre-commit-hooks
        rev: v4.6.0
        hooks:
          - id: trailing-whitespace
          - id: end-of-file-fixer
          - id: check-added-large-files
          - id: check-yaml
      - repo: https://github.com/astral-sh/ruff-pre-commit
        rev: v0.4.3
        hooks:
          - id: ruff
            args: [--fix, --exit-non-zero-on-fix]
          - id: ruff-format

CHANGELOG.md

  • Purpose: Documents notable changes to the project for each version release.

  • Functionality: Lists changes categorized by type (e.g., Features, Fixes, Documentation) and grouped by version. Managed by Commitizen, which generates entries based on commit history.

  • Integration: Generated and updated by Commitizen (cz bump --changelog).

  • Setup/Usage: Updated automatically when a new version is bumped using Commitizen.

  • Code Examples:

    ## v0.10.2 (2024-05-08)
    
    ### fix
    
    - ♻️ (chat.py): refactor code by consolidating the messages relationship definition to improve readability and maintainability.
    
    ## v0.10.1 (2024-03-12)
    
    ### fix
    
    - added workspace relationship to chat model

Workflow Diagram

The following diagram illustrates the development workflow:

graph TD
    A[Developer] --> B(Git Add);
    B --> C{Git Commit};
    C --> D{Pre-commit Hooks};
    D -- Pass --> E(Commitizen);
    E --> F[Git Commit];
    D -- Fail --> G[Fix Issues];
    G --> B;
    F --> H(Git Push);
Loading

This diagram illustrates the basic workflow, from making changes to pushing them to the repository. It shows the role of pre-commit hooks in ensuring code quality before a commit is created.

Commitizen Workflow

This diagram illustrates the commitizen workflow:

sequenceDiagram
    participant Developer
    participant Commitizen
    participant Git
    Developer->>Commitizen: cz commit
    Commitizen->>Developer: Prompts for commit details
    Developer->>Commitizen: Provides commit details
    Commitizen->>Git: Creates commit with conventional message
    Developer->>Commitizen: When ready to release: cz bump --changelog
    Commitizen->>Git: Reads commit history
    Commitizen->>Commitizen: Determines version bump
    Commitizen->>Commitizen: Generates changelog
    Commitizen->>Git: Creates new version tag
    Commitizen->>Developer: Updates CHANGELOG.md
    Developer->>Git: git push --tags
Loading

Conclusion

These files and tools help maintain code quality, manage dependencies, and automate the release process. By adhering to the practices outlined in this document, contributors can ensure a consistent and efficient development workflow.


Testing

Related Files

  • tests/__init__.py
  • tests/test_import.py
  • tests/test_model_chat.py
  • tests/test_model_organization.py
  • tests/test_model_user.py
  • tests/test_model_workspace.py
  • tests/test_schema_user.py
  • .github/workflows/test.yml

Related Pages

Related topics: Development Workflow and Tools

Testing in snap-saas-base

This page details the testing strategy, structure, and execution within the snap-saas-base repository. It covers the purpose and functionality of the tests, provides code examples, explains how testing fits into the overall architecture, and includes setup/usage instructions.

Purpose and Functionality

The primary goal of testing in this repository is to ensure the reliability and correctness of the core components, including models and schemas. Unit tests are used to validate individual components in isolation.

Test Structure

The tests directory contains several modules, each focusing on testing a specific part of the application.

  • tests/__init__.py: An empty file that marks the tests directory as a Python package.
  • tests/test_import.py: Contains a basic import test to verify that the snap_saas_base package can be imported successfully.
  • tests/test_model_chat.py: Tests the Chat and ChatMessage models.
  • tests/test_model_organization.py: Tests the Organization and OrgMember models.
  • tests/test_model_user.py: Tests the User model.
  • tests/test_model_workspace.py: Tests the Workspace, WorkspaceMember, and WorkspaceKv models.
  • tests/test_schema_user.py: Tests the UserBaseSchema schema.

Code Examples and Explanations

1. tests/test_import.py

This test ensures that the base package can be imported without errors.

# tests/test_import.py
"""Test Snap SAAS Base."""

import snap_saas_base


def test_import() -> None:
    """Test that the package can be imported."""
    assert isinstance(snap_saas_base.__name__, str)

This simple test confirms that the snap_saas_base module is importable and that its __name__ attribute is a string, indicating a successful import.

2. tests/test_model_user.py

This test focuses on the User model, verifying its instantiation and basic properties.

# tests/test_model_user.py
"""Test Snap SAAS Base."""

from unittest import TestCase

from snap_saas_base.models.user import User


class UserModelTest(TestCase):
    """Test class for User model."""

    def setUp(self) -> None:
        """Create instance of class to test."""
        print("Setting up model testcase")
        self.model_data = User()

    def test_creation(self) -> None:
        print("Test model object creation")
        assert isinstance(self.model_data.as_dict, dict)
        assert self.model_data.id is not None

This test case checks that a User object can be created and that it has an id and an as_dict method that returns a dictionary.

3. tests/test_schema_user.py

This test validates the UserBaseSchema schema, ensuring that it can be instantiated correctly and that its fields are properly defined.

# tests/test_schema_user.py
"""Test Snap SAAS Base."""

from unittest import TestCase

from snap_saas_base.schemas.user import UserBaseSchema


class UserBaseSchemaTest(TestCase):
    """Test class for User bae schema."""

    def setUp(self) -> None:
        """Create instance of class to test."""
        print("Setting up schema testcase")
        self.schema_data = {
            "username": "John Doe",
            "provider": "local",
            "email": "john.doe@domain.com",
            "cell_phone": "55279998812345",
            "avatar": "",
            "is_verified": True,
            "is_premium": False,
            "is_active": True,
            "is_superuser": False,
            "password": "SomeGoodPassword",
        }

    def test_creation(self) -> None:
        print("Test model object creation")
        new_obj = UserBaseSchema(**self.schema_data, full_name="John Doe")
        assert isinstance(new_obj, UserBaseSchema)
        assert new_obj.full_name is not None

This test case creates a UserBaseSchema object with sample data and asserts that the object is an instance of the schema and that the full_name attribute is correctly assigned.

Testing within the Architecture

The tests are designed to validate the core models and schemas, which are fundamental to the application's data structure and integrity. They ensure that the models behave as expected and that the schemas correctly serialize and deserialize data.

Setup and Usage Instructions

  1. Install Dependencies: Ensure that all development and testing dependencies are installed using Poetry.
    poetry install
  2. Run Tests: Execute the tests using pytest.
    pytest

Continuous Integration

The .github/workflows/test.yml file defines a GitHub Actions workflow that automatically runs the tests on each push and pull request.

# .github/workflows/test.yml
version: "3.9"

services:

  devcontainer:
    build:
      context: .
      target: dev
      args:
        PYTHON_VERSION: ${PYTHON_VERSION:-3.11}
        UID: ${UID:-1000}
        GID: ${GID:-1000}
    environment:
      - POETRY_PYPI_TOKEN_PYPI
    volumes:
      - ..:/workspaces
      - command-history-volume:/home/user/.history/

  dev:
    extends: devcontainer
    stdin_open: true
    tty: true
    entrypoint: []
    command:
      [
        "sh",
        "-c",
        "sudo chown user $$SSH_AUTH_SOCK && cp --update /opt/build/poetry/poetry.lock /workspaces/snap-saas-base/ && mkdir -p /workspaces/snap-saas-base/.git/hooks/ && cp --update /opt/build/git/* /workspaces/snap-saas-base/.git/hooks/ && zsh"
      ]
    environment:
      - POETRY_PYPI_TOKEN_PYPI
      - SSH_AUTH_SOCK=/run/host-services/ssh-auth.sock
    volumes:
      - ~/.gitconfig:/etc/gitconfig
      - ~/.ssh/known_hosts:/home/user/.ssh/known_hosts
      - ${SSH_AGENT_AUTH_SOCK:-/run/host-services/ssh-auth.sock}:/run/host-services/ssh-auth.sock
    profiles:
      - dev

volumes:
  command-history-volume:

This workflow sets up a development container, installs the necessary dependencies, and runs the tests using pytest. This ensures that the codebase is always tested and that any regressions are quickly detected.

Component Relationships

Here's a diagram illustrating the relationships between the models and schemas tested:

graph TD
    A[User Model] --> B(UserBaseSchema);
    C[Organization Model]
    D[OrgMember Model]
    E[Workspace Model]
    F[WorkspaceMember Model]
    G[WorkspaceKv Model]
    H[Chat Model]
    I[ChatMessage Model]
    style A fill:#f9f,stroke:#333,stroke-width:2px
    style B fill:#ccf,stroke:#333,stroke-width:2px
    style C fill:#f9f,stroke:#333,stroke-width:2px
    style D fill:#f9f,stroke:#333,stroke-width:2px
    style E fill:#f9f,stroke:#333,stroke-width:2px
    style F fill:#f9f,stroke:#333,stroke-width:2px
    style G fill:#f9f,stroke:#333,stroke-width:2px
    style H fill:#f9f,stroke:#333,stroke-width:2px
    style I fill:#f9f,stroke:#333,stroke-width:2px
Loading

This diagram shows the core models (User, Organization, OrgMember, Workspace, WorkspaceMember, WorkspaceKv, Chat, ChatMessage) and the UserBaseSchema and how the schema relates to the User model.


Clone this wiki locally