Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/fca_api/types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,4 @@
- `Pydantic Documentation <https://docs.pydantic.dev/>`_
"""

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
6 changes: 4 additions & 2 deletions src/fca_api/types/field_parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down
19 changes: 18 additions & 1 deletion src/fca_api/types/firm.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import pydantic

from . import base, field_parsers
from . import annotations, base, field_parsers


class FirmDetails(base.Base):
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -297,6 +307,7 @@ class FirmDetails(base.Base):
validation_alias=pydantic.AliasChoices("disciplinaryhistory", "disciplinary_history_url"),
serialization_alias="disciplinary_history_url",
),
annotations.FcaApiUrl(),
]


Expand Down Expand Up @@ -451,6 +462,7 @@ class FirmAddress(base.Base):
validation_alias=pydantic.AliasChoices("url", "address_url"),
serialization_alias="address_url",
),
annotations.FcaApiUrl(),
]


Expand Down Expand Up @@ -548,6 +560,7 @@ class FirmControlledFunction(base.Base):
pydantic.Field(
description="The URL of the controlled function record in the FCA register.",
),
annotations.FcaApiUrl(),
]


Expand Down Expand Up @@ -585,6 +598,7 @@ class FirmIndividual(base.Base):
pydantic.Field(
description="The URL of the individual record in the FCA register.",
),
annotations.FcaApiUrl(),
]


Expand Down Expand Up @@ -736,6 +750,7 @@ class FirmRequirement(base.RelaxedBase):
serialization_alias="financial_promotions_investment_types",
default=None,
),
annotations.FcaApiUrl(),
]


Expand Down Expand Up @@ -914,6 +929,7 @@ class FirmWaiver(base.Base):
validation_alias=pydantic.AliasChoices("waivers_discretions_url", "discretions_url"),
serialization_alias="discretions_url",
),
annotations.FcaApiUrl(),
]


Expand Down Expand Up @@ -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,
Expand Down
5 changes: 4 additions & 1 deletion src/fca_api/types/individual.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import pydantic

from . import base, field_parsers
from . import annotations, base, field_parsers


class Individual(base.Base):
Expand Down Expand Up @@ -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,
Expand All @@ -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(),
]


Expand Down Expand Up @@ -152,6 +154,7 @@ class IndividualControlledFunction(base.Base):
pydantic.Field(
description="URL to the controlled function details.",
),
annotations.FcaApiUrl(),
]


Expand Down
3 changes: 2 additions & 1 deletion src/fca_api/types/markets.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import pydantic

from . import base, field_parsers
from . import annotations, base, field_parsers


class RegulatedMarket(base.Base):
Expand Down Expand Up @@ -68,4 +68,5 @@ class RegulatedMarket(base.Base):
validation_alias=pydantic.AliasChoices("firmurl", "firm_url"),
serialization_alias="firm_url",
),
annotations.FcaApiUrl(),
]
7 changes: 6 additions & 1 deletion src/fca_api/types/products.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import pydantic

from . import base, field_parsers
from . import annotations, base, field_parsers


class ProductDetails(base.Base):
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -134,6 +137,7 @@ class ProductDetails(base.Base):
validation_alias=pydantic.AliasChoices("cis depositary", "cis_depositary_url"),
serialization_alias="cis_depositary_url",
),
annotations.FcaApiUrl(),
]


Expand Down Expand Up @@ -204,4 +208,5 @@ class SubFundDetails(base.Base):
pydantic.Field(
description="URL to the sub-fund details.",
),
annotations.FcaApiUrl(),
]
5 changes: 4 additions & 1 deletion src/fca_api/types/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@

import pydantic

from . import base
from . import annotations, base


class FirmSearchResult(base.Base):
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
16 changes: 7 additions & 9 deletions tests/units/test_types_field_parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
Loading