diff --git a/src/polymarket/models/data/activity.py b/src/polymarket/models/data/activity.py index b0f6a11..c954e26 100644 --- a/src/polymarket/models/data/activity.py +++ b/src/polymarket/models/data/activity.py @@ -8,7 +8,11 @@ from polymarket.errors import UnexpectedResponseError from polymarket.models.base import BaseModel -from polymarket.models.gamma.common import parse_epoch_seconds_optional, parse_optional_decimal +from polymarket.models.gamma.common import ( + empty_string_to_none, + parse_epoch_seconds_optional, + parse_optional_decimal, +) from polymarket.models.types import ( CtfConditionId, TokenId, @@ -58,6 +62,11 @@ def _parse_decimal(cls, value: object) -> Decimal | None: def _parse_timestamp(cls, value: object) -> datetime | None: return parse_epoch_seconds_optional(value) + @field_validator("icon", mode="before") + @classmethod + def _normalize_icon(cls, value: object) -> object | None: + return empty_string_to_none(value) + ActivityType = Literal[ "TRADE", diff --git a/src/polymarket/models/data/portfolio.py b/src/polymarket/models/data/portfolio.py index 8b79164..42001ff 100644 --- a/src/polymarket/models/data/portfolio.py +++ b/src/polymarket/models/data/portfolio.py @@ -8,6 +8,7 @@ from polymarket.models.base import BaseModel from polymarket.models.gamma.common import ( + empty_string_to_none, parse_epoch_seconds_optional, parse_optional_date, parse_optional_decimal, @@ -100,6 +101,11 @@ def _parse_decimal(cls, value: object) -> Decimal | None: def _parse_end_date(cls, value: object) -> date | None: return parse_optional_date(value) + @field_validator("icon", mode="before") + @classmethod + def _normalize_icon(cls, value: object) -> object | None: + return empty_string_to_none(value) + def _repr_html_(self) -> str: from polymarket._jupyter import card, safe_html_repr, truncate_mid @@ -170,6 +176,11 @@ def _parse_timestamp(cls, value: object) -> datetime | None: def _parse_end_date(cls, value: object) -> date | None: return parse_optional_date(value) + @field_validator("icon", mode="before") + @classmethod + def _normalize_icon(cls, value: object) -> object | None: + return empty_string_to_none(value) + class ComboPositionMarketEvent(BaseModel): event_id: str | None = None diff --git a/tests/unit/test_data_activity.py b/tests/unit/test_data_activity.py index b6df610..10ba115 100644 --- a/tests/unit/test_data_activity.py +++ b/tests/unit/test_data_activity.py @@ -15,7 +15,7 @@ YieldActivity, ) from polymarket.errors import UnexpectedResponseError -from polymarket.models.data.activity import parse_activities, parse_activity +from polymarket.models.data.activity import Trade, parse_activities, parse_activity _CONDITION_ID = "0x5c19f205507ce03ff5f3be08a8090a5969ea6870cc07b902a4ca2e61dfe48fdd" @@ -137,6 +137,16 @@ def test_market_event_empty_icon_normalizes_to_none() -> None: assert activity.icon is None +def test_trade_model_empty_icon_normalizes_to_none() -> None: + trade = Trade.parse_response({"icon": ""}) + assert trade.icon is None + + +def test_trade_model_keeps_populated_icon() -> None: + trade = Trade.parse_response({"icon": "https://example.test/icon.png"}) + assert trade.icon == "https://example.test/icon.png" + + def test_market_event_variants_parse() -> None: for activity_type, expected_class in [ ("SPLIT", SplitActivity), diff --git a/tests/unit/test_data_models.py b/tests/unit/test_data_models.py index b0b1ada..6aef84d 100644 --- a/tests/unit/test_data_models.py +++ b/tests/unit/test_data_models.py @@ -7,12 +7,14 @@ from polymarket.errors import UnexpectedResponseError from polymarket.models.data import ( BuilderVolumeEntry, + ClosedPosition, ComboPosition, Holder, LiveVolume, MetaHolder, OpenInterest, PortfolioValue, + Position, TradedMarketCount, ) @@ -159,6 +161,26 @@ def test_traded_market_count_parses_payload() -> None: assert count.traded == 42 +def test_position_empty_icon_normalizes_to_none() -> None: + position = Position.parse_response({"conditionId": _CTF_CONDITION_ID, "icon": ""}) + + assert position.icon is None + + +def test_position_keeps_populated_icon() -> None: + position = Position.parse_response( + {"conditionId": _CTF_CONDITION_ID, "icon": "https://example.test/icon.png"} + ) + + assert position.icon == "https://example.test/icon.png" + + +def test_closed_position_empty_icon_normalizes_to_none() -> None: + position = ClosedPosition.parse_response({"conditionId": _CTF_CONDITION_ID, "icon": ""}) + + assert position.icon is None + + def test_builder_volume_entry_renames_dt_to_bucket_at() -> None: entry = BuilderVolumeEntry.parse_response( {