diff --git a/src/fca_api/types/__init__.py b/src/fca_api/types/__init__.py index e52646f..0be6ead 100644 --- a/src/fca_api/types/__init__.py +++ b/src/fca_api/types/__init__.py @@ -40,4 +40,4 @@ - `Pydantic Documentation `_ """ -from . import base, field_parsers, firm, individual, markets, pagination, products, search, settings +from . import annotations, base, field_parsers, firm, individual, markets, pagination, products, search, settings diff --git a/src/fca_api/types/field_parsers.py b/src/fca_api/types/field_parsers.py index 52e988a..0cfec1a 100644 --- a/src/fca_api/types/field_parsers.py +++ b/src/fca_api/types/field_parsers.py @@ -6,7 +6,7 @@ @pydantic.BeforeValidator -def ParseFcaDate(date_str: str) -> datetime.datetime | None: +def ParseFcaDate(date_str: str | None) -> datetime.datetime | None: """Parse FCA date strings into ``datetime`` objects. The FCA API returns dates in a variety of formats. This helper tries @@ -18,7 +18,9 @@ def ParseFcaDate(date_str: str) -> datetime.datetime | None: TypeError: If the input is not a string. ValueError: If the value cannot be parsed using any known format. """ - if not isinstance(date_str, str): + if date_str is None: + return None + elif not isinstance(date_str, str): raise TypeError(f"Expected a string, got {type(date_str).__name__}") date_str = date_str.strip() if not date_str: diff --git a/src/fca_api/types/firm.py b/src/fca_api/types/firm.py index e7405dc..785756d 100644 --- a/src/fca_api/types/firm.py +++ b/src/fca_api/types/firm.py @@ -3,7 +3,7 @@ import pydantic -from . import base, field_parsers +from . import annotations, base, field_parsers class FirmDetails(base.Base): @@ -217,6 +217,7 @@ class FirmDetails(base.Base): validation_alias=pydantic.AliasChoices("name", "names_url"), serialization_alias="names_url", ), + annotations.FcaApiUrl(), ] individuals_url: Annotated[ pydantic.HttpUrl, @@ -225,6 +226,7 @@ class FirmDetails(base.Base): validation_alias=pydantic.AliasChoices("individuals", "individuals_url"), serialization_alias="individuals_url", ), + annotations.FcaApiUrl(), ] requirements_url: Annotated[ pydantic.HttpUrl, @@ -233,6 +235,7 @@ class FirmDetails(base.Base): validation_alias=pydantic.AliasChoices("requirements", "requirements_url"), serialization_alias="requirements_url", ), + annotations.FcaApiUrl(), ] permissions_url: Annotated[ pydantic.HttpUrl, @@ -241,6 +244,7 @@ class FirmDetails(base.Base): validation_alias=pydantic.AliasChoices("permission", "permissions_url"), serialization_alias="permissions_url", ), + annotations.FcaApiUrl(), ] passports_url: Annotated[ pydantic.HttpUrl, @@ -249,6 +253,7 @@ class FirmDetails(base.Base): validation_alias=pydantic.AliasChoices("passport", "passports_url"), serialization_alias="passports_url", ), + annotations.FcaApiUrl(), ] regulators_url: Annotated[ pydantic.HttpUrl, @@ -257,6 +262,7 @@ class FirmDetails(base.Base): validation_alias=pydantic.AliasChoices("regulators", "regulators_url"), serialization_alias="regulators_url", ), + annotations.FcaApiUrl(), ] waivers_url: Annotated[ pydantic.HttpUrl, @@ -265,6 +271,7 @@ class FirmDetails(base.Base): validation_alias=pydantic.AliasChoices("waivers", "waivers_url"), serialization_alias="waivers_url", ), + annotations.FcaApiUrl(), ] exclusions_url: Annotated[ pydantic.HttpUrl, @@ -273,6 +280,7 @@ class FirmDetails(base.Base): validation_alias=pydantic.AliasChoices("exclusions", "exclusions_url"), serialization_alias="exclusions_url", ), + annotations.FcaApiUrl(), ] address_url: Annotated[ pydantic.HttpUrl, @@ -281,6 +289,7 @@ class FirmDetails(base.Base): validation_alias=pydantic.AliasChoices("address", "address_url"), serialization_alias="address_url", ), + annotations.FcaApiUrl(), ] appointed_representative_url: Annotated[ pydantic.HttpUrl, @@ -289,6 +298,7 @@ class FirmDetails(base.Base): validation_alias=pydantic.AliasChoices("appointed representative", "appointed_representative_url"), serialization_alias="appointed_representative_url", ), + annotations.FcaApiUrl(), ] disciplinary_history_url: Annotated[ pydantic.HttpUrl, @@ -297,6 +307,7 @@ class FirmDetails(base.Base): validation_alias=pydantic.AliasChoices("disciplinaryhistory", "disciplinary_history_url"), serialization_alias="disciplinary_history_url", ), + annotations.FcaApiUrl(), ] @@ -451,6 +462,7 @@ class FirmAddress(base.Base): validation_alias=pydantic.AliasChoices("url", "address_url"), serialization_alias="address_url", ), + annotations.FcaApiUrl(), ] @@ -548,6 +560,7 @@ class FirmControlledFunction(base.Base): pydantic.Field( description="The URL of the controlled function record in the FCA register.", ), + annotations.FcaApiUrl(), ] @@ -585,6 +598,7 @@ class FirmIndividual(base.Base): pydantic.Field( description="The URL of the individual record in the FCA register.", ), + annotations.FcaApiUrl(), ] @@ -736,6 +750,7 @@ class FirmRequirement(base.RelaxedBase): serialization_alias="financial_promotions_investment_types", default=None, ), + annotations.FcaApiUrl(), ] @@ -914,6 +929,7 @@ class FirmWaiver(base.Base): validation_alias=pydantic.AliasChoices("waivers_discretions_url", "discretions_url"), serialization_alias="discretions_url", ), + annotations.FcaApiUrl(), ] @@ -1034,6 +1050,7 @@ class FirmAppointedRepresentative(base.Base): pydantic.Field( description="The URL of the appointed representative record in the FCA register.", ), + annotations.FcaApiUrl(), ] frn: Annotated[ str, diff --git a/src/fca_api/types/individual.py b/src/fca_api/types/individual.py index f3c7dbe..a918f43 100644 --- a/src/fca_api/types/individual.py +++ b/src/fca_api/types/individual.py @@ -3,7 +3,7 @@ import pydantic -from . import base, field_parsers +from . import annotations, base, field_parsers class Individual(base.Base): @@ -44,6 +44,7 @@ class Individual(base.Base): validation_alias=pydantic.AliasChoices("disciplinary history", "disciplinary_history"), serialization_alias="disciplinary_history", ), + annotations.FcaApiUrl(), ] status: Annotated[ str, @@ -62,6 +63,7 @@ class Individual(base.Base): validation_alias=pydantic.AliasChoices("current roles & activities", "current_roles_and_activities"), serialization_alias="current_roles_and_activities", ), + annotations.FcaApiUrl(), ] @@ -152,6 +154,7 @@ class IndividualControlledFunction(base.Base): pydantic.Field( description="URL to the controlled function details.", ), + annotations.FcaApiUrl(), ] diff --git a/src/fca_api/types/markets.py b/src/fca_api/types/markets.py index 802765b..8ac09a8 100644 --- a/src/fca_api/types/markets.py +++ b/src/fca_api/types/markets.py @@ -2,7 +2,7 @@ import pydantic -from . import base, field_parsers +from . import annotations, base, field_parsers class RegulatedMarket(base.Base): @@ -68,4 +68,5 @@ class RegulatedMarket(base.Base): validation_alias=pydantic.AliasChoices("firmurl", "firm_url"), serialization_alias="firm_url", ), + annotations.FcaApiUrl(), ] diff --git a/src/fca_api/types/products.py b/src/fca_api/types/products.py index eda31b2..2c37fc1 100644 --- a/src/fca_api/types/products.py +++ b/src/fca_api/types/products.py @@ -3,7 +3,7 @@ import pydantic -from . import base, field_parsers +from . import annotations, base, field_parsers class ProductDetails(base.Base): @@ -110,6 +110,7 @@ class ProductDetails(base.Base): validation_alias=pydantic.AliasChoices("operator", "operator_url"), serialization_alias="operator_url", ), + annotations.FcaApiUrl(), ] sub_funds_url: Annotated[ pydantic.HttpUrl, @@ -118,6 +119,7 @@ class ProductDetails(base.Base): validation_alias=pydantic.AliasChoices("sub-funds", "sub_funds_url"), serialization_alias="sub_funds_url", ), + annotations.FcaApiUrl(), ] other_name_url: Annotated[ pydantic.HttpUrl, @@ -126,6 +128,7 @@ class ProductDetails(base.Base): validation_alias=pydantic.AliasChoices("other name", "other_name_url"), serialization_alias="other_name_url", ), + annotations.FcaApiUrl(), ] cis_depositary_url: Annotated[ pydantic.HttpUrl, @@ -134,6 +137,7 @@ class ProductDetails(base.Base): validation_alias=pydantic.AliasChoices("cis depositary", "cis_depositary_url"), serialization_alias="cis_depositary_url", ), + annotations.FcaApiUrl(), ] @@ -204,4 +208,5 @@ class SubFundDetails(base.Base): pydantic.Field( description="URL to the sub-fund details.", ), + annotations.FcaApiUrl(), ] diff --git a/src/fca_api/types/search.py b/src/fca_api/types/search.py index 58cb2a9..ed5a23f 100644 --- a/src/fca_api/types/search.py +++ b/src/fca_api/types/search.py @@ -54,7 +54,7 @@ import pydantic -from . import base +from . import annotations, base class FirmSearchResult(base.Base): @@ -102,6 +102,7 @@ class FirmSearchResult(base.Base): pydantic.Field( description="The URL of the firm's record in the FCA register.", ), + annotations.FcaApiUrl(), ] frn: Annotated[ str, @@ -178,6 +179,7 @@ class IndividualSearchResult(base.Base): pydantic.Field( description="The URL of the individual's record in the FCA register.", ), + annotations.FcaApiUrl(), ] irn: Annotated[ str, @@ -258,6 +260,7 @@ class FundSearchResult(base.Base): pydantic.Field( description="The URL of the product's record in the FCA register.", ), + annotations.FcaApiUrl(), ] prn: Annotated[ str, diff --git a/tests/units/test_types_field_parsers.py b/tests/units/test_types_field_parsers.py index 07ab73b..e7bf099 100644 --- a/tests/units/test_types_field_parsers.py +++ b/tests/units/test_types_field_parsers.py @@ -25,19 +25,17 @@ def test_valid_date_formats(self): result = field_parsers.ParseFcaDate.func(date_str) assert result == expected, f"Failed to parse {date_str}" - def test_empty_string_returns_none(self): + @pytest.mark.parametrize("input_str", ["", " ", "\t\n", None]) + def test_empty_string_returns_none(self, input_str): """Test that empty strings return None.""" - assert field_parsers.ParseFcaDate.func("") is None - assert field_parsers.ParseFcaDate.func(" ") is None - assert field_parsers.ParseFcaDate.func("\t\n") is None + assert field_parsers.ParseFcaDate.func(input_str) is None - def test_non_string_input_raises_type_error(self): + @pytest.mark.parametrize("input_str", [123, [], {}, 12.34, datetime.datetime.now()]) + def test_non_string_input_raises_type_error(self, input_str): """Test that non-string inputs raise TypeError.""" - invalid_inputs = [123, None, [], {}, 12.34, datetime.datetime.now()] - for invalid_input in invalid_inputs: - with pytest.raises(TypeError, match=f"Expected a string, got {type(invalid_input).__name__}"): - field_parsers.ParseFcaDate.func(invalid_input) + with pytest.raises(TypeError, match=f"Expected a string, got {type(input_str).__name__}"): + field_parsers.ParseFcaDate.func(input_str) def test_unrecognized_format_raises_value_error(self): """Test that unrecognized date formats raise ValueError."""