99import logging
1010from dataclasses import dataclass
1111from datetime import UTC , date , datetime , timedelta
12- from typing import TYPE_CHECKING , Any , Final , Literal , cast
12+ from typing import TYPE_CHECKING , Final , Literal , cast
1313from urllib .parse import urlencode
1414
1515import httpx
@@ -54,24 +54,30 @@ class DeathVerdict:
5454 current_tvl : float | None = None
5555
5656
57- def _merge_chain_series (chain_tvls : dict [str , dict [str , Any ]]) -> list [tuple [date , float ]]: # pyright: ignore[reportExplicitAny]
57+ def _str_or_empty (value : object ) -> str :
58+ return value if isinstance (value , str ) else ""
59+
60+
61+ def _merge_chain_series (chain_tvls : dict [str , object ]) -> list [tuple [date , float ]]:
5862 """Sum daily totalLiquidityUSD across chains; dedupe intra-day duplicates last-write-wins.
5963
6064 DefiLlama ships multiple "today" snapshots; naive summing double-counts current TVL.
6165 """
6266 totals : dict [date , float ] = {}
6367 for chain_payload in chain_tvls .values ():
64- if not isinstance (chain_payload , dict ): # pyright: ignore[reportUnnecessaryIsInstance] — runtime defence; Any values are not statically narrowable
68+ if not isinstance (chain_payload , dict ):
6569 continue
66- series_obj : object = chain_payload .get ("tvl" )
70+ chain_dict = cast ("dict[str, object]" , chain_payload )
71+ series_obj : object = chain_dict .get ("tvl" )
6772 if not isinstance (series_obj , list ):
6873 continue
6974 per_day : dict [date , float ] = {}
70- for point in series_obj : # pyright: ignore[reportUnknownVariableType] — list[Any] from JSON
71- if not isinstance (point , dict ):
75+ for raw_point in cast ( " list[object]" , series_obj ):
76+ if not isinstance (raw_point , dict ):
7277 continue
73- ts : object = point .get ("date" ) # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType] — Any from JSON
74- tvl : object = point .get ("totalLiquidityUSD" ) # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType] — Any from JSON
78+ point = cast ("dict[str, object]" , raw_point )
79+ ts : object = point .get ("date" )
80+ tvl : object = point .get ("totalLiquidityUSD" )
7581 if not isinstance (ts , (int , float )) or not isinstance (tvl , (int , float )):
7682 continue
7783 d = datetime .fromtimestamp (float (ts ), tz = UTC ).date ()
@@ -82,7 +88,7 @@ def _merge_chain_series(chain_tvls: dict[str, dict[str, Any]]) -> list[tuple[dat
8288
8389
8490def classify_death (
85- chain_tvls : dict [str , dict [ str , Any ]], # pyright: ignore[reportExplicitAny]
91+ chain_tvls : dict [str , object ],
8692 * ,
8793 threshold_pct : float = DEFAULT_DEAD_THRESHOLD_PCT ,
8894 peak_floor_usd : float = DEFAULT_PEAK_FLOOR_USD ,
@@ -173,12 +179,15 @@ async def wayback_snapshot_near(
173179 return None
174180
175181
176- def _seed_markdown (detail : dict [str , Any ], verdict : DeathVerdict ) -> str : # pyright: ignore[reportExplicitAny]
182+ def _seed_markdown (
183+ * ,
184+ name : str ,
185+ category : str ,
186+ chain : str ,
187+ description : str ,
188+ verdict : DeathVerdict ,
189+ ) -> str :
177190 """Minimal pitch-shaped seed text. TavilyEnricher fills the real body from Wayback."""
178- name : object = detail .get ("name" ) or ""
179- category : object = detail .get ("category" ) or ""
180- chain : object = detail .get ("chain" ) or ""
181- description : object = detail .get ("description" ) or ""
182191 parts = [
183192 f"# { name } " ,
184193 "" ,
@@ -188,7 +197,7 @@ def _seed_markdown(detail: dict[str, Any], verdict: DeathVerdict) -> str: # pyr
188197 f"peak_date: { verdict .peak_date } " ,
189198 f"tvl_current_usd: { verdict .current_tvl } " ,
190199 "" ,
191- str ( description ) ,
200+ description ,
192201 ]
193202 return "\n " .join (parts ).strip ()
194203
@@ -234,18 +243,17 @@ async def _fetch_json(self, url: str) -> object | None:
234243 logger .warning ("defillama: non-JSON response for %s: %r" , url , exc )
235244 return None
236245
237- async def _classify_candidate (
238- self , slug : str , live_url : str
239- ) -> tuple [dict [str , Any ], DeathVerdict , str ] | None : # pyright: ignore[reportExplicitAny]
246+ async def _classify_candidate (self , slug : str , live_url : str ) -> tuple [str , str ] | None :
247+ """Fetch detail, classify, anchor to Wayback. Return (markdown_body, snapshot_url)."""
240248 detail_payload = await self ._fetch_json (f"{ DETAIL_ENDPOINT_BASE } /{ slug } " )
241249 if not isinstance (detail_payload , dict ):
242250 return None
243- detail = cast ("dict[str, Any ]" , detail_payload ) # pyright: ignore[reportExplicitAny]
251+ detail = cast ("dict[str, object ]" , detail_payload )
244252
245- chain_tvls_raw : object = detail .get ("chainTvls" ) or {}
253+ chain_tvls_raw : object = detail .get ("chainTvls" )
246254 if not isinstance (chain_tvls_raw , dict ):
247255 return None
248- chain_tvls = cast ("dict[str, dict[str, Any]] " , chain_tvls_raw ) # pyright: ignore[reportExplicitAny]
256+ chain_tvls = cast ("dict[str, object] " , chain_tvls_raw )
249257
250258 verdict = classify_death (
251259 chain_tvls ,
@@ -264,9 +272,16 @@ async def _classify_candidate(
264272 logger .info ("defillama: %s no wayback coverage near %s" , slug , verdict .peak_date )
265273 return None
266274
267- return detail , verdict , snapshot_url
275+ body = _seed_markdown (
276+ name = _str_or_empty (detail .get ("name" )),
277+ category = _str_or_empty (detail .get ("category" )),
278+ chain = _str_or_empty (detail .get ("chain" )),
279+ description = _str_or_empty (detail .get ("description" )),
280+ verdict = verdict ,
281+ )
282+ return body , snapshot_url
268283
269- async def _process_candidate (self , row : dict [str , Any ]) -> RawEntry | None : # pyright: ignore[reportExplicitAny]
284+ async def _process_candidate (self , row : dict [str , object ]) -> RawEntry | None :
270285 slug : object = row .get ("slug" )
271286 live_url : object = row .get ("url" )
272287 if not isinstance (slug , str ) or not slug :
@@ -278,14 +293,14 @@ async def _process_candidate(self, row: dict[str, Any]) -> RawEntry | None: # p
278293 result = await self ._classify_candidate (slug , live_url )
279294 if result is None :
280295 return None
281- detail , verdict , snapshot_url = result
296+ markdown_text , snapshot_url = result
282297
283298 return RawEntry (
284299 source = SOURCE_DEFILLAMA ,
285300 source_id = slug ,
286301 url = snapshot_url ,
287302 raw_html = None ,
288- markdown_text = _seed_markdown ( detail , verdict ) ,
303+ markdown_text = markdown_text ,
289304 fetched_at = datetime .now (UTC ),
290305 )
291306
@@ -303,7 +318,7 @@ async def fetch(self) -> AsyncIterator[RawEntry]:
303318 return
304319 if not isinstance (raw , dict ):
305320 continue
306- row = cast ("dict[str, Any ]" , raw ) # pyright: ignore[reportExplicitAny]
321+ row = cast ("dict[str, object ]" , raw )
307322 tvl_field : object = row .get ("tvl" )
308323 if not isinstance (tvl_field , (int , float )):
309324 continue
0 commit comments