uk_bin_collection 0.150.1__py3-none-any.whl → 0.152.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (19) hide show
  1. uk_bin_collection/tests/input.json +46 -8
  2. uk_bin_collection/uk_bin_collection/councils/AngusCouncil.py +149 -0
  3. uk_bin_collection/uk_bin_collection/councils/ArgyllandButeCouncil.py +0 -2
  4. uk_bin_collection/uk_bin_collection/councils/BarkingDagenham.py +11 -2
  5. uk_bin_collection/uk_bin_collection/councils/BrentCouncil.py +47 -33
  6. uk_bin_collection/uk_bin_collection/councils/BrightonandHoveCityCouncil.py +11 -2
  7. uk_bin_collection/uk_bin_collection/councils/BroadlandDistrictCouncil.py +21 -6
  8. uk_bin_collection/uk_bin_collection/councils/ChichesterDistrictCouncil.py +105 -53
  9. uk_bin_collection/uk_bin_collection/councils/FermanaghOmaghDistrictCouncil.py +102 -0
  10. uk_bin_collection/uk_bin_collection/councils/MaidstoneBoroughCouncil.py +115 -0
  11. uk_bin_collection/uk_bin_collection/councils/NorthEastDerbyshireDistrictCouncil.py +61 -39
  12. uk_bin_collection/uk_bin_collection/councils/RugbyBoroughCouncil.py +128 -71
  13. uk_bin_collection/uk_bin_collection/councils/SloughBoroughCouncil.py +140 -0
  14. uk_bin_collection/uk_bin_collection/councils/TewkesburyBoroughCouncil.py +40 -0
  15. {uk_bin_collection-0.150.1.dist-info → uk_bin_collection-0.152.0.dist-info}/METADATA +1 -1
  16. {uk_bin_collection-0.150.1.dist-info → uk_bin_collection-0.152.0.dist-info}/RECORD +19 -14
  17. {uk_bin_collection-0.150.1.dist-info → uk_bin_collection-0.152.0.dist-info}/LICENSE +0 -0
  18. {uk_bin_collection-0.150.1.dist-info → uk_bin_collection-0.152.0.dist-info}/WHEEL +0 -0
  19. {uk_bin_collection-0.150.1.dist-info → uk_bin_collection-0.152.0.dist-info}/entry_points.txt +0 -0
@@ -28,6 +28,16 @@
28
28
  "wiki_note": "You will need to use [FindMyAddress](https://www.findmyaddress.co.uk/search) to find the UPRN.",
29
29
  "LAD24CD": "E07000032"
30
30
  },
31
+ "AngusCouncil": {
32
+ "uprn": "117053733",
33
+ "skip_get_url": true,
34
+ "postcode": "DD7 7LE",
35
+ "url": "https://www.angus.gov.uk/bins_litter_and_recycling/bin_collection_days",
36
+ "web_driver": "http://selenium:4444",
37
+ "wiki_name": "Angus",
38
+ "wiki_note": "You will need to use [FindMyAddress](https://www.findmyaddress.co.uk/search) to find the UPRN. Requires Selenium",
39
+ "LAD24CD": "S12000041"
40
+ },
31
41
  "AntrimAndNewtonabbeyCouncil": {
32
42
  "LAD24CD": "N09000001",
33
43
  "url": "https://antrimandnewtownabbey.gov.uk/residents/bins-recycling/bins-schedule/?Id=643",
@@ -117,13 +127,13 @@
117
127
  "LAD24CD": "E07000200"
118
128
  },
119
129
  "BarkingDagenham": {
120
- "house_number": "19 KELLY WAY, CHADWELL HEATH, RM6 6XH",
130
+ "house_number": "19",
121
131
  "postcode": "RM6 6XH",
122
132
  "skip_get_url": true,
123
133
  "web_driver": "http://selenium:4444",
124
134
  "url": "https://www.lbbd.gov.uk/rubbish-recycling/household-bin-collection/check-your-bin-collection-days",
125
135
  "wiki_name": "Barking and Dagenham",
126
- "wiki_note": "Use the full address as it appears on the drop-down on the site when you search by postcode.",
136
+ "wiki_note": "Use house number and postcode. Requires Selenium.",
127
137
  "LAD24CD": "E09000002"
128
138
  },
