Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1049,7 +1049,7 @@ Removals:

0.15.15
-------
- Add ability to suppply a ``to_field=`` parameter for FK/O2O to a non-PK but still uniquely indexed remote field. (#287)
- Add ability to supply a ``to_field=`` parameter for FK/O2O to a non-PK but still uniquely indexed remote field. (#287)

0.15.14
-------
Expand Down Expand Up @@ -1600,7 +1600,7 @@ Docs/examples:

0.10.9
------
- Uses macros on SQLite driver to minimise syncronisation. ``aiosqlite>=0.7.0``
- Uses macros on SQLite driver to minimise synchronisation. ``aiosqlite>=0.7.0``
- Uses prepared statements for insert, large insert performance increase.
- Pre-generate base pypika query object per model, providing general purpose speedup.

Expand Down Expand Up @@ -1720,7 +1720,7 @@ Docs/examples:

- Fixed ``DatetimeField`` and ``DateField`` to work as expected on SQLite.
- Added ``PyLint`` plugin.
- Added test class to mange DB state for testing isolation.
- Added test class to manage DB state for testing isolation.

0.8.0
-----
Expand Down
2 changes: 1 addition & 1 deletion docs/query.rst
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ You could do it using ``.prefetch_related()``:
# This will fetch tournament with their events and teams for each event
tournament_list = await Tournament.all().prefetch_related('events__participants')
# Fetched result for m2m and backward fk relations are stored in list-like containe#r
# Fetched result for m2m and backward fk relations are stored in list-like container
for tournament in tournament_list:
print([e.name for e in tournament.events])
Expand Down
2 changes: 1 addition & 1 deletion examples/comprehensive_migrations_project/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Comprehensive Migrations Project
===================================

This example demonstrates Tortoise ORM's complete migration system through a realistic ERP schema that evolves
through 14 migrations. It covers all field types, migration operations (CreateModel, AddField, AlterField,
through 14 migrations. It covers all field types, migration operations (CreateModel, AddField, AlterField,
RemoveField, RenameField, RunPython, RunSQL, indexes, constraints), and fully reversible migrations.

Usage
Expand Down
7 changes: 3 additions & 4 deletions examples/starlette/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,20 @@
from starlette.exceptions import HTTPException
from starlette.requests import Request
from starlette.responses import JSONResponse
from starlette.routing import Route
from starlette.status import HTTP_201_CREATED, HTTP_400_BAD_REQUEST
from uvicorn.main import run

from tortoise.contrib.starlette import register_tortoise

logging.basicConfig(level=logging.DEBUG)

app = Starlette()


@app.route("/", methods=["GET"])
async def list_all(_: Request) -> JSONResponse:
users = await Users.all()
return JSONResponse({"users": [str(user) for user in users]})


@app.route("/user", methods=["POST"])
async def add_user(request: Request) -> JSONResponse:
try:
payload = await request.json()
Expand All @@ -37,6 +34,8 @@ async def add_user(request: Request) -> JSONResponse:
return JSONResponse({"user": str(user)}, status_code=HTTP_201_CREATED)


app = Starlette(routes=[Route("/", list_all), Route("/user", add_user, methods=["POST"])])

register_tortoise(
app, db_url="sqlite://:memory:", modules={"models": ["models"]}, generate_schemas=True
)
Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -236,3 +236,6 @@ exclude_dirs = [
"examples/postgres_full_text_search.py",
"examples/postgres.py",
]

[tool.codespell]
ignore-words-list = "notin,NotIn,brin,BRIN,astroid"
2 changes: 1 addition & 1 deletion tests/fields/type_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ class TypeTestModel(Model):
char_enum_field_non_null = CharEnumField(enum_type=Color, max_length=10, null=False)
char_enum_field_nullable = CharEnumField(enum_type=Color, max_length=10, null=True)

# inhereted fields
# inherited fields
inhereted_int_field_non_null = InheretedFromIntField(null=False)
inhereted_int_field_nullable = InheretedFromIntField(null=True)

Expand Down
2 changes: 1 addition & 1 deletion tests/test_model_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ async def test_update_or_create_with_defaults(tournament_model):
assert created is False
assert defaults["desc"] == updated_mdl.desc
assert mdl.desc != updated_mdl.desc
# Hint query: use defauts to update without checking conflict
# Hint query: use defaults to update without checking conflict
mdl2, created = await Tournament.update_or_create(
id=oldid, desc=desc, defaults=dict(mdl_dict, desc="new desc")
)
Expand Down
39 changes: 38 additions & 1 deletion tortoise/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@

from collections.abc import Mapping
from dataclasses import dataclass, field
from typing import Any
from typing import TYPE_CHECKING, Any

from tortoise.exceptions import ConfigurationError

if TYPE_CHECKING:
from collections.abc import Iterable
from types import ModuleType


@dataclass(frozen=True)
class DBUrlConfig:
Expand Down Expand Up @@ -202,3 +206,36 @@ def from_dict(cls, data: Mapping[str, Any]) -> TortoiseConfig:
use_tz=data.get("use_tz"),
timezone=data.get("timezone"),
)

@classmethod
def merge_args(
cls,
config: dict[str, Any] | TortoiseConfig | None = None,
config_file: str | None = None,
db_url: str | None = None,
modules: dict[str, Iterable[str | ModuleType]] | None = None,
) -> TortoiseConfig:
# Handle config_file: load it as config dict
if config_file is not None:
from tortoise import Tortoise

if config is not None:
raise ConfigurationError("Cannot specify both 'config' and 'config_file'")
config = Tortoise._get_config_from_config_file(config_file)

# Convert input to TortoiseConfig for typed access
typed_config: TortoiseConfig
if config is None:
from tortoise.backends.base.config_generator import generate_config

if db_url is None or modules is None:
raise ConfigurationError(
"Must provide either 'config', 'config_file', or both 'db_url' and 'modules'"
)
config_dict = generate_config(db_url, app_modules=modules)
typed_config = cls.from_dict(config_dict)
elif isinstance(config, dict):
typed_config = cls.from_dict(config)
else:
typed_config = config
return typed_config
21 changes: 1 addition & 20 deletions tortoise/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,26 +303,7 @@ async def init(
"""
from tortoise.apps import Apps

# Handle config_file: load it as config dict
if config_file is not None:
if config is not None:
raise ConfigurationError("Cannot specify both 'config' and 'config_file'")
config = self._get_config_from_config_file(config_file)

# Convert input to TortoiseConfig for typed access
typed_config: TortoiseConfig
if config is None:
if db_url is None or modules is None:
raise ConfigurationError(
"Must provide either 'config', 'config_file', or both 'db_url' and 'modules'"
)
config_dict = generate_config(db_url, app_modules=modules)
typed_config = TortoiseConfig.from_dict(config_dict)
elif isinstance(config, TortoiseConfig):
typed_config = config
else:
typed_config = TortoiseConfig.from_dict(config)

typed_config = TortoiseConfig.merge_args(config, config_file, db_url, modules)
config_dict = typed_config.to_dict()
connections_config = config_dict["connections"]
apps_config = config_dict["apps"]
Expand Down
11 changes: 9 additions & 2 deletions tortoise/contrib/starlette/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
from collections.abc import Iterable
from types import ModuleType

import starlette
from starlette.applications import Starlette # pylint: disable=E0401

from tortoise import Tortoise
from tortoise.connection import get_connections
from tortoise.exceptions import UnSupportedError
from tortoise.log import logger


Expand Down Expand Up @@ -80,15 +82,20 @@ def register_tortoise(
For any configuration error
"""

@app.on_event("startup")
async def init_orm() -> None: # pylint: disable=W0612
await Tortoise.init(config=config, config_file=config_file, db_url=db_url, modules=modules)
logger.info("Tortoise-ORM started, %s, %s", get_connections()._get_storage(), Tortoise.apps)
if generate_schemas:
logger.info("Tortoise-ORM generating schema")
await Tortoise.generate_schemas()

@app.on_event("shutdown")
async def close_orm() -> None: # pylint: disable=W0612
await Tortoise.close_connections()
logger.info("Tortoise-ORM shutdown")

if starlette.__version__ < "1":
if (on_event := getattr(app, "on_event", None)) is not None:
on_event("startup")(init_orm)
on_event("shutdown")(close_orm)
else:
raise UnSupportedError("Does not support Starlette 1.0 yet")
2 changes: 1 addition & 1 deletion tortoise/queryset.py
Original file line number Diff line number Diff line change
Expand Up @@ -1111,7 +1111,7 @@ def _resolve_only(self, only_lookup_expressions: tuple[str, ...]) -> None:
fetch_to_fields = defaultdict(list)
# the order is important here, we need to process the shallowest fields first
# because we want to populate _select_related_idx with actual items that need to be
# selected, not "filler" items tha just tell the executor that an empty instance has
# selected, not "filler" items than just tell the executor that an empty instance has
# to be created
for expression in sorted(only_lookup_expressions, key=lambda x: x.count("__")):
fetch_fields_lookup, __, field_name = expression.rpartition("__")
Expand Down
Loading
Loading