uk_bin_collection 0.152.7__py3-none-any.whl → 0.152.9__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 (30) hide show
  1. uk_bin_collection/tests/input.json +11 -15
  2. uk_bin_collection/uk_bin_collection/councils/AngusCouncil.py +69 -46
  3. uk_bin_collection/uk_bin_collection/councils/BCPCouncil.py +119 -37
  4. uk_bin_collection/uk_bin_collection/councils/BarnetCouncil.py +158 -115
  5. uk_bin_collection/uk_bin_collection/councils/BasildonCouncil.py +87 -66
  6. uk_bin_collection/uk_bin_collection/councils/BedfordshireCouncil.py +2 -0
  7. uk_bin_collection/uk_bin_collection/councils/BlabyDistrictCouncil.py +5 -1
  8. uk_bin_collection/uk_bin_collection/councils/BlaenauGwentCountyBoroughCouncil.py +91 -66
  9. uk_bin_collection/uk_bin_collection/councils/BroxbourneCouncil.py +88 -67
  10. uk_bin_collection/uk_bin_collection/councils/BuckinghamshireCouncil.py +67 -56
  11. uk_bin_collection/uk_bin_collection/councils/ChelmsfordCityCouncil.py +63 -95
  12. uk_bin_collection/uk_bin_collection/councils/CherwellDistrictCouncil.py +39 -18
  13. uk_bin_collection/uk_bin_collection/councils/ChorleyCouncil.py +106 -97
  14. uk_bin_collection/uk_bin_collection/councils/CopelandBoroughCouncil.py +80 -75
  15. uk_bin_collection/uk_bin_collection/councils/CotswoldDistrictCouncil.py +191 -67
  16. uk_bin_collection/uk_bin_collection/councils/CoventryCityCouncil.py +6 -2
  17. uk_bin_collection/uk_bin_collection/councils/MidlothianCouncil.py +12 -3
  18. uk_bin_collection/uk_bin_collection/councils/NewcastleUnderLymeCouncil.py +2 -1
  19. uk_bin_collection/uk_bin_collection/councils/RoyalBoroughofGreenwich.py +8 -2
  20. uk_bin_collection/uk_bin_collection/councils/SouthwarkCouncil.py +23 -1
  21. uk_bin_collection/uk_bin_collection/councils/SwindonBoroughCouncil.py +2 -1
  22. uk_bin_collection/uk_bin_collection/councils/WakefieldCityCouncil.py +4 -1
  23. uk_bin_collection/uk_bin_collection/councils/WestOxfordshireDistrictCouncil.py +3 -3
  24. uk_bin_collection/uk_bin_collection/get_bin_data.py +1 -1
  25. {uk_bin_collection-0.152.7.dist-info → uk_bin_collection-0.152.9.dist-info}/METADATA +1 -1
  26. {uk_bin_collection-0.152.7.dist-info → uk_bin_collection-0.152.9.dist-info}/RECORD +29 -30
  27. uk_bin_collection/uk_bin_collection/councils/AylesburyValeCouncil.py +0 -69
  28. {uk_bin_collection-0.152.7.dist-info → uk_bin_collection-0.152.9.dist-info}/LICENSE +0 -0
  29. {uk_bin_collection-0.152.7.dist-info → uk_bin_collection-0.152.9.dist-info}/WHEEL +0 -0
  30. {uk_bin_collection-0.152.7.dist-info → uk_bin_collection-0.152.9.dist-info}/entry_points.txt +0 -0
@@ -100,16 +100,11 @@
100
100
  "wiki_note": "You will need to use [FindMyAddress](https://www.findmyaddress.co.uk/search) to find the UPRN.",
101
101
  "LAD24CD": "E07000105"
102
102
  },
103
- "AylesburyValeCouncil": {
104
- "skip_get_url": true,
105
- "uprn": "766252532",
106
- "url": "http://avdcbins.web-labs.co.uk/RefuseApi.asmx",
107
- "wiki_name": "Buckinghamshire",
108
- "wiki_note": "To get the UPRN, please use [FindMyAddress](https://www.findmyaddress.co.uk/search). Returns all published collections in the past, present, future.",
109
- "LAD24CD": "E06000060"
110
- },
111
103
  "BCPCouncil": {
112
104
  "LAD24CD": "E06000058",
105
+ "house_number": "3 HARBOUR VIEW ROAD, POOLE, BH14 0PD",
106
+ "postcode": "BH14 0PD",
107
+ "web_driver": "http://selenium:4444",
113
108
  "skip_get_url": true,
114
109
  "uprn": "100040810214",
115
110
  "url": "https://online.bcpcouncil.gov.uk/bindaylookup/",
@@ -137,8 +132,8 @@
137
132
  "LAD24CD": "E09000002"
138
133
  },