129
139
  "BarnetCouncil": {
@@ -311,13 +321,13 @@
311
321
  "LAD24CD": "E09000005"
312
322
  },
313
323
  "BrightonandHoveCityCouncil": {
314
- "house_number": "44 Carden Avenue, Brighton, BN1 8NE",
324
+ "house_number": "44",
315
325
  "postcode": "BN1 8NE",
316
326
  "skip_get_url": true,
317
327
  "url": "https://cityclean.brighton-hove.gov.uk/link/collections",
318
328
  "web_driver": "http://selenium:4444",
319
329
  "wiki_name": "Brighton and Hove",
320
- "wiki_note": "Use the full address as it appears on the drop-down on the site when you search by postcode.",
330
+ "wiki_note": "Use house number and postcode. Requires Selenium",
321
331
  "LAD24CD": "E06000043"
322
332
  },
323
333
  "BristolCityCouncil": {
@@ -330,12 +340,12 @@
330
340
  },
331
341
  "BroadlandDistrictCouncil": {
332
342
  "skip_get_url": true,
333
- "house_number": "1 Park View, Horsford, Norfolk, NR10 3FD",
343
+ "house_number": "1",
334
344
  "postcode": "NR10 3FD",
335
345
  "url": "https://area.southnorfolkandbroadland.gov.uk/FindAddress",
336
346
  "web_driver": "http://selenium:4444",
337
347
  "wiki_name": "Broadland",
338
- "wiki_note": "Use the full address as it appears on the drop-down on the site when you search by postcode.",
348
+ "wiki_note": "Use house number and postcode. Requires Selenium.",
339
349
  "LAD24CD": "E07000144"
340
350
  },
341
351
  "BromleyBoroughCouncil": {
@@ -521,7 +531,7 @@
521
531
  "LAD24CD": "E07000034"
522
532
  },
523
533
  "ChichesterDistrictCouncil": {
524
- "house_number": "7, Plaistow Road, Kirdford, Billingshurst, West Sussex",
534
+ "house_number": "7",
525
535
  "postcode": "RH14 0JT",
526
536
  "skip_get_url": true,
527
537
  "url": "https://www.chichester.gov.uk/checkyourbinday",
@@ -901,6 +911,15 @@
901
911
  "wiki_note": "Pass the UPRN. You can find it using [FindMyAddress](https://www.findmyaddress.co.uk/search).",
902
912
  "LAD24CD": "E07000010"
903
913
  },
