From 2a6c712441f5f2092001dad0ce43678d1dd94db0 Mon Sep 17 00:00:00 2001 From: JeanArhancet Date: Wed, 23 Aug 2023 15:04:54 +0200 Subject: [PATCH] refactor: delete init function --- docs/ulid.md | 22 --------- mkdocs.yml | 76 ----------------------------- pydantic_extra_types/ulid.py | 74 +++++++++++++---------------- requirements/pyproject.txt | 4 +- tests/test_ulid.py | 92 ++++++++++++++++++------------------ 5 files changed, 81 insertions(+), 187 deletions(-) delete mode 100644 docs/ulid.md delete mode 100644 mkdocs.yml diff --git a/docs/ulid.md b/docs/ulid.md deleted file mode 100644 index b91ff072..00000000 --- a/docs/ulid.md +++ /dev/null @@ -1,22 +0,0 @@ - -The `ULID` type validates an ULID - Universally Unique Lexicographically Sortable Identifier. - -This class depends on the [python-ulid] package, which is a validate by the [ULID-spec]. - -```py -from pydantic import BaseModel - -from pydantic_extra_types.ulid import ULID - - -class User(BaseModel): - id: ULID - - -user = User(id='01BTGNYV6HRNK8K8VKZASZCFPE') -print(user.id) -#> 01BTGNYV6HRNK8K8VKZASZCFPE -``` - -[python-lib]: https://github.com/mdomke/python-ulid -[ULID-spec]: https://github.com/ulid/spec#implementations-in-other-languages diff --git a/mkdocs.yml b/mkdocs.yml deleted file mode 100644 index c13462ec..00000000 --- a/mkdocs.yml +++ /dev/null @@ -1,76 +0,0 @@ -site_name: pydantic -site_description: Data validation using Python type hints -strict: true -site_url: https://docs.pydantic.dev/ - -theme: - name: 'material' - custom_dir: 'docs/theme' - palette: - - media: "(prefers-color-scheme: light)" - scheme: default - primary: pink - accent: pink - toggle: - icon: material/lightbulb-outline - name: "Switch to dark mode" - - media: "(prefers-color-scheme: dark)" - scheme: slate - primary: pink - accent: pink - toggle: - icon: material/lightbulb - name: "Switch to light mode" - features: - - content.tabs.link - - content.code.annotate - - announce.dismiss - - navigation.tabs - logo: 'logo-white.svg' - favicon: 'favicon.png' - -repo_name: pydantic/pydantic -repo_url: https://github.com/pydantic/pydantic -edit_uri: edit/main/docs/ - -extra_css: -- 'extra/terminal.css' -- 'extra/tweaks.css' - -nav: -- Home: - - Color: 'color_types.md' - - Payment Card: 'payment_cards.md' - - Phone Number: 'phone_numbers.md' - - ABA Routing Number: 'routing_number.md' - - MAC address: 'mac_address.md' - - Coordinate: 'coordinate.md' - - ULID: 'ulid.md' - - -markdown_extensions: -- tables -- toc: - permalink: true - title: Page contents -- admonition -- pymdownx.highlight -- pymdownx.extra -- pymdownx.emoji: - emoji_index: !!python/name:materialx.emoji.twemoji - emoji_generator: !!python/name:materialx.emoji.to_svg -- pymdownx.tabbed: - alternate_style: true - -extra: - version: - provider: mike - -plugins: -- mike: - alias_type: symlink - canonical_version: latest -- search -- exclude: - glob: - - __pycache__/* diff --git a/pydantic_extra_types/ulid.py b/pydantic_extra_types/ulid.py index 7d75a9c7..d2bf650e 100644 --- a/pydantic_extra_types/ulid.py +++ b/pydantic_extra_types/ulid.py @@ -1,8 +1,17 @@ -from datetime import datetime -from typing import Any, Type, Union +""" +The `pydantic_extra_types.ULID` module provides the [`ULID`] data type. + +This class depends on the [python-ulid] package, which is a validate by the [ULID-spec](https://github.com/ulid/spec#implementations-in-other-languages). +""" +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Union from pydantic import GetCoreSchemaHandler +from pydantic._internal import _repr from pydantic_core import PydanticCustomError, core_schema + try: from ulid import ULID as _ULID except ModuleNotFoundError: # pragma: no cover @@ -10,61 +19,44 @@ 'The `ulid` module requires "python-ulid" to be installed. You can install it with "pip install python-ulid".' ) - UlidType = Union[str, bytes, int] -class ULID(str): +@dataclass +class ULID(_repr.Representation): """ - Represents an ULID - https://github.com/ulid/spec + A wrapper around [python-ulid](https://pypi.org/project/python-ulid/) package, which + is a validate by the [ULID-spec](https://github.com/ulid/spec#implementations-in-other-languages). """ - _ulid: _ULID - - def __init__(self, value: UlidType): - if isinstance(value, (bytes, str, int)): - self._ulid = self.validate_ulid(value) - else: - raise PydanticCustomError( - 'ulid_error', - 'value is not a valid ULID: value must be a string, int or bytes', - ) + ulid: _ULID @classmethod - def __get_pydantic_core_schema__( - cls, source: Type[Any], handler: GetCoreSchemaHandler - ) -> core_schema.AfterValidatorFunctionSchema: - return core_schema.general_after_validator_function( - cls.validate, - core_schema.str_schema(), + def __get_pydantic_core_schema__(cls, source: type[Any], handler: GetCoreSchemaHandler) -> core_schema.CoreSchema: + return core_schema.no_info_wrap_validator_function( + cls._validate_ulid, + core_schema.union_schema( + [ + core_schema.is_instance_schema(_ULID), + core_schema.int_schema(), + core_schema.bytes_schema(), + core_schema.str_schema(), + ] + ), ) @classmethod - def validate(cls, __input_value: str, _: core_schema.ValidationInfo) -> 'ULID': - return cls(__input_value) - - @classmethod - def validate_ulid(cls, value: UlidType) -> _ULID: + def _validate_ulid(cls, value: Any, handler: core_schema.ValidatorFunctionWrapHandler) -> Any: ulid: _ULID try: if isinstance(value, int): ulid = _ULID.from_int(value) elif isinstance(value, str): ulid = _ULID.from_str(value) + elif isinstance(value, _ULID): + ulid = value else: ulid = _ULID.from_bytes(value) - except ValueError as e: - raise PydanticCustomError('ulid_format', 'Unrecognized format') from e - return ulid - - @property - def hex(self) -> str: - return self._ulid.hex - - @property - def timestamp(self) -> float: - return self._ulid.timestamp - - @property - def datetime(self) -> datetime: - return self._ulid.datetime + except ValueError: + raise PydanticCustomError('ulid_format', 'Unrecognized format') + return handler(ulid) diff --git a/requirements/pyproject.txt b/requirements/pyproject.txt index e2f613fa..070df2cf 100644 --- a/requirements/pyproject.txt +++ b/requirements/pyproject.txt @@ -14,10 +14,12 @@ pydantic==2.0.3 # via pydantic-extra-types (pyproject.toml) pydantic-core==2.3.0 # via pydantic +python-ulid==1.1.0 + # via pydantic-extra-types (pyproject.toml) typing-extensions==4.6.3 # via # pydantic # pydantic-core # The following packages are considered to be unsafe in a requirements file: -# setuptools \ No newline at end of file +# setuptools diff --git a/tests/test_ulid.py b/tests/test_ulid.py index b60c64d2..2e1be666 100644 --- a/tests/test_ulid.py +++ b/tests/test_ulid.py @@ -3,11 +3,20 @@ import pytest from pydantic import BaseModel, ValidationError -from pydantic_core import PydanticCustomError -from ulid import base32 from pydantic_extra_types.ulid import ULID +try: + from ulid import ULID as _ULID +except ModuleNotFoundError: # pragma: no cover + raise RuntimeError( + 'The `ulid` module requires "python-ulid" to be installed. You can install it with "pip install python-ulid".' + ) + + +class Something(BaseModel): + ulid: ULID + @pytest.mark.parametrize( 'ulid, result, valid', @@ -18,65 +27,54 @@ # Invalid ULID for str format ('01BTGNYV6HRNK8K8VKZASZCFP', None, False), # Invalid ULID (short length) ('01BTGNYV6HRNK8K8VKZASZCFPEA', None, False), # Invalid ULID (long length) - # Valid ULID for bytes format - ( - base32.decode('01BTGNYV6HRNK8K8VKZASZCFPN'), - "b'\\x01^\\xa1_l\\xd1\\xc5f\\x89\\xa3s\\xfa\\xb3\\xf6>\\xd5'", - True, - ), - # Invalid ULID for bytes format + # Invalid ULID for _ULID format + (_ULID.from_str('01BTGNYV6HRNK8K8VKZASZCFPE'), '01BTGNYV6HRNK8K8VKZASZCFPE', True), + (_ULID.from_str('01BTGNYV6HRNK8K8VKZASZCFPF'), '01BTGNYV6HRNK8K8VKZASZCFPF', True), + # Invalid _ULID for bytes format (b'\x01\xBA\x1E\xB2\x8A\x9F\xFAy\x10\xD5\xA5k\xC8', None, False), # Invalid ULID (short length) (b'\x01\xBA\x1E\xB2\x8A\x9F\xFAy\x10\xD5\xA5k\xC8\xB6\x00', None, False), # Invalid ULID (long length) # Valid ULID for int format - (109667145845879622871206540411193812282, '109667145845879622871206540411193812282', True), - (109667145845879622871206540411193812283, '109667145845879622871206540411193812283', True), - (109667145845879622871206540411193812284, '109667145845879622871206540411193812284', True), + (109667145845879622871206540411193812282, '2JG4FVY7N8XS4GFVHPXGJZ8S9T', True), + (109667145845879622871206540411193812283, '2JG4FVY7N8XS4GFVHPXGJZ8S9V', True), + (109667145845879622871206540411193812284, '2JG4FVY7N8XS4GFVHPXGJZ8S9W', True), ], ) def test_format_for_ulid(ulid: Any, result: Any, valid: bool): if valid: - assert ULID(ulid) == result + assert str(Something(ulid=ulid).ulid) == result else: - with pytest.raises(PydanticCustomError, match='format'): - ULID(ulid) - - -@pytest.mark.parametrize( - 'ulid, valid', - [ - # Valid ULID for str format - ('01BTGNYV6HRNK8K8VKZASZCFPE', True), - # Invalid UUID - (123.45, False), # Bad type - float - ], -) -def test_type_for_ulid(ulid: Any, valid: bool): - if valid: - assert ULID(ulid) - else: - with pytest.raises( - PydanticCustomError, match='value is not a valid ULID: value must be a string, int or bytes' - ): - ULID(ulid) + with pytest.raises(ValidationError, match='format'): + Something(ulid=ulid) def test_property_for_ulid(): - ulid = ULID('01BTGNYV6HRNK8K8VKZASZCFPE') - + ulid = Something(ulid='01BTGNYV6HRNK8K8VKZASZCFPE').ulid assert ulid.hex == '015ea15f6cd1c56689a373fab3f63ece' assert ulid == '01BTGNYV6HRNK8K8VKZASZCFPE' assert ulid.datetime == datetime(2017, 9, 20, 22, 18, 59, 153000, tzinfo=timezone.utc) assert ulid.timestamp == 1505945939.153 -def test_model_validation(): - class Model(BaseModel): - ulid: ULID - - assert Model(ulid='01BTGNYV6HRNK8K8VKZASZCFPE').ulid == '01BTGNYV6HRNK8K8VKZASZCFPE' - with pytest.raises(ValidationError) as exc_info: - Model(ulid='1234') - - assert exc_info.value.errors() == [ - {'input': '1234', 'loc': ('ulid',), 'msg': 'Unrecognized format', 'type': 'ulid_format'} - ] +def test_json_schema(): + assert Something.model_json_schema(mode='validation') == { + 'properties': { + 'ulid': { + 'anyOf': [{'type': 'integer'}, {'format': 'binary', 'type': 'string'}, {'type': 'string'}], + 'title': 'Ulid', + } + }, + 'required': ['ulid'], + 'title': 'Something', + 'type': 'object', + } + assert Something.model_json_schema(mode='serialization') == { + 'properties': { + 'ulid': { + 'anyOf': [{'type': 'integer'}, {'format': 'binary', 'type': 'string'}, {'type': 'string'}], + 'title': 'Ulid', + } + }, + 'required': ['ulid'], + 'title': 'Something', + 'type': 'object', + }