diff --git a/applications/sharepoint_renamer/handler.py b/applications/sharepoint_renamer/handler.py index dfaa510ce..f55f54741 100644 --- a/applications/sharepoint_renamer/handler.py +++ b/applications/sharepoint_renamer/handler.py @@ -8,7 +8,9 @@ from orchestration.sharepoint_renamer_orchestrator import SharepointRenamerOrche from utils.sharepoint.domna_sharepoint_client import DomnaSharepointClient from utils.sharepoint.domna_sites import DomnaSites -CSV_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "sero_address_list.csv") +CSV_PATH = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "sero_address_list.csv" +) def handler(event: dict[str, Any], context: Any) -> None: diff --git a/applications/sharepoint_renamer/sero_address_list.csv b/applications/sharepoint_renamer/sero_address_list.csv index 51cdf2eff..604387747 100644 --- a/applications/sharepoint_renamer/sero_address_list.csv +++ b/applications/sharepoint_renamer/sero_address_list.csv @@ -1,182 +1,366 @@ -UPRN,Address,Postcode -U1027392,"26 Silverdale Road, Oprington",BR5 2LT -U1003906,"54 Barnsdale Crescent, Oprington",BR5 2AX -U1034479,"90 Mead Way, Bromley",BR2 9EU -U1005549,"79 Lower Gravel Road, Bromley",BR2 8LP -U1016743,"16 Princes Plain, Bromley",BR2 8LE -U1041937,"75 Turpington Lane, Bromley",BR2 8JD -U1034805,"38 Narrow Way, Bromley",BR2 8JB -U1041933,"31 Turpington Lane, Bromley",BR2 8JA -U1037833,"3 Stiles Close, Bromley",BR2 8EQ -U1042734,"86 Whitebeam Avenue, Bromley",BR2 8DW -U1042575,"90 Whitebeam Avenue, Bromley",BR2 8DW -U1033177,"30 Larch Way, Bromley",BR2 8DU -U1027989,"5 Larch Way, Bromley",BR2 8DT -U1012309,"13 Almond Close, Bromley",BR2 8DS -U1022525,"30 Lovelace Avenue, Bromley",BR2 8DQ -U1047613,"13 Whitebeam Avenue, Bromley",BR2 8DJ -U1027549,"14 Thorn Close, Bromley",BR2 8DH -U1022726,"31 Hornbeam Way, Bromley",BR2 8DB -U1021308,"70 Faringdon Avenue, Bromley",BR2 8BU -U1026958,"77 Faringdon Avenue, Bromley",BR2 8BT -U1007553,"115d Faringdon Avenue, Bromley",BR2 8BT -U1032132,"115f Faringdon Avenue, Bromley",BR2 8BT -U1014627,"81 Faringdon Avenue, Bromley",BR2 8BT -U1009607,"20 Parkfield Way, Bromley",BR2 8AF -U1027838,"15 Holmcroft Way, Bromley",BR2 8AD -U1052376,"3 Shoreham Way, Bromley",BR2 7PU -U1015499,"25 Boughton Avenue, Bromley",BR2 7PL -U1005741,"2 Malling Way, Bromley",BR2 7PJ -U1019906,"26 Eastry Avenue, Bromley",BR2 7PF -U1021769,"49 Eastry Avenue, Bromley",BR2 7PE -U1031121,"32 Laburnum Way, Bromley",BR2 8BZ -U1022281,"35 Kingsdown Way, Bromley",BR2 7PT -U1035115,"30 Thorn Close, Bromley",BR2 8DH -U1027493,"35 Sudbury Crescent, Bromley",BR1 4PY -U1016746,"68 Princes Plain, Bromley",BR2 8LE -U1005168,"38 Holbrook Way, Bromley",BR2 8EE -U1036446,"14 Meath Close, Oprington",BR5 2HF -U1010452,"5 Canbury Path, Oprington",BR5 2EU -U1019785,"50 Cray Valley Road, Oprington",BR5 2EZ -U1024065,"64 Marion Crescent, Oprington",BR5 2HD -U1042248,"16 Stanley Way, Oprington",BR5 2HE -U1029229,"2 Meath Close, Oprington",BR5 2HF -U1037768,"13 Silverdale Road, Oprington",BR5 2LU -U1014589,"71 Empress Drive, Chislehurst",BR7 5BQ -U1024698,"4 Palace View, Bromley",BR1 3EL -U1052536,"12 Thorn Close, Bromley",BR2 8DH -U1022018,"12 Hazel Walk, Bromley",BR2 8DF -U1007728,"2 Hazel Walk, Bromley",BR2 8DF -U1002456,"54 Birch Row, Bromley",BR2 8DA -U1020349,"21 Laburnum Way, Bromley",BR2 8BY -U1032129,"78 Faringdon Avenue, Bromley",BR2 8BU -U1032130,"86 Faringdon Avenue, Bromley",BR2 8BU -U1021824,"115g Faringdon Avenue, Bromley",BR2 8BT -U1021827,"121 Faringdon Avenue, Bromley",BR2 8BT -U1026960,"105 Faringdon Avenue, Bromley",BR2 8BT -U1010578,"6 Cranbrook Close, Bromley",BR2 7QA -U1024709,"3 Parkfield Way, Bromley",BR2 8AE -U1024580,"24 Parkfield Way, Bromley",BR2 8AF -U1011190,"11 Shoreham Way, Bromley",BR2 7PU -U1011191,"32 Shoreham Way, Bromley",BR2 7PU -U1021535,"4 Chilham Way, Bromley",BR2 7PR -U1007556,"11 Farleigh Avenue, Bromley",BR2 7PP -U1010255,"7 Boughton Avenue, Bromley",BR2 7PL -U1034810,"24 Newbury Road, Bromley",BR2 0QW -U1009032,"10 Malling Way, Bromley",BR2 7PJ -U1004686,"55 Baston Road, Bromley",BR2 7BD -U1032061,"30 Eastry Avenue, Bromley",BR2 7PF -U1010582,"13 Cranworth Cottages, Keston",BR2 6DB -U1009511,"27 Newbury Road, Bromley",BR2 0QN -U1037954,"28 Treewall Gardens, Bromley",BR1 5BT -U1014793,"59 Headcorn Road, Bromley",BR1 4SQ -U1005123,"24 Bonville Road, Bromley",BR1 4QA -U1037872,"20 Sudbury Crescent, Bromley",BR1 4PZ -U1022305,"18a Lansdowne Road, Bromley",BR1 3LZ -U1035052,"1 Sudbury Crescent, Bromley",BR1 4PY -U1042298,"39 Sudbury Crescent, Bromley",BR1 4PY -U1019564,"32 Brook Lane, Bromley",BR1 4PU -U1024511,"81 Nightingale Lane, Bromley",BR1 2SA -U1032133,"119 Faringdon Avenue, Bromley",BR2 8BT -U1032134,"125 Faringdon Avenue, Bromley",BR2 8BT -U1032131,"93 Faringdon Avenue, Bromley",BR2 8BT -U1021825,"117a Faringdon Avenue, Bromley",BR2 8BT -U1010107,"42 Birch Row, Bromley",BR2 8DA -U1016880,"25 Almond Way, Bromley",BR2 8DR -U1038107,"152 Whitebeam Avenue, Bromley",BR2 8DW -U1052455,"10 Stiles Close, Bromley",BR2 8EQ -U1028328,"76 Magpie Hall Lane, Bromley",BR2 8ER -U1020064,"28 Green Way, Bromley",BR2 8EY -U1041934,"51 Turpington Lane, Bromley",BR2 8JA -U1005696,"158 Magpie Hall Lane, Bromley",BR2 8JG -U1030892,"140 Poverest Road, Oprington",BR5 1RH -U1011072,"45 Rookery Gardens, Oprington",BR5 4BA -U1031058,"303 Keedonwood Road, Bromley",BR1 4QR -U1052429,"76 Southover, Bromley",BR1 4RY -U1042153,"4 Scotts Road, Bromley",BR1 3QD -U1037814,"42 Stanley Road, Bromley",BR2 9JH -U1008158,"71 Lower Gravel Road, Bromley",BR2 8LP -U1032062,"46 Eastry Avenue, Bromley",BR2 7PF -U1016742,"4 Princes Plain, Bromley",BR2 8LE -U1038106,"84 Whitebeam Avenue, Bromley",BR2 8DW -U1013831,"33 Ash Row, Bromley",BR2 8DZ -U1005742,"18 Malling Way, Bromley",BR2 7PJ -U1042572,"37 Whitebeam Avenue, Bromley",BR2 8DJ -U1005167,"30 Holbrook Way, Bromley",BR2 8EE -U1024581,"30 Parkfield Way, Bromley",BR2 8AF -U1024050,"10 Marden Avenue, Bromley",BR2 7PX -U1020328,"31 Kingsdown Way, Bromley",BR2 7PT -U1020327,"5 Kingsdown Way, Bromley",BR2 7PT -U1005131,"10 Boughton Avenue, Bromley",BR2 7PL -U1023415,"27 Malling Way, Bromley",BR2 7PJ -U1052580,"25 Trentham Drive, Oprington",BR5 2EP -U1026447,"50 Princes Plain, Bromley",BR2 8LE -U1007432,"21 Detling Road, Bromley",BR1 4SH -U1053388,"52 Whitebeam Avenue, Bromley",BR2 8DL -U1022307,"20 Larch Way, Bromley",BR2 8DU -U1027743,"17 Whitebeam Avenue, Bromley",BR2 8DJ -U1022533,"81 Lower Gravel Road, Bromley",BR2 8LP -U1014630,"118 Faringdon Avenue, Bromley",BR2 8BU -U1030897,"60 Princes Plain, Bromley",BR2 8LE -U1022931,"15 Lennard Road, Bromley",BR2 8LN -U1042735,"108 Whitebeam Avenue, Bromley",BR2 8DW -U1013830,"31 Ash Row, Bromley",BR2 8DZ -U1020368,"11 Larch Way, Bromley",BR2 8DT -U1020369,"13 Larch Way, Bromley",BR2 8DT -U1042576,"112 Whitebeam Avenue, Bromley",BR2 8DW -U1027830,"26 Holbrook Way, Bromley",BR2 8EE -U1016744,"18 Princes Plain, Bromley",BR2 8LE -U1041772,"13 Stanley Road, Bromley",BR2 9JE -U1042573,"47 Whitebeam Avenue, Bromley",BR2 8DJ -U1008151,"61 Lovelace Avenue, Bromley",BR2 8EA -U1034985,"272 Southborough Lane, Bromley",BR2 8AS -U1007947,"8 Laburnum Way, Bromley",BR2 8BZ -U1027744,"33 Whitebeam Avenue, Bromley",BR2 8DJ -U1038102,"14 Whitebeam Avenue, Bromley",BR2 8DL -U1027745,"148 Whitebeam Avenue, Bromley",BR2 8DW -U1020208,"16 Holbrook Way, Bromley",BR2 8EE -U1023463,"30 MANOR WAY, Bromley",BR2 8ES -U1032647,"32 Narrow Way, Bromley",BR2 8JB -U1033406,"67 Lower Gravel Road, Bromley",BR2 8LP -U1000649,"42 Barnsdale Crescent, Oprington",BR5 2AX -U1036332,"50 Marion Crescent, Oprington",BR5 2HD -U1038103,"29 Whitebeam Avenue, Bromley",BR2 8DJ -U1042574,"66 Whitebeam Avenue, Bromley",BR2 8DL -U1042256,"2 Stiles Close, Bromley",BR2 8EQ -U1034017,"48 Princes Plain, Bromley",BR2 8LE -U1041435,"64 Princes Plain, Bromley",BR2 8LE -U1053386,"27 Whitebeam Avenue, Bromley",BR2 8DJ -U1038104,"32 Whitebeam Avenue, Bromley",BR2 8DL -U1019567,"8 Broom Close, Bromley",BR2 8EU -U1023539,"2 Marsham Close, Chislehurst",BR7 6JD -U1037465,"6 Princes Plain, Bromley",BR2 8LE -U1042733,"68 Whitebeam Avenue, Bromley",BR2 8DL -U1004442,"7 Birch Row, Bromley",BR2 8BX -U1005006,"44 Birch Row, Bromley",BR2 8DA -U1035331,"104 Whitebeam Avenue, Bromley",BR2 8DW -U1024017,"13 Manor Way, Bromley",BR2 8ES -U1014078,"43 Aylesbury Road, Bromley",BR2 0QR -U1052186,"4 Ravensleigh Gardens, Bromley",BR1 5SN -U1037450,"70 Pontefract Road, Bromley",BR1 4RB -U1031294,"70 London Lane, Bromley",BR1 4HE -U1014077,"32 Aylesbury Road, Bromley",BR2 0QP -U1030962,"7 Ravensleigh Gardens, Bromley",BR1 5SN -U1042232,"154 Southover, Bromley",BR1 4RZ -U1024484,"55 Mounthurst Road, Bromley",BR2 7PG -U1007701,"46 Harwood Avenue, Bromley",BR1 3DU -U1020199,"78 Hillside Road, Bromley",BR2 0ST -U1036758,"46 Newbury Road, Bromley",BR2 0QW -U1009369,"17 Minster Road, Bromley",BR1 4DY -U1009194,"84 Mays Hill Road, Bromley",BR2 0HT -U1013358,"3 Bird In Hand Lane, Bromley",BR1 2NA -U1009202,"63 Mead Way, Bromley",BR2 9ER -U1022991,"5 Link Way, Bromley",BR2 8JH -U1025820,"46 Princes Plain, Bromley",BR2 8LE -U1020237,"33 Hornbeam Way, Bromley",BR2 8DB -U1021310,"126 Faringdon Avenue, Bromley",BR2 8BU -U1021353,"66 George Lane, Bromley",BR2 7LQ -U1033165,"2 Laburnum Way, Bromley",BR2 8BZ -U1010811,"13 Gilbert Road, Bromley",BR1 3QP -U1027449,"11 Station Road, Bromley",BR1 3LP -U1035326,"2 Whitebeam Avenue, Bromley",BR2 8DL -U1020351,"27 Laburnum Way, Bromley",BR2 8BY +UPRN,Address,Postcode +U1041316,"U1041316_58 Ravensworth Road, Mottingham",SE9 4LW +U2022716,"U2022716_306 St Helier Avenue, Morden",SM4 6JX +U2018001,"U2018001_37 Leominster Road, Morden",SM4 6HN +U2019131,"U2019131_34 Middleton Road, Morden",SM4 6RT +U1035331,"104 Whitebeam Avenue, Bromley",BR2 8DW +U1010255,"7 Boughton Avenue, Bromley",BR2 7PL +U1024581,"30 Parkfield Way, Bromley",BR2 8AF +U1032130,"86 Faringdon Avenue, Bromley",BR2 8BU +U1014077,"32 Aylesbury Road, Bromley",BR2 0QP +U1007728,"2 Hazel Walk, Bromley",BR2 8DF +U1020327,"5 Kingsdown Way, Bromley",BR2 7PT +U1032134,"125 Faringdon Avenue, Bromley",BR2 8BT +U1053411,"U1053411_78 Windsor Drive, Orpington",BR6 6HF +U2022615,"U2022615_56 St Helier Avenue, Morden",SM4 6LF +U1022991,"5 Link Way, Bromley",BR2 8JH +U1010811,"13 Gilbert Road, Bromley",BR1 3QP +U1020328,"31 Kingsdown Way, Bromley",BR2 7PT +U1005131,"10 Boughton Avenue, Bromley",BR2 7PL +U1021353,"66 George Lane, Bromley",BR2 7LQ +U1034017,"48 Princes Plain, Bromley",BR2 8LE +U1036758,"46 Newbury Road, Bromley",BR2 0QW +U1020351,"27 Laburnum Way, Bromley",BR2 8BY +U1035052,"1 Sudbury Crescent, Bromley",BR1 4PY +U1027073,"U1027073_29 Haddon Road, St Mary Cray",BR5 4BU +U1034887,"U1034887_2 Saltwood Close, Orpington",BR6 9BU +U1035329,"U1035329_79 Whitebeam Avenue, Bromley",BR2 8DN +U1042571,"U1042571_19 Whitebeam Avenue, Bromley",BR2 8DJ +U1035330,"U1035330_87 Whitebeam Avenue, Bromley",BR2 8DN +U1047521,"U1047521_19 Walnuts Road, Orpington",BR6 0RG +U1033037,"U1033037_73 Homefield Rise, Orpington",BR6 0RL +U1027845,"U1027845_92 Homefield Rise, Orpington",BR6 0RW +U1031691,"U1031691_46 Boyland Road, Bromley",BR1 4QF +U1026916,"U1026916_19 Elizabeth Way, St Mary Cray",BR5 4BJ +U1015363,"U1015363_44a Birch Row, Bromley",BR2 8DA +U1010400,"U1010400_42 Brook Lane, Bromley",BR1 4PU +U1052426,"U1052426_49 Southover, Bromley",BR1 4SL +U1019591,"U1019591_18 Buckland Road, Orpington",BR6 9SR +U1011434,"U1011434_24 Upper Elmers End Road, Beckenham",BR3 4AL +U1023417,"U1023417_44 Malling Way, Hayes, Bromley",BR2 7PJ +U1014671,"U1014671_61 George Lane, Hayes, Bromley",BR2 7LG +U1037550,"U1037550_228 Repton Road, Orpington",BR6 9JB +U1014433,"U1014433_57 Cowden Road, Orpington",BR6 0TP +U1015779,"U1015779_21 Beck Lane, Beckenham",BR3 4RD +U1042236,"U1042236_250 Southover, Bromley",BR1 4SE +U1033036,"U1033036_61 Homefield Rise, Orpington",BR6 0RL +U1027951,"U1027951_2 Kingsdown Way, Hayes, Bromley",BR2 7PT +U1032770,"U1032770_18 Pembury Close, Hayes, Bromley",BR2 7PS +U1011129,"U1011129_40 Saltwood Close, Orpington",BR6 9BU +U1023267,"U1023267_47a Mackenzie Road, Beckenham",BR3 4RT +U1016998,"U1016998_68 Arundel Drive, Orpington",BR6 9JG +U1026576,"U1026576_1 Burwash Court, St Mary Cray",BR5 4BG +U1037867,"U1037867_45 Stowe Road, Orpington",BR6 9HG +U1027839,"U1027839_30 Holmcroft Way, Bromley",BR2 8AD +U1005351,"U1005351_2 Leaveland Close, Beckenham",BR3 3PL +U1041968,"U1041968_59 Uplands Road, Orpington",BR6 0RH +U1032149,"U1032149_12 Fordwich Close, Orpington",BR6 0TT +U1047347,"U1047347_30 Sudbury Crescent, Bromley",BR1 4PZ +U1004249,"U1004249_6 Beck Lane, Beckenham",BR3 4RE +U1005772,"U1005772_10 Manor Park Road, West Wickham",BR4 0JZ +U1010810,"U1010810_79 George Lane, Hayes, Bromley",BR2 7LG +U1011008,"U1011008_170 Repton Road, Orpington",BR6 9JA +U1026969,"U1026969_2 Fordwich Close, Orpington",BR6 0TT +U1032000,"U1032000_16 Detling Road, Bromley",BR1 4SH +U1013268,"U1013268_9 Bell Gardens, St Mary Cray",BR5 4DA +U1020217,"U1020217_71 Homefield Rise, Orpington",BR6 0RL +U1033287,"U1033287_12 Lincoln Green Road, St Mary Cray",BR5 2DX +U1041997,"U1041997_31 Walnuts Road, Orpington",BR6 0RG +U1047520,"U1047520_2 Walnuts Road, Orpington",BR6 0RQ +U1019606,"U1019606_20 Burrfield Drive, St Mary Cray",BR5 4BZ +U1007354,"U1007354_30 Cowden Road, Orpington",BR6 0TR +U1015603,"U1015603_1 Barwood Avenue, West Wickham",BR4 0LB +U1035393,"U1035393_18 Yalding Grove, St Mary Cray",BR5 3SE +U1000177,"U1000177_79 Arundel Drive, Orpington",BR6 9JQ +U1011428,"U1011428_21 Uplands Road, Orpington",BR6 0RH +U1024671,"U1024671_115 Oregon Square, Orpington",BR6 8BE +U1021513,"U1021513_18 Chamberlain Crescent, West Wickham",BR4 0LL +U1024663,"U1024663_32b Okemore Gardens, Orpington",BR5 3PJ +U1020879,"U1020879_8 Brockwell Close, St Mary Cray",BR5 1RJ +U1022713,"U1022713_53 Homefield Rise, Orpington",BR6 0RL +U1031157,"U1031157_94 Leesons Hill, St Pauls Cray",BR5 2NH +U1013666,"U1013666_11 Ampleforth Close, Orpington",BR6 9ES +U1052617,"U1052617_44 Uplands Road, Orpington",BR6 0RJ +U1010783,"U1010783_32 Foxbury Drive, Chelsfield, Orpington",BR6 6EL +U1021700,"U1021700_25 Daleside, Orpington",BR6 6EG +U1011163,"U1011163_35 Selwyn Place, St Pauls Cray",BR5 3BA +U1022175,"U1022175_54 Homefield Rise, Orpington",BR6 0RW +U1021461,"U1021461_27 Burrfield Drive, St Mary Cray",BR5 4BZ +U1007184,"U1007184_7 Buckland Road, Orpington",BR6 9SR +U1023112,"U1023112_97 Lower Gravel Road, Bromley",BR2 8LP +U1024516,"U1024516_18 Northolme Rise, Off Tubbenden Lane, Orpington",BR6 9RF +U1023910,"U1023910_40 Moorfield Road, Orpington",BR6 0HQ +U1031342,"U1031342_55 Lower Gravel Road, Bromley",BR2 8LP +U1037905,"U1037905_37 Sweeps Lane, St Mary Cray",BR5 3PE +U2019260,"U2019260_88 Montacute Road, Morden",SM4 6RL +U2019214,"U2019214_11 Missenden Gardens, Morden",SM4 6HW +U2022891,"U2022891_20 Swains Road, Tooting, London",SW17 9HS +U2022684,"U2022684_231 St Helier Avenue, Morden",SM4 6JH +U2023196,"U2023196_210 Tudor Drive, Morden",SM4 4PQ +U2019671,"U2019671_16 Netley Gardens, Morden",SM4 6JW +U2018944,"U2018944_79 Malmesbury Road, Morden",SM4 6HG +U2015873,"U2015873_1 Furness Road, Morden",SM4 6PS +U1041503,"U1041503_103 Ravensworth Road, Mottingham",SE9 4LX +U2023185,"U2023185_157 Tudor Drive, Morden",SM4 4PJ +U1053395,"U1053395_90 Widecombe Road, Mottingham",SE9 4HJ +U2018009,"U2018009_23 Leominster Walk, Morden",SM4 6HB +U2018941,"U2018941_74 Malmesbury Road, Morden",SM4 6HE +U2013950,"U2013950_2 Buckland Walk, Morden",SM4 5RZ +U2018160,"U2018160_6 Llanthony Road, Morden",SM4 6DX +U2015790,"U2015790_12 Flaxley Road, Morden",SM4 6LJ +U2013914,"U2013914_36 Bruton Road, Morden",SM4 5RY +U2022611,"U2022611_50 St Helier Avenue, Morden",SM4 6LF +U2015270,"U2015270_39 Dorchester Road, Morden",SM4 6QE +U1009098,"U1009098_37b Maple Road, Penge",SE20 8LA +U2018926,"U2018926_19 Malmesbury Road, Morden",SM4 6HD +U2023192,"U2023192_195 Tudor Drive, Morden",SM4 4PJ +U1027362,"U1027362_187 Seymour Villas, Penge",SE20 8TP +U2018973,"U2018973_26 Marham Gardens, Morden",SM4 6JJ +U2013913,"U2013913_30 Bruton Road, Morden",SM4 5RY +U1042214,"U1042214_25 Snowdown Close, London",SE20 7RU +U2013911,"U2013911_23 Bruton Road, Morden",SM4 5RY +U2013583,"U2013583_300 Bishopsford Road, Morden",SM4 6BY +U2018925,"U2018925_14 Malmesbury Road, Morden",SM4 6HD +U1037938,"U1037938_5 Torr Road, Penge",SE20 7PS +U1038117,"U1038117_40 Widecombe Road, Mottingham",SE9 4HQ +U1052365,"U1052365_127 Seymour Villas, Penge",SE20 8TP +U1024726,"U1024726_2 Penge Lane, Penge",SE20 7DU +U1026505,"U1026505_7 Bridlington Close, Biggin Hill",TN16 3PD +U2018913,"U2018913_23 Malling Gardens, Morden",SM4 6JG +U2015708,"U2015708_46 Faversham Road, Morden",SM4 6RE +U2019530,"U2019530_30 Neath Gardens, Morden",SM4 6JN +U2015710,"U2015710_49 Faversham Road, Morden",SM4 6RE +U2014478,"U2014478_222 Central Road, Morden",SM4 5PP +U2018922,"U2018922_11 Malmesbury Road, Morden",SM4 6HD +U2013615,"U2013615_47 Blanchland Road, Morden",SM4 5ND +U2022641,"U2022641_127 St Helier Avenue, Morden",SM4 6JD +U2019268,"U2019268_97 Montacute Road, Morden",SM4 6RJ +U2013947,"U2013947_95 Buckfast Road, Morden",SM4 5NA +U2018025,"U2018025_21 Lessness Road, Morden",SM4 6HP +U2013415,"U2013415_51 Bodnant Gardens, Raynes Park, London",SW20 0UD +U2022728,"U2022728_340 St Helier Avenue, Morden",SM4 6JU +U1009623,"U1009623_23 Pawleyne Close, Penge",SE20 8JH +U2015884,"U2015884_30 Furness Road, Morden",SM4 6PR +U2019728,"U2019728_17 Newhouse Walk, Morden",SM4 6BS +U1024719,"U1024719_14 Pawleyne Close, Penge",SE20 8JH +U2013935,"U2013935_43 Buckfast Road, Morden",SM4 5NA +U2013623,"U2013623_4 Bodmin Grove, Morden",SM4 5LU +U2018811,"U2018811_354 Lynmouth Avenue, Morden",SM4 4RT +U2019672,"U2019672_19 Netley Gardens, Morden",SM4 6JW +U2018927,"U2018927_29 Malmesbury Road, Morden",SM4 6HD +U2019237,"U2019237_37 Montacute Road, Morden",SM4 6RJ +U2019730,"U2019730_23 Newhouse Walk, Morden",SM4 6BS +U1038118,"U1038118_46 Widecombe Road, Mottingham",SE9 4HQ +U1000359,"U1000359_5 Avenue Road, Penge",SE20 7RT +U2018164,"U2018164_24 Llanthony Road, Morden",SM4 6DX +U2013982,"U2013982_18 Bury Grove, Morden",SM4 5NG +U1030964,"U1030964_13 Ravensworth Road, Mottingham",SE9 4LN +U2015882,"U2015882_25 Furness Road, Morden",SM4 6PS +U1031105,"U1031105_21 Kingsmead, Biggin Hill, Westerham",TN16 3UB +U1014008,"U1014008_9 Avenue Road, Penge",SE20 7RT +U2015779,"U2015779_57 Flanders Crescent, Tooting, London",SW17 9JB +U2013413,"U2013413_49 Bodnant Gardens, Raynes Park, London",SW20 0UD +U1021199,"U1021199_7 Dittisham Road, Mottingham",SE9 4BJ +U1041705,"U1041705_87 Seymour Villas, Penge",SE20 8TR +U1041706,"U1041706_91 Seymour Villas, Penge",SE20 8TR +U1042213,"U1042213_9 Snowdown Close, London",SE20 7RU +U2014444,"U2014444_86 Central Road, Morden",SM4 5RJ +U2013940,"U2013940_63 Buckfast Road, Morden",SM4 5NA +U1037789,"U1037789_5 Snowdown Close, London",SE20 7RU +U2013630,"U2013630_16 Bodmin Grove, Morden",SM4 5LU +U2014445,"U2014445_88 Central Road, Morden",SM4 5RJ +U2013602,"U2013602_5 Blanchland Road, Morden",SM4 5ND +U1017178,"U1017178_11 Avenue Road, Penge",SE20 7RT +U1013690,"U1013690_74 Anerley Vale, London",SE19 2BG +U2013676,"U2013676_52 Bordesley Road, Morden",SM4 5LR +U1042212,"U1042212_1 Snowdown Close, London",SE20 7RU +U2018810,"U2018810_331 Lynmouth Avenue, Morden",SM4 4RY +U2013476,"U2013476_1 Bristol Road, Morden",SM4 5SA +U1026660,"U1026660_40 Chilham Road, Mottingham",SE9 4BG +U1036877,"U1036877_2 Pawleyne Close, Penge",SE20 8JH +U2018814,"U2018814_364 Lynmouth Avenue, Morden",SM4 4RT +U2013573,"U2013573_264 Bishopsford Road, Morden",SM4 6BZ +U1007236,"U1007236_6 Castledine Road, Penge",SE20 8PL +U1027701,"U1027701_4 Wayside Grove, Mottingham",SE9 4ND +U1047208,"U1047208_83 Seymour Villas, Penge",SE20 8TR +U2013695,"U2013695_111 Bordesley Road, Morden",SM4 5LW +U1004961,"U1004961_36 Beverley Road, Penge",SE20 8SJ +U2015795,"U2015795_24 Flaxley Road, Morden",SM4 6LJ +U2014451,"U2014451_102 Central Road, Morden",SM4 5RJ +U2016606,"U2016606_3 Grayswood Gardens, Raynes Park, London",SW20 0UF +U1035025,"U1035025_29 Steyning Grove, Mottingham",SE9 4NG +U1027588,"U1027588_94 Tovil Close, Penge",SE20 8SZ +U2022632,"U2022632_112 St Helier Avenue, Morden",SM4 6LB +U1037462,"U1037462_68 Prestbury Square, Mottingham",SE9 4NA +U2018027,"U2018027_29 Lessness Road, Morden",SM4 6HP +U2018909,"U2018909_12 Malling Gardens, Morden",SM4 6JG +U1001247,"U1001247_13 Pawleyne Close, Penge",SE20 8JH +U1018547,"U1018547_9b Avenue Road, Penge",SE20 7RT +U1008780,"U1008780_7b Avenue Road, Penge",SE20 7RT +U1007212,"U1007212_2 Calcott Walk, Mottingham",SE9 4BN +U1003650,"U1003650_5b Avenue Road, Penge",SE20 7RT +U1010359,"U1010359_2 Bredhurst Close, Penge",SE20 7BG +U1041772,"13 Stanley Road, Bromley",BR2 9JE +U1011072,"45 Rookery Gardens, Oprington",BR5 4BA +U1020208,"16 Holbrook Way, Bromley",BR2 8EE +U1052580,"25 Trentham Drive, Oprington",BR5 2EP +U1038106,"84 Whitebeam Avenue, Bromley",BR2 8DW +U1010107,"42 Birch Row, Bromley",BR2 8DA +U1022307,"20 Larch Way, Bromley",BR2 8DU +U1020064,"28 Green Way, Bromley",BR2 8EY +U1023463,"30 MANOR WAY, Bromley",BR2 8ES +U1027745,"148 Whitebeam Avenue, Bromley",BR2 8DW +U1008151,"61 Lovelace Avenue, Bromley",BR2 8EA +U1019785,"50 Cray Valley Road, Oprington",BR5 2EZ +U1029229,"2 Meath Close, Oprington",BR5 2HF +U1005696,"158 Magpie Hall Lane, Bromley",BR2 8JG +U1036332,"50 Marion Crescent, Oprington",BR5 2HD +U1027830,"26 Holbrook Way, Bromley",BR2 8EE +U1052455,"10 Stiles Close, Bromley",BR2 8EQ +U1023539,"2 Marsham Close, Chislehurst",BR7 6JD +U1030962,"7 Ravensleigh Gardens, Bromley",BR1 5SN +U1037954,"28 Treewall Gardens, Bromley",BR1 5BT +U1020368,"11 Larch Way, Bromley",BR2 8DT +U1027743,"17 Whitebeam Avenue, Bromley",BR2 8DJ +U1021825,"117a Faringdon Avenue, Bromley",BR2 8BT +U1028328,"76 Magpie Hall Lane, Bromley",BR2 8ER +U1025820,"46 Princes Plain, Bromley",BR2 8LE +U1024580,"24 Parkfield Way, Bromley",BR2 8AF +U1010452,"5 Canbury Path, Oprington",BR5 2EU +U1022018,"12 Hazel Walk, Bromley",BR2 8DF +U1037814,"42 Stanley Road, Bromley",BR2 9JH +U1016744,"18 Princes Plain, Bromley",BR2 8LE +U1021310,"126 Faringdon Avenue, Bromley",BR2 8BU +U1035115,"30 Thorn Close, Bromley",BR2 8DH +U1033165,"2 Laburnum Way, Bromley",BR2 8BZ +U1000649,"42 Barnsdale Crescent, Oprington",BR5 2AX +U1052536,"12 Thorn Close, Bromley",BR2 8DH +U1042298,"39 Sudbury Crescent, Bromley",BR1 4PY +U1053388,"52 Whitebeam Avenue, Bromley",BR2 8DL +U1007556,"11 Farleigh Avenue, Bromley",BR2 7PP +U1007947,"8 Laburnum Way, Bromley",BR2 8BZ +U1019567,"8 Broom Close, Bromley",BR2 8EU +U1013358,"3 Bird In Hand Lane, Bromley",BR1 2NA +U1022533,"81 Lower Gravel Road, Bromley",BR2 8LP +U1021824,"115g Faringdon Avenue, Bromley",BR2 8BT +U1009369,"17 Minster Road, Bromley",BR1 4DY +U1022931,"15 Lennard Road, Bromley",BR2 8LN +U1024065,"64 Marion Crescent, Oprington",BR5 2HD +U1005168,"38 Holbrook Way, Bromley",BR2 8EE +U1038104,"32 Whitebeam Avenue, Bromley",BR2 8DL +U1021827,"121 Faringdon Avenue, Bromley",BR2 8BT +U1030892,"140 Poverest Road, Oprington",BR5 1RH +U1013831,"33 Ash Row, Bromley",BR2 8DZ +U1004442,"7 Birch Row, Bromley",BR2 8BX +U1019564,"32 Brook Lane, Bromley",BR1 4PU +U1032131,"93 Faringdon Avenue, Bromley",BR2 8BT +U1026960,"105 Faringdon Avenue, Bromley",BR2 8BT +U1005006,"44 Birch Row, Bromley",BR2 8DA +U1005742,"18 Malling Way, Bromley",BR2 7PJ +U1022281,"35 Kingsdown Way, Bromley",BR2 7PT +U1042574,"66 Whitebeam Avenue, Bromley",BR2 8DL +U1011191,"32 Shoreham Way, Bromley",BR2 7PU +U1032129,"78 Faringdon Avenue, Bromley",BR2 8BU +U1009032,"10 Malling Way, Bromley",BR2 7PJ +U1031121,"32 Laburnum Way, Bromley",BR2 8BZ +U1022305,"18a Lansdowne Road, Bromley",BR1 3LZ +U1042248,"16 Stanley Way, Oprington",BR5 2HE +U1038102,"14 Whitebeam Avenue, Bromley",BR2 8DL +U1007432,"21 Detling Road, Bromley",BR1 4SH +U1041934,"51 Turpington Lane, Bromley",BR2 8JA +U1032647,"32 Narrow Way, Bromley",BR2 8JB +U1052429,"76 Southover, Bromley",BR1 4RY +U1007701,"46 Harwood Avenue, Bromley",BR1 3DU +U1042572,"37 Whitebeam Avenue, Bromley",BR2 8DJ +U1042576,"112 Whitebeam Avenue, Bromley",BR2 8DW +U1020349,"21 Laburnum Way, Bromley",BR2 8BY +U1021535,"4 Chilham Way, Bromley",BR2 7PR +U1004686,"55 Baston Road, Bromley",BR2 7BD +U1027744,"33 Whitebeam Avenue, Bromley",BR2 8DJ +U1016880,"25 Almond Way, Bromley",BR2 8DR +U1023415,"27 Malling Way, Bromley",BR2 7PJ +U1011190,"11 Shoreham Way, Bromley",BR2 7PU +U1042256,"2 Stiles Close, Bromley",BR2 8EQ +U1053386,"27 Whitebeam Avenue, Bromley",BR2 8DJ +U1020237,"33 Hornbeam Way, Bromley",BR2 8DB +U1005167,"30 Holbrook Way, Bromley",BR2 8EE +U1014630,"118 Faringdon Avenue, Bromley",BR2 8BU +U1034810,"24 Newbury Road, Bromley",BR2 0QW +U1037768,"13 Silverdale Road, Oprington",BR5 2LU +U1024709,"3 Parkfield Way, Bromley",BR2 8AE +U1010582,"13 Cranworth Cottages, Keston",BR2 6DB +U1010578,"6 Cranbrook Close, Bromley",BR2 7QA +U1031058,"303 Keedonwood Road, Bromley",BR1 4QR +U1026447,"50 Princes Plain, Bromley",BR2 8LE +U1016746,"68 Princes Plain, Bromley",BR2 8LE +U1038107,"152 Whitebeam Avenue, Bromley",BR2 8DW +U1036446,"14 Meath Close, Oprington",BR5 2HF +U1014589,"71 Empress Drive, Chislehurst",BR7 5BQ +U1002456,"54 Birch Row, Bromley",BR2 8DA +U1020369,"13 Larch Way, Bromley",BR2 8DT +U1034985,"272 Southborough Lane, Bromley",BR2 8AS +U1041435,"64 Princes Plain, Bromley",BR2 8LE +U1031294,"70 London Lane, Bromley",BR1 4HE +U1033406,"67 Lower Gravel Road, Bromley",BR2 8LP +U1032061,"30 Eastry Avenue, Bromley",BR2 7PF +U1042573,"47 Whitebeam Avenue, Bromley",BR2 8DJ +U1032133,"119 Faringdon Avenue, Bromley",BR2 8BT +U1013830,"31 Ash Row, Bromley",BR2 8DZ +U1009511,"27 Newbury Road, Bromley",BR2 0QN +U1042735,"108 Whitebeam Avenue, Bromley",BR2 8DW +U1024511,"81 Nightingale Lane, Bromley",BR1 2SA +U1024050,"10 Marden Avenue, Bromley",BR2 7PX +U1035326,"2 Whitebeam Avenue, Bromley",BR2 8DL +U1009202,"63 Mead Way, Bromley",BR2 9ER +U1020199,"78 Hillside Road, Bromley",BR2 0ST +U1042232,"154 Southover, Bromley",BR1 4RZ +U1030897,"60 Princes Plain, Bromley",BR2 8LE +U1027989,"5 Larch Way, Bromley",BR2 8DT +U1021308,"70 Faringdon Avenue, Bromley",BR2 8BU +U1019906,"26 Eastry Avenue, Bromley",BR2 7PF +U1022726,"31 Hornbeam Way, Bromley",BR2 8DB +U1024698,"4 Palace View, Bromley",BR1 3EL +U1005549,"79 Lower Gravel Road, Bromley",BR2 8LP +U1052376,"3 Shoreham Way, Bromley",BR2 7PU +U1041937,"75 Turpington Lane, Bromley",BR2 8JD +U1005123,"24 Bonville Road, Bromley",BR1 4QA +U1008158,"71 Lower Gravel Road, Bromley",BR2 8LP +U1042575,"90 Whitebeam Avenue, Bromley",BR2 8DW +U1014793,"59 Headcorn Road, Bromley",BR1 4SQ +U1012309,"13 Almond Close, Bromley",BR2 8DS +U1009194,"84 Mays Hill Road, Bromley",BR2 0HT +U1027838,"15 Holmcroft Way, Bromley",BR2 8AD +U1027449,"11 Station Road, Bromley",BR1 3LP +U1014627,"81 Faringdon Avenue, Bromley",BR2 8BT +U1027493,"35 Sudbury Crescent, Bromley",BR1 4PY +U1032132,"115f Faringdon Avenue, Bromley",BR2 8BT +U1037833,"3 Stiles Close, Bromley",BR2 8EQ +U1016743,"16 Princes Plain, Bromley",BR2 8LE +U1007553,"115d Faringdon Avenue, Bromley",BR2 8BT +U1009607,"20 Parkfield Way, Bromley",BR2 8AF +U1034805,"38 Narrow Way, Bromley",BR2 8JB +U1032062,"46 Eastry Avenue, Bromley",BR2 7PF +U1047613,"13 Whitebeam Avenue, Bromley",BR2 8DJ +U1024484,"55 Mounthurst Road, Bromley",BR2 7PG +U1038103,"29 Whitebeam Avenue, Bromley",BR2 8DJ +U1022525,"30 Lovelace Avenue, Bromley",BR2 8DQ +U1042153,"4 Scotts Road, Bromley",BR1 3QD +U1037465,"6 Princes Plain, Bromley",BR2 8LE +U1041933,"31 Turpington Lane, Bromley",BR2 8JA +U1021769,"49 Eastry Avenue, Bromley",BR2 7PE +U1042734,"86 Whitebeam Avenue, Bromley",BR2 8DW +U1026958,"77 Faringdon Avenue, Bromley",BR2 8BT +U1037872,"20 Sudbury Crescent, Bromley",BR1 4PZ +U1027549,"14 Thorn Close, Bromley",BR2 8DH +U1037450,"70 Pontefract Road, Bromley",BR1 4RB +U1016742,"4 Princes Plain, Bromley",BR2 8LE +U1042733,"68 Whitebeam Avenue, Bromley",BR2 8DL +U1003906,"54 Barnsdale Crescent, Oprington",BR5 2AX +U1027392,"26 Silverdale Road, Oprington",BR5 2LT +U1052186,"4 Ravensleigh Gardens, Bromley",BR1 5SN +U1034479,"90 Mead Way, Bromley",BR2 9EU +U1005741,"2 Malling Way, Bromley",BR2 7PJ +U1024017,"13 Manor Way, Bromley",BR2 8ES +U1014078,"43 Aylesbury Road, Bromley",BR2 0QR +U1015499,"25 Boughton Avenue, Bromley",BR2 7PL +U1033177,"30 Larch Way, Bromley",BR2 8DU diff --git a/applications/sharepoint_renamer/sero_address_list_test.csv b/applications/sharepoint_renamer/sero_address_list_test.csv deleted file mode 100644 index 72b280477..000000000 --- a/applications/sharepoint_renamer/sero_address_list_test.csv +++ /dev/null @@ -1,2 +0,0 @@ -UPRN,Address,Postcode -U1014630,"118 Faringdon Avenue, Bromley",BR2 8BU \ No newline at end of file diff --git a/orchestration/sharepoint_renamer_orchestrator.py b/orchestration/sharepoint_renamer_orchestrator.py index 42b0e803c..3c7d3e8d3 100644 --- a/orchestration/sharepoint_renamer_orchestrator.py +++ b/orchestration/sharepoint_renamer_orchestrator.py @@ -11,6 +11,7 @@ BASE_PATH = ( "Sero Project Documents/Property Folders" ) ASSESSMENT_SUBFOLDER = "A. Assessment" +BATCH_FOLDER_PREFIX = "_Sero Batch" logger = setup_logger() @@ -65,7 +66,18 @@ class SharepointRenamerOrchestrator: self._csv_path = csv_path self._dry_run = dry_run + def _discover_roots(self) -> list[str]: + contents = self._sp_client.get_folders_in_path(BASE_PATH) + batch_roots = sorted( + f"{BASE_PATH}/{item['name']}" + for item in contents.get("value", []) + if "folder" in item + and item["name"].lower().startswith(BATCH_FOLDER_PREFIX.lower()) + ) + return [BASE_PATH, *batch_roots] + def run(self) -> None: + roots = self._discover_roots() with open(self._csv_path, newline="", encoding="utf-8-sig") as f: reader = csv.DictReader(f) required = {"UPRN", "Address", "Postcode"} @@ -75,23 +87,37 @@ class SharepointRenamerOrchestrator: ) for row in reader: - uprn = row["UPRN"].strip() - address = row["Address"].strip() - postcode = row["Postcode"].strip() - folder_path = ( - f"{BASE_PATH}/{address}, {postcode}" - f"/{SharepointSubfolders.ASSESSMENT.value}/{ASSESSMENT_SUBFOLDER}" + self._process_row( + roots, + uprn=row["UPRN"].strip(), + address=row["Address"].strip(), + postcode=row["Postcode"].strip(), ) - self._process_folder(folder_path, uprn, address, postcode) + + def _process_row( + self, roots: list[str], uprn: str, address: str, postcode: str + ) -> None: + # Batch folders name properties "{uprn}_{address}"; filenames must not + # repeat the UPRN, so the folder lookup and the canonical name diverge. + display_address = address.removeprefix(f"{uprn}_") + for root in roots: + folder_path = ( + f"{root}/{address}, {postcode}" + f"/{SharepointSubfolders.ASSESSMENT.value}/{ASSESSMENT_SUBFOLDER}" + ) + if self._process_folder(folder_path, uprn, display_address, postcode): + return + logger.warning( + f"Missing folder for UPRN {uprn} in any root: {address}, {postcode}" + ) def _process_folder( self, folder_path: str, uprn: str, address: str, postcode: str - ) -> None: + ) -> bool: try: contents = self._sp_client.get_folders_in_path(folder_path) except ValueError: - logger.warning(f"Missing folder for UPRN {uprn}: {folder_path}") - return + return False for item in contents.get("value", []): if "folder" in item: @@ -123,3 +149,4 @@ class SharepointRenamerOrchestrator: logger.error( f'Failed to rename "{original_name}" → "{new_name}" (UPRN: {uprn}): {e}' ) + return True diff --git a/tests/scripts/test_rename_sharepoint_files.py b/tests/scripts/test_rename_sharepoint_files.py index 5affea7e2..c732383c7 100644 --- a/tests/scripts/test_rename_sharepoint_files.py +++ b/tests/scripts/test_rename_sharepoint_files.py @@ -1,14 +1,32 @@ +from pathlib import Path from typing import Any from unittest.mock import MagicMock import pytest +from backend.pashub_fetcher.sharepoint_subfolders import SharepointSubfolders from orchestration.sharepoint_renamer_orchestrator import ( + ASSESSMENT_SUBFOLDER, + BASE_PATH, SharepointRenamerOrchestrator, build_canonical_filename, ) +def _assessment_path(root: str, address: str, postcode: str) -> str: + return ( + f"{root}/{address}, {postcode}" + f"/{SharepointSubfolders.ASSESSMENT.value}/{ASSESSMENT_SUBFOLDER}" + ) + + +def _write_csv(tmp_path: Path, rows: list[tuple[str, str, str]]) -> str: + lines = ["UPRN,Address,Postcode"] + [",".join(row) for row in rows] + csv_file = tmp_path / "addresses.csv" + csv_file.write_text("\n".join(lines) + "\n", encoding="utf-8") + return str(csv_file) + + def _make_file(name: str, item_id: str = "id-1") -> dict[str, Any]: return {"name": name, "id": item_id, "file": {}} @@ -47,6 +65,92 @@ def test_no_prefix_still_canonical() -> None: assert result == "100_1 High St AB1 2CD_Survey.pdf" +# --------------------------------------------------------------------------- +# _discover_roots — batch subfolders become extra roots +# --------------------------------------------------------------------------- + + +def test_discover_roots_includes_sero_batch_folders() -> None: + # Arrange + sp = MagicMock() + sp.get_folders_in_path.return_value = { + "value": [ + _make_folder("1 High St, AB1 2CD"), + _make_folder("_Sero Batch 3"), + _make_folder("_Sero Batch 2"), + _make_file("stray.pdf"), + ] + } + + # Act + roots = _make_orchestrator(sp)._discover_roots() + + # Assert + assert roots == [ + BASE_PATH, + f"{BASE_PATH}/_Sero Batch 2", + f"{BASE_PATH}/_Sero Batch 3", + ] + sp.get_folders_in_path.assert_called_once_with(BASE_PATH) + + +# --------------------------------------------------------------------------- +# run — properties under a batch root are still found and renamed +# --------------------------------------------------------------------------- + + +def test_run_renames_files_for_property_under_batch_root(tmp_path: Path) -> None: + # Arrange + csv_path = _write_csv(tmp_path, [("100", "1 High St", "AB1 2CD")]) + batch_assessment = _assessment_path( + f"{BASE_PATH}/_Sero Batch 2", "1 High St", "AB1 2CD" + ) + + sp = MagicMock() + + def fake_get(path: str) -> dict[str, Any]: + if path == BASE_PATH: + return {"value": [_make_folder("_Sero Batch 2")]} + if path == batch_assessment: + return {"value": [_make_file("Survey.pdf", "id-1")]} + raise ValueError("not found") + + sp.get_folders_in_path.side_effect = fake_get + + # Act + SharepointRenamerOrchestrator(sp, csv_path).run() + + # Assert + sp.rename_file.assert_called_once_with("id-1", "100_1 High St AB1 2CD_Survey.pdf") + + +def test_run_uprn_prefixed_address_yields_single_uprn_filename( + tmp_path: Path, +) -> None: + # Arrange + csv_path = _write_csv(tmp_path, [("100", "100_1 High St", "AB1 2CD")]) + batch_assessment = _assessment_path( + f"{BASE_PATH}/_Sero Batch 2", "100_1 High St", "AB1 2CD" + ) + + sp = MagicMock() + + def fake_get(path: str) -> dict[str, Any]: + if path == BASE_PATH: + return {"value": [_make_folder("_Sero Batch 2")]} + if path == batch_assessment: + return {"value": [_make_file("Survey.pdf", "id-1")]} + raise ValueError("not found") + + sp.get_folders_in_path.side_effect = fake_get + + # Act + SharepointRenamerOrchestrator(sp, csv_path).run() + + # Assert + sp.rename_file.assert_called_once_with("id-1", "100_1 High St AB1 2CD_Survey.pdf") + + # --------------------------------------------------------------------------- # _process_folder — files only at root level # --------------------------------------------------------------------------- @@ -121,14 +225,46 @@ def test_ignores_package_items() -> None: # --------------------------------------------------------------------------- -def test_missing_folder_logs_warning_and_returns(caplog: pytest.LogCaptureFixture) -> None: +def test_missing_folder_returns_false_without_warning( + caplog: pytest.LogCaptureFixture, +) -> None: + # Arrange sp = MagicMock() sp.get_folders_in_path.side_effect = ValueError("not found") - _make_orchestrator(sp)._process_folder("missing/path", "400", "4 Elm St", "AA2 2BB") + # Act + found = _make_orchestrator(sp)._process_folder( + "missing/path", "400", "4 Elm St", "AA2 2BB" + ) + # Assert + assert found is False sp.rename_file.assert_not_called() - assert any("Missing folder" in r.message and "400" in r.message for r in caplog.records) + assert not any("Missing folder" in r.message for r in caplog.records) + + +def test_run_warns_once_when_property_missing_from_all_roots( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + # Arrange + csv_path = _write_csv(tmp_path, [("400", "4 Elm St", "AA2 2BB")]) + sp = MagicMock() + + def fake_get(path: str) -> dict[str, Any]: + if path == BASE_PATH: + return {"value": [_make_folder("_Sero Batch 2")]} + raise ValueError("not found") + + sp.get_folders_in_path.side_effect = fake_get + + # Act + SharepointRenamerOrchestrator(sp, csv_path).run() + + # Assert + sp.rename_file.assert_not_called() + warnings = [r for r in caplog.records if "Missing folder" in r.message] + assert len(warnings) == 1 + assert "400" in warnings[0].message # --------------------------------------------------------------------------- diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/utils/test_sharepoint_client_error_handling.py b/tests/utils/test_sharepoint_client_error_handling.py new file mode 100644 index 000000000..4cae1578e --- /dev/null +++ b/tests/utils/test_sharepoint_client_error_handling.py @@ -0,0 +1,81 @@ +import logging +from typing import Any, Callable, Optional, cast + +import pytest + +from utils.sharepoint.sharepoint_client import ( + ResourceNotFoundError, + api_call_decorator, # pyright: ignore[reportUnknownVariableType] +) + +# The legacy decorator is untyped; cast so this module stays pyright-strict. +_decorate = cast( + Callable[[Callable[..., Any]], Callable[..., Any]], api_call_decorator +) + + +class _FakeResponse: + def __init__(self, status_code: int, error_code: str, message: str) -> None: + self.status_code = status_code + self._error_code = error_code + self._message = message + self.headers: dict[str, str] = {} + + def json(self) -> dict[str, Any]: + return {"error": {"code": self._error_code, "message": self._message}} + + +class _FakeClient: + headers: Optional[dict[str, str]] = {} + + def is_access_token_expired(self) -> bool: + return False + + @_decorate + def fetch(self, path: str) -> Any: + return "GET", f"https://graph.example.invalid/root:/{path}:/children", None + + +def _patch_response( + monkeypatch: pytest.MonkeyPatch, response: _FakeResponse +) -> None: + def fake_request(*args: Any, **kwargs: Any) -> _FakeResponse: + return response + + monkeypatch.setattr( + "utils.sharepoint.sharepoint_client.requests.request", fake_request + ) + + +def test_404_raises_resource_not_found_without_error_logs( + monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture +) -> None: + # Arrange + _patch_response( + monkeypatch, + _FakeResponse(404, "itemNotFound", "The resource could not be found."), + ) + + # Act + with pytest.raises(ResourceNotFoundError): + _FakeClient().fetch("missing/folder") + + # Assert + error_records = [r for r in caplog.records if r.levelno >= logging.ERROR] + assert error_records == [] + + +def test_other_client_errors_still_log_at_error_level( + monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture +) -> None: + # Arrange + _patch_response( + monkeypatch, _FakeResponse(403, "accessDenied", "Access denied.") + ) + + # Act + with pytest.raises(ValueError): + _FakeClient().fetch("forbidden/folder") + + # Assert + assert any(r.levelno >= logging.ERROR for r in caplog.records) diff --git a/utils/sharepoint/sharepoint_client.py b/utils/sharepoint/sharepoint_client.py index 38107dbfc..0364320f4 100644 --- a/utils/sharepoint/sharepoint_client.py +++ b/utils/sharepoint/sharepoint_client.py @@ -21,6 +21,10 @@ from utils.logger import setup_logger logger = setup_logger() +class ResourceNotFoundError(ValueError): + """Raised when the API returns 404 — an expected miss, not a failure.""" + + def handle_error(response): """ Handle errors based on HTTP status codes and log detailed information. @@ -35,6 +39,12 @@ def handle_error(response): inner_error = error_json.get("innererror", {}) details = error_json.get("details", []) + if response.status_code == 404: + logger.debug(f"Not found ({error_code}): {error_message}") + raise ResourceNotFoundError( + f"API request failed with status code 404 - {error_message}" + ) + logger.error(f"Error Code: {error_code}") logger.error(f"Error Message: {error_message}") if inner_error: @@ -46,8 +56,6 @@ def handle_error(response): logger.error("Unauthorized. Token might be invalid.") elif response.status_code == 403: logger.error("Forbidden. Access denied to the requested resource.") - elif response.status_code == 404: - logger.error("Not Found. The requested resource doesn’t exist.") elif response.status_code == 429: retry_after = int( response.headers.get("Retry-After", 5) @@ -121,6 +129,8 @@ def api_call_decorator(func): return response_data + except ResourceNotFoundError: + raise except Exception as e: logger.exception("An error occurred during the API call.") raise e