Skip to content

Commit

Permalink
v1 (#258)
Browse files Browse the repository at this point in the history
* pydantic v2 support (#245)

* pydantic_v2_support

* pin to Piccolo v1

* add comment

* remove KeyError Exception

* upgrade coverage

* fix type warnings with `nested`

* fix mypy warnings

* update black

* replacement for `outer_type`

* change assertion

---------

Co-authored-by: Daniel Townsend <[email protected]>

* update github actions

* bumped version

* fix `_get_type` - check for `NoneType` (#254)

* fix `_get_type` - check for `NoneType`

* add tests

* fix typo

* add a test for the new union syntax

* also check for `UnionType`

* bumped version

* update JSON schema (#257)

* bumped version

* Update requirements.txt

---------

Co-authored-by: sinisaos <[email protected]>
  • Loading branch information
dantownsend and sinisaos authored Oct 20, 2023
1 parent 6031dbb commit 3ae09ac
Show file tree
Hide file tree
Showing 24 changed files with 389 additions and 213 deletions.
62 changes: 31 additions & 31 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
@@ -1,41 +1,41 @@
name: "CodeQL"

on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
schedule:
- cron: "22 2 * * 2"
push:
branches: ["master", "v1"]
pull_request:
branches: ["master", "v1"]
schedule:
- cron: "22 2 * * 2"

jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write

strategy:
fail-fast: false
matrix:
language: [ python ]
strategy:
fail-fast: false
matrix:
language: [python]

steps:
- name: Checkout
uses: actions/checkout@v3
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
queries: +security-and-quality
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
queries: +security-and-quality

- name: Autobuild
uses: github/codeql-action/autobuild@v2
- name: Autobuild
uses: github/codeql-action/autobuild@v2

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
with:
category: "/language:${{ matrix.language }}"
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
with:
category: "/language:${{ matrix.language }}"
38 changes: 19 additions & 19 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
name: Release

on:
release:
types:
- created
workflow_dispatch: {}
release:
types:
- created
workflow_dispatch: {}

jobs:
release:
name: "Publish release"
runs-on: "ubuntu-latest"
release:
name: "Publish release"
runs-on: "ubuntu-latest"

steps:
- uses: "actions/checkout@v2"
- uses: "actions/setup-python@v1"
with:
python-version: 3.11
- name: "Install dependencies"
run: "pip install -r requirements/dev-requirements.txt"
- name: "Publish to PyPI"
run: "./scripts/release.sh"
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
steps:
- uses: "actions/checkout@v4"
- uses: "actions/setup-python@v4"
with:
python-version: 3.11
- name: "Install dependencies"
run: "pip install -r requirements/dev-requirements.txt"
- name: "Publish to PyPI"
run: "./scripts/release.sh"
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
16 changes: 8 additions & 8 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@ name: Test Suite

on:
push:
branches: ["master"]
branches: ["master", "v1"]
pull_request:
branches: ["master"]
branches: ["master", "v1"]

jobs:
linters:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: 3.11
- name: Install dependencies
Expand Down Expand Up @@ -49,7 +49,7 @@ jobs:
- 5432:5432

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
Expand All @@ -76,9 +76,9 @@ jobs:
python-version: ["3.8", "3.9", "3.10", "3.11"]

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
Expand All @@ -89,5 +89,5 @@ jobs:
- name: Test with pytest, SQLite
run: ./scripts/test-sqlite.sh
- name: Upload coverage
uses: codecov/codecov-action@v1
uses: codecov/codecov-action@v3
if: matrix.python-version == '3.11'
22 changes: 22 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,28 @@
Changes
=======

1.0a3
-----

Using the new ``json_schema_extra`` argument for ``create_pydantic_model``.

-------------------------------------------------------------------------------

1.0a2
-----

Fixed a bug with extracting the type from an optional type. Thanks to @sinisaos
for discovering this issue.

-------------------------------------------------------------------------------

1.0a1
-----

Pydantic v2 support - many thanks to @sinisaos for this.

-------------------------------------------------------------------------------

0.58.0
------

Expand Down
2 changes: 1 addition & 1 deletion piccolo_api/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__VERSION__ = "0.58.0"
__VERSION__ = "1.0a3"
52 changes: 29 additions & 23 deletions piccolo_api/crud/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
from piccolo.query.methods.select import Select
from piccolo.table import Table
from piccolo.utils.encoding import dump_json
from pydantic.error_wrappers import ValidationError
from starlette.requests import Request
from starlette.responses import JSONResponse, Response
from starlette.routing import Route, Router
Expand All @@ -38,7 +37,7 @@
)

from .exceptions import MalformedQuery, db_exception_handler
from .serializers import Config, create_pydantic_model
from .serializers import create_pydantic_model
from .validators import Validators, apply_validators

if t.TYPE_CHECKING: # pragma: no cover
Expand Down Expand Up @@ -300,14 +299,14 @@ def pydantic_model(self) -> t.Type[pydantic.BaseModel]:
self.table,
model_name=f"{self.table.__name__}In",
exclude_columns=(self.table._meta.primary_key,),
**self.schema_extra,
json_schema_extra={"extra": self.schema_extra},
)

def _pydantic_model_output(
self,
include_readable: bool = False,
include_columns: t.Tuple[Column, ...] = (),
nested: t.Union[bool, t.Tuple[Column, ...]] = False,
nested: t.Union[bool, t.Tuple[ForeignKey, ...]] = False,
) -> t.Type[pydantic.BaseModel]:
return create_pydantic_model(
self.table,
Expand Down Expand Up @@ -343,8 +342,8 @@ def pydantic_model_plural(
self,
include_readable=False,
include_columns: t.Tuple[Column, ...] = (),
nested: t.Union[bool, t.Tuple[Column, ...]] = False,
):
nested: t.Union[bool, t.Tuple[ForeignKey, ...]] = False,
) -> t.Type[pydantic.BaseModel]:
"""
This is for when we want to serialise many copies of the model.
"""
Expand All @@ -358,7 +357,9 @@ def pydantic_model_plural(
)
return pydantic.create_model(
str(self.table.__name__) + "Plural",
__config__=Config,
__config__=pydantic.config.ConfigDict(
arbitrary_types_allowed=True
),
rows=(t.List[base_model], None),
)

Expand All @@ -367,7 +368,7 @@ async def get_schema(self, request: Request) -> JSONResponse:
"""
Return a representation of the model, so a UI can generate a form.
"""
return JSONResponse(self.pydantic_model.schema())
return JSONResponse(self.pydantic_model.model_json_schema())

###########################################################################

Expand Down Expand Up @@ -713,7 +714,7 @@ def _apply_filters(
"""
fields = params.fields
if fields:
model_dict = self.pydantic_model_optional(**fields).dict()
model_dict = self.pydantic_model_optional(**fields).model_dump()
for field_name in fields.keys():
value = model_dict.get(field_name, ...)
if value is ...:
Expand Down Expand Up @@ -778,7 +779,9 @@ async def get_all(
nested: t.Union[bool, t.Tuple[Column, ...]]
if visible_fields:
nested = tuple(
i for i in visible_fields if len(i._meta.call_chain) > 0
i._meta.call_chain[-1]
for i in visible_fields
if len(i._meta.call_chain) > 0
)
else:
visible_fields = self.table._meta.columns
Expand Down Expand Up @@ -865,7 +868,7 @@ async def get_all(
include_readable=include_readable,
include_columns=tuple(visible_fields),
nested=nested,
)(rows=rows).json()
)(rows=rows).model_dump_json()
return CustomJSONResponse(json, headers=headers)

###########################################################################
Expand Down Expand Up @@ -894,19 +897,19 @@ async def post_single(
cleaned_data = self._clean_data(data)
try:
model = self.pydantic_model(**cleaned_data)
except ValidationError as exception:
except pydantic.ValidationError as exception:
return Response(str(exception), status_code=400)

if issubclass(self.table, BaseUser):
try:
user = await self.table.create_user(**model.dict())
user = await self.table.create_user(**model.model_dump())
json = dump_json({"id": user.id})
return CustomJSONResponse(json, status_code=201)
except Exception as e:
return Response(f"Error: {e}", status_code=400)
else:
try:
row = self.table(**model.dict())
row = self.table(**model.model_dump())
if self._hook_map:
row = await execute_post_hooks(
hooks=self._hook_map,
Expand Down Expand Up @@ -969,7 +972,7 @@ async def get_new(self, request: Request) -> CustomJSONResponse:
row_dict.pop(column_name)

return CustomJSONResponse(
self.pydantic_model_optional(**row_dict).json()
self.pydantic_model_optional(**row_dict).model_dump_json()
)

###########################################################################
Expand Down Expand Up @@ -1053,11 +1056,13 @@ async def get_single(self, request: Request, row_id: PK_TYPES) -> Response:
return Response(str(exception), status_code=400)

# Visible fields
nested: t.Union[bool, t.Tuple[Column, ...]]
nested: t.Union[bool, t.Tuple[ForeignKey, ...]]
visible_fields = split_params.visible_fields
if visible_fields:
nested = tuple(
i for i in visible_fields if len(i._meta.call_chain) > 0
i._meta.call_chain[-1]
for i in visible_fields
if len(i._meta.call_chain) > 0
)
else:
visible_fields = self.table._meta.columns
Expand Down Expand Up @@ -1098,7 +1103,7 @@ async def get_single(self, request: Request, row_id: PK_TYPES) -> Response:
include_readable=split_params.include_readable,
include_columns=tuple(visible_fields),
nested=nested,
)(**row).json()
)(**row).model_dump_json()
)

@apply_validators
Expand All @@ -1113,7 +1118,7 @@ async def put_single(

try:
model = self.pydantic_model(**cleaned_data)
except ValidationError as exception:
except pydantic.ValidationError as exception:
return Response(str(exception), status_code=400)

cls = self.table
Expand All @@ -1123,7 +1128,6 @@ async def put_single(
}

try:

await cls.update(values).where(
cls._meta.primary_key == row_id
).run()
Expand All @@ -1143,7 +1147,7 @@ async def patch_single(

try:
model = self.pydantic_model_optional(**cleaned_data)
except ValidationError as exception:
except pydantic.ValidationError as exception:
return Response(str(exception), status_code=400)

cls = self.table
Expand All @@ -1168,7 +1172,9 @@ async def patch_single(
for key in data.keys()
}
except AttributeError:
unrecognised_keys = set(data.keys()) - set(model.dict().keys())
unrecognised_keys = set(data.keys()) - set(
model.model_dump().keys()
)
return Response(
f"Unrecognised keys - {unrecognised_keys}.",
status_code=400,
Expand All @@ -1195,7 +1201,7 @@ async def patch_single(
)
assert new_row
return CustomJSONResponse(
self.pydantic_model(**new_row).json()
self.pydantic_model(**new_row).model_dump_json()
)
except ValueError:
return Response(
Expand Down
Loading

0 comments on commit 3ae09ac

Please sign in to comment.