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 CHANGED
@@ -1 +1 @@
1
- __VERSION__ = "0.109.0"
1
+ __VERSION__ = "0.111.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(
@@ -2,9 +2,16 @@ from __future__ import annotations
2
2
 
3
3
  import typing as t
4
4
 
5
- from piccolo.custom_types import TableInstance
5
+ from typing_extensions import Literal
6
+
7
+ from piccolo.custom_types import Combinable, TableInstance
6
8
  from piccolo.query.base import Query
7
- from piccolo.query.mixins import AddDelegate, ReturningDelegate
9
+ from piccolo.query.mixins import (
10
+ AddDelegate,
11
+ OnConflictAction,
12
+ OnConflictDelegate,
13
+ ReturningDelegate,
14
+ )
8
15
  from piccolo.querystring import QueryString
9
16
 
10
17
  if t.TYPE_CHECKING: # pragma: no cover
@@ -15,7 +22,7 @@ if t.TYPE_CHECKING: # pragma: no cover
15
22
  class Insert(
16
23
  t.Generic[TableInstance], Query[TableInstance, t.List[t.Dict[str, t.Any]]]
17
24
  ):
18
- __slots__ = ("add_delegate", "returning_delegate")
25
+ __slots__ = ("add_delegate", "on_conflict_delegate", "returning_delegate")
19
26
 
