uk_bin_collection 0.153.0__py3-none-any.whl → 0.154.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 (21) hide show
  1. uk_bin_collection/tests/input.json +13 -15
  2. uk_bin_collection/uk_bin_collection/councils/BCPCouncil.py +45 -120
  3. uk_bin_collection/uk_bin_collection/councils/BasingstokeCouncil.py +4 -1
  4. uk_bin_collection/uk_bin_collection/councils/BrightonandHoveCityCouncil.py +15 -36
  5. uk_bin_collection/uk_bin_collection/councils/CastlepointDistrictCouncil.py +55 -24
  6. uk_bin_collection/uk_bin_collection/councils/ErewashBoroughCouncil.py +32 -34
  7. uk_bin_collection/uk_bin_collection/councils/FarehamBoroughCouncil.py +5 -2
  8. uk_bin_collection/uk_bin_collection/councils/FolkstoneandHytheDistrictCouncil.py +22 -0
  9. uk_bin_collection/uk_bin_collection/councils/GlasgowCityCouncil.py +1 -1
  10. uk_bin_collection/uk_bin_collection/councils/HartlepoolBoroughCouncil.py +3 -1
  11. uk_bin_collection/uk_bin_collection/councils/NorthHertfordshireDistrictCouncil.py +26 -128
  12. uk_bin_collection/uk_bin_collection/councils/NorthumberlandCouncil.py +63 -79
  13. uk_bin_collection/uk_bin_collection/councils/RushmoorCouncil.py +4 -2
  14. uk_bin_collection/uk_bin_collection/councils/SandwellBoroughCouncil.py +4 -11
  15. uk_bin_collection/uk_bin_collection/councils/SloughBoroughCouncil.py +39 -21
  16. uk_bin_collection/uk_bin_collection/councils/WestBerkshireCouncil.py +16 -13
  17. {uk_bin_collection-0.153.0.dist-info → uk_bin_collection-0.154.0.dist-info}/METADATA +1 -1
  18. {uk_bin_collection-0.153.0.dist-info → uk_bin_collection-0.154.0.dist-info}/RECORD +21 -21
  19. {uk_bin_collection-0.153.0.dist-info → uk_bin_collection-0.154.0.dist-info}/LICENSE +0 -0
  20. {uk_bin_collection-0.153.0.dist-info → uk_bin_collection-0.154.0.dist-info}/WHEEL +0 -0
  21. {uk_bin_collection-0.153.0.dist-info → uk_bin_collection-0.154.0.dist-info}/entry_points.txt +0 -0
@@ -102,12 +102,9 @@
102
102
  },
103
103
  "BCPCouncil": {
104
104
  "LAD24CD": "E06000058",
105
- "house_number": "3 HARBOUR VIEW ROAD, POOLE, BH14 0PD",
106
- "postcode": "BH14 0PD",
107
- "web_driver": "http://selenium:4444",
108
105
  "skip_get_url": true,
109
106
  "uprn": "100040810214",
110
- "url": "https://online.bcpcouncil.gov.uk/bindaylookup/",
107
+ "url": "https://bcpportal.bcpcouncil.gov.uk/checkyourbincollection",
111
108
  "wiki_name": "Bournemouth, Christchurch and Poole",
112
109
  "wiki_note": "You will need to use [FindMyAddress](https://www.findmyaddress.co.uk/search) to find the UPRN."
113
110
  },
@@ -888,7 +885,7 @@
888
885
  "ErewashBoroughCouncil": {
889
886
  "skip_get_url": true,
890
887
  "uprn": "10003582028",
891
- "url": "https://map.erewash.gov.uk/isharelive.web/myerewash.aspx",
888
+ "url": "https://www.erewash.gov.uk",
892
889
  "wiki_name": "Erewash",
893
890
  "wiki_note": "Pass the UPRN. You can find it using [FindMyAddress](https://www.findmyaddress.co.uk/search).",
894
891
  "LAD24CD": "E07000036"
@@ -995,7 +992,7 @@
995
992
  },