914
+ "FermanaghOmaghDistrictCouncil": {
915
+ "house_number": "20",
916
+ "postcode": "BT74 6DQ",
917
+ "skip_get_url": true,
918
+ "url": "https://www.fermanaghomagh.com/services/environment-and-waste/waste-collection-calendar/",
919
+ "wiki_name": "Fermanagh and Omagh",
920
+ "wiki_note": "Pass the house number and postcode in their respective parameters.",
921
+ "LAD24CD": "N09000006"
922
+ },
904
923
  "FifeCouncil": {
905
924
  "uprn": "320203521",
906
925
  "url": "https://www.fife.gov.uk",
@@ -1199,6 +1218,7 @@
1199
1218
  "LAD24CD": "E07000120",
1200
1219
  "uprn": "100010448773",
1201
1220
  "url": "https://iapp.itouchvision.com/iappcollectionday/collection-day/?uuid=FEBA68993831481FD81B2E605364D00A8DC017A4",
1221
+ "skip_get_url": true,
1202
1222
  "web_driver": "http://selenium:4444",
1203
1223
  "wiki_name": "Hyndburn",
1204
1224
  "wiki_note": "Pass the UPRN. You can find it using [FindMyAddress](https://www.findmyaddress.co.uk/search). This parser requires a Selenium webdriver."
@@ -1622,7 +1642,7 @@
1622
1642
  "NorthEastDerbyshireDistrictCouncil": {
1623
1643
  "postcode": "S42 5RB",
1624
1644
  "skip_get_url": true,
1625
- "uprn": "010034492221",
1645
+ "uprn": "010034492222",
1626
1646
  "url": "https://myselfservice.ne-derbyshire.gov.uk/service/Check_your_Bin_Day",
1627
1647
  "web_driver": "http://selenium:4444",
1628
1648
  "wiki_name": "North East Derbyshire",
@@ -1935,6 +1955,7 @@
1935
1955
  "skip_get_url": true,
1936
1956
  "uprn": "100070182634",
1937
1957
  "url": "https://www.rugby.gov.uk/check-your-next-bin-day",
1958
+ "web_driver": "http://selenium:4444",
1938
1959
  "wiki_name": "Rugby",
1939
1960
  "wiki_note": "Provide your UPRN and postcode. You can find your UPRN using [FindMyAddress](https://www.findmyaddress.co.uk/search).",
1940
1961
  "LAD24CD": "E07000220"
@@ -2012,6 +2033,15 @@
2012
2033
  "wiki_note": "Follow the instructions [here](https://bins.shropshire.gov.uk/) until you get the page showing your bin collection dates, then copy the URL and replace the URL in the command.",
2013
2034
  "LAD24CD": "E06000051"
2014
2035
  },
2036
+ "SloughBoroughCouncil": {
2037
+ "postcode": "SL2 2EW",
2038
+ "skip_get_url": true,
2039
+ "url": "https://www.slough.gov.uk/bin-collections",
2040
+ "web_driver": "http://selenium:4444",
2041
+ "wiki_name": "Slough",
2042
+ "wiki_note": "Pass the UPRN and postcode in their respective parameters. This parser requires a Selenium webdriver.",
2043
+ "LAD24CD": "E06000039"
2044
+ },
2015
2045
  "SolihullCouncil": {
2016
2046
  "url": "https://digital.solihull.gov.uk/BinCollectionCalendar/Calendar.aspx?UPRN=100071005444",
2017
2047
  "wiki_command_url_override": "https://digital.solihull.gov.uk/BinCollectionCalendar/Calendar.aspx?UPRN=XXXXXXXX",
@@ -2331,6 +2361,14 @@
2331
2361
  "wiki_note": "Provide your UPRN. Find it using [FindMyAddress](https://www.findmyaddress.co.uk/search).",
2332
2362
  "LAD24CD": "E06000020"
2333
2363
  },
2364
+ "TewkesburyBoroughCouncil": {
2365
+ "skip_get_url": true,
2366
+ "uprn": "10067626314",
2367
+ "url": "https://tewkesbury.gov.uk/services/waste-and-recycling/",
2368
+ "wiki_name": "Tewkesbury",
2369
+ "wiki_note": "Provide your UPRN. Find it using [FindMyAddress](https://www.findmyaddress.co.uk/search).",
2370
+ "LAD24CD": "E07000083"
2371
+ },
2334
2372
  "TendringDistrictCouncil": {
2335
2373
  "postcode": "CO15 4EU",
2336
2374
  "skip_get_url": true,
@@ -0,0 +1,149 @@
1
+ import time
2
+ import re
3
+ from datetime import datetime
4
+
5
+ from bs4 import BeautifulSoup
6
+ from selenium.webdriver.common.by import By
7
+ from selenium.webdriver.common.keys import Keys
8
+ from selenium.webdriver.support import expected_conditions as EC
9
+ from selenium.webdriver.support.ui import Select, WebDriverWait
10
+
11
+ from uk_bin_collection.uk_bin_collection.common import *
12
+ from uk_bin_collection.uk_bin_collection.get_bin_data import AbstractGetBinDataClass
13
+
14
+
15
+ class CouncilClass(AbstractGetBinDataClass):
16
+ def parse_data(self, page: str, **kwargs) -> dict:
17
+ driver = None
18
+ try:
19
+ user_postcode = kwargs.get("postcode")
20
+ if not user_postcode:
21
+ raise ValueError("No postcode provided.")
22
+ check_postcode(user_postcode)
23
+
24
+ user_uprn = kwargs.get("uprn")
25
+ check_uprn(user_uprn)
26
+
27
+ headless = kwargs.get("headless")
28
+ web_driver = kwargs.get("web_driver")
29
+ driver = create_webdriver(web_driver, headless, None, __name__)
30
+ page = "https://www.angus.gov.uk/bins_litter_and_recycling/bin_collection_days"
31
+
32
+ driver.get(page)
33
+
34
+ wait = WebDriverWait(driver, 10)
35
+ accept_cookies_button = wait.until(
36
+ EC.element_to_be_clickable((By.ID, "ccc-recommended-settings"))
37
+ )
38
+ accept_cookies_button.click()
39
+
40
+ find_your_collection_button = wait.until(
41
+ EC.element_to_be_clickable(
42
+ (By.XPATH, "/html/body/div[2]/div[2]/div/div/section/div[2]/div/article/div/div/p[2]/a")
43
+ )
44
+ )
45
+ find_your_collection_button.click()
46
+
47
+ iframe = wait.until(EC.presence_of_element_located((By.ID, "fillform-frame-1")))
48
+ driver.switch_to.frame(iframe)
49
+
50
+ postcode_input = wait.until(EC.presence_of_element_located((By.ID, "searchString")))
51
+ postcode_input.send_keys(user_postcode + Keys.TAB + Keys.ENTER)
52
+
53
+ time.sleep(15)
54
+
55
+ select_elem = wait.until(EC.presence_of_element_located((By.ID, "customerAddress")))
56
+ WebDriverWait(driver, 10).until(
57
+ lambda d: len(select_elem.find_elements(By.TAG_NAME, "option")) > 1
58
+ )
59
+ dropdown = Select(select_elem)
60
+ dropdown.select_by_value(user_uprn)
61
+
62
+ time.sleep(10)
63
+
64
+ wait.until(
65
+ EC.presence_of_element_located(
66
+ (By.CSS_SELECTOR, "span.fieldInput.content.html.non-input"))
67
+ )
68
+
69
+ soup = BeautifulSoup(driver.page_source, "html.parser")
70
+ bin_data = {"bins": []}
71
+ current_date = datetime.now()
72
+ current_formatted_date = None
73
+
74
+ spans = soup.select("span.fieldInput.content.html.non-input")
75
+ print(f"Found {len(spans)} bin info spans.")
76
+
77
+ for i, span in enumerate(spans):
78
+ try:
79
+ # Look for any non-empty <u> tag recursively
80
+ date_tag = next(
81
+ (u for u in span.find_all("u") if u and u.text.strip()),
82
+ None
83
+ )
84
+ bin_type_tag = span.find("b")
85
+
86
+ if date_tag:
87
+ raw_date = date_tag.text.strip().replace(",", "")
88
+ full_date_str = f"{raw_date} {current_date.year}"
89
+ full_date_str = re.sub(r"\s+", " ", full_date_str)
90
+
91
+ try:
92
+ parsed_date = datetime.strptime(full_date_str, "%A %d %B %Y")
93
+ if parsed_date.date() < current_date.date():
94
+ parsed_date = parsed_date.replace(year=current_date.year + 1)
95
+ current_formatted_date = parsed_date.strftime("%d/%m/%Y")
96
+ print(f"[{i}] Parsed date: {current_formatted_date}")
97
+ except ValueError as ve:
98
+ print(f"[{i}] Could not parse date: '{full_date_str}' - {ve}")
99
+ continue
100
+ else:
101
+ print(f"[{i}] No date tag found, using last valid date: {current_formatted_date}")
102
+
103
+ if not current_formatted_date:
104
+ print(f"[{i}] No current date to associate bin type with — skipping.")
105
+ continue
106
+
107
+ if not bin_type_tag or not bin_type_tag.text.strip():
108
+ print(f"[{i}] No bin type found — skipping.")
109
+ continue
110
+
111
+ bin_type = bin_type_tag.text.strip()
112
+
113
+ # Optional seasonal override
114
+ try:
115
+ overrides_dict = get_seasonal_overrides()
116
+ if current_formatted_date in overrides_dict:
117
+ current_formatted_date = overrides_dict[current_formatted_date]
118
+ except Exception:
119
+ pass
120
+
121
+ print(f"[{i}] Found bin: {bin_type} on {current_formatted_date}")
122
+
123
+ bin_data["bins"].append({
124
+ "type": bin_type,
125
+ "collectionDate": current_formatted_date
126
+ })
127
+
128
+ except Exception as inner_e:
129
+ print(f"[{i}] Skipping span due to error: {inner_e}")
130
+ continue
131
+
132
+ except Exception as inner_e:
133
+ print(f"Skipping span due to error: {inner_e}")
134
+ continue
135
+
136
+ if not bin_data["bins"]:
137
+ raise ValueError("No bin data found.")
138
+
139
+ print(bin_data)
140
+
141
+ return bin_data
142
+
143
+ except Exception as e:
144
+ print(f"An error occurred: {e}")
145
+ raise
146
+
147
+ finally:
148
+ if driver:
149
+ driver.quit()
@@ -8,8 +8,6 @@ from uk_bin_collection.uk_bin_collection.get_bin_data import AbstractGetBinDataC
8
8
 
9
9
 
10
10
  # import the wonderful Beautiful Soup and the URL grabber
11
-
12
-
13
11
  class CouncilClass(AbstractGetBinDataClass):
14
12
  """
15
13
  Concrete classes have to implement all abstract operations of the
@@ -84,10 +84,19 @@ class CouncilClass(AbstractGetBinDataClass):
84
84
  EC.element_to_be_clickable((By.ID, "address")),
85
85
  message="Address dropdown not found",
86
86
  )
87
+
87
88
  dropdown = Select(address_select)
88
89
 
89
- dropdown.select_by_visible_text(user_paon)
90
- print("Address selected successfully")
90
+ found = False
91
+ for option in dropdown.options:
92
+ if user_paon in option.text:
93
+ option.click()
94
+ found = True
95
+ print("Address selected successfully")
96
+ break
97
+
98
+ if not found:
99
+ raise Exception(f"No matching address containing '{user_paon}' found.")
91
100
 
92
101
  driver.switch_to.active_element.send_keys(Keys.TAB + Keys.ENTER)
93
102
  print("Pressed ENTER on Next button")
@@ -74,42 +74,56 @@ class CouncilClass(AbstractGetBinDataClass):
74
74
  )
75
75
 
76
76
  if service_details:
77
-
78
- # Extract next collection date
77
+ # Extract next collection date only
79
78
  next_collection_row = service_details.find(
80
79
  "dt", string="Next collection"
81
80
  )
82
- next_collection = (
83
- next_collection_row.find_next_sibling("dd").get_text(
84
- strip=True
85
- )
86
- if next_collection_row
87
- else "Unknown"
88
- )
89
-
90
- # Parse dates into standard dd/mm/yyyy format
91
- next_collection_date = datetime.strptime(
92
- remove_ordinal_indicator_from_date_string(next_collection),
93
- "%A, %d %B",
94
- )
95
-
96
- if (datetime.now().month == 12) and (
97
- next_collection.month == 1
98
- ):
99
- next_collection_date = next_collection_date.replace(
100
- year=next_year
81
+ if next_collection_row:
82
+ next_collection = next_collection_row.find_next_sibling(
83
+ "dd"
84
+ ).get_text(strip=True)
85
+
86
+ # Remove the adjusted collection time message
87
+ if (
88
+ "(this collection has been adjusted from its usual time)"
89
+ in next_collection
90
+ ):
91
+ next_collection = next_collection.replace(
92
+ "(this collection has been adjusted from its usual time)",
93
+ "",
94
+ ).strip()
95
+
96
+ # Parse date from format like "Wednesday, 7th May"
97
+ next_collection = remove_ordinal_indicator_from_date_string(
98
+ next_collection
101
99
  )
102
- else:
103
- next_collection_date = next_collection_date.replace(
104
- year=current_year
105
- )
106
-
107
- dict_data = {
108
- "type": collection_type.strip(),
109
- "collectionDate": next_collection_date.strftime(
110
- date_format
111
- ),
112
- }
113
- data["bins"].append(dict_data)
100
+ try:
101
+ next_collection_date = datetime.strptime(
102
+ next_collection, "%A, %d %B"
103
+ )
104
+
105
+ # Handle year rollover
106
+ if (
107
+ datetime.now().month == 12
108
+ and next_collection_date.month == 1
109
+ ):
110
+ next_collection_date = next_collection_date.replace(
111
+ year=next_year
112
+ )
113
+ else:
114
+ next_collection_date = next_collection_date.replace(
115
+ year=current_year
116
+ )
117
+
118
+ dict_data = {
119
+ "type": collection_type.strip(),
120
+ "collectionDate": next_collection_date.strftime(
121
+ date_format
122
+ ),
123
+ }
124
+ data["bins"].append(dict_data)
125
+ print(dict_data)
126
+ except ValueError as e:
127
+ print(f"Error parsing date {next_collection}: {e}")
114
128
 
115
129
  return data
@@ -63,8 +63,16 @@ class CouncilClass(AbstractGetBinDataClass):
63
63
 
64
64
  # Create a 'Select' for it, then select the first address in the list
65
65
  # (Index 0 is "Make a selection from the list")
66
- dropdownSelect = Select(parent_element)
67
- dropdownSelect.select_by_visible_text(str(user_paon))
66
+ options = parent_element.find_elements(By.TAG_NAME, "option")
67
+ found = False
68
+ for option in options:
69
+ if user_paon in option.text:
70
+ option.click()
71
+ found = True
72
+ break
73
+
74
+ if not found:
75
+ raise Exception(f"Address containing '{user_paon}' not found in dropdown options")
68
76
 
69
77
  submit_btn = wait.until(
70
78
  EC.presence_of_element_located(
@@ -125,6 +133,7 @@ class CouncilClass(AbstractGetBinDataClass):
125
133
  break
126
134
  dict_data = {"type": bin_type, "collectionDate": bin_date}
127
135
  data["bins"].append(dict_data)
136
+ print(data)
128
137
  except Exception as e:
129
138
  # Here you can log the exception if needed
130
139
  print(f"An error occurred: {e}")
@@ -83,15 +83,30 @@ class CouncilClass(AbstractGetBinDataClass):
83
83
  )
84
84
  print("Found address dropdown")
85
85
 
86
- # Create a Select object for the dropdown
87
86
  dropdown_select = Select(address_dropdown)
88
87
 
89
- # Search for the exact address
90
- print(f"Looking for address: {user_paon}")
88
+ print(f"Looking for address containing: {user_paon}")
91
89
 
92
- # Select the address by visible text
93
- dropdown_select.select_by_visible_text(user_paon)
94
- print(f"Selected address: {user_paon}")
90
+ found = False
91
+ user_paon_clean = user_paon.lower().strip()
92
+
93
+ for option in dropdown_select.options:
94
+ option_text_clean = option.text.lower().strip()
95
+
96
+ if (
97
+ option_text_clean == user_paon_clean # Exact match if full address given
98
+ or option_text_clean.startswith(f"{user_paon_clean} ") # Startswith match if just a number
99
+ ):
100
+ option.click()
101
+ found = True
102
+ print(f"Selected address: {option.text.strip()}")
103
+ break
104
+
105
+ if not found:
106
+ all_options = [opt.text for opt in dropdown_select.options]
107
+ raise Exception(
108
+ f"Could not find a matching address for '{user_paon}'. Available options: {all_options}"
109
+ )
95
110
 
96
111
  print("Looking for submit button after address selection...")
97
112
  submit_btn = wait.until(