piccolo 1.27.1__py3-none-any.whl → 1.29.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.
Files changed (132) hide show
  1. piccolo/__init__.py +1 -1
  2. piccolo/apps/app/commands/new.py +3 -3
  3. piccolo/apps/asgi/commands/new.py +2 -3
  4. piccolo/apps/asgi/commands/templates/app/_blacksheep_app.py.jinja +57 -29
  5. piccolo/apps/asgi/commands/templates/app/_esmerald_app.py.jinja +48 -21
  6. piccolo/apps/asgi/commands/templates/app/_falcon_app.py.jinja +63 -8
  7. piccolo/apps/asgi/commands/templates/app/_fastapi_app.py.jinja +51 -24
  8. piccolo/apps/asgi/commands/templates/app/_litestar_app.py.jinja +34 -10
  9. piccolo/apps/asgi/commands/templates/app/_quart_app.py.jinja +38 -15
  10. piccolo/apps/asgi/commands/templates/app/_sanic_app.py.jinja +34 -11
  11. piccolo/apps/fixtures/commands/dump.py +8 -8
  12. piccolo/apps/fixtures/commands/load.py +5 -5
  13. piccolo/apps/fixtures/commands/shared.py +9 -9
  14. piccolo/apps/migrations/auto/diffable_table.py +12 -12
  15. piccolo/apps/migrations/auto/migration_manager.py +59 -66
  16. piccolo/apps/migrations/auto/operations.py +14 -14
  17. piccolo/apps/migrations/auto/schema_differ.py +35 -34
  18. piccolo/apps/migrations/auto/schema_snapshot.py +3 -4
  19. piccolo/apps/migrations/auto/serialisation.py +27 -24
  20. piccolo/apps/migrations/auto/serialisation_legacy.py +2 -2
  21. piccolo/apps/migrations/commands/backwards.py +1 -2
  22. piccolo/apps/migrations/commands/base.py +12 -12
  23. piccolo/apps/migrations/commands/check.py +2 -3
  24. piccolo/apps/migrations/commands/clean.py +3 -3
  25. piccolo/apps/migrations/commands/forwards.py +1 -2
  26. piccolo/apps/migrations/commands/new.py +6 -6
  27. piccolo/apps/migrations/tables.py +3 -3
  28. piccolo/apps/playground/commands/run.py +72 -13
  29. piccolo/apps/schema/commands/generate.py +49 -49
  30. piccolo/apps/schema/commands/graph.py +5 -5
  31. piccolo/apps/shell/commands/run.py +1 -2
  32. piccolo/apps/sql_shell/commands/run.py +4 -4
  33. piccolo/apps/tester/commands/run.py +3 -3
  34. piccolo/apps/user/commands/change_permissions.py +6 -6
  35. piccolo/apps/user/commands/create.py +7 -7
  36. piccolo/apps/user/commands/list.py +2 -2
  37. piccolo/apps/user/tables.py +8 -8
  38. piccolo/columns/base.py +84 -52
  39. piccolo/columns/choices.py +2 -2
  40. piccolo/columns/column_types.py +299 -177
  41. piccolo/columns/combination.py +15 -12
  42. piccolo/columns/defaults/base.py +4 -4
  43. piccolo/columns/defaults/date.py +4 -3
  44. piccolo/columns/defaults/interval.py +4 -3
  45. piccolo/columns/defaults/time.py +4 -3
  46. piccolo/columns/defaults/timestamp.py +4 -3
  47. piccolo/columns/defaults/timestamptz.py +4 -3
  48. piccolo/columns/defaults/uuid.py +3 -2
  49. piccolo/columns/m2m.py +28 -35
  50. piccolo/columns/readable.py +4 -3
  51. piccolo/columns/reference.py +9 -9
  52. piccolo/conf/apps.py +53 -54
  53. piccolo/custom_types.py +28 -6
  54. piccolo/engine/base.py +14 -14
  55. piccolo/engine/cockroach.py +5 -4
  56. piccolo/engine/finder.py +2 -2
  57. piccolo/engine/postgres.py +20 -19
  58. piccolo/engine/sqlite.py +23 -22
  59. piccolo/query/base.py +30 -29
  60. piccolo/query/functions/__init__.py +12 -0
  61. piccolo/query/functions/aggregate.py +4 -3
  62. piccolo/query/functions/array.py +151 -0
  63. piccolo/query/functions/base.py +3 -3
  64. piccolo/query/functions/datetime.py +22 -22
  65. piccolo/query/functions/string.py +4 -4
  66. piccolo/query/functions/type_conversion.py +30 -15
  67. piccolo/query/methods/alter.py +47 -46
  68. piccolo/query/methods/count.py +11 -10
  69. piccolo/query/methods/create.py +6 -5
  70. piccolo/query/methods/create_index.py +9 -8
  71. piccolo/query/methods/delete.py +7 -6
  72. piccolo/query/methods/drop_index.py +7 -6
  73. piccolo/query/methods/exists.py +6 -5
  74. piccolo/query/methods/indexes.py +4 -4
  75. piccolo/query/methods/insert.py +21 -14
  76. piccolo/query/methods/objects.py +60 -50
  77. piccolo/query/methods/raw.py +7 -6
  78. piccolo/query/methods/refresh.py +8 -7
  79. piccolo/query/methods/select.py +56 -49
  80. piccolo/query/methods/table_exists.py +5 -5
  81. piccolo/query/methods/update.py +8 -7
  82. piccolo/query/mixins.py +56 -61
  83. piccolo/query/operators/json.py +11 -11
  84. piccolo/query/proxy.py +8 -9
  85. piccolo/querystring.py +14 -15
  86. piccolo/schema.py +10 -10
  87. piccolo/table.py +105 -98
  88. piccolo/table_reflection.py +9 -9
  89. piccolo/testing/model_builder.py +16 -13
  90. piccolo/testing/random_builder.py +14 -2
  91. piccolo/testing/test_case.py +4 -4
  92. piccolo/utils/dictionary.py +3 -3
  93. piccolo/utils/encoding.py +5 -5
  94. piccolo/utils/lazy_loader.py +3 -3
  95. piccolo/utils/list.py +7 -8
  96. piccolo/utils/objects.py +4 -6
  97. piccolo/utils/pydantic.py +21 -24
  98. piccolo/utils/sql_values.py +3 -3
  99. piccolo/utils/sync.py +4 -3
  100. piccolo/utils/warnings.py +1 -2
  101. {piccolo-1.27.1.dist-info → piccolo-1.29.0.dist-info}/METADATA +1 -1
  102. {piccolo-1.27.1.dist-info → piccolo-1.29.0.dist-info}/RECORD +132 -131
  103. tests/apps/fixtures/commands/test_dump_load.py +1 -2
  104. tests/apps/migrations/auto/integration/test_migrations.py +32 -7
  105. tests/apps/migrations/auto/test_migration_manager.py +2 -2
  106. tests/apps/migrations/auto/test_schema_differ.py +22 -23
  107. tests/apps/migrations/commands/test_forwards_backwards.py +3 -3
  108. tests/columns/m2m/base.py +20 -49
  109. tests/columns/test_array.py +176 -10
  110. tests/columns/test_boolean.py +2 -4
  111. tests/columns/test_combination.py +29 -1
  112. tests/columns/test_db_column_name.py +2 -2
  113. tests/engine/test_extra_nodes.py +2 -2
  114. tests/engine/test_pool.py +3 -3
  115. tests/engine/test_transaction.py +4 -4
  116. tests/query/test_freeze.py +4 -4
  117. tests/table/instance/test_get_related.py +2 -2
  118. tests/table/test_alter.py +4 -4
  119. tests/table/test_indexes.py +1 -2
  120. tests/table/test_metaclass.py +7 -3
  121. tests/table/test_refresh.py +2 -2
  122. tests/table/test_select.py +58 -0
  123. tests/table/test_str.py +30 -22
  124. tests/table/test_update.py +18 -3
  125. tests/testing/test_model_builder.py +1 -2
  126. tests/testing/test_random_builder.py +5 -0
  127. tests/utils/test_pydantic.py +152 -134
  128. tests/utils/test_table_reflection.py +1 -2
  129. {piccolo-1.27.1.dist-info → piccolo-1.29.0.dist-info}/WHEEL +0 -0
  130. {piccolo-1.27.1.dist-info → piccolo-1.29.0.dist-info}/entry_points.txt +0 -0
  131. {piccolo-1.27.1.dist-info → piccolo-1.29.0.dist-info}/licenses/LICENSE +0 -0
  132. {piccolo-1.27.1.dist-info → piccolo-1.29.0.dist-info}/top_level.txt +0 -0