996
993
  "GlasgowCityCouncil": {
997
994
  "uprn": "906700034497",
998
- "url": "https://onlineservices.glasgow.gov.uk/forms/RefuseAndRecyclingWebApplication/AddressSearch.aspx",
995
+ "url": "https://onlineservices.glasgow.gov.uk/forms/refuseandrecyclingcalendar/AddressSearch.aspx",
999
996
  "skip_get_url": true,
1000
997
  "wiki_name": "Glasgow City",
1001
998
  "wiki_note": "You will need to use [FindMyAddress](https://www.findmyaddress.co.uk/search) to find the UPRN.",
@@ -1779,21 +1776,22 @@
1779
1776
  "LAD24CD": "E06000065"
1780
1777
  },
1781
1778
  "NorthumberlandCouncil": {
1782
- "house_number": "22",
1783
- "postcode": "NE46 1UQ",
1779
+ "uprn": "010096302588",
1780
+ "postcode": "NE65 0ZP",
1784
1781
  "skip_get_url": true,
1785
- "url": "https://www.northumberland.gov.uk/Waste/Household-waste/Household-bin-collections/Bin-Calendars.aspx",
1782
+ "url": "https://bincollection.northumberland.gov.uk/postcode",
1786
1783
  "web_driver": "http://selenium:4444",
1787
1784
  "wiki_name": "Northumberland",
1788
- "wiki_note": "Pass the house number and postcode in their respective parameters. This parser requires a Selenium webdriver.",
1785
+ "wiki_note": "Pass the UPRN. You can find it using [FindMyAddress](https://www.findmyaddress.co.uk/search).",
1789
1786
  "LAD24CD": "E06000057"
1790
1787
  },
1791
1788
  "NorwichCityCouncil": {
1792
- "uprn": "100090888980",
1793
- "url": "https://www.norwich.gov.uk",
1794
- "wiki_command_url_override": "https://www.norwich.gov.uk",
1789
+ "house_number": "2",
1790
+ "postcode": "NR2 3TT",
1791
+ "url": "https://bnr-wrp.whitespacews.com",
1792
+ "wiki_command_url_override": "hhttps://bnr-wrp.whitespacews.com",
1795
1793
  "wiki_name": "Norwich",
1796
- "wiki_note": "You will need to use [FindMyAddress](https://www.findmyaddress.co.uk/search) to find the UPRN.",
1794
+ "wiki_note": "Pass the house number and postcode in their respective parameters.",
1797
1795
  "LAD24CD": "E07000148"
1798
1796
  },
1799
1797
  "NottinghamCityCouncil": {
@@ -2804,4 +2802,4 @@
2804
2802
  "wiki_note": "Provide your UPRN.",
2805
2803
  "LAD24CD": "E06000014"
2806
2804
  }
2807
- }
2805
+ }
@@ -1,15 +1,13 @@
1
- import json
2
1
  import time
3
- from datetime import datetime
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
2
+
3
+ import requests
4
+ from dateutil.relativedelta import relativedelta
5
+
9
6
  from uk_bin_collection.uk_bin_collection.common import *
10
7
  from uk_bin_collection.uk_bin_collection.get_bin_data import AbstractGetBinDataClass
11
8
 
12
9
 
10
+ # import the wonderful Beautiful Soup and the URL grabber
13
11
  class CouncilClass(AbstractGetBinDataClass):
14
12
  """
15
13
  Concrete classes have to implement all abstract operations of the
@@ -18,116 +16,43 @@ class CouncilClass(AbstractGetBinDataClass):
18
16
  """
19
17
 
