uk_bin_collection 0.144.2__py3-none-any.whl → 0.144.3__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.
@@ -990,6 +990,15 @@
990
990
  "wiki_name": "Highland Council",
991
991
  "wiki_note": "You will need to use [FindMyAddress](https://www.findmyaddress.co.uk/search) to find the UPRN."
992
992
  },
993
+ "Hillingdon": {
994
+ "house_number": "1, Milverton Drive, Ickenham, UB10 8PP, Ickenham, Hillingdon",
995
+ "postcode": "UB10 8PP",
996
+ "skip_get_url": true,
997
+ "url": "https://www.hillingdon.gov.uk/collection-day",
998
+ "web_driver": "http://selenium:4444",
999
+ "wiki_name": "High Peak Council",
1000
+ "wiki_note": "Pass the postcode and the full address as it appears in the address pulldown menu."
1001
+ },
993
1002
  "HinckleyandBosworthBoroughCouncil": {
994
1003
  "uprn": "100030533512",
995
1004
  "url": "https://www.hinckley-bosworth.gov.uk",
@@ -0,0 +1,273 @@
1
+ import json
2
+ from datetime import datetime, timedelta
3
+ from typing import Any, Dict
4
+
5
+ from bs4 import BeautifulSoup
6
+ from dateutil.parser import parse
7
+ from selenium.common.exceptions import (
8
+ NoSuchElementException,
9
+ StaleElementReferenceException,
10
+ TimeoutException,
11
+ )
12
+ from selenium.webdriver.common.by import By
13
+ from selenium.webdriver.common.keys import Keys
14
+ from selenium.webdriver.remote.webdriver import WebDriver
15
+ from selenium.webdriver.support import expected_conditions as EC
16
+ from selenium.webdriver.support.wait import WebDriverWait
17
+
18
+ from uk_bin_collection.uk_bin_collection.common import *
19
+ from uk_bin_collection.uk_bin_collection.get_bin_data import AbstractGetBinDataClass
20
+
21
+ # Dictionary mapping day names to their weekday numbers (Monday=0, Sunday=6)
22
+ DAYS_OF_WEEK = {
23
+ "Monday": 0,
24
+ "Tuesday": 1,
25
+ "Wednesday": 2,
26
+ "Thursday": 3,
27
+ "Friday": 4,
28
+ "Saturday": 5,
29
+ "Sunday": 6,
30
+ }
31
+
32
+
33
+ # This function checks for bank holiday collection changes,
34
+ # but the page seems manually written so might break easily
35
+ def get_bank_holiday_changes(driver: WebDriver) -> Dict[str, str]:
36
+ """Fetch and parse bank holiday collection changes from the council website."""
37
+ bank_holiday_url = "https://www.hillingdon.gov.uk/bank-holiday-collections"
38
+ driver.get(bank_holiday_url)
39
+
40
+ # Wait for page to load
41
+ wait = WebDriverWait(driver, 10)
42
+ wait.until(EC.presence_of_element_located((By.TAG_NAME, "table")))
43
+
44
+ # Parse the page
45
+ soup = BeautifulSoup(driver.page_source, features="html.parser")
46
+ changes: Dict[str, str] = {}
47
+
48
+ # Find all tables with collection changes
49
+ tables = soup.find_all("table")
50
+ for table in tables:
51
+ # Check if this is a collection changes table
52
+ headers = [th.text.strip() for th in table.find_all("th")]
53
+ if "Normal collection day" in headers and "Revised collection day" in headers:
54
+ # Process each row
55
+ for row in table.find_all("tr")[1:]: # Skip header row
56
+ cols = row.find_all("td")
57
+ if len(cols) >= 2:
58
+ normal_date = cols[0].text.strip()
59
+ revised_date = cols[1].text.strip()
60
+
61
+ # Parse dates
62
+ try:
63
+ normal_date = parse(normal_date, fuzzy=True).strftime(
64
+ "%d/%m/%Y"
65
+ )
66
+ revised_date = parse(revised_date, fuzzy=True).strftime(
67
+ "%d/%m/%Y"
68
+ )
69
+ changes[normal_date] = revised_date
70
+ except Exception as e:
71
+ print(f"Error parsing dates: {e}")
72
+ continue
73
+
74
+ return changes
75
+
76
+
77
+ class CouncilClass(AbstractGetBinDataClass):
78
+ def parse_data(self, page: str, **kwargs: Any) -> Dict[str, Any]:
79
+ driver = None
80
+ try:
81
+ data: Dict[str, Any] = {"bins": []}
82
+ user_paon = kwargs.get("paon")
83
+ user_postcode = kwargs.get("postcode")
84
+ web_driver = kwargs.get("web_driver")
85
+ headless = kwargs.get("headless")
86
+ url = kwargs.get("url")
87
+
88
+ check_paon(user_paon)
89
+ check_postcode(user_postcode)
90
+
91
+ driver = create_webdriver(web_driver, headless, None, __name__)
92
+ driver.get(url)
93
+
94
+ # Handle cookie banner if present
95
+ wait = WebDriverWait(driver, 30)
96
+ try:
97
+ cookie_button = wait.until(
98
+ EC.element_to_be_clickable(
99
+ (
100
+ By.CLASS_NAME,
101
+ "btn btn--cookiemessage btn--cancel btn--contrast",
102
+ )
103
+ )
104
+ )
105
+ cookie_button.click()
106
+ except (TimeoutException, NoSuchElementException):
107
+ pass
108
+
109
+ # Enter postcode
110
+ post_code_input = wait.until(
111
+ EC.element_to_be_clickable(
112
+ (
113
+ By.ID,
114
+ "WASTECOLLECTIONDAYLOOKUPINCLUDEGARDEN_ADDRESSLOOKUPPOSTCODE",
115
+ )
116
+ )
117
+ )
118
+ post_code_input.clear()
119
+ post_code_input.send_keys(user_postcode)
120
+ post_code_input.send_keys(Keys.TAB + Keys.ENTER)
121
+
122
+ # Wait for address options to populate
123
+ try:
124
+ # Wait for the address dropdown to be present and clickable
125
+ address_select = wait.until(
126
+ EC.presence_of_element_located(
127
+ (
128
+ By.ID,
129
+ "WASTECOLLECTIONDAYLOOKUPINCLUDEGARDEN_ADDRESSLOOKUPADDRESS",
130
+ )
131
+ )
132
+ )
133
+
134
+ # Wait for actual address options to appear
135
+ wait.until(
136
+ lambda driver: len(driver.find_elements(By.TAG_NAME, "option")) > 1
137
+ )
138
+
139
+ # Find and select address
140
+ options = address_select.find_elements(By.TAG_NAME, "option")[
141
+ 1:
142
+ ] # Skip placeholder
143
+ if not options:
144
+ raise Exception(f"No addresses found for postcode: {user_postcode}")
145
+
146
+ # Normalize user input by keeping only alphanumeric characters
147
+ normalized_user_input = "".join(
148
+ c for c in user_paon if c.isalnum()
149
+ ).lower()
150
+
151
+ # Find matching address in dropdown
152
+ for option in options:
153
+ # Normalize option text by keeping only alphanumeric characters
154
+ normalized_option = "".join(
155
+ c for c in option.text if c.isalnum()
156
+ ).lower()
157
+ if normalized_user_input in normalized_option:
158
+ option.click()
159
+ break
160
+ except TimeoutException:
161
+ raise Exception("Timeout waiting for address options to populate")
162
+
163
+ # Wait for collection table and day text
164
+ wait.until(
165
+ EC.presence_of_element_located(
166
+ (By.ID, "WASTECOLLECTIONDAYLOOKUPINCLUDEGARDEN_COLLECTIONTABLE")
167
+ )
168
+ )
169
+
170
+ # Wait for collection day text to be fully populated
171
+ wait.until(
172
+ lambda driver: len(
173
+ driver.find_element(
174
+ By.ID, "WASTECOLLECTIONDAYLOOKUPINCLUDEGARDEN_COLLECTIONTABLE"
175
+ )
176
+ .find_elements(By.TAG_NAME, "tr")[2]
177
+ .find_elements(By.TAG_NAME, "td")[1]
178
+ .text.strip()
179
+ .split()
180
+ )
181
+ > 1
182
+ )
183
+
184
+ # Parse the table
185
+ soup = BeautifulSoup(driver.page_source, features="html.parser")
186
+ table = soup.find(
187
+ "div", id="WASTECOLLECTIONDAYLOOKUPINCLUDEGARDEN_COLLECTIONTABLE"
188
+ ).find("table")
189
+
190
+ # Get collection day
191
+ collection_day_text = table.find_all("tr")[2].find_all("td")[1].text.strip()
192
+ day_of_week = next(
193
+ (
194
+ day
195
+ for day in DAYS_OF_WEEK
196
+ if day.lower() in collection_day_text.lower()
197
+ ),
198
+ None,
199
+ )
200
+ if not day_of_week:
201
+ raise Exception(
202
+ f"Could not determine collection day from text: '{collection_day_text}'"
203
+ )
204
+
205
+ # Calculate next collection date
206
+ today = datetime.now()
207
+ days_ahead = (DAYS_OF_WEEK[day_of_week] - today.weekday()) % 7
208
+ if days_ahead == 0: # If today is collection day, get next week's date
209
+ days_ahead = 7
210
+ next_collection = today + timedelta(days=days_ahead)
211
+
212
+ # Add collection dates for each bin type
213
+ bin_types = ["General Waste", "Recycling", "Food Waste"]
214
+ for bin_type in bin_types:
215
+ data["bins"].append(
216
+ {
217
+ "type": bin_type,
218
+ "collectionDate": next_collection.strftime("%d/%m/%Y"),
219
+ }
220
+ )
221
+
222
+ # Process collection details
223
+ bin_rows = soup.select("div.bin--row:not(:first-child)")
224
+ for row in bin_rows:
225
+ try:
226
+ bin_type = row.select_one("div.col-md-3").text.strip()
227
+ collection_dates_div = row.select("div.col-md-3")[1]
228
+ next_collection_text = "".join(
229
+ collection_dates_div.find_all(text=True, recursive=False)
230
+ ).strip()
231
+ cleaned_date_text = remove_ordinal_indicator_from_date_string(
232
+ next_collection_text
233
+ )
234
+ parsed_date = parse(cleaned_date_text, fuzzy=True)
235
+ bin_date = parsed_date.strftime("%d/%m/%Y")
236
+
237
+ if bin_type and bin_date:
238
+ data["bins"].append(
239
+ {
240
+ "type": bin_type,
241
+ "collectionDate": bin_date,
242
+ }
243
+ )
244
+ except Exception as e:
245
+ print(f"Error processing item: {e}")
246
+ continue
247
+
248
+ # Get bank holiday changes
249
+ print("\nChecking for bank holiday collection changes...")
250
+ bank_holiday_changes = get_bank_holiday_changes(driver)
251
+
252
+ # Apply any bank holiday changes to collection dates
253
+ for bin_data in data["bins"]:
254
+ original_date = bin_data["collectionDate"]
255
+ if original_date in bank_holiday_changes:
256
+ new_date = bank_holiday_changes[original_date]
257
+ print(
258
+ f"Bank holiday change: {bin_data['type']} collection moved from {original_date} to {new_date}"
259
+ )
260
+ bin_data["collectionDate"] = new_date
261
+
262
+ except Exception as e:
263
+ print(f"An error occurred: {e}")
264
+ raise
265
+ finally:
266
+ if driver:
267
+ driver.quit()
268
+
269
+ # Print the final data dictionary for debugging
270
+ print("\nFinal data dictionary:")
271
+ print(json.dumps(data, indent=2))
272
+
273
+ return data
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: uk_bin_collection
3
- Version: 0.144.2
3
+ Version: 0.144.3
4
4
  Summary: Python Lib to collect UK Bin Data
5
5
  Author: Robert Bradley
6
6
  Author-email: robbrad182@gmail.com
@@ -3,7 +3,7 @@ uk_bin_collection/tests/check_selenium_url_in_input.json.py,sha256=Iecdja0I3XIiY
3
3
  uk_bin_collection/tests/council_feature_input_parity.py,sha256=DO6Mk4ImYgM5ZCZ-cutwz5RoYYWZRLYx2tr6zIs_9Rc,3843
4
4
  uk_bin_collection/tests/features/environment.py,sha256=VQZjJdJI_kZn08M0j5cUgvKT4k3iTw8icJge1DGOkoA,127
5
5
  uk_bin_collection/tests/features/validate_council_outputs.feature,sha256=SJK-Vc737hrf03tssxxbeg_JIvAH-ddB8f6gU1LTbuQ,251
6
- uk_bin_collection/tests/input.json,sha256=khL_z76OZZqmFtagerGv_djRNUYZC5iGAaLNTvttV2o,122435
6
+ uk_bin_collection/tests/input.json,sha256=zxDyP74ZRcogEGCcRCGeDj1EOU61mpQJXSsv8z5sznE,122869
7
7
  uk_bin_collection/tests/output.schema,sha256=ZwKQBwYyTDEM4G2hJwfLUVM-5v1vKRvRK9W9SS1sd18,1086
8
8
  uk_bin_collection/tests/step_defs/step_helpers/file_handler.py,sha256=Ygzi4V0S1MIHqbdstUlIqtRIwnynvhu4UtpweJ6-5N8,1474
9
9
  uk_bin_collection/tests/step_defs/test_validate_council.py,sha256=VZ0a81sioJULD7syAYHjvK_-nT_Rd36tUyzPetSA0gk,3475
@@ -146,6 +146,7 @@ uk_bin_collection/uk_bin_collection/councils/HerefordshireCouncil.py,sha256=JpQh
146
146
  uk_bin_collection/uk_bin_collection/councils/HertsmereBoroughCouncil.py,sha256=ZbSsmqHStd2JtTMAq1Bhcvsj1BYp6ijELyOjZFX2GSw,6435
147
147
  uk_bin_collection/uk_bin_collection/councils/HighPeakCouncil.py,sha256=x7dfy8mdt2iGl8qJxHb-uBh4u0knmi9MJ6irOJw9WYA,4805
148
148
  uk_bin_collection/uk_bin_collection/councils/HighlandCouncil.py,sha256=GNxDU65QuZHV5va2IrKtcJ6TQoDdwmV03JvkVqOauP4,3291
149
+ uk_bin_collection/uk_bin_collection/councils/Hillingdon.py,sha256=R1enDv5gjwCUT3HKgj8C87xWrwvrutAN6XLu5P7tef8,10532
149
150
  uk_bin_collection/uk_bin_collection/councils/HinckleyandBosworthBoroughCouncil.py,sha256=51vXTKrstfJhb7cLCcrsvA9qKCsptyNMZvy7ML9DasM,2344
150
151
  uk_bin_collection/uk_bin_collection/councils/HounslowCouncil.py,sha256=LXhJ47rujx7k3naz0tFiTT1l5k6gAYcVdekJN1t_HLY,4564
151
152
  uk_bin_collection/uk_bin_collection/councils/HullCityCouncil.py,sha256=UHcesBoctFVcXDYuwfag43KbcJcopkEDzJ-54NxtK0Q,1851
@@ -330,8 +331,8 @@ uk_bin_collection/uk_bin_collection/councils/YorkCouncil.py,sha256=I2kBYMlsD4bId
330
331
  uk_bin_collection/uk_bin_collection/councils/council_class_template/councilclasstemplate.py,sha256=EQWRhZ2pEejlvm0fPyOTsOHKvUZmPnxEYO_OWRGKTjs,1158
331
332
  uk_bin_collection/uk_bin_collection/create_new_council.py,sha256=m-IhmWmeWQlFsTZC4OxuFvtw5ZtB8EAJHxJTH4O59lQ,1536
332
333
  uk_bin_collection/uk_bin_collection/get_bin_data.py,sha256=YvmHfZqanwrJ8ToGch34x-L-7yPe31nB_x77_Mgl_vo,4545
333
- uk_bin_collection-0.144.2.dist-info/LICENSE,sha256=vABBUOzcrgfaTKpzeo-si9YVEun6juDkndqA8RKdKGs,1071
334
- uk_bin_collection-0.144.2.dist-info/METADATA,sha256=h2gau2rSpNnurR8hcPU1G1O4c69ZM0K3vMNL2tn85PM,19858
335
- uk_bin_collection-0.144.2.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
336
- uk_bin_collection-0.144.2.dist-info/entry_points.txt,sha256=36WCSGMWSc916S3Hi1ZkazzDKHaJ6CD-4fCEFm5MIao,90
337
- uk_bin_collection-0.144.2.dist-info/RECORD,,
334
+ uk_bin_collection-0.144.3.dist-info/LICENSE,sha256=vABBUOzcrgfaTKpzeo-si9YVEun6juDkndqA8RKdKGs,1071
335
+ uk_bin_collection-0.144.3.dist-info/METADATA,sha256=p836krZfbmZyZq_lEotYfXqB6JdLNtK3y6b4lhFIioM,19858
336
+ uk_bin_collection-0.144.3.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
337
+ uk_bin_collection-0.144.3.dist-info/entry_points.txt,sha256=36WCSGMWSc916S3Hi1ZkazzDKHaJ6CD-4fCEFm5MIao,90
338
+ uk_bin_collection-0.144.3.dist-info/RECORD,,