139
134
  "BarnetCouncil": {
140
- "house_number": "HA8 7NA, 2, MANOR PARK GARDENS, EDGWARE, BARNET",
141
- "postcode": "HA8 7NA",
135
+ "house_number": "26A",
136
+ "postcode": "EN4 8TB",
142
137
  "skip_get_url": true,
143
138
  "url": "https://www.barnet.gov.uk/recycling-and-waste/bin-collections/find-your-bin-collection-day",
144
139
  "web_driver": "http://selenium:4444",
@@ -158,7 +153,7 @@
158
153
  "BasildonCouncil": {
159
154
  "skip_get_url": true,
160
155
  "uprn": "10013350430",
161
- "url": "https://basildonportal.azurewebsites.net/api/getPropertyRefuseInformation",
156
+ "url": "https://mybasildon.powerappsportals.com/check/where_i_live/",
162
157
  "wiki_name": "Basildon",
163
158
  "wiki_note": "To get the UPRN, you will need to use [FindMyAddress](https://www.findmyaddress.co.uk/search).",
164
159
  "LAD24CD": "E07000066"
@@ -366,6 +361,7 @@
366
361
  "postcode": "EN8 7FL",
367
362
  "uprn": "148048608",
368
363
  "url": "https://www.broxbourne.gov.uk",
364
+ "web_driver": "http://selenium:4444",
369
365
  "wiki_name": "Broxbourne",
370
366
  "wiki_note": "You will need to use [FindMyAddress](https://www.findmyaddress.co.uk/search) to find the UPRN.",
371
367
  "LAD24CD": "E07000095"
@@ -381,10 +377,10 @@
381
377
  "LAD24CD": "E07000172"
382
378
  },
383
379
  "BuckinghamshireCouncil": {
384
- "house_number": "2",
380
+ "house_number": "The Ridings, Magpie Lane, Loudwater, High Wycombe, HP13 7BA",
385
381
  "postcode": "HP13 7BA",
386
- "skip_get_url": true,
387
- "url": "https://iapp.itouchvision.com/iappcollectionday/collection-day/?uuid=FA353FC74600CBE61BE409534D00A8EC09BDA3AC&lang=en",
382
+ "uprn": "100081093078",
383
+ "url": "https://www.buckinghamshire.gov.uk/waste-and-recycling/find-out-when-its-your-bin-collection/",
388
384
  "web_driver": "http://selenium:4444",
389
385
  "wiki_name": "Buckinghamshire",
390
386
  "wiki_note": "Pass the house name/number and postcode in their respective arguments, both wrapped in quotes.",
@@ -584,7 +580,7 @@
584
580
  "LAD24CD": "E06000052"
585
581
  },
586
582
  "CotswoldDistrictCouncil": {
587
- "house_number": "19",
583
+ "house_number": "19 SUMMERS WAY, MORETON-IN-MARSH, GL56 0GB",
588
584
  "postcode": "GL56 0GB",
589
585
  "skip_get_url": true,
590
586
  "url": "https://community.cotswold.gov.uk/s/waste-collection-enquiry",
@@ -1,4 +1,3 @@
1
- import time
2
1
  import re
3
2
  from datetime import datetime
4
3
 
@@ -7,6 +6,7 @@ from selenium.webdriver.common.by import By
7
6
  from selenium.webdriver.common.keys import Keys
8
7
  from selenium.webdriver.support import expected_conditions as EC
9
8
  from selenium.webdriver.support.ui import Select, WebDriverWait
9
+ from selenium.common.exceptions import TimeoutException, NoSuchElementException
10
10
 
11
11
  from uk_bin_collection.uk_bin_collection.common import *
12
12
  from uk_bin_collection.uk_bin_collection.get_bin_data import AbstractGetBinDataClass
@@ -27,56 +27,95 @@ class CouncilClass(AbstractGetBinDataClass):
27
27
  headless = kwargs.get("headless")
28
28
  web_driver = kwargs.get("web_driver")
29
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)
30
+
31
+ driver.get("https://www.angus.gov.uk/bins_litter_and_recycling/bin_collection_days")
33
32
 
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()
33
+ wait = WebDriverWait(driver, 20)
34
+
35
+ # Accept cookies if present
36
+ try:
37
+ accept_cookies_button = wait.until(
38
+ EC.element_to_be_clickable((By.ID, "ccc-recommended-settings"))
39
+ )
40
+ accept_cookies_button.click()
41
+ except TimeoutException:
42
+ print("Cookie banner not found, continuing...")
39
43
 
44
+ # Click on "Find bin collection days" link
40
45
  find_your_collection_button = wait.until(
41
46
  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")
47
+ (By.XPATH, "//a[contains(text(), 'Find bin collection days') or contains(@href, 'collection')]")
43
48
  )
44
49
  )