20
18
  def parse_data(self, page: str, **kwargs) -> dict:
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()
19
+ # Make a BS4 object
20
+ uprn = kwargs.get("uprn")
21
+ # usrn = kwargs.get("paon")
22
+ check_uprn(uprn)
23
+ # check_usrn(usrn)
24
+ bindata = {"bins": []}
25
+
26
+ # uprn = uprn.zfill(12)
27
+
28
+ API_URL = "https://prod-17.uksouth.logic.azure.com/workflows/58253d7b7d754447acf9fe5fcf76f493/triggers/manual/paths/invoke?api-version=2016-06-01&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=TAvYIUFj6dzaP90XQCm2ElY6Cd34ze05I3ba7LKTiBs"
29
+
30
+ headers = {
31
+ "Content-Type": "application/json",
32
+ "Accept": "*/*",
33
+ "User-Agent": "Mozilla/5.0",
34
+ "Referer": "https://bcpportal.bcpcouncil.gov.uk/",
35
+ }
36
+ s = requests.session()
37
+ data = {
38
+ "uprn": uprn,
39
+ }
40
+
41
+ r = s.post(API_URL, json=data, headers=headers)
42
+ r.raise_for_status()
43
+
44
+ data = r.json()
45
+ rows_data = data["data"]
46
+ for row in rows_data:
47
+ bin_type = row["wasteContainerUsageTypeDescription"]
48
+ collections = row["scheduleDateRange"]
49
+ for collection in collections:
50
+ dict_data = {
51
+ "type": bin_type,
52
+ "collectionDate": datetime.strptime(
53
+ collection, "%Y-%m-%d"
54
+ ).strftime(date_format),
55
+ }
56
+ bindata["bins"].append(dict_data)
57
+
58
+ return bindata
@@ -1,6 +1,8 @@
1
- from bs4 import BeautifulSoup
2
1
  from datetime import datetime
2
+
3
3
  import requests
4
+ from bs4 import BeautifulSoup
5
+
4
6
  from uk_bin_collection.uk_bin_collection.common import *
5
7
  from uk_bin_collection.uk_bin_collection.get_bin_data import AbstractGetBinDataClass
6
8
 
@@ -10,6 +12,7 @@ COLLECTION_KINDS = {
10
12
  "glass": "rteelem_ctl03_pnlCollections_Glass",
11
13
  # Garden waste data is only returned if the property is subscribed to the Garden Waste service
12
14
  "garden": "rteelem_ctl03_pnlCollections_GardenWaste",
15
+ "food": "rteelem_ctl03_pnlCollections_Food",
13
16
  }
14
17
 
15
18
 
@@ -72,7 +72,9 @@ class CouncilClass(AbstractGetBinDataClass):
72
72
  break
73
73
 
74
74
  if not found:
75
- raise Exception(f"Address containing '{user_paon}' not found in dropdown options")
75
+ raise Exception(
76
+ f"Address containing '{user_paon}' not found in dropdown options"
77
+ )
76
78
 
