Skip to content

Commit 9b5147a

Browse files
Refactor for better separation between template and app, and update docs
1 parent 21e6d35 commit 9b5147a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+170
-212
lines changed

.cursor/rules/routers.mdc

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
---
22
description: Testing FastAPI routes
3-
globs: routers/*.py
3+
globs: routers/**/*.py
44
alwaysApply: false
55
---
66
Here are the five most critical patterns to maintain consistency when adding a new router:
77

88
1. **Authentication & Dependency Injection**
9-
- Import `get_authenticated_user` from `utils.dependencies` and include `user: User = Depends(get_authenticated_user)` in the arguments of routes requiring authentication
9+
- Import `get_authenticated_user` from `utils.core.dependencies` and include `user: User = Depends(get_authenticated_user)` in the arguments of routes requiring authentication
1010
- Similarly, use the `get_optional_user` dependency for public routes with potential auth status
1111

1212
2. **Validation Patterns**
@@ -21,7 +21,7 @@ Here are the five most critical patterns to maintain consistency when adding a n
2121
- Check permissions at both route and template levels via `user_permissions`
2222

2323
4. **Database & Transaction Patterns**
24-
- Inject session via `Depends(get_session)`
24+
- Inject session via `Depends(get_session)` from `utils/core/dependencies.py`
2525
- Commit after writes and refresh objects where needed
2626
- Use `selectinload` for eager loading relationships
2727
- Follow PRG pattern with RedirectResponse after mutations

.cursor/rules/routers_tests.mdc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
description:
3-
globs: tests/routers/test_*.py
3+
globs: tests/routers/**/*.py
44
alwaysApply: false
55
---
66
# Setting test expectations regarding HTTP status codes

