From d8b4c8e33a2b61506378bdd8935bc44cfcd198d9 Mon Sep 17 00:00:00 2001 From: 100gle <569590461@qq.com> Date: Thu, 22 Dec 2022 11:51:01 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E6=8A=80=E8=83=BD?= =?UTF-8?q?=E6=89=A9=E5=B1=95N10=E4=B8=80=E7=AB=A0=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E7=A4=BA=E4=BE=8B=E6=BA=90=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- code/newsletter/N10/attrs.ipynb | 487 ++++++++++++++++++++++++ code/newsletter/N10/dataclasses.ipynb | 270 ++++++++++++++ code/newsletter/N10/pydantic.ipynb | 516 ++++++++++++++++++++++++++ 3 files changed, 1273 insertions(+) create mode 100644 code/newsletter/N10/attrs.ipynb create mode 100644 code/newsletter/N10/dataclasses.ipynb create mode 100644 code/newsletter/N10/pydantic.ipynb diff --git a/code/newsletter/N10/attrs.ipynb b/code/newsletter/N10/attrs.ipynb new file mode 100644 index 0000000..42267f9 --- /dev/null +++ b/code/newsletter/N10/attrs.ipynb @@ -0,0 +1,487 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 定义数据类" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "User(username='100gle', password='password', email='email@example.com', is_vip=False)" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import attrs\n", + "\n", + "\n", + "@attrs.define\n", + "class User:\n", + " username: str\n", + " password: str\n", + " email: str\n", + " is_vip: bool = False\n", + "\n", + "User(\"100gle\", \"password\", email=\"email@example.com\")" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "User(username='100gle', password=***, email='email@example.com')" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "@attrs.define\n", + "class User:\n", + " username: str\n", + " password: str = attrs.field(repr=lambda v: \"***\")\n", + " email: str\n", + " is_vip: bool = attrs.field(default=False, repr=False)\n", + "\n", + "User(\"100gle\", \"password\", email=\"email@example.com\")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "from datetime import datetime\n", + "\n", + "import attrs\n", + "\n", + "\n", + "@attrs.define\n", + "class User:\n", + " username: str\n", + " email: str\n", + " _password: str = attrs.field(repr=lambda v: \"***\")\n", + " _is_vip: bool = attrs.field(init=False, default=False, repr=False)\n", + " _recent_login_at: datetime = attrs.field(init=False, default=datetime.now())" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "User(username='100gle', email='email@example.com', _password=***, _recent_login_at=datetime.datetime(2022, 11, 19, 10, 44, 48, 848285))" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "User(\"100gle\", email=\"email@example.com\", password=\"password\")" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "from datetime import datetime\n", + "\n", + "import attr\n", + "\n", + "\n", + "@attr.s\n", + "class User:\n", + " username = attr.ib(type=str)\n", + " email = attr.ib(type=str)\n", + " _password = attr.ib(repr=lambda v: \"***\")\n", + " _is_vip = attr.ib(default=False, repr=False)\n", + " _recent_login_at = attr.ib(init=False, default=datetime.now())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 自定义字段校验逻辑" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "class LimitError(Exception):\n", + " pass\n", + "\n", + "\n", + "@attrs.define\n", + "class User:\n", + " username: str\n", + " email: str = attrs.field()\n", + " _password: str = attrs.field(repr=lambda v: \"***\")\n", + " _is_vip: bool = attrs.field(default=False, repr=False)\n", + " _recent_login_at: datetime = attrs.field(init=False, default=datetime.now())\n", + "\n", + " @email.validator\n", + " def _check_email(self, attribute, value: str):\n", + " if not \"@\" in value:\n", + " raise ValueError(\"invalid email that doesn't contains an '@' symbol\")\n", + "\n", + " if len(value) >= 15:\n", + " raise LimitError(\"email out of the max length of 15 chars\")\n", + "\n", + " return value" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "# User(\"100gle\", email=\"email#example.com\", password=\"password\") # raise ValueError here!" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "import attrs\n", + "\n", + "class LimitError(Exception):\n", + " pass\n", + "\n", + "def check_email_validation(instance, attribute, value: str):\n", + " if not \"@\" in value:\n", + " raise ValueError(\"invalid email that doesn't contains an '@' symbol\")\n", + "\n", + "\n", + "def check_email_length(instance, attribute, value: str):\n", + " if len(value) >= 15:\n", + " raise LimitError(\"email out of the max length of 15 chars\")\n", + "\n", + "@attrs.define\n", + "class User:\n", + " username: str\n", + " email: str = attrs.field(\n", + " validator=[\n", + " check_email_validation,\n", + " check_email_length,\n", + " ]\n", + " )\n", + " _password: str = attrs.field(repr=lambda v: \"***\")\n", + " _is_vip: bool = attrs.field(default=False, repr=False)\n", + " _recent_login_at: datetime = attrs.field(init=False, default=datetime.now())\n", + "\n", + " @email.validator\n", + " def _check_email_suffix(self, attribute, value: str):\n", + " if not value.endswith(\"com\"):\n", + " raise ValueError(\"invalid domain\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "# User(\"100gle\", email=\"email#example.com\", password=\"password\") # raise ValueError here!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 自定义转换器" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "@attrs.define\n", + "class C:\n", + " x: int = attrs.field(converter=int)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "o = C(\"1\")\n", + "o.x\n" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "int" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(o.x)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "from datetime import datetime\n", + "\n", + "import attrs\n", + "\n", + "\n", + "class LimitError(Exception):\n", + " pass\n", + "\n", + "\n", + "def fix_invalid_email(value: str) -> str:\n", + " if not \"@\" in value and \"#\" in value:\n", + " return value.replace(\"#\", \"@\")\n", + " return value\n", + "\n", + "@attrs.define\n", + "class User:\n", + " username: str\n", + " email: str = attrs.field(converter=fix_invalid_email)\n", + " _password: str = attrs.field(repr=lambda v: \"***\")\n", + " _is_vip: bool = attrs.field(default=False, repr=False)\n", + " _recent_login_at: datetime = attrs.field(init=False, default=datetime.now())\n", + "\n", + " @email.validator\n", + " def _check_email_suffix(self, attribute, value: str):\n", + " if not value.endswith(\"com\"):\n", + " raise ValueError(\"invalid domain\")" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "User(username='100gle', email='email@example.com', _password=***, _recent_login_at=datetime.datetime(2022, 11, 19, 10, 44, 49, 229669))" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "User(\"100gle\", email=\"email#example.com\", password=\"password\")" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "# User(\"100gle\", email=\"email@example.net\", password=\"password\") # raise ValueError here!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 导出数据类" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'username': '100gle',\n", + " 'email': 'email@example.com',\n", + " '_password': 'password',\n", + " '_is_vip': False,\n", + " '_recent_login_at': datetime.datetime(2022, 11, 19, 10, 44, 49, 229669)}" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "user = User(\"100gle\", email=\"email#example.com\", password=\"password\")\n", + "attrs.asdict(user)" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'username': '100gle', 'email': 'email@example.com'}" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "attrs.asdict(\n", + " user,\n", + " filter=lambda attr, value: not attr.name.startswith(\"_\"),\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'username': '100gle', 'email': 'email@example.com'}" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from attrs import fields, filters\n", + "\n", + "attrs.asdict(\n", + " user,\n", + " filter=filters.include(\n", + " fields(User).username,\n", + " fields(User).email,\n", + " ),\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'username': '100gle', 'email': 'email@example.com'}" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "attrs.asdict(\n", + " user,\n", + " filter=filters.exclude(\n", + " fields(User)._password,\n", + " fields(User)._is_vip,\n", + " datetime,\n", + " ),\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3.9.0 ('pandas-startup')", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.0" + }, + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "13977d4cc82dee5f9d9535ceb495bd0ab12a43c33c664e5f0d53c24cf634b67f" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/code/newsletter/N10/dataclasses.ipynb b/code/newsletter/N10/dataclasses.ipynb new file mode 100644 index 0000000..8d7210e --- /dev/null +++ b/code/newsletter/N10/dataclasses.ipynb @@ -0,0 +1,270 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import typing as t\n", + "from dataclasses import dataclass\n", + "\n", + "@dataclass\n", + "class Item:\n", + " name: str\n", + " price: t.Union[int, float]\n", + " number: int" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "import typing as t\n", + "from dataclasses import dataclass\n", + "\n", + "@dataclass(repr=False, eq=True, order=False)\n", + "class Item:\n", + " name: str\n", + " price: t.Union[int, float]\n", + " number: int" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "import typing as t\n", + "from dataclasses import dataclass\n", + "\n", + "@dataclass\n", + "class Item:\n", + " name: str\n", + " price: t.Union[int, float] = 0\n", + " number: int = 0" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "import typing as t\n", + "from dataclasses import dataclass, field\n", + "\n", + "@dataclass\n", + "class Item:\n", + " name: str\n", + " price: t.Union[int, float] = 0\n", + " number: int = 0\n", + " categories: t.List[str] = field(default_factory=list)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "import typing as t\n", + "from dataclasses import dataclass, field\n", + "\n", + "@dataclass\n", + "class Item:\n", + " name: str\n", + " price: t.Union[int, float] = field(default=0)\n", + " number: int = field(default=0)\n", + " categories: t.List[str] = field(default_factory=list)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Item(name='iPhone 14', price=6999, number=1000, categories=[Category(level=1, name='')])" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import typing as t\n", + "from dataclasses import dataclass, field\n", + "\n", + "\n", + "@dataclass\n", + "class Category:\n", + " level: int = 1\n", + " name: str = \"\"\n", + "\n", + "\n", + "def default_categories():\n", + " return [Category()]\n", + "\n", + "\n", + "@dataclass\n", + "class Item:\n", + " name: str\n", + " price: t.Union[int, float] = field(default=0)\n", + " number: int = field(default=0)\n", + " categories: t.List[Category] = field(default_factory=default_categories)\n", + "\n", + "\n", + "Item(\"iPhone 14\", price=6999, number=1000)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "if sys.version_info >= (3, 10):\n", + " from dataclasses import KW_ONLY\n", + "\n", + " import typing as t\n", + " from dataclasses import KW_ONLY, dataclass, field\n", + "\n", + "\n", + " @dataclass\n", + " class Category:\n", + " level: int = 1\n", + " name: str = \"\"\n", + "\n", + "\n", + " def default_categories():\n", + " return [Category()]\n", + "\n", + "\n", + " @dataclass\n", + " class Item:\n", + " name: str\n", + " _: KW_ONLY\n", + " price: t.Union[int, float] = field(default=0)\n", + " number: int = field(default=0)\n", + " categories: t.List[Category] = field(default_factory=default_categories)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "import typing as t\n", + "from dataclasses import asdict, astuple, dataclass, field\n", + "\n", + "\n", + "@dataclass\n", + "class Category:\n", + " level: int = 1\n", + " name: str = \"\"\n", + "\n", + "\n", + "def default_categories():\n", + " return [Category()]\n", + "\n", + "\n", + "@dataclass\n", + "class Item:\n", + " name: str\n", + " price: t.Union[int, float] = field(default=0)\n", + " number: int = field(default=0)\n", + " categories: t.List[Category] = field(default_factory=default_categories)\n", + "\n", + "\n", + "item = Item(\"iPhone 14\", price=6999, number=1000)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "('iPhone 14', 6999, 1000, [(1, '')])" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "astuple(item)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'name': 'iPhone 14',\n", + " 'price': 6999,\n", + " 'number': 1000,\n", + " 'categories': [{'level': 1, 'name': ''}]}" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "asdict(item)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3.9.0 ('pandas-startup')", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.0" + }, + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "13977d4cc82dee5f9d9535ceb495bd0ab12a43c33c664e5f0d53c24cf634b67f" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/code/newsletter/N10/pydantic.ipynb b/code/newsletter/N10/pydantic.ipynb new file mode 100644 index 0000000..d1341ea --- /dev/null +++ b/code/newsletter/N10/pydantic.ipynb @@ -0,0 +1,516 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Item(name='iPhone', price=0, number=0, categories=[Category(name='', level=1)])" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import typing as t\n", + "\n", + "from pydantic import BaseModel\n", + "\n", + "\n", + "class Category(BaseModel):\n", + " name: str = \"\"\n", + " level: int = 1\n", + "\n", + "\n", + "class Item(BaseModel):\n", + " name: str\n", + " price: t.Union[int, float] = 0\n", + " number: int = 0\n", + " categories: t.List[Category] = [Category()]\n", + "\n", + "\n", + "Item(name=\"iPhone\")\n", + "Item(name='iPhone', price=0, number=0, categories=[Category(name='', level=1)])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 额外的类型注解" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "from pydantic import BaseModel, FilePath, HttpUrl, IPvAnyAddress, PostgresDsn\n", + "from pydantic.color import Color\n", + "\n", + "class FileSystem(BaseModel):\n", + " path: FilePath\n", + "\n", + "\n", + "class Link(BaseModel):\n", + " url: HttpUrl\n", + "\n", + "\n", + "class Network(BaseModel):\n", + " ip: IPvAnyAddress\n", + "\n", + "\n", + "class PostgreDatabase(BaseModel):\n", + " dsn: PostgresDsn\n", + "\n", + "\n", + "class WebColor(BaseModel):\n", + " value: Color" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Item(name='iphone', number=0)" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from pydantic import constr, conint\n", + "\n", + "class Item(BaseModel):\n", + " name: constr(strip_whitespace=True, min_length=2, max_length=10)\n", + " number: conint(ge=0) = 0\n", + "\n", + "\n", + "Item(name=\" iphone \")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "# Item(name=\"iPhone\", number=-1) # raise ValidationError here!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 数据校验" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "import typing as t\n", + "from dataclasses import dataclass\n", + "from pydantic import BaseModel, validator\n", + "\n", + "@dataclass\n", + "class Category:\n", + " level: int = 1\n", + " name: str = \"\"\n", + "\n", + "class Item(BaseModel):\n", + " name: str\n", + " price: t.Union[int, float] = 0\n", + " number: int = 0\n", + " categories: t.List[Category] = [Category()]\n", + "\n", + "\n", + " @validator(\"price\", \"number\")\n", + " def prevent_negative_number(cls, v):\n", + " if v < 0:\n", + " raise ValueError(\"can't set a negative number\")\n", + " return v" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "# Item(name=\"iPhone\", price=-1) # raise ValidationError here!" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "import typing as t\n", + "from dataclasses import field\n", + "\n", + "from pydantic import validator\n", + "from pydantic.dataclasses import dataclass # <-- use pydantic.dataclasses.dataclass\n", + "\n", + "\n", + "@dataclass\n", + "class Category:\n", + " name: str = \"\"\n", + " level: int = 1\n", + "\n", + "\n", + "def default_categories():\n", + " return [Category()]\n", + "\n", + "\n", + "@dataclass\n", + "class Item:\n", + " name: str\n", + " price: t.Union[int, float] = field(default=0)\n", + " number: int = field(default=0)\n", + " categories: t.List[Category] = field(default_factory=default_categories)\n", + "\n", + " @validator(\"price\", \"number\")\n", + " def prevent_negative_number(cls, v):\n", + " if v < 0:\n", + " raise ValueError(\"can't set a negative number\")\n", + " return v" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "import typing as t\n", + "\n", + "from pydantic import BaseModel, validator\n", + "from pydantic.dataclasses import dataclass\n", + "\n", + "\n", + "@dataclass\n", + "class Category:\n", + " name: str = \"\"\n", + " level: int = 1\n", + "\n", + "\n", + "\n", + "class ItemCode(str):\n", + " @classmethod\n", + " def __get_validators__(cls):\n", + " yield cls.validate\n", + "\n", + " @classmethod\n", + " def validate(cls, v):\n", + " if not v.startswith(\"ITEM\"):\n", + " raise ValueError(\"invalid item code format\")\n", + " return v\n", + "\n", + "\n", + "class Item(BaseModel):\n", + " name: str\n", + " itemcode: ItemCode\n", + " price: t.Union[int, float] = 0\n", + " number: int = 0\n", + " categories: t.List[Category] = [Category()]\n", + "\n", + " @validator(\"price\", \"number\")\n", + " def prevent_negative_number(cls, v):\n", + " if v < 0:\n", + " raise ValueError(\"can't set a negative number\")\n", + " return v" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Item(name='iPhone', itemcode='ITEM31415926', price=0, number=0, categories=[Category(name='', level=1)])" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Item(name=\"iPhone\", itemcode=\"ITEM31415926\")" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "# Item(name=\"iPhone\", itemcode=\"aabbcc\") # raise ValidationError here!" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "import typing as t\n", + "\n", + "from pydantic import BaseModel, validator\n", + "from pydantic.dataclasses import dataclass\n", + "\n", + "\n", + "class ItemValidator:\n", + " @classmethod\n", + " def prevent_negative_number(cls, v):\n", + " if v < 0:\n", + " raise ValueError(\"can't set a negative number\")\n", + " return v\n", + "\n", + " @classmethod\n", + " def has_itemcode_preffix(cls, v: str):\n", + " if not v.startswith(\"ITEM\"):\n", + " raise ValueError(\"invalid item code format\")\n", + " return v\n", + "\n", + "\n", + "@dataclass\n", + "class Category:\n", + " name: str = \"\"\n", + " level: int = 1\n", + "\n", + "\n", + "\n", + "class Item(BaseModel):\n", + " name: str\n", + " itemcode: str\n", + " price: t.Union[int, float] = 0\n", + " number: int = 0\n", + " categories: t.List[Category] = [Category()]\n", + "\n", + " check_number = validator(\"price\", \"number\")(ItemValidator.prevent_negative_number)\n", + " check_itemcode = validator(\"itemcode\")(ItemValidator.has_itemcode_preffix)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 导出数据类" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "import typing as t\n", + "\n", + "from pydantic import BaseModel\n", + "\n", + "\n", + "class Category(BaseModel):\n", + " name: str = \"\"\n", + " level: int = 1\n", + "\n", + "\n", + "class Item(BaseModel):\n", + " name: str\n", + " price: t.Union[int, float] = 0\n", + " number: int = 0\n", + " categories: t.List[Category] = [Category()]\n", + "\n", + "\n", + "item = Item(\n", + " name=\"iPhone\",\n", + " price=6999,\n", + " number=1000,\n", + " categories=[Category(name=\"Phone\")],\n", + ")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'name': 'iPhone',\n", + " 'price': 6999,\n", + " 'number': 1000,\n", + " 'categories': [{'name': 'Phone', 'level': 1}]}" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "item.dict()" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'{\"name\": \"iPhone\", \"price\": 6999, \"number\": 1000, \"categories\": [{\"name\": \"Phone\", \"level\": 1}]}'" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "item.json()" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'name': 'iPhone',\n", + " 'price': 6999,\n", + " 'categories': [{'name': 'Phone', 'level': 1}]}" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "item.dict(exclude={\"number\": True})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 从 ORM 中进行转换" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [], + "source": [ + "import typing as t\n", + "from datetime import datetime\n", + "\n", + "import peewee as pw\n", + "from pydantic import BaseModel, Field\n", + "\n", + "db = pw.SqliteDatabase(\":memory:\")\n", + "\n", + "\n", + "class NoteORM(pw.Model):\n", + " id = pw.AutoField()\n", + " title = pw.CharField()\n", + " content = pw.TextField()\n", + " tags = pw.CharField()\n", + " create_at = pw.DateTimeField(default=datetime.now())\n", + "\n", + " class Meta:\n", + " database = db\n", + "\n", + "\n", + "class NoteModel(BaseModel):\n", + " id: t.Optional[int] = Field(example=\"null\")\n", + " title: str\n", + " content: str\n", + " tags: t.Optional[t.List[str]] = Field(example=\"null\")\n", + "\n", + " class Config:\n", + " orm_mode = True" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'id': None, 'title': '测试', 'content': '# Hello, world\\n', 'tags': ['技术分享']}" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "note_db = NoteORM(title=\"测试\", content=\"# Hello, world\\n\", tags=[\"技术分享\"])\n", + "note = NoteModel.from_orm(note_db)\n", + "note.dict()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3.9.0 ('pandas-startup')", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.0" + }, + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "13977d4cc82dee5f9d9535ceb495bd0ab12a43c33c664e5f0d53c24cf634b67f" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +}