piccolo 1.21.0__py3-none-any.whl → 1.23.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 +3 -0
- piccolo/apps/asgi/commands/templates/app/_falcon_app.py.jinja +60 -0
- piccolo/apps/asgi/commands/templates/app/_quart_app.py.jinja +119 -0
- piccolo/apps/asgi/commands/templates/app/_sanic_app.py.jinja +121 -0
- piccolo/apps/asgi/commands/templates/app/app.py.jinja +6 -0
- piccolo/apps/asgi/commands/templates/app/home/_falcon_endpoints.py.jinja +19 -0
- piccolo/apps/asgi/commands/templates/app/home/_quart_endpoints.py.jinja +18 -0
- piccolo/apps/asgi/commands/templates/app/home/_sanic_endpoints.py.jinja +17 -0
- piccolo/apps/asgi/commands/templates/app/home/endpoints.py.jinja +6 -0
- piccolo/apps/asgi/commands/templates/app/home/templates/home.html.jinja_raw +15 -0
- piccolo/apps/playground/commands/run.py +9 -0
- piccolo/columns/column_types.py +78 -39
- piccolo/columns/defaults/timestamptz.py +1 -0
- piccolo/engine/sqlite.py +14 -2
- piccolo/query/base.py +11 -6
- piccolo/query/operators/__init__.py +0 -0
- piccolo/query/operators/json.py +111 -0
- piccolo/querystring.py +14 -2
- piccolo/table_reflection.py +17 -5
- {piccolo-1.21.0.dist-info → piccolo-1.23.0.dist-info}/METADATA +34 -22
- {piccolo-1.21.0.dist-info → piccolo-1.23.0.dist-info}/RECORD +31 -20
- {piccolo-1.21.0.dist-info → piccolo-1.23.0.dist-info}/WHEEL +1 -1
- tests/columns/test_integer.py +32 -0
- tests/columns/test_jsonb.py +100 -43
- tests/query/operators/__init__.py +0 -0
- tests/query/operators/test_json.py +52 -0
- tests/table/test_insert.py +1 -1
- {piccolo-1.21.0.dist-info → piccolo-1.23.0.dist-info}/LICENSE +0 -0
- {piccolo-1.21.0.dist-info → piccolo-1.23.0.dist-info}/entry_points.txt +0 -0
- {piccolo-1.21.0.dist-info → piccolo-1.23.0.dist-info}/top_level.txt +0 -0
piccolo/__init__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__VERSION__ = "1.
|
1
|
+
__VERSION__ = "1.23.0"
|
@@ -17,6 +17,9 @@ ROUTER_DEPENDENCIES = {
|
|
17
17
|
"litestar": ["litestar"],
|
18
18
|
"esmerald": ["esmerald"],
|
19
19
|
"lilya": ["lilya"],
|
20
|
+
"quart": ["quart", "quart_schema"],
|
21
|
+
"falcon": ["falcon"],
|
22
|
+
"sanic": ["sanic", "sanic_ext"],
|
20
23
|
}
|
21
24
|
ROUTERS = list(ROUTER_DEPENDENCIES.keys())
|
22
25
|
|
@@ -0,0 +1,60 @@
|
|
1
|
+
import os
|
2
|
+
import typing as t
|
3
|
+
|
4
|
+
import falcon.asgi
|
5
|
+
from hypercorn.middleware import DispatcherMiddleware
|
6
|
+
from piccolo.engine import engine_finder
|
7
|
+
from piccolo_admin.endpoints import create_admin
|
8
|
+
from piccolo_api.crud.endpoints import PiccoloCRUD
|
9
|
+
|
10
|
+
from home.endpoints import HomeEndpoint
|
11
|
+
from home.piccolo_app import APP_CONFIG
|
12
|
+
from home.tables import Task
|
13
|
+
|
14
|
+
|
15
|
+
async def open_database_connection_pool():
|
16
|
+
try:
|
17
|
+
engine = engine_finder()
|
18
|
+
await engine.start_connection_pool()
|
19
|
+
except Exception:
|
20
|
+
print("Unable to connect to the database")
|
21
|
+
|
22
|
+
|
23
|
+
async def close_database_connection_pool():
|
24
|
+
try:
|
25
|
+
engine = engine_finder()
|
26
|
+
await engine.close_connection_pool()
|
27
|
+
except Exception:
|
28
|
+
print("Unable to connect to the database")
|
29
|
+
|
30
|
+
|
31
|
+
class LifespanMiddleware:
|
32
|
+
async def process_startup(
|
33
|
+
self, scope: t.Dict[str, t.Any], event: t.Dict[str, t.Any]
|
34
|
+
) -> None:
|
35
|
+
await open_database_connection_pool()
|
36
|
+
|
37
|
+
async def process_shutdown(
|
38
|
+
self, scope: t.Dict[str, t.Any], event: t.Dict[str, t.Any]
|
39
|
+
) -> None:
|
40
|
+
await close_database_connection_pool()
|
41
|
+
|
42
|
+
|
43
|
+
app: t.Any = falcon.asgi.App(middleware=LifespanMiddleware())
|
44
|
+
app.add_static_route("/static", directory=os.path.abspath("static"))
|
45
|
+
app.add_route("/", HomeEndpoint())
|
46
|
+
|
47
|
+
PICCOLO_CRUD: t.Any = PiccoloCRUD(table=Task)
|
48
|
+
|
49
|
+
# enable the Admin and PiccoloCrud app using DispatcherMiddleware
|
50
|
+
app = DispatcherMiddleware( # type: ignore
|
51
|
+
{
|
52
|
+
"/admin": create_admin(
|
53
|
+
tables=APP_CONFIG.table_classes,
|
54
|
+
# Required when running under HTTPS:
|
55
|
+
# allowed_hosts=['my_site.com']
|
56
|
+
),
|
57
|
+
"/tasks": PICCOLO_CRUD,
|
58
|
+
"": app,
|
59
|
+
}
|
60
|
+
)
|
@@ -0,0 +1,119 @@
|
|
1
|
+
import typing as t
|
2
|
+
from http import HTTPStatus
|
3
|
+
|
4
|
+
from hypercorn.middleware import DispatcherMiddleware
|
5
|
+
from piccolo.engine import engine_finder
|
6
|
+
from piccolo_admin.endpoints import create_admin
|
7
|
+
from piccolo_api.crud.serializers import create_pydantic_model
|
8
|
+
from quart import Quart
|
9
|
+
from quart_schema import (
|
10
|
+
Info,
|
11
|
+
QuartSchema,
|
12
|
+
hide,
|
13
|
+
tag,
|
14
|
+
validate_request,
|
15
|
+
validate_response,
|
16
|
+
)
|
17
|
+
|
18
|
+
from home.endpoints import index
|
19
|
+
from home.piccolo_app import APP_CONFIG
|
20
|
+
from home.tables import Task
|
21
|
+
|
22
|
+
|
23
|
+
app = Quart(__name__, static_folder="static")
|
24
|
+
QuartSchema(app, info=Info(title="Quart API", version="0.1.0"))
|
25
|
+
|
26
|
+
|
27
|
+
TaskModelIn: t.Any = create_pydantic_model(
|
28
|
+
table=Task,
|
29
|
+
model_name="TaskModelIn",
|
30
|
+
)
|
31
|
+
TaskModelOut: t.Any = create_pydantic_model(
|
32
|
+
table=Task,
|
33
|
+
include_default_columns=True,
|
34
|
+
model_name="TaskModelOut",
|
35
|
+
)
|
36
|
+
|
37
|
+
|
38
|
+
@app.get("/")
|
39
|
+
@hide
|
40
|
+
def home():
|
41
|
+
return index()
|
42
|
+
|
43
|
+
|
44
|
+
@app.get("/tasks/")
|
45
|
+
@validate_response(t.List[TaskModelOut])
|
46
|
+
@tag(["Task"])
|
47
|
+
async def tasks():
|
48
|
+
return await Task.select().order_by(Task._meta.primary_key, ascending=False)
|
49
|
+
|
50
|
+
|
51
|
+
@app.post("/tasks/")
|
52
|
+
@validate_request(TaskModelIn)
|
53
|
+
@validate_response(TaskModelOut)
|
54
|
+
@tag(["Task"])
|
55
|
+
async def create_task(data: TaskModelIn):
|
56
|
+
task = Task(**data.model_dump())
|
57
|
+
await task.save()
|
58
|
+
return task.to_dict(), HTTPStatus.CREATED
|
59
|
+
|
60
|
+
|
61
|
+
@app.put("/tasks/<int:task_id>/")
|
62
|
+
@validate_request(TaskModelIn)
|
63
|
+
@validate_response(TaskModelOut)
|
64
|
+
@tag(["Task"])
|
65
|
+
async def update_task(task_id: int, data: TaskModelIn):
|
66
|
+
task = await Task.objects().get(Task._meta.primary_key == task_id)
|
67
|
+
if not task:
|
68
|
+
return {}, HTTPStatus.NOT_FOUND
|
69
|
+
|
70
|
+
for key, value in data.model_dump().items():
|
71
|
+
setattr(task, key, value)
|
72
|
+
|
73
|
+
await task.save()
|
74
|
+
|
75
|
+
return task.to_dict(), HTTPStatus.OK
|
76
|
+
|
77
|
+
|
78
|
+
@app.delete("/tasks/<int:task_id>/")
|
79
|
+
@validate_response(TaskModelOut)
|
80
|
+
@tag(["Task"])
|
81
|
+
async def delete_task(task_id: int):
|
82
|
+
task = await Task.objects().get(Task._meta.primary_key == task_id)
|
83
|
+
if not task:
|
84
|
+
return {}, HTTPStatus.NOT_FOUND
|
85
|
+
|
86
|
+
await task.remove()
|
87
|
+
|
88
|
+
return {}, HTTPStatus.OK
|
89
|
+
|
90
|
+
|
91
|
+
@app.before_serving
|
92
|
+
async def open_database_connection_pool():
|
93
|
+
try:
|
94
|
+
engine = engine_finder()
|
95
|
+
await engine.start_connection_pool()
|
96
|
+
except Exception:
|
97
|
+
print("Unable to connect to the database")
|
98
|
+
|
99
|
+
|
100
|
+
@app.after_serving
|
101
|
+
async def close_database_connection_pool():
|
102
|
+
try:
|
103
|
+
engine = engine_finder()
|
104
|
+
await engine.close_connection_pool()
|
105
|
+
except Exception:
|
106
|
+
print("Unable to connect to the database")
|
107
|
+
|
108
|
+
|
109
|
+
# enable the admin application using DispatcherMiddleware
|
110
|
+
app = DispatcherMiddleware( # type: ignore
|
111
|
+
{
|
112
|
+
"/admin": create_admin(
|
113
|
+
tables=APP_CONFIG.table_classes,
|
114
|
+
# Required when running under HTTPS:
|
115
|
+
# allowed_hosts=['my_site.com']
|
116
|
+
),
|
117
|
+
"": app,
|
118
|
+
}
|
119
|
+
)
|
@@ -0,0 +1,121 @@
|
|
1
|
+
import asyncio
|
2
|
+
import typing as t
|
3
|
+
|
4
|
+
from hypercorn.middleware import DispatcherMiddleware
|
5
|
+
from piccolo.engine import engine_finder
|
6
|
+
from piccolo_admin.endpoints import create_admin
|
7
|
+
from piccolo_api.crud.serializers import create_pydantic_model
|
8
|
+
from sanic import Request, Sanic, json
|
9
|
+
from sanic_ext import openapi
|
10
|
+
|
11
|
+
from home.endpoints import index
|
12
|
+
from home.piccolo_app import APP_CONFIG
|
13
|
+
from home.tables import Task
|
14
|
+
|
15
|
+
app = Sanic(__name__)
|
16
|
+
app.static("/static/", "static")
|
17
|
+
|
18
|
+
|
19
|
+
TaskModelIn: t.Any = create_pydantic_model(
|
20
|
+
table=Task,
|
21
|
+
model_name="TaskModelIn",
|
22
|
+
)
|
23
|
+
TaskModelOut: t.Any = create_pydantic_model(
|
24
|
+
table=Task,
|
25
|
+
include_default_columns=True,
|
26
|
+
model_name="TaskModelOut",
|
27
|
+
)
|
28
|
+
|
29
|
+
|
30
|
+
@app.get("/")
|
31
|
+
@openapi.exclude()
|
32
|
+
def home(request: Request):
|
33
|
+
return index()
|
34
|
+
|
35
|
+
|
36
|
+
@app.get("/tasks/")
|
37
|
+
@openapi.tag("Task")
|
38
|
+
@openapi.response(200, {"application/json": TaskModelOut.model_json_schema()})
|
39
|
+
async def tasks(request: Request):
|
40
|
+
return json(
|
41
|
+
await Task.select().order_by(Task._meta.primary_key, ascending=False),
|
42
|
+
status=200,
|
43
|
+
)
|
44
|
+
|
45
|
+
|
46
|
+
@app.post("/tasks/")
|
47
|
+
@openapi.definition(
|
48
|
+
body={"application/json": TaskModelIn.model_json_schema()},
|
49
|
+
tag="Task",
|
50
|
+
)
|
51
|
+
@openapi.response(201, {"application/json": TaskModelOut.model_json_schema()})
|
52
|
+
async def create_task(request: Request):
|
53
|
+
task = Task(**request.json)
|
54
|
+
await task.save()
|
55
|
+
return json(task.to_dict(), status=201)
|
56
|
+
|
57
|
+
|
58
|
+
@app.put("/tasks/<task_id:int>/")
|
59
|
+
@openapi.definition(
|
60
|
+
body={"application/json": TaskModelIn.model_json_schema()},
|
61
|
+
tag="Task",
|
62
|
+
)
|
63
|
+
@openapi.response(200, {"application/json": TaskModelOut.model_json_schema()})
|
64
|
+
async def update_task(request: Request, task_id: int):
|
65
|
+
task = await Task.objects().get(Task._meta.primary_key == task_id)
|
66
|
+
if not task:
|
67
|
+
return json({}, status=404)
|
68
|
+
for key, value in request.json.items():
|
69
|
+
setattr(task, key, value)
|
70
|
+
|
71
|
+
await task.save()
|
72
|
+
return json(task.to_dict(), status=200)
|
73
|
+
|
74
|
+
|
75
|
+
@app.delete("/tasks/<task_id:int>/")
|
76
|
+
@openapi.tag("Task")
|
77
|
+
async def delete_task(request: Request, task_id: int):
|
78
|
+
task = await Task.objects().get(Task._meta.primary_key == task_id)
|
79
|
+
if not task:
|
80
|
+
return json({}, status=404)
|
81
|
+
await task.remove()
|
82
|
+
return json({}, status=200)
|
83
|
+
|
84
|
+
|
85
|
+
async def open_database_connection_pool():
|
86
|
+
try:
|
87
|
+
engine = engine_finder()
|
88
|
+
await engine.start_connection_pool()
|
89
|
+
except Exception:
|
90
|
+
print("Unable to connect to the database")
|
91
|
+
|
92
|
+
|
93
|
+
async def close_database_connection_pool():
|
94
|
+
try:
|
95
|
+
engine = engine_finder()
|
96
|
+
await engine.close_connection_pool()
|
97
|
+
except Exception:
|
98
|
+
print("Unable to connect to the database")
|
99
|
+
|
100
|
+
|
101
|
+
@app.after_server_start
|
102
|
+
async def startup(app, loop):
|
103
|
+
await open_database_connection_pool()
|
104
|
+
|
105
|
+
|
106
|
+
@app.before_server_stop
|
107
|
+
async def shutdown(app, loop):
|
108
|
+
await close_database_connection_pool()
|
109
|
+
|
110
|
+
|
111
|
+
# enable the admin application using DispatcherMiddleware
|
112
|
+
app = DispatcherMiddleware( # type: ignore
|
113
|
+
{
|
114
|
+
"/admin": create_admin(
|
115
|
+
tables=APP_CONFIG.table_classes,
|
116
|
+
# Required when running under HTTPS:
|
117
|
+
# allowed_hosts=['my_site.com']
|
118
|
+
),
|
119
|
+
"": app,
|
120
|
+
}
|
121
|
+
)
|
@@ -10,4 +10,10 @@
|
|
10
10
|
{% include '_esmerald_app.py.jinja' %}
|
11
11
|
{% elif router == 'lilya' %}
|
12
12
|
{% include '_lilya_app.py.jinja' %}
|
13
|
+
{% elif router == 'quart' %}
|
14
|
+
{% include '_quart_app.py.jinja' %}
|
15
|
+
{% elif router == 'falcon' %}
|
16
|
+
{% include '_falcon_app.py.jinja' %}
|
17
|
+
{% elif router == 'sanic' %}
|
18
|
+
{% include '_sanic_app.py.jinja' %}
|
13
19
|
{% endif %}
|
@@ -0,0 +1,19 @@
|
|
1
|
+
import os
|
2
|
+
|
3
|
+
import falcon
|
4
|
+
import jinja2
|
5
|
+
|
6
|
+
ENVIRONMENT = jinja2.Environment(
|
7
|
+
loader=jinja2.FileSystemLoader(
|
8
|
+
searchpath=os.path.join(os.path.dirname(__file__), "templates")
|
9
|
+
)
|
10
|
+
)
|
11
|
+
|
12
|
+
|
13
|
+
class HomeEndpoint:
|
14
|
+
async def on_get(self, req, resp):
|
15
|
+
template = ENVIRONMENT.get_template("home.html.jinja")
|
16
|
+
content = template.render(title="Piccolo + ASGI",)
|
17
|
+
resp.status = falcon.HTTP_200
|
18
|
+
resp.content_type = "text/html"
|
19
|
+
resp.text = content
|
@@ -0,0 +1,18 @@
|
|
1
|
+
import os
|
2
|
+
|
3
|
+
import jinja2
|
4
|
+
|
5
|
+
from quart import Response
|
6
|
+
|
7
|
+
ENVIRONMENT = jinja2.Environment(
|
8
|
+
loader=jinja2.FileSystemLoader(
|
9
|
+
searchpath=os.path.join(os.path.dirname(__file__), "templates")
|
10
|
+
)
|
11
|
+
)
|
12
|
+
|
13
|
+
|
14
|
+
def index():
|
15
|
+
template = ENVIRONMENT.get_template("home.html.jinja")
|
16
|
+
content = template.render(title="Piccolo + ASGI")
|
17
|
+
return Response(content)
|
18
|
+
|
@@ -0,0 +1,17 @@
|
|
1
|
+
import os
|
2
|
+
|
3
|
+
import jinja2
|
4
|
+
|
5
|
+
from sanic import HTTPResponse
|
6
|
+
|
7
|
+
ENVIRONMENT = jinja2.Environment(
|
8
|
+
loader=jinja2.FileSystemLoader(
|
9
|
+
searchpath=os.path.join(os.path.dirname(__file__), "templates")
|
10
|
+
)
|
11
|
+
)
|
12
|
+
|
13
|
+
|
14
|
+
def index():
|
15
|
+
template = ENVIRONMENT.get_template("home.html.jinja")
|
16
|
+
content = template.render(title="Piccolo + ASGI")
|
17
|
+
return HTTPResponse(content)
|
@@ -8,4 +8,10 @@
|
|
8
8
|
{% include '_esmerald_endpoints.py.jinja' %}
|
9
9
|
{% elif router == 'lilya' %}
|
10
10
|
{% include '_lilya_endpoints.py.jinja' %}
|
11
|
+
{% elif router == 'quart' %}
|
12
|
+
{% include '_quart_endpoints.py.jinja' %}
|
13
|
+
{% elif router == 'falcon' %}
|
14
|
+
{% include '_falcon_endpoints.py.jinja' %}
|
15
|
+
{% elif router == 'sanic' %}
|
16
|
+
{% include '_sanic_endpoints.py.jinja' %}
|
11
17
|
{% endif %}
|
@@ -66,6 +66,21 @@
|
|
66
66
|
<li><a href="/admin/">Admin</a></li>
|
67
67
|
<li><a href="/tasks/">JSON endpoint</a></li>
|
68
68
|
</ul>
|
69
|
+
<h3>Quart</h3>
|
70
|
+
<ul>
|
71
|
+
<li><a href="/admin/">Admin</a></li>
|
72
|
+
<li><a href="/docs">Swagger API</a></li>
|
73
|
+
</ul>
|
74
|
+
<h3>Falcon</h3>
|
75
|
+
<ul>
|
76
|
+
<li><a href="/admin/">Admin</a></li>
|
77
|
+
<li><a href="/tasks/">JSON endpoint</a></li>
|
78
|
+
</ul>
|
79
|
+
<h3>Sanic</h3>
|
80
|
+
<ul>
|
81
|
+
<li><a href="/admin/">Admin</a></li>
|
82
|
+
<li><a href="/docs/swagger">Swagger API</a></li>
|
83
|
+
</ul>
|
69
84
|
</section>
|
70
85
|
</div>
|
71
86
|
{% endblock content %}
|
@@ -233,6 +233,11 @@ def populate():
|
|
233
233
|
RecordingStudio.facilities: {
|
234
234
|
"restaurant": True,
|
235
235
|
"mixing_desk": True,
|
236
|
+
"instruments": {"electric_guitars": 10, "drum_kits": 2},
|
237
|
+
"technicians": [
|
238
|
+
{"name": "Alice Jones"},
|
239
|
+
{"name": "Bob Williams"},
|
240
|
+
],
|
236
241
|
},
|
237
242
|
}
|
238
243
|
)
|
@@ -244,6 +249,10 @@ def populate():
|
|
244
249
|
RecordingStudio.facilities: {
|
245
250
|
"restaurant": False,
|
246
251
|
"mixing_desk": True,
|
252
|
+
"instruments": {"electric_guitars": 6, "drum_kits": 3},
|
253
|
+
"technicians": [
|
254
|
+
{"name": "Frank Smith"},
|
255
|
+
],
|
247
256
|
},
|
248
257
|
},
|
249
258
|
)
|
piccolo/columns/column_types.py
CHANGED
@@ -70,6 +70,10 @@ from piccolo.utils.warnings import colored_warning
|
|
70
70
|
|
71
71
|
if t.TYPE_CHECKING: # pragma: no cover
|
72
72
|
from piccolo.columns.base import ColumnMeta
|
73
|
+
from piccolo.query.operators.json import (
|
74
|
+
GetChildElement,
|
75
|
+
GetElementFromPath,
|
76
|
+
)
|
73
77
|
from piccolo.table import Table
|
74
78
|
|
75
79
|
|
@@ -2319,6 +2323,76 @@ class JSON(Column):
|
|
2319
2323
|
else:
|
2320
2324
|
return "JSON"
|
2321
2325
|
|
2326
|
+
###########################################################################
|
2327
|
+
|
2328
|
+
def arrow(self, key: t.Union[str, int, QueryString]) -> GetChildElement:
|
2329
|
+
"""
|
2330
|
+
Allows a child element of the JSON structure to be returned - for
|
2331
|
+
example::
|
2332
|
+
|
2333
|
+
>>> await RecordingStudio.select(
|
2334
|
+
... RecordingStudio.facilities.arrow("restaurant")
|
2335
|
+
... )
|
2336
|
+
|
2337
|
+
"""
|
2338
|
+
from piccolo.query.operators.json import GetChildElement
|
2339
|
+
|
2340
|
+
alias = self._alias or self._meta.get_default_alias()
|
2341
|
+
return GetChildElement(identifier=self, key=key, alias=alias)
|
2342
|
+
|
2343
|
+
def __getitem__(
|
2344
|
+
self, value: t.Union[str, int, QueryString]
|
2345
|
+
) -> GetChildElement:
|
2346
|
+
"""
|
2347
|
+
A shortcut for the ``arrow`` method, used for retrieving a child
|
2348
|
+
element.
|
2349
|
+
|
2350
|
+
For example:
|
2351
|
+
|
2352
|
+
.. code-block:: python
|
2353
|
+
|
2354
|
+
>>> await RecordingStudio.select(
|
2355
|
+
... RecordingStudio.facilities["restaurant"]
|
2356
|
+
... )
|
2357
|
+
|
2358
|
+
"""
|
2359
|
+
return self.arrow(key=value)
|
2360
|
+
|
2361
|
+
def from_path(
|
2362
|
+
self,
|
2363
|
+
path: t.List[t.Union[str, int]],
|
2364
|
+
) -> GetElementFromPath:
|
2365
|
+
"""
|
2366
|
+
Allows an element of the JSON structure to be returned, which can be
|
2367
|
+
arbitrarily deep. For example::
|
2368
|
+
|
2369
|
+
>>> await RecordingStudio.select(
|
2370
|
+
... RecordingStudio.facilities.from_path([
|
2371
|
+
... "technician",
|
2372
|
+
... 0,
|
2373
|
+
... "first_name"
|
2374
|
+
... ])
|
2375
|
+
... )
|
2376
|
+
|
2377
|
+
It's the same as calling ``arrow`` multiple times, but is more
|
2378
|
+
efficient / convenient if extracting highly nested data::
|
2379
|
+
|
2380
|
+
>>> await RecordingStudio.select(
|
2381
|
+
... RecordingStudio.facilities.arrow(
|
2382
|
+
... "technician"
|
2383
|
+
... ).arrow(
|
2384
|
+
... 0
|
2385
|
+
... ).arrow(
|
2386
|
+
... "first_name"
|
2387
|
+
... )
|
2388
|
+
... )
|
2389
|
+
|
2390
|
+
"""
|
2391
|
+
from piccolo.query.operators.json import GetElementFromPath
|
2392
|
+
|
2393
|
+
alias = self._alias or self._meta.get_default_alias()
|
2394
|
+
return GetElementFromPath(identifier=self, path=path, alias=alias)
|
2395
|
+
|
2322
2396
|
###########################################################################
|
2323
2397
|
# Descriptors
|
2324
2398
|
|
@@ -2337,10 +2411,10 @@ class JSON(Column):
|
|
2337
2411
|
|
2338
2412
|
class JSONB(JSON):
|
2339
2413
|
"""
|
2340
|
-
Used for storing JSON strings - Postgres only. The data is
|
2341
|
-
binary format, and can be queried. Insertion
|
2342
|
-
be converted to the binary format). The
|
2343
|
-
outweigh the downsides.
|
2414
|
+
Used for storing JSON strings - Postgres / CochroachDB only. The data is
|
2415
|
+
stored in a binary format, and can be queried more efficiently. Insertion
|
2416
|
+
can be slower (as it needs to be converted to the binary format). The
|
2417
|
+
benefits of JSONB generally outweigh the downsides.
|
2344
2418
|
|
2345
2419
|
:param default:
|
2346
2420
|
Either a JSON string can be provided, or a Python ``dict`` or ``list``
|
@@ -2352,41 +2426,6 @@ class JSONB(JSON):
|
|
2352
2426
|
def column_type(self):
|
2353
2427
|
return "JSONB" # Must be defined, we override column_type() in JSON()
|
2354
2428
|
|
2355
|
-
def arrow(self, key: str) -> JSONB:
|
2356
|
-
"""
|
2357
|
-
Allows part of the JSON structure to be returned - for example,
|
2358
|
-
for {"a": 1}, and a key value of "a", then 1 will be returned.
|
2359
|
-
"""
|
2360
|
-
instance = t.cast(JSONB, self.copy())
|
2361
|
-
instance.json_operator = f"-> '{key}'"
|
2362
|
-
return instance
|
2363
|
-
|
2364
|
-
def get_select_string(
|
2365
|
-
self, engine_type: str, with_alias: bool = True
|
2366
|
-
) -> QueryString:
|
2367
|
-
select_string = self._meta.get_full_name(with_alias=False)
|
2368
|
-
|
2369
|
-
if self.json_operator is not None:
|
2370
|
-
select_string += f" {self.json_operator}"
|
2371
|
-
|
2372
|
-
if with_alias:
|
2373
|
-
alias = self._alias or self._meta.get_default_alias()
|
2374
|
-
select_string += f' AS "{alias}"'
|
2375
|
-
|
2376
|
-
return QueryString(select_string)
|
2377
|
-
|
2378
|
-
def eq(self, value) -> Where:
|
2379
|
-
"""
|
2380
|
-
See ``Boolean.eq`` for more details.
|
2381
|
-
"""
|
2382
|
-
return self.__eq__(value)
|
2383
|
-
|
2384
|
-
def ne(self, value) -> Where:
|
2385
|
-
"""
|
2386
|
-
See ``Boolean.ne`` for more details.
|
2387
|
-
"""
|
2388
|
-
return self.__ne__(value)
|
2389
|
-
|
2390
2429
|
###########################################################################
|
2391
2430
|
# Descriptors
|
2392
2431
|
|
piccolo/engine/sqlite.py
CHANGED
@@ -173,9 +173,21 @@ def convert_numeric_out(value: str) -> Decimal:
|
|
173
173
|
@decode_to_string
|
174
174
|
def convert_int_out(value: str) -> int:
|
175
175
|
"""
|
176
|
-
Make sure
|
176
|
+
Make sure INTEGER values are actually of type ``int``.
|
177
|
+
|
178
|
+
SQLite doesn't enforce that the values in INTEGER columns are actually
|
179
|
+
integers - they could be strings ('hello'), or floats (1.0).
|
180
|
+
|
181
|
+
There's not much we can do if the value is something like 'hello' - a
|
182
|
+
``ValueError`` is appropriate in this situation.
|
183
|
+
|
184
|
+
For a value like ``1.0``, it seems reasonable to handle this, and return a
|
185
|
+
value of ``1``.
|
186
|
+
|
177
187
|
"""
|
178
|
-
|
188
|
+
# We used to use int(float(value)), but it was incorrect, because float has
|
189
|
+
# limited precision for large numbers.
|
190
|
+
return int(Decimal(value))
|
179
191
|
|
180
192
|
|
181
193
|
@decode_to_string
|
piccolo/query/base.py
CHANGED
@@ -6,6 +6,7 @@ from time import time
|
|
6
6
|
from piccolo.columns.column_types import JSON, JSONB
|
7
7
|
from piccolo.custom_types import QueryResponseType, TableInstance
|
8
8
|
from piccolo.query.mixins import ColumnsDelegate
|
9
|
+
from piccolo.query.operators.json import JSONQueryString
|
9
10
|
from piccolo.querystring import QueryString
|
10
11
|
from piccolo.utils.encoding import load_json
|
11
12
|
from piccolo.utils.objects import make_nested_object
|
@@ -65,16 +66,20 @@ class Query(t.Generic[TableInstance, QueryResponseType]):
|
|
65
66
|
self, "columns_delegate", None
|
66
67
|
)
|
67
68
|
|
69
|
+
json_column_names: t.List[str] = []
|
70
|
+
|
68
71
|
if columns_delegate is not None:
|
69
|
-
json_columns = [
|
70
|
-
|
71
|
-
|
72
|
-
if isinstance(
|
73
|
-
|
72
|
+
json_columns: t.List[t.Union[JSON, JSONB]] = []
|
73
|
+
|
74
|
+
for column in columns_delegate.selected_columns:
|
75
|
+
if isinstance(column, (JSON, JSONB)):
|
76
|
+
json_columns.append(column)
|
77
|
+
elif isinstance(column, JSONQueryString):
|
78
|
+
if alias := column._alias:
|
79
|
+
json_column_names.append(alias)
|
74
80
|
else:
|
75
81
|
json_columns = self.table._meta.json_columns
|
76
82
|
|
77
|
-
json_column_names = []
|
78
83
|
for column in json_columns:
|
79
84
|
if column._alias is not None:
|
80
85
|
json_column_names.append(column._alias)
|
File without changes
|