.cursor/rules/tests.mdc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
description: Building, running, and debugging tests
3-
globs: tests/*.py
3+
globs: tests/**.py
44
alwaysApply: false
55
---
66
This project uses `uv` for dependency management, so tests must be run with `uv run pytest` to ensure they are run in the project's virtual environment.
@@ -9,4 +9,4 @@ The project uses test-driven development, so failing tests are often what we wan
99

1010
Session-wide test setup is performed in `tests/conftest.py`. In that file, you will find fixtures that can and should be reused across the test suite, including fixtures for database setup and teardown. We have intentionally used PostgreSQL, not SQLite, in the test suite to keep the test environment as production-like as possible, and you should never change the database engine unless explicitly told to do so.
1111

12-
If you find that the test database is not available, you may need to start Docker Desktop with `systemctl --user start docker-desktop` or the database with `docker compose up`. You may `grep` the `DB_PORT=` line from `.env` if you need to know what port the database is available on. (This environment variable is used for port mapping in `docker-compose.yml` as well as in the `get_connection_url` function defined in `utils/db.py`.) If dropping tables fails during test setup due to changes to the database schema, `docker compose down -v && docker compose up` may resolve the issue.
12+
If you find that the test database is not available, you may need to start Docker Desktop with `systemctl --user start docker-desktop` or the database with `docker compose up`. You may `grep` the `DB_PORT=` line from `.env` if you need to know what port the database is available on. (This environment variable is used for port mapping in `docker-compose.yml` as well as in the `get_connection_url` function defined in `utils/core/db.py`.) If dropping tables fails during test setup due to changes to the database schema, `docker compose down -v && docker compose up` may resolve the issue.

docs/customization.qmd

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,7 @@ We find that mypy is an enormous time-saver, catching many errors early and grea
5757

5858
### Developing with LLMs
5959

60-
In line with the [llms.txt standard](https://llmstxt.org/), we have provided a Markdown-formatted prompt—designed to help LLM agents understand how to work with this template—as a text file: [llms.txt](static/llms.txt).
61-
62-
One use case for this file, if using the Cursor IDE, is to rename it to `.cursorrules` and place it in your project directory (see the [Cursor docs](https://docs.cursor.com/context/rules-for-ai) on this for more information). Alternatively, you could use it as a custom system prompt in the web interface for ChatGPT, Claude, or the LLM of your choice.
63-
64-
We have also exposed the full Markdown-formatted project documentation as a [single text file](static/documentation.txt) for easy downloading and embedding for RAG workflows.
60+
The `.cursor/rules` folder contains a set of AI rules for working on this codebase in the Cursor IDE. We have also provided an [llms.txt](static/llms.txt) system prompt file for use with other agentic LLM workflows and exposed the full Markdown-formatted project documentation as a [single text file](docs/static/documentation.txt) for easy downloading and embedding for RAG.
6561

6662
## Application architecture
6763

@@ -71,45 +67,48 @@ In this template, we use FastAPI to define the "API endpoints" of our applicatio
7167

7268
We also create POST endpoints, which accept form submissions so the user can create, update, and delete data in the database. This template follows the Post-Redirect-Get (PRG) pattern to handle POST requests. When a form is submitted, the server processes the data and then returns a "redirect" response, which sends the user to a GET endpoint to re-render the page with the updated data. (See [Architecture](https://promptlytechnologies.com/fastapi-jinja2-postgres-webapp/docs/architecture.html) for more details.)
7369

74-
#### Customizable folders and files
70+
#### File structure
7571

7672
- FastAPI application entry point and homepage GET route: `main.py`
77-
- FastAPI routes: `routers/`
73+
- Template FastAPI routes: `routers/core/`
7874
- Account and authentication endpoints: `account.py`
7975
- User profile management endpoints: `user.py`
8076
- Organization management endpoints: `organization.py`
8177
- Role management endpoints: `role.py`
8278
- Dashboard page: `dashboard.py`
8379
- Static pages (e.g., about, privacy policy, terms of service): `static_pages.py`
80+
- Custom FastAPI routes for your app: `routers/app/`
8481
- Jinja2 templates: `templates/`
8582
- Static assets: `static/`
8683
- Unit tests: `tests/`
8784
- Test database configuration: `docker-compose.yml`
88-
- Helper functions: `utils/`
85+
- Template helper functions: `utils/core/`
8986
- Auth helpers: `auth.py`
9087
- Database helpers: `db.py`
9188
- FastAPI dependencies: `dependencies.py`
9289
- Enums: `enums.py`
9390
- Image helpers: `images.py`
9491
- Database models: `models.py`
92+
- Custom template helper functions for your app: `utils/app/`
9593
- Exceptions: `exceptions/`
9694
- HTTP exceptions: `http_exceptions.py`
9795
- Other custom exceptions: `exceptions.py`
98-
- Environment variables: `.env.example`
96+
- Environment variables: `.env.example`, `.env`
9997
- CI/CD configuration: `.github/`
10098
- Project configuration: `pyproject.toml`
10199
- Quarto documentation:
102100
- README source: `index.qmd`
103101
- Website source: `index.qmd` + `docs/`
104-
- Configuration: `_quarto.yml`
102+
- Configuration: `_quarto.yml` + `_environment`
103+
- Rules for developing with LLMs in Cursor IDE: `.cursor/rules/`
105104

106105
Most everything else is auto-generated and should not be manually modified.
107106

108107
## Backend
109108

110109
### Code conventions
111110

112-
The GET route for the homepage is defined in the main entry point for the application, `main.py`. The entrypoint imports router modules from the `routers/` directory, which contain the other GET and POST routes for the application. In CRUD style, the router modules are named after the resource they manage, e.g., `account.py` for account management.
111+
The GET route for the homepage is defined in the main entry point for the application, `main.py`. The entrypoint imports router modules from the `routers/core/` directory (for core/template logic) and `routers/app/` directory (for app-specific logic). In CRUD style, the core router modules are named after the resource they manage, e.g., `account.py` for account management. You should place your own endpoints in `routers/app/`.
113112

114113
We name our GET routes using the convention `read_<name>`, where `<name>` is the name of the resource, to indicate that they are read-only endpoints that do not modify the database. In POST routes that modify the database, you can use the `get_session` dependency as an argument to get a database session.
115114

@@ -177,7 +176,7 @@ SQLModel is an Object-Relational Mapping (ORM) library that allows us to interac
177176

178177
### Models and relationships
179178

180-
Our database models are defined in `utils/models.py`. Each model is a Python class that inherits from `SQLModel` and represents a database table. The key models are:
179+
Core database models are defined in `utils/core/models.py`. Each model is a Python class that inherits from `SQLModel` and represents a database table. The key core models are:
181180

182181
- `Account`: Represents a user account with email and password hash
183182
- `User`: Represents a user profile with details like name and avatar; the email and password hash are stored in the related `Account` model
@@ -192,15 +191,15 @@ Two additional models are used by SQLModel to manage many-to-many relationships;
192191
- `UserRoleLink`: Maps users to their roles (many-to-many relationship)
193192
- `RolePermissionLink`: Maps roles to their permissions (many-to-many relationship)
194193

195-
Here's an entity-relationship diagram (ERD) of the current database schema, automatically generated from our SQLModel definitions:
194+
Here's an entity-relationship diagram (ERD) of the current core database schema, automatically generated from our SQLModel definitions:
196195

197196
```{python}
198197
#| echo: false
199198
#| warning: false
200199
import sys
201200
sys.path.append("..")
202-
from utils.models import *
203-
from utils.db import engine
201+
from utils.core.models import *
202+
from utils.core.db import engine
204203
from sqlalchemy import MetaData
205204
from sqlalchemy_schemadisplay import create_schema_graph
206205
@@ -220,16 +219,17 @@ graph.write_png('static/schema.png')
220219

221220
![Database Schema](static/schema.png)
222221

222+
To extend the database schema, define your own models in `utils/app/models.py` and import them in `utils/core/db.py` to make sure they are included in the `metadata` object in the `create_all` function.
223223

224224
### Database helpers
225225

226-
Database operations are facilitated by helper functions in `utils/db.py`. Key functions include:
226+
Database operations are facilitated by helper functions in `utils/core/db.py` (for core logic) and `utils/app/` (for app-specific helpers). Key functions in the core utils include:
227227

228228
- `set_up_db()`: Initializes the database schema and default data (which we do on every application start in `main.py`)
229229
- `get_connection_url()`: Creates a database connection URL from environment variables in `.env`
230230
- `get_session()`: Provides a database session for performing operations
231231

232-
To perform database operations in route handlers, inject the database session as a dependency:
232+
To perform database operations in route handlers, inject the database session as a dependency (from `utils/core/db.py`):
233233

234234
```python
235235
@app.get("/users")
@@ -240,7 +240,7 @@ async def get_users(session: Session = Depends(get_session)):
240240

241241
The session automatically handles transaction management, ensuring that database operations are atomic and consistent.
242242

243-
There is also a helper method on the `User` model that checks if a user has a specific permission for a given organization. Its first argument must be a `ValidPermissions` enum value (from `utils/models.py`), and its second argument must be an `Organization` object or an `int` representing an organization ID:
243+
There is also a helper method on the `User` model that checks if a user has a specific permission for a given organization. Its first argument must be a `ValidPermissions` enum value (from `utils/core/models.py`), and its second argument must be an `Organization` object or an `int` representing an organization ID:
244244

245245
```python
246246
permission = ValidPermissions.CREATE_ROLE
@@ -249,7 +249,7 @@ organization = session.exec(select(Organization).where(Organization.name == "Acm
249249
user.has_permission(permission, organization)
250250
```
251251

252-
You should create custom `ValidPermissions` enum values for your application and validate that users have the necessary permissions before allowing them to modify organization data resources.
252+
You should create custom `AppPermissions` enum values for your application in `utils/app/` (if needed) and validate that users have the necessary permissions before allowing them to modify organization data resources.
253253

254254
### Cascade deletes
255255

docs/static/documentation.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -785,8 +785,8 @@ Here's an entity-relationship diagram (ERD) of the current database schema, auto
785785
#| warning: false
786786
import sys
787787
sys.path.append("..")
788-
from utils.models import *
789-
from utils.db import engine
788+
from utils.core.models import *
789+
from utils.core.db import engine
790790
from sqlalchemy import MetaData
791791
from sqlalchemy_schemadisplay import create_schema_graph
792792

docs/static/llms.txt

Lines changed: 21 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,39 @@
11
# Project Architecture
2-
- Keep GET routes in main.py and POST routes in routers/ directory
3-
- Name GET routes using read_<name> convention
2+
- Name GET routes using `read_<name>` convention
43
- Follow Post-Redirect-Get (PRG) pattern for all form submissions
54
- Use Jinja2 HTML templates for server-side rendering and minimize client-side JavaScript
65
- Use forms for all POST routes
76
- Validate form data comprehensively on the client side as first line of defense, with server-side Pydantic validation as fallback
8-
9-
# File Structure
10-
- main.py: Application entry point and GET routes
11-
- routers/: POST route modules
12-
- templates/: Jinja2 templates
13-
- static/: Static assets
14-
- tests/: Unit tests
15-
- utils/: Helper functions and models
16-
- docker-compose.yml: Test database configuration
17-
- .env: Environment variables
18-
19-
# Python/FastAPI Guidelines
20-
- For all POST routes, define request models in a separate section at the top of the router file
21-
- Implement as_form() classmethod for all form-handling request models
22-
- Use Pydantic for request/response models with @field_validator and custom exceptions for custom form validation
237
- Use middleware defined in main.py for centralized exception handling
24-
- Add type hints to all function signatures and variables
25-
- Follow mypy type checking standards rigorously
8+
- Add type hints to all function signatures and variables for static type checking
269

27-
# Form Validation Strategy
28-
- Implement thorough client-side validation via HTML pattern attributes where possible and Javascript otherwise
29-
- Use Pydantic models with custom validators as server-side fallback
30-
- Handle validation errors through middleware exception handlers
31-
- Render validation_error.html template for failed server-side validation
10+
# File Structure
11+
- `main.py`: Application entry point and GET routes
12+
- `routers/core`: Base webapp template API routes
13+
- `routers/app`: Application API routes that extend the template
14+
- `utils/core`, `utils/app`: Helper functions, FastAPI dependencies, database models
15+
- `templates/`: Jinja2 templates
16+
- `static/`: Static assets
17+
- `tests/`: Unit tests
18+
- `docker-compose.yml`: Test database configuration
19+
- `.env`: Environment variables
20+
- `docs/`: Quarto documentation website source files
3221

3322
# Database Operations
3423
- Use SQLModel for all database interactions
35-
- Use get_session() from utils/db.py for database connections
36-
- Define database relational models explicitly in utils/models.py
37-
- Inject database session as dependency in route handlers
24+
- Use `get_session()` FastAPI dependency from `utils/core/dependencies.py` for database connections
3825

3926
# Authentication System
40-
- JWT-based token authentication with separate access/refresh tokens and bcrypt for password hashing are defined in utils/auth.py
41-
- Password and email reset tokens with expiration and password reset email flow powered by Resend are defined in utils/auth.py
42-
- HTTP-only cookies are implemented with secure flag and SameSite=strict
43-
- Inject common_authenticated_parameters as a dependency in all authenticated GET routes
44-
- Inject common_unauthenticated_parameters as a dependency in all unauthenticated GET routes
45-
- Inject get_session as a dependency in all POST routes
46-
- Handle security-related errors without leaking information
47-
48-
# Testing
49-
- Run mypy type checking before committing code
50-
- Write comprehensive unit tests using pytest
51-
- Test both success and error cases
52-
- Use test fixtures from tests/conftest.py: engine, session, client, test_user
53-
- set_up_database and clean_db fixtures are autoused by pytest to ensure clean database state
54-
55-
# Error Handling
56-
- Use middleware for centralized exception handling
57-
- Define custom exception classes for specific error cases
58-
- Return appropriate HTTP status codes and error messages
59-
- Render error templates with context data
60-
- Log errors with "uvicorn.error" logger
61-
62-
# Template Structure
63-
- Extend base.html for consistent layout
64-
- Use block tags for content sections
65-
- Include reusable components
66-
- Pass request object and context data to all templates
67-
- Keep form validation logic in corresponding templates
68-
- Use Bootstrap for styling
27+
- JWT-based token authentication with separate access/refresh tokens and bcrypt for password hashing are defined in `utils/core/auth.py`
28+
- Password and email reset tokens with expiration and password reset email flow powered by Resend are defined in `utils/core/auth.py`
29+
- HTTP-only cookies are implemented with secure flag and `SameSite=strict`
30+
- Inject `common_authenticated_parameters` as a dependency in all authenticated GET routes
31+
- Inject `common_unauthenticated_parameters` as a dependency in all unauthenticated GET routes
6932

7033
# Contributing Guidelines
7134
- Follow existing code style and patterns
7235
- Preserve existing comments and docstrings
7336
- Ensure all tests pass before submitting PR
7437
- Update .qmd documentation files for significant changes
75-
- Use uv for dependency management
38+
- Use uv for dependency management
39+
- Run `uv run mypy .` to ensure code passes a static type check

exceptions/exceptions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from utils.models import User
1+
from utils.core.models import User
22

33

44
class NeedsNewTokens(Exception):

exceptions/http_exceptions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from fastapi import HTTPException, status
2-
from utils.enums import ValidPermissions
2+
from utils.core.enums import ValidPermissions
33

44
class EmailAlreadyRegisteredError(HTTPException):
55
def __init__(self):

index.qmd

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -207,11 +207,7 @@ with open(output_path, 'w', encoding='utf-8') as f:
207207
f.write(final_content)
208208
```
209209

210-
In line with the [llms.txt standard](https://llmstxt.org/), we have provided a Markdown-formatted prompt—designed to help LLM agents understand how to work with this template—as a text file: [llms.txt](docs/static/llms.txt).
211-
212-
One use case for this file, if using the Cursor IDE, is to rename it to `.cursorrules` and place it in your project directory (see the [Cursor docs](https://docs.cursor.com/context/rules-for-ai) on this for more information). Alternatively, you could use it as a custom system prompt in the web interface for ChatGPT, Claude, or the LLM of your choice.
213-
214-
We have also exposed the full Markdown-formatted project documentation as a [single text file](docs/static/documentation.txt) for easy downloading and embedding for RAG workflows.
210+
The `.cursor/rules` folder contains a set of AI rules for working on this codebase in the Cursor IDE. We have also provided an [llms.txt](static/llms.txt) system prompt file for use with other agentic LLM workflows and exposed the full Markdown-formatted project documentation as a [single text file](docs/static/documentation.txt) for easy downloading and embedding for RAG.
215211

216212
## Contributing
217213

main.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
from fastapi.templating import Jinja2Templates
88
from fastapi.exceptions import RequestValidationError
99
from starlette.exceptions import HTTPException as StarletteHTTPException
10-
from routers import account, dashboard, organization, role, user, static_pages, invitation
11-
from utils.dependencies import (
10+
from routers.core import account, dashboard, organization, role, user, static_pages, invitation
11+
from utils.core.dependencies import (
1212
get_optional_user
1313
)
1414
from exceptions.http_exceptions import (
@@ -18,8 +18,8 @@
1818
from exceptions.exceptions import (
1919
NeedsNewTokens
2020
)
21-
from utils.db import set_up_db
22-
from utils.models import User
21+
from utils.core.db import set_up_db
22+
from utils.core.models import User
2323

2424
logger = logging.getLogger("uvicorn.error")
2525
logger.setLevel(logging.DEBUG)

0 commit comments

Comments
 (0)