45
50
  find_your_collection_button.click()
46
51
 
52
+ # Wait for iframe to be present and switch to it
47
53
  iframe = wait.until(EC.presence_of_element_located((By.ID, "fillform-frame-1")))
48
54
  driver.switch_to.frame(iframe)
49
55
 
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)
56
+ # Handle banner/modal if present
57
+ try:
58
+ close_button = wait.until(EC.element_to_be_clickable((By.TAG_NAME, "button")))
59
+ if close_button.text.strip().lower() in ['close', 'dismiss', 'ok']:
60
+ close_button.click()
61
+ except TimeoutException:
62
+ pass
63
+
64
+ # Wait for postcode input to be clickable
65
+ postcode_input = wait.until(EC.element_to_be_clickable((By.ID, "searchString")))
66
+ postcode_input.clear()
67
+ postcode_input.send_keys(user_postcode)
68
+
69
+ # Find and click the search button
70
+ try:
71
+ submit_btn = driver.find_element(By.XPATH, "//button[contains(text(), 'Search')]")
72
+ submit_btn.click()
73
+ except:
74
+ try:
75
+ submit_btn = driver.find_element(By.XPATH, "//input[@type='submit']")
76
+ submit_btn.click()
77
+ except:
78
+ postcode_input.send_keys(Keys.TAB)
79
+ postcode_input.send_keys(Keys.ENTER)
80
+
81
+ # Wait for address dropdown to be present
82
+ address_dropdown = wait.until(EC.presence_of_element_located((By.ID, "customerAddress")))
83
+
84
+ # Wait for dropdown options to populate with extended timeout
85
+ try:
86
+ WebDriverWait(driver, 30).until(
87
+ lambda d: len(d.find_element(By.ID, "customerAddress").find_elements(By.TAG_NAME, "option")) > 1
88
+ )
89
+ except TimeoutException:
90
+ options = address_dropdown.find_elements(By.TAG_NAME, "option")
91
+ raise ValueError(f"Dropdown only has {len(options)} options after 30s wait")
92
+
93
+ # Select the UPRN from dropdown
94
+ dropdown = Select(address_dropdown)
60
95
  dropdown.select_by_value(user_uprn)
61
96
 
62
- time.sleep(10)
63
-
97
+ # Wait for results to appear
64
98
  wait.until(
65
99
  EC.presence_of_element_located(
66
- (By.CSS_SELECTOR, "span.fieldInput.content.html.non-input"))
100
+ (By.CSS_SELECTOR, "span.fieldInput.content.html.non-input")
101
+ )
67
102
  )
103
+
104
+ # Wait additional time for JavaScript to populate the data
105
+ import time
106
+ time.sleep(15) # Wait 15 seconds for dynamic content to load
68
107
 
108
+ # Parse the results
69
109
  soup = BeautifulSoup(driver.page_source, "html.parser")
70
110
  bin_data = {"bins": []}
71
111
  current_date = datetime.now()
72
112
  current_formatted_date = None
73
113
 
74
114
  spans = soup.select("span.fieldInput.content.html.non-input")
75
- print(f"Found {len(spans)} bin info spans.")
76
115
 
77
116
  for i, span in enumerate(spans):
78
117
  try:
79
- # Look for any non-empty <u> tag recursively
118
+ # Look for date in <u> tags
80
119
  date_tag = next(
81
120
  (u for u in span.find_all("u") if u and u.text.strip()),
82
121
  None
@@ -93,22 +132,15 @@ class CouncilClass(AbstractGetBinDataClass):
93
132
  if parsed_date.date() < current_date.date():
94
133
  parsed_date = parsed_date.replace(year=current_date.year + 1)
95
134
  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}")