piccolo/__init__.py CHANGED
@@ -1 +1 @@
1
- __VERSION__ = "1.27.1"
1
+ __VERSION__ = "1.29.0"
@@ -5,7 +5,7 @@ import os
5
5
  import pathlib
6
6
  import string
7
7
  import sys
8
- import typing as t
8
+ from typing import Any
9
9
 
10
10
  import black
11
11
  import jinja2
@@ -46,7 +46,7 @@ def validate_app_name(app_name: str):
46
46
 
47
47
  """
48
48
  for char in app_name:
49
- if not char.lower() in APP_NAME_ALLOWED_CHARACTERS:
49
+ if char.lower() not in APP_NAME_ALLOWED_CHARACTERS:
50
50
  raise ValueError(
51
51
  f"The app name contains a disallowed character: `{char}`. "
52
52
  "It must only include a-z, 0-9, and _ characters."
@@ -81,7 +81,7 @@ def new_app(app_name: str, root: str = ".", register: bool = False):
81
81
  with open(os.path.join(app_root, "__init__.py"), "w"):
82
82
  pass
83
83
 
84
- templates: t.Dict[str, t.Any] = {
84
+ templates: dict[str, Any] = {
85
85
  "piccolo_app.py": {"app_name": app_name},
86
86
  "tables.py": {},
87
87
  }
@@ -2,7 +2,6 @@ from __future__ import annotations
2
2
 
3
3
  import os
4
4
  import shutil
5
- import typing as t
6
5
 
7
6
  import black
8
7
  import colorama
@@ -13,7 +12,7 @@ SERVERS = ["uvicorn", "Hypercorn", "granian"]
13
12
  ROUTER_DEPENDENCIES = {
14
13
  "starlette": ["starlette"],
15
14
  "fastapi": ["fastapi"],
16
- "blacksheep": ["blacksheep"],
15
+ "blacksheep": ["blacksheep[full]"],
17
16
  "litestar": ["litestar"],
18
17
  "esmerald": ["esmerald"],
19
18
  "lilya": ["lilya"],
@@ -28,7 +27,7 @@ def print_instruction(message: str):
28
27
  print(f"{colorama.Fore.CYAN}{message}{colorama.Fore.RESET}")
29
28
 
30
29
 
31
- def get_options_string(options: t.List[str]):
30
+ def get_options_string(options: list[str]):
32
31
  return ", ".join(f"{name} [{index}]" for index, name in enumerate(options))
33
32
 
34
33
 
@@ -1,10 +1,9 @@
1
- import typing as t
1
+ from typing import Any
2
2
 
3
+ from blacksheep.exceptions import HTTPException
3
4
  from blacksheep.server import Application
4
5
  from blacksheep.server.bindings import FromJSON
5
- from blacksheep.server.openapi.v3 import OpenAPIHandler
6
- from blacksheep.server.responses import json
7
- from openapidocs.v3 import Info
6
+ from blacksheep.server.openapi.v3 import OpenAPIHandler, Info
8
7
  from piccolo.engine import engine_finder
9
8
  from piccolo_admin.endpoints import create_admin
10
9
  from piccolo_api.crud.serializers import create_pydantic_model
@@ -34,38 +33,67 @@ app.serve_files("static", root_path="/static")
34
33
  app.router.add_get("/", home)
35
34
 
36
35
 
37
- TaskModelIn: t.Any = create_pydantic_model(table=Task, model_name="TaskModelIn")
38
- TaskModelOut: t.Any = create_pydantic_model(
39
- table=Task, include_default_columns=True, model_name="TaskModelOut"
36
+ TaskModelIn: Any = create_pydantic_model(
37
+ table=Task,
38
+ model_name="TaskModelIn",
40
39
  )
41
- TaskModelPartial: t.Any = create_pydantic_model(
42
- table=Task, model_name="TaskModelPartial", all_optional=True
40
+ TaskModelOut: Any = create_pydantic_model(
41
+ table=Task,
42
+ include_default_columns=True,
43
+ model_name="TaskModelOut",
43
44
  )
45
+ TaskModelPartial: Any = (
46
+ create_pydantic_model(
47
+ table=Task,
48
+ model_name="TaskModelPartial",
49
+ all_optional=True,
50
+ ),
51
+ )
52
+
53
+
54
+ # Check if the record is None. Use for query callback
55
+ def check_record_not_found(result: dict[str, Any]) -> dict[str, Any]:
56
+ if result is None:
57
+ raise HTTPException(status=404)
58
+ return result
44
59
 
45
60
 
46
61
  @app.router.get("/tasks/")
47
- async def tasks() -> t.List[TaskModelOut]:
48
- return await Task.select().order_by(Task._meta.primary_key, ascending=False)
62
+ async def tasks() -> list[TaskModelOut]:
63
+ tasks = await Task.select().order_by(Task._meta.primary_key, ascending=False)
64
+ return [TaskModelOut(**task) for task in tasks]
65
+
66
+
67
+ @app.router.get("/tasks/{task_id}/")
68
+ async def single_task(task_id: int) -> TaskModelOut:
69
+ task = (
70
+ await Task.select()
71
+ .where(Task._meta.primary_key == task_id)
72
+ .first()
73
+ .callback(check_record_not_found)
74
+ )
75
+ return TaskModelOut(**task)
49
76
 
50
77
 
51
78
  @app.router.post("/tasks/")
52
79
  async def create_task(task_model: FromJSON[TaskModelIn]) -> TaskModelOut:
53
- task = Task(**task_model.value.dict())
80
+ task = Task(**task_model.value.model_dump())
54
81
  await task.save()
55
82
  return TaskModelOut(**task.to_dict())
56
83
 
57
84
 
58
85
  @app.router.put("/tasks/{task_id}/")
59
86
  async def put_task(task_id: int, task_model: FromJSON[TaskModelIn]) -> TaskModelOut:
60
- task = await Task.objects().get(Task._meta.primary_key == task_id)
61
- if not task:
62
- return json({}, status=404)
87
+ task = (
88
+ await Task.objects()
89
+ .get(Task._meta.primary_key == task_id)
90
+ .callback(check_record_not_found)
91
+ )
63
92
 
64
- for key, value in task_model.value.dict().items():
93
+ for key, value in task_model.value.model_dump().items():
65
94
  setattr(task, key, value)
66
95
 
67
96
  await task.save()
68
-
69
97
  return TaskModelOut(**task.to_dict())
70
98
 
71
99
 
@@ -73,29 +101,29 @@ async def put_task(task_id: int, task_model: FromJSON[TaskModelIn]) -> TaskModel
73
101
  async def patch_task(
74
102
  task_id: int, task_model: FromJSON[TaskModelPartial]
75
103
  ) -> TaskModelOut:
76
- task = await Task.objects().get(Task._meta.primary_key == task_id)
77
- if not task:
78
- return json({}, status=404)
104
+ task = (
105
+ await Task.objects()
106
+ .get(Task._meta.primary_key == task_id)
107
+ .callback(check_record_not_found)
108
+ )
79
109
 
80
- for key, value in task_model.value.dict().items():
110
+ for key, value in task_model.value.model_dump().items():
81
111
  if value is not None:
82
112
  setattr(task, key, value)
83
113
 
84
114
  await task.save()
85
-
86
115
  return TaskModelOut(**task.to_dict())
87
116
 
88
117
 
89
118
  @app.router.delete("/tasks/{task_id}/")
90
- async def delete_task(task_id: int):
91
- task = await Task.objects().get(Task._meta.primary_key == task_id)
92
- if not task:
93
- return json({}, status=404)
94
-
119
+ async def delete_task(task_id: int) -> None:
120
+ task = (
121
+ await Task.objects()
122
+ .get(Task._meta.primary_key == task_id)
123
+ .callback(check_record_not_found)
124
+ )
95
125
  await task.remove()
96
126
 
97
- return json({})
98
-
99
127
 
100
128
  async def open_database_connection_pool(application):
101
129
  try:
@@ -1,12 +1,12 @@
1
- import typing as t
1
+ from typing import Any
2
2
  from pathlib import Path
3
3
 
4
4
  from esmerald import (
5
5
  APIView,
6
6
  Esmerald,
7
7
  Gateway,
8
+ HTTPException,
8
9
  Include,
9
- JSONResponse,
10
10
  delete,
11
11
  get,
12
12
  post,
@@ -38,45 +38,72 @@ async def close_database_connection_pool():
38
38
  print("Unable to connect to the database")
39
39
 
40
40
 
41
- TaskModelIn: t.Any = create_pydantic_model(table=Task, model_name="TaskModelIn")
42
- TaskModelOut: t.Any = create_pydantic_model(
43
- table=Task, include_default_columns=True, model_name="TaskModelOut"
41
+ TaskModelIn: Any = create_pydantic_model(
42
+ table=Task,
43
+ model_name="TaskModelIn",
44
44
  )
45
+ TaskModelOut: Any = create_pydantic_model(
46
+ table=Task,
47
+ include_default_columns=True,
48
+ model_name="TaskModelOut",
49
+ )
50
+
51
+
52
+ # Check if the record is None. Use for query callback
53
+ def check_record_not_found(result: dict[str, Any]) -> dict[str, Any]:
54
+ if result is None:
55
+ raise HTTPException(
56
+ detail="Record not found",
57
+ status_code=404,
58
+ )
59
+ return result
45
60
 
46
61
 
47
62
  class TaskAPIView(APIView):
48
63
  path: str = "/"
49
- tags: str = ["Task"]
64
+ tags: list[str] = ["Task"]
50
65
 
51
66
  @get("/")
52
- async def tasks(self) -> t.List[TaskModelOut]:
53
- return await Task.select().order_by(Task._meta.primary_key, ascending=False)
67
+ async def tasks(self) -> list[TaskModelOut]:
68
+ tasks = await Task.select().order_by(Task._meta.primary_key, ascending=False)
69
+ return [TaskModelOut(**task) for task in tasks]
70
+
71
+ @get("/{task_id}")
72
+ async def single_task(self, task_id: int) -> TaskModelOut:
73
+ task = (
74
+ await Task.select()
75
+ .where(Task._meta.primary_key == task_id)
76
+ .first()
77
+ .callback(check_record_not_found)
78
+ )
79
+ return TaskModelOut(**task)
54
80
 
55
81
  @post("/")
56
82
  async def create_task(self, payload: TaskModelIn) -> TaskModelOut:
57
- task = Task(**payload.dict())
83
+ task = Task(**payload.model_dump())
58
84
  await task.save()
59
- return task.to_dict()
85
+ return TaskModelOut(**task.to_dict())
60
86
 
61
87
  @put("/{task_id}")
62
88
  async def update_task(self, payload: TaskModelIn, task_id: int) -> TaskModelOut:
63
- task = await Task.objects().get(Task._meta.primary_key == task_id)
64
- if not task:
65
- return JSONResponse({}, status_code=404)
66
-
67
- for key, value in payload.dict().items():
89
+ task = (
90
+ await Task.objects()
91
+ .get(Task._meta.primary_key == task_id)
92
+ .callback(check_record_not_found)
93
+ )
94
+ for key, value in payload.model_dump().items():
68
95
  setattr(task, key, value)
69
96
 
70
97
  await task.save()
71
-
72
- return task.to_dict()
98
+ return TaskModelOut(**task.to_dict())
73
99
 
74
100
  @delete("/{task_id}")
75
101
  async def delete_task(self, task_id: int) -> None:
76
- task = await Task.objects().get(Task._meta.primary_key == task_id)
77
- if not task:
78
- return JSONResponse({}, status_code=404)
79
-
102
+ task = (
103
+ await Task.objects()
104
+ .get(Task._meta.primary_key == task_id)
105
+ .callback(check_record_not_found)
106
+ )
80
107
  await task.remove()
81
108
 
82
109
 
@@ -1,11 +1,10 @@
1
1
  import os
2
- import typing as t
2
+ from typing import Any
3
3
 
4
4
  import falcon.asgi
5
5
  from hypercorn.middleware import DispatcherMiddleware
6
6
  from piccolo.engine import engine_finder
7
7
  from piccolo_admin.endpoints import create_admin
8
- from piccolo_api.crud.endpoints import PiccoloCRUD
9
8
 
10
9
  from home.endpoints import HomeEndpoint
11
10
  from home.piccolo_app import APP_CONFIG
@@ -28,25 +27,82 @@ async def close_database_connection_pool():
28
27
  print("Unable to connect to the database")
29
28
 
30
29
 
30
+ # Check if the record is None. Use for query callback
31
+ def check_record_not_found(result: dict[str, Any]) -> dict[str, Any]:
32
+ if result is None:
33
+ raise falcon.HTTPNotFound()
34
+ return result
35
+
36
+
31
37
  class LifespanMiddleware:
32
38
  async def process_startup(
33
- self, scope: t.Dict[str, t.Any], event: t.Dict[str, t.Any]
39
+ self, scope: dict[str, Any], event: dict[str, Any]
34
40
  ) -> None:
35
41
  await open_database_connection_pool()
36
42
 
37
43
  async def process_shutdown(
38
- self, scope: t.Dict[str, t.Any], event: t.Dict[str, t.Any]
44
+ self, scope: dict[str, Any], event: dict[str, Any]
39
45
  ) -> None:
40
46
  await close_database_connection_pool()
41
47
 
42
48
 
43
- app: t.Any = falcon.asgi.App(middleware=LifespanMiddleware())
49
+ class TaskCollectionResource:
50
+ async def on_get(self, req, resp):
51
+ tasks = await Task.select().order_by(Task._meta.primary_key, ascending=False)
52
+ resp.media = tasks
53
+
54
+ async def on_post(self, req, resp):
55
+ data = await req.media
56
+ task = Task(**data)
57
+ await task.save()
58
+ resp.status = falcon.HTTP_201
59
+ resp.media = task.to_dict()
60
+
61
+
62
+ class TaskItemResource:
63
+ async def on_get(self, req, resp, task_id):
64
+ task = (
65
+ await Task.select()
66
+ .where(Task._meta.primary_key == task_id)
67
+ .first()
68
+ .callback(check_record_not_found)
69
+ )
70
+ resp.status = falcon.HTTP_200
71
+ resp.media = task
72
+
73
+ async def on_put(self, req, resp, task_id):
74
+ task = (
75
+ await Task.objects()
76
+ .get(Task._meta.primary_key == task_id)
77
+ .callback(check_record_not_found)
78
+ )
79
+
80
+ data = await req.media
81
+ for key, value in data.items():
82
+ setattr(task, key, value)
83
+
84
+ await task.save()
85
+ resp.status = falcon.HTTP_200
86
+ resp.media = task.to_dict()
87
+
88
+ async def on_delete(self, req, resp, task_id):
89
+ task = (
90
+ await Task.objects()
91
+ .get(Task._meta.primary_key == task_id)
92
+ .callback(check_record_not_found)
93
+ )
94
+ resp.status = falcon.HTTP_204
95
+ await task.remove()
96
+
97
+
98
+ app: Any = falcon.asgi.App(middleware=LifespanMiddleware())
44
99
  app.add_static_route("/static", directory=os.path.abspath("static"))
45
100
  app.add_route("/", HomeEndpoint())
101
+ app.add_route("/tasks/", TaskCollectionResource())
102
+ app.add_route("/tasks/{task_id:int}", TaskItemResource())
46
103
 
47
- PICCOLO_CRUD: t.Any = PiccoloCRUD(table=Task)
48
104
 
49
- # enable the Admin and PiccoloCrud app using DispatcherMiddleware
105
+ # enable the admin application using DispatcherMiddleware
50
106
  app = DispatcherMiddleware( # type: ignore
51
107
  {
52
108
  "/admin": create_admin(
@@ -54,7 +110,6 @@ app = DispatcherMiddleware( # type: ignore
54
110
  # Required when running under HTTPS:
55
111
  # allowed_hosts=['my_site.com']
56
112
  ),
57
- "/tasks": PICCOLO_CRUD,
58
113
  "": app,
59
114
  }
60
115
  )
@@ -1,8 +1,8 @@
1
- import typing as t
2
1
  from contextlib import asynccontextmanager
2
+ from typing import Any
3
3
 
4
- from fastapi import FastAPI
5
- from fastapi.responses import JSONResponse
4
+ from fastapi import FastAPI, status
5
+ from fastapi.exceptions import HTTPException
6
6
  from piccolo.engine import engine_finder
7
7
  from piccolo_admin.endpoints import create_admin
8
8
  from piccolo_api.crud.serializers import create_pydantic_model
@@ -54,47 +54,74 @@ app = FastAPI(
54
54
  )
55
55
 
56
56
 
57
- TaskModelIn: t.Any = create_pydantic_model(
57
+ TaskModelIn: Any = create_pydantic_model(
58
58
  table=Task,
59
59
  model_name="TaskModelIn",
60
60
  )
61
- TaskModelOut: t.Any = create_pydantic_model(
62
- table=Task, include_default_columns=True, model_name="TaskModelOut"
61
+
62
+ TaskModelOut: Any = create_pydantic_model(
63
+ table=Task,
64
+ include_default_columns=True,
65
+ model_name="TaskModelOut",
63
66
  )
64
67
 
65
68
 
66
- @app.get("/tasks/", response_model=t.List[TaskModelOut])
69
+ # Check if the record is None. Use for query callback
70
+ def check_record_not_found(result: dict[str, Any]) -> dict[str, Any]:
71
+ if result is None:
72
+ raise HTTPException(
73
+ detail="Record not found",
74
+ status_code=status.HTTP_404_NOT_FOUND,
75
+ )
76
+ return result
77
+
78
+
79
+ @app.get("/tasks/", response_model=list[TaskModelOut], tags=["Task"])
67
80
  async def tasks():
68
- return await Task.select().order_by(Task.id)
81
+ return await Task.select().order_by(Task._meta.primary_key, ascending=False)
82
+
83
+
84
+ @app.get("/tasks/{task_id}/", response_model=TaskModelOut, tags=["Task"])
85
+ async def single_task(task_id: int):
86
+ task = (
87
+ await Task.select()
88
+ .where(Task._meta.primary_key == task_id)
89
+ .first()
90
+ .callback(check_record_not_found)
91
+ )
92
+ return task
69
93
 
70
94
 
71
- @app.post("/tasks/", response_model=TaskModelOut)
95
+ @app.post("/tasks/", response_model=TaskModelOut, tags=["Task"])
72
96
  async def create_task(task_model: TaskModelIn):
73
- task = Task(**task_model.dict())
97
+ task = Task(**task_model.model_dump())
74
98
  await task.save()
75
99
  return task.to_dict()
76
100
 
77
101
 
78
- @app.put("/tasks/{task_id}/", response_model=TaskModelOut)
102
+ @app.put("/tasks/{task_id}/", response_model=TaskModelOut, tags=["Task"])
79
103
  async def update_task(task_id: int, task_model: TaskModelIn):
80
- task = await Task.objects().get(Task._meta.primary_key == task_id)
81
- if not task:
82
- return JSONResponse({}, status_code=404)
83
-
84
- for key, value in task_model.dict().items():
104
+ task = (
105
+ await Task.objects()
106
+ .get(Task._meta.primary_key == task_id)
107
+ .callback(check_record_not_found)
108
+ )
109
+ for key, value in task_model.model_dump().items():
85
110
  setattr(task, key, value)
86
111
 
87
112
  await task.save()
88
-
89
113
  return task.to_dict()
90
114
 
91
115
 
92
- @app.delete("/tasks/{task_id}/")
116
+ @app.delete(
117
+ "/tasks/{task_id}/",
118
+ status_code=status.HTTP_204_NO_CONTENT,
119
+ tags=["Task"],
120
+ )
93
121
  async def delete_task(task_id: int):
94
- task = await Task.objects().get(Task._meta.primary_key == task_id)
95
- if not task:
96
- return JSONResponse({}, status_code=404)
97
-
122
+ task = (
123
+ await Task.objects()
124
+ .get(Task._meta.primary_key == task_id)
125
+ .callback(check_record_not_found)
126
+ )
98
127
  await task.remove()
99
-
100
- return JSONResponse({})
@@ -1,4 +1,4 @@
1
- import typing as t
1
+ from typing import Any
2
2
 
3
3
  from litestar import Litestar, asgi, delete, get, patch, post
4
4
  from litestar.contrib.jinja import JinjaTemplateEngine
@@ -21,11 +21,11 @@ you can use `create_pydantic_model` as in other asgi templates
21
21
 
22
22
  from piccolo.utils.pydantic import create_pydantic_model
23
23
 
24
- TaskModelIn: t.Any = create_pydantic_model(
24
+ TaskModelIn: Any = create_pydantic_model(
25
25
  table=Task,
26
26
  model_name="TaskModelIn",
27
27
  )
28
- TaskModelOut: t.Any = create_pydantic_model(
28
+ TaskModelOut: Any = create_pydantic_model(
29
29
  table=Task,
30
30
  include_default_columns=True,
31
31
  model_name="TaskModelOut",
@@ -50,12 +50,30 @@ async def admin(scope: "Scope", receive: "Receive", send: "Send") -> None:
50
50
  await create_admin(tables=APP_CONFIG.table_classes)(scope, receive, send)
51
51
 
52
52
 
53
+ # Check if the record is None. Use for query callback
54
+ def check_record_not_found(result: dict[str, Any]) -> dict[str, Any]:
55
+ if result is None:
56
+ raise NotFoundException(detail="Record not found")
57
+ return result
58
+
59
+
53
60
  @get("/tasks", tags=["Task"])
54
- async def tasks() -> t.List[TaskModelOut]:
61
+ async def tasks() -> list[TaskModelOut]:
55
62
  tasks = await Task.select().order_by(Task._meta.primary_key, ascending=False)
56
63
  return [TaskModelOut(**task) for task in tasks]
57
64
 
58
65
 
66
+ @get("/tasks/{task_id:int}", tags=["Task"])
67
+ async def single_task(task_id: int) -> TaskModelOut:
68
+ task = (
69
+ await Task.select()
70
+ .where(Task._meta.primary_key == task_id)
71
+ .first()
72
+ .callback(check_record_not_found)
73
+ )
74
+ return TaskModelOut(**task)
75
+
76
+
59
77
  @post("/tasks", tags=["Task"])
60
78
  async def create_task(data: TaskModelIn) -> TaskModelOut:
61
79
  task = Task(**data.model_dump())
@@ -65,9 +83,12 @@ async def create_task(data: TaskModelIn) -> TaskModelOut:
65
83
 
66
84
  @patch("/tasks/{task_id:int}", tags=["Task"])
67
85
  async def update_task(task_id: int, data: TaskModelIn) -> TaskModelOut:
68
- task = await Task.objects().get(Task._meta.primary_key == task_id)
69
- if not task:
70
- raise NotFoundException("Task does not exist")
86
+ task = (
87
+ await Task.objects()
88
+ .get(Task._meta.primary_key == task_id)
89
+ .callback(check_record_not_found)
90
+ )
91
+
71
92
  for key, value in data.model_dump().items():
72
93
  setattr(task, key, value)
73
94
 
@@ -77,9 +98,11 @@ async def update_task(task_id: int, data: TaskModelIn) -> TaskModelOut:
77
98
 
78
99
  @delete("/tasks/{task_id:int}", tags=["Task"])
79
100
  async def delete_task(task_id: int) -> None:
80
- task = await Task.objects().get(Task._meta.primary_key == task_id)
81
- if not task:
82
- raise NotFoundException("Task does not exist")
101
+ task = (
102
+ await Task.objects()
103
+ .get(Task._meta.primary_key == task_id)
104
+ .callback(check_record_not_found)
105
+ )
83
106
  await task.remove()
84
107
 
85
108
 
@@ -104,6 +127,7 @@ app = Litestar(
104
127
  admin,
105
128
  home,
106
129
  tasks,
130
+ single_task,
107
131
  create_task,
108
132
  update_task,
109
133
  delete_task,