piccolo 0.109.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 CHANGED
@@ -1 +1 @@
1
- __VERSION__ = "0.109.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", "xpresso", "starlite"]
13
+ ROUTERS = ["starlette", "fastapi", "blacksheep", "litestar"]
14
14
  ROUTER_DEPENDENCIES = {
15
- "starlite": ["starlite>=1.46.0"],
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 starlite import Starlite, asgi, delete, get, patch, post
6
- from starlite.config.static_files import StaticFilesConfig
7
- from starlite.config.template import TemplateConfig
8
- from starlite.contrib.jinja import JinjaTemplateEngine
9
- from starlite.exceptions import NotFoundException
10
- from starlite.plugins.piccolo_orm import PiccoloORMPlugin
11
- from starlite.types import Receive, Scope, Send
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 = Starlite(
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 == 'xpresso' %}
8
- {% include '_xpresso_app.py.jinja' %}
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 starlite import MediaType, Request, Response, get
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 == 'xpresso' %}
6
- {% include '_xpresso_endpoints.py.jinja' %}
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>Xpresso</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
- if _table.table_exists().run_sync():
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
- discount_code = DiscountCode(code=uuid.uuid4())
184
- discount_code.save().run_sync()
182
+ DiscountCode.insert(
183
+ *[DiscountCode({DiscountCode.code: uuid.uuid4()}) for _ in range(5)]
184
+ ).run_sync()
185
185
 
186
- recording_studio = RecordingStudio(
187
- name="Abbey Road", facilities={"restaurant": True, "mixing_desk": True}
188
- )
189
- recording_studio.save().run_sync()
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(
@@ -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 ``OnDelete.cascade``, any rows referencing the
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 OnDelete
1803
+ from piccolo.columns import OnUpdate
1804
1804
 
1805
1805
  class Band(Table):
1806
1806
  name = ForeignKey(
@@ -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(self: Self) -> Self:
357
- self.distinct_delegate.distinct()
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
- select = (
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
- for join in joins:
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
- args: t.List[t.Any] = []
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: bool = False
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
- def distinct(self):
265
- self._distinct = True
339
+ self._distinct = Distinct(enabled=enabled, on=on)
266
340
 
267
341
 
268
342
  @dataclass
@@ -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.table import Table
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[Table],
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
- ) -> Table:
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[Table],
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
- ) -> Table:
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[Table],
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
- ) -> Table:
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.109.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/), [Xpresso](https://xpresso-api.dev/) and [Starlite](https://starlite-api.github.io/starlite/) are currently supported.
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=0KbSmIu_5PgqXUdmc7nIf-QktxNlQI_QsU4eiZclOJ8,24
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=yuGOGy6bJtqyBlJeaegajWyUqtuN1A3mPXXQhSU6JfE,4160
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/_starlite_app.py.jinja,sha256=RUtYfORvmV5gyKZVvBuR07R9Rf9Ai9fH1c85NYAAtgc,2636
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/_starlite_endpoints.py.jinja,sha256=D8dT3-hSGYcmCdxIRi-F-YJVa3EQdcbuDo6NZZjbySI,530
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=G7M_jt0ueeW0W-zkt35pFL55tpNtHOaeIuZKHBaD61A,2453
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=ee0pbj_panKndmQjyzy_1P1Gp850IITdEcm2nFpk8Nk,6949
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
@@ -113,7 +111,7 @@ piccolo/apps/user/piccolo_migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeu
113
111
  piccolo/columns/__init__.py,sha256=OYhO_n9anMiU9nL-K6ATq9FhAtm8RyMpqYQ7fTVbhxI,1120
114
112
  piccolo/columns/base.py,sha256=lD3fzhpHCVlfieME2q9gCiHdpJqKNo2Te67HrjUX2c0,31355
115
113
  piccolo/columns/choices.py,sha256=-HNQuk9vMmVZIPZ5PMeXGTfr23o4nzKPSAkvcG1k0y8,723
116
- piccolo/columns/column_types.py,sha256=P1zPKkDoXWfI-Qsc7CiAcYF6jB-SbqRPPe7kK5EVsq0,77278
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=_JcvWZ-IQCvIyLsW40VGDItSOH48BRcm-Zd-r1Un71Q,14442
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=xlHjDnTMsccpQKM6jdDJiVv6rv8o0-R8t4FCG__3-gA,24997
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=6-s4rzLKj6f0NldrV2L2JQlbqC7mJMn-5mNoAtUwU34,5635
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
@@ -315,7 +313,7 @@ tests/table/test_raw.py,sha256=AxT6qB0bEjVbOz6lmGQ9_IiDuEoVmh5c72gzyiatBwo,1683
315
313
  tests/table/test_ref.py,sha256=eYNRnYHzNMXuMbV3B1ca5EidpIg4500q6hr1ccuVaso,269
316
314
  tests/table/test_refresh.py,sha256=tZktBoUQth3S2_vou5NcKiwOJDFrhF6CIuC7YZ2janw,2694
317
315
  tests/table/test_repr.py,sha256=dKdM0HRygvqjmSPz-l95SJQXQ-O18MHUGrcIzzYKrsQ,411
318
- tests/table/test_select.py,sha256=Dcw-4hbLDv2OvQJ12qFibKJ5CmslUdfQNWds_VuPFhc,34824
316
+ tests/table/test_select.py,sha256=0BdI_gVHIt2eyjWWojFfoLuwL9h5LzCpUAJQzvPl-Zs,39683
319
317
  tests/table/test_str.py,sha256=eztWNULcjARR1fr9X5n4tojhDNgDfatVyNHwuYrzHAo,1731
320
318
  tests/table/test_table_exists.py,sha256=9Qqwbg6Q3OfasSH4FUqD3z99ExvJqpHSu0pYu7aa998,375
321
319
  tests/table/test_update.py,sha256=ZEkIDgQ9PHcAn58dlN4WIHRJm0RENi6AVDsmgLX9Qlw,20342
@@ -342,9 +340,9 @@ tests/utils/test_sql_values.py,sha256=vzxRmy16FfLZPH-sAQexBvsF9MXB8n4smr14qoEOS5
342
340
  tests/utils/test_sync.py,sha256=9ytVo56y2vPQePvTeIi9lHIouEhWJbodl1TmzkGFrSo,799
343
341
  tests/utils/test_table_reflection.py,sha256=SIzuat-IpcVj1GCFyOWKShI8YkhdOPPFH7qVrvfyPNE,3794
344
342
  tests/utils/test_warnings.py,sha256=NvSC_cvJ6uZcwAGf1m-hLzETXCqprXELL8zg3TNLVMw,269
345
- piccolo-0.109.0.dist-info/LICENSE,sha256=zFIpi-16uIJ420UMIG75NU0JbDBykvrdnXcj5U_EYBI,1059
346
- piccolo-0.109.0.dist-info/METADATA,sha256=7uqM1YtjsbxK6bGP66e8Yf1vbadKg2Edtvsqoz5j0SU,5169
347
- piccolo-0.109.0.dist-info/WHEEL,sha256=00yskusixUoUt5ob_CiUp6LsnN5lqzTJpoqOFg_FVIc,92
348
- piccolo-0.109.0.dist-info/entry_points.txt,sha256=zYhu-YNtMlh2N_8wptCS8YWKOgc81UPL3Ji5gly8ouc,47
349
- piccolo-0.109.0.dist-info/top_level.txt,sha256=-SR74VGbk43VoPy1HH-mHm97yoGukLK87HE5kdBW6qM,24
350
- piccolo-0.109.0.dist-info/RECORD,,
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,,
@@ -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(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)