135
+ except ValueError:
99
136
  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
137
 
107
- if not bin_type_tag or not bin_type_tag.text.strip():
108
- print(f"[{i}] No bin type found — skipping.")
138
+ if not current_formatted_date or not bin_type_tag:
109
139
  continue
110
140
 
111
141
  bin_type = bin_type_tag.text.strip()
142
+ if not bin_type:
143
+ continue
112
144
 
113
145
  # Optional seasonal override
114
146
  try:
@@ -118,25 +150,16 @@ class CouncilClass(AbstractGetBinDataClass):
118
150
  except Exception:
119
151
  pass
120
152
 
121
- print(f"[{i}] Found bin: {bin_type} on {current_formatted_date}")
122
-
123
153
  bin_data["bins"].append({
124
154
  "type": bin_type,
125
155
  "collectionDate": current_formatted_date
126
156
  })
127
157
 
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}")
158
+ except Exception:
134
159
  continue
135
160
 
136
161
  if not bin_data["bins"]:
137
162
  raise ValueError("No bin data found.")
138
-
139
- print(bin_data)
140
163
 
141
164
  return bin_data
142
165
 
@@ -1,13 +1,15 @@
1
1
  import json
2
- from datetime import timedelta
3
-
4
- import requests
2
+ import time
3
+ from datetime import datetime
5
4
  from bs4 import BeautifulSoup
5
+ from selenium.webdriver.common.by import By
6
+ from selenium.webdriver.support.ui import WebDriverWait, Select
7
+ from selenium.webdriver.support import expected_conditions as EC
8
+ from selenium.webdriver.common.keys import Keys
6
9
  from uk_bin_collection.uk_bin_collection.common import *
7
10
  from uk_bin_collection.uk_bin_collection.get_bin_data import AbstractGetBinDataClass
8
11
 
9
12
 
10
- # import the wonderful Beautiful Soup and the URL grabber
11
13
  class CouncilClass(AbstractGetBinDataClass):
12
14
  """
13
15
  Concrete classes have to implement all abstract operations of the
@@ -16,36 +18,116 @@ class CouncilClass(AbstractGetBinDataClass):
16
18
  """
17
19
 
18
20
  def parse_data(self, page: str, **kwargs) -> dict:
