piccolo 1.18.0__py3-none-any.whl → 1.19.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- piccolo/__init__.py +1 -1
- piccolo/apps/asgi/commands/new.py +2 -2
- piccolo/apps/asgi/commands/templates/app/_blacksheep_app.py.jinja +9 -13
- piccolo/apps/asgi/commands/templates/app/_esmerald_app.py.jinja +14 -24
- piccolo/apps/asgi/commands/templates/app/_lilya_app.py.jinja +6 -7
- piccolo/apps/asgi/commands/templates/app/_litestar_app.py.jinja +32 -11
- piccolo/columns/m2m.py +6 -2
- piccolo/query/methods/objects.py +25 -0
- piccolo/query/methods/select.py +34 -0
- piccolo/query/mixins.py +88 -0
- {piccolo-1.18.0.dist-info → piccolo-1.19.1.dist-info}/METADATA +1 -1
- {piccolo-1.18.0.dist-info → piccolo-1.19.1.dist-info}/RECORD +18 -18
- tests/columns/m2m/base.py +16 -0
- tests/table/test_select.py +34 -0
- {piccolo-1.18.0.dist-info → piccolo-1.19.1.dist-info}/LICENSE +0 -0
- {piccolo-1.18.0.dist-info → piccolo-1.19.1.dist-info}/WHEEL +0 -0
- {piccolo-1.18.0.dist-info → piccolo-1.19.1.dist-info}/entry_points.txt +0 -0
- {piccolo-1.18.0.dist-info → piccolo-1.19.1.dist-info}/top_level.txt +0 -0
piccolo/__init__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__VERSION__ = "1.
|
1
|
+
__VERSION__ = "1.19.1"
|
@@ -12,10 +12,10 @@ TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "templates/app/")
|
|
12
12
|
SERVERS = ["uvicorn", "Hypercorn", "granian"]
|
13
13
|
ROUTER_DEPENDENCIES = {
|
14
14
|
"starlette": ["starlette"],
|
15
|
-
"fastapi": ["fastapi
|
15
|
+
"fastapi": ["fastapi"],
|
16
16
|
"blacksheep": ["blacksheep"],
|
17
17
|
"litestar": ["litestar"],
|
18
|
-
"esmerald": ["esmerald
|
18
|
+
"esmerald": ["esmerald"],
|
19
19
|
"lilya": ["lilya"],
|
20
20
|
}
|
21
21
|
ROUTERS = list(ROUTER_DEPENDENCIES.keys())
|
@@ -1,20 +1,18 @@
|
|
1
1
|
import typing as t
|
2
2
|
|
3
|
-
from piccolo_admin.endpoints import create_admin
|
4
|
-
from piccolo_api.crud.serializers import create_pydantic_model
|
5
|
-
from piccolo.engine import engine_finder
|
6
|
-
|
7
3
|
from blacksheep.server import Application
|
8
4
|
from blacksheep.server.bindings import FromJSON
|
9
|
-
from blacksheep.server.responses import json
|
10
5
|
from blacksheep.server.openapi.v3 import OpenAPIHandler
|
6
|
+
from blacksheep.server.responses import json
|
11
7
|
from openapidocs.v3 import Info
|
8
|
+
from piccolo.engine import engine_finder
|
9
|
+
from piccolo_admin.endpoints import create_admin
|
10
|
+
from piccolo_api.crud.serializers import create_pydantic_model
|
12
11
|
|
13
12
|
from home.endpoints import home
|
14
13
|
from home.piccolo_app import APP_CONFIG
|
15
14
|
from home.tables import Task
|
16
15
|
|
17
|
-
|
18
16
|
app = Application()
|
19
17
|
|
20
18
|
app.mount(
|
@@ -47,7 +45,7 @@ TaskModelPartial: t.Any = create_pydantic_model(
|
|
47
45
|
|
48
46
|
@app.router.get("/tasks/")
|
49
47
|
async def tasks() -> t.List[TaskModelOut]:
|
50
|
-
return await Task.select().order_by(Task.
|
48
|
+
return await Task.select().order_by(Task._meta.primary_key, ascending=False)
|
51
49
|
|
52
50
|
|
53
51
|
@app.router.post("/tasks/")
|
@@ -58,10 +56,8 @@ async def create_task(task_model: FromJSON[TaskModelIn]) -> TaskModelOut:
|
|
58
56
|
|
59
57
|
|
60
58
|
@app.router.put("/tasks/{task_id}/")
|
61
|
-
async def put_task(
|
62
|
-
|
63
|
-
) -> TaskModelOut:
|
64
|
-
task = await Task.objects().get(Task.id == task_id)
|
59
|
+
async def put_task(task_id: int, task_model: FromJSON[TaskModelIn]) -> TaskModelOut:
|
60
|
+
task = await Task.objects().get(Task._meta.primary_key == task_id)
|
65
61
|
if not task:
|
66
62
|
return json({}, status=404)
|
67
63
|
|
@@ -77,7 +73,7 @@ async def put_task(
|
|
77
73
|
async def patch_task(
|
78
74
|
task_id: int, task_model: FromJSON[TaskModelPartial]
|
79
75
|
) -> TaskModelOut:
|
80
|
-
task = await Task.objects().get(Task.
|
76
|
+
task = await Task.objects().get(Task._meta.primary_key == task_id)
|
81
77
|
if not task:
|
82
78
|
return json({}, status=404)
|
83
79
|
|
@@ -92,7 +88,7 @@ async def patch_task(
|
|
92
88
|
|
93
89
|
@app.router.delete("/tasks/{task_id}/")
|
94
90
|
async def delete_task(task_id: int):
|
95
|
-
task = await Task.objects().get(Task.
|
91
|
+
task = await Task.objects().get(Task._meta.primary_key == task_id)
|
96
92
|
if not task:
|
97
93
|
return json({}, status=404)
|
98
94
|
|
@@ -1,23 +1,21 @@
|
|
1
1
|
import typing as t
|
2
|
-
|
3
2
|
from pathlib import Path
|
4
3
|
|
5
|
-
from piccolo.utils.pydantic import create_pydantic_model
|
6
|
-
from piccolo.engine import engine_finder
|
7
|
-
from piccolo_admin.endpoints import create_admin
|
8
|
-
|
9
4
|
from esmerald import (
|
5
|
+
APIView,
|
10
6
|
Esmerald,
|
11
|
-
Include,
|
12
7
|
Gateway,
|
8
|
+
Include,
|
13
9
|
JSONResponse,
|
14
|
-
|
10
|
+
delete,
|
15
11
|
get,
|
16
12
|
post,
|
17
13
|
put,
|
18
|
-
delete
|
19
14
|
)
|
20
15
|
from esmerald.config import StaticFilesConfig
|
16
|
+
from piccolo.engine import engine_finder
|
17
|
+
from piccolo.utils.pydantic import create_pydantic_model
|
18
|
+
from piccolo_admin.endpoints import create_admin
|
21
19
|
|
22
20
|
from home.endpoints import home
|
23
21
|
from home.piccolo_app import APP_CONFIG
|
@@ -40,14 +38,9 @@ async def close_database_connection_pool():
|
|
40
38
|
print("Unable to connect to the database")
|
41
39
|
|
42
40
|
|
43
|
-
TaskModelIn: t.Any = create_pydantic_model(
|
44
|
-
table=Task,
|
45
|
-
model_name='TaskModelIn'
|
46
|
-
)
|
41
|
+
TaskModelIn: t.Any = create_pydantic_model(table=Task, model_name="TaskModelIn")
|
47
42
|
TaskModelOut: t.Any = create_pydantic_model(
|
48
|
-
table=Task,
|
49
|
-
include_default_columns=True,
|
50
|
-
model_name='TaskModelOut'
|
43
|
+
table=Task, include_default_columns=True, model_name="TaskModelOut"
|
51
44
|
)
|
52
45
|
|
53
46
|
|
@@ -57,19 +50,17 @@ class TaskAPIView(APIView):
|
|
57
50
|
|
58
51
|
@get("/")
|
59
52
|
async def tasks(self) -> t.List[TaskModelOut]:
|
60
|
-
return await Task.select().order_by(Task.
|
61
|
-
|
53
|
+
return await Task.select().order_by(Task._meta.primary_key, ascending=False)
|
62
54
|
|
63
|
-
@post(
|
55
|
+
@post("/")
|
64
56
|
async def create_task(self, payload: TaskModelIn) -> TaskModelOut:
|
65
57
|
task = Task(**payload.dict())
|
66
58
|
await task.save()
|
67
59
|
return task.to_dict()
|
68
60
|
|
69
|
-
|
70
|
-
@put('/{task_id}')
|
61
|
+
@put("/{task_id}")
|
71
62
|
async def update_task(self, payload: TaskModelIn, task_id: int) -> TaskModelOut:
|
72
|
-
task = await Task.objects().get(Task.
|
63
|
+
task = await Task.objects().get(Task._meta.primary_key == task_id)
|
73
64
|
if not task:
|
74
65
|
return JSONResponse({}, status_code=404)
|
75
66
|
|
@@ -80,10 +71,9 @@ class TaskAPIView(APIView):
|
|
80
71
|
|
81
72
|
return task.to_dict()
|
82
73
|
|
83
|
-
|
84
|
-
@delete('/{task_id}')
|
74
|
+
@delete("/{task_id}")
|
85
75
|
async def delete_task(self, task_id: int) -> None:
|
86
|
-
task = await Task.objects().get(Task.
|
76
|
+
task = await Task.objects().get(Task._meta.primary_key == task_id)
|
87
77
|
if not task:
|
88
78
|
return JSONResponse({}, status_code=404)
|
89
79
|
|
@@ -1,15 +1,14 @@
|
|
1
|
-
from piccolo_admin.endpoints import create_admin
|
2
|
-
from piccolo_api.crud.endpoints import PiccoloCRUD
|
3
|
-
from piccolo.engine import engine_finder
|
4
|
-
from lilya.routing import Path, Include
|
5
1
|
from lilya.apps import Lilya
|
2
|
+
from lilya.routing import Include, Path
|
6
3
|
from lilya.staticfiles import StaticFiles
|
4
|
+
from piccolo.engine import engine_finder
|
5
|
+
from piccolo_admin.endpoints import create_admin
|
6
|
+
from piccolo_api.crud.endpoints import PiccoloCRUD
|
7
7
|
|
8
8
|
from home.endpoints import HomeController
|
9
9
|
from home.piccolo_app import APP_CONFIG
|
10
10
|
from home.tables import Task
|
11
11
|
|
12
|
-
|
13
12
|
app = Lilya(
|
14
13
|
routes=[
|
15
14
|
Path("/", HomeController),
|
@@ -19,10 +18,10 @@ app = Lilya(
|
|
19
18
|
tables=APP_CONFIG.table_classes,
|
20
19
|
# Required when running under HTTPS:
|
21
20
|
# allowed_hosts=['my_site.com']
|
22
|
-
)
|
21
|
+
),
|
23
22
|
),
|
24
23
|
Include("/static/", StaticFiles(directory="static")),
|
25
|
-
Include("/tasks/", PiccoloCRUD(table=Task))
|
24
|
+
Include("/tasks/", PiccoloCRUD(table=Task)),
|
26
25
|
],
|
27
26
|
)
|
28
27
|
|
@@ -1,8 +1,5 @@
|
|
1
1
|
import typing as t
|
2
2
|
|
3
|
-
from home.endpoints import home
|
4
|
-
from home.piccolo_app import APP_CONFIG
|
5
|
-
from home.tables import Task
|
6
3
|
from litestar import Litestar, asgi, delete, get, patch, post
|
7
4
|
from litestar.contrib.jinja import JinjaTemplateEngine
|
8
5
|
from litestar.exceptions import NotFoundException
|
@@ -10,8 +7,19 @@ from litestar.static_files import StaticFilesConfig
|
|
10
7
|
from litestar.template import TemplateConfig
|
11
8
|
from litestar.types import Receive, Scope, Send
|
12
9
|
from piccolo.engine import engine_finder
|
13
|
-
from piccolo.utils.pydantic import create_pydantic_model
|
14
10
|
from piccolo_admin.endpoints import create_admin
|
11
|
+
from pydantic import BaseModel
|
12
|
+
|
13
|
+
from home.endpoints import home
|
14
|
+
from home.piccolo_app import APP_CONFIG
|
15
|
+
from home.tables import Task
|
16
|
+
|
17
|
+
"""
|
18
|
+
NOTE: `create_pydantic_model` is not compatible with Litestar
|
19
|
+
version higher than 2.11.0. If you are using Litestar<=2.11.0,
|
20
|
+
you can use `create_pydantic_model` as in other asgi templates
|
21
|
+
|
22
|
+
from piccolo.utils.pydantic import create_pydantic_model
|
15
23
|
|
16
24
|
TaskModelIn: t.Any = create_pydantic_model(
|
17
25
|
table=Task,
|
@@ -22,6 +30,18 @@ TaskModelOut: t.Any = create_pydantic_model(
|
|
22
30
|
include_default_columns=True,
|
23
31
|
model_name="TaskModelOut",
|
24
32
|
)
|
33
|
+
"""
|
34
|
+
|
35
|
+
|
36
|
+
class TaskModelIn(BaseModel):
|
37
|
+
name: str
|
38
|
+
completed: bool = False
|
39
|
+
|
40
|
+
|
41
|
+
class TaskModelOut(BaseModel):
|
42
|
+
id: int
|
43
|
+
name: str
|
44
|
+
completed: bool = False
|
25
45
|
|
26
46
|
|
27
47
|
# mounting Piccolo Admin
|
@@ -32,31 +52,32 @@ async def admin(scope: "Scope", receive: "Receive", send: "Send") -> None:
|
|
32
52
|
|
33
53
|
@get("/tasks", tags=["Task"])
|
34
54
|
async def tasks() -> t.List[TaskModelOut]:
|
35
|
-
|
55
|
+
tasks = await Task.select().order_by(Task._meta.primary_key, ascending=False)
|
56
|
+
return [TaskModelOut(**task) for task in tasks]
|
36
57
|
|
37
58
|
|
38
59
|
@post("/tasks", tags=["Task"])
|
39
60
|
async def create_task(data: TaskModelIn) -> TaskModelOut:
|
40
|
-
task = Task(**data.
|
61
|
+
task = Task(**data.model_dump())
|
41
62
|
await task.save()
|
42
|
-
return task.to_dict()
|
63
|
+
return TaskModelOut(**task.to_dict())
|
43
64
|
|
44
65
|
|
45
66
|
@patch("/tasks/{task_id:int}", tags=["Task"])
|
46
67
|
async def update_task(task_id: int, data: TaskModelIn) -> TaskModelOut:
|
47
|
-
task = await Task.objects().get(Task.
|
68
|
+
task = await Task.objects().get(Task._meta.primary_key == task_id)
|
48
69
|
if not task:
|
49
70
|
raise NotFoundException("Task does not exist")
|
50
|
-
for key, value in data.
|
71
|
+
for key, value in data.model_dump().items():
|
51
72
|
setattr(task, key, value)
|
52
73
|
|
53
74
|
await task.save()
|
54
|
-
return task.to_dict()
|
75
|
+
return TaskModelOut(**task.to_dict())
|
55
76
|
|
56
77
|
|
57
78
|
@delete("/tasks/{task_id:int}", tags=["Task"])
|
58
79
|
async def delete_task(task_id: int) -> None:
|
59
|
-
task = await Task.objects().get(Task.
|
80
|
+
task = await Task.objects().get(Task._meta.primary_key == task_id)
|
60
81
|
if not task:
|
61
82
|
raise NotFoundException("Task does not exist")
|
62
83
|
await task.remove()
|
piccolo/columns/m2m.py
CHANGED
@@ -384,8 +384,12 @@ class M2MGetRelated:
|
|
384
384
|
.output(as_list=True)
|
385
385
|
)
|
386
386
|
|
387
|
-
results =
|
388
|
-
secondary_table.
|
387
|
+
results = (
|
388
|
+
await secondary_table.objects().where(
|
389
|
+
secondary_table._meta.primary_key.is_in(ids)
|
390
|
+
)
|
391
|
+
if len(ids) > 0
|
392
|
+
else []
|
389
393
|
)
|
390
394
|
|
391
395
|
return results
|
piccolo/query/methods/objects.py
CHANGED
@@ -13,6 +13,8 @@ from piccolo.query.mixins import (
|
|
13
13
|
CallbackDelegate,
|
14
14
|
CallbackType,
|
15
15
|
LimitDelegate,
|
16
|
+
LockRowsDelegate,
|
17
|
+
LockStrength,
|
16
18
|
OffsetDelegate,
|
17
19
|
OrderByDelegate,
|
18
20
|
OrderByRaw,
|
@@ -250,6 +252,7 @@ class Objects(
|
|
250
252
|
"callback_delegate",
|
251
253
|
"prefetch_delegate",
|
252
254
|
"where_delegate",
|
255
|
+
"lock_rows_delegate",
|
253
256
|
)
|
254
257
|
|
255
258
|
def __init__(
|
@@ -269,6 +272,7 @@ class Objects(
|
|
269
272
|
self.prefetch_delegate = PrefetchDelegate()
|
270
273
|
self.prefetch(*prefetch)
|
271
274
|
self.where_delegate = WhereDelegate()
|
275
|
+
self.lock_rows_delegate = LockRowsDelegate()
|
272
276
|
|
273
277
|
def output(self: Self, load_json: bool = False) -> Self:
|
274
278
|
self.output_delegate.output(
|
@@ -328,6 +332,26 @@ class Objects(
|
|
328
332
|
self.limit_delegate.limit(1)
|
329
333
|
return First[TableInstance](query=self)
|
330
334
|
|
335
|
+
def lock_rows(
|
336
|
+
self: Self,
|
337
|
+
lock_strength: t.Union[
|
338
|
+
LockStrength,
|
339
|
+
t.Literal[
|
340
|
+
"UPDATE",
|
341
|
+
"NO KEY UPDATE",
|
342
|
+
"KEY SHARE",
|
343
|
+
"SHARE",
|
344
|
+
],
|
345
|
+
] = LockStrength.update,
|
346
|
+
nowait: bool = False,
|
347
|
+
skip_locked: bool = False,
|
348
|
+
of: t.Tuple[type[Table], ...] = (),
|
349
|
+
) -> Self:
|
350
|
+
self.lock_rows_delegate.lock_rows(
|
351
|
+
lock_strength, nowait, skip_locked, of
|
352
|
+
)
|
353
|
+
return self
|
354
|
+
|
331
355
|
def get(self, where: Combinable) -> Get[TableInstance]:
|
332
356
|
self.where_delegate.where(where)
|
333
357
|
self.limit_delegate.limit(1)
|
@@ -378,6 +402,7 @@ class Objects(
|
|
378
402
|
"offset_delegate",
|
379
403
|
"output_delegate",
|
380
404
|
"order_by_delegate",
|
405
|
+
"lock_rows_delegate",
|
381
406
|
):
|
382
407
|
setattr(select, attr, getattr(self, attr))
|
383
408
|
|
piccolo/query/methods/select.py
CHANGED
@@ -19,6 +19,8 @@ from piccolo.query.mixins import (
|
|
19
19
|
DistinctDelegate,
|
20
20
|
GroupByDelegate,
|
21
21
|
LimitDelegate,
|
22
|
+
LockRowsDelegate,
|
23
|
+
LockStrength,
|
22
24
|
OffsetDelegate,
|
23
25
|
OrderByDelegate,
|
24
26
|
OrderByRaw,
|
@@ -150,6 +152,7 @@ class Select(Query[TableInstance, t.List[t.Dict[str, t.Any]]]):
|
|
150
152
|
"output_delegate",
|
151
153
|
"callback_delegate",
|
152
154
|
"where_delegate",
|
155
|
+
"lock_rows_delegate",
|
153
156
|
)
|
154
157
|
|
155
158
|
def __init__(
|
@@ -174,6 +177,7 @@ class Select(Query[TableInstance, t.List[t.Dict[str, t.Any]]]):
|
|
174
177
|
self.output_delegate = OutputDelegate()
|
175
178
|
self.callback_delegate = CallbackDelegate()
|
176
179
|
self.where_delegate = WhereDelegate()
|
180
|
+
self.lock_rows_delegate = LockRowsDelegate()
|
177
181
|
|
178
182
|
self.columns(*columns_list)
|
179
183
|
|
@@ -219,6 +223,26 @@ class Select(Query[TableInstance, t.List[t.Dict[str, t.Any]]]):
|
|
219
223
|
self.offset_delegate.offset(number)
|
220
224
|
return self
|
221
225
|
|
226
|
+
def lock_rows(
|
227
|
+
self: Self,
|
228
|
+
lock_strength: t.Union[
|
229
|
+
LockStrength,
|
230
|
+
t.Literal[
|
231
|
+
"UPDATE",
|
232
|
+
"NO KEY UPDATE",
|
233
|
+
"KEY SHARE",
|
234
|
+
"SHARE",
|
235
|
+
],
|
236
|
+
] = LockStrength.update,
|
237
|
+
nowait: bool = False,
|
238
|
+
skip_locked: bool = False,
|
239
|
+
of: t.Tuple[type[Table], ...] = (),
|
240
|
+
) -> Self:
|
241
|
+
self.lock_rows_delegate.lock_rows(
|
242
|
+
lock_strength, nowait, skip_locked, of
|
243
|
+
)
|
244
|
+
return self
|
245
|
+
|
222
246
|
async def _splice_m2m_rows(
|
223
247
|
self,
|
224
248
|
response: t.List[t.Dict[str, t.Any]],
|
@@ -618,6 +642,16 @@ class Select(Query[TableInstance, t.List[t.Dict[str, t.Any]]]):
|
|
618
642
|
query += "{}"
|
619
643
|
args.append(self.offset_delegate._offset.querystring)
|
620
644
|
|
645
|
+
if self.lock_rows_delegate._lock_rows:
|
646
|
+
if engine_type == "sqlite":
|
647
|
+
raise NotImplementedError(
|
648
|
+
"SQLite doesn't support row locking e.g. SELECT ... FOR "
|
649
|
+
"UPDATE"
|
650
|
+
)
|
651
|
+
|
652
|
+
query += "{}"
|
653
|
+
args.append(self.lock_rows_delegate._lock_rows.querystring)
|
654
|
+
|
621
655
|
querystring = QueryString(query, *args)
|
622
656
|
|
623
657
|
return [querystring]
|
piccolo/query/mixins.py
CHANGED
@@ -784,3 +784,91 @@ class OnConflictDelegate:
|
|
784
784
|
target=target, action=action_, values=values, where=where
|
785
785
|
)
|
786
786
|
)
|
787
|
+
|
788
|
+
|
789
|
+
class LockStrength(str, Enum):
|
790
|
+
"""
|
791
|
+
Specify lock strength
|
792
|
+
|
793
|
+
https://www.postgresql.org/docs/current/sql-select.html#SQL-FOR-UPDATE-SHARE
|
794
|
+
"""
|
795
|
+
|
796
|
+
update = "UPDATE"
|
797
|
+
no_key_update = "NO KEY UPDATE"
|
798
|
+
share = "SHARE"
|
799
|
+
key_share = "KEY SHARE"
|
800
|
+
|
801
|
+
|
802
|
+
@dataclass
|
803
|
+
class LockRows:
|
804
|
+
__slots__ = ("lock_strength", "nowait", "skip_locked", "of")
|
805
|
+
|
806
|
+
lock_strength: LockStrength
|
807
|
+
nowait: bool
|
808
|
+
skip_locked: bool
|
809
|
+
of: t.Tuple[t.Type[Table], ...]
|
810
|
+
|
811
|
+
def __post_init__(self):
|
812
|
+
if not isinstance(self.lock_strength, LockStrength):
|
813
|
+
raise TypeError("lock_strength must be a LockStrength")
|
814
|
+
if not isinstance(self.nowait, bool):
|
815
|
+
raise TypeError("nowait must be a bool")
|
816
|
+
if not isinstance(self.skip_locked, bool):
|
817
|
+
raise TypeError("skip_locked must be a bool")
|
818
|
+
if not isinstance(self.of, tuple) or not all(
|
819
|
+
hasattr(x, "_meta") for x in self.of
|
820
|
+
):
|
821
|
+
raise TypeError("of must be a tuple of Table")
|
822
|
+
if self.nowait and self.skip_locked:
|
823
|
+
raise TypeError(
|
824
|
+
"The nowait option cannot be used with skip_locked"
|
825
|
+
)
|
826
|
+
|
827
|
+
@property
|
828
|
+
def querystring(self) -> QueryString:
|
829
|
+
sql = f" FOR {self.lock_strength.value}"
|
830
|
+
if self.of:
|
831
|
+
tables = ", ".join(
|
832
|
+
i._meta.get_formatted_tablename() for i in self.of
|
833
|
+
)
|
834
|
+
sql += " OF " + tables
|
835
|
+
if self.nowait:
|
836
|
+
sql += " NOWAIT"
|
837
|
+
if self.skip_locked:
|
838
|
+
sql += " SKIP LOCKED"
|
839
|
+
|
840
|
+
return QueryString(sql)
|
841
|
+
|
842
|
+
def __str__(self) -> str:
|
843
|
+
return self.querystring.__str__()
|
844
|
+
|
845
|
+
|
846
|
+
@dataclass
|
847
|
+
class LockRowsDelegate:
|
848
|
+
|
849
|
+
_lock_rows: t.Optional[LockRows] = None
|
850
|
+
|
851
|
+
def lock_rows(
|
852
|
+
self,
|
853
|
+
lock_strength: t.Union[
|
854
|
+
LockStrength,
|
855
|
+
t.Literal[
|
856
|
+
"UPDATE",
|
857
|
+
"NO KEY UPDATE",
|
858
|
+
"KEY SHARE",
|
859
|
+
"SHARE",
|
860
|
+
],
|
861
|
+
] = LockStrength.update,
|
862
|
+
nowait=False,
|
863
|
+
skip_locked=False,
|
864
|
+
of: t.Tuple[type[Table], ...] = (),
|
865
|
+
):
|
866
|
+
lock_strength_: LockStrength
|
867
|
+
if isinstance(lock_strength, LockStrength):
|
868
|
+
lock_strength_ = lock_strength
|
869
|
+
elif isinstance(lock_strength, str):
|
870
|
+
lock_strength_ = LockStrength(lock_strength.upper())
|
871
|
+
else:
|
872
|
+
raise ValueError("Unrecognised `lock_strength` value.")
|
873
|
+
|
874
|
+
self._lock_rows = LockRows(lock_strength_, nowait, skip_locked, of)
|
@@ -1,4 +1,4 @@
|
|
1
|
-
piccolo/__init__.py,sha256=
|
1
|
+
piccolo/__init__.py,sha256=JP09HcvsWvSb_xYAg-Jfrxz8qqh31rbqbIN3dcneflw,23
|
2
2
|
piccolo/custom_types.py,sha256=7HMQAze-5mieNLfbQ5QgbRQgR2abR7ol0qehv2SqROY,604
|
3
3
|
piccolo/main.py,sha256=1VsFV67FWTUikPTysp64Fmgd9QBVa_9wcwKfwj2UCEA,5117
|
4
4
|
piccolo/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -17,13 +17,13 @@ piccolo/apps/app/commands/templates/tables.py.jinja,sha256=revzdrvDDwe78VedBKz0z
|
|
17
17
|
piccolo/apps/asgi/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
18
18
|
piccolo/apps/asgi/piccolo_app.py,sha256=7VUvqQJbB-ScO0A62S6MiJmQL9F5DS-SdlqlDLbAblE,217
|
19
19
|
piccolo/apps/asgi/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
20
|
-
piccolo/apps/asgi/commands/new.py,sha256=
|
20
|
+
piccolo/apps/asgi/commands/new.py,sha256=xrN9RJ0Lnz54qY_rxqA9Z2SS4kC_0RXdYYW7tXmGgIo,4232
|
21
21
|
piccolo/apps/asgi/commands/templates/app/README.md.jinja,sha256=As3gNEZt9qcRmTVkjCzNtXJ8r4-3g0fCSe7Q-P39ezI,214
|
22
|
-
piccolo/apps/asgi/commands/templates/app/_blacksheep_app.py.jinja,sha256=
|
23
|
-
piccolo/apps/asgi/commands/templates/app/_esmerald_app.py.jinja,sha256=
|
22
|
+
piccolo/apps/asgi/commands/templates/app/_blacksheep_app.py.jinja,sha256=IKOql1G5wrEKm5qErlizOmrwYKlnxkm-d8NY5uVg9KA,3186
|
23
|
+
piccolo/apps/asgi/commands/templates/app/_esmerald_app.py.jinja,sha256=nTzXc5IJLl_al1FuzG5AnaA1vSn-ipMurpPK7BibmB8,2710
|
24
24
|
piccolo/apps/asgi/commands/templates/app/_fastapi_app.py.jinja,sha256=mKnYfUOnYyWJA1jFoRLCUOGQlK6imaxx_1qaauGjeeQ,2627
|
25
|
-
piccolo/apps/asgi/commands/templates/app/_lilya_app.py.jinja,sha256=
|
26
|
-
piccolo/apps/asgi/commands/templates/app/_litestar_app.py.jinja,sha256=
|
25
|
+
piccolo/apps/asgi/commands/templates/app/_lilya_app.py.jinja,sha256=PUph5Jj_AXVpxXZmpUzzHXogUchU8vjKBL_7WvgrfCU,1260
|
26
|
+
piccolo/apps/asgi/commands/templates/app/_litestar_app.py.jinja,sha256=VCY4FoA7YlEhtjWB09XWQqi8GgL36VQwGGBpSXUDO5o,3349
|
27
27
|
piccolo/apps/asgi/commands/templates/app/_starlette_app.py.jinja,sha256=vHcAzsS9I3OevYoznwZp8zucI4OEyUjj-EOAtscmlSE,1443
|
28
28
|
piccolo/apps/asgi/commands/templates/app/app.py.jinja,sha256=gROY-LbHl8NtHDM_ntkI7Rjcbtg2ypDZ1FunBvpdjE4,458
|
29
29
|
piccolo/apps/asgi/commands/templates/app/conftest.py.jinja,sha256=ZG1pRVMv3LhIfOsO3_08c_fF3EV4_EApuDHiIFFPJdk,497
|
@@ -120,7 +120,7 @@ piccolo/columns/choices.py,sha256=-HNQuk9vMmVZIPZ5PMeXGTfr23o4nzKPSAkvcG1k0y8,72
|
|
120
120
|
piccolo/columns/column_types.py,sha256=X_ZsA0C4WBNVonV2OsizRdt1osMshgtQ3Ob6JN-amfg,83485
|
121
121
|
piccolo/columns/combination.py,sha256=vMXC2dfY7pvnCFhsT71XFVyb4gdQzfRsCMaiduu04Ss,6900
|
122
122
|
piccolo/columns/indexes.py,sha256=NfNok3v_791jgDlN28KmhP9ZCjl6031BXmjxV3ovXJk,372
|
123
|
-
piccolo/columns/m2m.py,sha256=
|
123
|
+
piccolo/columns/m2m.py,sha256=QMeSOnm4DT2cG9U5jC6sOZ6z9DxCWwDyZMSqk0wR2q4,14682
|
124
124
|
piccolo/columns/readable.py,sha256=hganxUPfIK5ZXn-qgteBxsOJfBJucgr9U0QLsLFYcuI,1562
|
125
125
|
piccolo/columns/reference.py,sha256=n4SW5CGrUSwsRk2Pm7UbikmKdUHuJ2D5OobQ7Mea1vQ,3669
|
126
126
|
piccolo/columns/defaults/__init__.py,sha256=7hpB13baEJgc1zbZjRKDFr-5hltxM2VGj8KnKfOiS8c,145
|
@@ -147,7 +147,7 @@ piccolo/engine/postgres.py,sha256=DekL3KafCdzSAEQ6_EgOiUB1ERXh2xpePYwI9QvmN-c,18
|
|
147
147
|
piccolo/engine/sqlite.py,sha256=KwJc3UttBP_8qSREbLJshqEfROF17ENf0Ju9BwI5_so,25236
|
148
148
|
piccolo/query/__init__.py,sha256=bcsMV4813rMRAIqGv4DxI4eyO4FmpXkDv9dfTk5pt3A,699
|
149
149
|
piccolo/query/base.py,sha256=iI9Fv3oOw7T4ZWZvRKRwdtClvQtSaAepslH24vwxZVA,14616
|
150
|
-
piccolo/query/mixins.py,sha256=
|
150
|
+
piccolo/query/mixins.py,sha256=X9HEYnj6uOjgTkGr4vgqTwN_dokJPzVagwbFx385atQ,24468
|
151
151
|
piccolo/query/proxy.py,sha256=Yq4jNc7IWJvdeO3u7_7iPyRy2WhVj8KsIUcIYHBIi9Q,1839
|
152
152
|
piccolo/query/functions/__init__.py,sha256=pZkzOIh7Sg9HPNOeegOwAS46Oxt31ATlSVmwn-lxCbc,605
|
153
153
|
piccolo/query/functions/aggregate.py,sha256=OdjDjr_zyD4S9UbrZ2C3V5mz4OT2sIfAFAdTGr4WL54,4248
|
@@ -166,10 +166,10 @@ piccolo/query/methods/drop_index.py,sha256=5x3vHpoOmQ1SMhj6L7snKXX6M9l9j1E1PFSO6
|
|
166
166
|
piccolo/query/methods/exists.py,sha256=lTMjtrFPFygZmaPV3sfQKXc3K0sVqJ2S6PDc3fRK6YQ,1203
|
167
167
|
piccolo/query/methods/indexes.py,sha256=J-QUqaBJwpgahskUH0Cu0Mq7zEKcfVAtDsUVIVX-C4c,943
|
168
168
|
piccolo/query/methods/insert.py,sha256=ssLJ_wn08KnOwwr7t-VILyn1P4hrvM63CfPIcAJWT5k,4701
|
169
|
-
piccolo/query/methods/objects.py,sha256=
|
169
|
+
piccolo/query/methods/objects.py,sha256=kWHSgnXFVYe-x6tIHstbZvZic6mQCCGES-L_2Icbg-I,13910
|
170
170
|
piccolo/query/methods/raw.py,sha256=wQWR8b-yA_Gr-5lqRMZe9BOAAMBAw8CqTx37qVYvM1A,987
|
171
171
|
piccolo/query/methods/refresh.py,sha256=wg1zghKfwz-VmqK4uWa4GNMiDtK-skTqow591Hb3ONM,5854
|
172
|
-
piccolo/query/methods/select.py,sha256=
|
172
|
+
piccolo/query/methods/select.py,sha256=jNR7CsEPUarffYMsXytKm7iScrQ_nqpRX-5mTPrXfjg,22414
|
173
173
|
piccolo/query/methods/table_exists.py,sha256=0yb3n6Jd2ovSBWlZ-gl00K4E7Jnbj7J8qAAX5d7hvNk,1259
|
174
174
|
piccolo/query/methods/update.py,sha256=LfWqIXEl1aecc0rkVssTFmwyD6wXGhlKcTrUVhtlEsw,3705
|
175
175
|
piccolo/testing/__init__.py,sha256=pRFSqRInfx95AakOq54atmvqoB-ue073q2aR8u8zR40,83
|
@@ -273,7 +273,7 @@ tests/columns/test_timestamptz.py,sha256=P7zblPC6Fjjdk6iOhVUGIKnFFzbbUPVNSY98qbu
|
|
273
273
|
tests/columns/test_uuid.py,sha256=taFYNvRZjQztMPbTQHYtwQutvcLnKPt6_aUxsf2o04Q,372
|
274
274
|
tests/columns/test_varchar.py,sha256=fbwBdimHoGaylfrqkFIgQ5m2q80umSoUNHIwofM6j_c,721
|
275
275
|
tests/columns/m2m/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
276
|
-
tests/columns/m2m/base.py,sha256=
|
276
|
+
tests/columns/m2m/base.py,sha256=QOFeiTu7NPSsBju4ZW3up2utFr1dI5HMhhi2A1MG21o,15016
|
277
277
|
tests/columns/m2m/test_m2m.py,sha256=0ObmIHUJF6CZoNBznc5xXVr5_BbGBqOmWwtpg8IcPt4,13055
|
278
278
|
tests/columns/m2m/test_m2m_schema.py,sha256=oxu7eAjFFpDjnq9Eq-5OTNmlnsEIMFWx18OItfpVs-s,339
|
279
279
|
tests/conf/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -339,7 +339,7 @@ tests/table/test_raw.py,sha256=9PTvYngQi41nYd5lKzkJdTqsEcwrdOXcvZjq-W26CwQ,1683
|
|
339
339
|
tests/table/test_ref.py,sha256=eYNRnYHzNMXuMbV3B1ca5EidpIg4500q6hr1ccuVaso,269
|
340
340
|
tests/table/test_refresh.py,sha256=-BaLS6fZiR2RtQaFa7D9WGBjrbrss1-tt5xz1NE_m8E,9250
|
341
341
|
tests/table/test_repr.py,sha256=uahz3_GffGQrf2mDE-4-Pu4AmSLBAyso6-9rbohCl58,446
|
342
|
-
tests/table/test_select.py,sha256=
|
342
|
+
tests/table/test_select.py,sha256=PcLRtHGEZfYNB0Qm2b9P6NFJOH2lWvviRjsr1cEmsMQ,41503
|
343
343
|
tests/table/test_str.py,sha256=eztWNULcjARR1fr9X5n4tojhDNgDfatVyNHwuYrzHAo,1731
|
344
344
|
tests/table/test_table_exists.py,sha256=upv2e9UD32V2QZOShzmcw0reMqRbYiX_jxWx57p25jg,1082
|
345
345
|
tests/table/test_update.py,sha256=Cqi0xX3kEuJ0k-x_emPGB3arXuGWZ9e3CJ3HPFnw9Zw,20505
|
@@ -368,9 +368,9 @@ tests/utils/test_sql_values.py,sha256=vzxRmy16FfLZPH-sAQexBvsF9MXB8n4smr14qoEOS5
|
|
368
368
|
tests/utils/test_sync.py,sha256=9ytVo56y2vPQePvTeIi9lHIouEhWJbodl1TmzkGFrSo,799
|
369
369
|
tests/utils/test_table_reflection.py,sha256=SIzuat-IpcVj1GCFyOWKShI8YkhdOPPFH7qVrvfyPNE,3794
|
370
370
|
tests/utils/test_warnings.py,sha256=NvSC_cvJ6uZcwAGf1m-hLzETXCqprXELL8zg3TNLVMw,269
|
371
|
-
piccolo-1.
|
372
|
-
piccolo-1.
|
373
|
-
piccolo-1.
|
374
|
-
piccolo-1.
|
375
|
-
piccolo-1.
|
376
|
-
piccolo-1.
|
371
|
+
piccolo-1.19.1.dist-info/LICENSE,sha256=zFIpi-16uIJ420UMIG75NU0JbDBykvrdnXcj5U_EYBI,1059
|
372
|
+
piccolo-1.19.1.dist-info/METADATA,sha256=owDJXe8zA0Tiloy78LB-2NPyDCxfM7qfFWOeDnXGNO8,5178
|
373
|
+
piccolo-1.19.1.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
|
374
|
+
piccolo-1.19.1.dist-info/entry_points.txt,sha256=SJPHET4Fi1bN5F3WqcKkv9SClK3_F1I7m4eQjk6AFh0,46
|
375
|
+
piccolo-1.19.1.dist-info/top_level.txt,sha256=-SR74VGbk43VoPy1HH-mHm97yoGukLK87HE5kdBW6qM,24
|
376
|
+
piccolo-1.19.1.dist-info/RECORD,,
|
tests/columns/m2m/base.py
CHANGED
@@ -434,6 +434,22 @@ class M2MBase:
|
|
434
434
|
|
435
435
|
self.assertEqual([i.name for i in genres], ["Rock", "Folk"])
|
436
436
|
|
437
|
+
def test_get_m2m_no_rows(self):
|
438
|
+
"""
|
439
|
+
If there are no matching objects, then an empty list should be
|
440
|
+
returned.
|
441
|
+
|
442
|
+
https://github.com/piccolo-orm/piccolo/issues/1090
|
443
|
+
|
444
|
+
"""
|
445
|
+
band = Band.objects().get(Band.name == "Pythonistas").run_sync()
|
446
|
+
assert band is not None
|
447
|
+
|
448
|
+
Genre.delete(force=True).run_sync()
|
449
|
+
|
450
|
+
genres = band.get_m2m(Band.genres).run_sync()
|
451
|
+
self.assertEqual(genres, [])
|
452
|
+
|
437
453
|
def test_remove_m2m(self):
|
438
454
|
"""
|
439
455
|
Make sure we can remove related items via the joining table.
|
tests/table/test_select.py
CHANGED
@@ -1028,6 +1028,40 @@ class TestSelect(DBTestCase):
|
|
1028
1028
|
response, [{"name": "Pythonistas", "popularity_log": 3.0}]
|
1029
1029
|
)
|
1030
1030
|
|
1031
|
+
@pytest.mark.skipif(
|
1032
|
+
is_running_sqlite(),
|
1033
|
+
reason="SQLite doesn't support SELECT ... FOR UPDATE.",
|
1034
|
+
)
|
1035
|
+
def test_lock_rows(self):
|
1036
|
+
"""
|
1037
|
+
Make sure the for_update clause works.
|
1038
|
+
"""
|
1039
|
+
self.insert_rows()
|
1040
|
+
|
1041
|
+
query = Band.select()
|
1042
|
+
self.assertNotIn("FOR UPDATE", query.__str__())
|
1043
|
+
|
1044
|
+
query = query.lock_rows()
|
1045
|
+
self.assertTrue(query.__str__().endswith("FOR UPDATE"))
|
1046
|
+
|
1047
|
+
query = query.lock_rows(lock_strength="KEY SHARE")
|
1048
|
+
self.assertTrue(query.__str__().endswith("FOR KEY SHARE"))
|
1049
|
+
|
1050
|
+
query = query.lock_rows(skip_locked=True)
|
1051
|
+
self.assertTrue(query.__str__().endswith("FOR UPDATE SKIP LOCKED"))
|
1052
|
+
|
1053
|
+
query = query.lock_rows(nowait=True)
|
1054
|
+
self.assertTrue(query.__str__().endswith("FOR UPDATE NOWAIT"))
|
1055
|
+
|
1056
|
+
query = query.lock_rows(of=(Band,))
|
1057
|
+
self.assertTrue(query.__str__().endswith('FOR UPDATE OF "band"'))
|
1058
|
+
|
1059
|
+
with self.assertRaises(TypeError):
|
1060
|
+
query = query.lock_rows(skip_locked=True, nowait=True)
|
1061
|
+
|
1062
|
+
response = query.run_sync()
|
1063
|
+
assert response is not None
|
1064
|
+
|
1031
1065
|
|
1032
1066
|
class TestSelectSecret(TestCase):
|
1033
1067
|
def setUp(self):
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|