Skip to content

Commit

Permalink
refactor: delete init function
Browse files Browse the repository at this point in the history
  • Loading branch information
JeanArhancet committed Oct 30, 2023
1 parent e6d33b3 commit 2a6c712
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 187 deletions.
22 changes: 0 additions & 22 deletions docs/ulid.md

This file was deleted.

76 changes: 0 additions & 76 deletions mkdocs.yml

This file was deleted.

74 changes: 33 additions & 41 deletions pydantic_extra_types/ulid.py
Original file line number Diff line number Diff line change
@@ -1,70 +1,62 @@
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
raise RuntimeError(
'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)
4 changes: 3 additions & 1 deletion requirements/pyproject.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
# setuptools
92 changes: 45 additions & 47 deletions tests/test_ulid.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
}

0 comments on commit 2a6c712

Please sign in to comment.