piccolo 0.108.0__py3-none-any.whl → 0.110.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/base.py +45 -0
- piccolo/columns/column_types.py +2 -2
- piccolo/query/methods/select.py +25 -10
- piccolo/query/mixins.py +77 -3
- piccolo/testing/model_builder.py +9 -7
- {piccolo-0.108.0.dist-info → piccolo-0.110.0.dist-info}/METADATA +2 -2
- {piccolo-0.108.0.dist-info → piccolo-0.110.0.dist-info}/RECORD +21 -22
- tests/table/test_join_on.py +107 -0
- 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.108.0.dist-info → piccolo-0.110.0.dist-info}/LICENSE +0 -0
- {piccolo-0.108.0.dist-info → piccolo-0.110.0.dist-info}/WHEEL +0 -0
- {piccolo-0.108.0.dist-info → piccolo-0.110.0.dist-info}/entry_points.txt +0 -0
- {piccolo-0.108.0.dist-info → piccolo-0.110.0.dist-info}/top_level.txt +0 -0
piccolo/__init__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__VERSION__ = "0.
|
1
|
+
__VERSION__ = "0.110.0"
|
@@ -10,10 +10,9 @@ from jinja2 import Environment, FileSystemLoader
|
|
10
10
|
|
11
11
|
TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "templates/app/")
|
12
12
|
SERVERS = ["uvicorn", "Hypercorn"]
|
13
|
-
ROUTERS = ["starlette", "fastapi", "blacksheep", "
|
13
|
+
ROUTERS = ["starlette", "fastapi", "blacksheep", "litestar"]
|
14
14
|
ROUTER_DEPENDENCIES = {
|
15
|
-
"
|
16
|
-
"xpresso": ["xpresso==0.43.0", "di==0.72.1"],
|
15
|
+
"litestar": ["litestar>=2.0.0a3"],
|
17
16
|
}
|
18
17
|
|
19
18
|
|
@@ -2,13 +2,13 @@ import typing as t
|
|
2
2
|
|
3
3
|
from piccolo.engine import engine_finder
|
4
4
|
from piccolo_admin.endpoints import create_admin
|
5
|
-
from
|
6
|
-
from
|
7
|
-
from
|
8
|
-
from
|
9
|
-
from
|
10
|
-
from
|
11
|
-
from
|
5
|
+
from litestar import Litestar, asgi, delete, get, patch, post
|
6
|
+
from litestar.static_files import StaticFilesConfig
|
7
|
+
from litestar.template import TemplateConfig
|
8
|
+
from litestar.contrib.jinja import JinjaTemplateEngine
|
9
|
+
from litestar.contrib.piccolo_orm import PiccoloORMPlugin
|
10
|
+
from litestar.exceptions import NotFoundException
|
11
|
+
from litestar.types import Receive, Scope, Send
|
12
12
|
|
13
13
|
from home.endpoints import home
|
14
14
|
from home.piccolo_app import APP_CONFIG
|
@@ -71,7 +71,7 @@ async def close_database_connection_pool():
|
|
71
71
|
print("Unable to connect to the database")
|
72
72
|
|
73
73
|
|
74
|
-
app =
|
74
|
+
app = Litestar(
|
75
75
|
route_handlers=[
|
76
76
|
admin,
|
77
77
|
home,
|
@@ -4,8 +4,6 @@
|
|
4
4
|
{% include '_starlette_app.py.jinja' %}
|
5
5
|
{% elif router == 'blacksheep' %}
|
6
6
|
{% include '_blacksheep_app.py.jinja' %}
|
7
|
-
{% elif router == '
|
8
|
-
{% include '
|
9
|
-
{% elif router == 'starlite' %}
|
10
|
-
{% include '_starlite_app.py.jinja' %}
|
7
|
+
{% elif router == 'litestar' %}
|
8
|
+
{% include '_litestar_app.py.jinja' %}
|
11
9
|
{% endif %}
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import os
|
2
2
|
|
3
3
|
import jinja2
|
4
|
-
from
|
4
|
+
from litestar import MediaType, Request, Response, get
|
5
5
|
|
6
6
|
ENVIRONMENT = jinja2.Environment(
|
7
7
|
loader=jinja2.FileSystemLoader(
|
@@ -19,3 +19,4 @@ def home(request: Request) -> Response:
|
|
19
19
|
media_type=MediaType.HTML,
|
20
20
|
status_code=200,
|
21
21
|
)
|
22
|
+
|
@@ -2,8 +2,6 @@
|
|
2
2
|
{% include '_starlette_endpoints.py.jinja' %}
|
3
3
|
{% elif router == 'blacksheep' %}
|
4
4
|
{% include '_blacksheep_endpoints.py.jinja' %}
|
5
|
-
{% elif router == '
|
6
|
-
{% include '
|
7
|
-
{% elif router == 'starlite' %}
|
8
|
-
{% include '_starlite_endpoints.py.jinja' %}
|
5
|
+
{% elif router == 'litestar' %}
|
6
|
+
{% include '_litestar_endpoints.py.jinja' %}
|
9
7
|
{% endif %}
|
@@ -51,12 +51,7 @@
|
|
51
51
|
<li><a href="/admin/">Admin</a></li>
|
52
52
|
<li><a href="/docs/">Swagger API</a></li>
|
53
53
|
</ul>
|
54
|
-
<h3>
|
55
|
-
<ul>
|
56
|
-
<li><a href="/admin/">Admin</a></li>
|
57
|
-
<li><a href="/docs/">Swagger API</a></li>
|
58
|
-
</ul>
|
59
|
-
<h3>Starlite</h3>
|
54
|
+
<h3>Litestar</h3>
|
60
55
|
<ul>
|
61
56
|
<li><a href="/admin/">Admin</a></li>
|
62
57
|
<li><a href="/schema/swagger">Swagger API</a></li>
|
@@ -136,8 +136,7 @@ def populate():
|
|
136
136
|
"""
|
137
137
|
for _table in reversed(TABLES):
|
138
138
|
try:
|
139
|
-
|
140
|
-
_table.alter().drop_table().run_sync()
|
139
|
+
_table.alter().drop_table(if_exists=True).run_sync()
|
141
140
|
except Exception as e:
|
142
141
|
print(e)
|
143
142
|
|
@@ -180,13 +179,30 @@ def populate():
|
|
180
179
|
ticket = Ticket(concert=concert.id, price=Decimal("50.0"))
|
181
180
|
ticket.save().run_sync()
|
182
181
|
|
183
|
-
|
184
|
-
|
182
|
+
DiscountCode.insert(
|
183
|
+
*[DiscountCode({DiscountCode.code: uuid.uuid4()}) for _ in range(5)]
|
184
|
+
).run_sync()
|
185
185
|
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
186
|
+
RecordingStudio.insert(
|
187
|
+
RecordingStudio(
|
188
|
+
{
|
189
|
+
RecordingStudio.name: "Abbey Road",
|
190
|
+
RecordingStudio.facilities: {
|
191
|
+
"restaurant": True,
|
192
|
+
"mixing_desk": True,
|
193
|
+
},
|
194
|
+
}
|
195
|
+
),
|
196
|
+
RecordingStudio(
|
197
|
+
{
|
198
|
+
RecordingStudio.name: "Electric Lady",
|
199
|
+
RecordingStudio.facilities: {
|
200
|
+
"restaurant": False,
|
201
|
+
"mixing_desk": True,
|
202
|
+
},
|
203
|
+
},
|
204
|
+
),
|
205
|
+
).run_sync()
|
190
206
|
|
191
207
|
|
192
208
|
def run(
|
piccolo/columns/base.py
CHANGED
@@ -759,6 +759,51 @@ class Column(Selectable):
|
|
759
759
|
column._alias = name
|
760
760
|
return column
|
761
761
|
|
762
|
+
def join_on(self, column: Column) -> ForeignKey:
|
763
|
+
"""
|
764
|
+
Joins are typically performed via foreign key columns. For example,
|
765
|
+
here we get the band's name and the manager's name::
|
766
|
+
|
767
|
+
class Manager(Table):
|
768
|
+
name = Varchar()
|
769
|
+
|
770
|
+
class Band(Table):
|
771
|
+
name = Varchar()
|
772
|
+
manager = ForeignKey(Manager)
|
773
|
+
|
774
|
+
>>> await Band.select(Band.name, Band.manager.name)
|
775
|
+
|
776
|
+
The ``join_on`` method lets you join tables even when foreign keys
|
777
|
+
don't exist, by joining on a column in another table.
|
778
|
+
|
779
|
+
For example, here we want to get the manager's email, but no foreign
|
780
|
+
key exists::
|
781
|
+
|
782
|
+
class Manager(Table):
|
783
|
+
name = Varchar(unique=True)
|
784
|
+
email = Varchar()
|
785
|
+
|
786
|
+
class Band(Table):
|
787
|
+
name = Varchar()
|
788
|
+
manager_name = Varchar()
|
789
|
+
|
790
|
+
>>> await Band.select(
|
791
|
+
... Band.name,
|
792
|
+
... Band.manager_name.join_on(Manager.name).email
|
793
|
+
... )
|
794
|
+
|
795
|
+
"""
|
796
|
+
from piccolo.columns.column_types import ForeignKey
|
797
|
+
|
798
|
+
virtual_foreign_key = ForeignKey(
|
799
|
+
references=column._meta.table, target_column=column
|
800
|
+
)
|
801
|
+
virtual_foreign_key._meta._name = self._meta.name
|
802
|
+
virtual_foreign_key._meta.call_chain = [*self._meta.call_chain]
|
803
|
+
virtual_foreign_key._meta._table = self._meta.table
|
804
|
+
virtual_foreign_key.set_proxy_columns()
|
805
|
+
return virtual_foreign_key
|
806
|
+
|
762
807
|
def get_default_value(self) -> t.Any:
|
763
808
|
"""
|
764
809
|
If the column has a default attribute, return it. If it's callable,
|
piccolo/columns/column_types.py
CHANGED
@@ -1784,7 +1784,7 @@ class ForeignKey(Column):
|
|
1784
1784
|
|
1785
1785
|
:param on_update:
|
1786
1786
|
Determines what the database should do when a row has it's primary key
|
1787
|
-
updated. If set to ``
|
1787
|
+
updated. If set to ``OnUpdate.cascade``, any rows referencing the
|
1788
1788
|
updated row will have their references updated to point to the new
|
1789
1789
|
primary key.
|
1790
1790
|
|
@@ -1800,7 +1800,7 @@ class ForeignKey(Column):
|
|
1800
1800
|
|
1801
1801
|
.. code-block:: python
|
1802
1802
|
|
1803
|
-
from piccolo.columns import
|
1803
|
+
from piccolo.columns import OnUpdate
|
1804
1804
|
|
1805
1805
|
class Band(Table):
|
1806
1806
|
name = ForeignKey(
|
piccolo/query/methods/select.py
CHANGED
@@ -353,8 +353,18 @@ class Select(Query[TableInstance, t.List[t.Dict[str, t.Any]]]):
|
|
353
353
|
self.columns_delegate.columns(*_columns)
|
354
354
|
return self
|
355
355
|
|
356
|
-
def distinct(
|
357
|
-
self.
|
356
|
+
def distinct(
|
357
|
+
self: Self, *, on: t.Optional[t.Sequence[Column]] = None
|
358
|
+
) -> Self:
|
359
|
+
if on is not None and self.engine_type not in (
|
360
|
+
"postgres",
|
361
|
+
"cockroach",
|
362
|
+
):
|
363
|
+
raise ValueError(
|
364
|
+
"Only Postgres and Cockroach supports DISTINCT ON"
|
365
|
+
)
|
366
|
+
|
367
|
+
self.distinct_delegate.distinct(enabled=True, on=on)
|
358
368
|
return self
|
359
369
|
|
360
370
|
def group_by(self: Self, *columns: t.Union[Column, str]) -> Self:
|
@@ -722,17 +732,22 @@ class Select(Query[TableInstance, t.List[t.Dict[str, t.Any]]]):
|
|
722
732
|
|
723
733
|
#######################################################################
|
724
734
|
|
725
|
-
|
726
|
-
"SELECT DISTINCT" if self.distinct_delegate._distinct else "SELECT"
|
727
|
-
)
|
728
|
-
query = f"{select} {columns_str} FROM {self.table._meta.tablename}"
|
735
|
+
args: t.List[t.Any] = []
|
729
736
|
|
730
|
-
|
731
|
-
query += f" {join}"
|
737
|
+
query = "SELECT"
|
732
738
|
|
733
|
-
|
739
|
+
distinct = self.distinct_delegate._distinct
|
740
|
+
if distinct:
|
741
|
+
if distinct.on:
|
742
|
+
distinct.validate_on(self.order_by_delegate._order_by)
|
734
743
|
|
735
|
-
|
744
|
+
query += "{}"
|
745
|
+
args.append(distinct.querystring)
|
746
|
+
|
747
|
+
query += f" {columns_str} FROM {self.table._meta.tablename}"
|
748
|
+
|
749
|
+
for join in joins:
|
750
|
+
query += f" {join}"
|
736
751
|
|
737
752
|
if self.as_of_delegate._as_of:
|
738
753
|
query += "{}"
|
piccolo/query/mixins.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
import asyncio
|
4
|
+
import collections.abc
|
4
5
|
import itertools
|
5
6
|
import typing as t
|
6
7
|
from dataclasses import dataclass, field
|
@@ -18,6 +19,70 @@ if t.TYPE_CHECKING: # pragma: no cover
|
|
18
19
|
from piccolo.table import Table # noqa
|
19
20
|
|
20
21
|
|
22
|
+
class DistinctOnError(ValueError):
|
23
|
+
"""
|
24
|
+
Raised when ``DISTINCT ON`` queries are malformed.
|
25
|
+
"""
|
26
|
+
|
27
|
+
pass
|
28
|
+
|
29
|
+
|
30
|
+
@dataclass
|
31
|
+
class Distinct:
|
32
|
+
__slots__ = ("enabled", "on")
|
33
|
+
|
34
|
+
enabled: bool
|
35
|
+
on: t.Optional[t.Sequence[Column]]
|
36
|
+
|
37
|
+
@property
|
38
|
+
def querystring(self) -> QueryString:
|
39
|
+
if self.enabled:
|
40
|
+
if self.on:
|
41
|
+
column_names = ", ".join(
|
42
|
+
i._meta.get_full_name(with_alias=False) for i in self.on
|
43
|
+
)
|
44
|
+
return QueryString(f" DISTINCT ON ({column_names})")
|
45
|
+
else:
|
46
|
+
return QueryString(" DISTINCT")
|
47
|
+
else:
|
48
|
+
return QueryString(" ALL")
|
49
|
+
|
50
|
+
def validate_on(self, order_by: OrderBy):
|
51
|
+
"""
|
52
|
+
When using the `on` argument, the first column must match the first
|
53
|
+
order by column.
|
54
|
+
|
55
|
+
:raises DistinctOnError:
|
56
|
+
If the columns don't match.
|
57
|
+
|
58
|
+
"""
|
59
|
+
validated = True
|
60
|
+
|
61
|
+
try:
|
62
|
+
first_order_column = order_by.order_by_items[0].columns[0]
|
63
|
+
except IndexError:
|
64
|
+
validated = False
|
65
|
+
else:
|
66
|
+
if not self.on:
|
67
|
+
validated = False
|
68
|
+
elif isinstance(first_order_column, Column) and not self.on[
|
69
|
+
0
|
70
|
+
]._equals(first_order_column):
|
71
|
+
validated = False
|
72
|
+
|
73
|
+
if not validated:
|
74
|
+
raise DistinctOnError(
|
75
|
+
"The first `order_by` column must match the first column "
|
76
|
+
"passed to `on`."
|
77
|
+
)
|
78
|
+
|
79
|
+
def __str__(self) -> str:
|
80
|
+
return self.querystring.__str__()
|
81
|
+
|
82
|
+
def copy(self) -> Distinct:
|
83
|
+
return self.__class__(enabled=self.enabled, on=self.on)
|
84
|
+
|
85
|
+
|
21
86
|
@dataclass
|
22
87
|
class Limit:
|
23
88
|
__slots__ = ("number",)
|
@@ -259,10 +324,19 @@ class AsOfDelegate:
|
|
259
324
|
@dataclass
|
260
325
|
class DistinctDelegate:
|
261
326
|
|
262
|
-
_distinct:
|
327
|
+
_distinct: Distinct = field(
|
328
|
+
default_factory=lambda: Distinct(enabled=False, on=None)
|
329
|
+
)
|
330
|
+
|
331
|
+
def distinct(
|
332
|
+
self, enabled: bool, on: t.Optional[t.Sequence[Column]] = None
|
333
|
+
):
|
334
|
+
if on and not isinstance(on, collections.abc.Sequence):
|
335
|
+
# Check a sequence is passed in, otherwise the user will get some
|
336
|
+
# unuseful errors later on.
|
337
|
+
raise ValueError("`on` must be a sequence of `Column` instances")
|
263
338
|
|
264
|
-
|
265
|
-
self._distinct = True
|
339
|
+
self._distinct = Distinct(enabled=enabled, on=on)
|
266
340
|
|
267
341
|
|
268
342
|
@dataclass
|
piccolo/testing/model_builder.py
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import json
|
2
4
|
import typing as t
|
3
5
|
from datetime import date, datetime, time, timedelta
|
@@ -5,7 +7,7 @@ from decimal import Decimal
|
|
5
7
|
from uuid import UUID
|
6
8
|
|
7
9
|
from piccolo.columns import Array, Column
|
8
|
-
from piccolo.
|
10
|
+
from piccolo.custom_types import TableInstance
|
9
11
|
from piccolo.testing.random_builder import RandomBuilder
|
10
12
|
from piccolo.utils.sync import run_sync
|
11
13
|
|
@@ -27,11 +29,11 @@ class ModelBuilder:
|
|
27
29
|
@classmethod
|
28
30
|
async def build(
|
29
31
|
cls,
|
30
|
-
table_class: t.Type[
|
32
|
+
table_class: t.Type[TableInstance],
|
31
33
|
defaults: t.Dict[t.Union[Column, str], t.Any] = None,
|
32
34
|
persist: bool = True,
|
33
35
|
minimal: bool = False,
|
34
|
-
) ->
|
36
|
+
) -> TableInstance:
|
35
37
|
"""
|
36
38
|
Build a ``Table`` instance with random data and save async.
|
37
39
|
If the ``Table`` has any foreign keys, then the related rows are also
|
@@ -78,11 +80,11 @@ class ModelBuilder:
|
|
78
80
|
@classmethod
|
79
81
|
def build_sync(
|
80
82
|
cls,
|
81
|
-
table_class: t.Type[
|
83
|
+
table_class: t.Type[TableInstance],
|
82
84
|
defaults: t.Dict[t.Union[Column, str], t.Any] = None,
|
83
85
|
persist: bool = True,
|
84
86
|
minimal: bool = False,
|
85
|
-
) ->
|
87
|
+
) -> TableInstance:
|
86
88
|
"""
|
87
89
|
A sync wrapper around :meth:`build`.
|
88
90
|
"""
|
@@ -98,11 +100,11 @@ class ModelBuilder:
|
|
98
100
|
@classmethod
|
99
101
|
async def _build(
|
100
102
|
cls,
|
101
|
-
table_class: t.Type[
|
103
|
+
table_class: t.Type[TableInstance],
|
102
104
|
defaults: t.Dict[t.Union[Column, str], t.Any] = None,
|
103
105
|
minimal: bool = False,
|
104
106
|
persist: bool = True,
|
105
|
-
) ->
|
107
|
+
) -> TableInstance:
|
106
108
|
model = table_class(_ignore_missing=True)
|
107
109
|
defaults = {} if not defaults else defaults
|
108
110
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: piccolo
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.110.0
|
4
4
|
Summary: A fast, user friendly ORM and query builder which supports asyncio.
|
5
5
|
Home-page: https://github.com/piccolo-orm/piccolo
|
6
6
|
Author: Daniel Townsend
|
@@ -144,7 +144,7 @@ Let Piccolo scaffold you an ASGI web app, using Piccolo as the ORM:
|
|
144
144
|
piccolo asgi new
|
145
145
|
```
|
146
146
|
|
147
|
-
[Starlette](https://www.starlette.io/), [FastAPI](https://fastapi.tiangolo.com/), [BlackSheep](https://www.neoteroi.dev/blacksheep/)
|
147
|
+
[Starlette](https://www.starlette.io/), [FastAPI](https://fastapi.tiangolo.com/), [BlackSheep](https://www.neoteroi.dev/blacksheep/) and [Litestar](https://litestar.dev/) are currently supported.
|
148
148
|
|
149
149
|
## Are you a Django user?
|
150
150
|
|
@@ -1,4 +1,4 @@
|
|
1
|
-
piccolo/__init__.py,sha256=
|
1
|
+
piccolo/__init__.py,sha256=zz24Vmp-MN5KVpqbveNBFX2-Th4v1ySTLtZmXtwh1cY,24
|
2
2
|
piccolo/custom_types.py,sha256=7HMQAze-5mieNLfbQ5QgbRQgR2abR7ol0qehv2SqROY,604
|
3
3
|
piccolo/main.py,sha256=2W2EXXEr-EN1PG8s8xHIWCvU7t7kT004fBChK9CZhzo,5024
|
4
4
|
piccolo/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -16,14 +16,13 @@ piccolo/apps/app/commands/templates/tables.py.jinja,sha256=revzdrvDDwe78VedBKz0z
|
|
16
16
|
piccolo/apps/asgi/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
17
17
|
piccolo/apps/asgi/piccolo_app.py,sha256=7VUvqQJbB-ScO0A62S6MiJmQL9F5DS-SdlqlDLbAblE,217
|
18
18
|
piccolo/apps/asgi/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
19
|
-
piccolo/apps/asgi/commands/new.py,sha256=
|
19
|
+
piccolo/apps/asgi/commands/new.py,sha256=bcK7xnbmCsP5yapjjvrpdZDBPOc9W_Itdz8ycUc0V4E,4100
|
20
20
|
piccolo/apps/asgi/commands/templates/app/README.md.jinja,sha256=As3gNEZt9qcRmTVkjCzNtXJ8r4-3g0fCSe7Q-P39ezI,214
|
21
21
|
piccolo/apps/asgi/commands/templates/app/_blacksheep_app.py.jinja,sha256=bgAGe0a9nWk0LAqK3VNDhPcKGqg0z8V-eIX2YmMoZLk,3117
|
22
22
|
piccolo/apps/asgi/commands/templates/app/_fastapi_app.py.jinja,sha256=cCmRVAN8gw6zfHBcLI_NxapwN7LGM5QSXM7K94imDh8,2436
|
23
|
+
piccolo/apps/asgi/commands/templates/app/_litestar_app.py.jinja,sha256=e0784WBJSsrAPmDXjVuYfmrUJWLAkQyDxoSDl80ucWQ,2622
|
23
24
|
piccolo/apps/asgi/commands/templates/app/_starlette_app.py.jinja,sha256=YvNUlJuTd4mj-pm3WQKbQq3w3x3VfDb_Wz6aQLUsORo,1271
|
24
|
-
piccolo/apps/asgi/commands/templates/app/
|
25
|
-
piccolo/apps/asgi/commands/templates/app/_xpresso_app.py.jinja,sha256=D3D3ScpItr0V4InaqBMFwy4BvnSMI2AM_6E6sKmUjJE,2855
|
26
|
-
piccolo/apps/asgi/commands/templates/app/app.py.jinja,sha256=Hsoq3xsCc3yzNFHXV3zRBxrh42J9W04qyVc7FZ7Bg4Y,387
|
25
|
+
piccolo/apps/asgi/commands/templates/app/app.py.jinja,sha256=NnpkkIFswR4SUsOrhB6x2rNrDfR_QOaGewRv_mP5zgQ,314
|
27
26
|
piccolo/apps/asgi/commands/templates/app/conftest.py.jinja,sha256=ZG1pRVMv3LhIfOsO3_08c_fF3EV4_EApuDHiIFFPJdk,497
|
28
27
|
piccolo/apps/asgi/commands/templates/app/main.py.jinja,sha256=azwXyWZGkrIbZv5bZF_4Tvbly7AXkw5yFWGCHYImGeo,421
|
29
28
|
piccolo/apps/asgi/commands/templates/app/piccolo_conf.py.jinja,sha256=f9Nb08_yipi0_mDUYrUvVoGCz7MRRS5QjCdUGBHN760,379
|
@@ -31,15 +30,14 @@ piccolo/apps/asgi/commands/templates/app/piccolo_conf_test.py.jinja,sha256=ZB32I
|
|
31
30
|
piccolo/apps/asgi/commands/templates/app/requirements.txt.jinja,sha256=gR_AvX3YH0bsjkAY28eXNy_D1YuJAvEnT1tGtVzMnnY,152
|
32
31
|
piccolo/apps/asgi/commands/templates/app/home/__init__.py.jinja,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
33
32
|
piccolo/apps/asgi/commands/templates/app/home/_blacksheep_endpoints.py.jinja,sha256=Rri_xzDkl87G5ME74qTxY25cwKIKufuzgkRsy__mNts,510
|
33
|
+
piccolo/apps/asgi/commands/templates/app/home/_litestar_endpoints.py.jinja,sha256=mk0LTygP-HOaiWYO4SDnJ3C-WZxLxRFYxUrIYwatAyc,531
|
34
34
|
piccolo/apps/asgi/commands/templates/app/home/_starlette_endpoints.py.jinja,sha256=KEjNEUKiZNBIWYAt9EgPHe4yCbkKLtlhaCBce9YI-RQ,498
|
35
|
-
piccolo/apps/asgi/commands/templates/app/home/
|
36
|
-
piccolo/apps/asgi/commands/templates/app/home/_xpresso_endpoints.py.jinja,sha256=JO9SCIL2jDAkoop8zPhdTeUt_oP-Zent2KqIt6NAPy4,402
|
37
|
-
piccolo/apps/asgi/commands/templates/app/home/endpoints.py.jinja,sha256=dgpHlA8mjmYCnNs5RiToUj3S3u6mZObOVC1VU-jbRCc,351
|
35
|
+
piccolo/apps/asgi/commands/templates/app/home/endpoints.py.jinja,sha256=OeRMBHjjxVyGIXAuSLQzl6qsWOmD7-pZcToA3V5gWx4,272
|
38
36
|
piccolo/apps/asgi/commands/templates/app/home/piccolo_app.py.jinja,sha256=4gETiW9ukTNsomeJOvrRkqPbToZ_FU0b3LsNIaEYyP8,505
|
39
37
|
piccolo/apps/asgi/commands/templates/app/home/tables.py.jinja,sha256=wk34RAsuoFn5iJ4OHlQzUqgatq6QB2G9tFE0BYkaers,197
|
40
38
|
piccolo/apps/asgi/commands/templates/app/home/piccolo_migrations/README.md,sha256=ji6UOtHvzHX-eS_qhhKTN36ZXNZ7QwtjwjdE4Qgm35A,59
|
41
39
|
piccolo/apps/asgi/commands/templates/app/home/templates/base.html.jinja_raw,sha256=3RqiNuyAap_P-xNK3uhNaQQ6rC365VzPmRqmmXSLO8o,451
|
42
|
-
piccolo/apps/asgi/commands/templates/app/home/templates/home.html.jinja_raw,sha256=
|
40
|
+
piccolo/apps/asgi/commands/templates/app/home/templates/home.html.jinja_raw,sha256=u_oXpGzR3o_eCufFPRpjD6iL1SdiixvrmOOeWp7WF10,2278
|
43
41
|
piccolo/apps/asgi/commands/templates/app/static/favicon.ico,sha256=IvcgeJHObd9kj2mNIXkJdXYxMU8OaOymyYQWnWfbtHo,7406
|
44
42
|
piccolo/apps/asgi/commands/templates/app/static/main.css,sha256=vudarPLglQ6NOgJiNeU2x0yQl0DiWScqb09QZv2wAzM,1056
|
45
43
|
piccolo/apps/fixtures/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -74,7 +72,7 @@ piccolo/apps/migrations/commands/templates/migration.py.jinja,sha256=wMC8RTIcQj3
|
|
74
72
|
piccolo/apps/playground/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
75
73
|
piccolo/apps/playground/piccolo_app.py,sha256=zs6nGxt-lgUF8nEwI0uDTNZDKQqjZaNDH8le5RqrMNE,222
|
76
74
|
piccolo/apps/playground/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
77
|
-
piccolo/apps/playground/commands/run.py,sha256=
|
75
|
+
piccolo/apps/playground/commands/run.py,sha256=PaY3ls4C0j0TnVSTY85abv6e2SuTsII0H33HlkXlZzc,7350
|
78
76
|
piccolo/apps/project/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
79
77
|
piccolo/apps/project/piccolo_app.py,sha256=mT3O0m3QcCfS0oOr3jt0QZ9TX6gUavGPjJeNn2C_fdM,220
|
80
78
|
piccolo/apps/project/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -111,9 +109,9 @@ piccolo/apps/user/piccolo_migrations/2020-06-11T21-38-55.py,sha256=JG_LFPrEljnSE
|
|
111
109
|
piccolo/apps/user/piccolo_migrations/2021-04-30T16-14-15.py,sha256=Y_Dj4ROSxjnPsRDqcnpWeyk8UpF8c80T08_O2uq-GoA,1219
|
112
110
|
piccolo/apps/user/piccolo_migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
113
111
|
piccolo/columns/__init__.py,sha256=OYhO_n9anMiU9nL-K6ATq9FhAtm8RyMpqYQ7fTVbhxI,1120
|
114
|
-
piccolo/columns/base.py,sha256=
|
112
|
+
piccolo/columns/base.py,sha256=lD3fzhpHCVlfieME2q9gCiHdpJqKNo2Te67HrjUX2c0,31355
|
115
113
|
piccolo/columns/choices.py,sha256=-HNQuk9vMmVZIPZ5PMeXGTfr23o4nzKPSAkvcG1k0y8,723
|
116
|
-
piccolo/columns/column_types.py,sha256=
|
114
|
+
piccolo/columns/column_types.py,sha256=X0nTvlc8epfcu8Q8uuIBlnHv_R_GTJ58dehC343vNgA,77278
|
117
115
|
piccolo/columns/combination.py,sha256=vMXC2dfY7pvnCFhsT71XFVyb4gdQzfRsCMaiduu04Ss,6900
|
118
116
|
piccolo/columns/indexes.py,sha256=NfNok3v_791jgDlN28KmhP9ZCjl6031BXmjxV3ovXJk,372
|
119
117
|
piccolo/columns/m2m.py,sha256=C7IKMg7ik2yE3143Gwdbx3YNB3VrZbltJAlX0XxQwAI,14067
|
@@ -143,7 +141,7 @@ piccolo/engine/postgres.py,sha256=UvGihQeUgg2MniTN5mABlSMPkBgtQQSmx9QE32uv9SA,18
|
|
143
141
|
piccolo/engine/sqlite.py,sha256=io3fBdXxXjOoSzLgP-HYGKgTDgHIRpmFa7mU6ifbqn8,21915
|
144
142
|
piccolo/query/__init__.py,sha256=WkG78nTz4Ww3rE9Pu5tZI130JMVfGnwyghhu97XFk0w,617
|
145
143
|
piccolo/query/base.py,sha256=BsrzbeuJo7k-KJn4YlRUibxITw_J7xslbgJg0eVFB8s,15124
|
146
|
-
piccolo/query/mixins.py,sha256=
|
144
|
+
piccolo/query/mixins.py,sha256=zD13SqgKq613BAcqjSTblT0u9fMyctv_AGS03E7xR0c,16585
|
147
145
|
piccolo/query/proxy.py,sha256=Hg5S6tp1EiKD899eYdDKHscFYucHdKtL3YC2GTcL2Jk,1833
|
148
146
|
piccolo/query/methods/__init__.py,sha256=_PfGUdOd6AsKq1sqXeZUHhESHE-e1cNpwFr8Lyz7QoY,421
|
149
147
|
piccolo/query/methods/alter.py,sha256=gyx4kVF4EiN4sSFjIqcKykxcKB808j1ioa2lKrLdP4Y,14935
|
@@ -158,11 +156,11 @@ piccolo/query/methods/insert.py,sha256=qmwvPysnrANjubF-F41TtBYKVdy2dHfw_lgD3C_T9
|
|
158
156
|
piccolo/query/methods/objects.py,sha256=CAsZjTzOtD6elMqc2nIK_Ra1GKu7sxHBgcDRh9NAes8,11556
|
159
157
|
piccolo/query/methods/raw.py,sha256=VhYpCB52mZk4zqFTsqK5CHKTDGskUjISXTBV7UjohmA,600
|
160
158
|
piccolo/query/methods/refresh.py,sha256=P1Eo_HYU_L7kcGM_cvDDgyLi1boCXY7Pc4tv_eDAzvc,2769
|
161
|
-
piccolo/query/methods/select.py,sha256=
|
159
|
+
piccolo/query/methods/select.py,sha256=e7BuECHITRwYJ902NvC5ZHG8tirCljeVoGWzpQYxRts,25379
|
162
160
|
piccolo/query/methods/table_exists.py,sha256=rPY20QNdJI9TvKjGyTPVvGGEuD3bDnQim8K1ZurthmU,1211
|
163
161
|
piccolo/query/methods/update.py,sha256=0hURc7PQU9NX7QQFJ1XgFJvw3nXYIrWUjE-D_7W5JV4,3625
|
164
162
|
piccolo/testing/__init__.py,sha256=pRFSqRInfx95AakOq54atmvqoB-ue073q2aR8u8zR40,83
|
165
|
-
piccolo/testing/model_builder.py,sha256=
|
163
|
+
piccolo/testing/model_builder.py,sha256=_tss3L-n-hwIaygNJ3dVmWvZxXE035h2QdDaLYJhH7c,5734
|
166
164
|
piccolo/testing/random_builder.py,sha256=o3Ebzak1AG_3nG1iIYN2ZNn5NKQTRECha4ZEubAl9yQ,2005
|
167
165
|
piccolo/utils/__init__.py,sha256=SDFFraauI9Op8dCRkreQv1dwUcab8Mi1eC-n0EwlTy8,36
|
168
166
|
piccolo/utils/dictionary.py,sha256=8vRPxgaXadDVhqihP1UxL7nUBgM6Gpe_Eu3xJq7zzGM,1886
|
@@ -307,6 +305,7 @@ tests/table/test_indexes.py,sha256=GdlPfLmvM0s2fe-4-2XSqQLYYjwsBu5dzf6o7ZvlPj4,1
|
|
307
305
|
tests/table/test_inheritance.py,sha256=s5JIo8hZN7xqOPlZ9EDkkNLo5_kWirsfCJAqaXSHn88,3034
|
308
306
|
tests/table/test_insert.py,sha256=8G7TvgZIKqKNDJR0-Ck5gTtzQa0c89OeFNaWf9wPpBk,2274
|
309
307
|
tests/table/test_join.py,sha256=tk2r5OUaay9-4U37aj2-qul1XybchBG3xr-k7K_IQh0,14705
|
308
|
+
tests/table/test_join_on.py,sha256=NhJRg_7_YQ0o2ox5mF330ZaIvmtq09Xl2lfDTwKtUng,2719
|
310
309
|
tests/table/test_metaclass.py,sha256=liJuKArpco1qb3lshSQTwRsqXXZZNgzmFoMDP9r2uHw,2637
|
311
310
|
tests/table/test_objects.py,sha256=aEv-y0xkg6UMrWo8rd098ahCI7m177AL3dwU3JNMKRY,7895
|
312
311
|
tests/table/test_output.py,sha256=BvALFil1VlWKPmlRiqlrhhiAVXcuj3E-Bg85JMqGNlQ,2984
|
@@ -314,7 +313,7 @@ tests/table/test_raw.py,sha256=AxT6qB0bEjVbOz6lmGQ9_IiDuEoVmh5c72gzyiatBwo,1683
|
|
314
313
|
tests/table/test_ref.py,sha256=eYNRnYHzNMXuMbV3B1ca5EidpIg4500q6hr1ccuVaso,269
|
315
314
|
tests/table/test_refresh.py,sha256=tZktBoUQth3S2_vou5NcKiwOJDFrhF6CIuC7YZ2janw,2694
|
316
315
|
tests/table/test_repr.py,sha256=dKdM0HRygvqjmSPz-l95SJQXQ-O18MHUGrcIzzYKrsQ,411
|
317
|
-
tests/table/test_select.py,sha256=
|
316
|
+
tests/table/test_select.py,sha256=0BdI_gVHIt2eyjWWojFfoLuwL9h5LzCpUAJQzvPl-Zs,39683
|
318
317
|
tests/table/test_str.py,sha256=eztWNULcjARR1fr9X5n4tojhDNgDfatVyNHwuYrzHAo,1731
|
319
318
|
tests/table/test_table_exists.py,sha256=9Qqwbg6Q3OfasSH4FUqD3z99ExvJqpHSu0pYu7aa998,375
|
320
319
|
tests/table/test_update.py,sha256=ZEkIDgQ9PHcAn58dlN4WIHRJm0RENi6AVDsmgLX9Qlw,20342
|
@@ -341,9 +340,9 @@ tests/utils/test_sql_values.py,sha256=vzxRmy16FfLZPH-sAQexBvsF9MXB8n4smr14qoEOS5
|
|
341
340
|
tests/utils/test_sync.py,sha256=9ytVo56y2vPQePvTeIi9lHIouEhWJbodl1TmzkGFrSo,799
|
342
341
|
tests/utils/test_table_reflection.py,sha256=SIzuat-IpcVj1GCFyOWKShI8YkhdOPPFH7qVrvfyPNE,3794
|
343
342
|
tests/utils/test_warnings.py,sha256=NvSC_cvJ6uZcwAGf1m-hLzETXCqprXELL8zg3TNLVMw,269
|
344
|
-
piccolo-0.
|
345
|
-
piccolo-0.
|
346
|
-
piccolo-0.
|
347
|
-
piccolo-0.
|
348
|
-
piccolo-0.
|
349
|
-
piccolo-0.
|
343
|
+
piccolo-0.110.0.dist-info/LICENSE,sha256=zFIpi-16uIJ420UMIG75NU0JbDBykvrdnXcj5U_EYBI,1059
|
344
|
+
piccolo-0.110.0.dist-info/METADATA,sha256=zYj2Bfz5piDmp3d4zRoKWGva_r4JNZ7dPVLAEpGWvfw,5113
|
345
|
+
piccolo-0.110.0.dist-info/WHEEL,sha256=00yskusixUoUt5ob_CiUp6LsnN5lqzTJpoqOFg_FVIc,92
|
346
|
+
piccolo-0.110.0.dist-info/entry_points.txt,sha256=zYhu-YNtMlh2N_8wptCS8YWKOgc81UPL3Ji5gly8ouc,47
|
347
|
+
piccolo-0.110.0.dist-info/top_level.txt,sha256=-SR74VGbk43VoPy1HH-mHm97yoGukLK87HE5kdBW6qM,24
|
348
|
+
piccolo-0.110.0.dist-info/RECORD,,
|
@@ -0,0 +1,107 @@
|
|
1
|
+
from unittest import TestCase
|
2
|
+
|
3
|
+
from piccolo.columns import Varchar
|
4
|
+
from piccolo.table import Table
|
5
|
+
|
6
|
+
|
7
|
+
class Manager(Table):
|
8
|
+
name = Varchar(unique=True)
|
9
|
+
email = Varchar(unique=True)
|
10
|
+
|
11
|
+
|
12
|
+
class Band(Table):
|
13
|
+
name = Varchar(unique=True)
|
14
|
+
manager_name = Varchar()
|
15
|
+
|
16
|
+
|
17
|
+
class Concert(Table):
|
18
|
+
title = Varchar()
|
19
|
+
band_name = Varchar()
|
20
|
+
|
21
|
+
|
22
|
+
class TestJoinOn(TestCase):
|
23
|
+
|
24
|
+
tables = [Manager, Band, Concert]
|
25
|
+
|
26
|
+
def setUp(self):
|
27
|
+
for table in self.tables:
|
28
|
+
table.create_table().run_sync()
|
29
|
+
|
30
|
+
Manager.insert(
|
31
|
+
Manager(name="Guido", email="guido@example.com"),
|
32
|
+
Manager(name="Maz", email="maz@example.com"),
|
33
|
+
Manager(name="Graydon", email="graydon@example.com"),
|
34
|
+
).run_sync()
|
35
|
+
|
36
|
+
Band.insert(
|
37
|
+
Band(name="Pythonistas", manager_name="Guido"),
|
38
|
+
Band(name="Rustaceans", manager_name="Graydon"),
|
39
|
+
).run_sync()
|
40
|
+
|
41
|
+
Concert.insert(
|
42
|
+
Concert(
|
43
|
+
title="Rockfest",
|
44
|
+
band_name="Pythonistas",
|
45
|
+
),
|
46
|
+
).run_sync()
|
47
|
+
|
48
|
+
def tearDown(self):
|
49
|
+
for table in self.tables:
|
50
|
+
table.alter().drop_table().run_sync()
|
51
|
+
|
52
|
+
def test_join_on(self):
|
53
|
+
"""
|
54
|
+
Do a simple join between two tables.
|
55
|
+
"""
|
56
|
+
query = Band.select(
|
57
|
+
Band.name,
|
58
|
+
Band.manager_name,
|
59
|
+
Band.manager_name.join_on(Manager.name).email.as_alias(
|
60
|
+
"manager_email"
|
61
|
+
),
|
62
|
+
).order_by(Band.id)
|
63
|
+
|
64
|
+
response = query.run_sync()
|
65
|
+
|
66
|
+
self.assertListEqual(
|
67
|
+
response,
|
68
|
+
[
|
69
|
+
{
|
70
|
+
"name": "Pythonistas",
|
71
|
+
"manager_name": "Guido",
|
72
|
+
"manager_email": "guido@example.com",
|
73
|
+
},
|
74
|
+
{
|
75
|
+
"name": "Rustaceans",
|
76
|
+
"manager_name": "Graydon",
|
77
|
+
"manager_email": "graydon@example.com",
|
78
|
+
},
|
79
|
+
],
|
80
|
+
)
|
81
|
+
|
82
|
+
def test_deeper_join(self):
|
83
|
+
"""
|
84
|
+
Do a join between three tables.
|
85
|
+
"""
|
86
|
+
response = (
|
87
|
+
Concert.select(
|
88
|
+
Concert.title,
|
89
|
+
Concert.band_name,
|
90
|
+
Concert.band_name.join_on(Band.name)
|
91
|
+
.manager_name.join_on(Manager.name)
|
92
|
+
.email.as_alias("manager_email"),
|
93
|
+
)
|
94
|
+
.order_by(Concert.id)
|
95
|
+
.run_sync()
|
96
|
+
)
|
97
|
+
|
98
|
+
self.assertListEqual(
|
99
|
+
response,
|
100
|
+
[
|
101
|
+
{
|
102
|
+
"title": "Rockfest",
|
103
|
+
"band_name": "Pythonistas",
|
104
|
+
"manager_email": "guido@example.com",
|
105
|
+
}
|
106
|
+
],
|
107
|
+
)
|
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(ValueError) as manager:
|
1368
|
+
Album.select().distinct(on=[Album.band])
|
1369
|
+
|
1370
|
+
self.assertEqual(
|
1371
|
+
manager.exception.__str__(),
|
1372
|
+
"Only Postgres and Cockroach supports 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
|