Skip to content

Commit

Permalink
✨ add ulib type
Browse files Browse the repository at this point in the history
Add `Latitude`, `Longitude` and `Coordinate`  (#76)

* feat: add latitude, longitude and coordinate

* refactor: apply feedbacks

* refactor: apply feedbacks

* refactor: delete __init__ functions

* fix: coordinate parsing

* docs: update coordinate documentation

* refactor: use latitude, longitude in schema

* 🚧 Some improvements for `Coordinate` type PR (#2)

* refactor: delete __init__ functions

* 🚧 Some improvements for `Coordinate` type PR

* Get tests passing

* ✨ Test serialization json schema

* ⬆ Upgrade deps in `pyproject.toml` and `requirements/pyproject.txt

---------

Co-authored-by: JeanArhancet <[email protected]>
Co-authored-by: David Montague <[email protected]>

* fix: test and requirements

* docs: fix supported format

---------

Co-authored-by: Serge Matveenko <[email protected]>
Co-authored-by: David Montague <[email protected]>

✨  add ulib type

refactor: delete init function
  • Loading branch information
JeanArhancet committed Oct 30, 2023
1 parent 0c42c50 commit 179393b
Show file tree
Hide file tree
Showing 5 changed files with 160 additions and 0 deletions.
62 changes: 62 additions & 0 deletions pydantic_extra_types/ulid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""
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]


@dataclass
class ULID(_repr.Representation):
"""
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

@classmethod
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_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:
raise PydanticCustomError('ulid_format', 'Unrecognized format')
return handler(ulid)
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ dynamic = ['version']
all = [
'phonenumbers>=8,<9',
'pycountry>=22,<23',
'python-ulid>=1,<2',
]

[project.urls]
Expand Down
2 changes: 2 additions & 0 deletions requirements/pyproject.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ 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
Expand Down
15 changes: 15 additions & 0 deletions tests/test_json_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
)
from pydantic_extra_types.mac_address import MacAddress
from pydantic_extra_types.payment import PaymentCardNumber
from pydantic_extra_types.ulid import ULID


@pytest.mark.parametrize(
Expand Down Expand Up @@ -170,6 +171,20 @@
'type': 'object',
},
),
(
ULID,
{
'properties': {
'x': {
'anyOf': [{'type': 'integer'}, {'format': 'binary', 'type': 'string'}, {'type': 'string'}],
'title': 'X',
}
},
'required': ['x'],
'title': 'Model',
'type': 'object',
},
),
],
)
def test_json_schema(cls, expected):
Expand Down
80 changes: 80 additions & 0 deletions tests/test_ulid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
from datetime import datetime, timezone
from typing import Any

import pytest
from pydantic import BaseModel, ValidationError

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',
[
# Valid ULID for str format
('01BTGNYV6HRNK8K8VKZASZCFPE', '01BTGNYV6HRNK8K8VKZASZCFPE', True),
('01BTGNYV6HRNK8K8VKZASZCFPF', '01BTGNYV6HRNK8K8VKZASZCFPF', True),
# Invalid ULID for str format
('01BTGNYV6HRNK8K8VKZASZCFP', None, False), # Invalid ULID (short length)
('01BTGNYV6HRNK8K8VKZASZCFPEA', None, False), # Invalid ULID (long length)
# Valid 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, '2JG4FVY7N8XS4GFVHPXGJZ8S9T', True),
(109667145845879622871206540411193812283, '2JG4FVY7N8XS4GFVHPXGJZ8S9V', True),
(109667145845879622871206540411193812284, '2JG4FVY7N8XS4GFVHPXGJZ8S9W', True),
],
)
def test_format_for_ulid(ulid: Any, result: Any, valid: bool):
if valid:
assert str(Something(ulid=ulid).ulid) == result
else:
with pytest.raises(ValidationError, match='format'):
Something(ulid=ulid)


def test_property_for_ulid():
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_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 179393b

Please sign in to comment.