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.
- uk_bin_collection/tests/input.json +21 -10
- uk_bin_collection/uk_bin_collection/councils/AberdeenCityCouncil.py +0 -1
- uk_bin_collection/uk_bin_collection/councils/DacorumBoroughCouncil.py +22 -13
- uk_bin_collection/uk_bin_collection/councils/EastDunbartonshireCouncil.py +52 -0
- uk_bin_collection/uk_bin_collection/councils/IslingtonCouncil.py +8 -5
- uk_bin_collection/uk_bin_collection/councils/LancasterCityCouncil.py +23 -10
- uk_bin_collection/uk_bin_collection/councils/LondonBoroughSutton.py +60 -49
- uk_bin_collection/uk_bin_collection/councils/MidSuffolkDistrictCouncil.py +70 -92
- uk_bin_collection/uk_bin_collection/councils/NewForestCouncil.py +104 -47
- uk_bin_collection/uk_bin_collection/councils/NewportCityCouncil.py +138 -21
- uk_bin_collection/uk_bin_collection/councils/NorthumberlandCouncil.py +182 -3
- uk_bin_collection/uk_bin_collection/councils/OxfordCityCouncil.py +1 -0
- uk_bin_collection/uk_bin_collection/councils/RenfrewshireCouncil.py +170 -13
- uk_bin_collection/uk_bin_collection/councils/RotherhamCouncil.py +70 -38
- uk_bin_collection/uk_bin_collection/councils/SomersetCouncil.py +136 -21
- uk_bin_collection/uk_bin_collection/councils/SouthGloucestershireCouncil.py +18 -22
- uk_bin_collection/uk_bin_collection/councils/TestValleyBoroughCouncil.py +138 -21
- {uk_bin_collection-0.154.0.dist-info → uk_bin_collection-0.158.0.dist-info}/METADATA +1 -1
- {uk_bin_collection-0.154.0.dist-info → uk_bin_collection-0.158.0.dist-info}/RECORD +22 -21
- {uk_bin_collection-0.154.0.dist-info → uk_bin_collection-0.158.0.dist-info}/LICENSE +0 -0
- {uk_bin_collection-0.154.0.dist-info → uk_bin_collection-0.158.0.dist-info}/WHEEL +0 -0
- {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/
|
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
|
-
"
|
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
|
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
|
-
"
|
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
|
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://
|
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
|
2427
|
+
"postcode": "SO51 0BY",
|
2419
2428
|
"skip_get_url": true,
|
2420
|
-
"
|
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
|
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
|
-
|
80
|
-
if
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
"
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
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
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
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
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
54
|
-
|
55
|
-
|
56
|
-
(
|
57
|
-
(
|
58
|
-
(
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
(
|
63
|
-
("
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
97
|
-
|
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
|
-
|
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
|
-
|
120
|
-
|
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
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
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
|
-
|
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
|
-
|
157
|
+
# Skip garden collections during Christmas/New Year period
|
158
|
+
if is_christmas_period(initial_date):
|
173
159
|
continue
|
174
160
|
|
175
|
-
|
176
|
-
|
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",
|