20
27
  def __init__(
21
28
  self, table: t.Type[TableInstance], *instances: TableInstance, **kwargs
@@ -23,6 +30,7 @@ class Insert(
23
30
  super().__init__(table, **kwargs)
24
31
  self.add_delegate = AddDelegate()
25
32
  self.returning_delegate = ReturningDelegate()
33
+ self.on_conflict_delegate = OnConflictDelegate()
26
34
  self.add(*instances)
27
35
 
28
36
  ###########################################################################
@@ -36,6 +44,43 @@ class Insert(
36
44
  self.returning_delegate.returning(columns)
37
45
  return self
38
46
 
47
+ def on_conflict(
48
+ self: Self,
49
+ target: t.Optional[t.Union[str, Column, t.Tuple[Column, ...]]] = None,
50
+ action: t.Union[
51
+ OnConflictAction, Literal["DO NOTHING", "DO UPDATE"]
52
+ ] = OnConflictAction.do_nothing,
53
+ values: t.Optional[
54
+ t.Sequence[t.Union[Column, t.Tuple[Column, t.Any]]]
55
+ ] = None,
56
+ where: t.Optional[Combinable] = None,
57
+ ) -> Self:
58
+ if (
59
+ self.engine_type == "sqlite"
60
+ and self.table._meta.db.get_version_sync() < 3.24
61
+ ):
62
+ raise NotImplementedError(
63
+ "SQLite versions lower than 3.24 don't support ON CONFLICT"
64
+ )
65
+
66
+ if (
67
+ self.engine_type in ("postgres", "cockroach")
68
+ and len(self.on_conflict_delegate._on_conflict.on_conflict_items)
69
+ == 1
70
+ ):
71
+ raise NotImplementedError(
72
+ "Postgres and Cockroach only support a single ON CONFLICT "
73
+ "clause."
74
+ )
75
+
76
+ self.on_conflict_delegate.on_conflict(
77
+ target=target,
78
+ action=action,
79
+ values=values,
80
+ where=where,
81
+ )
82
+ return self
83
+
39
84
  ###########################################################################
40
85
 
41
86
  def _raw_response_callback(self, results):
@@ -70,16 +115,27 @@ class Insert(
70
115
 
71
116
  engine_type = self.engine_type
72
117
 
118
+ on_conflict = self.on_conflict_delegate._on_conflict
119
+ if on_conflict.on_conflict_items:
120
+ querystring = QueryString(
121
+ "{}{}",
122
+ querystring,
123
+ on_conflict.querystring,
124
+ query_type="insert",
125
+ table=self.table,
126
+ )
127
+
73
128
  if engine_type in ("postgres", "cockroach") or (
74
129
  engine_type == "sqlite"
75
130
  and self.table._meta.db.get_version_sync() >= 3.35
76
131
  ):
77
- if self.returning_delegate._returning:
132
+ returning = self.returning_delegate._returning
133
+ if returning:
78
134
  return [
79
135
  QueryString(
80
136
  "{}{}",
81
137
  querystring,
82
- self.returning_delegate._returning.querystring,
138
+ returning.querystring,
83
139
  query_type="insert",
84
140
  table=self.table,
85
141
  )
@@ -230,6 +230,8 @@ class Objects(
230
230
  return self
231
231
 
232
232
  def as_of(self, interval: str = "-1s") -> Objects:
233
+ if self.engine_type != "cockroach":
234
+ raise NotImplementedError("Only CockroachDB supports AS OF")
233
235
  self.as_of_delegate.as_of(interval)
234
236
  return self
235
237
 
@@ -353,8 +353,13 @@ 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 == "sqlite":
360
+ raise NotImplementedError("SQLite doesn't support DISTINCT ON")
361
+
362
+ self.distinct_delegate.distinct(enabled=True, on=on)
358
363
  return self
359
364
 
360
365
  def group_by(self: Self, *columns: t.Union[Column, str]) -> Self:
@@ -367,6 +372,9 @@ class Select(Query[TableInstance, t.List[t.Dict[str, t.Any]]]):
367
372
  return self
368
373
 
369
374
  def as_of(self: Self, interval: str = "-1s") -> Self:
375
+ if self.engine_type != "cockroach":
376
+ raise NotImplementedError("Only CockroachDB supports AS OF")
377
+
370
378
  self.as_of_delegate.as_of(interval)
371
379
  return self
372
380
 
@@ -722,17 +730,22 @@ class Select(Query[TableInstance, t.List[t.Dict[str, t.Any]]]):
722
730
 
723
731
  #######################################################################
724
732
 
725
- select = (
726
- "SELECT DISTINCT" if self.distinct_delegate._distinct else "SELECT"
727
- )
728
- query = f"{select} {columns_str} FROM {self.table._meta.tablename}"
733
+ args: t.List[t.Any] = []
729
734
 
730
- for join in joins:
731
- query += f" {join}"
735
+ query = "SELECT"
732
736
 
733
- #######################################################################
737
+ distinct = self.distinct_delegate._distinct
738
+ if distinct:
739
+ if distinct.on:
740
+ distinct.validate_on(self.order_by_delegate._order_by)
734
741
 
735
- args: t.List[t.Any] = []
742
+ query += "{}"
743
+ args.append(distinct.querystring)
744
+
745
+ query += f" {columns_str} FROM {self.table._meta.tablename}"
746
+
747
+ for join in joins:
748
+ query += f" {join}"
736
749
 
737
750
  if self.as_of_delegate._as_of:
738
751
  query += "{}"
piccolo/query/mixins.py CHANGED
@@ -1,11 +1,14 @@
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
7
8
  from enum import Enum, auto
8
9
 
10
+ from typing_extensions import Literal
11
+
9
12
  from piccolo.columns import And, Column, Or, Where
10
13
  from piccolo.columns.column_types import ForeignKey
11
14
  from piccolo.custom_types import Combinable
@@ -18,6 +21,70 @@ if t.TYPE_CHECKING: # pragma: no cover
18
21
  from piccolo.table import Table # noqa
19
22
 
20
23
 
24
+ class DistinctOnError(ValueError):
25
+ """
26
+ Raised when ``DISTINCT ON`` queries are malformed.
27
+ """
28
+
29
+ pass
30
+
31
+
32
+ @dataclass
33
+ class Distinct:
34
+ __slots__ = ("enabled", "on")
35
+
36
+ enabled: bool
37
+ on: t.Optional[t.Sequence[Column]]
38
+
39
+ @property
40
+ def querystring(self) -> QueryString:
41
+ if self.enabled:
42
+ if self.on:
43
+ column_names = ", ".join(
44
+ i._meta.get_full_name(with_alias=False) for i in self.on
45
+ )
46
+ return QueryString(f" DISTINCT ON ({column_names})")
47
+ else:
48
+ return QueryString(" DISTINCT")
49
+ else:
50
+ return QueryString(" ALL")
51
+
52
+ def validate_on(self, order_by: OrderBy):
53
+ """
54
+ When using the `on` argument, the first column must match the first
55
+ order by column.
56
+
57
+ :raises DistinctOnError:
58
+ If the columns don't match.
59
+
60
+ """
61
+ validated = True
62
+
63
+ try:
64
+ first_order_column = order_by.order_by_items[0].columns[0]
65
+ except IndexError:
66
+ validated = False
67
+ else:
68
+ if not self.on:
69
+ validated = False
70
+ elif isinstance(first_order_column, Column) and not self.on[
71
+ 0
72
+ ]._equals(first_order_column):
73
+ validated = False
74
+
75
+ if not validated:
76
+ raise DistinctOnError(
77
+ "The first `order_by` column must match the first column "
78
+ "passed to `on`."
79
+ )
80
+
81
+ def __str__(self) -> str:
82
+ return self.querystring.__str__()
83
+
84
+ def copy(self) -> Distinct:
85
+ return self.__class__(enabled=self.enabled, on=self.on)
86
+
87
+
21
88
  @dataclass
22
89
  class Limit:
23
90
  __slots__ = ("number",)
@@ -259,10 +326,19 @@ class AsOfDelegate:
259
326
  @dataclass
260
327
  class DistinctDelegate:
261
328
 
262
- _distinct: bool = False
329
+ _distinct: Distinct = field(
330
+ default_factory=lambda: Distinct(enabled=False, on=None)
331
+ )
332
+
333
+ def distinct(
334
+ self, enabled: bool, on: t.Optional[t.Sequence[Column]] = None
335
+ ):
336
+ if on and not isinstance(on, collections.abc.Sequence):
337
+ # Check a sequence is passed in, otherwise the user will get some
338
+ # unuseful errors later on.
339
+ raise ValueError("`on` must be a sequence of `Column` instances")
263
340
 
264
- def distinct(self):
265
- self._distinct = True
341
+ self._distinct = Distinct(enabled=enabled, on=on)
266
342
 
267
343
 
268
344
  @dataclass
@@ -507,9 +583,10 @@ class OffsetDelegate:
507
583
 
508
584
  Typically used in conjunction with order_by and limit.
509
585
 
510
- Example usage:
586
+ Example usage::
587
+
588
+ .offset(100)
511
589
 
512
- .offset(100)
513
590
  """
514
591
 
515
592
  _offset: t.Optional[Offset] = None
@@ -539,12 +616,173 @@ class GroupBy:
539
616
  @dataclass
540
617
  class GroupByDelegate:
541
618
  """
542
- Used to group results - needed when doing aggregation.
619
+ Used to group results - needed when doing aggregation::
620
+
621
+ .group_by(Band.name)
543
622
 
544
- .group_by(Band.name)
545
623
  """
546
624
 
547
625
  _group_by: t.Optional[GroupBy] = None
548
626
 
549
627
  def group_by(self, *columns: Column):
550
628
  self._group_by = GroupBy(columns=columns)
629
+
630
+
631
+ class OnConflictAction(str, Enum):
632
+ """
633
+ Specify which action to take on conflict.
634
+ """
635
+
636
+ do_nothing = "DO NOTHING"
637
+ do_update = "DO UPDATE"
638
+
639
+
640
+ @dataclass
641
+ class OnConflictItem:
642
+ target: t.Optional[t.Union[str, Column, t.Tuple[Column, ...]]] = None
643
+ action: t.Optional[OnConflictAction] = None
644
+ values: t.Optional[
645
+ t.Sequence[t.Union[Column, t.Tuple[Column, t.Any]]]
646
+ ] = None
647
+ where: t.Optional[Combinable] = None
648
+
649
+ @property
650
+ def target_string(self) -> str:
651
+ target = self.target
652
+ assert target
653
+
654
+ def to_string(value) -> str:
655
+ if isinstance(value, Column):
656
+ return f'"{value._meta.db_column_name}"'
657
+ else:
658
+ raise ValueError("OnConflict.target isn't a valid type")
659
+
660
+ if isinstance(target, str):
661
+ return f'ON CONSTRAINT "{target}"'
662
+ elif isinstance(target, Column):
663
+ return f"({to_string(target)})"
664
+ elif isinstance(target, tuple):
665
+ columns_str = ", ".join([to_string(i) for i in target])
666
+ return f"({columns_str})"
667
+ else:
668
+ raise ValueError("OnConflict.target isn't a valid type")
669
+
670
+ @property
671
+ def action_string(self) -> QueryString:
672
+ action = self.action
673
+ if isinstance(action, OnConflictAction):
674
+ if action == OnConflictAction.do_nothing:
675
+ return QueryString(OnConflictAction.do_nothing.value)
676
+ elif action == OnConflictAction.do_update:
677
+ values = []
678
+ query = f"{OnConflictAction.do_update.value} SET"
679
+
680
+ if not self.values:
681
+ raise ValueError("No values specified for `on conflict`")
682
+
683
+ for value in self.values:
684
+ if isinstance(value, Column):
685
+ column_name = value._meta.db_column_name
686
+ query += f' "{column_name}"=EXCLUDED."{column_name}",'
687
+ elif isinstance(value, tuple):
688
+ column = value[0]
689
+ value_ = value[1]
690
+ if isinstance(column, Column):
691
+ column_name = column._meta.db_column_name
692
+ else:
693
+ raise ValueError("Unsupported column type")
694
+
695
+ query += f' "{column_name}"={{}},'
696
+ values.append(value_)
697
+
698
+ return QueryString(query.rstrip(","), *values)
699
+
700
+ raise ValueError("OnConflict.action isn't a valid type")
701
+
702
+ @property
703
+ def querystring(self) -> QueryString:
704
+ query = " ON CONFLICT"
705
+ values = []
706
+
707
+ if self.target:
708
+ query += f" {self.target_string}"
709
+
710
+ if self.action:
711
+ query += " {}"
712
+ values.append(self.action_string)
713
+
714
+ if self.where:
715
+ query += " WHERE {}"
716
+ values.append(self.where.querystring)
717
+
718
+ return QueryString(query, *values)
719
+
720
+ def __str__(self) -> str:
721
+ return self.querystring.__str__()
722
+
723
+
724
+ @dataclass
725
+ class OnConflict:
726
+ """
727
+ Multiple `ON CONFLICT` statements are allowed - which is why we have this
728
+ parent class.
729
+ """
730
+
731
+ on_conflict_items: t.List[OnConflictItem] = field(default_factory=list)
732
+
733
+ @property
734
+ def querystring(self) -> QueryString:
735
+ query = "".join("{}" for i in self.on_conflict_items)
736
+ return QueryString(
737
+ query, *[i.querystring for i in self.on_conflict_items]
738
+ )
739
+
740
+ def __str__(self) -> str:
741
+ return self.querystring.__str__()
742
+
743
+
744
+ @dataclass
745
+ class OnConflictDelegate:
746
+ """
747
+ Used with insert queries to specify what to do when a query fails due to
748
+ a constraint::
749
+
750
+ .on_conflict(action='DO NOTHING')
751
+
752
+ .on_conflict(action='DO UPDATE', values=[Band.popularity])
753
+
754
+ .on_conflict(action='DO UPDATE', values=[(Band.popularity, 1)])
755
+
756
+ """
757
+
758
+ _on_conflict: OnConflict = field(default_factory=OnConflict)
759
+
760
+ def on_conflict(
761
+ self,
762
+ target: t.Optional[t.Union[str, Column, t.Tuple[Column, ...]]] = None,
763
+ action: t.Union[
764
+ OnConflictAction, Literal["DO NOTHING", "DO UPDATE"]
765
+ ] = OnConflictAction.do_nothing,
766
+ values: t.Optional[
767
+ t.Sequence[t.Union[Column, t.Tuple[Column, t.Any]]]
768
+ ] = None,
769
+ where: t.Optional[Combinable] = None,
770
+ ):
771
+ action_: OnConflictAction
772
+ if isinstance(action, OnConflictAction):
773
+ action_ = action
774
+ elif isinstance(action, str):
775
+ action_ = OnConflictAction(action.upper())
776
+ else:
777
+ raise ValueError("Unrecognised `on conflict` action.")
778
+
779
+ if where and action_ == OnConflictAction.do_nothing:
780
+ raise ValueError(
781
+ "The `where` option can only be used with DO NOTHING."
782
+ )
783
+
784
+ self._on_conflict.on_conflict_items.append(
785
+ OnConflictItem(
786
+ target=target, action=action_, values=values, where=where
787
+ )
788
+ )