uk_bin_collection 0.154.0__py3-none-any.whl → 0.158.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 (22) hide show
  1. uk_bin_collection/tests/input.json +21 -10
  2. uk_bin_collection/uk_bin_collection/councils/AberdeenCityCouncil.py +0 -1
  3. uk_bin_collection/uk_bin_collection/councils/DacorumBoroughCouncil.py +22 -13
  4. uk_bin_collection/uk_bin_collection/councils/EastDunbartonshireCouncil.py +52 -0
  5. uk_bin_collection/uk_bin_collection/councils/IslingtonCouncil.py +8 -5
  6. uk_bin_collection/uk_bin_collection/councils/LancasterCityCouncil.py +23 -10
  7. uk_bin_collection/uk_bin_collection/councils/LondonBoroughSutton.py +60 -49
  8. uk_bin_collection/uk_bin_collection/councils/MidSuffolkDistrictCouncil.py +70 -92
  9. uk_bin_collection/uk_bin_collection/councils/NewForestCouncil.py +104 -47
  10. uk_bin_collection/uk_bin_collection/councils/NewportCityCouncil.py +138 -21
  11. uk_bin_collection/uk_bin_collection/councils/NorthumberlandCouncil.py +182 -3
  12. uk_bin_collection/uk_bin_collection/councils/OxfordCityCouncil.py +1 -0
  13. uk_bin_collection/uk_bin_collection/councils/RenfrewshireCouncil.py +170 -13
  14. uk_bin_collection/uk_bin_collection/councils/RotherhamCouncil.py +70 -38
  15. uk_bin_collection/uk_bin_collection/councils/SomersetCouncil.py +136 -21
  16. uk_bin_collection/uk_bin_collection/councils/SouthGloucestershireCouncil.py +18 -22
  17. uk_bin_collection/uk_bin_collection/councils/TestValleyBoroughCouncil.py +138 -21
  18. {uk_bin_collection-0.154.0.dist-info → uk_bin_collection-0.158.0.dist-info}/METADATA +1 -1
  19. {uk_bin_collection-0.154.0.dist-info → uk_bin_collection-0.158.0.dist-info}/RECORD +22 -21
  20. {uk_bin_collection-0.154.0.dist-info → uk_bin_collection-0.158.0.dist-info}/LICENSE +0 -0
  21. {uk_bin_collection-0.154.0.dist-info → uk_bin_collection-0.158.0.dist-info}/WHEEL +0 -0
  22. {uk_bin_collection-0.154.0.dist-info → uk_bin_collection-0.158.0.dist-info}/entry_points.txt +0 -0
@@ -760,6 +760,13 @@
760
760
  "wiki_note": "Pass the UPRN. You can find it using [FindMyAddress](https://www.findmyaddress.co.uk/search).",
761
761
  "LAD24CD": "E07000040"
762
762
  },
