Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

pydantic v2 support #327

Closed
wants to merge 11 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/playwright.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
- name: "Setup Node"
uses: actions/setup-node@v3
with:
node-version: 18
node-version: 16
- name: "Install all dependencies"
run: |
python -m pip install --upgrade pip
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
python-version: 3.11
- uses: actions/setup-node@v3
with:
node-version: 18
node-version: 16
- name: "Install dependencies"
run: |
pip install -r requirements/dev-requirements.txt
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements/requirements.txt
pip install -r requirements/dev-requirements.txt
pip install -r requirements/test-requirements.txt
- name: Lint
Expand Down
8 changes: 8 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
Changes
=======

0.58.0
------

The default rate limiting is now more aggressive. This can be overridden using
the ``rate_limit_provider`` argument of ``create_admin``.

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

0.57.0
------

Expand Down
3 changes: 2 additions & 1 deletion admin_ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"is-svg": "^4.3.1",
"js-cookie": "^2.2.1",
"json-bigint": "^1.0.0",
"moment": "^2.29.4",
"ssri": "^8.0.1",
"vue": "^2.7.14",
"vue-class-component": "^7.2.6",
Expand Down Expand Up @@ -57,4 +58,4 @@
"vue-template-compiler": "^2.6.12"
},
"license": "ISC"
}
}
6 changes: 4 additions & 2 deletions admin_ui/src/components/DurationWidget.vue
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@
</div>
</template>

<script>
<script lang="ts">
import { secondsToISO8601Duration } from "../utils"

