Skip to content

Commit d9188fc

Browse files
committed
Add Search, rework bulk upsert
1 parent 477d108 commit d9188fc

File tree

12 files changed

+258
-133
lines changed

12 files changed

+258
-133
lines changed

README.md

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,37 +25,30 @@ TODO
2525

2626
```git clone git@github.com:pavdwest/docker_litestar_piccolo_pg.git```
2727

28-
2. Enter app directory:
28+
2. Create .env file and populate with your own secrets:
2929

30-
```cd docker_litestar_piccolo_pg/services/backend/app```
31-
32-
3. Create & activate virtual environment:
33-
34-
```uv venv .venv && source .venv/bin/activate```
30+
```cp docker_litestar_piccolo_pg/.env.example docker_litestar_piccolo_pg/.env```
3531

36-
4. Install dependencies for local development/intellisense:
32+
3. Enter app directory:
3733

38-
```uv sync```
39-
40-
5. Add .env file and populate with your secrets:
41-
42-
```cd ../../..```
34+
```cd docker_litestar_piccolo_pg/services/backend/app```
4335

44-
```cp ./.env.example ./.env```
36+
4. Create & activate virtual environment (only required for local development):
4537

38+
```uv venv .venv && source .venv/bin/activate && uv sync```
4639

47-
6. Run stack (we attach only to the backend to ignore other containers' log spam):
40+
5. Run stack (we attach only to the backend to ignore other containers' log spam):
4841

4942
```docker compose up --build --attach backend```
5043

51-
7. Everything's running:
44+
6. Everything's running:
5245

5346
[http://127.0.0.1:8000/schema](http://127.0.0.1:8000/schema)
5447

55-
8. Run migrations with Piccolo migrations:
48+
7. Run migrations with Piccolo migrations:
5649

5750
TODO
5851

59-
9. Run tests:
52+
8. Run tests:
6053

6154
`docker compose exec backend pytest`

docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
services:
22
database-host:
3-
image: postgres:16-alpine
3+
image: postgres:17-alpine
44
restart: unless-stopped
55
environment:
66
POSTGRES_INITDB_ARGS: --auth-host=scram-sha-256

services/backend/app/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ dependencies = [
1414
"sqlalchemy-utils>=0.41.2",
1515
"psycopg2-binary>=2.9.10",
1616
"pytest-asyncio>=0.25.3",
17+
"polars>=1.22.0",
1718
]
1819

1920
[dependency-groups]

services/backend/app/src/app.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from typing import Sequence
2+
import socket
23

34
from litestar import Litestar
45
from litestar.openapi import OpenAPIConfig
@@ -11,7 +12,6 @@
1112
)
1213
from litestar_granian import GranianPlugin
1314

14-
from src.config import PROJECT_NAME
1515
from src.versions import AppVersion
1616
from src.controllers.all import CONTROLLERS
1717

@@ -20,15 +20,15 @@ def create_app(lifespan: Sequence) -> Litestar:
2020
app = Litestar(
2121
lifespan=lifespan,
2222
openapi_config=OpenAPIConfig(
23-
title=f"{PROJECT_NAME} API v{AppVersion.BETA}",
24-
description="Powered by LiteStar",
23+
title="Project API",
24+
description="Powered by LiteStar: <a href='/schema/openapi.json'>schema.json</a>",
2525
version=f"{AppVersion.BETA}",
2626
render_plugins=[
2727
RapidocRenderPlugin(path="rapidoc"),
2828
ScalarRenderPlugin(path="scalar"),
29-
SwaggerRenderPlugin(path="swagger"),
3029
StoplightRenderPlugin(path="stoplight"),
3130
RedocRenderPlugin(path="redoc"),
31+
SwaggerRenderPlugin(path="swagger"),
3232
],
3333
),
3434
plugins=[GranianPlugin()],
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
STRING_SHORT_LENGTH = 256
2+
STRING_LONG_LENGTH = 4096

services/backend/app/src/controllers/crud.py

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
from litestar.exceptions import HTTPException
66
from msgspec import Struct
77
from litestar.openapi import ResponseSpec
8+
from enum import StrEnum
9+
import operator
810

911
from src.models.base import AppModel
1012
from src.controllers.base import AppController
@@ -13,13 +15,26 @@
1315
AppReadDTO,
1416
AppUpdateDTO,
1517
AppUpdateWithIdDTO,
18+
AppSearchDTO,
1619
AppBulkActionResultDTO,
1720
AppReadAllPaginationDetailsDTO,
1821
IntID,
22+
IntPositive,
1923
IntNonNegative,
2024
IntMaxLimit,
2125
)
22-
from src.responses import ConflictResponse
26+
from src.response_specs import ConflictResponse
27+
28+
29+
class JoinOperator(StrEnum):
30+
AND: str = "and"
31+
OR: str = "or"
32+
33+
def operator_mapping(self):
34+
if self == JoinOperator.AND:
35+
return operator.and_
36+
elif self == JoinOperator.OR:
37+
return operator.or_
2338

2439

2540
class CrudController(AppController): ...
@@ -31,6 +46,7 @@ def generate_crud_controller(
3146
ReadDTO: type[AppReadDTO],
3247
UpdateDTO: type[AppUpdateDTO],
3348
UpdateWithIdDTO: type[AppUpdateWithIdDTO],
49+
SearchDTO: type[AppSearchDTO],
3450
api_version_prefix: str,
3551
exclude_from_auth: bool = False,
3652
read_all_limit_default: int = 100,
@@ -62,7 +78,6 @@ def generate_crud_controller(
6278
status_codes.HTTP_409_CONFLICT: ResponseSpec(
6379
data_container=ConflictResponse,
6480
description=f"Cannot create a {Model.humanise()} because one with the same primary key already exists.",
65-
examples=ConflictResponse.examples,
6681
)
6782
},
6883
)
@@ -247,4 +262,26 @@ async def delete_all(
247262
await Model.delete_all(force=True)
248263
setattr(controller_class, "delete_all", delete_all)
249264

265+
266+
@post(
267+
"/search",
268+
description=f"Search for {pluralize(Model.humanise())}.",
269+
exclude_from_auth=exclude_from_auth,
270+
status_code=status_codes.HTTP_200_OK,
271+
)
272+
async def search(
273+
self,
274+
data: SearchDTO, # type: ignore
275+
join_operator: JoinOperator = JoinOperator.AND,
276+
offset: IntNonNegative = 0,
277+
limit: IntMaxLimit = read_all_limit_default,
278+
) -> list[ReadDTO]: # type: ignore
279+
return await Model.search(
280+
data,
281+
join_operator.operator_mapping(),
282+
offset,
283+
read_all_limit_default
284+
)
285+
setattr(controller_class, "search", search)
286+
250287
return controller_class

services/backend/app/src/dtos.py

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,20 @@
1-
from datetime import datetime
2-
from typing import Annotated
1+
from datetime import datetime, timedelta
2+
from typing import Annotated, Optional
33
from functools import lru_cache
44

55
from litestar.params import Parameter
66
from msgspec import Struct, structs
77

8-
from src.models.constants import STRING_SHORT_LENGTH, STRING_LONG_LENGTH
8+
from src.constants import STRING_SHORT_LENGTH, STRING_LONG_LENGTH
99

1010

1111
# Some commonly used constraints
12-
IntID = Annotated[int, Parameter(ge=1, le=2**63 - 1)]
13-
StringShort = Annotated[str, Parameter(min_length=1, max_length=STRING_SHORT_LENGTH)]
14-
StringLong = Annotated[str, Parameter(min_length=1, max_length=STRING_LONG_LENGTH)]
12+
IntID = Annotated[int, Parameter(ge=1)]
13+
StringShort = Annotated[str, Parameter(max_length=STRING_SHORT_LENGTH)]
14+
StringLong = Annotated[str, Parameter(max_length=STRING_LONG_LENGTH)]
1515
IntPositive = Annotated[int, Parameter(ge=1)]
1616
IntNonNegative = Annotated[int, Parameter(ge=0)]
1717
IntMaxLimit = Annotated[int, Parameter(ge=1, le=200)]
18-
DatetimeMin = Annotated[datetime, Parameter(ge=datetime(1970, 1, 1))]
1918

2019

2120
# Abstract
@@ -32,23 +31,23 @@ def dict_ordered(self):
3231

3332

3433
class AppReadDTO(AppDTO):
35-
id: IntID
36-
created_at: DatetimeMin
37-
updated_at: DatetimeMin
34+
id: IntPositive
35+
created_at: datetime
36+
updated_at: datetime
3837
is_active: bool
3938

4039

4140
class AppCreateDTO(AppDTO):
42-
is_active: bool = True
41+
is_active: Optional[bool] = True
4342

4443

4544
class AppUpdateDTO(AppDTO):
46-
is_active: bool = True
45+
is_active: Optional[bool] = None
4746

4847

4948
class AppUpdateWithIdDTO(AppDTO):
50-
id: IntID
51-
is_active: bool = True
49+
id: IntPositive
50+
is_active: Optional[bool] = None
5251

5352
@classmethod
5453
@lru_cache(maxsize=1)
@@ -67,12 +66,20 @@ class AppDeleteAllDTO(AppDTO):
6766

6867

6968
class AppDeleteResponseDTO(AppDTO):
70-
id: IntID
69+
id: IntPositive
7170

7271

7372
class AppDeleteAllResponseDTO(AppDTO):
7473
count: IntNonNegative
7574

7675

7776
class AppBulkActionResultDTO(AppDTO):
78-
ids: list[IntID]
77+
ids: list[IntPositive]
78+
79+
80+
class AppSearchDTO(AppDTO, kw_only=True):
81+
created_at_min: Optional[datetime] = None
82+
created_at_max: Optional[datetime] = None
83+
updated_at_min: Optional[datetime] = None
84+
updated_at_max: Optional[datetime] = None
85+
is_active: Optional[bool] = None

0 commit comments

Comments
 (0)