Skip to content

Commit 5ef0b49

Browse files
Merge pull request #127 from Promptly-Technologies-LLC/126-move-template-routers-into-a-core-folder-with-a-separate-one-for-non-template-app-logic
Refactor for better template-app separation, make clear where to put app logic
2 parents 21e6d35 + 52cea8c commit 5ef0b49

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

+178
-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: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@ If you are using VSCode or Cursor as your IDE, you will need to select the `uv`-
2222

2323
If your IDE does not automatically detect and display this option, you can manually select the interpreter by selecting "Enter interpreter path" and then navigating to the `.venv/bin/python` subfolder in your project directory.
2424

25+
### Extending the template
26+
27+
The `routers/core/` and `utils/core/` directories contain the core backend logic for the template.
28+
29+
Your custom Python backend code should go primarily in the `routers/app/` and `utils/app/` directories.
30+
31+
For the frontend, you will also need to develop custom Jinja2 templates in the `templates/` folder and add custom static assets in `static/`.
32+
2533
### Testing
2634

2735
The project uses Pytest for unit testing. It's highly recommended to write and run tests before committing code to ensure nothing is broken!
@@ -57,11 +65,7 @@ We find that mypy is an enormous time-saver, catching many errors early and grea
5765

5866
### Developing with LLMs
5967

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.
68+
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.
6569

6670
## Application architecture
6771

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

7276
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.)
7377

74-
#### Customizable folders and files
78+
#### File structure
7579

7680
- FastAPI application entry point and homepage GET route: `main.py`
77-
- FastAPI routes: `routers/`
81+
- Template FastAPI routes: `routers/core/`
7882
- Account and authentication endpoints: `account.py`
7983
- User profile management endpoints: `user.py`
8084
- Organization management endpoints: `organization.py`
8185
- Role management endpoints: `role.py`
8286
- Dashboard page: `dashboard.py`
8387
- Static pages (e.g., about, privacy policy, terms of service): `static_pages.py`
88+
- Custom FastAPI routes for your app: `routers/app/`
8489
- Jinja2 templates: `templates/`
8590
- Static assets: `static/`
8691
- Unit tests: `tests/`
8792
- Test database configuration: `docker-compose.yml`
88-
- Helper functions: `utils/`
93+
- Template helper functions: `utils/core/`
8994
- Auth helpers: `auth.py`
9095
- Database helpers: `db.py`
9196
- FastAPI dependencies: `dependencies.py`
9297
- Enums: `enums.py`
9398
- Image helpers: `images.py`
9499
- Database models: `models.py`
100+
- Custom template helper functions for your app: `utils/app/`
95101
- Exceptions: `exceptions/`
96102
- HTTP exceptions: `http_exceptions.py`
97103
- Other custom exceptions: `exceptions.py`
98-
- Environment variables: `.env.example`
104+
- Environment variables: `.env.example`, `.env`
99105
- CI/CD configuration: `.github/`
100106
- Project configuration: `pyproject.toml`
101107
- Quarto documentation:
102108
- README source: `index.qmd`
103109
- Website source: `index.qmd` + `docs/`
104-
- Configuration: `_quarto.yml`
110+
- Configuration: `_quarto.yml` + `_environment`
111+
- Rules for developing with LLMs in Cursor IDE: `.cursor/rules/`
105112

106113
Most everything else is auto-generated and should not be manually modified.
107114

108115
## Backend
109116

110117
### Code conventions
111118

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.
119+
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/`.
113120

114121
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.
115122

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

178185
### Models and relationships
179186

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:
187+
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:
181188

182189
- `Account`: Represents a user account with email and password hash
183190
- `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 +199,15 @@ Two additional models are used by SQLModel to manage many-to-many relationships;
192199
- `UserRoleLink`: Maps users to their roles (many-to-many relationship)
193200
- `RolePermissionLink`: Maps roles to their permissions (many-to-many relationship)
194201

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

197204
```{python}
198205
#| echo: false
199206
#| warning: false
200207
import sys
201208
sys.path.append("..")
202-
from utils.models import *
203-
from utils.db import engine
209+
from utils.core.models import *
210+
from utils.core.db import engine
204211
from sqlalchemy import MetaData
205212
from sqlalchemy_schemadisplay import create_schema_graph
206213
@@ -220,16 +227,17 @@ graph.write_png('static/schema.png')
220227

221228
![Database Schema](static/schema.png)
222229

230+
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.
223231

224232
### Database helpers
225233

226-
Database operations are facilitated by helper functions in `utils/db.py`. Key functions include:
234+
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:
227235

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

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

234242
```python
235243
@app.get("/users")
@@ -240,7 +248,7 @@ async def get_users(session: Session = Depends(get_session)):
240248

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

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:
251+
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:
244252

245253
```python
246254
permission = ValidPermissions.CREATE_ROLE
@@ -249,7 +257,7 @@ organization = session.exec(select(Organization).where(Organization.name == "Acm
249257
user.has_permission(permission, organization)
250258
```
251259

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.
260+
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.
253261

254262
### Cascade deletes
255263

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

0 commit comments

Comments
 (0)