piccolo 1.10.0__py3-none-any.whl → 1.12.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/templates/app/_fastapi_app.py.jinja +35 -30
- piccolo/apps/asgi/commands/templates/app/_starlette_app.py.jinja +29 -21
- piccolo/apps/migrations/auto/migration_manager.py +1 -0
- piccolo/apps/migrations/commands/forwards.py +8 -7
- piccolo/apps/playground/commands/run.py +11 -0
- piccolo/columns/column_types.py +83 -35
- piccolo/query/functions/__init__.py +11 -1
- piccolo/query/functions/datetime.py +260 -0
- piccolo/query/functions/string.py +45 -0
- {piccolo-1.10.0.dist-info → piccolo-1.12.0.dist-info}/METADATA +15 -15
- {piccolo-1.10.0.dist-info → piccolo-1.12.0.dist-info}/RECORD +42 -40
- {piccolo-1.10.0.dist-info → piccolo-1.12.0.dist-info}/WHEEL +1 -1
- tests/apps/migrations/commands/test_forwards_backwards.py +32 -1
- tests/columns/test_array.py +3 -7
- tests/columns/test_bigint.py +3 -9
- tests/columns/test_boolean.py +3 -7
- tests/columns/test_bytea.py +5 -14
- tests/columns/test_choices.py +5 -14
- tests/columns/test_date.py +5 -13
- tests/columns/test_double_precision.py +3 -8
- tests/columns/test_interval.py +5 -13
- tests/columns/test_json.py +9 -26
- tests/columns/test_jsonb.py +3 -11
- tests/columns/test_numeric.py +3 -7
- tests/columns/test_primary_key.py +11 -33
- tests/columns/test_readable.py +5 -7
- tests/columns/test_real.py +3 -8
- tests/columns/test_reserved_column_names.py +3 -8
- tests/columns/test_smallint.py +3 -8
- tests/columns/test_time.py +5 -14
- tests/columns/test_timestamp.py +5 -13
- tests/columns/test_timestamptz.py +5 -13
- tests/columns/test_uuid.py +3 -7
- tests/columns/test_varchar.py +3 -9
- tests/query/functions/base.py +2 -15
- tests/query/functions/test_datetime.py +112 -0
- tests/query/functions/test_math.py +2 -3
- tests/query/functions/test_string.py +34 -2
- {piccolo-1.10.0.dist-info → piccolo-1.12.0.dist-info}/LICENSE +0 -0
- {piccolo-1.10.0.dist-info → piccolo-1.12.0.dist-info}/entry_points.txt +0 -0
- {piccolo-1.10.0.dist-info → piccolo-1.12.0.dist-info}/top_level.txt +0 -0
piccolo/__init__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__VERSION__ = "1.
|
1
|
+
__VERSION__ = "1.12.0"
|
@@ -1,11 +1,12 @@
|
|
1
1
|
import typing as t
|
2
|
+
from contextlib import asynccontextmanager
|
2
3
|
|
3
4
|
from fastapi import FastAPI
|
4
5
|
from fastapi.responses import JSONResponse
|
6
|
+
from piccolo.engine import engine_finder
|
5
7
|
from piccolo_admin.endpoints import create_admin
|
6
8
|
from piccolo_api.crud.serializers import create_pydantic_model
|
7
|
-
from
|
8
|
-
from starlette.routing import Route, Mount
|
9
|
+
from starlette.routing import Mount, Route
|
9
10
|
from starlette.staticfiles import StaticFiles
|
10
11
|
|
11
12
|
from home.endpoints import HomeEndpoint
|
@@ -13,6 +14,29 @@ from home.piccolo_app import APP_CONFIG
|
|
13
14
|
from home.tables import Task
|
14
15
|
|
15
16
|
|
17
|
+
async def open_database_connection_pool():
|
18
|
+
try:
|
19
|
+
engine = engine_finder()
|
20
|
+
await engine.start_connection_pool()
|
21
|
+
except Exception:
|
22
|
+
print("Unable to connect to the database")
|
23
|
+
|
24
|
+
|
25
|
+
async def close_database_connection_pool():
|
26
|
+
try:
|
27
|
+
engine = engine_finder()
|
28
|
+
await engine.close_connection_pool()
|
29
|
+
except Exception:
|
30
|
+
print("Unable to connect to the database")
|
31
|
+
|
32
|
+
|
33
|
+
@asynccontextmanager
|
34
|
+
async def lifespan(app: FastAPI):
|
35
|
+
await open_database_connection_pool()
|
36
|
+
yield
|
37
|
+
await close_database_connection_pool()
|
38
|
+
|
39
|
+
|
16
40
|
app = FastAPI(
|
17
41
|
routes=[
|
18
42
|
Route("/", HomeEndpoint),
|
@@ -22,21 +46,20 @@ app = FastAPI(
|
|
22
46
|
tables=APP_CONFIG.table_classes,
|
23
47
|
# Required when running under HTTPS:
|
24
48
|
# allowed_hosts=['my_site.com']
|
25
|
-
)
|
49
|
+
),
|
26
50
|
),
|
27
51
|
Mount("/static/", StaticFiles(directory="static")),
|
28
52
|
],
|
53
|
+
lifespan=lifespan,
|
29
54
|
)
|
30
55
|
|
31
56
|
|
32
57
|
TaskModelIn: t.Any = create_pydantic_model(
|
33
58
|
table=Task,
|
34
|
-
model_name=
|
59
|
+
model_name="TaskModelIn",
|
35
60
|
)
|
36
61
|
TaskModelOut: t.Any = create_pydantic_model(
|
37
|
-
table=Task,
|
38
|
-
include_default_columns=True,
|
39
|
-
model_name='TaskModelOut'
|
62
|
+
table=Task, include_default_columns=True, model_name="TaskModelOut"
|
40
63
|
)
|
41
64
|
|
42
65
|
|
@@ -45,16 +68,16 @@ async def tasks():
|
|
45
68
|
return await Task.select().order_by(Task.id)
|
46
69
|
|
47
70
|
|
48
|
-
@app.post(
|
71
|
+
@app.post("/tasks/", response_model=TaskModelOut)
|
49
72
|
async def create_task(task_model: TaskModelIn):
|
50
73
|
task = Task(**task_model.dict())
|
51
74
|
await task.save()
|
52
75
|
return task.to_dict()
|
53
76
|
|
54
77
|
|
55
|
-
@app.put(
|
78
|
+
@app.put("/tasks/{task_id}/", response_model=TaskModelOut)
|
56
79
|
async def update_task(task_id: int, task_model: TaskModelIn):
|
57
|
-
task = await Task.objects().get(Task.
|
80
|
+
task = await Task.objects().get(Task._meta.primary_key == task_id)
|
58
81
|
if not task:
|
59
82
|
return JSONResponse({}, status_code=404)
|
60
83
|
|
@@ -66,30 +89,12 @@ async def update_task(task_id: int, task_model: TaskModelIn):
|
|
66
89
|
return task.to_dict()
|
67
90
|
|
68
91
|
|
69
|
-
@app.delete(
|
92
|
+
@app.delete("/tasks/{task_id}/")
|
70
93
|
async def delete_task(task_id: int):
|
71
|
-
task = await Task.objects().get(Task.
|
94
|
+
task = await Task.objects().get(Task._meta.primary_key == task_id)
|
72
95
|
if not task:
|
73
96
|
return JSONResponse({}, status_code=404)
|
74
97
|
|
75
98
|
await task.remove()
|
76
99
|
|
77
100
|
return JSONResponse({})
|
78
|
-
|
79
|
-
|
80
|
-
@app.on_event("startup")
|
81
|
-
async def open_database_connection_pool():
|
82
|
-
try:
|
83
|
-
engine = engine_finder()
|
84
|
-
await engine.start_connection_pool()
|
85
|
-
except Exception:
|
86
|
-
print("Unable to connect to the database")
|
87
|
-
|
88
|
-
|
89
|
-
@app.on_event("shutdown")
|
90
|
-
async def close_database_connection_pool():
|
91
|
-
try:
|
92
|
-
engine = engine_finder()
|
93
|
-
await engine.close_connection_pool()
|
94
|
-
except Exception:
|
95
|
-
print("Unable to connect to the database")
|
@@ -1,8 +1,10 @@
|
|
1
|
+
from contextlib import asynccontextmanager
|
2
|
+
|
3
|
+
from piccolo.engine import engine_finder
|
1
4
|
from piccolo_admin.endpoints import create_admin
|
2
5
|
from piccolo_api.crud.endpoints import PiccoloCRUD
|
3
|
-
from piccolo.engine import engine_finder
|
4
|
-
from starlette.routing import Route, Mount
|
5
6
|
from starlette.applications import Starlette
|
7
|
+
from starlette.routing import Mount, Route
|
6
8
|
from starlette.staticfiles import StaticFiles
|
7
9
|
|
8
10
|
from home.endpoints import HomeEndpoint
|
@@ -10,24 +12,6 @@ from home.piccolo_app import APP_CONFIG
|
|
10
12
|
from home.tables import Task
|
11
13
|
|
12
14
|
|
13
|
-
app = Starlette(
|
14
|
-
routes=[
|
15
|
-
Route("/", HomeEndpoint),
|
16
|
-
Mount(
|
17
|
-
"/admin/",
|
18
|
-
create_admin(
|
19
|
-
tables=APP_CONFIG.table_classes,
|
20
|
-
# Required when running under HTTPS:
|
21
|
-
# allowed_hosts=['my_site.com']
|
22
|
-
)
|
23
|
-
),
|
24
|
-
Mount("/static/", StaticFiles(directory="static")),
|
25
|
-
Mount("/tasks/", PiccoloCRUD(table=Task))
|
26
|
-
],
|
27
|
-
)
|
28
|
-
|
29
|
-
|
30
|
-
@app.on_event("startup")
|
31
15
|
async def open_database_connection_pool():
|
32
16
|
try:
|
33
17
|
engine = engine_finder()
|
@@ -36,10 +20,34 @@ async def open_database_connection_pool():
|
|
36
20
|
print("Unable to connect to the database")
|
37
21
|
|
38
22
|
|
39
|
-
@app.on_event("shutdown")
|
40
23
|
async def close_database_connection_pool():
|
41
24
|
try:
|
42
25
|
engine = engine_finder()
|
43
26
|
await engine.close_connection_pool()
|
44
27
|
except Exception:
|
45
28
|
print("Unable to connect to the database")
|
29
|
+
|
30
|
+
|
31
|
+
@asynccontextmanager
|
32
|
+
async def lifespan(app: Starlette):
|
33
|
+
await open_database_connection_pool()
|
34
|
+
yield
|
35
|
+
await close_database_connection_pool()
|
36
|
+
|
37
|
+
|
38
|
+
app = Starlette(
|
39
|
+
routes=[
|
40
|
+
Route("/", HomeEndpoint),
|
41
|
+
Mount(
|
42
|
+
"/admin/",
|
43
|
+
create_admin(
|
44
|
+
tables=APP_CONFIG.table_classes,
|
45
|
+
# Required when running under HTTPS:
|
46
|
+
# allowed_hosts=['my_site.com']
|
47
|
+
),
|
48
|
+
),
|
49
|
+
Mount("/static/", StaticFiles(directory="static")),
|
50
|
+
Mount("/tasks/", PiccoloCRUD(table=Task)),
|
51
|
+
],
|
52
|
+
lifespan=lifespan,
|
53
|
+
)
|
@@ -72,18 +72,19 @@ class ForwardsMigrationManager(BaseMigrationManager):
|
|
72
72
|
print(f"🚀 Running {n} migration{'s' if n != 1 else ''}:")
|
73
73
|
|
74
74
|
for _id in subset:
|
75
|
-
|
76
|
-
|
77
|
-
else:
|
78
|
-
migration_module = migration_modules[_id]
|
79
|
-
response = await migration_module.forwards()
|
75
|
+
migration_module = migration_modules[_id]
|
76
|
+
response = await migration_module.forwards()
|
80
77
|
|
81
|
-
|
78
|
+
if isinstance(response, MigrationManager):
|
79
|
+
if self.fake or response.fake:
|
80
|
+
print(f"- {_id}: faked! ⏭️")
|
81
|
+
else:
|
82
82
|
if self.preview:
|
83
83
|
response.preview = True
|
84
84
|
await response.run()
|
85
85
|
|
86
|
-
|
86
|
+
print("ok! ✔️")
|
87
|
+
|
87
88
|
if not self.preview:
|
88
89
|
await Migration.insert().add(
|
89
90
|
Migration(name=_id, app_name=app_config.app_name)
|
@@ -19,6 +19,7 @@ from piccolo.columns import (
|
|
19
19
|
Interval,
|
20
20
|
Numeric,
|
21
21
|
Serial,
|
22
|
+
Text,
|
22
23
|
Timestamp,
|
23
24
|
Varchar,
|
24
25
|
)
|
@@ -55,6 +56,12 @@ class Band(Table):
|
|
55
56
|
)
|
56
57
|
|
57
58
|
|
59
|
+
class FanClub(Table):
|
60
|
+
id: Serial
|
61
|
+
address = Text()
|
62
|
+
band = ForeignKey(Band, unique=True)
|
63
|
+
|
64
|
+
|
58
65
|
class Venue(Table):
|
59
66
|
id: Serial
|
60
67
|
name = Varchar(length=100)
|
@@ -154,6 +161,7 @@ class Album(Table):
|
|
154
161
|
TABLES = (
|
155
162
|
Manager,
|
156
163
|
Band,
|
164
|
+
FanClub,
|
157
165
|
Venue,
|
158
166
|
Concert,
|
159
167
|
Ticket,
|
@@ -185,6 +193,9 @@ def populate():
|
|
185
193
|
pythonistas = Band(name="Pythonistas", manager=guido.id, popularity=1000)
|
186
194
|
pythonistas.save().run_sync()
|
187
195
|
|
196
|
+
fan_club = FanClub(address="1 Flying Circus, UK", band=pythonistas)
|
197
|
+
fan_club.save().run_sync()
|
198
|
+
|
188
199
|
graydon = Manager(name="Graydon")
|
189
200
|
graydon.save().run_sync()
|
190
201
|
|
piccolo/columns/column_types.py
CHANGED
@@ -86,47 +86,38 @@ class ConcatDelegate:
|
|
86
86
|
|
87
87
|
def get_querystring(
|
88
88
|
self,
|
89
|
-
|
90
|
-
value: t.Union[str,
|
89
|
+
column: Column,
|
90
|
+
value: t.Union[str, Column, QueryString],
|
91
91
|
reverse: bool = False,
|
92
92
|
) -> QueryString:
|
93
|
-
|
94
|
-
|
93
|
+
"""
|
94
|
+
:param reverse:
|
95
|
+
By default the value is appended to the column's value. If
|
96
|
+
``reverse=True`` then the value is prepended to the column's
|
97
|
+
value instead.
|
98
|
+
|
99
|
+
"""
|
100
|
+
if isinstance(value, Column):
|
95
101
|
if len(column._meta.call_chain) > 0:
|
96
102
|
raise ValueError(
|
97
103
|
"Adding values across joins isn't currently supported."
|
98
104
|
)
|
99
|
-
other_column_name = column._meta.db_column_name
|
100
|
-
if reverse:
|
101
|
-
return QueryString(
|
102
|
-
Concat.template.format(
|
103
|
-
value_1=other_column_name, value_2=column_name
|
104
|
-
)
|
105
|
-
)
|
106
|
-
else:
|
107
|
-
return QueryString(
|
108
|
-
Concat.template.format(
|
109
|
-
value_1=column_name, value_2=other_column_name
|
110
|
-
)
|
111
|
-
)
|
112
105
|
elif isinstance(value, str):
|
113
|
-
|
114
|
-
|
115
|
-
return QueryString(
|
116
|
-
Concat.template.format(value_1="{}", value_2=column_name),
|
117
|
-
value_1,
|
118
|
-
)
|
119
|
-
else:
|
120
|
-
value_2 = QueryString("CAST({} AS text)", value)
|
121
|
-
return QueryString(
|
122
|
-
Concat.template.format(value_1=column_name, value_2="{}"),
|
123
|
-
value_2,
|
124
|
-
)
|
125
|
-
else:
|
106
|
+
value = QueryString("CAST({} AS TEXT)", value)
|
107
|
+
elif not isinstance(value, QueryString):
|
126
108
|
raise ValueError(
|
127
|
-
"Only str,
|
109
|
+
"Only str, Column and QueryString values can be added."
|
128
110
|
)
|
129
111
|
|
112
|
+
args = [value, column] if reverse else [column, value]
|
113
|
+
|
114
|
+
# We use the concat operator instead of the concat function, because
|
115
|
+
# this is what we historically used, and they treat null values
|
116
|
+
# differently.
|
117
|
+
return QueryString(
|
118
|
+
Concat.template.format(value_1="{}", value_2="{}"), *args
|
119
|
+
)
|
120
|
+
|
130
121
|
|
131
122
|
class MathDelegate:
|
132
123
|
"""
|
@@ -340,12 +331,13 @@ class Varchar(Column):
|
|
340
331
|
|
341
332
|
def __add__(self, value: t.Union[str, Varchar, Text]) -> QueryString:
|
342
333
|
return self.concat_delegate.get_querystring(
|
343
|
-
|
334
|
+
column=self,
|
335
|
+
value=value,
|
344
336
|
)
|
345
337
|
|
346
338
|
def __radd__(self, value: t.Union[str, Varchar, Text]) -> QueryString:
|
347
339
|
return self.concat_delegate.get_querystring(
|
348
|
-
|
340
|
+
column=self,
|
349
341
|
value=value,
|
350
342
|
reverse=True,
|
351
343
|
)
|
@@ -442,12 +434,13 @@ class Text(Column):
|
|
442
434
|
|
443
435
|
def __add__(self, value: t.Union[str, Varchar, Text]) -> QueryString:
|
444
436
|
return self.concat_delegate.get_querystring(
|
445
|
-
|
437
|
+
column=self,
|
438
|
+
value=value,
|
446
439
|
)
|
447
440
|
|
448
441
|
def __radd__(self, value: t.Union[str, Varchar, Text]) -> QueryString:
|
449
442
|
return self.concat_delegate.get_querystring(
|
450
|
-
|
443
|
+
column=self,
|
451
444
|
value=value,
|
452
445
|
reverse=True,
|
453
446
|
)
|
@@ -2023,6 +2016,61 @@ class ForeignKey(Column, t.Generic[ReferencedTable]):
|
|
2023
2016
|
if column._meta.name not in excluded_column_names
|
2024
2017
|
]
|
2025
2018
|
|
2019
|
+
def reverse(self) -> ForeignKey:
|
2020
|
+
"""
|
2021
|
+
If there's a unique foreign key, this function reverses it.
|
2022
|
+
|
2023
|
+
.. code-block:: python
|
2024
|
+
|
2025
|
+
class Band(Table):
|
2026
|
+
name = Varchar()
|
2027
|
+
|
2028
|
+
class FanClub(Table):
|
2029
|
+
band = ForeignKey(Band, unique=True)
|
2030
|
+
address = Text()
|
2031
|
+
|
2032
|
+
class Treasurer(Table):
|
2033
|
+
fan_club = ForeignKey(FanClub, unique=True)
|
2034
|
+
name = Varchar()
|
2035
|
+
|
2036
|
+
It's helpful with ``get_related``, for example:
|
2037
|
+
|
2038
|
+
.. code-block:: python
|
2039
|
+
|
2040
|
+
>>> band = await Band.objects().first()
|
2041
|
+
>>> await band.get_related(FanClub.band.reverse())
|
2042
|
+
<Fan Club: 1>
|
2043
|
+
|
2044
|
+
It works multiple levels deep:
|
2045
|
+
|
2046
|
+
.. code-block:: python
|
2047
|
+
|
2048
|
+
>>> await band.get_related(Treasurer.fan_club._.band.reverse())
|
2049
|
+
<Treasurer: 1>
|
2050
|
+
|
2051
|
+
"""
|
2052
|
+
if not self._meta.unique or any(
|
2053
|
+
not i._meta.unique for i in self._meta.call_chain
|
2054
|
+
):
|
2055
|
+
raise ValueError("Only reverse unique foreign keys.")
|
2056
|
+
|
2057
|
+
foreign_keys = [*self._meta.call_chain, self]
|
2058
|
+
|
2059
|
+
root_foreign_key = foreign_keys[0]
|
2060
|
+
target_column = (
|
2061
|
+
root_foreign_key._foreign_key_meta.resolved_target_column
|
2062
|
+
)
|
2063
|
+
foreign_key = target_column.join_on(root_foreign_key)
|
2064
|
+
|
2065
|
+
call_chain = []
|
2066
|
+
for fk in reversed(foreign_keys[1:]):
|
2067
|
+
target_column = fk._foreign_key_meta.resolved_target_column
|
2068
|
+
call_chain.append(target_column.join_on(fk))
|
2069
|
+
|
2070
|
+
foreign_key._meta.call_chain = call_chain
|
2071
|
+
|
2072
|
+
return foreign_key
|
2073
|
+
|
2026
2074
|
def all_related(
|
2027
2075
|
self, exclude: t.Optional[t.List[t.Union[ForeignKey, str]]] = None
|
2028
2076
|
) -> t.List[ForeignKey]:
|
@@ -1,6 +1,7 @@
|
|
1
1
|
from .aggregate import Avg, Count, Max, Min, Sum
|
2
|
+
from .datetime import Day, Extract, Hour, Month, Second, Strftime, Year
|
2
3
|
from .math import Abs, Ceil, Floor, Round
|
3
|
-
from .string import Length, Lower, Ltrim, Reverse, Rtrim, Upper
|
4
|
+
from .string import Concat, Length, Lower, Ltrim, Reverse, Rtrim, Upper
|
4
5
|
from .type_conversion import Cast
|
5
6
|
|
6
7
|
__all__ = (
|
@@ -8,16 +9,25 @@ __all__ = (
|
|
8
9
|
"Avg",
|
9
10
|
"Cast",
|
10
11
|
"Ceil",
|
12
|
+
"Concat",
|
11
13
|
"Count",
|
14
|
+
"Day",
|
15
|
+
"Extract",
|
16
|
+
"Extract",
|
12
17
|
"Floor",
|
18
|
+
"Hour",
|
13
19
|
"Length",
|
14
20
|
"Lower",
|
15
21
|
"Ltrim",
|
16
22
|
"Max",
|
17
23
|
"Min",
|
24
|
+
"Month",
|
18
25
|
"Reverse",
|
19
26
|
"Round",
|
20
27
|
"Rtrim",
|
28
|
+
"Second",
|
29
|
+
"Strftime",
|
21
30
|
"Sum",
|
22
31
|
"Upper",
|
32
|
+
"Year",
|
23
33
|
)
|