Repository navigation
snap saas base_wiki
Wiki Documentation for https://github.com/snapenv/snap-saas-base
Generated on: 2025-05-03 13:44:06
- Introduction
- Development Environment Setup
- Data Model (SQLAlchemy ORM)
- Data Schemas (Pydantic)
- Development Workflow and Tools
- Testing
README.md
Related topics: Development Environment Setup, Data Model (SQLAlchemy ORM)
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.
- 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.
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
-
Installation: Add the package as a dependency to your project using
poetry add snap-saas-base. - Development Environment: Choose one of the supported development environments: GitHub Codespaces, Dev Containers, PyCharm, or Terminal.
-
Configuration: Configure the development environment and tools as described in the "Contributing" section of the
README.mdfile. - Development: Use the provided base models and tools to build your SAAS application.
poetry add snap-saas-baseThis command adds snap-saas-base as a dependency to your pyproject.toml file.
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.
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:
-
Install VS Code and the Dev Containers extension.
-
Click on the "Open in Dev Containers" badge in the
README.mdor use the following link: -
This will clone the repository in a container volume and create a Dev Container with VS Code.
The README.md file also outlines the contribution process, including guidelines for using Conventional Commits, running tests, and managing dependencies.
README.mdTOOLS.mdDockerfiledocker-compose.yml.devcontainer/devcontainer.json
Related topics: Introduction, Development Workflow and Tools
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.
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.
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 thedevcontaineranddevservices. -
.devcontainer/devcontainer.json: Configures the VS Code Dev Container, specifying settings, extensions, and post-create commands.
Before setting up the development environment, ensure you have the following prerequisites installed:
-
Git with SSH: Configure Git to use SSH for authentication with GitHub. Refer to the
README.mdfor detailed instructions.cat << EOF >> ~/.ssh/config Host * AddKeysToAgent yes IgnoreUnknown UseKeychain UseKeychain yes EOF
- Docker: Install Docker Desktop and enable Compose V2. Linux users may need to configure UID/GID mapping.
- VS Code or PyCharm: Install your preferred IDE. For VS Code, install the Dev Containers extension.
-
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
- gpt4docstrings, aider, cz-conventional-gitmoji: Install these tools using Poetry within the development environment.
The recommended way to set up the development environment is by using VS Code Dev Containers:
- Clone the repository to your local machine.
- Open the project in VS Code.
- If prompted, click "Reopen in Container" or use the command palette (
Ctrl/Cmd + Shift + P) and select "Dev Containers: Reopen in Container". - VS Code will build the Docker image (based on
Dockerfile) and start thedevservice defined indocker-compose.yml. - The
.devcontainer/devcontainer.jsonfile configures VS Code to use the container, install extensions, and run post-create commands.
Alternatively, you can set up the development environment using Docker Compose from the terminal:
- Clone the repository to your local machine.
- Open a terminal in the project directory.
- Start the
devservice in detached mode:docker compose up --detach dev
- Open a shell prompt inside the running container:
docker compose exec dev zsh
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.
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.
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"]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: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 thedocker-compose.ymlfile. -
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.
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.
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
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
src/snap_saas_base/models/__init__.pysrc/snap_saas_base/models/base_model.pysrc/snap_saas_base/models/user.pysrc/snap_saas_base/models/organization.pysrc/snap_saas_base/models/workspace.pysrc/snap_saas_base/models/chat.pytests/test_model_user.pytests/test_model_organization.pytests/test_model_workspace.pytests/test_model_chat.py
Related topics: Introduction, Data Schemas (Pydantic)
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.
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.
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
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 byuuid6.uuid7. -
created_at: A timestamp column that automatically records the creation time. -
updated_at: A timestamp column that automatically updates on each modification.
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.
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 theUserwho created the organization.
The OrgMember model represents the many-to-many relationship between users and organizations.
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 theOrganizationto 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.
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 theWorkspaceto 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.
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]
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.
To use these models:
- Configure the database connection using SQLAlchemy.
- Import the model classes.
- 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}")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 NoneThese tests help ensure the integrity and reliability of the data model.
src/snap_saas_base/schemas/__init__.pysrc/snap_saas_base/schemas/user.pytests/test_schema_user.py
Related topics: Data Model (SQLAlchemy ORM)
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.
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).
-
src/snap_saas_base/schemas/__init__.py: Initializes theschemaspackage. 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.
The user.py file defines several Pydantic models for different user-related operations:
-
UserBaseSchema: Base schema with common user attributes. -
UserInDBBaseSchema: ExtendsUserBaseSchemawith database-related fields (id, password, timestamps). -
UserCreateSchema: ExtendsUserBaseSchemafor user creation, including a password field. -
UserUpdateSchema: ExtendsUserBaseSchemafor 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):
passThe 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 NoneThis test creates a UserBaseSchema object using data in self.schema_data. Pydantic validates the data during object creation.
These schemas are typically used in the API layer for:
- Request Validation: Validating incoming data from API requests.
- Response Serialization: Converting internal data to a format suitable for API responses (e.g., JSON).
- Internal Data Structures: Providing type hints and structure for data within the application.
graph TD
A[UserBaseSchema] --> B[UserInDBBaseSchema]
A --> C[UserCreateSchema]
A --> D[UserUpdateSchema]
This diagram illustrates the inheritance relationships between the User schemas. UserInDBBaseSchema, UserCreateSchema, and UserUpdateSchema inherit from UserBaseSchema.
README.mdTOOLS.mdCHANGELOG.mdpyproject.toml.pre-commit-config.yaml
Related topics: Development Environment Setup, Testing
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.
-
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.
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.
-
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.mdfile 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
-
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.mdby 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>
-
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, andpoetry 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"
-
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
-
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
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);
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.
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
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.
tests/__init__.pytests/test_import.pytests/test_model_chat.pytests/test_model_organization.pytests/test_model_user.pytests/test_model_workspace.pytests/test_schema_user.py.github/workflows/test.yml
Related topics: Development Workflow and Tools
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.
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.
The tests directory contains several modules, each focusing on testing a specific part of the application.
-
tests/__init__.py: An empty file that marks thetestsdirectory as a Python package. -
tests/test_import.py: Contains a basic import test to verify that thesnap_saas_basepackage can be imported successfully. -
tests/test_model_chat.py: Tests theChatandChatMessagemodels. -
tests/test_model_organization.py: Tests theOrganizationandOrgMembermodels. -
tests/test_model_user.py: Tests theUsermodel. -
tests/test_model_workspace.py: Tests theWorkspace,WorkspaceMember, andWorkspaceKvmodels. -
tests/test_schema_user.py: Tests theUserBaseSchemaschema.
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.
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 NoneThis 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.
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 NoneThis 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.
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.
-
Install Dependencies: Ensure that all development and testing dependencies are installed using Poetry.
poetry install
-
Run Tests: Execute the tests using
pytest.pytest
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.
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
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.