forked from litestar-org/litestar
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsqlalchemy_sync_repository.py
More file actions
220 lines (176 loc) · 6.82 KB
/
Copy pathsqlalchemy_sync_repository.py
File metadata and controls
220 lines (176 loc) · 6.82 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
from __future__ import annotations
from datetime import date
from typing import TYPE_CHECKING
from uuid import UUID
from advanced_alchemy.extensions.litestar import (
SQLAlchemyInitPlugin,
SQLAlchemySyncConfig,
base,
repository,
)
from advanced_alchemy.filters import LimitOffset
from pydantic import BaseModel as _BaseModel
from pydantic import TypeAdapter
from sqlalchemy import ForeignKey, select
from sqlalchemy.orm import Mapped, mapped_column, relationship, selectinload
from litestar import Litestar, get
from litestar.controller import Controller
from litestar.di import Provide
from litestar.handlers.http_handlers.decorators import delete, patch, post
from litestar.pagination import OffsetPagination
from litestar.params import Parameter
if TYPE_CHECKING:
from sqlalchemy.orm import Session
class BaseModel(_BaseModel):
"""Extend Pydantic's BaseModel to enable ORM mode"""
model_config = {"from_attributes": True}
# The SQLAlchemy base includes a declarative model for you to use in your models.
# The `UUIDBase` class includes a `UUID` based primary key (`id`)
class AuthorModel(base.UUIDBase):
# we can optionally provide the table name instead of auto-generating it
__tablename__ = "author" # type: ignore[assignment]
name: Mapped[str]
dob: Mapped[date | None]
books: Mapped[list[BookModel]] = relationship(back_populates="author", lazy="noload")
# The `UUIDAuditBase` class includes the same UUID` based primary key (`id`) and 2
# additional columns: `created_at` and `updated_at`. `created_at` is a timestamp of when the
# record created, and `updated_at` is the last time the record was modified.
class BookModel(base.UUIDAuditBase):
__tablename__ = "book" # type: ignore[assignment]
title: Mapped[str]
author_id: Mapped[UUID] = mapped_column(ForeignKey("author.id"))
author: Mapped[AuthorModel] = relationship(lazy="joined", innerjoin=True, viewonly=True)
# we will explicitly define the schema instead of using DTO objects for clarity.
class Author(BaseModel):
id: UUID | None
name: str
dob: date | None = None
class AuthorCreate(BaseModel):
name: str
dob: date | None = None
class AuthorUpdate(BaseModel):
name: str | None = None
dob: date | None = None
class AuthorRepository(repository.SQLAlchemySyncRepository[AuthorModel]):
"""Author repository."""
model_type = AuthorModel
async def provide_authors_repo(db_session: Session) -> AuthorRepository:
"""This provides the default Authors repository."""
return AuthorRepository(session=db_session)
# we can optionally override the default `select` used for the repository to pass in
# specific SQL options such as join details
async def provide_author_details_repo(db_session: Session) -> AuthorRepository:
"""This provides a simple example demonstrating how to override the join options
for the repository."""
return AuthorRepository(
statement=select(AuthorModel).options(selectinload(AuthorModel.books)),
session=db_session,
)
def provide_limit_offset_pagination(
current_page: int = Parameter(ge=1, query="currentPage", default=1, required=False),
page_size: int = Parameter(
query="pageSize",
ge=1,
default=10,
required=False,
),
) -> LimitOffset:
"""Add offset/limit pagination.
Return type consumed by `Repository.apply_limit_offset_pagination()`.
Parameters
----------
current_page : int
LIMIT to apply to select.
page_size : int
OFFSET to apply to select.
"""
return LimitOffset(page_size, page_size * (current_page - 1))
class AuthorController(Controller):
"""Author CRUD"""
dependencies = {"authors_repo": Provide(provide_authors_repo, sync_to_thread=False)}
@get(path="/authors")
def list_authors(
self,
authors_repo: AuthorRepository,
limit_offset: LimitOffset,
) -> OffsetPagination[Author]:
"""List authors."""
results, total = authors_repo.list_and_count(limit_offset)
type_adapter = TypeAdapter(list[Author])
return OffsetPagination[Author](
items=type_adapter.validate_python(results),
total=total,
limit=limit_offset.limit,
offset=limit_offset.offset,
)
@post(path="/authors")
def create_author(
self,
authors_repo: AuthorRepository,
data: AuthorCreate,
) -> Author:
"""Create a new author."""
obj = authors_repo.add(
AuthorModel(**data.model_dump(exclude_unset=True, exclude_none=True)),
)
authors_repo.session.commit()
return Author.model_validate(obj)
# we override the authors_repo to use the version that joins the Books in
@get(
path="/authors/{author_id:uuid}",
dependencies={"authors_repo": Provide(provide_author_details_repo, sync_to_thread=False)},
)
def get_author(
self,
authors_repo: AuthorRepository,
author_id: UUID = Parameter(
title="Author ID",
description="The author to retrieve.",
),
) -> Author:
"""Get an existing author."""
obj = authors_repo.get(author_id)
return Author.model_validate(obj)
@patch(
path="/authors/{author_id:uuid}",
dependencies={"authors_repo": Provide(provide_author_details_repo, sync_to_thread=False)},
)
def update_author(
self,
authors_repo: AuthorRepository,
data: AuthorUpdate,
author_id: UUID = Parameter(
title="Author ID",
description="The author to update.",
),
) -> Author:
"""Update an author."""
raw_obj = data.model_dump(exclude_unset=True, exclude_none=True)
raw_obj.update({"id": author_id})
obj = authors_repo.update(AuthorModel(**raw_obj))
authors_repo.session.commit()
return Author.model_validate(obj)
@delete(path="/authors/{author_id:uuid}")
def delete_author(
self,
authors_repo: AuthorRepository,
author_id: UUID = Parameter(
title="Author ID",
description="The author to delete.",
),
) -> None:
"""Delete a author from the system."""
_ = authors_repo.delete(author_id)
authors_repo.session.commit()
sqlalchemy_config = SQLAlchemySyncConfig(connection_string="sqlite:///test.sqlite") # Create 'db_session' dependency.
sqlalchemy_plugin = SQLAlchemyInitPlugin(config=sqlalchemy_config)
def on_startup() -> None:
"""Initializes the database."""
with sqlalchemy_config.get_engine().begin() as conn:
base.UUIDBase.metadata.create_all(conn)
app = Litestar(
route_handlers=[AuthorController],
on_startup=[on_startup],
plugins=[SQLAlchemyInitPlugin(config=sqlalchemy_config)],
dependencies={"limit_offset": Provide(provide_limit_offset_pagination)},
)