From e6b0b20305ffdd91cc0330cf73233f0172891ebc Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sat, 27 Apr 2024 04:48:08 +0200 Subject: Merging upstream version 2.7.0. Signed-off-by: Daniel Baumann --- HISTORY.md | 175 ++++++++++++++------------- Makefile | 4 +- pydantic_extra_types/__init__.py | 2 +- pydantic_extra_types/color.py | 1 + pydantic_extra_types/coordinate.py | 1 + pydantic_extra_types/country.py | 1 + pydantic_extra_types/currency_code.py | 1 + pydantic_extra_types/language_code.py | 212 ++++++++++++++++++++++++++++++++- pydantic_extra_types/pendulum_dt.py | 118 ++++++++++++++++++ pydantic_extra_types/phone_numbers.py | 1 + pydantic_extra_types/routing_number.py | 1 + pydantic_extra_types/ulid.py | 1 + requirements/linting.txt | 6 +- requirements/testing.txt | 6 +- tests/test_json_schema.py | 20 +++- tests/test_language_codes.py | 55 +++++++++ tests/test_pendulum_dt.py | 93 ++++++++++++++- tests/test_ulid.py | 4 +- 18 files changed, 603 insertions(+), 99 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 430800c..c3f4864 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,82 +1,97 @@ # CHANGELOG -## v2.6.0 - -* Allow python-ulid 2.x on Python 3.9 and later by @musicinmybrain in -* Do not pin the ”major” version of pycountry by @musicinmybrain in -* πŸ€– Create dependabot.yml for updating GitHub action by @yezz123 in -* :memo: Refactor Documentation for ISBN and MAC address modules by @yezz123 in -* Add language code definitions and test by @07pepa in -* :memo: Create a `changelog` to match release notes by @yezz123 in -* Add currency code ISO 4217 and its subset that includes only currencies by @07pepa in -* πŸ”¨ Update code formatting and linting configurations by @yezz123 in -* πŸ‘· Add Python checking for dependencies by @yezz123 in -* πŸ› fix single quote issue by @yezz123 in - -## v2.5.0 - -* Add Pendulum DT support by @theunkn0wn1 in - -## v2.4.1 - -* Fix refs blocking docs build by @sydney-runkle in - -## v2.4.0 - -* Add: New type ISBN by @lucasmucidas in -* fix validate_digits actually allowing non digit characters by @romaincaillon in -* ♻️ refactor the `validate_brand` method & add new types by @yezz123 in -* βœ… Drop python 3.7 & support 3.12 by @yezz123 in - -## v2.3.0 - -* Upgrade pydantic version to >=2.5.2 by @hramezani in - -## v.2.2.0 - -* Add `long` and `short` format to `as_hex` by @DJRHails in -* Refactor documentation by @Kludex in -* ✨ add `ULID` type by @JeanArhancet in -* Added `__get_pydantic_json_schema__` method with `format='tel'` by @hasansezertasan in - -## v2.1.0 - -* ✨ add `MacAddress` type by @JeanArhancet in -* :memo: fix usage of `MAC address` by @yezz123 in -* Add docstrings for payment cards by @tpdorsey in -* Fix mac adddress validation by @JeanArhancet in -* Remove work in progress part from README.md by @hramezani in -* Add `Latitude`, `Longitude` and `Coordinate` by @JeanArhancet in -* Refactor: use stdlib and remove useless code by @eumiro in -* Make Latitude and Longitude evaluated by @Kludex in - -## v2.0.0 - -* Migrate `Color` & `Payment Card` by @yezz123 in -* add `pydantic` to classifiers by @yezz123 in -* remove dependencies caching by @yezz123 in -* :bug: deprecate `__modify_schema__` method by @yezz123 in -* Fix Color JSON schema generation by @dmontagu in -* fix issues of `pydantic_core.core_schema` has no attribute `xxx` by @yezz123 in -* Fix Failed tests for `color` type by @yezz123 in -* Created Country type by @HomiGrotas in -* Add phone number types by @JamesHutchison in -* make `phonenumbers` a requirement by @yezz123 in -* chore(feat): Add ABARouting number type by @RevinderDev in -* add missing countries by @EssaAlshammri in -* chore: resolve `pydantic-core` dependency conflict by @hirotasoshu in -* Add `MIR` card brand by @hirotasoshu in -* fix dependencies version by @yezz123 in -* πŸ“ Add documentation for `Color` and `PaymentCardNumber` by @Kludex in -* Add hooky by @Kludex in -* ♻️ Simplify project structure by @Kludex in -* πŸ‘· Add coverage check on the pipeline by @Kludex in -* ♻️ refactor country type using `pycountry` by @yezz123 in -* βœ… Add 100% coverage by @Kludex in -* Add support for transparent Color by @CollinHeist in -* πŸ“ Add documentation for `PhoneNumber` and `ABARoutingNumber` by @Kludex in -* πŸ“ Refactor README by @Kludex in -* 🚚 Rename `routing_number.md` to `routing_numbers.md` by @Kludex in -* :memo: fix code in `payment` documentation by @yezz123 in -* uprev pydantic to b3 by @samuelcolvin in -* Prepare for release 2.0.0 by @hramezani in +## Latest Changes + +## 2.7.0 + +* πŸ”₯ Remove latest-changes workflow. PR [#165](https://github.com/pydantic/pydantic-extra-types/pull/165) by [yezz123](https://github.com/yezz123) +* πŸ”¨ Add latest-changes workflow to generate Changes. PR [#164](https://github.com/pydantic/pydantic-extra-types/pull/164) by [yezz123](https://github.com/yezz123) +* Added LanguageAlpha2 and LanguageName types. PR [#153](https://github.com/pydantic/pydantic-extra-types/pull/153) by [odelmarcelle](https://github.com/odelmarcelle) +* Added support for pendulum Dates. PR [#154](https://github.com/pydantic/pydantic-extra-types/pull/154) by [Woody1193](https://github.com/Woody1193) +* Add support for pendulum Duration. PR [#162](https://github.com/pydantic/pydantic-extra-types/pull/162) by [tempookian](https://github.com/tempookian) + +### Dependencies + +* ⬆ Bump the python-packages group with 1 update. PR [#150](https://github.com/pydantic/pydantic-extra-types/pull/150) by [dependabot](https://github.com/dependabot) +* ⬆ Bump the python-packages group with 6 updates. PR [#160](https://github.com/pydantic/pydantic-extra-types/pull/160) by [dependabot](https://github.com/dependabot) + +## 2.6.0 + +* Allow python-ulid 2.x on Python 3.9 and later. PR [#131](https://github.com/pydantic/pydantic-extra-types/pull/131) by [@musicinmybrain](https://github.com/musicinmybrain) +* Do not pin the ”major” version of pycountry. PR [#132](https://github.com/pydantic/pydantic-extra-types/pull/132) by [@musicinmybrain](https://github.com/musicinmybrain) +* πŸ€– Create dependabot.yml for updating GitHub action. PR [#134](https://github.com/pydantic/pydantic-extra-types/pull/134) by [@yezz123](https://github.com/yezz123) +* Refactor Documentation for ISBN and MAC address modules. PR [#124](https://github.com/pydantic/pydantic-extra-types/pull/124) by [@yezz123](https://github.com/yezz123) +* Add language code definitions and test. PR [#141](https://github.com/pydantic/pydantic-extra-types/pull/141) by [@07pepa](https://github.com/07pepa) +* Create a `changelog` to match release notes. PR [#142](https://github.com/pydantic/pydantic-extra-types/pull/142) by [@yezz123](https://github.com/yezz123) +* Add currency code ISO 4217 and its subset that includes only currencies. PR [#143](https://github.com/pydantic/pydantic-extra-types/pull/143) by [@07pepa](https://github.com/07pepa) +* πŸ”¨ Update code formatting and linting configurations. PR [#144](https://github.com/pydantic/pydantic-extra-types/pull/144) by [@yezz123](https://github.com/yezz123) +* πŸ‘· Add Python checking for dependencies. PR [#145](https://github.com/pydantic/pydantic-extra-types/pull/145) by [@yezz123](https://github.com/yezz123) +* πŸ› Fix single quote issue. PR [#148](https://github.com/pydantic/pydantic-extra-types/pull/148) by [@yezz123](https://github.com/yezz123) + +## 2.5.0 + +* Add Pendulum DT support. PR [#110](https://github.com/pydantic/pydantic-extra-types/pull/110) by [@theunkn0wn1](https://github.com/theunkn0wn1) + +## 2.4.1 + +* Fix refs blocking docs build. PR [#125](https://github.com/pydantic/pydantic-extra-types/pull/125) by [@sydney-runkle](https://github.com/sydney-runkle) + +## 2.4.0 + +* Add: New type ISBN. PR [#116](https://github.com/pydantic/pydantic-extra-types/pull/116) by [lucasmucidas](https://github.com/lucasmucidas) +* Fix validate_digits actually allowing non-digit characters. PR [#120](https://github.com/pydantic/pydantic-extra-types/pull/120) by [romaincaillon](https://github.com/romaincaillon) +* Refactor the `validate_brand` method & add new types. PR [#56](https://github.com/pydantic/pydantic-extra-types/pull/56) by [yezz123](https://github.com/yezz123) +* Drop Python 3.7 & support 3.12. PR [#122](https://github.com/pydantic/pydantic-extra-types/pull/122) by [yezz123](https://github.com/yezz123) + +## 2.3.0 + +* Upgrade pydantic version to >=2.5.2. PR [#113](https://github.com/pydantic/pydantic-extra-types/pull/113) by [hramezani](https://github.com/hramezani) + +## 2.2.0 + +* Add `long` and `short` format to `as_hex`. PR [#93](https://github.com/pydantic/pydantic-extra-types/pull/93) by [DJRHails](https://github.com/DJRHails) +* Refactor documentation. PR [#98](https://github.com/pydantic/pydantic-extra-types/pull/98) by [Kludex](https://github.com/Kludex) +* Add `ULID` type. PR [#73](https://github.com/pydantic/pydantic-extra-types/pull/73) by [JeanArhancet](https://github.com/JeanArhancet) +* Add `__get_pydantic_json_schema__` method with `format='tel'`. PR [#106](https://github.com/pydantic/pydantic-extra-types/pull/106) by [hasansezertasan](https://github.com/hasansezertasan) + +## 2.1.0 + +* Add `MacAddress` type. PR [#71](https://github.com/pydantic/pydantic-extra-types/pull/71) by [JeanArhancet](https://github.com/JeanArhancet) +* Fix usage of `MAC address`. PR [#72](https://github.com/pydantic/pydantic-extra-types/pull/72) by [yezz123](https://github.com/yezz123) +* Add docstrings for payment cards. PR [#77](https://github.com/pydantic/pydantic-extra-types/pull/77) by [tpdorsey](https://github.com/tpdorsey) +* Fix MAC address validation. PR [#79](https://github.com/pydantic/pydantic-extra-types/pull/79) by [JeanArhancet](https://github.com/JeanArhancet) +* Remove work in progress part from README.md. PR [#81](https://github.com/pydantic/pydantic-extra-types/pull/81) by [hramezani](https://github.com/hramezani) +* Add `Latitude`, `Longitude`, and `Coordinate`. PR [#76](https://github.com/pydantic/pydantic-extra-types/pull/76) by [JeanArhancet](https://github.com/JeanArhancet) +* Refactor: use stdlib and remove useless code. PR [#86](https://github.com/pydantic/pydantic-extra-types/pull/86) by [eumiro](https://github.com/eumiro) +* Make Latitude and Longitude evaluated. PR [#90](https://github.com/pydantic/pydantic-extra-types/pull/90) by [Kludex](https://github.com/Kludex) + +## 2.0.0 + +* Migrate `Color` & `Payment Card`. PR [#2](https://github.com/pydantic/pydantic-extra-types/pull/2) by [yezz123](https://github.com/yezz123) +* Add `pydantic` to classifiers. PR [#13](https://github.com/pydantic/pydantic-extra-types/pull/13) by [yezz123](https://github.com/yezz123) +* Remove dependencies caching. PR [#16](https://github.com/pydantic/pydantic-extra-types/pull/16) by [yezz123](https://github.com/yezz123) +* Deprecate `__modify_schema__` method. PR [#20](https://github.com/pydantic/pydantic-extra-types/pull/20) by [yezz123](https://github.com/yezz123) +* Fix Color JSON schema generation. PR [#21](https://github.com/pydantic/pydantic-extra-types/pull/21) by [dmontagu](https://github.com/dmontagu) +* Fix issues of `pydantic_core.core_schema` has no attribute `xxx`. PR [#23](https://github.com/pydantic/pydantic-extra-types/pull/23) by [yezz123](https://github.com/yezz123) +* Fix Failed tests for `color` type. PR [#26](https://github.com/pydantic/pydantic-extra-types/pull/26) by [yezz123](https://github.com/yezz123) +* Created Country type. PR [#14](https://github.com/pydantic/pydantic-extra-types/pull/14) by [HomiGrotas](https://github.com/HomiGrotas) +* Add phone number types. PR [#25](https://github.com/pydantic/pydantic-extra-types/pull/25) by [JamesHutchison](https://github.com/JamesHutchison) +* Make `phonenumbers` a requirement. PR [#29](https://github.com/pydantic/pydantic-extra-types/pull/29) by [yezz123](https://github.com/yezz123) +* Add ABARouting number type. PR [#30](https://github.com/pydantic/pydantic-extra-types/pull/30) by [RevinderDev](https://github.com/RevinderDev) +* Add missing countries. PR [#32](https://github.com/pydantic/pydantic-extra-types/pull/32) by [EssaAlshammri](https://github.com/EssaAlshammri) +* Resolve `pydantic-core` dependency conflict. PR [#45](https://github.com/pydantic/pydantic-extra-types/pull/45) by [hirotasoshu](https://github.com/hirotasoshu) +* Add `MIR` card brand. PR [#46](https://github.com/pydantic/pydantic-extra-types/pull/46) by [hirotasoshu](https://github.com/hirotasoshu) +* Fix dependencies version. PR [#48](https://github.com/pydantic/pydantic-extra-types/pull/48) by [yezz123](https://github.com/yezz123) +* Add documentation for `Color` and `PaymentCardNumber`. PR [#50](https://github.com/pydantic/pydantic-extra-types/pull/50) by [Kludex](https://github.com/Kludex) +* Add hooky. PR [#51](https://github.com/pydantic/pydantic-extra-types/pull/51) by [Kludex](https://github.com/Kludex) +* Simplify project structure. PR [#52](https://github.com/pydantic/pydantic-extra-types/pull/52) by [Kludex](https://github.com/Kludex) +* Add coverage check on the pipeline. PR [#53](https://github.com/pydantic/pydantic-extra-types/pull/53) by [Kludex](https://github.com/Kludex) +* Refactor country type using `pycountry`. PR [#54](https://github.com/pydantic/pydantic-extra-types/pull/54) by [yezz123](https://github.com/yezz123) +* Add 100% coverage. PR [#57](https://github.com/pydantic/pydantic-extra-types/pull/57) by [Kludex](https://github.com/Kludex) +* Add support for transparent Color. PR [#59](https://github.com/pydantic/pydantic-extra-types/pull/59) by [CollinHeist](https://github.com/CollinHeist) +* Add documentation for `PhoneNumber` and `ABARoutingNumber`. PR [#60](https://github.com/pydantic/pydantic-extra-types/pull/60) by [Kludex](https://github.com/Kludex) +* Refactor README. PR [#61](https://github.com/pydantic/pydantic-extra-types/pull/61) by [Kludex](https://github.com/Kludex) +* Rename `routing_number.md` to `routing_numbers.md`. PR [#62](https://github.com/pydantic/pydantic-extra-types/pull/62) by [Kludex](https://github.com/Kludex) +* Fix code in `payment` documentation. PR [#63](https://github.com/pydantic/pydantic-extra-types/pull/63) by [yezz123](https://github.com/yezz123) +* Uprev pydantic to b3. PR [#69](https://github.com/pydantic/pydantic-extra-types/pull/69) by [samuelcolvin](https://github.com/samuelcolvin) +* Prepare for release 2.0.0. PR [#70](https://github.com/pydantic/pydantic-extra-types/pull/70) by [hramezani](https://github.com/hramezani) diff --git a/Makefile b/Makefile index f25b84f..f8d968f 100644 --- a/Makefile +++ b/Makefile @@ -18,12 +18,12 @@ refresh-lockfiles: .PHONY: format format: - ruff --fix $(sources) + ruff check --fix $(sources) ruff format $(sources) .PHONY: lint lint: - ruff $(sources) + ruff check $(sources) ruff format --check $(sources) .PHONY: mypy diff --git a/pydantic_extra_types/__init__.py b/pydantic_extra_types/__init__.py index f0e5e1e..766ce2d 100644 --- a/pydantic_extra_types/__init__.py +++ b/pydantic_extra_types/__init__.py @@ -1 +1 @@ -__version__ = '2.6.0' +__version__ = '2.7.0' diff --git a/pydantic_extra_types/color.py b/pydantic_extra_types/color.py index 34aa441..8b9eabe 100644 --- a/pydantic_extra_types/color.py +++ b/pydantic_extra_types/color.py @@ -7,6 +7,7 @@ A few colors have multiple names referring to the sames colors, eg. `grey` and ` In these cases the _last_ color when sorted alphabetically takes preferences, eg. `Color((0, 255, 255)).as_named() == 'cyan'` because "cyan" comes after "aqua". """ + from __future__ import annotations import math diff --git a/pydantic_extra_types/coordinate.py b/pydantic_extra_types/coordinate.py index df470d5..10eaa05 100644 --- a/pydantic_extra_types/coordinate.py +++ b/pydantic_extra_types/coordinate.py @@ -3,6 +3,7 @@ The `pydantic_extra_types.coordinate` module provides the [`Latitude`][pydantic_ [`Longitude`][pydantic_extra_types.coordinate.Longitude], and [`Coordinate`][pydantic_extra_types.coordinate.Coordinate] data types. """ + from dataclasses import dataclass from typing import Any, ClassVar, Tuple, Type diff --git a/pydantic_extra_types/country.py b/pydantic_extra_types/country.py index a6d26e2..7af99c7 100644 --- a/pydantic_extra_types/country.py +++ b/pydantic_extra_types/country.py @@ -1,6 +1,7 @@ """ Country definitions that are based on the [ISO 3166](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes). """ + from __future__ import annotations from dataclasses import dataclass diff --git a/pydantic_extra_types/currency_code.py b/pydantic_extra_types/currency_code.py index c19d9bf..fbc0cbd 100644 --- a/pydantic_extra_types/currency_code.py +++ b/pydantic_extra_types/currency_code.py @@ -1,6 +1,7 @@ """ Currency definitions that are based on the [ISO4217](https://en.wikipedia.org/wiki/ISO_4217). """ + from __future__ import annotations from typing import Any diff --git a/pydantic_extra_types/language_code.py b/pydantic_extra_types/language_code.py index 117e877..8c15385 100644 --- a/pydantic_extra_types/language_code.py +++ b/pydantic_extra_types/language_code.py @@ -1,9 +1,12 @@ """ Language definitions that are based on the [ISO 639-3](https://en.wikipedia.org/wiki/ISO_639-3) & [ISO 639-5](https://en.wikipedia.org/wiki/ISO_639-5). """ + from __future__ import annotations -from typing import Any +from dataclasses import dataclass +from functools import lru_cache +from typing import Any, Union from pydantic import GetCoreSchemaHandler, GetJsonSchemaHandler from pydantic_core import PydanticCustomError, core_schema @@ -17,6 +20,213 @@ except ModuleNotFoundError: # pragma: no cover ) +@dataclass +class LanguageInfo: + """ + LanguageInfo is a dataclass that contains the language information. + + Args: + alpha2: The language code in the [ISO 639-1 alpha-2](https://en.wikipedia.org/wiki/ISO_639-1) format. + alpha3: The language code in the [ISO 639-3 alpha-3](https://en.wikipedia.org/wiki/ISO_639-3) format. + name: The language name. + """ + + alpha2: Union[str, None] + alpha3: str + name: str + + +@lru_cache +def _languages() -> list[LanguageInfo]: + """ + Return a list of LanguageInfo objects containing the language information. + + Returns: + A list of LanguageInfo objects containing the language information. + """ + return [ + LanguageInfo( + alpha2=getattr(language, 'alpha_2', None), + alpha3=language.alpha_3, + name=language.name, + ) + for language in pycountry.languages + ] + + +@lru_cache +def _index_by_alpha2() -> dict[str, LanguageInfo]: + """ + Return a dictionary with the language code in the [ISO 639-1 alpha-2](https://en.wikipedia.org/wiki/ISO_639-1) format as the key and the LanguageInfo object as the value. + """ + return {language.alpha2: language for language in _languages() if language.alpha2 is not None} + + +@lru_cache +def _index_by_alpha3() -> dict[str, LanguageInfo]: + """ + Return a dictionary with the language code in the [ISO 639-3 alpha-3](https://en.wikipedia.org/wiki/ISO_639-3) format as the key and the LanguageInfo object as the value. + """ + return {language.alpha3: language for language in _languages()} + + +@lru_cache +def _index_by_name() -> dict[str, LanguageInfo]: + """ + Return a dictionary with the language name as the key and the LanguageInfo object as the value. + """ + return {language.name: language for language in _languages()} + + +class LanguageAlpha2(str): + """LanguageAlpha2 parses languages codes in the [ISO 639-1 alpha-2](https://en.wikipedia.org/wiki/ISO_639-1) + format. + + ```py + from pydantic import BaseModel + + from pydantic_extra_types.language_code import LanguageAlpha2 + + class Movie(BaseModel): + audio_lang: LanguageAlpha2 + subtitles_lang: LanguageAlpha2 + + movie = Movie(audio_lang='de', subtitles_lang='fr') + print(movie) + #> audio_lang='de' subtitles_lang='fr' + ``` + """ + + @classmethod + def _validate(cls, __input_value: str, _: core_schema.ValidationInfo) -> LanguageAlpha2: + """ + Validate a language code in the ISO 639-1 alpha-2 format from the provided str value. + + Args: + __input_value: The str value to be validated. + _: The Pydantic ValidationInfo. + + Returns: + The validated language code in the ISO 639-1 alpha-2 format. + """ + if __input_value not in _index_by_alpha2(): + raise PydanticCustomError('language_alpha2', 'Invalid language alpha2 code') + return cls(__input_value) + + @classmethod + def __get_pydantic_core_schema__( + cls, source: type[Any], handler: GetCoreSchemaHandler + ) -> core_schema.AfterValidatorFunctionSchema: + """ + Return a Pydantic CoreSchema with the language code in the ISO 639-1 alpha-2 format validation. + + Args: + source: The source type. + handler: The handler to get the CoreSchema. + + Returns: + A Pydantic CoreSchema with the language code in the ISO 639-1 alpha-2 format validation. + """ + return core_schema.with_info_after_validator_function( + cls._validate, + core_schema.str_schema(to_lower=True), + ) + + @classmethod + def __get_pydantic_json_schema__( + cls, schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler + ) -> dict[str, Any]: + """ + Return a Pydantic JSON Schema with the language code in the ISO 639-1 alpha-2 format validation. + + Args: + schema: The Pydantic CoreSchema. + handler: The handler to get the JSON Schema. + + Returns: + A Pydantic JSON Schema with the language code in the ISO 639-1 alpha-2 format validation. + """ + json_schema = handler(schema) + json_schema.update({'pattern': r'^\w{2}$'}) + return json_schema + + @property + def alpha3(self) -> str: + """The language code in the [ISO 639-3 alpha-3](https://en.wikipedia.org/wiki/ISO_639-3) format.""" + return _index_by_alpha2()[self].alpha3 + + @property + def name(self) -> str: + """The language name.""" + return _index_by_alpha2()[self].name + + +class LanguageName(str): + """LanguageName parses languages names listed in the [ISO 639-3 standard](https://en.wikipedia.org/wiki/ISO_639-3) + format. + + ```py + from pydantic import BaseModel + + from pydantic_extra_types.language_code import LanguageName + + class Movie(BaseModel): + audio_lang: LanguageName + subtitles_lang: LanguageName + + movie = Movie(audio_lang='Dutch', subtitles_lang='Mandarin Chinese') + print(movie) + #> audio_lang='Dutch' subtitles_lang='Mandarin Chinese' + ``` + """ + + @classmethod + def _validate(cls, __input_value: str, _: core_schema.ValidationInfo) -> LanguageName: + """ + Validate a language name from the provided str value. + + Args: + __input_value: The str value to be validated. + _: The Pydantic ValidationInfo. + + Returns: + The validated language name. + """ + if __input_value not in _index_by_name(): + raise PydanticCustomError('language_name', 'Invalid language name') + return cls(__input_value) + + @classmethod + def __get_pydantic_core_schema__( + cls, source: type[Any], handler: GetCoreSchemaHandler + ) -> core_schema.AfterValidatorFunctionSchema: + """ + Return a Pydantic CoreSchema with the language name validation. + + Args: + source: The source type. + handler: The handler to get the CoreSchema. + + Returns: + A Pydantic CoreSchema with the language name validation. + """ + return core_schema.with_info_after_validator_function( + cls._validate, + core_schema.str_schema(), + serialization=core_schema.to_string_ser_schema(), + ) + + @property + def alpha2(self) -> Union[str, None]: + """The language code in the [ISO 639-1 alpha-2](https://en.wikipedia.org/wiki/ISO_639-1) format. Does not exist for all languages.""" + return _index_by_name()[self].alpha2 + + @property + def alpha3(self) -> str: + """The language code in the [ISO 639-3 alpha-3](https://en.wikipedia.org/wiki/ISO_639-3) format.""" + return _index_by_name()[self].alpha3 + + class ISO639_3(str): """ISO639_3 parses Language in the [ISO 639-3 alpha-3](https://en.wikipedia.org/wiki/ISO_639-3_alpha-3) format. diff --git a/pydantic_extra_types/pendulum_dt.py b/pydantic_extra_types/pendulum_dt.py index f507779..f3a304f 100644 --- a/pydantic_extra_types/pendulum_dt.py +++ b/pydantic_extra_types/pendulum_dt.py @@ -4,7 +4,9 @@ CoreSchema implementation. This allows Pydantic to validate the DateTime object. """ try: + from pendulum import Date as _Date from pendulum import DateTime as _DateTime + from pendulum import Duration as _Duration from pendulum import parse except ModuleNotFoundError: # pragma: no cover raise RuntimeError( @@ -72,3 +74,119 @@ class DateTime(_DateTime): except Exception as exc: raise PydanticCustomError('value_error', 'value is not a valid timestamp') from exc return handler(data) + + +class Date(_Date): + """ + A `pendulum.Date` object. At runtime, this type decomposes into pendulum.Date automatically. + This type exists because Pydantic throws a fit on unknown types. + + ```python + from pydantic import BaseModel + from pydantic_extra_types.pendulum_dt import Date + + class test_model(BaseModel): + dt: Date + + print(test_model(dt='2021-01-01')) + + #> test_model(dt=Date(2021, 1, 1)) + ``` + """ + + __slots__: List[str] = [] + + @classmethod + def __get_pydantic_core_schema__(cls, source: Type[Any], handler: GetCoreSchemaHandler) -> core_schema.CoreSchema: + """ + Return a Pydantic CoreSchema with the Date validation + + Args: + source: The source type to be converted. + handler: The handler to get the CoreSchema. + + Returns: + A Pydantic CoreSchema with the Date validation. + """ + return core_schema.no_info_wrap_validator_function(cls._validate, core_schema.date_schema()) + + @classmethod + def _validate(cls, value: Any, handler: core_schema.ValidatorFunctionWrapHandler) -> Any: + """ + Validate the date object and return it. + + Args: + value: The value to validate. + handler: The handler to get the CoreSchema. + + Returns: + The validated value or raises a PydanticCustomError. + """ + # if we are passed an existing instance, pass it straight through. + if isinstance(value, _Date): + return handler(value) + + # otherwise, parse it. + try: + data = parse(value) + except Exception as exc: + raise PydanticCustomError('value_error', 'value is not a valid date') from exc + return handler(data) + + +class Duration(_Duration): + """ + A `pendulum.Duration` object. At runtime, this type decomposes into pendulum.Duration automatically. + This type exists because Pydantic throws a fit on unknown types. + + ```python + from pydantic import BaseModel + from pydantic_extra_types.pendulum_dt import Duration + + class test_model(BaseModel): + delta_t: Duration + + print(test_model(delta_t='P1DT25H')) + + #> test_model(delta_t=Duration(days=2, hours=1)) + ``` + """ + + __slots__: List[str] = [] + + @classmethod + def __get_pydantic_core_schema__(cls, source: Type[Any], handler: GetCoreSchemaHandler) -> core_schema.CoreSchema: + """ + Return a Pydantic CoreSchema with the Duration validation + + Args: + source: The source type to be converted. + handler: The handler to get the CoreSchema. + + Returns: + A Pydantic CoreSchema with the Duration validation. + """ + return core_schema.no_info_wrap_validator_function(cls._validate, core_schema.timedelta_schema()) + + @classmethod + def _validate(cls, value: Any, handler: core_schema.ValidatorFunctionWrapHandler) -> Any: + """ + Validate the Duration object and return it. + + Args: + value: The value to validate. + handler: The handler to get the CoreSchema. + + Returns: + The validated value or raises a PydanticCustomError. + """ + # if we are passed an existing instance, pass it straight through. + if isinstance(value, _Duration): + return handler(value) + + # otherwise, parse it. + try: + data = parse(value) + except Exception as exc: + raise PydanticCustomError('value_error', 'value is not a valid duration') from exc + return handler(data) diff --git a/pydantic_extra_types/phone_numbers.py b/pydantic_extra_types/phone_numbers.py index 7acaa89..cf03417 100644 --- a/pydantic_extra_types/phone_numbers.py +++ b/pydantic_extra_types/phone_numbers.py @@ -4,6 +4,7 @@ The `pydantic_extra_types.phone_numbers` module provides the This class depends on the [phonenumbers] package, which is a Python port of Google's [libphonenumber]. """ + from __future__ import annotations from typing import Any, Callable, ClassVar, Generator diff --git a/pydantic_extra_types/routing_number.py b/pydantic_extra_types/routing_number.py index 22ea6e8..b4d53a8 100644 --- a/pydantic_extra_types/routing_number.py +++ b/pydantic_extra_types/routing_number.py @@ -2,6 +2,7 @@ The `pydantic_extra_types.routing_number` module provides the [`ABARoutingNumber`][pydantic_extra_types.routing_number.ABARoutingNumber] data type. """ + from typing import Any, ClassVar, Type from pydantic import GetCoreSchemaHandler diff --git a/pydantic_extra_types/ulid.py b/pydantic_extra_types/ulid.py index d2bf650..5891f9f 100644 --- a/pydantic_extra_types/ulid.py +++ b/pydantic_extra_types/ulid.py @@ -3,6 +3,7 @@ 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 diff --git a/requirements/linting.txt b/requirements/linting.txt index 9bc7bc1..07ccbb2 100644 --- a/requirements/linting.txt +++ b/requirements/linting.txt @@ -14,7 +14,7 @@ filelock==3.13.1 # via virtualenv identify==2.5.35 # via pre-commit -mypy==1.8.0 +mypy==1.9.0 # via -r requirements/linting.in mypy-extensions==1.0.0 # via mypy @@ -22,11 +22,11 @@ nodeenv==1.8.0 # via pre-commit platformdirs==4.2.0 # via virtualenv -pre-commit==3.6.2 +pre-commit==3.7.0 # via -r requirements/linting.in pyyaml==6.0.1 # via pre-commit -ruff==0.2.2 +ruff==0.3.5 # via -r requirements/linting.in typing-extensions==4.10.0 # via mypy diff --git a/requirements/testing.txt b/requirements/testing.txt index 0c36fc5..37e7c23 100644 --- a/requirements/testing.txt +++ b/requirements/testing.txt @@ -10,7 +10,7 @@ charset-normalizer==3.3.2 # via requests codecov==2.1.13 # via -r requirements/testing.in -coverage[toml]==7.4.3 +coverage[toml]==7.4.4 # via # -r requirements/testing.in # codecov @@ -31,12 +31,12 @@ pluggy==1.4.0 # via pytest pygments==2.17.2 # via rich -pytest==8.0.2 +pytest==8.1.1 # via # -r requirements/testing.in # pytest-cov # pytest-pretty -pytest-cov==4.1.0 +pytest-cov==5.0.0 # via -r requirements/testing.in pytest-pretty==1.2.0 # via -r requirements/testing.in diff --git a/tests/test_json_schema.py b/tests/test_json_schema.py index 5daa246..d0798bc 100644 --- a/tests/test_json_schema.py +++ b/tests/test_json_schema.py @@ -13,7 +13,7 @@ from pydantic_extra_types.country import ( ) from pydantic_extra_types.currency_code import ISO4217, Currency from pydantic_extra_types.isbn import ISBN -from pydantic_extra_types.language_code import ISO639_3, ISO639_5 +from pydantic_extra_types.language_code import ISO639_3, ISO639_5, LanguageAlpha2, LanguageName from pydantic_extra_types.mac_address import MacAddress from pydantic_extra_types.payment import PaymentCardNumber from pydantic_extra_types.pendulum_dt import DateTime @@ -219,6 +219,24 @@ everyday_currencies.sort() 'type': 'object', }, ), + ( + LanguageAlpha2, + { + 'properties': {'x': {'pattern': '^\\w{2}$', 'title': 'X', 'type': 'string'}}, + 'required': ['x'], + 'title': 'Model', + 'type': 'object', + }, + ), + ( + LanguageName, + { + 'properties': {'x': {'title': 'X', 'type': 'string'}}, + 'required': ['x'], + 'title': 'Model', + 'type': 'object', + }, + ), ( ISO639_3, { diff --git a/tests/test_language_codes.py b/tests/test_language_codes.py index 27cc44a..d32cda9 100644 --- a/tests/test_language_codes.py +++ b/tests/test_language_codes.py @@ -1,10 +1,37 @@ import re +from string import printable import pycountry import pytest from pydantic import BaseModel, ValidationError from pydantic_extra_types import language_code +from pydantic_extra_types.language_code import ( + LanguageAlpha2, + LanguageInfo, + LanguageName, + _index_by_alpha2, + _index_by_alpha3, + _index_by_name, +) + +PARAMS_AMOUNT = 20 + + +@pytest.fixture(scope='module', name='MovieAlpha2') +def movie_alpha2_fixture(): + class Movie(BaseModel): + audio_lang: LanguageAlpha2 + + return Movie + + +@pytest.fixture(scope='module', name='MovieName') +def movie_name_fixture(): + class Movie(BaseModel): + audio_lang: LanguageName + + return Movie class ISO3CheckingModel(BaseModel): @@ -15,6 +42,34 @@ class ISO5CheckingModel(BaseModel): lang: language_code.ISO639_5 +@pytest.mark.parametrize('alpha2, language_data', list(_index_by_alpha2().items())) +def test_valid_alpha2(alpha2: str, language_data: LanguageInfo, MovieAlpha2): + the_godfather = MovieAlpha2(audio_lang=alpha2) + assert the_godfather.audio_lang == language_data.alpha2 + assert the_godfather.audio_lang.alpha3 == language_data.alpha3 + assert the_godfather.audio_lang.name == language_data.name + + +@pytest.mark.parametrize('alpha2', list(printable) + list(_index_by_alpha3().keys())[:PARAMS_AMOUNT]) +def test_invalid_alpha2(alpha2: str, MovieAlpha2): + with pytest.raises(ValidationError, match='Invalid language alpha2 code'): + MovieAlpha2(audio_lang=alpha2) + + +@pytest.mark.parametrize('name, language_data', list(_index_by_name().items())[:PARAMS_AMOUNT]) +def test_valid_name(name: str, language_data: LanguageInfo, MovieName): + the_godfather = MovieName(audio_lang=name) + assert the_godfather.audio_lang == language_data.name + assert the_godfather.audio_lang.alpha2 == language_data.alpha2 + assert the_godfather.audio_lang.alpha3 == language_data.alpha3 + + +@pytest.mark.parametrize('name', set(printable) - {'E', 'U'}) # E and U are valid language codes +def test_invalid_name(name: str, MovieName): + with pytest.raises(ValidationError, match='Invalid language name'): + MovieName(audio_lang=name) + + @pytest.mark.parametrize('lang', map(lambda lang: lang.alpha_3, pycountry.languages)) def test_iso_ISO639_3_code_ok(lang: str): model = ISO3CheckingModel(lang=lang) diff --git a/tests/test_pendulum_dt.py b/tests/test_pendulum_dt.py index 31306d7..18ec5ef 100644 --- a/tests/test_pendulum_dt.py +++ b/tests/test_pendulum_dt.py @@ -2,38 +2,119 @@ import pendulum import pytest from pydantic import BaseModel, ValidationError -from pydantic_extra_types.pendulum_dt import DateTime +from pydantic_extra_types.pendulum_dt import Date, DateTime, Duration -class Model(BaseModel): +class DtModel(BaseModel): dt: DateTime +class DateModel(BaseModel): + d: Date + + +class DurationModel(BaseModel): + delta_t: Duration + + def test_pendulum_dt_existing_instance(): """ Verifies that constructing a model with an existing pendulum dt doesn't throw. """ now = pendulum.now() - model = Model(dt=now) + model = DtModel(dt=now) assert model.dt == now +def test_pendulum_date_existing_instance(): + """ + Verifies that constructing a model with an existing pendulum date doesn't throw. + """ + today = pendulum.today().date() + model = DateModel(d=today) + assert model.d == today + + +def test_pendulum_duration_existing_instance(): + """ + Verifies that constructing a model with an existing pendulum duration doesn't throw. + """ + delta_t = pendulum.duration(days=42, hours=13, minutes=37) + model = DurationModel(delta_t=delta_t) + + assert model.delta_t.total_seconds() == delta_t.total_seconds() + + @pytest.mark.parametrize( - 'dt', [pendulum.now().to_iso8601_string(), pendulum.now().to_w3c_string(), pendulum.now().to_iso8601_string()] + 'dt', + [ + pendulum.now().to_iso8601_string(), + pendulum.now().to_w3c_string(), + pendulum.now().to_iso8601_string(), + ], ) def test_pendulum_dt_from_serialized(dt): """ Verifies that building an instance from serialized, well-formed strings decode properly. """ dt_actual = pendulum.parse(dt) - model = Model(dt=dt) + model = DtModel(dt=dt) assert model.dt == dt_actual +def test_pendulum_date_from_serialized(): + """ + Verifies that building an instance from serialized, well-formed strings decode properly. + """ + date_actual = pendulum.parse('2024-03-18').date() + model = DateModel(d='2024-03-18') + assert model.d == date_actual + + +@pytest.mark.parametrize( + 'delta_t_str', + [ + 'P3.14D', + 'PT404H', + 'P1DT25H', + 'P2W', + 'P10Y10M10D', + ], +) +def test_pendulum_duration_from_serialized(delta_t_str): + """ + Verifies that building an instance from serialized, well-formed strings decode properly. + """ + true_delta_t = pendulum.parse(delta_t_str) + model = DurationModel(delta_t=delta_t_str) + assert model.delta_t == true_delta_t + + @pytest.mark.parametrize('dt', [None, 'malformed', pendulum.now().to_iso8601_string()[:5], 42]) def test_pendulum_dt_malformed(dt): """ Verifies that the instance fails to validate if malformed dt are passed. """ with pytest.raises(ValidationError): - Model(dt=dt) + DtModel(dt=dt) + + +@pytest.mark.parametrize('date', [None, 'malformed', pendulum.today().to_iso8601_string()[:5], 42]) +def test_pendulum_date_malformed(date): + """ + Verifies that the instance fails to validate if malformed date are passed. + """ + with pytest.raises(ValidationError): + DateModel(d=date) + + +@pytest.mark.parametrize( + 'delta_t', + [None, 'malformed', pendulum.today().to_iso8601_string()[:5], 42, '12m'], +) +def test_pendulum_duration_malformed(delta_t): + """ + Verifies that the instance fails to validate if malformed durations are passed. + """ + with pytest.raises(ValidationError): + DurationModel(delta_t=delta_t) diff --git a/tests/test_ulid.py b/tests/test_ulid.py index b9ae527..94cec2d 100644 --- a/tests/test_ulid.py +++ b/tests/test_ulid.py @@ -31,8 +31,8 @@ class Something(BaseModel): (_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) + (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), -- cgit v1.2.3