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.
- uk_bin_collection/tests/input.json +9 -0
- uk_bin_collection/uk_bin_collection/councils/Hillingdon.py +273 -0
- {uk_bin_collection-0.144.2.dist-info → uk_bin_collection-0.144.3.dist-info}/METADATA +1 -1
- {uk_bin_collection-0.144.2.dist-info → uk_bin_collection-0.144.3.dist-info}/RECORD +7 -6
- {uk_bin_collection-0.144.2.dist-info → uk_bin_collection-0.144.3.dist-info}/LICENSE +0 -0
- {uk_bin_collection-0.144.2.dist-info → uk_bin_collection-0.144.3.dist-info}/WHEEL +0 -0
- {uk_bin_collection-0.144.2.dist-info → uk_bin_collection-0.144.3.dist-info}/entry_points.txt +0 -0
@@ -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
|
@@ -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=
|
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.
|
334
|
-
uk_bin_collection-0.144.
|
335
|
-
uk_bin_collection-0.144.
|
336
|
-
uk_bin_collection-0.144.
|
337
|
-
uk_bin_collection-0.144.
|
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,,
|
File without changes
|
File without changes
|
{uk_bin_collection-0.144.2.dist-info → uk_bin_collection-0.144.3.dist-info}/entry_points.txt
RENAMED
File without changes
|