diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f1a6bbc..af4e930 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,4 +1,9 @@ repos: + - repo: https://github.com/asottile/pyupgrade + rev: v3.15.2 + hooks: + - id: pyupgrade + args: ["--py39-plus"] - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. rev: v0.3.2 @@ -7,8 +12,3 @@ repos: - id: ruff # Run the formatter. - id: ruff-format - - repo: https://github.com/asottile/pyupgrade - rev: v3.15.2 - hooks: - - id: pyupgrade - args: ["--py39-plus"] diff --git a/exchange_calendars_extensions/api/changes.py b/exchange_calendars_extensions/api/changes.py index 0d2f20d..2f7e0e1 100644 --- a/exchange_calendars_extensions/api/changes.py +++ b/exchange_calendars_extensions/api/changes.py @@ -5,8 +5,16 @@ from typing import Union, Annotated, Callable import pandas as pd -from pydantic import BaseModel, Field, RootModel, model_validator, validate_call -from pydantic.functional_validators import BeforeValidator, AfterValidator +from pydantic import ( + BaseModel, + Field, + RootModel, + model_validator, + validate_call, + WithJsonSchema, + BeforeValidator, + AfterValidator, +) from typing_extensions import ( Literal, Any, @@ -100,14 +108,24 @@ def _to_date(value: pd.Timestamp) -> pd.Timestamp: # A type alias for pd.Timestamp that allows initialisation from suitably formatted string values. -TimestampLike = Annotated[pd.Timestamp, BeforeValidator(_to_timestamp)] +TimestampLike = Annotated[ + pd.Timestamp, + BeforeValidator(_to_timestamp), + WithJsonSchema({"type": "string", "format": "date-time"}), + Field(examples=["2020-01-01T00:00:00Z"]), +] # A type alias for TimestampLike that normalizes the timestamp to a date-like value. # # Date-like means that the timestamp is timezone-naive and normalized to the date boundary, i.e. midnight of the day it # represents. If the input converts to a valid pd.Timestamp, any timezone information, if present, is discarded. If the # result is not aligned with a date boundary, it is normalized to midnight of the same day. -DateLike = Annotated[TimestampLike, AfterValidator(_to_date)] +DateLike = Annotated[ + TimestampLike, + AfterValidator(_to_date), + WithJsonSchema({"type": "string", "format": "date"}), + Field(examples=["2020-01-01"]), +] class AbstractDayProps( @@ -117,7 +135,9 @@ class AbstractDayProps( Abstract base class for special day properties. """ - name: str # The name of the day. + name: str = Field( + examples=["Holiday", "Ad-hoc Holiday", "Special Close Day", "Special Open Day"] + ) # The name of the day. class DayProps(AbstractDayProps): @@ -125,9 +145,11 @@ class DayProps(AbstractDayProps): Vanilla special day specification. """ - type: Literal[ - DayType.HOLIDAY, DayType.MONTHLY_EXPIRY, DayType.QUARTERLY_EXPIRY - ] # The type of the special day. + type: Literal[DayType.HOLIDAY, DayType.MONTHLY_EXPIRY, DayType.QUARTERLY_EXPIRY] = ( + Field( + examples=[DayType.HOLIDAY, DayType.MONTHLY_EXPIRY, DayType.QUARTERLY_EXPIRY] + ) + ) # The type of the special day. def __str__(self): return f'{{type={self.type.name}, name="{self.name}"}}' @@ -168,7 +190,12 @@ def _to_time(value: Union[dt.time, str]): # A type alias for dt.time that allows initialisation from suitably formatted string values. -TimeLike = Annotated[dt.time, BeforeValidator(_to_time)] +TimeLike = Annotated[ + dt.time, + BeforeValidator(_to_time), + WithJsonSchema({"type": "string", "format": "time"}), + Field(examples=["09:00", "16:30"]), +] class DayPropsWithTime(AbstractDayProps): @@ -188,7 +215,10 @@ def __str__(self): # Type alias for valid day properties. DayPropsLike = Annotated[Union[DayProps, DayPropsWithTime], Field(discriminator="type")] -Tags = Union[list[str], Union[tuple[str], Union[set[str], None]]] +Tags = Annotated[ + Union[list[str], Union[tuple[str], Union[set[str], None]]], + Field(examples=[["tag1", "tag2"]]), +] class DayMeta( @@ -202,7 +232,7 @@ class DayMeta( tags: Tags = [] # Free-form comment. - comment: Union[str, None] = None + comment: Union[str, None] = Field(default=None, examples=["This is a comment."]) @model_validator(mode="after") def _canonicalize(self) -> "DayMeta": @@ -296,9 +326,17 @@ class ChangeSet( already exist in any day type category, and updated/moved to the new day type if it does. """ - add: dict[DateLike, DayPropsLike] = Field(default_factory=dict) - remove: list[DateLike] = Field(default_factory=list) - meta: dict[DateLike, DayMeta] = Field(default_factory=dict) + add: dict[DateLike, DayPropsLike] = Field( + default_factory=dict, + examples=[{"2020-01-01": {"type": "holiday", "name": "New Year's Day"}}], + ) + remove: list[DateLike] = Field(default_factory=list, examples=["2020-01-01"]) + meta: dict[DateLike, DayMeta] = Field( + default_factory=dict, + examples=[ + {"2020-01-01": {"tags": ["tag1", "tag2"], "comment": "This is a comment."}} + ], + ) @model_validator(mode="after") def _canonicalize(self) -> "ChangeSet": @@ -552,7 +590,7 @@ def all_days(self, include_meta: bool = False) -> tuple[pd.Timestamp, ...]: # A type alias for a dictionary of changesets, mapping exchange key to a corresponding change set. class ChangeSetDict(RootModel): - root: dict[str, ChangeSet] + root: dict[str, ChangeSet] = Field(default_factory=dict) # Delegate all dictionary-typical methods to the root dictionary. def __getitem__(self, key): diff --git a/poetry.lock b/poetry.lock index b2b01f2..fa8035f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "annotated-types" @@ -342,13 +342,13 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pre-commit" -version = "3.7.0" +version = "3.7.1" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.9" files = [ - {file = "pre_commit-3.7.0-py2.py3-none-any.whl", hash = "sha256:5eae9e10c2b5ac51577c3452ec0a490455c45a0533f7960f993a0d01e59decab"}, - {file = "pre_commit-3.7.0.tar.gz", hash = "sha256:e209d61b8acdcf742404408531f0c37d49d2c734fd7cff2d6076083d191cb060"}, + {file = "pre_commit-3.7.1-py2.py3-none-any.whl", hash = "sha256:fae36fd1d7ad7d6a5a1c0b0d5adb2ed1a3bda5a21bf6c3e5372073d7a11cd4c5"}, + {file = "pre_commit-3.7.1.tar.gz", hash = "sha256:8ca3ad567bc78a4972a3f1a477e94a79d4597e8140a6e0b651c5e33899c3654a"}, ] [package.dependencies] @@ -470,13 +470,13 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pytest" -version = "8.2.0" +version = "8.2.1" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.2.0-py3-none-any.whl", hash = "sha256:1733f0620f6cda4095bbf0d9ff8022486e91892245bb9e7d5542c018f612f233"}, - {file = "pytest-8.2.0.tar.gz", hash = "sha256:d507d4482197eac0ba2bae2e9babf0672eb333017bcedaa5fb1a3d42c1174b3f"}, + {file = "pytest-8.2.1-py3-none-any.whl", hash = "sha256:faccc5d332b8c3719f40283d0d44aa5cf101cec36f88cde9ed8f2bc0538612b1"}, + {file = "pytest-8.2.1.tar.gz", hash = "sha256:5046e5b46d8e4cac199c373041f26be56fdb81eb4e67dc11d4e10811fc3408fd"}, ] [package.dependencies] @@ -545,6 +545,7 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -552,8 +553,16 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -570,6 +579,7 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -577,6 +587,7 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},