77
79
  submit_btn = wait.until(
78
80
  EC.presence_of_element_located(
@@ -84,7 +86,7 @@ class CouncilClass(AbstractGetBinDataClass):
84
86
 
85
87
  results = wait.until(
86
88
  EC.presence_of_element_located(
87
- (By.XPATH, f'//span[contains(@class,"collection-sub")]')
89
+ (By.XPATH, f'//div[contains(@class,"mx-name-listView1")]')
88
90
  )
89
91
  )
90
92
 
@@ -96,44 +98,21 @@ class CouncilClass(AbstractGetBinDataClass):
96
98
  current_date = datetime.now()
97
99
 
98
100
  # Find all elements with class starting with 'mx-name-index-'
99
- bins = soup.find_all(class_=lambda x: x and x.startswith("mx-name-index-"))
101
+ bin_view = soup.find(class_="mx-name-listView1")
102
+ bins = bin_view.find_all(
103
+ class_=lambda x: x and x.startswith("mx-name-index-")
104
+ )
100
105
 
101
106
  for bin_item in bins:
102
- bin_type = bin_item.find(class_="collection-main").text.strip()
103
- day_of_week_elements = bin_item.find_all(class_="collection-header")
104
- bin_date = None
105
-
106
- for elem in day_of_week_elements:
107
- if (
108
- elem.text.strip() != bin_type
109
- ): # Avoid taking the bin type as the date
110
- next_sibling = elem.find_next_sibling()
111
- if next_sibling:
112
- bin_date_str = next_sibling.text.strip()
113
- try:
114
- # Try parsing the date string in the format 'dd Month' (e.g., '30 Dec', '5 January')
115
- bin_date = datetime.strptime(bin_date_str, "%d %b")
116
- except ValueError:
117
- try:
118
- # If the above format fails, try 'dd MonthName' (e.g., '30 December', '5 January')
119
- bin_date = datetime.strptime(bin_date_str, "%d %B")
120
- except ValueError:
121
- pass
122
-
123
- if bin_date:
124
- # Set the year based on the logic provided
125
- if bin_date.month < current_date.month:
126
- bin_date = bin_date.replace(
127
- year=current_date.year + 1
128
- )
129
- else:
130
- bin_date = bin_date.replace(year=current_date.year)
131
- # Format the date to the desired format
132
- bin_date = bin_date.strftime("%d/%m/%Y")
133
- break
107
+ bin_type = bin_item.find(class_="mx-name-text31").text.strip()
108
+
109
+ bin_date_str = bin_item.find(class_="mx-name-text29").text.strip()
110
+
111
+ bin_date = datetime.strptime(bin_date_str, "%d %B %Y")
112
+ bin_date = bin_date.strftime(date_format)
113
+
134
114
  dict_data = {"type": bin_type, "collectionDate": bin_date}
135
115
  data["bins"].append(dict_data)
136
- print(data)
137
116
  except Exception as e:
138
117
  # Here you can log the exception if needed
139
118
  print(f"An error occurred: {e}")
@@ -26,7 +26,9 @@ class CouncilClass(AbstractGetBinDataClass):
26
26
  uprn = kwargs.get("uprn")
27
27
  check_uprn(uprn)
28
28
 
29
- post_url = "https://apps.castlepoint.gov.uk/cpapps/index.cfm?fa=myStreet.displayDetails"
29
+ base_url = "https://apps.castlepoint.gov.uk/cpapps/"
30
+
31
+ post_url = f"{base_url}index.cfm?fa=myStreet.displayDetails"
30
32
  post_header_str = (
31
33
  "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,"
32
34
  "image/apng,"
@@ -51,31 +53,60 @@ class CouncilClass(AbstractGetBinDataClass):
51
53
  soup = BeautifulSoup(post_response.text, features="html.parser")
52
54
  soup.prettify()
53
55
 
56
+ calMonthNext = f"{base_url}{soup.select_one("div.calMonthNext a")["href"]}"
57
+ nextmonth_response = requests.post(
58
+ calMonthNext, headers=post_headers, data=form_data, verify=False
59
+ )
60
+ soup_nextmonth = BeautifulSoup(nextmonth_response.text, features="html.parser")
61
+ soup_nextmonth.prettify()
62
+
54
63
  data = {"bins": []}
55
- collection_tuple = []
56
64
 
57
- calendar = soup.find("table", class_="calendar")
58
- month = datetime.strptime(
59
- soup.find("div", class_="calMonthCurrent").get_text(), "[%b]"
60
- ).strftime("%m")
61
- year = datetime.strptime(
62
- soup.find("h1").get_text(), "About my Street - %B %Y"
63
- ).strftime("%Y")
64
-
65
- pink_days = [
66
- day.get_text().strip() for day in calendar.find_all("td", class_="pink")
67
- ]
68
- black_days = [
69
- day.get_text().strip() for day in calendar.find_all("td", class_="normal")
70
- ]
71
-
72
- for day in pink_days:
73
- collection_date = datetime(year=int(year), month=int(month), day=int(day))
74
- collection_tuple.append(("Pink collection", collection_date))
75
-
76
- for day in black_days:
77
- collection_date = datetime(year=int(year), month=int(month), day=int(day))
78
- collection_tuple.append(("Normal collection", collection_date))
65
+ def parse_calendar_month(soup_one_month):
66
+ out = []
67
+
68
+ calendar = soup_one_month.find("table", class_="calendar")
69
+ if not calendar:
70
+ return out # be robust
71
+
72
+ # e.g. "[Aug]"
73
+ month_txt = soup_one_month.find("div", class_="calMonthCurrent").get_text(
74
+ strip=True
75
+ )
76
+ month = datetime.strptime(month_txt, "[%b]").strftime("%m")
77
+
78
+ # e.g. "About my Street - August 2025"
79
+ year_txt = soup_one_month.find("h1").get_text(strip=True)
80
+ year = datetime.strptime(year_txt, "About my Street - %B %Y").strftime("%Y")
81
+
82
+ pink_days = [
83
+ td.get_text(strip=True) for td in calendar.find_all("td", class_="pink")
84
+ ]
85
+ black_days = [
86
+ td.get_text(strip=True)
87
+ for td in calendar.find_all("td", class_="normal")
88
+ ]
89
+
90
+ for day in pink_days:
91
+ out.append(
92
+ (
93
+ "Pink collection",
94
+ datetime(year=int(year), month=int(month), day=int(day)),
95
+ )
96
+ )
97
+ for day in black_days:
98
+ out.append(
99
+ (
100
+ "Normal collection",
101
+ datetime(year=int(year), month=int(month), day=int(day)),
102
+ )
103
+ )
104
+
105
+ return out
106
+
107
+ collection_tuple = []
108
+ for s in (soup, soup_nextmonth):
109
+ collection_tuple.extend(parse_calendar_month(s))
79
110
 
80
111
  ordered_data = sorted(collection_tuple, key=lambda x: x[1])
81
112
 
@@ -1,4 +1,7 @@
1
+ import json
2
+
1
3
  from bs4 import BeautifulSoup
4
+
2
5
  from uk_bin_collection.uk_bin_collection.common import *
3
6
  from uk_bin_collection.uk_bin_collection.get_bin_data import AbstractGetBinDataClass
4
7
 
@@ -16,46 +19,41 @@ class CouncilClass(AbstractGetBinDataClass):
16
19
  uprn = kwargs.get("uprn")
17
20
  check_uprn(uprn)
18
21
 
22
+ label_map = {
23
+ "domestic-waste-collection-service": "Household Waste",
24
+ "recycling-collection-service": "Recycling",
25
+ "garden-waste-collection-service": "Garden Waste",
26
+ }
27
+
19
28
  requests.packages.urllib3.disable_warnings()
20
29
  response = requests.get(
21
- f"https://map.erewash.gov.uk/isharelive.web/myerewash.aspx?action=SetAddress&UniqueId={uprn}",
30
+ f"https://www.erewash.gov.uk/bbd-whitespace/one-year-collection-dates-without-christmas?uprn={uprn}",
22
31
  headers={"User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64)"},
23
32
  )
33
+ # Parse the JSON response
34
+ payload = response.json()
35
+ bin_collection = json.loads(payload) if isinstance(payload, str) else payload
24
36
 
25
- soup = BeautifulSoup(response.text, features="html.parser")
26
- collections = soup.find("div", {"aria-label": "Waste Collection"}).find_all(
27
- "div", {"class": "atPanelContent"}
37
+ cd = next(
38
+ i["settings"]["collection_dates"]
39
+ for i in bin_collection
40
+ if i.get("command") == "settings"
28
41
  )
29
- for c in collections:
30
- bin_type = c.find("h4").get_text(strip=True)
31
- if "my next" in bin_type.lower():
32
- collection_info = c.find("div", {"class": "atPanelData"}).get_text(
33
- strip=True
34
- )
35
- results = re.search(
36
- "([A-Za-z]+ \\d+[A-Za-z]+ [A-Za-z]+ \\d*)", collection_info
42
+
43
+ for month in cd.values():
44
+ for e in month:
45
+ d = e["date"] # "YYYY-MM-DD"
46
+ label = label_map.get(
47
+ e.get("service-identifier"),
48
+ e.get("service") or e.get("service-identifier"),
37
49
  )
38
- if results:
39
- collection_date = datetime.strptime(
40
- remove_ordinal_indicator_from_date_string(results[1]).strip(),
41
- "%A %d %B %Y",
42
- ).strftime(date_format)
43
- dict_data = {
44
- "type": bin_type.replace("My Next ", "").replace(
45
- " Collection", ""
46
- ),
47
- "collectionDate": collection_date,
48
- }
49
- data["bins"].append(dict_data)
50
- if "garden waste" in collection_info.lower():
51
- dict_data = {
52
- "type": "Garden Waste",
53
- "collectionDate": collection_date,
54
- }
55
- data["bins"].append(dict_data)
56
-
57
- data["bins"].sort(
58
- key=lambda x: datetime.strptime(x.get("collectionDate"), date_format)
59
- )
50
+
51
+ dict_data = {
52
+ "type": label,
53
+ "collectionDate": datetime.strptime(d, "%Y-%m-%d").strftime(
54
+ date_format
55
+ ),
56
+ }
57
+ data["bins"].append(dict_data)
60
58
 
61
59
  return data
@@ -38,11 +38,14 @@ class CouncilClass(AbstractGetBinDataClass):
38
38
  if "rows" in bin_data:
39
39
  collection_str = bin_data["rows"][0]["DomesticBinDay"]
40
40
 
41
- results = re.findall(r"(\d\d?\/\d\d?\/\d{4}) \((\w*)\)", collection_str)
41
+ results = re.findall(r'(\d{1,2}/\d{1,2}/\d{4}|today)\s*\(([^)]+)\)', collection_str)
42
42
 
43
43
  if results:
44
44
  for result in results:
45
- collection_date = datetime.strptime(result[0], "%d/%m/%Y")
45
+ if (result[0] == "today"):
46
+ collection_date = datetime.today()
47
+ else:
48
+ collection_date = datetime.strptime(result[0], "%d/%m/%Y")
46
49
  dict_data = {
47
50
  "type": result[1],
48
51
  "collectionDate": collection_date.strftime(date_format),
@@ -74,6 +74,28 @@ class CouncilClass(AbstractGetBinDataClass):
74
74
  }
75
75
  bindata["bins"].append(dict_data)
76
76
 
77
+ # Extract the Garden Waste schedule
78
+ garden_waste_section = soup.find(
79
+ "span", text=lambda x: x and "Garden Waste" in x
80
+ )
81
+ if garden_waste_section:
82
+ bin_types = garden_waste_section.text.replace("Garden Waste: ", "").split(
83
+ " / "
84
+ )
85
+ garden_waste_dates = garden_waste_section.find_next("ul").find_all("li")
86
+ for date in garden_waste_dates:
87
+ for bin_type in bin_types:
88
+ dict_data = {
89
+ "type": bin_type.strip(),
90
+ "collectionDate": datetime.strptime(
91
+ remove_ordinal_indicator_from_date_string(
92
+ date.text.strip()
93
+ ),
94
+ "%A %d %B %Y",
95
+ ).strftime("%d/%m/%Y"),
96
+ }
97
+ bindata["bins"].append(dict_data)
98
+
77
99
  bindata["bins"].sort(
78
100
  key=lambda x: datetime.strptime(x.get("collectionDate"), "%d/%m/%Y")
79
101
  )
@@ -18,7 +18,7 @@ class CouncilClass(AbstractGetBinDataClass):
18
18
  try:
19
19
  user_uprn = kwargs.get("uprn")
20
20
  check_uprn(user_uprn)
21
- url = f"https://onlineservices.glasgow.gov.uk/forms/RefuseAndRecyclingWebApplication/CollectionsCalendar.aspx?UPRN={user_uprn}"
21
+ url = f"https://onlineservices.glasgow.gov.uk/forms/refuseandrecyclingcalendar/CollectionsCalendar.aspx?UPRN={user_uprn}"
22
22
  if not user_uprn:
23
23
  # This is a fallback for if the user stored a URL in old system. Ensures backwards compatibility.
24
24
  url = kwargs.get("url")
@@ -73,7 +73,9 @@ class CouncilClass(AbstractGetBinDataClass):
73
73
  for div in soup.find_all("div"):
74
74
  # Extract bin type and date from the span tag
75
75
  text = div.find("span").text.strip()
76
- bin_type, date = text.split(" ", 1)
76
+ parts = text.split(" ")
77
+ date = parts[-1] # assume the last token is the date
78
+ bin_type = " ".join(parts[:-1])
77
79
  dict_data = {
78
80
  "type": bin_type,
79
81
  "collectionDate": date,