First version

This commit is contained in:
2026-01-28 08:59:33 -03:00
commit 3e73a3494b
24 changed files with 2114 additions and 0 deletions
+216
View File
@@ -0,0 +1,216 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[codz]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
# lib/ # Commented out to allow inclusion of project.lib modules.
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py.cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
# Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
# poetry.lock
# poetry.toml
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
# pdm.lock
# pdm.toml
.pdm-python
.pdm-build/
# pixi
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
# pixi.lock
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
# in the .venv directory. It is recommended not to include this directory in version control.
.pixi
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# Redis
*.rdb
*.aof
*.pid
# RabbitMQ
mnesia/
rabbitmq/
rabbitmq-data/
# ActiveMQ
activemq-data/
# SageMath parsed files
*.sage.py
# Environments
.env
.envrc
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
# .idea/
# Abstra
# Abstra is an AI-powered process automation framework.
# Ignore directories containing user credentials, local state, and settings.
# Learn more at https://abstra.io/docs
.abstra/
# Visual Studio Code
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
# and can be added to the global gitignore or merged into this file. However, if you prefer,
# you could uncomment the following to ignore the entire vscode folder
# .vscode/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc
# Marimo
marimo/_static/
marimo/_lsp/
__marimo__/
# Streamlit
.streamlit/secrets.toml
+1
View File
@@ -0,0 +1 @@
3.13
+466
View File
@@ -0,0 +1,466 @@
# Uv + FastAPI + Tortoise | Template
A template FastAPI project with Tortoise ORM integration.
## Project Structure
This project is divided into 3 sub-modules, `project.api`, `project.db`, and `project.lib`.
- `project.api`: All the API-related code and the public-facing part of your code.
- `project.db`: DB models, fields, migration, setup, and more. The storage part of
your code.
- `project.lib`: All the business logic of your code. External API calls, internal
utilities, and any other internal code goes here.
### API
The `project.api` module contains all the `FastAPI` code of your project. Routers and
schemas all go in here.
#### Handler Groups
The API module is divided in sub-groups. Each of them usually represents a single
OpenAPI tag, and a group of related handlers. For example, to operate on a `Book`
resource you may have a `project.api.books` module with its respective sub-modules.
Sub-modules inside those tag modules usually include:
- `router.py`: The API router and all the API view handler code.
- `schemas.py`: All the handler-specific API schemas.
For example, a minimal `router.py` file could look like this:
```py
from fastapi import APIRouter
from project.api.example.schemas import MessageSchema
router = APIRouter(tags=["Example"])
@router.get("/hello")
async def hello_world() -> MessageSchema:
"""An example API endpoint that returns a hello world message."""
return MessageSchema(message="Hello, World!")
```
That example was taken from the example API module bundled by default with this
template. Check it out at `project/api/example/router.py`.
The code snippet above references `project.api.example.schemas`, the `schemas.py` file
mentioned above. The example file looks like this:
```py
from pydantic import BaseModel
class MessageSchema(BaseModel):
message: str
```
For more information about FastAPI return types and Pydantic models, check [FastAPI's
tutorial on response types](https://fastapi.tiangolo.com/tutorial/response-model/).
To create new handler groups, create a new directory module under `project/api/` and
name it to your group. Inside it, create an empty `__init__.py` file and a `router.py`
file with the following base code:
```py
from fastapi import APIRouter
router = APIRouter()
```
Next, go to `project/api/__init__.py` and import the router aliasing it to
`{module}_router`, e.g. `books_router`, `auth_router`, etc.
Last but not least, at the bottom of the file, add the following line:
```py
app.include_router(module_router)
```
You can check the example module's importing and inclusion lines for a practical
example.
#### API-Wide Schemas
Sometimes, you have API-wide schemas. That may be the response base schema, pagination
schemas, error schemas, or any other schemas the whole API uses. In those cases, you
can use the `project/api/schemas.py` module and include them there.
For example, given that you want a base response schema where data is always in a
`data` property in an object (JSON), your `project/api/schemas.py` would look like
this:
```py
from pydantic import BaseModel
class ResponseSchema[T](BaseModel):
data: T
```
The included `project/api/schemas.py` file includes a `NanoID` type by default. It
represents NanoID fields, as the name conveys, and it's useful when using nanoids for
your models instead of numeric IDs, UUIDs, or any other ID type. You can use it like
follows:
```py
from pydantic import BaseModel, Field
from project.api.schemas import NanoID
class ExampleSchema(BaseModel):
id: NanoID
# Or
id: NanoID = Field(..., description="This example's ID.")
```
It can also be used as path parameters, query parameters, and anywhere that takes a
pydantic model in FastAPI. For example:
```py
from project.api.schemas import NanoID
@router.get("/example/{example_id}")
def get_example(example_id: NanoID) -> None:
...
```
#### Schema Conventions
Schemas are always named `*Schema` in this template to differenciate from models.
For example, a `Book` model cannot have a `Book` schema because it'd cause name
conflicts in `router.py` files. A `BookSchema` schema allows you to differenciate the
schema from the model easily.
Additionally, model-representing schemas have a `from_orm(cls, obj: Model)` method that
simplifies the conversion of models to schemas. For example, taking our example `Book`
model in `project/db/models/example.py`, a `BookSchema` would look like follows:
```py
from datetime import datetime
from pydantic import BaseModel
from project.db import Book
from project.api.schemas import NanoID
class BookSchema(BaseModel):
id: NanoID
title: str
author: str
published_at: datetime
created_at: datetime
updated_at: datetime
@classmethod
def from_orm(cls, obj: Book) -> "BookSchema":
return cls(
id=obj.id,
title=obj.title,
author=obj.author,
published_at=obj.published_at,
created_at=obj.created_at,
updated_at=obj.updated_at,
)
```
Your handler code would then look like:
```py
from project.db import Book
from project.api.errors import NotFoundError
from project.api.schemas import ErrorSchema, NanoID
from project.api.books.schemas import BookSchema
@router.get("/books/{book_id}", responses={
200: BookSchema,
404: ErrorSchema,
422: ErrorSchema,
500: ErrorSchema,
})
async def get_book_by_id(id: NanoID) -> BookSchema:
"""Fetches a book by ID."""
book = Book.get_or_none(id=id)
if book is None:
raise NotFoundError("book")
return BookSchema.from_orm(book)
```
#### Errors
You usually want to have a standard error schema within your API for your clients to
easily parse errors. This template makes it easy to raise errors in a standard way.
The `project/api/errors.py` file contains a few pre-defined error types you can raise
right away from your API handlers to return errors. For example:
```py
from project.api.errors import NotFoundError
from project.api.schemas import ErrorSchema
@router.get("/not-found", status_code=404)
def not_found() -> ErrorSchema:
raise NotFoundError("duck")
```
When calling that endpoint, the error will look like:
```json
{
"title": "Not Found",
"message": "The requested duck was not found"
}
```
The response status code will be `404 Not Found`.
Any error subclassing `project.api.errors.BaseError` will be handled and returned
following the schema you saw above. You can follow the default error types provided to
create your own, customize messages, customize the response schema, and more.
For example, to create a `418 I'm a Teapot` raisable error type, you'd do:
```py
class ImATeapotError(ErrorInitMixin, BaseError):
title = "I'm a Teapot"
message = "Coffee? That's for losers, we drink Toy Story-themed tea here."
status_code = 418
```
Your handler will then look something like:
```py
from project.api.errors import ImATeapotError
from project.api.schemas import ErrorSchema
@router.get("/coffee", status_code=418)
def get_coffee() -> ErrorSchema:
raise ImATeapotError()
```
To customize the error schema, update the `ErrorSchema` class definition in
`project/api/schemas.py` and update the `project.api.errors.BaseError.handler` method
to reflect those changes.
#### Documentation
The documentation you're reading here has built-in support for writing it using
markdown files.
Documentation files live under the `docs/` folder, next to the `project/` folder at the
top level of the repository. Each file is named after the OpenAPI tag it documents,
like the bundled-in `Example.md` file. Any markdown files you create in there (prefixed
with `.md`) will create a tag named after the file's name (without the extension) in
your OpenAPI docs documented with the file's contents.
The only special name there is is the `_Project.md` file, which documents the API at
the root level.
### Database
This template uses [Tortoise ORM](https://tortoise.github.io/), a simple and ergonomic
ORM that's Django ORM-like but prettier.
This guide does not focus on teaching you how to use Tortoise ORM, rather on the
project structure of this template. Check their documentation for more information
about ORM usage.
The `project.db` module has a few sub-modules whose purpose can be inferred based on
the naming. The modules you'll most commonly edit are the following:
- `project.db` (`__init__.py`): This file re-exports models to make importing
elsewhere easier.
- `project.db.models`: Contains concern-specific submodules with database model
definitions. For example, `users.py` for `User` models, `books.py` for `Book` and
`Author` models (e.g. in a books-related application).
- `project.db.fields`: Custom DB fields and field aliases. It contains a
`NanoIDField` function which aliases to a nanoid `CharField`. Add any custom DB
fields here.
- `project.db.lifespan`: Contains `on_startup()` and `on_shutdown()`. They get called
from `project.api.lifespan` when the API goes up and down, applying migrations and
initializing DB connections on startup and closing connections on shutdown.
#### Model Modules
Models are divided into modules. For example, you may create a `users.py` module for
user-related models like `User` and `Session`, and a `books.py` for `Book` and `Author`
models following the books app example.
These modules contain nothing but model definitions. For example, the bundled-in
`example.py` contains:
```py
from tortoise import fields
from tortoise.models import Model
from project.db.fields import NanoIDField
class Book(Model):
id = NanoIDField(primary_key=True)
title = fields.CharField(max_length=255)
author = fields.CharField(max_length=255)
published_at = fields.DatetimeField(null=True)
created_at = fields.DatetimeField(auto_now_add=True)
updated_at = fields.DatetimeField(auto_now=True)
```
These modules are pointed at in `project.lib.settings.DATABASE`, which looks like this
by default:
```py
CONFIG = {
"connections": {"default": os.getenv("DATABASE_URL", "sqlite://db.sqlite3")},
"apps": {
"models": {
"models": [
"project.db.models.example",
"aerich.models", # Keep this one for migrations.
],
"default_connection": "default",
}
},
"use_tz": True,
"timezone": "UTC",
}
```
To create a new models module, create a new module under `project/db/models` and add it
to the `apps.models.models` list in the `project.lib.settings.DATABASE` dict.
Last but not least, re-export your models from `project/db/__init__.py`. It's a little
QoL thing that makes your life easier later on when importing models. For example, the
default `project/db/__init__.py` looks like this:
```py
from project.db.models.example import Book as Book
```
To add a new model, just add it to the imports list.
#### Migrations
This template uses [aerich](https://github.com/tortoise/aerich) to handle migrations.
Migrations are automatically-generated and stored under `project/db/migrations/models`.
Migrations are database-specific, meaning that your SQLite migrations won't work on
PostgreSQL, and neither will any other DB combination that doesn't mirror the SQL
language implementation perfectly.
The migrations folder is initialized automatically when you start the server if the
`project/db/migrations` folder is missing. If you wish to initalize the directory
manaully without running the server, run the following in your terminal:
```sh
$ uv run aerich init-migrations
```
That'll automatically create the migrations folder in the proper location and a first
migration file.
To create a new migration after you make an update, use `aerich migrate`.
The server automatically creates the `project/db/migrations/` folder if missing and
applies any pending migrations on startup.
Since migrations are database-specific, you'll need to delete the
`project/db/migrations/` folder completely when switching database management systems.
Note that migrations keep a record of the migrations applied, so deleting the folder
means you won't be able to keep making changes on a database following the now-deleted
migrations unless you kept a backup of them somewhere and move those back to the
`project/db/migrations/` folder back.
### Lib and Business Logic
All your internal business logic goes in the `project/lib/` directory.
For example, if you need to add a cache backend to your server, you could create a
`cache.py` file under `project/lib/` containing your caching code, or a directory, you
choose.
#### Project Settings
The template comes with a `settings.py` file which works just like a Django
`settings.py` file. In case you're not familiar with Django, it's just a file with
setting constants. `DATABASE_URL`, `REDIS_URL`, `THIRD_PARTY_SERVICE_API_KEY`, and any
other constants go there.
This template comes with support for `.env` files by default.
## Getting Started
To start with, delete the example models and API router (you can keep it if you want a
base to work on).
To do that:
1. Delete the `project/api/example` directory.
2. Delete the `docs/Example.md` file.
3. Open `project/api/__init__.py` and remove the inclusion of the example handler.
4. Delete the `project/db/models/example.py` file.
5. Open `project/db/setup.py` and remove `project.db.models.example` from the list of
models.
6. Open `project/db/__init__.py` and remove the re-export of the `Book` example model.
### Template Defaults Cleanup
To get started with, you may want to upgrade dependencies and rename the project to
something you like better.
To do that, do the following:
1. Replace all case-sensitive appearances of `project` under the `project/` directory
with your new import name.
2. Replace all case-sensitive appearances of `Project` under the `project/` directory
with your project's name.
3. Rename the `project/` directory to your new import name.
4. Rename your project in `pyproject.toml`.
5. Update the `[tool.aerich]` section in your `pyproject.toml` file to point to the
new directory and root module name.
The following steps mention deletion, but you can always just update those files
instead if you want to keep them as a base for starting. It also still mentions
`project/`, which by this time you'll already have renamed. Assume `project/` means
your now-renamed source code folder.
6. Empty the `docs/` directory.
7. Delete the `project/api/example/` directory.
8. Remove `project.api.example` imports and router inclusion from
`project/api/__init__.py`.
9. Delete the `project/db/models/example.py` file.
10. Remove the `project.db.models.example.*` re-exports from `project/db/__init__.py`.
11. Remove `"project.db.models.example"` from the `DATABASE` object in
`project/lib/settings.py`.
12. Delete the `project/db/migrations/` directory.
### Run the Server
To start the server, run:
```sh
$ uv run fastapi run project
```
`project` is your import name.
+2
View File
@@ -0,0 +1,2 @@
Some example endpoints for you to get started with. Check out
`project/api/example` for the code and `docs/Example.md` for this tag's docs.
+1
View File
@@ -0,0 +1 @@
A template FastAPI project with Tortoise ORM integration.
+1
View File
@@ -0,0 +1 @@
from project.api import app as app
+27
View File
@@ -0,0 +1,27 @@
from fastapi import FastAPI
from fastapi.responses import ORJSONResponse
from project.lib import settings
from project.api import docs
from project.api.errors import BaseError, NotFoundError
from project.api.lifespan import lifespan
from project.api.example.router import router as example_router
app = FastAPI(
debug=settings.DEBUG,
title="Project API",
description=docs.OPENAPI_DESCRIPTION,
version="1.0.0",
docs_url=None,
redoc_url="/docs",
lifespan=lifespan,
openapi_tags=docs.OPENAPI_TAGS,
default_response_class=ORJSONResponse,
)
app.include_router(example_router)
app.add_exception_handler(BaseError, BaseError.handler)
app.add_exception_handler(404, NotFoundError.handler)
+27
View File
@@ -0,0 +1,27 @@
import os
OPENAPI_TAGS = []
OPENAPI_DESCRIPTION = ""
for doc_file in os.listdir("./docs"):
if not doc_file.endswith(".md"):
continue
with open(f"./docs/{doc_file}", "r", encoding="utf-8") as f:
content = f.read()
name = doc_file.split(".")[0]
if name == "_Project":
OPENAPI_DESCRIPTION = content
else:
OPENAPI_TAGS.append(
{
"name": name,
"description": content,
}
)
+94
View File
@@ -0,0 +1,94 @@
from fastapi.responses import ORJSONResponse
from project.api.schemas import ErrorSchema
class BaseError(Exception):
status_code: int = 500
title: str = "Error"
message: str = "An error occurred."
def __init__(
self,
*,
title: str = ...,
message: str = ...,
status_code: int = ...,
):
if title is not ...:
self.title = title
if message is not ...:
self.message = message
if status_code is not ...:
self.status_code = status_code
@classmethod
async def handler(cls, _request, exc: "BaseError"):
"""Handles the exception and returns a standardized error response."""
return ORJSONResponse(
status_code=exc.status_code,
content=ErrorSchema(
title=exc.title if hasattr(exc, "title") else cls.title,
message=exc.message if hasattr(exc, "message") else cls.message,
).model_dump(mode="json"),
)
def __repr__(self):
return f"{self.__class__.__name__}(title={self.title!r}, message={self.message!r}, status_code={self.status_code!r})"
class ErrorInitMixin:
def __init__(self, message: str = ..., *, title: str = ...):
if title is not ...:
self.title = title
if message is not ...:
self.message = message
class InternalServerError(ErrorInitMixin, BaseError):
title: str = "Internal Server Error"
message: str = "An unexpected error occurred on the server."
status_code: int = 500
class ValidationError(ErrorInitMixin, BaseError):
title: str = "Validation Error"
message: str = "One or more validation errors occurred."
status_code: int = 422
class ConflictError(ErrorInitMixin, BaseError):
title: str = "Conflict"
message: str = "The request could not be completed due to a conflict with the current state of the resource."
status_code: int = 409
class NotFoundError(BaseError):
title: str = "Not Found"
message: str = "The requested resource was not found."
status_code: int = 404
def __init__(self, resource: str = "resource"):
self.message = f"The requested {resource} was not found."
class ForbiddenError(ErrorInitMixin, BaseError):
title: str = "Forbidden"
message: str = "You do not have permission to access this resource."
status_code: int = 403
class PaymentRequiredError(ErrorInitMixin, BaseError):
title: str = "Payment Required"
message: str = "Payment is required to access this resource."
status_code: int = 402
class UnauthorizedError(ErrorInitMixin, BaseError):
title: str = "Unauthorized"
message: str = "You must be authenticated to access this resource."
status_code: int = 401
View File
+13
View File
@@ -0,0 +1,13 @@
from fastapi import APIRouter
from project.api.example.schemas import MessageSchema
router = APIRouter(tags=["Example"])
@router.get("/hello")
async def hello_world() -> MessageSchema:
"""An example API endpoint that returns a hello world message."""
return MessageSchema(message="Hello, World!")
+5
View File
@@ -0,0 +1,5 @@
from pydantic import BaseModel
class MessageSchema(BaseModel):
message: str
+14
View File
@@ -0,0 +1,14 @@
from fastapi import FastAPI
from contextlib import asynccontextmanager
from project.db.lifespan import on_startup, on_shutdown
@asynccontextmanager
async def lifespan(app: FastAPI):
await on_startup()
yield
await on_shutdown()
+29
View File
@@ -0,0 +1,29 @@
from typing import Annotated
from pydantic import BaseModel, Field
NanoID = Annotated[
str, Field(min_length=21, max_length=21, pattern="^[A-Za-z0-9_-]{21}$")
]
"""A nanoid identifier type for pydantic models.
Example:
```
from pydantic import BaseModel
from project.api.schemas import NanoID
class BookSchema(BaseModel):
id: NanoID
title: str
```
"""
class ErrorSchema(BaseModel):
"""A standard error response schema."""
title: str
message: str
+1
View File
@@ -0,0 +1 @@
from project.db.models.example import Book as Book
+26
View File
@@ -0,0 +1,26 @@
import nanoid
from tortoise import fields
def NanoIDField(*args, **kwargs):
"""A nanoid DB field.
This is an alias for a CharField with a max length of 21 characters, using
`nanoid.generate` as the default value generator. The arguments and keyword
arguments are passed directly to the CharField constructor.
Arguments:
*args: Positional arguments for the CharField.
**kwargs: Keyword arguments for the CharField.
Returns:
tortoise.fields.CharField: A CharField configured as a nanoid primary key.
"""
return fields.CharField(
max_length=21,
default=nanoid.generate,
*args,
**kwargs,
)
+29
View File
@@ -0,0 +1,29 @@
from pathlib import Path
from aerich import Command
from tortoise import Tortoise, connections
from project.lib import settings
async def on_startup():
"""Connects to the database and creates the missing schemas on FastAPI startup."""
async with Command(
tortoise_config=settings.DATABASE, app="models", location="project/db/migrations"
) as command:
parent_dir = Path(__file__).parent
if not (parent_dir / "migrations").exists():
await command.init_migrations(safe=True)
await command.upgrade()
await Tortoise.init(config=settings.CONFIG)
async def on_shutdown():
"""Closes the database connections on FastAPI shutdown."""
await connections.close_all()
@@ -0,0 +1,44 @@
from tortoise import BaseDBAsyncClient
RUN_IN_TRANSACTION = True
async def upgrade(db: BaseDBAsyncClient) -> str:
return """
CREATE TABLE IF NOT EXISTS "book" (
"id" VARCHAR(21) NOT NULL PRIMARY KEY,
"title" VARCHAR(255) NOT NULL,
"author" VARCHAR(255) NOT NULL,
"published_at" TIMESTAMP,
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS "aerich" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
"version" VARCHAR(255) NOT NULL,
"app" VARCHAR(100) NOT NULL,
"content" JSON NOT NULL
);"""
async def downgrade(db: BaseDBAsyncClient) -> str:
return """
"""
MODELS_STATE = (
"eJztlm1v2jAQx79KlFdM6lDJoK2maVJgTGVaYWrZg1pVkYlNsEjsNHHaoorvPp9JYhIeBi"
"0arcS75H939t3vLso9mQHHxI+rTc7H5kfjyWQoIPKhoB8ZJgpDrYIg0MBXjoPMYxCLCLlC"
"akPkx0RKmMRuRENBOZMqS3wfRO5KR8o8LSWM3iXEEdwjYkQiabi5lTJlmDySOHsNx86QEh"
"8X0qQY7la6Iyah0lojFH1VnnDdwHG5nwRMe4cTMeIsd5fZgOoRRiIkCJ4rAPJL68ykWa5S"
"EFFC8iSxFjAZosQHDOanYcJcqN5giHGKq9kd+cNncwtMLmeAmDIRKwYBenR8wjwxkq9WbT"
"qrVrOYeUEiv+zL1rl9WbFq7+BCLvs0a143tVjKNFVHIIFmhyjyGrWgQgZtQTsP2A3wTNDE"
"9ZxlyDN4OyHaaGyCtNFYzRRsAFVDRIlEEW1DUUccMOYYw2Tg03hEsIPEIswvEoegAVkOtB"
"xbworT4Gr2sAHk9NP/j4zXIO13LtpXffviByQexPGdr5jY/TZYLKVOSmrlpEQ/P8T43emf"
"G/BqXPe6bQWMx8KL1I3ar39tQk5yWrnD+IOD8HzZmZxJhWa6EQG0z2hlMXIHjdzH1yJrwD"
"3mT9I5eiOdTUd+bWOTED+zscXIQ2P32liVPGxgw/HcYgDCALnjBxRhZ8HCLb7Kd9EUWEFZ"
"QQx5qivAFrJM91GbRNQdLdtUU8vaXRVpn1ezrXaY2GJZlcNVnva0YS/bVV846h7c8t6q1U"
"/rZx9O6mfSRWWSK6drpr/T7f9j9bwnUQwpbbE2zYUc9ia9fspPY5vdc+b+NgHWjo83ACi9"
"VgJUtiJAeaMgbMn/7NtVr7tiSdEhJZA/mSzwBlNXHBlyJRW3rxPrGopQdeGflcGrXNh/yl"
"xb33vN8s8IDmhKxnv9vUz/AqoDb/I="
)
View File
+16
View File
@@ -0,0 +1,16 @@
from tortoise import fields
from tortoise.models import Model
from project.db.fields import NanoIDField
class Book(Model):
id = NanoIDField(primary_key=True)
title = fields.CharField(max_length=255)
author = fields.CharField(max_length=255)
published_at = fields.DatetimeField(null=True)
created_at = fields.DatetimeField(auto_now_add=True)
updated_at = fields.DatetimeField(auto_now=True)
View File
+25
View File
@@ -0,0 +1,25 @@
import os
from dotenv import load_dotenv
load_dotenv()
DEBUG = os.getenv("DEBUG", "False").lower() in ("true", "1", "t")
DATABASE = {
"connections": {"default": os.getenv("DATABASE_URL", "sqlite://db.sqlite3")},
"apps": {
"models": {
"models": [
"project.db.models.example",
"aerich.models", # Keep this one for migrations.
],
"default_connection": "default",
}
},
"use_tz": True,
"timezone": "UTC",
}
+19
View File
@@ -0,0 +1,19 @@
[project]
name = "project"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"aerich[toml]>=0.9.2",
"fastapi[standard]>=0.128.0",
"nanoid>=2.0.0",
"orjson>=3.11.5",
"python-dotenv>=1.2.1",
"tortoise-orm[accel,asyncpg]>=0.25.3",
]
[tool.aerich]
tortoise_orm = "project.lib.settings.DATABASE"
location = "project/db/migrations"
src_folder = "./."
Generated
+1058
View File
File diff suppressed because it is too large Load Diff