const MINUTE = 60
const HOUR = MINUTE * 60
const DAY = HOUR * 24
Expand Down Expand Up @@ -83,7 +85,7 @@ export default {
this.hours * HOUR +
this.days * DAY +
this.weeks * WEEK
this.$emit("newTimedelta", timedelta)
this.$emit("newTimedelta", secondsToISO8601Duration(timedelta))
},
setupValues(timedelta) {
this.weeks = Math.floor(timedelta / WEEK)
Expand Down
40 changes: 27 additions & 13 deletions admin_ui/src/components/InputField.vue
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,21 @@
></textarea>
</div>

<div v-else-if="format == 'duration'">
<template>
<OperatorField :columnName="columnName" v-if="isFilter" />
<DurationWidget
v-bind:timedelta="convertDurationToSeconds"
v-on:newTimedelta="localValue = $event"
/>
<input
type="hidden"
v-bind:name="columnName"
:value="convertSecondsToDuration"
/>
</template>
</div>

<div v-else-if="format == 'json'">
<textarea
v-model="localValue"
Expand Down Expand Up @@ -136,19 +151,7 @@
</template>

<template v-else-if="type == 'number'">
<template v-if="format == 'time-delta'">
<OperatorField :columnName="columnName" v-if="isFilter" />
<DurationWidget
v-bind:timedelta="localValue"
v-on:newTimedelta="localValue = $event"
/>
<input
type="hidden"
v-bind:name="columnName"
v-model="localValue"
/>
</template>
<template v-else>
<template>
<OperatorField :columnName="columnName" v-if="isFilter" />
<input
type="text"
Expand Down Expand Up @@ -182,6 +185,7 @@
import Vue, { PropType } from "vue"
import axios from "axios"
import flatPickr from "vue-flatpickr-component"
import moment from "moment"
import { VueEditor } from "vue2-editor"

import ArrayWidget from "./ArrayWidget.vue"
Expand All @@ -196,6 +200,7 @@ import {
APIResponseMessage,
MediaViewerConfig
} from "@/interfaces"
import { secondsToISO8601Duration } from "../utils"

export default Vue.extend({
props: {
Expand Down Expand Up @@ -276,6 +281,15 @@ export default Vue.extend({
}
},
computed: {
schema() {
return this.$store.state.schema
},
convertDurationToSeconds() {
return moment.duration(this.localValue).asSeconds()
},
convertSecondsToDuration() {
return secondsToISO8601Duration(this.localValue)
},
placeholder() {
if (this.isFilter) {
return "All"
Expand Down
64 changes: 48 additions & 16 deletions admin_ui/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
import { Schema, OrderByConfig } from "@/interfaces"
import router from "./router"
import moment from 'moment'

export function readableInterval(timeRange: number) {
/**
* We need to parse ISO 8601 duration string (e.g. P17DT14706S) to
* seconds and then convert to human readable interval.
*
* @param timeValue ISO 8601 duration string which we need to parse.
* @returns A string of nicelly formated timeValue
*/
export function readableInterval(timeValue: string) {
const parsedTimeValue = moment.duration(timeValue)
const timeRange = parsedTimeValue.asSeconds()
if (timeRange === 0) {
return "0 seconds"
}
Expand Down Expand Up @@ -30,6 +40,17 @@ export function readableInterval(timeRange: number) {
)
}

/**
* We need to convert interval from seconds to ISO 8601 duration
* string (e.g. P17DT14706S) for form fields.
*
* @param value interval seconds which we need to convert.
* @returns ISO 8601 duration string
*/
export function secondsToISO8601Duration(value: number) {
return moment.duration(value, 'seconds').toISOString()
}

export function titleCase(value: string) {
return value
.toLowerCase()
Expand Down Expand Up @@ -106,32 +127,43 @@ export function convertFormValue(params: {
value = null
} else if (schema?.properties[key].type == "array") {
value = JSON.parse(String(value))
} else if (schema?.properties[key].format == "uuid" && value == "") {
} else if (schema?.properties[key].format == "uuid" &&
schema?.properties[key].extra["nullable"] == true
&& value == "") {
value = null
} else if (schema?.properties[key].format == "email" && value == "") {
} else if (schema?.properties[key].format == "email" &&
schema?.properties[key].extra["nullable"] == true
&& value == "") {
value = null
} else if (schema?.properties[key].format == "date-time" && value == "") {
} else if (schema?.properties[key].format == "date-time" &&
schema?.properties[key].extra["nullable"] == true
&& value == "") {
value = null
} else if (schema?.properties[key].format == "date" && value == "") {
} else if (schema?.properties[key].format == "date" &&
schema?.properties[key].extra["nullable"] == true
&& value == "") {
value = null
} else if (schema?.properties[key].type == "integer" && value == "") {
} else if (schema?.properties[key].format == "json" &&
schema?.properties[key].extra["nullable"] == true
&& value == "") {
value = null
} else if (schema?.properties[key].type == "number" && value == "") {
}
else if (schema?.properties[key]["anyOf"][0].type == "integer" &&
Comment on lines +130 to +151
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it's possible to dramatically simplify this logic. I wonder if there are any column types left now, which when nullable and have an empty string as a value aren't converted to null?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can use a few variables to reduce code repetition. Something like this

export function convertFormValue(params: {
    key: string
    value: any
    schema: Schema
}): any {
    let { key, value, schema } = params

    const columnTypeAnyOf = schema?.properties[key]["anyOf"][0].type
    const columnFormat = schema?.properties[key].format
    const columnNullable = schema?.properties[key].extra["nullable"] == true
    const possibleFormats = [
        "uuid",
        "email",
        "date-time",
        "date",
        "json",
        "integer",
        "number",
        "string"
    ]

    if (value == "null") {
        value = null
    } else if (schema?.properties[key].type == "array") {
        value = JSON.parse(String(value))
    } else if (possibleFormats.includes(columnFormat) &&
        columnNullable
        && value == "") {
        value = null
    } else if (possibleFormats.includes(columnTypeAnyOf) &&
        columnNullable
        && value == "") {
        value = null
    } else if (schema?.properties[key].extra.foreign_key == true &&
        columnNullable &&
        value == ""
    ) {
        value = null
    }
    return value
}

schema?.properties[key].extra["nullable"] == true
&& value == "") {
value = null
} else if (
schema?.properties[key].type == "string" &&
schema?.properties[key].nullable &&
value == ""
) {
} else if (schema?.properties[key]["anyOf"][0].type == "number" &&
schema?.properties[key].extra["nullable"] == true
&& value == "") {
value = null
} else if (
schema?.properties[key].format == "json" &&
schema?.properties[key].nullable &&
value == ""
) {
schema?.properties[key]["anyOf"][0].type == "string" &&
schema?.properties[key].extra["nullable"] == true
&& value == "") {
value = null
} else if (
schema?.properties[key].extra.foreign_key == true &&
schema?.properties[key].extra["nullable"] == true &&
value == ""
) {
value = null
Expand Down
7 changes: 2 additions & 5 deletions admin_ui/src/views/RowListing.vue
Original file line number Diff line number Diff line change
Expand Up @@ -326,10 +326,7 @@
<li>
<DeleteButton
:includeTitle="true"
class="
subtle
delete
"
class="subtle delete"
v-on:triggered="
deleteRow(
row[pkName]
Expand Down Expand Up @@ -551,7 +548,7 @@ export default Vue.extend({
return this.schema.properties[name]["type"] == "boolean"
},
isInterval(name: string): boolean {
return this.schema.properties[name]["format"] == "time-delta"
return this.schema.properties[name]["format"] == "duration"
},
isJSON(name: string): boolean {
return this.schema.properties[name]["format"] == "json"
Expand Down
10 changes: 5 additions & 5 deletions piccolo_admin/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,7 @@ def __init__(
production: bool = False,
site_name: str = "Piccolo Admin",
default_language_code: str = "auto",
translations: t.List[Translation] = None,
translations: t.Optional[t.List[Translation]] = None,
allowed_hosts: t.Sequence[str] = [],
debug: bool = False,
sidebar_links: t.Dict[str, str] = {},
Expand Down Expand Up @@ -656,7 +656,7 @@ def __init__(

if not rate_limit_provider:
rate_limit_provider = InMemoryLimitProvider(
limit=1000, timespan=300
limit=100, timespan=300
)

public_app.mount(
Expand Down Expand Up @@ -802,7 +802,7 @@ async def store_file(

try:
file_key = await media_storage.store_file(
file_name=file.filename,
file_name=str(file.filename),
file=file.file,
user=request.user.user,
)
Expand Down Expand Up @@ -879,7 +879,7 @@ def get_single_form_schema(self, form_slug: str) -> t.Dict[str, t.Any]:
if form_config is None:
raise HTTPException(status_code=404, detail="No such form found")
else:
return form_config.pydantic_model.schema()
return form_config.pydantic_model.model_json_schema()

async def post_single_form(
self, request: Request, form_slug: str
Expand Down Expand Up @@ -1047,7 +1047,7 @@ def create_admin(
production: bool = False,
site_name: str = "Piccolo Admin",
default_language_code: str = "auto",
translations: t.List[Translation] = None,
translations: t.Optional[t.List[Translation]] = None,
auto_include_related: bool = True,
allowed_hosts: t.Sequence[str] = [],
debug: bool = False,
Expand Down
7 changes: 4 additions & 3 deletions piccolo_admin/example.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
from piccolo_api.media.local import LocalMediaStorage
from piccolo_api.media.s3 import S3MediaStorage
from piccolo_api.session_auth.tables import SessionsBase
from pydantic import BaseModel, validator
from pydantic import BaseModel, field_validator
from starlette.requests import Request

from piccolo_admin.endpoints import (
Expand Down Expand Up @@ -309,7 +309,7 @@ class BusinessEmailModel(BaseModel):
title: str = "Enquiry"
content: str

@validator("email")
@field_validator("email")
def validate_email(cls, v):
if "@" not in v:
raise ValueError("not valid email")
Expand All @@ -321,7 +321,7 @@ class BookingModel(BaseModel):
name: str
notes: str = "N/A"

@validator("email")
@field_validator("email")
def validate_email(cls, v):
if "@" not in v:
raise ValueError("not valid email")
Expand Down Expand Up @@ -391,6 +391,7 @@ def booking_endpoint(request: Request, data: BookingModel) -> str:
Movie._meta.primary_key,
Movie.name,
Movie.rating,
Movie.duration,
Movie.director,
Movie.poster,
Movie.tags,
Expand Down
2 changes: 1 addition & 1 deletion piccolo_admin/version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.57.0
0.58.0
6 changes: 3 additions & 3 deletions requirements/dev-requirements.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
black==21.12b0
isort==5.10.1
black==23.7.0
isort==5.12.0
twine==4.0.0
mypy==0.942
mypy==1.5.1
pip-upgrader==1.4.15
wheel==0.37.1
python-dotenv==0.20.0
6 changes: 3 additions & 3 deletions requirements/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
piccolo>=0.104.0
piccolo_api>=0.57.0
piccolo>=1.0a1
piccolo_api>=1.0a1
uvicorn
aiofiles>=0.5.0
Hypercorn
targ>=0.1.9
fastapi>=0.87.0,<0.100.0
fastapi>=0.100.0
Loading