@@ -93,7 +93,7 @@ def pairs(items: list[tuple[str, object]]) -> dict[str, object]:
9393 return value
9494
9595
96- def _csv_snapshot (value : object , expected_header : tuple [str , ...], code : str ) -> tuple [int , tuple [str , ...]]:
96+ def _csv_snapshot (value : object , expected_header : tuple [str , ...], code : str , * , allow_empty : bool = False ) -> tuple [int , tuple [str , ...]]:
9797 if type (value ) is not bytes :
9898 raise _invalid (code )
9999 try :
@@ -103,15 +103,44 @@ def _csv_snapshot(value: object, expected_header: tuple[str, ...], code: str) ->
103103 raise _invalid (code ) from None
104104 if not rows or tuple (rows [0 ]) != expected_header :
105105 raise _invalid (f"{ code } _header" )
106- if len (rows ) < 2 or any (len (row ) != len (expected_header ) for row in rows [1 :]):
106+ if ( not allow_empty and len (rows ) < 2 ) or any (len (row ) != len (expected_header ) for row in rows [1 :]):
107107 raise _invalid (f"{ code } _rows" )
108108 return len (rows ) - 1 , expected_header
109109
110110
111+ def _filter_events (value : object , period_start : date , as_of : date ) -> bytes :
112+ if type (value ) is not bytes :
113+ raise _invalid ("events_csv_invalid" )
114+ try :
115+ rows = list (csv .reader (io .StringIO (value .decode ("utf-8" ), newline = "" )))
116+ except (UnicodeError , csv .Error , ValueError , RecursionError ):
117+ raise _invalid ("events_csv_invalid" ) from None
118+ if not rows or tuple (rows [0 ]) != EVENT_HEADER :
119+ raise _invalid ("events_csv_header" )
120+ selected : list [list [str ]] = []
121+ for row in rows [1 :]:
122+ if len (row ) != len (EVENT_HEADER ):
123+ raise _invalid ("events_csv_rows" )
124+ event_date = row [1 ]
125+ if type (event_date ) is not str or len (event_date ) != 10 :
126+ raise _invalid ("events_date_invalid" )
127+ try :
128+ parsed_date = date .fromisoformat (event_date )
129+ except ValueError :
130+ raise _invalid ("events_date_invalid" ) from None
131+ if period_start <= parsed_date <= as_of :
132+ selected .append (row )
133+ output = io .StringIO (newline = "" )
134+ writer = csv .writer (output , lineterminator = "\n " )
135+ writer .writerow (EVENT_HEADER )
136+ writer .writerows (selected )
137+ return output .getvalue ().encode ("utf-8" )
138+
139+
111140def _feed_status (value : object ) -> WeeklyFeedStatus :
112141 if not isinstance (value , Mapping ):
113142 raise _invalid ("feed_status_invalid" )
114- required = ("feed_count" , "successful_feed_count" , "failed_feed_count" )
143+ required = ("feed_count" , "successful_feed_count" , "failed_feed_count" , "complete" )
115144 if any (key not in value for key in required ):
116145 raise _invalid ("feed_status_invalid" )
117146 try :
@@ -121,7 +150,7 @@ def _feed_status(value: object) -> WeeklyFeedStatus:
121150 value ["failed_feed_count" ],
122151 value .get ("stale_feed_count" , 0 ),
123152 value .get ("missing_feed_count" , 0 ),
124- True ,
153+ value [ "complete" ] ,
125154 )
126155 except (WeeklyContractError , TypeError , ValueError ):
127156 raise _invalid ("feed_status_incomplete" ) from None
@@ -173,7 +202,7 @@ def _file_metadata(name: str, value: bytes, *, role: str, row_count: int | None
173202
174203
175204def _manifest_payload (lock : PoliticalEventWeeklyPeriodLockV1 , contract : WeeklySourceContract , files : Mapping [str , bytes ]) -> dict [str , object ]:
176- event_rows , event_header = _csv_snapshot (files [EVENTS_NAME ], EVENT_HEADER , "events_csv_invalid" )
205+ event_rows , event_header = _csv_snapshot (files [EVENTS_NAME ], EVENT_HEADER , "events_csv_invalid" , allow_empty = True )
177206 watch_rows , watch_header = _csv_snapshot (files [WATCHLIST_NAME ], WATCHLIST_HEADER , "watchlist_csv_invalid" )
178207 return {
179208 "manifest_version" : MANIFEST_VERSION ,
@@ -221,14 +250,16 @@ def build_weekly_artifact(
221250 generated_at = _timestamp (generated_at )
222251 if generated_at < datetime .combine (period_end , datetime .min .time (), timezone .utc ):
223252 raise _invalid ("generated_at_invalid" )
224- event_rows , _ = _csv_snapshot (source_events , EVENT_HEADER , "events_csv_invalid" )
253+ raw_snapshot_digest = _snapshot_digest (source_events , watchlist )
254+ period_events = _filter_events (source_events , period_start , as_of )
255+ event_rows , _ = _csv_snapshot (period_events , EVENT_HEADER , "events_csv_invalid" , allow_empty = True )
225256 watch_rows , _ = _csv_snapshot (watchlist , WATCHLIST_HEADER , "watchlist_csv_invalid" )
226257 status = _feed_status (feed_status )
227258 if type (source_provenance ) is not str or source_provenance != "official_rss_source_pipeline_v1" :
228259 raise _invalid ("source_provenance_invalid" )
229260 if type (run_mode ) is not str or run_mode not in {"scheduled" , "manual" }:
230261 raise _invalid ("run_mode_invalid" )
231- source_snapshot_digest = _snapshot_digest ( source_events , watchlist )
262+ source_snapshot_digest = raw_snapshot_digest
232263 source_snapshot_id = f"rss_source_snapshot_{ as_of :%Y%m%d} _{ source_run_id } "
233264 try :
234265 lock = PoliticalEventWeeklyPeriodLockV1 (
@@ -243,7 +274,7 @@ def build_weekly_artifact(
243274 source_snapshot_digest ,
244275 source_provenance ,
245276 (
246- SourceSnapshotArtifact (EVENTS_NAME , _sha256 (source_events ), event_rows ),
277+ SourceSnapshotArtifact (EVENTS_NAME , _sha256 (period_events ), event_rows ),
247278 SourceSnapshotArtifact (WATCHLIST_NAME , _sha256 (watchlist ), watch_rows ),
248279 ),
249280 )
@@ -256,7 +287,7 @@ def build_weekly_artifact(
256287 producer_ref ,
257288 source_provenance ,
258289 (
259- WeeklySourceArtifact (EVENTS_NAME , _sha256 (source_events ), event_rows ),
290+ WeeklySourceArtifact (EVENTS_NAME , _sha256 (period_events ), event_rows ),
260291 WeeklySourceArtifact (WATCHLIST_NAME , _sha256 (watchlist ), watch_rows ),
261292 ),
262293 status ,
@@ -267,7 +298,7 @@ def build_weekly_artifact(
267298 raise _invalid ("weekly_artifact_invalid" ) from None
268299 files : dict [str , bytes ] = {
269300 PERIOD_LOCK_NAME : period_lock ,
270- EVENTS_NAME : source_events ,
301+ EVENTS_NAME : period_events ,
271302 WATCHLIST_NAME : watchlist ,
272303 WEEKLY_NAME : weekly ,
273304 }
@@ -293,10 +324,11 @@ def parse_weekly_artifact(files: Mapping[str, bytes]) -> dict[str, bytes]:
293324 raise _invalid ("weekly_contract_invalid" ) from None
294325 if serialize_weekly_contract (contract ) != files [WEEKLY_NAME ]:
295326 raise _invalid ("weekly_noncanonical" )
296- event_rows , _ = _csv_snapshot (files [EVENTS_NAME ], EVENT_HEADER , "events_csv_invalid" )
327+ expected_events = _filter_events (files [EVENTS_NAME ], contract .period_start , contract .as_of )
328+ if expected_events != files [EVENTS_NAME ]:
329+ raise _invalid ("events_period_mismatch" )
330+ event_rows , _ = _csv_snapshot (files [EVENTS_NAME ], EVENT_HEADER , "events_csv_invalid" , allow_empty = True )
297331 watch_rows , _ = _csv_snapshot (files [WATCHLIST_NAME ], WATCHLIST_HEADER , "watchlist_csv_invalid" )
298- if _snapshot_digest (files [EVENTS_NAME ], files [WATCHLIST_NAME ]) != lock .source_snapshot_digest :
299- raise _invalid ("source_snapshot_digest_mismatch" )
300332 expected_artifacts = (
301333 (EVENTS_NAME , _sha256 (files [EVENTS_NAME ]), event_rows ),
302334 (WATCHLIST_NAME , _sha256 (files [WATCHLIST_NAME ]), watch_rows ),
@@ -313,7 +345,6 @@ def parse_weekly_artifact(files: Mapping[str, bytes]) -> dict[str, bytes]:
313345 or lock .producer_ref != contract .producer_ref
314346 or lock .source_provenance != contract .source_provenance
315347 or lock .source_snapshot_id != expected_snapshot_id
316- or lock .source_snapshot_digest != _snapshot_digest (files [EVENTS_NAME ], files [WATCHLIST_NAME ])
317348 or lock .source_attempt != 1
318349 ):
319350 raise _invalid ("period_contract_mismatch" )
0 commit comments