uk_bin_collection 0.138.1__py3-none-any.whl → 0.140.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 (32) hide show
  1. uk_bin_collection/tests/input.json +63 -26
  2. uk_bin_collection/uk_bin_collection/councils/AberdeenCityCouncil.py +2 -1
  3. uk_bin_collection/uk_bin_collection/councils/AberdeenshireCouncil.py +1 -0
  4. uk_bin_collection/uk_bin_collection/councils/ArdsAndNorthDownCouncil.py +1 -0
  5. uk_bin_collection/uk_bin_collection/councils/BarnsleyMBCouncil.py +1 -0
  6. uk_bin_collection/uk_bin_collection/councils/BroadlandDistrictCouncil.py +185 -0
  7. uk_bin_collection/uk_bin_collection/councils/BroxbourneCouncil.py +7 -3
  8. uk_bin_collection/uk_bin_collection/councils/CeredigionCountyCouncil.py +157 -0
  9. uk_bin_collection/uk_bin_collection/councils/CheltenhamBoroughCouncil.py +95 -61
  10. uk_bin_collection/uk_bin_collection/councils/CheshireEastCouncil.py +1 -0
  11. uk_bin_collection/uk_bin_collection/councils/CoventryCityCouncil.py +4 -1
  12. uk_bin_collection/uk_bin_collection/councils/ForestOfDeanDistrictCouncil.py +52 -41
  13. uk_bin_collection/uk_bin_collection/councils/GooglePublicCalendarCouncil.py +3 -4
  14. uk_bin_collection/uk_bin_collection/councils/LondonBoroughOfRichmondUponThames.py +11 -9
  15. uk_bin_collection/uk_bin_collection/councils/MiddlesbroughCouncil.py +13 -4
  16. uk_bin_collection/uk_bin_collection/councils/MonmouthshireCountyCouncil.py +5 -1
  17. uk_bin_collection/uk_bin_collection/councils/NewForestCouncil.py +1 -3
  18. uk_bin_collection/uk_bin_collection/councils/NorthDevonCountyCouncil.py +159 -0
  19. uk_bin_collection/uk_bin_collection/councils/NorwichCityCouncil.py +15 -3
  20. uk_bin_collection/uk_bin_collection/councils/NuneatonBedworthBoroughCouncil.py +873 -871
  21. uk_bin_collection/uk_bin_collection/councils/RugbyBoroughCouncil.py +1 -1
  22. uk_bin_collection/uk_bin_collection/councils/RushcliffeBoroughCouncil.py +3 -6
  23. uk_bin_collection/uk_bin_collection/councils/SouthHollandDistrictCouncil.py +136 -0
  24. uk_bin_collection/uk_bin_collection/councils/WalsallCouncil.py +6 -2
  25. uk_bin_collection/uk_bin_collection/councils/WalthamForest.py +1 -1
  26. uk_bin_collection/uk_bin_collection/councils/WestLindseyDistrictCouncil.py +6 -3
  27. uk_bin_collection/uk_bin_collection/councils/WychavonDistrictCouncil.py +1 -0
  28. {uk_bin_collection-0.138.1.dist-info → uk_bin_collection-0.140.0.dist-info}/METADATA +1 -1
  29. {uk_bin_collection-0.138.1.dist-info → uk_bin_collection-0.140.0.dist-info}/RECORD +32 -28
  30. {uk_bin_collection-0.138.1.dist-info → uk_bin_collection-0.140.0.dist-info}/LICENSE +0 -0
  31. {uk_bin_collection-0.138.1.dist-info → uk_bin_collection-0.140.0.dist-info}/WHEEL +0 -0
  32. {uk_bin_collection-0.138.1.dist-info → uk_bin_collection-0.140.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,159 @@
1
+ from time import sleep
2
+
3
+ from bs4 import BeautifulSoup
4
+ from selenium.webdriver.common.by import By
5
+ from selenium.webdriver.support import expected_conditions as EC
6
+ from selenium.webdriver.support.ui import Select, WebDriverWait
7
+
8
+ from uk_bin_collection.uk_bin_collection.common import *
9
+ from uk_bin_collection.uk_bin_collection.get_bin_data import AbstractGetBinDataClass
10
+
11
+
12
+ # import the wonderful Beautiful Soup and the URL grabber
13
+ class CouncilClass(AbstractGetBinDataClass):
14
+ """
15
+ Concrete classes have to implement all abstract operations of the base
16
+ class. They can also override some operations with a default
17
+ implementation.
18
+ """
19
+
20
+ def parse_data(self, page: str, **kwargs) -> dict:
21
+ driver = None
22
+ try:
23
+ user_uprn = kwargs.get("uprn")
24
+ user_postcode = kwargs.get("postcode")
25
+ web_driver = kwargs.get("web_driver")
26
+ headless = kwargs.get("headless")
27
+ check_uprn(user_uprn)
28
+ check_postcode(user_postcode)
29
+
30
+ # Create Selenium webdriver
31
+ driver = create_webdriver(web_driver, headless, None, __name__)
32
+ driver.get(
33
+ "https://my.northdevon.gov.uk/service/WasteRecyclingCollectionCalendar"
34
+ )
35
+
36
+ # Wait for iframe to load and switch to it
37
+ WebDriverWait(driver, 30).until(
38
+ EC.frame_to_be_available_and_switch_to_it((By.ID, "fillform-frame-1"))
39
+ )
40
+
41
+ # Wait for postcode entry box
42
+ postcode = WebDriverWait(driver, 10).until(
43
+ EC.presence_of_element_located((By.ID, "postcode_search"))
44
+ )
45
+ # Enter postcode
46
+ postcode.send_keys(user_postcode.replace(" ", ""))
47
+
48
+ # Wait for address selection dropdown to appear
49
+ address = Select(
50
+ WebDriverWait(driver, 10).until(
51
+ EC.visibility_of_element_located((By.ID, "chooseAddress"))
52
+ )
53
+ )
54
+
55
+ # Wait for spinner to disappear (signifies options are loaded for select)
56
+ WebDriverWait(driver, 10).until(
57
+ EC.invisibility_of_element_located(
58
+ (By.CLASS_NAME, "spinner-outer")
59
+ ) # row-fluid spinner-outer
60
+ )
61
+
62
+ # Sometimes the options aren't fully there despite the spinner being gone, wait another 2 seconds.
63
+ sleep(2)
64
+
65
+ # Select address by UPRN
66
+ address.select_by_value(user_uprn)
67
+
68
+ # Wait for spinner to disappear (signifies data is loaded)
69
+ WebDriverWait(driver, 10).until(
70
+ EC.invisibility_of_element_located((By.CLASS_NAME, "spinner-outer"))
71
+ )
72
+
73
+ sleep(2)
74
+
75
+ address_confirmation = WebDriverWait(driver, 10).until(
76
+ EC.presence_of_element_located(
77
+ (By.XPATH, "//h2[contains(text(), 'Your address')]")
78
+ )
79
+ )
80
+
81
+ next_button = WebDriverWait(driver, 10).until(
82
+ EC.presence_of_element_located(
83
+ (By.XPATH, "//button/span[contains(@class, 'nextText')]")
84
+ )
85
+ )
86
+
87
+ next_button.click()
88
+
89
+ results = WebDriverWait(driver, 10).until(
90
+ EC.presence_of_element_located(
91
+ (By.XPATH, "//h4[contains(text(), 'Key')]")
92
+ )
93
+ )
94
+
95
+ # Find data table
96
+ data_table = WebDriverWait(driver, 10).until(
97
+ EC.presence_of_element_located(
98
+ (
99
+ By.XPATH,
100
+ '//div[@data-field-name="html1"]/div[contains(@class, "fieldContent")]',
101
+ )
102
+ )
103
+ )
104
+
105
+ # Make a BS4 object
106
+ soup = BeautifulSoup(
107
+ data_table.get_attribute("innerHTML"), features="html.parser"
108
+ )
109
+
110
+ # Initialize the data dictionary
111
+ data = {"bins": []}
112
+
113
+ # Loop through each list of waste dates
114
+ waste_sections = soup.find_all("ul", class_="wasteDates")
115
+
116
+ current_month_year = None
117
+
118
+ for section in waste_sections:
119
+ for li in section.find_all("li", recursive=False):
120
+ if "MonthLabel" in li.get("class", []):
121
+ # Extract month and year (e.g., "April 2025")
122
+ header = li.find("h4")
123
+ if header:
124
+ current_month_year = header.text.strip()
125
+ elif any(
126
+ bin_class in li.get("class", [])
127
+ for bin_class in ["BlackBin", "GreenBin", "Recycling"]
128
+ ):
129
+ bin_type = li.find("span", class_="wasteType").text.strip()
130
+ day = li.find("span", class_="wasteDay").text.strip()
131
+ weekday = li.find("span", class_="wasteName").text.strip()
132
+
133
+ if current_month_year and day:
134
+ try:
135
+ full_date = f"{day} {current_month_year}"
136
+ collection_date = datetime.strptime(
137
+ full_date, "%d %B %Y"
138
+ ).strftime(date_format)
139
+ dict_data = {
140
+ "type": bin_type,
141
+ "collectionDate": collection_date,
142
+ }
143
+ data["bins"].append(dict_data)
144
+ except Exception as e:
145
+ print(f"Skipping invalid date '{full_date}': {e}")
146
+
147
+ data["bins"].sort(
148
+ key=lambda x: datetime.strptime(x.get("collectionDate"), date_format)
149
+ )
150
+ except Exception as e:
151
+ # Here you can log the exception if needed
152
+ print(f"An error occurred: {e}")
153
+ # Optionally, re-raise the exception if you want it to propagate
154
+ raise
155
+ finally:
156
+ # This block ensures that the driver is closed regardless of an exception
157
+ if driver:
158
+ driver.quit()
159
+ return data
@@ -53,7 +53,12 @@ class CouncilClass(AbstractGetBinDataClass):
53
53
  if alternateCheck:
54
54
  bin_types = strong[2].text.strip().replace(".", "").split(" and ")
55
55
  for bin in bin_types:
56
- collections.append((bin.capitalize(), datetime.strptime(strong[1].text.strip(), date_format)))
56
+ collections.append(
57
+ (
58
+ bin.capitalize(),
59
+ datetime.strptime(strong[1].text.strip(), date_format),
60
+ )
61
+ )
57
62
 
58
63
  else:
59
64
  p_tag = soup.find_all("p")
@@ -63,11 +68,18 @@ class CouncilClass(AbstractGetBinDataClass):
63
68
  p.text.split("Your ")[1].split(" is collected")[0].split(" and ")
64
69
  )
65
70
  for bin in bin_types:
66
- collections.append((bin.capitalize(), datetime.strptime(strong[1].text.strip(), date_format)))
71
+ collections.append(
72
+ (
73
+ bin.capitalize(),
74
+ datetime.strptime(strong[1].text.strip(), date_format),
75
+ )
76
+ )
67
77
  i += 2
68
78
 
69
79
  if len(strong) > 3:
70
- collections.append(("Garden", datetime.strptime(strong[4].text.strip(), date_format)))
80
+ collections.append(
81
+ ("Garden", datetime.strptime(strong[4].text.strip(), date_format))
82
+ )
71
83
 
72
84
  ordered_data = sorted(collections, key=lambda x: x[1])
73
85
  for item in ordered_data: