11#!/usr/bin/env python
22import json
3+ import os
4+ import tempfile
35from datetime import datetime , timedelta , timezone
46from typing import Any , Dict , List
57
@@ -156,11 +158,53 @@ def _batch_delete_pointers(
156158 return pointers_deleted , sorted (failed_deletes_set )
157159
158160
161+ def _write_result_file (result : Dict [str , Any ], output_file : str ) -> None :
162+ """
163+ Atomically write result dict to output_file as JSON.
164+ Raises on failure.
165+ """
166+ out_dir = os .path .dirname (os .path .abspath (output_file )) or "."
167+ with tempfile .NamedTemporaryFile (
168+ "w" , delete = False , dir = out_dir , prefix = ".tmp_delete_results_" , suffix = ".json"
169+ ) as tf :
170+ json .dump (result , tf , indent = 2 )
171+ tf .flush ()
172+ os .fsync (tf .fileno ())
173+ os .replace (tf .name , output_file )
174+
175+
176+ def _print_summary (result : Dict [str , Any ]) -> None :
177+ """
178+ Print a concise summary of the result to stdout.
179+ """
180+
181+ def count_from (field ):
182+ val = result .get (field )
183+ if isinstance (val , dict ):
184+ return val .get ("count" , 0 )
185+ if isinstance (val , list ):
186+ return len (val )
187+ return 0
188+
189+ print ("Summary:" )
190+ print (f" pointers_to_delete: { result .get ('pointers_to_delete' )} " )
191+ print (f" ods_code_matched: { count_from ('ods_code_matched' )} " )
192+ print (f" ods_code_mismatched:{ count_from ('ods_code_mismatched' )} " )
193+ print (f" pointer_not_found: { count_from ('pointer_not_found' )} " )
194+ print (f" deleted_pointers: { count_from ('deleted_pointers' )} " )
195+ print (f" failed_deletes: { count_from ('failed_deletes' )} " )
196+ if "_output_error" in result :
197+ print (f" output_error: { result ['_output_error' ]} " )
198+ if "deletes-took-secs" in result :
199+ print (f" deletes-took-secs: { result .get ('deletes-took-secs' )} " )
200+
201+
159202def _delete_pointers_by_id (
160203 table_name : str ,
161204 ods_code : str ,
162205 pointers_to_delete : List [str ] | None = None ,
163206 pointers_file : str | None = None ,
207+ output_file : str | None = None ,
164208) -> Dict [str , Any ]:
165209 """
166210 Delete pointers from DynamoDB table.
@@ -175,6 +219,7 @@ def _delete_pointers_by_id(
175219 - ods_code: ODS code of the organisation that the pointers belong to
176220 - pointers_to_delete: list of pointer ids to delete
177221 - pointers_file: path to JSON file (array of objects with "id" field) or text file (one id per line)
222+ - output_file: optional path to write the result JSON to (atomic replace)
178223 """
179224 if pointers_to_delete is None and pointers_file is None :
180225 raise ValueError ("Provide either pointers_to_delete or pointers_file" )
@@ -187,16 +232,25 @@ def _delete_pointers_by_id(
187232 pointers_to_delete = _load_pointers_from_file (pointers_file )
188233
189234 if len (pointers_to_delete ) == 0 :
190- return {
235+ result = {
191236 "pointers_to_delete" : 0 ,
192237 "ods_code" : ods_code ,
193- "ods_code_matched" : [] ,
194- "ods_code_mismatched" : [] ,
195- "pointer_not_found" : [] ,
196- "deleted_pointers" : [] ,
197- "failed_deletes" : [] ,
238+ "ods_code_matched" : { "count" : 0 , "ids" : []} ,
239+ "ods_code_mismatched" : { "count" : 0 , "ids" : []} ,
240+ "pointer_not_found" : { "count" : 0 , "ids" : []} ,
241+ "deleted_pointers" : { "count" : 0 , "ids" : []} ,
242+ "failed_deletes" : { "count" : 0 , "ids" : []} ,
198243 "deletes-took-secs" : 0 ,
199244 }
245+ if output_file :
246+ try :
247+ _write_result_file (result , output_file )
248+ except Exception as exc :
249+ result ["_output_error" ] = (
250+ f"Failed to write output_file { output_file } : { exc } "
251+ )
252+ _print_summary (result )
253+ return result
200254
201255 start_time = datetime .now (tz = timezone .utc )
202256
@@ -214,7 +268,7 @@ def _delete_pointers_by_id(
214268 if len (matched_pointers ) == 0 :
215269 print (f"None of the pointer IDs are a match for ODS code { ods_code } . Exiting." )
216270 end_time = datetime .now (tz = timezone .utc )
217- return {
271+ result = {
218272 "pointers_to_delete" : len (pointers_to_delete ),
219273 "ods_code" : ods_code ,
220274 "ods_code_matched" : {
@@ -225,20 +279,20 @@ def _delete_pointers_by_id(
225279 "count" : len (mismatched_pointers ),
226280 "ids" : mismatched_pointers ,
227281 },
228- "pointer_not_found" : {
229- "count" : 0 ,
230- "ids" : [],
231- },
232- "deleted_pointers" : {
233- "count" : 0 ,
234- "ids" : [],
235- },
236- "failed_deletes" : {
237- "count" : 0 ,
238- "ids" : [],
239- },
282+ "pointer_not_found" : {"count" : 0 , "ids" : []},
283+ "deleted_pointers" : {"count" : 0 , "ids" : []},
284+ "failed_deletes" : {"count" : 0 , "ids" : []},
240285 "deletes-took-secs" : timedelta .total_seconds (end_time - start_time ),
241286 }
287+ if output_file :
288+ try :
289+ _write_result_file (result , output_file )
290+ except Exception as exc :
291+ result ["_output_error" ] = (
292+ f"Failed to write output_file { output_file } : { exc } "
293+ )
294+ _print_summary (result )
295+ return result
242296
243297 print (f"Checking existence of { len (matched_pointers )} pointers in { table_name } ..." )
244298 existing_pointers , not_found_pointers = _batch_get_existing_pointers (
@@ -252,7 +306,7 @@ def _delete_pointers_by_id(
252306 if len (existing_pointers ) == 0 :
253307 print ("No pointers found to delete. Exiting." )
254308 end_time = datetime .now (tz = timezone .utc )
255- return {
309+ result = {
256310 "pointers_to_delete" : len (pointers_to_delete ),
257311 "ods_code" : ods_code ,
258312 "ods_code_matched" : {
@@ -267,16 +321,19 @@ def _delete_pointers_by_id(
267321 "count" : len (not_found_pointers ),
268322 "ids" : not_found_pointers ,
269323 },
270- "deleted_pointers" : {
271- "count" : 0 ,
272- "ids" : [],
273- },
274- "failed_deletes" : {
275- "count" : 0 ,
276- "ids" : [],
277- },
324+ "deleted_pointers" : {"count" : 0 , "ids" : []},
325+ "failed_deletes" : {"count" : 0 , "ids" : []},
278326 "deletes-took-secs" : timedelta .total_seconds (end_time - start_time ),
279327 }
328+ if output_file :
329+ try :
330+ _write_result_file (result , output_file )
331+ except Exception as exc :
332+ result ["_output_error" ] = (
333+ f"Failed to write output_file { output_file } : { exc } "
334+ )
335+ _print_summary (result )
336+ return result
280337
281338 # Proceed with deletion using BatchWriteItem
282339 pointers_deleted , failed_deletes = _batch_delete_pointers (
@@ -285,14 +342,10 @@ def _delete_pointers_by_id(
285342
286343 end_time = datetime .now (tz = timezone .utc )
287344
288- print (" Done" )
289- return {
345+ result = {
290346 "pointers_to_delete" : len (pointers_to_delete ),
291347 "ods_code" : ods_code ,
292- "ods_code_matched" : {
293- "count" : len (matched_pointers ),
294- "ids" : matched_pointers ,
295- },
348+ "ods_code_matched" : {"count" : len (matched_pointers ), "ids" : matched_pointers },
296349 "ods_code_mismatched" : {
297350 "count" : len (mismatched_pointers ),
298351 "ids" : mismatched_pointers ,
@@ -301,17 +354,23 @@ def _delete_pointers_by_id(
301354 "count" : len (not_found_pointers ),
302355 "ids" : not_found_pointers ,
303356 },
304- "deleted_pointers" : {
305- "count" : len (pointers_deleted ),
306- "ids" : pointers_deleted ,
307- },
308- "failed_deletes" : {
309- "count" : len (failed_deletes ),
310- "ids" : failed_deletes ,
311- },
357+ "deleted_pointers" : {"count" : len (pointers_deleted ), "ids" : pointers_deleted },
358+ "failed_deletes" : {"count" : len (failed_deletes ), "ids" : failed_deletes },
312359 "deletes-took-secs" : timedelta .total_seconds (end_time - start_time ),
313360 }
314361
362+ if output_file :
363+ try :
364+ _write_result_file (result , output_file )
365+ except Exception as exc :
366+ result ["_output_error" ] = (
367+ f"Failed to write output_file { output_file } : { exc } "
368+ )
369+
370+ _print_summary (result )
371+ print (" Done" )
372+ return result
373+
315374
316375if __name__ == "__main__" :
317376 fire .Fire (_delete_pointers_by_id )
0 commit comments