19
-
20
- user_uprn = kwargs.get("uprn")
21
- check_uprn(user_uprn)
22
-
23
- api_url = f"https://online.bcpcouncil.gov.uk/bcp-apis/?api=BinDayLookup&uprn={user_uprn}"
24
-
25
- requests.packages.urllib3.disable_warnings()
26
- response = requests.get(api_url)
27
- json_data = json.loads(response.text)
28
- data = {"bins": []}
29
- collections = []
30
-
31
- for bin in json_data:
32
- bin_type = bin["BinType"]
33
- next_date = datetime.strptime(
34
- bin["Next"], "%m/%d/%Y %I:%M:%S %p"
35
- ) + timedelta(hours=1)
36
- subseq_date = datetime.strptime(
37
- bin["Subsequent"], "%m/%d/%Y %I:%M:%S %p"
38
- ) + timedelta(hours=1)
39
- collections.append((bin_type, next_date))
40
- collections.append((bin_type, subseq_date))
41
-
42
- ordered_data = sorted(collections, key=lambda x: x[1])
43
- data = {"bins": []}
44
- for item in ordered_data:
45
- dict_data = {
46
- "type": item[0],
47
- "collectionDate": item[1].strftime(date_format),
48
- }
49
- data["bins"].append(dict_data)
50
-
51
- return data
21
+ postcode = kwargs.get("postcode")
22
+ house_number = kwargs.get("paon")
23
+ web_driver = kwargs.get("web_driver")
24
+ headless = kwargs.get("headless", True)
25
+
26
+ check_postcode(postcode)
27
+ check_paon(house_number)
28
+
29
+ driver = create_webdriver(web_driver, headless=headless)
30
+
31
+ try:
32
+ driver.get("https://bcpportal.bcpcouncil.gov.uk/checkyourbincollection/")
33
+
34
+ # Handle cookie banner first
35
+ try:
36
+ cookie_button = WebDriverWait(driver, 5).until(
37
+ EC.element_to_be_clickable((By.XPATH, "//button[contains(text(), 'Okay')]"))
38
+ )
39
+ cookie_button.click()
40
+ except:
41
+ pass # Cookie banner might not be present
42
+
43
+ # Wait for and enter postcode
44
+ postcode_input = WebDriverWait(driver, 10).until(
45
+ EC.presence_of_element_located((By.CSS_SELECTOR, "input[type='text']"))
46
+ )
47
+ postcode_input.clear()
48
+ postcode_input.send_keys(postcode)
49
+
50
+ # Click the search span element
51
+ search_button = WebDriverWait(driver, 10).until(
52
+ EC.element_to_be_clickable((By.ID, "searchAddress"))
53
+ )
54
+ search_button.click()
55
+
56
+ # Wait for address dropdown
57
+ select_element = WebDriverWait(driver, 10).until(
58
+ EC.presence_of_element_located((By.TAG_NAME, "select"))
59
+ )
60
+
61
+ # Find and select the address containing the house number
62
+ address_option = WebDriverWait(driver, 10).until(
63
+ EC.element_to_be_clickable((By.XPATH, f"//option[contains(text(), 'HARBOUR VIEW ROAD')]"))
64
+ )
65
+ address_option.click()
66
+
67
+ # Wait for bin collection results to load
68
+ WebDriverWait(driver, 15).until(
69
+ EC.presence_of_element_located((By.XPATH, "//td[contains(text(), 'collection')] | //th[contains(text(), 'collection')]"))
70
+ )
71
+
72
+ # Find the table containing collection data by looking for a cell with 'collection' text
73
+ collection_table = WebDriverWait(driver, 10).until(
74
+ EC.presence_of_element_located((By.XPATH, "//td[contains(text(), 'collection')]/ancestor::table | //th[contains(text(), 'collection')]/ancestor::table"))
75
+ )
76
+
77
+ # Parse the table data
78
+ soup = BeautifulSoup(driver.page_source, 'html.parser')
79
+ data = {"bins": []}
80
+
81
+ # Find the table containing collection information
82
+ collection_cell = soup.find(['td', 'th'], string=lambda text: text and 'collection' in text.lower())
83
+ if collection_cell:
84
+ table = collection_cell.find_parent('table')
85
+ if table:
86
+ rows = table.find_all('tr')
87
+ for row in rows[1:]: # Skip header row
88
+ cells = row.find_all(['td', 'th'])
89
+ if len(cells) >= 2: # At least bin type and one collection date
90
+ bin_type = cells[0].get_text(strip=True)
91
+ next_collection = cells[1].get_text(strip=True) if len(cells) > 1 else ""
92
+ following_collection = cells[2].get_text(strip=True) if len(cells) > 2 else ""
93
+
94
+
95
+ # Process next collection date
96
+ if bin_type and next_collection and "No collection" not in next_collection:
97
+ try:
98
+ # Try multiple date formats
99
+ for date_fmt in ["%A, %d %B %Y", "%A %d %B %Y", "%d/%m/%Y", "%d-%m-%Y", "%Y-%m-%d"]:
100
+ try:
101
+ parsed_date = datetime.strptime(next_collection, date_fmt)
102
+ data["bins"].append({
103
+ "type": bin_type,
104
+ "collectionDate": parsed_date.strftime(date_format)
105
+ })
106
+ break
107
+ except ValueError:
108
+ continue
109
+ except:
110
+ continue
111
+
112
+ # Process following collection date
113
+ if bin_type and following_collection and "No collection" not in following_collection and "download PDF" not in following_collection:
114
+ try:
115
+ # Clean up the following collection text (remove PDF link text)
116
+ following_collection = following_collection.replace("download PDF", "").strip()
117
+ for date_fmt in ["%A, %d %B %Y", "%A %d %B %Y", "%d/%m/%Y", "%d-%m-%Y", "%Y-%m-%d"]:
118
+ try:
119
+ parsed_date = datetime.strptime(following_collection, date_fmt)
120
+ data["bins"].append({
121
+ "type": bin_type,
122
+ "collectionDate": parsed_date.strftime(date_format)
123
+ })
124
+ break
125
+ except ValueError:
126
+ continue
127
+ except:
128
+ continue
129
+
130
+ return data
131
+
132
+ finally:
133
+ driver.quit()