763
+ "EastDunbartonshireCouncil": {
764
+ "uprn": "132027197",
765
+ "url": "https://www.eastdunbarton.gov.uk/",
766
+ "wiki_name": "East Dunbartonshire",
767
+ "wiki_note": "You will need to use [FindMyAddress](https://www.findmyaddress.co.uk/search) to find the UPRN.",
768
+ "LAD24CD": "S12000045"
769
+ },
763
770
  "EastHertsCouncil": {
764
771
  "LAD24CD": "E07000097",
765
772
  "skip_get_url": true,
@@ -1608,7 +1615,7 @@
1608
1615
  "postcode": "SO41 0GJ",
1609
1616
  "skip_get_url": true,
1610
1617
  "uprn": "100060482345",
1611
- "url": "https://forms.newforest.gov.uk/id/FIND_MY_COLLECTION",
1618
+ "url": "https://forms.newforest.gov.uk/ufs/FIND_MY_BIN_BAR.eb",
1612
1619
  "web_driver": "http://selenium:4444",
1613
1620
  "wiki_name": "New Forest",
1614
1621
  "wiki_note": "Pass the postcode and UPRN. This parser requires a Selenium webdriver.",
@@ -1647,10 +1654,11 @@
1647
1654
  "NewportCityCouncil": {
1648
1655
  "postcode": "NP20 4HE",
1649
1656
  "skip_get_url": true,
1650
- "uprn": "100100688837",
1657
+ "house_number": "6",
1651
1658
  "url": "https://www.newport.gov.uk/",
1659
+ "web_driver": "http://selenium:4444",
1652
1660
  "wiki_name": "Newport",
1653
- "wiki_note": "Pass the postcode and UPRN. You can find the UPRN using [FindMyAddress](https://www.findmyaddress.co.uk/search).",
1661
+ "wiki_note": "Pass the postcode and house number in their respective arguments, both wrapped in quotes.",
1654
1662
  "LAD24CD": "W06000022"
1655
1663
  },
1656
1664
  "NorthAyrshireCouncil": {
@@ -2087,10 +2095,11 @@
2087
2095
  "SomersetCouncil": {
2088
2096
  "postcode": "TA6 4AA",
2089
2097
  "skip_get_url": true,
2090
- "uprn": "10090857775",
2098
+ "house_number": "5",
2091
2099
  "url": "https://www.somerset.gov.uk/",
2100
+ "web_driver": "http://selenium:4444",
2092
2101
  "wiki_name": "Somerset",
2093
- "wiki_note": "Provide your UPRN and postcode. Find your UPRN using [FindMyAddress](https://www.findmyaddress.co.uk/search).",
2102
+ "wiki_note": "Provide your house number and postcode",
2094
2103
  "LAD24CD": "E06000066"
2095
2104
  },
2096
2105
  "SouthAyrshireCouncil": {
@@ -2122,7 +2131,7 @@
2122
2131
  "SouthGloucestershireCouncil": {
2123
2132
  "skip_get_url": true,
2124
2133
  "uprn": "566419",
2125
- "url": "https://beta.southglos.gov.uk/waste-and-recycling-collection-date",
2134
+ "url": "https://api.southglos.gov.uk/wastecomp/GetCollectionDetails",
2126
2135
  "wiki_name": "South Gloucestershire",
2127
2136
  "wiki_note": "Provide your UPRN. You can find it using [FindMyAddress](https://www.findmyaddress.co.uk/search).",
2128
2137
  "LAD24CD": "E06000025"
@@ -2415,12 +2424,13 @@
2415
2424
  "LAD24CD": "E07000076"
2416
2425
  },
2417
2426
  "TestValleyBoroughCouncil": {
2418
- "postcode": "SO51 9ZD",
2427
+ "postcode": "SO51 0BY",
2419
2428
  "skip_get_url": true,
2420
- "uprn": "200010012019",
2421
- "url": "https://testvalley.gov.uk/wasteandrecycling/when-are-my-bins-collected",
2429
+ "house_number": "2",
2430
+ "url": "https://testvalley.gov.uk/wasteandrecycling/when-are-my-bins-collected/when-are-my-bins-collected",
2431
+ "web_driver": "http://selenium:4444",
2422
2432
  "wiki_name": "Test Valley",
2423
- "wiki_note": "Provide your UPRN and postcode. Use [FindMyAddress](https://www.findmyaddress.co.uk/search) to find your UPRN.",
2433
+ "wiki_note": "Provide your house number and postcode",
2424
2434
  "LAD24CD": "E07000093"
2425
2435
  },
2426
2436
  "ThanetDistrictCouncil": {
@@ -2463,6 +2473,7 @@
2463
2473
  "skip_get_url": true,
2464
2474
  "uprn": "10000016984",
2465
2475
  "postcode": "TQ1 1AG",
2476
+ "web_driver": "http://selenium:4444",
2466
2477
  "url": "https://www.torbay.gov.uk/recycling/bin-collections/",
2467
2478
  "wiki_name": "Torbay",
2468
2479
  "wiki_note": "Provide your UPRN. Use [FindMyAddress](https://www.findmyaddress.co.uk/search) to find it.",
@@ -5,7 +5,6 @@ import requests
5
5
  from uk_bin_collection.uk_bin_collection.common import *
6
6
  from uk_bin_collection.uk_bin_collection.get_bin_data import AbstractGetBinDataClass
7
7
 
8
-
9
8
  # import the wonderful Beautiful Soup and the URL grabber
10
9
  class CouncilClass(AbstractGetBinDataClass):
11
10
  """
@@ -76,19 +76,28 @@ class CouncilClass(AbstractGetBinDataClass):
76
76
  )
77
77
 
78
78
  for Collection in NextCollections:
79
- BinType = Collection.find("strong").text.strip()
80
- if BinType:
81
- CollectionDate = datetime.strptime(
82
- Collection.find_all("div", {"style": "display:table-cell;"})[1]
83
- .get_text()
84
- .strip(),
85
- "%a, %d %b %Y",
86
- )
87
- dict_data = {
88
- "type": BinType,
89
- "collectionDate": CollectionDate.strftime("%d/%m/%Y"),
90
- }
91
- data["bins"].append(dict_data)
79
+ strong_element = Collection.find("strong")
80
+ if strong_element:
81
+ BinType = strong_element.text.strip()
82
+ # Skip if this is not a bin type (e.g., informational text)
83
+ if BinType and not any(skip_text in BinType.lower() for skip_text in
84
+ ["please note", "we may collect", "bank holiday", "different day"]):
85
+ date_cells = Collection.find_all("div", {"style": "display:table-cell;"})
86
+ if len(date_cells) > 1:
87
+ date_text = date_cells[1].get_text().strip()
88
+ if date_text:
89
+ try:
90
+ CollectionDate = datetime.strptime(date_text, "%a, %d %b %Y")
91
+ dict_data = {
92
+ "type": BinType,
93
+ "collectionDate": CollectionDate.strftime("%d/%m/%Y"),
94
+ }
95
+ # Check for duplicates before adding
96
+ if dict_data not in data["bins"]:
97
+ data["bins"].append(dict_data)
98
+ except ValueError:
99
+ # Skip if date parsing fails
100
+ continue
92
101
 
93
102
  except Exception as e:
94
103
  # Here you can log the exception if needed
@@ -0,0 +1,52 @@
1
+ import requests
2
+ from bs4 import BeautifulSoup, Tag
3
+
4
+ from uk_bin_collection.uk_bin_collection.common import *
5
+ from uk_bin_collection.uk_bin_collection.get_bin_data import AbstractGetBinDataClass
6
+
7
+
8
+ # import the wonderful Beautiful Soup and the URL grabber
9
+ class CouncilClass(AbstractGetBinDataClass):
10
+ """
11
+ Concrete classes have to implement all abstract operations of the
12
+ base class. They can also override some operations with a default
13
+ implementation.
14
+ """
15
+
16
+ def parse_data(self, page: str, **kwargs) -> dict:
17
+
18
+ user_uprn = kwargs.get("uprn")
19
+ check_uprn(user_uprn)
20
+ bindata = {"bins": []}
21
+
22
+ URI = f"https://www.eastdunbarton.gov.uk/services/a-z-of-services/bins-waste-and-recycling/bins-and-recycling/collections/?uprn={user_uprn}"
23
+
24
+ # Make the GET request
25
+ response = requests.get(URI)
26
+
27
+ soup = BeautifulSoup(response.text, "html.parser")
28
+
29
+ table = soup.find("table", {"class": "bin-table"})
30
+
31
+ tbody = table.find("tbody")
32
+
33
+ trs = tbody.find_all("tr")
34
+
35
+ for tr in trs:
36
+ tds = tr.find_all("td")
37
+ bin_type = tds[0].get_text()
38
+ collection_date_str = tds[1].find("span").get_text()
39
+
40
+ collection_date = datetime.strptime(collection_date_str, "%A, %d %B %Y")
41
+
42
+ dict_data = {
43
+ "type": bin_type,
44
+ "collectionDate": collection_date.strftime(date_format),
45
+ }
46
+ bindata["bins"].append(dict_data)
47
+
48
+ bindata["bins"].sort(
49
+ key=lambda x: datetime.strptime(x.get("collectionDate"), date_format)
50
+ )
51
+
52
+ return bindata
@@ -17,11 +17,14 @@ class CouncilClass(AbstractGetBinDataClass):
17
17
 
18
18
  data = {"bins": []}
19
19
 
20
- waste_table = (
21
- soup.find(string="Waste and recycling collections")
22
- .find_next("div", class_="m-toggle-content")
23
- .find("table")
24
- )
20
+ # Find the waste and recycling section with proper null checking
21
+ waste_section = soup.find(string="Waste and recycling collections")
22
+ waste_table = None
23
+
24
+ if waste_section:
25
+ toggle_content = waste_section.find_next("div", class_="m-toggle-content")
26
+ if toggle_content:
27
+ waste_table = toggle_content.find("table")
25
28
 
26
29
  if waste_table:
27
30
  rows = waste_table.find_all("tr")
@@ -57,17 +57,30 @@ class CouncilClass(AbstractGetBinDataClass):
57
57
  response = session.get(addr_link)
58
58
  new_soup = BeautifulSoup(response.text, features="html.parser")
59
59
  services = new_soup.find("section", {"id": "scheduled-collections"})
60
+
61
+ if services is None:
62
+ raise Exception("Could not find scheduled collections section on the page")
63
+
60
64
  services_sub = services.find_all("li")
65
+ if not services_sub:
66
+ raise Exception("No collection services found")
67
+
61
68
  for i in range(0, len(services_sub), 3):
62
- dt = datetime.strptime(services_sub[i + 1].text.strip(), "%d/%m/%Y").date()
63
- bin_type = BeautifulSoup(services_sub[i + 2].text, features="lxml").find(
64
- "p"
65
- )
66
- data["bins"].append(
67
- {
68
- "type": bin_type.text.strip().removesuffix(" Collection Service"),
69
- "collectionDate": dt.strftime(date_format),
70
- }
71
- )
69
+ if i + 2 < len(services_sub):
70
+ date_text = services_sub[i + 1].text.strip() if services_sub[i + 1] else None
71
+ if date_text:
72
+ try:
73
+ dt = datetime.strptime(date_text, "%d/%m/%Y").date()
74
+ bin_type_element = BeautifulSoup(services_sub[i + 2].text, features="lxml").find("p")
75
+ if bin_type_element and bin_type_element.text:
76
+ data["bins"].append(
77
+ {
78
+ "type": bin_type_element.text.strip().removesuffix(" Collection Service"),
79
+ "collectionDate": dt.strftime(date_format),
80
+ }
81
+ )
82
+ except (ValueError, AttributeError) as e:
83
+ # Skip invalid date or missing elements
84
+ continue
72
85
 
73
86
  return data
@@ -1,30 +1,24 @@
1
- from time import sleep
2
-
3
1
  import requests
4
2
  from bs4 import BeautifulSoup
3
+ from datetime import datetime
4
+ import re
5
+ from time import sleep
5
6
 
6
7
  from uk_bin_collection.uk_bin_collection.common import *
7
8
  from uk_bin_collection.uk_bin_collection.get_bin_data import AbstractGetBinDataClass
8
9
 
10
+ def remove_ordinal_indicator_from_date_string(date_str):
11
+ return re.sub(r'(\d+)(st|nd|rd|th)', r'\1', date_str)
9
12
 
10
- # import the wonderful Beautiful Soup and the URL grabber
11
13
  class CouncilClass(AbstractGetBinDataClass):
12
- """
13
- Concrete classes have to implement all abstract operations of the
14
- base class. They can also override some operations with a default
15
- implementation.
16
- """
17
14
 
18
15
  def parse_data(self, page: str, **kwargs) -> dict:
19
-
20
16
  user_uprn = kwargs.get("uprn")
21
- # check_uprn(user_uprn)
22
17
  bindata = {"bins": []}
23
18
 
24
19
  URI = f"https://waste-services.sutton.gov.uk/waste/{user_uprn}"
25
20
 
26
21
  s = requests.Session()
27
-
28
22
  r = s.get(URI)
29
23
  while "Loading your bin days..." in r.text:
30
24
  sleep(2)
@@ -32,48 +26,65 @@ class CouncilClass(AbstractGetBinDataClass):
32
26
  r.raise_for_status()
33
27
 
34
28
  soup = BeautifulSoup(r.content, "html.parser")
35
-
36
29
  current_year = datetime.now().year
37
30
  next_year = current_year + 1
38
31
 
39
- services = soup.find_all("h3", class_="govuk-heading-m waste-service-name")
40
-
32
+ # Find all h3 headers (bin types)
33
+ services = soup.find_all("h3")
41
34
  for service in services:
42
- bin_type = service.get_text(
43
- strip=True
44
- ) # Bin type name (e.g., 'Food waste', 'Mixed recycling')
45
- if bin_type == "Bulky Waste":
35
+ bin_type = service.get_text(strip=True)
36
+ if "Bulky Waste" in bin_type:
46
37
  continue
47
- service_details = service.find_next("div", class_="govuk-grid-row")
48
-
49
- next_collection = (
50
- (
51
- service_details.find("dt", string="Next collection")
52
- .find_next_sibling("dd")
53
- .get_text(strip=True)
54
- )
55
- .replace("(this collection has been adjusted from its usual time)", "")
56
- .strip()
57
- )
58
-
59
- next_collection = datetime.strptime(
60
- remove_ordinal_indicator_from_date_string(next_collection),
61
- "%A, %d %B",
62
- )
63
-
64
- if (datetime.now().month == 12) and (next_collection.month == 1):
65
- next_collection = next_collection.replace(year=next_year)
66
- else:
67
- next_collection = next_collection.replace(year=current_year)
68
-
69
- dict_data = {
70
- "type": bin_type,
71
- "collectionDate": next_collection.strftime("%d/%m/%Y"),
72
- }
73
- bindata["bins"].append(dict_data)
74
-
75
- bindata["bins"].sort(
76
- key=lambda x: datetime.strptime(x.get("collectionDate"), "%d/%m/%Y")
77
- )
78
38
 
39
+ # Find the next element (next sibling) which is likely a paragraph with date info
40
+ next_sib = service.find_next_sibling()
41
+ while next_sib and getattr(next_sib, 'name', None) not in [None, 'p']:
42
+ next_sib = next_sib.find_next_sibling()
43
+
44
+ next_coll = None
45
+ if next_sib:
46
+ text = next_sib.get_text() if hasattr(next_sib, 'get_text') else str(next_sib)
47
+ match = re.search(r"Next collection\s*([A-Za-z]+,? \d{1,2}(?:st|nd|rd|th)? [A-Za-z]+)", text)
48
+ if match:
49
+ next_coll = match.group(1)
50
+ else:
51
+ # Sometimes the text may be attached without a space after 'Next collection'
52
+ match = re.search(r"Next collection([A-Za-z]+,? \d{1,2}(?:st|nd|rd|th)? [A-Za-z]+)", text)
53
+ if match:
54
+ next_coll = match.group(1)
55
+
56
+ # Try several siblings forward if not found
57
+ if not next_coll:
58
+ sib_try = service
59
+ for _ in range(3):
60
+ if sib_try:
61
+ sib_try = sib_try.find_next_sibling()
62
+ else:
63
+ break
64
+ if sib_try:
65
+ text = sib_try.get_text() if hasattr(sib_try, 'get_text') else str(sib_try)
66
+ match = re.search(r"Next collection\s*([A-Za-z]+,? \d{1,2}(?:st|nd|rd|th)? [A-Za-z]+)", text)
67
+ if match:
68
+ next_coll = match.group(1)
69
+ break
70
+
71
+ if next_coll:
72
+ next_coll = remove_ordinal_indicator_from_date_string(next_coll)
73
+ try:
74
+ next_collection = datetime.strptime(next_coll, "%A, %d %B")
75
+ except ValueError:
76
+ continue
77
+
78
+ if (datetime.now().month == 12 and next_collection.month == 1):
79
+ next_collection = next_collection.replace(year=next_year)
80
+ else:
81
+ next_collection = next_collection.replace(year=current_year)
82
+
83
+ dict_data = {
84
+ "type": bin_type,
85
+ "collectionDate": next_collection.strftime("%d/%m/%Y"),
86
+ }
87
+ bindata["bins"].append(dict_data)
88
+
89
+ bindata["bins"].sort(key=lambda x: datetime.strptime(x.get("collectionDate"), "%d/%m/%Y"))
79
90
  return bindata
@@ -1,6 +1,7 @@
1
1
  import re
2
2
  import time
3
3
 
4
+ import holidays
4
5
  import requests
5
6
  from bs4 import BeautifulSoup
6
7
  from selenium.webdriver.common.by import By
@@ -50,58 +51,63 @@ class CouncilClass(AbstractGetBinDataClass):
50
51
  refuse_dates = get_dates_every_x_days(refusestartDate, 14, 28)
51
52
  recycling_dates = get_dates_every_x_days(recyclingstartDate, 14, 28)
52
53
 
53
- bank_holidays = [
54
- ("25/12/2024", 2),
55
- ("26/12/2024", 2),
56
- ("27/12/2024", 3),
57
- ("30/12/2024", 1),
58
- ("31/12/2024", 2),
59
- ("01/01/2025", 2),
60
- ("02/01/2025", 2),
61
- ("03/01/2025", 3),
62
- ("06/01/2025", 1),
63
- ("07/01/2025", 1),
64
- ("08/01/2025", 1),
65
- ("09/01/2025", 1),
66
- ("10/01/2025", 1),
67
- ("18/04/2025", 1),
68
- ("21/04/2025", 1),
69
- ("22/04/2025", 1),
70
- ("23/04/2025", 1),
71
- ("24/04/2025", 1),
72
- ("25/04/2025", 1),
73
- ("05/05/2025", 1),
74
- ("06/05/2025", 1),
75
- ("07/05/2025", 1),
76
- ("08/05/2025", 1),
77
- ("09/05/2025", 1),
78
- ("26/05/2025", 1),
79
- ("27/05/2025", 1),
80
- ("28/05/2025", 1),
81
- ("29/05/2025", 1),
82
- ("30/05/2025", 1),
83
- ("25/08/2025", 1),
84
- ("26/08/2025", 1),
85
- ("27/08/2025", 1),
86
- ("28/08/2025", 1),
87
- ("29/08/2025", 1),
88
- ]
54
+ # Generate bank holidays dynamically using the holidays library
55
+ def get_bank_holidays_set():
56
+ """Get set of bank holiday dates for quick lookup."""
57
+ current_year = datetime.now().year
58
+ uk_holidays = holidays.UK(years=range(current_year - 1, current_year + 3))
59
+ return set(uk_holidays.keys())
60
+
61
+ def find_next_collection_day(original_date):
62
+ """Find the next valid collection day, avoiding weekends and bank holidays."""
63
+ bank_holiday_dates = get_bank_holidays_set()
64
+ check_date = datetime.strptime(original_date, "%d/%m/%Y")
65
+
66
+ # Safety limit to prevent infinite loops
67
+ max_attempts = 10
68
+ attempts = 0
69
+
70
+ # Keep moving forward until we find a valid collection day
71
+ while attempts < max_attempts:
72
+ attempts += 1
73
+
74
+ # Check if it's a weekend (Saturday=5, Sunday=6)
75
+ if check_date.weekday() >= 5:
76
+ check_date += timedelta(days=1)
77
+ continue
89
78
 
90
- for refuseDate in refuse_dates:
79
+ # Check if it's a bank holiday
80
+ if check_date.date() in bank_holiday_dates:
81
+ # Major holidays (Christmas/New Year) get bigger delays
82
+ holiday_name = str(holidays.UK().get(check_date.date(), ''))
83
+ is_major_holiday = (
84
+ 'Christmas' in holiday_name or
85
+ 'Boxing' in holiday_name or
86
+ 'New Year' in holiday_name
87
+ )
88
+ delay_days = 2 if is_major_holiday else 1
89
+ check_date += timedelta(days=delay_days)
90
+ continue
91
+
92
+ # Found a valid collection day
93
+ break
94
+
95
+ # If we've exhausted attempts, return the original date as fallback
96
+ if attempts >= max_attempts:
97
+ return original_date
98
+
99
+ return check_date.strftime("%d/%m/%Y")
100
+
101
+ bank_holidays = [] # No longer needed - using smart date calculation
91
102
 
92
- collection_date = (
103
+ for refuseDate in refuse_dates:
104
+ # Calculate initial collection date
105
+ initial_date = (
93
106
  datetime.strptime(refuseDate, "%d/%m/%Y") + timedelta(days=offset_days)
94
107
  ).strftime("%d/%m/%Y")
95
108
 
96
- holiday_offset = next(
97
- (value for date, value in bank_holidays if date == collection_date), 0
98
- )
99
-
100
- if holiday_offset > 0:
101
- collection_date = (
102
- datetime.strptime(collection_date, "%d/%m/%Y")
103
- + timedelta(days=holiday_offset)
104
- ).strftime("%d/%m/%Y")
109
+ # Find the next valid collection day (handles weekends + cascading holidays)
110
+ collection_date = find_next_collection_day(initial_date)
105
111
 
106
112
  dict_data = {
107
113
  "type": "Refuse Bin",
@@ -110,21 +116,14 @@ class CouncilClass(AbstractGetBinDataClass):
110
116
  bindata["bins"].append(dict_data)
111
117
 
112
118
  for recyclingDate in recycling_dates:
113
-
114
- collection_date = (
119
+ # Calculate initial collection date
120
+ initial_date = (
115
121
  datetime.strptime(recyclingDate, "%d/%m/%Y")
116
122
  + timedelta(days=offset_days)
117
123
  ).strftime("%d/%m/%Y")
118
124
 
119
- holiday_offset = next(
120
- (value for date, value in bank_holidays if date == collection_date), 0
121
- )
122
-
123
- if holiday_offset > 0:
124
- collection_date = (
125
- datetime.strptime(collection_date, "%d/%m/%Y")
126
- + timedelta(days=holiday_offset)
127
- ).strftime("%d/%m/%Y")
125
+ # Find the next valid collection day (handles weekends + cascading holidays)
126
+ collection_date = find_next_collection_day(initial_date)
128
127
 
129
128
  dict_data = {
130
129
  "type": "Recycling Bin",
@@ -140,48 +139,27 @@ class CouncilClass(AbstractGetBinDataClass):
140
139
 
141
140
  garden_dates = get_dates_every_x_days(gardenstartDate, 14, 28)
142
141
 
143
- garden_bank_holidays = [
144
- ("23/12/2024", 1),
145
- ("24/12/2024", 1),
146
- ("25/12/2024", 1),
147
- ("26/12/2024", 1),
148
- ("27/12/2024", 1),
149
- ("30/12/2024", 1),
150
- ("31/12/2024", 1),
151
- ("01/01/2025", 1),
152
- ("02/01/2025", 1),
153
- ("03/01/2025", 1),
154
- ]
142
+ def is_christmas_period(date_obj):
143
+ """Check if date is in Christmas/New Year skip period for garden collections."""
144
+ if date_obj.month == 12 and date_obj.day >= 23:
145
+ return True
146
+ if date_obj.month == 1 and date_obj.day <= 3:
147
+ return True
148
+ return False
155
149
 
156
150
  for gardenDate in garden_dates:
157
-
158
- collection_date = (
151
+ # Calculate initial collection date
152
+ initial_date = (
159
153
  datetime.strptime(gardenDate, "%d/%m/%Y")
160
154
  + timedelta(days=offset_days_garden)
161
- ).strftime("%d/%m/%Y")
162
-
163
- garden_holiday = next(
164
- (
165
- value
166
- for date, value in garden_bank_holidays
167
- if date == collection_date
168
- ),
169
- 0,
170
155
  )
171
156
 
172
- if garden_holiday > 0:
157
+ # Skip garden collections during Christmas/New Year period
158
+ if is_christmas_period(initial_date):
173
159
  continue
174
160
 
175
- holiday_offset = next(
176
- (value for date, value in bank_holidays if date == collection_date),
177
- 0,
178
- )
179
-
180
- if holiday_offset > 0:
181
- collection_date = (
182
- datetime.strptime(collection_date, "%d/%m/%Y")
183
- + timedelta(days=holiday_offset)
184
- ).strftime("%d/%m/%Y")
161
+ # Find the next valid collection day (handles weekends + holidays)
162
+ collection_date = find_next_collection_day(initial_date.strftime("%d/%m/%Y"))
185
163
 
186
164
  dict_data = {
187
165
  "type": "Garden Bin",