piccolo 0.109.0__py3-none-any.whl → 0.111.0__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 -3
- piccolo/apps/asgi/commands/templates/app/{_starlite_app.py.jinja → _litestar_app.py.jinja} +8 -8
- piccolo/apps/asgi/commands/templates/app/app.py.jinja +2 -4
- piccolo/apps/asgi/commands/templates/app/home/{_starlite_endpoints.py.jinja → _litestar_endpoints.py.jinja} +2 -1
- piccolo/apps/asgi/commands/templates/app/home/endpoints.py.jinja +2 -4
- piccolo/apps/asgi/commands/templates/app/home/templates/home.html.jinja_raw +1 -6
- piccolo/apps/playground/commands/run.py +24 -8
- piccolo/columns/column_types.py +2 -2
- piccolo/query/methods/insert.py +61 -5
- piccolo/query/methods/objects.py +2 -0
- piccolo/query/methods/select.py +23 -10
- piccolo/query/mixins.py +245 -7
- piccolo/testing/model_builder.py +9 -7
- {piccolo-0.109.0.dist-info → piccolo-0.111.0.dist-info}/METADATA +2 -2
- {piccolo-0.109.0.dist-info → piccolo-0.111.0.dist-info}/RECORD +22 -24
- tests/table/test_insert.py +397 -1
- tests/table/test_select.py +160 -1
- piccolo/apps/asgi/commands/templates/app/_xpresso_app.py.jinja +0 -113
- piccolo/apps/asgi/commands/templates/app/home/_xpresso_endpoints.py.jinja +0 -20
- {piccolo-0.109.0.dist-info → piccolo-0.111.0.dist-info}/LICENSE +0 -0
- {piccolo-0.109.0.dist-info → piccolo-0.111.0.dist-info}/WHEEL +0 -0
- {piccolo-0.109.0.dist-info → piccolo-0.111.0.dist-info}/entry_points.txt +0 -0
- {piccolo-0.109.0.dist-info → piccolo-0.111.0.dist-info}/top_level.txt +0 -0
tests/table/test_select.py
CHANGED
@@ -1,12 +1,15 @@
|
|
1
|
+
import datetime
|
1
2
|
from unittest import TestCase
|
2
3
|
|
3
4
|
import pytest
|
4
5
|
|
5
6
|
from piccolo.apps.user.tables import BaseUser
|
7
|
+
from piccolo.columns import Date, Varchar
|
6
8
|
from piccolo.columns.combination import WhereRaw
|
7
9
|
from piccolo.query import OrderByRaw
|
8
10
|
from piccolo.query.methods.select import Avg, Count, Max, Min, SelectRaw, Sum
|
9
|
-
from piccolo.
|
11
|
+
from piccolo.query.mixins import DistinctOnError
|
12
|
+
from piccolo.table import Table, create_db_tables_sync, drop_db_tables_sync
|
10
13
|
from tests.base import (
|
11
14
|
DBTestCase,
|
12
15
|
engine_is,
|
@@ -521,6 +524,27 @@ class TestSelect(DBTestCase):
|
|
521
524
|
response = query.run_sync()
|
522
525
|
self.assertEqual(response, [{"name": "Pythonistas"}])
|
523
526
|
|
527
|
+
def test_distinct_on(self):
|
528
|
+
"""
|
529
|
+
Make sure the distinct clause works, with the ``on`` param.
|
530
|
+
"""
|
531
|
+
self.insert_rows()
|
532
|
+
self.insert_rows()
|
533
|
+
|
534
|
+
query = Band.select(Band.name).where(Band.name == "Pythonistas")
|
535
|
+
self.assertNotIn("DISTINCT", query.__str__())
|
536
|
+
|
537
|
+
response = query.run_sync()
|
538
|
+
self.assertEqual(
|
539
|
+
response, [{"name": "Pythonistas"}, {"name": "Pythonistas"}]
|
540
|
+
)
|
541
|
+
|
542
|
+
query = query.distinct()
|
543
|
+
self.assertIn("DISTINCT", query.__str__())
|
544
|
+
|
545
|
+
response = query.run_sync()
|
546
|
+
self.assertEqual(response, [{"name": "Pythonistas"}])
|
547
|
+
|
524
548
|
def test_count_group_by(self):
|
525
549
|
"""
|
526
550
|
Test grouping and counting all rows.
|
@@ -1237,3 +1261,138 @@ class TestSelectOrderBy(TestCase):
|
|
1237
1261
|
{"name": "Rustaceans"},
|
1238
1262
|
],
|
1239
1263
|
)
|
1264
|
+
|
1265
|
+
|
1266
|
+
class Album(Table):
|
1267
|
+
band = Varchar()
|
1268
|
+
title = Varchar()
|
1269
|
+
release_date = Date()
|
1270
|
+
|
1271
|
+
|
1272
|
+
class TestDistinctOn(TestCase):
|
1273
|
+
def setUp(self):
|
1274
|
+
Album.create_table().run_sync()
|
1275
|
+
|
1276
|
+
def tearDown(self):
|
1277
|
+
Album.alter().drop_table().run_sync()
|
1278
|
+
|
1279
|
+
@engines_only("postgres", "cockroach")
|
1280
|
+
def test_distinct_on(self):
|
1281
|
+
"""
|
1282
|
+
Make sure the ``distinct`` method can be used to create a
|
1283
|
+
``DISTINCT ON`` clause.
|
1284
|
+
"""
|
1285
|
+
Album.insert(
|
1286
|
+
Album(
|
1287
|
+
{
|
1288
|
+
Album.band: "Pythonistas",
|
1289
|
+
Album.title: "P1",
|
1290
|
+
Album.release_date: datetime.date(
|
1291
|
+
year=2022, month=1, day=1
|
1292
|
+
),
|
1293
|
+
}
|
1294
|
+
),
|
1295
|
+
Album(
|
1296
|
+
{
|
1297
|
+
Album.band: "Pythonistas",
|
1298
|
+
Album.title: "P2",
|
1299
|
+
Album.release_date: datetime.date(
|
1300
|
+
year=2023, month=1, day=1
|
1301
|
+
),
|
1302
|
+
}
|
1303
|
+
),
|
1304
|
+
Album(
|
1305
|
+
{
|
1306
|
+
Album.band: "Rustaceans",
|
1307
|
+
Album.title: "R1",
|
1308
|
+
Album.release_date: datetime.date(
|
1309
|
+
year=2022, month=1, day=1
|
1310
|
+
),
|
1311
|
+
}
|
1312
|
+
),
|
1313
|
+
Album(
|
1314
|
+
{
|
1315
|
+
Album.band: "Rustaceans",
|
1316
|
+
Album.title: "R2",
|
1317
|
+
Album.release_date: datetime.date(
|
1318
|
+
year=2023, month=1, day=1
|
1319
|
+
),
|
1320
|
+
}
|
1321
|
+
),
|
1322
|
+
Album(
|
1323
|
+
{
|
1324
|
+
Album.band: "C-Sharps",
|
1325
|
+
Album.title: "C1",
|
1326
|
+
Album.release_date: datetime.date(
|
1327
|
+
year=2022, month=1, day=1
|
1328
|
+
),
|
1329
|
+
}
|
1330
|
+
),
|
1331
|
+
Album(
|
1332
|
+
{
|
1333
|
+
Album.band: "C-Sharps",
|
1334
|
+
Album.title: "C2",
|
1335
|
+
Album.release_date: datetime.date(
|
1336
|
+
year=2023, month=1, day=1
|
1337
|
+
),
|
1338
|
+
}
|
1339
|
+
),
|
1340
|
+
).run_sync()
|
1341
|
+
|
1342
|
+
# Get the most recent album for each band.
|
1343
|
+
query = (
|
1344
|
+
Album.select(Album.band, Album.title)
|
1345
|
+
.distinct(on=[Album.band])
|
1346
|
+
.order_by(Album.band)
|
1347
|
+
.order_by(Album.release_date, ascending=False)
|
1348
|
+
)
|
1349
|
+
self.assertIn("DISTINCT ON", query.__str__())
|
1350
|
+
response = query.run_sync()
|
1351
|
+
|
1352
|
+
self.assertEqual(
|
1353
|
+
response,
|
1354
|
+
[
|
1355
|
+
{"band": "C-Sharps", "title": "C2"},
|
1356
|
+
{"band": "Pythonistas", "title": "P2"},
|
1357
|
+
{"band": "Rustaceans", "title": "R2"},
|
1358
|
+
],
|
1359
|
+
)
|
1360
|
+
|
1361
|
+
@engines_only("sqlite")
|
1362
|
+
def test_distinct_on_sqlite(self):
|
1363
|
+
"""
|
1364
|
+
SQLite doesn't support ``DISTINCT ON``, so a ``ValueError`` should be
|
1365
|
+
raised.
|
1366
|
+
"""
|
1367
|
+
with self.assertRaises(NotImplementedError) as manager:
|
1368
|
+
Album.select().distinct(on=[Album.band])
|
1369
|
+
|
1370
|
+
self.assertEqual(
|
1371
|
+
manager.exception.__str__(),
|
1372
|
+
"SQLite doesn't support DISTINCT ON",
|
1373
|
+
)
|
1374
|
+
|
1375
|
+
@engines_only("postgres", "cockroach")
|
1376
|
+
def test_distinct_on_error(self):
|
1377
|
+
"""
|
1378
|
+
If we pass in something other than a sequence of columns, it should
|
1379
|
+
raise a ValueError.
|
1380
|
+
"""
|
1381
|
+
with self.assertRaises(ValueError) as manager:
|
1382
|
+
Album.select().distinct(on=Album.band)
|
1383
|
+
|
1384
|
+
self.assertEqual(
|
1385
|
+
manager.exception.__str__(),
|
1386
|
+
"`on` must be a sequence of `Column` instances",
|
1387
|
+
)
|
1388
|
+
|
1389
|
+
@engines_only("postgres", "cockroach")
|
1390
|
+
def test_distinct_on_order_by_error(self):
|
1391
|
+
"""
|
1392
|
+
The first column passed to `order_by` must match the first column
|
1393
|
+
passed to `on`, otherwise an exception is raised.
|
1394
|
+
"""
|
1395
|
+
with self.assertRaises(DistinctOnError):
|
1396
|
+
Album.select().distinct(on=[Album.band]).order_by(
|
1397
|
+
Album.release_date
|
1398
|
+
).run_sync()
|
@@ -1,113 +0,0 @@
|
|
1
|
-
import typing as t
|
2
|
-
from contextlib import asynccontextmanager
|
3
|
-
|
4
|
-
from piccolo.engine import engine_finder
|
5
|
-
from piccolo.utils.pydantic import create_pydantic_model
|
6
|
-
from piccolo_admin.endpoints import create_admin
|
7
|
-
from starlette.staticfiles import StaticFiles
|
8
|
-
from xpresso import App, FromJson, FromPath, HTTPException, Operation, Path
|
9
|
-
from xpresso.routing.mount import Mount
|
10
|
-
|
11
|
-
from home.endpoints import home
|
12
|
-
from home.piccolo_app import APP_CONFIG
|
13
|
-
from home.tables import Task
|
14
|
-
|
15
|
-
TaskModelIn: t.Any = create_pydantic_model(table=Task, model_name="TaskModelIn")
|
16
|
-
TaskModelOut: t.Any = create_pydantic_model(
|
17
|
-
table=Task, include_default_columns=True, model_name="TaskModelOut"
|
18
|
-
)
|
19
|
-
|
20
|
-
|
21
|
-
async def tasks() -> t.List[TaskModelOut]:
|
22
|
-
return await Task.select().order_by(Task.id)
|
23
|
-
|
24
|
-
|
25
|
-
async def create_task(task_model: FromJson[TaskModelIn]) -> TaskModelOut:
|
26
|
-
task = Task(**task_model.dict())
|
27
|
-
await task.save()
|
28
|
-
return task.to_dict()
|
29
|
-
|
30
|
-
|
31
|
-
async def update_task(
|
32
|
-
task_id: FromPath[int], task_model: FromJson[TaskModelIn]
|
33
|
-
) -> TaskModelOut:
|
34
|
-
task = await Task.objects().get(Task.id == task_id)
|
35
|
-
if not task:
|
36
|
-
raise HTTPException(status_code=404)
|
37
|
-
|
38
|
-
for key, value in task_model.dict().items():
|
39
|
-
setattr(task, key, value)
|
40
|
-
|
41
|
-
await task.save()
|
42
|
-
|
43
|
-
return task.to_dict()
|
44
|
-
|
45
|
-
|
46
|
-
async def delete_task(task_id: FromPath[int]):
|
47
|
-
task = await Task.objects().get(Task.id == task_id)
|
48
|
-
if not task:
|
49
|
-
raise HTTPException(status_code=404)
|
50
|
-
|
51
|
-
await task.remove()
|
52
|
-
|
53
|
-
return {}
|
54
|
-
|
55
|
-
|
56
|
-
@asynccontextmanager
|
57
|
-
async def lifespan():
|
58
|
-
await open_database_connection_pool()
|
59
|
-
try:
|
60
|
-
yield
|
61
|
-
finally:
|
62
|
-
await close_database_connection_pool()
|
63
|
-
|
64
|
-
|
65
|
-
app = App(
|
66
|
-
routes=[
|
67
|
-
Path(
|
68
|
-
"/",
|
69
|
-
get=Operation(
|
70
|
-
home,
|
71
|
-
include_in_schema=False,
|
72
|
-
),
|
73
|
-
),
|
74
|
-
Mount(
|
75
|
-
"/admin/",
|
76
|
-
create_admin(
|
77
|
-
tables=APP_CONFIG.table_classes,
|
78
|
-
# Required when running under HTTPS:
|
79
|
-
# allowed_hosts=['my_site.com']
|
80
|
-
),
|
81
|
-
),
|
82
|
-
Path(
|
83
|
-
"/tasks/",
|
84
|
-
get=tasks,
|
85
|
-
post=create_task,
|
86
|
-
tags=["Task"],
|
87
|
-
),
|
88
|
-
Path(
|
89
|
-
"/tasks/{task_id}/",
|
90
|
-
put=update_task,
|
91
|
-
delete=delete_task,
|
92
|
-
tags=["Task"],
|
93
|
-
),
|
94
|
-
Mount("/static/", StaticFiles(directory="static")),
|
95
|
-
],
|
96
|
-
lifespan=lifespan,
|
97
|
-
)
|
98
|
-
|
99
|
-
|
100
|
-
async def open_database_connection_pool():
|
101
|
-
try:
|
102
|
-
engine = engine_finder()
|
103
|
-
await engine.start_connection_pool()
|
104
|
-
except Exception:
|
105
|
-
print("Unable to connect to the database")
|
106
|
-
|
107
|
-
|
108
|
-
async def close_database_connection_pool():
|
109
|
-
try:
|
110
|
-
engine = engine_finder()
|
111
|
-
await engine.close_connection_pool()
|
112
|
-
except Exception:
|
113
|
-
print("Unable to connect to the database")
|
@@ -1,20 +0,0 @@
|
|
1
|
-
import os
|
2
|
-
|
3
|
-
import jinja2
|
4
|
-
from xpresso.responses import HTMLResponse
|
5
|
-
|
6
|
-
ENVIRONMENT = jinja2.Environment(
|
7
|
-
loader=jinja2.FileSystemLoader(
|
8
|
-
searchpath=os.path.join(os.path.dirname(__file__), "templates")
|
9
|
-
)
|
10
|
-
)
|
11
|
-
|
12
|
-
|
13
|
-
async def home():
|
14
|
-
template = ENVIRONMENT.get_template("home.html.jinja")
|
15
|
-
|
16
|
-
content = template.render(
|
17
|
-
title="Piccolo + ASGI",
|
18
|
-
)
|
19
|
-
|
20
|
-
return HTMLResponse(content)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|