fix: resolve all mypy type errors for strict type checking (#48)

## Summary

Fixed 30 mypy type errors across 8 files to enable strict type checking.

## Changes by Category

### no-any-return errors (fixed with explicit type casts)
- alerter/history.py: Cast Redis zcount/zremrangebyscore returns to int
- ingestor/clob_client.py: Cast API response values appropriately
- ingestor/publisher.py: Cast Redis xack, xlen, xtrim returns to int
- ingestor/metadata_sync.py: Cast Redis delete return to int
- detector/scorer.py: Cast Redis delete return to int

### operator errors (fixed with null checks)
- alerter/formatter.py: Added `if age_hours is not None:` guards

### External library typing issues (fixed with type: ignore)
- ingestor/publisher.py: redis-py typing issue with xadd dict parameter
- profiler/funding.py: web3 typing issue with block parameters
- storage/repos.py: SQLAlchemy Result.rowcount typing issue

### assignment errors (fixed by renaming)
- storage/repos.py: Renamed SQLite insert stmt to avoid type conflict

Closes #48

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Patrick Selamy
2026-01-04 19:15:32 -05:00
co-authored by Claude Opus 4.5
parent f57e0243dd
commit 5c5440d776
8 changed files with 60 additions and 45 deletions
@@ -190,10 +190,11 @@ class AlertFormatter:
wallet_age_str = "" wallet_age_str = ""
if assessment.fresh_wallet_signal: if assessment.fresh_wallet_signal:
age_hours = assessment.fresh_wallet_signal.wallet_profile.age_hours age_hours = assessment.fresh_wallet_signal.wallet_profile.age_hours
if age_hours < 1: if age_hours is not None:
wallet_age_str = f" (Age: {int(age_hours * 60)}m)" if age_hours < 1:
else: wallet_age_str = f" (Age: {int(age_hours * 60)}m)"
wallet_age_str = f" (Age: {age_hours:.0f}h)" else:
wallet_age_str = f" (Age: {age_hours:.0f}h)"
fields: list[dict[str, object]] = [ fields: list[dict[str, object]] = [
{ {
@@ -282,10 +283,11 @@ class AlertFormatter:
wallet_line = f"*Wallet:* `{wallet_short}`" wallet_line = f"*Wallet:* `{wallet_short}`"
if assessment.fresh_wallet_signal: if assessment.fresh_wallet_signal:
age_hours = assessment.fresh_wallet_signal.wallet_profile.age_hours age_hours = assessment.fresh_wallet_signal.wallet_profile.age_hours
if age_hours < 1: if age_hours is not None:
wallet_line += f" \\(Age: {int(age_hours * 60)}m\\)" if age_hours < 1:
else: wallet_line += f" \\(Age: {int(age_hours * 60)}m\\)"
wallet_line += f" \\(Age: {age_hours:.0f}h\\)" else:
wallet_line += f" \\(Age: {age_hours:.0f}h\\)"
lines.append(wallet_line) lines.append(wallet_line)
# Risk score # Risk score
@@ -366,10 +368,11 @@ class AlertFormatter:
wallet_line = f"Wallet: {wallet_short}" wallet_line = f"Wallet: {wallet_short}"
if assessment.fresh_wallet_signal: if assessment.fresh_wallet_signal:
age_hours = assessment.fresh_wallet_signal.wallet_profile.age_hours age_hours = assessment.fresh_wallet_signal.wallet_profile.age_hours
if age_hours < 1: if age_hours is not None:
wallet_line += f" (Age: {int(age_hours * 60)}m)" if age_hours < 1:
else: wallet_line += f" (Age: {int(age_hours * 60)}m)"
wallet_line += f" (Age: {age_hours:.0f}h)" else:
wallet_line += f" (Age: {age_hours:.0f}h)"
lines.append(wallet_line) lines.append(wallet_line)
# Risk # Risk
@@ -362,7 +362,7 @@ class AlertHistory:
start.timestamp(), start.timestamp(),
end.timestamp(), end.timestamp(),
) )
return count return int(count)
async def cleanup_old_alerts(self) -> int: async def cleanup_old_alerts(self) -> int:
"""Remove alerts older than retention period. """Remove alerts older than retention period.
@@ -393,4 +393,4 @@ class AlertHistory:
# Note: Individual alert records will expire via TTL # Note: Individual alert records will expire via TTL
# Wallet/market indexes will also expire via TTL # Wallet/market indexes will also expire via TTL
logger.info(f"Cleaned up {removed} old alert references") logger.info(f"Cleaned up {removed} old alert references")
return removed return int(removed)
@@ -268,7 +268,7 @@ class RiskScorer:
""" """
key = f"{self._key_prefix}{wallet_address}:{market_id}" key = f"{self._key_prefix}{wallet_address}:{market_id}"
deleted = await self._redis.delete(key) deleted = await self._redis.delete(key)
return deleted > 0 return int(deleted) > 0
async def assess_batch(self, bundles: list[SignalBundle]) -> list[RiskAssessment]: async def assess_batch(self, bundles: list[SignalBundle]) -> list[RiskAssessment]:
"""Assess multiple trade bundles. """Assess multiple trade bundles.
@@ -287,7 +287,8 @@ class ClobClient:
try: try:
response = self._client.get_midpoint(token_id) response = self._client.get_midpoint(token_id)
return response.get("mid") mid = response.get("mid")
return str(mid) if mid is not None else None
except Exception as e: except Exception as e:
logger.warning("Failed to get midpoint for %s: %s", token_id, e) logger.warning("Failed to get midpoint for %s: %s", token_id, e)
return None return None
@@ -307,7 +308,8 @@ class ClobClient:
try: try:
response = self._client.get_price(token_id, side=side) response = self._client.get_price(token_id, side=side)
return response.get("price") price = response.get("price")
return str(price) if price is not None else None
except Exception as e: except Exception as e:
logger.warning("Failed to get %s price for %s: %s", side, token_id, e) logger.warning("Failed to get %s price for %s: %s", side, token_id, e)
return None return None
@@ -321,7 +323,7 @@ class ClobClient:
try: try:
self._rate_limiter.acquire_sync() self._rate_limiter.acquire_sync()
result = self._client.get_ok() result = self._client.get_ok()
return result == "OK" return str(result) == "OK"
except Exception as e: except Exception as e:
logger.error("Health check failed: %s", e) logger.error("Health check failed: %s", e)
return False return False
@@ -334,7 +336,8 @@ class ClobClient:
""" """
try: try:
self._rate_limiter.acquire_sync() self._rate_limiter.acquire_sync()
return self._client.get_server_time() result = self._client.get_server_time()
return int(result) if result is not None else None
except Exception as e: except Exception as e:
logger.error("Failed to get server time: %s", e) logger.error("Failed to get server time: %s", e)
return None return None
@@ -351,7 +351,7 @@ class MarketMetadataSync:
""" """
key = f"{self._key_prefix}{condition_id}" key = f"{self._key_prefix}{condition_id}"
deleted = await self._redis.delete(key) deleted = await self._redis.delete(key)
return deleted > 0 return int(deleted) > 0
async def force_sync(self) -> None: async def force_sync(self) -> None:
"""Force an immediate sync of all markets. """Force an immediate sync of all markets.
@@ -186,9 +186,10 @@ class EventPublisher:
The entry ID assigned by Redis. The entry ID assigned by Redis.
""" """
data = _serialize_trade_event(event) data = _serialize_trade_event(event)
# redis-py typing expects broader dict type than dict[str, str]
entry_id = await self._redis.xadd( entry_id = await self._redis.xadd(
self._stream_name, self._stream_name,
data, data, # type: ignore[arg-type]
maxlen=self._max_len, maxlen=self._max_len,
) )
# entry_id may be bytes or str # entry_id may be bytes or str
@@ -213,7 +214,8 @@ class EventPublisher:
pipe = self._redis.pipeline() pipe = self._redis.pipeline()
for event in events: for event in events:
data = _serialize_trade_event(event) data = _serialize_trade_event(event)
pipe.xadd(self._stream_name, data, maxlen=self._max_len) # redis-py typing expects broader dict type than dict[str, str]
pipe.xadd(self._stream_name, data, maxlen=self._max_len) # type: ignore[arg-type]
results = await pipe.execute() results = await pipe.execute()
@@ -384,7 +386,8 @@ class EventPublisher:
""" """
if not entry_ids: if not entry_ids:
return 0 return 0
return await self._redis.xack(self._stream_name, group_name, *entry_ids) result = await self._redis.xack(self._stream_name, group_name, *entry_ids)
return int(result)
async def get_stream_info(self) -> dict[str, Any]: async def get_stream_info(self) -> dict[str, Any]:
"""Get information about the stream. """Get information about the stream.
@@ -404,7 +407,8 @@ class EventPublisher:
Returns: Returns:
Number of entries in the stream. Number of entries in the stream.
""" """
return await self._redis.xlen(self._stream_name) result = await self._redis.xlen(self._stream_name)
return int(result)
async def trim_stream(self, max_len: int | None = None) -> int: async def trim_stream(self, max_len: int | None = None) -> int:
"""Trim the stream to a maximum length. """Trim the stream to a maximum length.
@@ -416,4 +420,5 @@ class EventPublisher:
Number of entries removed. Number of entries removed.
""" """
length = max_len or self._max_len length = max_len or self._max_len
return await self._redis.xtrim(self._stream_name, maxlen=length) result = await self._redis.xtrim(self._stream_name, maxlen=length)
return int(result)
@@ -232,6 +232,7 @@ class FundingTracer:
) )
# Get logs with Transfer event filtering by recipient # Get logs with Transfer event filtering by recipient
# Note: web3 typing is overly restrictive for block params
logs = await w3.eth.get_logs( logs = await w3.eth.get_logs(
{ {
"address": AsyncWeb3.to_checksum_address(token_address), "address": AsyncWeb3.to_checksum_address(token_address),
@@ -240,8 +241,8 @@ class FundingTracer:
None, # from (any) None, # from (any)
padded_to, # to (target address) padded_to, # to (target address)
], ],
"fromBlock": from_block, "fromBlock": from_block, # type: ignore[typeddict-item]
"toBlock": to_block, "toBlock": to_block, # type: ignore[typeddict-item]
} }
) )
@@ -317,7 +318,7 @@ class FundingTracer:
origin_type="error", origin_type="error",
) )
else: else:
chains[addr.lower()] = result # type: ignore[assignment] chains[addr.lower()] = result
return chains return chains
+20 -17
View File
@@ -208,20 +208,20 @@ class WalletRepository:
await self.session.execute(stmt) await self.session.execute(stmt)
except Exception: except Exception:
# Fall back to SQLite upsert for testing # Fall back to SQLite upsert for testing
stmt = sqlite_insert(WalletProfileModel).values(**values, created_at=now) sqlite_stmt = sqlite_insert(WalletProfileModel).values(**values, created_at=now)
stmt = stmt.on_conflict_do_update( sqlite_stmt = sqlite_stmt.on_conflict_do_update(
index_elements=["address"], index_elements=["address"],
set_={ set_={
"nonce": stmt.excluded.nonce, "nonce": sqlite_stmt.excluded.nonce,
"first_seen_at": stmt.excluded.first_seen_at, "first_seen_at": sqlite_stmt.excluded.first_seen_at,
"is_fresh": stmt.excluded.is_fresh, "is_fresh": sqlite_stmt.excluded.is_fresh,
"matic_balance": stmt.excluded.matic_balance, "matic_balance": sqlite_stmt.excluded.matic_balance,
"usdc_balance": stmt.excluded.usdc_balance, "usdc_balance": sqlite_stmt.excluded.usdc_balance,
"analyzed_at": stmt.excluded.analyzed_at, "analyzed_at": sqlite_stmt.excluded.analyzed_at,
"updated_at": stmt.excluded.updated_at, "updated_at": sqlite_stmt.excluded.updated_at,
}, },
) )
await self.session.execute(stmt) await self.session.execute(sqlite_stmt)
await self.session.flush() await self.session.flush()
return dto return dto
@@ -238,7 +238,8 @@ class WalletRepository:
result = await self.session.execute( result = await self.session.execute(
delete(WalletProfileModel).where(WalletProfileModel.address == address.lower()) delete(WalletProfileModel).where(WalletProfileModel.address == address.lower())
) )
return result.rowcount > 0 # SQLAlchemy Result does have rowcount but typing doesn't reflect it
return (result.rowcount or 0) > 0 # type: ignore[attr-defined]
async def mark_stale(self, address: str) -> bool: async def mark_stale(self, address: str) -> bool:
"""Mark a wallet profile as stale (soft delete). """Mark a wallet profile as stale (soft delete).
@@ -257,7 +258,8 @@ class WalletRepository:
.where(WalletProfileModel.address == address.lower()) .where(WalletProfileModel.address == address.lower())
.values(analyzed_at=stale_time, updated_at=datetime.now(UTC)) .values(analyzed_at=stale_time, updated_at=datetime.now(UTC))
) )
return result.rowcount > 0 # SQLAlchemy Result does have rowcount but typing doesn't reflect it
return (result.rowcount or 0) > 0 # type: ignore[attr-defined]
class FundingRepository: class FundingRepository:
@@ -478,12 +480,12 @@ class RelationshipRepository:
await self.session.execute(stmt) await self.session.execute(stmt)
except Exception: except Exception:
# Fall back to SQLite upsert for testing # Fall back to SQLite upsert for testing
stmt = sqlite_insert(WalletRelationshipModel).values(**values) sqlite_stmt = sqlite_insert(WalletRelationshipModel).values(**values)
stmt = stmt.on_conflict_do_update( sqlite_stmt = sqlite_stmt.on_conflict_do_update(
index_elements=["wallet_a", "wallet_b", "relationship_type"], index_elements=["wallet_a", "wallet_b", "relationship_type"],
set_={"confidence": stmt.excluded.confidence}, set_={"confidence": sqlite_stmt.excluded.confidence},
) )
await self.session.execute(stmt) await self.session.execute(sqlite_stmt)
await self.session.flush() await self.session.flush()
return dto return dto
@@ -506,4 +508,5 @@ class RelationshipRepository:
WalletRelationshipModel.relationship_type == relationship_type, WalletRelationshipModel.relationship_type == relationship_type,
) )
) )
return result.rowcount > 0 # SQLAlchemy Result does have rowcount but typing doesn't reflect it
return (result.rowcount or 0) > 0 # type: ignore[attr-defined]