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.
@@ -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.table import create_db_tables_sync, drop_db_tables_sync
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)