uk_bin_collection 0.74.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/README.rst +0 -0
- uk_bin_collection/tests/council_feature_input_parity.py +79 -0
- uk_bin_collection/tests/features/environment.py +7 -0
- uk_bin_collection/tests/features/validate_council_outputs.feature +767 -0
- uk_bin_collection/tests/input.json +1077 -0
- uk_bin_collection/tests/output.schema +41 -0
- uk_bin_collection/tests/step_defs/step_helpers/file_handler.py +46 -0
- uk_bin_collection/tests/step_defs/test_validate_council.py +87 -0
- uk_bin_collection/tests/test_collect_data.py +104 -0
- uk_bin_collection/tests/test_common_functions.py +342 -0
- uk_bin_collection/uk_bin_collection/collect_data.py +133 -0
- uk_bin_collection/uk_bin_collection/common.py +292 -0
- uk_bin_collection/uk_bin_collection/councils/AdurAndWorthingCouncils.py +43 -0
- uk_bin_collection/uk_bin_collection/councils/ArunCouncil.py +97 -0
- uk_bin_collection/uk_bin_collection/councils/AylesburyValeCouncil.py +69 -0
- uk_bin_collection/uk_bin_collection/councils/BCPCouncil.py +51 -0
- uk_bin_collection/uk_bin_collection/councils/BarnetCouncil.py +180 -0
- uk_bin_collection/uk_bin_collection/councils/BarnsleyMBCouncil.py +109 -0
- uk_bin_collection/uk_bin_collection/councils/BasingstokeCouncil.py +72 -0
- uk_bin_collection/uk_bin_collection/councils/BathAndNorthEastSomersetCouncil.py +100 -0
- uk_bin_collection/uk_bin_collection/councils/BedfordBoroughCouncil.py +49 -0
- uk_bin_collection/uk_bin_collection/councils/BedfordshireCouncil.py +70 -0
- uk_bin_collection/uk_bin_collection/councils/BexleyCouncil.py +147 -0
- uk_bin_collection/uk_bin_collection/councils/BirminghamCityCouncil.py +119 -0
- uk_bin_collection/uk_bin_collection/councils/BlackburnCouncil.py +105 -0
- uk_bin_collection/uk_bin_collection/councils/BoltonCouncil.py +104 -0
- uk_bin_collection/uk_bin_collection/councils/BradfordMDC.py +103 -0
- uk_bin_collection/uk_bin_collection/councils/BrightonandHoveCityCouncil.py +137 -0
- uk_bin_collection/uk_bin_collection/councils/BristolCityCouncil.py +141 -0
- uk_bin_collection/uk_bin_collection/councils/BromleyBoroughCouncil.py +115 -0
- uk_bin_collection/uk_bin_collection/councils/BroxtoweBoroughCouncil.py +107 -0
- uk_bin_collection/uk_bin_collection/councils/BuckinghamshireCouncil.py +95 -0
- uk_bin_collection/uk_bin_collection/councils/BuryCouncil.py +65 -0
- uk_bin_collection/uk_bin_collection/councils/CalderdaleCouncil.py +123 -0
- uk_bin_collection/uk_bin_collection/councils/CannockChaseDistrictCouncil.py +65 -0
- uk_bin_collection/uk_bin_collection/councils/CardiffCouncil.py +172 -0
- uk_bin_collection/uk_bin_collection/councils/CastlepointDistrictCouncil.py +96 -0
- uk_bin_collection/uk_bin_collection/councils/CharnwoodBoroughCouncil.py +54 -0
- uk_bin_collection/uk_bin_collection/councils/ChelmsfordCityCouncil.py +127 -0
- uk_bin_collection/uk_bin_collection/councils/CheshireEastCouncil.py +32 -0
- uk_bin_collection/uk_bin_collection/councils/CheshireWestAndChesterCouncil.py +125 -0
- uk_bin_collection/uk_bin_collection/councils/ChorleyCouncil.py +134 -0
- uk_bin_collection/uk_bin_collection/councils/ConwyCountyBorough.py +27 -0
- uk_bin_collection/uk_bin_collection/councils/CrawleyBoroughCouncil.py +61 -0
- uk_bin_collection/uk_bin_collection/councils/CroydonCouncil.py +291 -0
- uk_bin_collection/uk_bin_collection/councils/DerbyshireDalesDistrictCouncil.py +100 -0
- uk_bin_collection/uk_bin_collection/councils/DoncasterCouncil.py +77 -0
- uk_bin_collection/uk_bin_collection/councils/DorsetCouncil.py +58 -0
- uk_bin_collection/uk_bin_collection/councils/DoverDistrictCouncil.py +41 -0
- uk_bin_collection/uk_bin_collection/councils/DurhamCouncil.py +49 -0
- uk_bin_collection/uk_bin_collection/councils/EastCambridgeshireCouncil.py +44 -0
- uk_bin_collection/uk_bin_collection/councils/EastDevonDC.py +74 -0
- uk_bin_collection/uk_bin_collection/councils/EastLindseyDistrictCouncil.py +108 -0
- uk_bin_collection/uk_bin_collection/councils/EastRidingCouncil.py +142 -0
- uk_bin_collection/uk_bin_collection/councils/EastSuffolkCouncil.py +112 -0
- uk_bin_collection/uk_bin_collection/councils/EastleighBoroughCouncil.py +70 -0
- uk_bin_collection/uk_bin_collection/councils/EnvironmentFirst.py +48 -0
- uk_bin_collection/uk_bin_collection/councils/ErewashBoroughCouncil.py +61 -0
- uk_bin_collection/uk_bin_collection/councils/FenlandDistrictCouncil.py +65 -0
- uk_bin_collection/uk_bin_collection/councils/ForestOfDeanDistrictCouncil.py +113 -0
- uk_bin_collection/uk_bin_collection/councils/GatesheadCouncil.py +118 -0
- uk_bin_collection/uk_bin_collection/councils/GedlingBoroughCouncil.py +1580 -0
- uk_bin_collection/uk_bin_collection/councils/GlasgowCityCouncil.py +55 -0
- uk_bin_collection/uk_bin_collection/councils/GuildfordCouncil.py +150 -0
- uk_bin_collection/uk_bin_collection/councils/HaltonBoroughCouncil.py +142 -0
- uk_bin_collection/uk_bin_collection/councils/HaringeyCouncil.py +59 -0
- uk_bin_collection/uk_bin_collection/councils/HarrogateBoroughCouncil.py +63 -0
- uk_bin_collection/uk_bin_collection/councils/HighPeakCouncil.py +134 -0
- uk_bin_collection/uk_bin_collection/councils/HullCityCouncil.py +48 -0
- uk_bin_collection/uk_bin_collection/councils/HuntingdonDistrictCouncil.py +44 -0
- uk_bin_collection/uk_bin_collection/councils/KingstonUponThamesCouncil.py +84 -0
- uk_bin_collection/uk_bin_collection/councils/KirkleesCouncil.py +130 -0
- uk_bin_collection/uk_bin_collection/councils/KnowsleyMBCouncil.py +139 -0
- uk_bin_collection/uk_bin_collection/councils/LancasterCityCouncil.py +71 -0
- uk_bin_collection/uk_bin_collection/councils/LeedsCityCouncil.py +137 -0
- uk_bin_collection/uk_bin_collection/councils/LisburnCastlereaghCityCouncil.py +101 -0
- uk_bin_collection/uk_bin_collection/councils/LiverpoolCityCouncil.py +65 -0
- uk_bin_collection/uk_bin_collection/councils/LondonBoroughHounslow.py +82 -0
- uk_bin_collection/uk_bin_collection/councils/LondonBoroughRedbridge.py +161 -0
- uk_bin_collection/uk_bin_collection/councils/MaldonDistrictCouncil.py +52 -0
- uk_bin_collection/uk_bin_collection/councils/MalvernHillsDC.py +57 -0
- uk_bin_collection/uk_bin_collection/councils/ManchesterCityCouncil.py +106 -0
- uk_bin_collection/uk_bin_collection/councils/MansfieldDistrictCouncil.py +38 -0
- uk_bin_collection/uk_bin_collection/councils/MertonCouncil.py +58 -0
- uk_bin_collection/uk_bin_collection/councils/MidAndEastAntrimBoroughCouncil.py +128 -0
- uk_bin_collection/uk_bin_collection/councils/MidSussexDistrictCouncil.py +80 -0
- uk_bin_collection/uk_bin_collection/councils/MiltonKeynesCityCouncil.py +54 -0
- uk_bin_collection/uk_bin_collection/councils/MoleValleyDistrictCouncil.py +98 -0
- uk_bin_collection/uk_bin_collection/councils/NeathPortTalbotCouncil.py +139 -0
- uk_bin_collection/uk_bin_collection/councils/NewarkAndSherwoodDC.py +52 -0
- uk_bin_collection/uk_bin_collection/councils/NewcastleCityCouncil.py +57 -0
- uk_bin_collection/uk_bin_collection/councils/NewhamCouncil.py +58 -0
- uk_bin_collection/uk_bin_collection/councils/NewportCityCouncil.py +203 -0
- uk_bin_collection/uk_bin_collection/councils/NorthEastDerbyshireDistrictCouncil.py +115 -0
- uk_bin_collection/uk_bin_collection/councils/NorthEastLincs.py +53 -0
- uk_bin_collection/uk_bin_collection/councils/NorthKestevenDistrictCouncil.py +45 -0
- uk_bin_collection/uk_bin_collection/councils/NorthLanarkshireCouncil.py +46 -0
- uk_bin_collection/uk_bin_collection/councils/NorthLincolnshireCouncil.py +58 -0
- uk_bin_collection/uk_bin_collection/councils/NorthNorfolkDistrictCouncil.py +108 -0
- uk_bin_collection/uk_bin_collection/councils/NorthNorthamptonshireCouncil.py +72 -0
- uk_bin_collection/uk_bin_collection/councils/NorthSomersetCouncil.py +76 -0
- uk_bin_collection/uk_bin_collection/councils/NorthTynesideCouncil.py +220 -0
- uk_bin_collection/uk_bin_collection/councils/NorthWestLeicestershire.py +114 -0
- uk_bin_collection/uk_bin_collection/councils/NorthYorkshire.py +58 -0
- uk_bin_collection/uk_bin_collection/councils/NorthumberlandCouncil.py +123 -0
- uk_bin_collection/uk_bin_collection/councils/NottinghamCityCouncil.py +36 -0
- uk_bin_collection/uk_bin_collection/councils/OldhamCouncil.py +51 -0
- uk_bin_collection/uk_bin_collection/councils/PortsmouthCityCouncil.py +131 -0
- uk_bin_collection/uk_bin_collection/councils/PrestonCityCouncil.py +97 -0
- uk_bin_collection/uk_bin_collection/councils/ReadingBoroughCouncil.py +30 -0
- uk_bin_collection/uk_bin_collection/councils/ReigateAndBansteadBoroughCouncil.py +81 -0
- uk_bin_collection/uk_bin_collection/councils/RenfrewshireCouncil.py +135 -0
- uk_bin_collection/uk_bin_collection/councils/RhonddaCynonTaffCouncil.py +80 -0
- uk_bin_collection/uk_bin_collection/councils/RochdaleCouncil.py +69 -0
- uk_bin_collection/uk_bin_collection/councils/RochfordCouncil.py +60 -0
- uk_bin_collection/uk_bin_collection/councils/RugbyBoroughCouncil.py +93 -0
- uk_bin_collection/uk_bin_collection/councils/RushcliffeBoroughCouncil.py +100 -0
- uk_bin_collection/uk_bin_collection/councils/RushmoorCouncil.py +81 -0
- uk_bin_collection/uk_bin_collection/councils/SalfordCityCouncil.py +70 -0
- uk_bin_collection/uk_bin_collection/councils/SevenoaksDistrictCouncil.py +106 -0
- uk_bin_collection/uk_bin_collection/councils/SheffieldCityCouncil.py +54 -0
- uk_bin_collection/uk_bin_collection/councils/ShropshireCouncil.py +45 -0
- uk_bin_collection/uk_bin_collection/councils/SolihullCouncil.py +48 -0
- uk_bin_collection/uk_bin_collection/councils/SomersetCouncil.py +203 -0
- uk_bin_collection/uk_bin_collection/councils/SouthAyrshireCouncil.py +73 -0
- uk_bin_collection/uk_bin_collection/councils/SouthCambridgeshireCouncil.py +65 -0
- uk_bin_collection/uk_bin_collection/councils/SouthGloucestershireCouncil.py +74 -0
- uk_bin_collection/uk_bin_collection/councils/SouthLanarkshireCouncil.py +78 -0
- uk_bin_collection/uk_bin_collection/councils/SouthNorfolkCouncil.py +91 -0
- uk_bin_collection/uk_bin_collection/councils/SouthOxfordshireCouncil.py +93 -0
- uk_bin_collection/uk_bin_collection/councils/SouthTynesideCouncil.py +98 -0
- uk_bin_collection/uk_bin_collection/councils/StAlbansCityAndDistrictCouncil.py +43 -0
- uk_bin_collection/uk_bin_collection/councils/StHelensBC.py +56 -0
- uk_bin_collection/uk_bin_collection/councils/StaffordshireMoorlandsDistrictCouncil.py +112 -0
- uk_bin_collection/uk_bin_collection/councils/StockportBoroughCouncil.py +39 -0
- uk_bin_collection/uk_bin_collection/councils/StokeOnTrentCityCouncil.py +79 -0
- uk_bin_collection/uk_bin_collection/councils/StratfordUponAvonCouncil.py +94 -0
- uk_bin_collection/uk_bin_collection/councils/SunderlandCityCouncil.py +100 -0
- uk_bin_collection/uk_bin_collection/councils/SwaleBoroughCouncil.py +52 -0
- uk_bin_collection/uk_bin_collection/councils/TamesideMBCouncil.py +62 -0
- uk_bin_collection/uk_bin_collection/councils/TandridgeDistrictCouncil.py +60 -0
- uk_bin_collection/uk_bin_collection/councils/TelfordAndWrekinCouncil.py +50 -0
- uk_bin_collection/uk_bin_collection/councils/TestValleyBoroughCouncil.py +203 -0
- uk_bin_collection/uk_bin_collection/councils/TonbridgeAndMallingBC.py +101 -0
- uk_bin_collection/uk_bin_collection/councils/TorbayCouncil.py +51 -0
- uk_bin_collection/uk_bin_collection/councils/TorridgeDistrictCouncil.py +154 -0
- uk_bin_collection/uk_bin_collection/councils/ValeofGlamorganCouncil.py +119 -0
- uk_bin_collection/uk_bin_collection/councils/ValeofWhiteHorseCouncil.py +103 -0
- uk_bin_collection/uk_bin_collection/councils/WakefieldCityCouncil.py +89 -0
- uk_bin_collection/uk_bin_collection/councils/WarwickDistrictCouncil.py +34 -0
- uk_bin_collection/uk_bin_collection/councils/WaverleyBoroughCouncil.py +119 -0
- uk_bin_collection/uk_bin_collection/councils/WealdenDistrictCouncil.py +86 -0
- uk_bin_collection/uk_bin_collection/councils/WelhatCouncil.py +73 -0
- uk_bin_collection/uk_bin_collection/councils/WestBerkshireCouncil.py +134 -0
- uk_bin_collection/uk_bin_collection/councils/WestLindseyDistrictCouncil.py +118 -0
- uk_bin_collection/uk_bin_collection/councils/WestLothianCouncil.py +103 -0
- uk_bin_collection/uk_bin_collection/councils/WestNorthamptonshireCouncil.py +34 -0
- uk_bin_collection/uk_bin_collection/councils/WestSuffolkCouncil.py +64 -0
- uk_bin_collection/uk_bin_collection/councils/WiganBoroughCouncil.py +97 -0
- uk_bin_collection/uk_bin_collection/councils/WiltshireCouncil.py +135 -0
- uk_bin_collection/uk_bin_collection/councils/WindsorAndMaidenheadCouncil.py +134 -0
- uk_bin_collection/uk_bin_collection/councils/WokingBoroughCouncil.py +114 -0
- uk_bin_collection/uk_bin_collection/councils/WyreCouncil.py +89 -0
- uk_bin_collection/uk_bin_collection/councils/YorkCouncil.py +45 -0
- uk_bin_collection/uk_bin_collection/councils/council_class_template/councilclasstemplate.py +33 -0
- uk_bin_collection/uk_bin_collection/get_bin_data.py +165 -0
- uk_bin_collection-0.74.0.dist-info/LICENSE +21 -0
- uk_bin_collection-0.74.0.dist-info/METADATA +247 -0
- uk_bin_collection-0.74.0.dist-info/RECORD +171 -0
- uk_bin_collection-0.74.0.dist-info/WHEEL +4 -0
- uk_bin_collection-0.74.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,118 @@
|
|
1
|
+
import urllib.parse
|
2
|
+
|
3
|
+
from bs4 import BeautifulSoup
|
4
|
+
from dateutil.relativedelta import relativedelta
|
5
|
+
|
6
|
+
from uk_bin_collection.uk_bin_collection.common import *
|
7
|
+
from uk_bin_collection.uk_bin_collection.get_bin_data import AbstractGetBinDataClass
|
8
|
+
|
9
|
+
|
10
|
+
class CouncilClass(AbstractGetBinDataClass):
|
11
|
+
"""
|
12
|
+
Concrete classes have to implement all abstract operations of the
|
13
|
+
base class. They can also override some operations with a default
|
14
|
+
implementation.
|
15
|
+
"""
|
16
|
+
|
17
|
+
def parse_data(self, page: str, **kwargs) -> dict:
|
18
|
+
data = {"bins": []}
|
19
|
+
|
20
|
+
user_postcode = kwargs.get("postcode")
|
21
|
+
user_number = kwargs.get("paon")
|
22
|
+
|
23
|
+
user_address = "{} {}".format(user_number, user_postcode)
|
24
|
+
user_address = urllib.parse.quote(user_address)
|
25
|
+
|
26
|
+
# This first URL checks against a string representing the users address and returns values used for a second lookup.
|
27
|
+
stage1_url = "https://wlnk.statmap.co.uk/map/Cluster.svc/findLocation?callback=getAddressesCallback1702938375023&script=%5CCluster%5CCluster.AuroraScript%24&address={}".format(
|
28
|
+
user_address
|
29
|
+
)
|
30
|
+
|
31
|
+
address_data = requests.get(stage1_url).text
|
32
|
+
|
33
|
+
# Strip data and parse the JSON
|
34
|
+
address_data = json.loads(
|
35
|
+
re.sub("getAddressesCallback\d+\(", "", address_data)[:-2]
|
36
|
+
)
|
37
|
+
|
38
|
+
if address_data["TotalHits"] == 0:
|
39
|
+
raise Exception(
|
40
|
+
"No address found for string {}. See Wiki".format(user_address)
|
41
|
+
)
|
42
|
+
elif address_data["TotalHits"] != 1:
|
43
|
+
# Multiple hits returned. Let's pick the first one. We could raise an exception here if this causes problems.
|
44
|
+
pass
|
45
|
+
|
46
|
+
# Pull out the address data needed for the next step
|
47
|
+
address_id = address_data["Locations"][0]["Id"]
|
48
|
+
address_x = address_data["Locations"][0]["X"]
|
49
|
+
address_y = address_data["Locations"][0]["Y"]
|
50
|
+
|
51
|
+
stage2_url = "https://wlnk.statmap.co.uk/map/Cluster.svc/getpage?script=\Cluster\Cluster.AuroraScript$&taskId=bins&format=js&updateOnly=true&query=x%3D{}%3By%3D{}%3Bid%3D{}".format(
|
52
|
+
address_x, address_y, address_id
|
53
|
+
)
|
54
|
+
|
55
|
+
bin_query = requests.get(stage2_url).text
|
56
|
+
|
57
|
+
# Test that what we got is good
|
58
|
+
if "injectCss" not in bin_query:
|
59
|
+
raise Exception(
|
60
|
+
"Error. Data has not been returned correctly. Please raise an issue on the GitHub page"
|
61
|
+
)
|
62
|
+
|
63
|
+
# Return only the HTML contained within the Javascript function payload.
|
64
|
+
pattern = 'document\.getElementById\("DR1"\)\.innerHTML="(.+)";'
|
65
|
+
|
66
|
+
bin_html = re.findall(pattern, bin_query)
|
67
|
+
|
68
|
+
if len(bin_html) != 1:
|
69
|
+
# This exception is raised if the regular expression above finds anything other than one expected match.
|
70
|
+
raise Exception(
|
71
|
+
"Incorrect number of matches found during phase 2 search. Please raise an issue on the Github page"
|
72
|
+
)
|
73
|
+
|
74
|
+
# Some silly python foo required here to unescape the unicode contained.
|
75
|
+
bin_html = bin_html[0].encode().decode("unicode-escape")
|
76
|
+
|
77
|
+
soup = BeautifulSoup(bin_html, "html.parser")
|
78
|
+
|
79
|
+
collection_rows = soup.find("li", {"class": "auroraListItem"}).find_all("li")
|
80
|
+
|
81
|
+
collections = []
|
82
|
+
|
83
|
+
for row in collection_rows:
|
84
|
+
# Get bin type
|
85
|
+
bin_type = row.find("span").text
|
86
|
+
|
87
|
+
# Get bin date
|
88
|
+
bin_date_text = row.text
|
89
|
+
pattern = "\d+\/\d+"
|
90
|
+
bin_dates = re.findall(pattern, bin_date_text)
|
91
|
+
|
92
|
+
for bin_date in bin_dates:
|
93
|
+
# Split the bin date into day and month and build a full date with the current year
|
94
|
+
split_date = bin_date.split("/")
|
95
|
+
full_date = datetime(
|
96
|
+
datetime.now().year, int(split_date[1]), int(split_date[0])
|
97
|
+
)
|
98
|
+
|
99
|
+
# If the current month is December and one of the next collections is in January, increment the year
|
100
|
+
if datetime.now().month == 12 and int(split_date[1]) == 1:
|
101
|
+
full_date = bin_date + relativedelta(years=1)
|
102
|
+
|
103
|
+
# Since data in unordered, add to a tuple
|
104
|
+
collections.append((bin_type.title(), full_date))
|
105
|
+
|
106
|
+
# Sort the tuple by date
|
107
|
+
ordered_data = sorted(collections, key=lambda x: x[1])
|
108
|
+
|
109
|
+
# Add everything into the dictionary
|
110
|
+
for item in ordered_data:
|
111
|
+
dict_data = {
|
112
|
+
"type": item[0],
|
113
|
+
"collectionDate": item[1].strftime(date_format),
|
114
|
+
}
|
115
|
+
|
116
|
+
data["bins"].append(dict_data)
|
117
|
+
|
118
|
+
return data
|
@@ -0,0 +1,103 @@
|
|
1
|
+
from bs4 import BeautifulSoup
|
2
|
+
from selenium.webdriver.common.by import By
|
3
|
+
from selenium.webdriver.support import expected_conditions as EC
|
4
|
+
from selenium.webdriver.support.wait import WebDriverWait
|
5
|
+
|
6
|
+
from uk_bin_collection.uk_bin_collection.common import *
|
7
|
+
from uk_bin_collection.uk_bin_collection.get_bin_data import AbstractGetBinDataClass
|
8
|
+
|
9
|
+
|
10
|
+
# import the wonderful Beautiful Soup and the URL grabber
|
11
|
+
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
|
+
|
18
|
+
def parse_data(self, page: str, **kwargs) -> dict:
|
19
|
+
driver = None
|
20
|
+
try:
|
21
|
+
data = {"bins": []}
|
22
|
+
user_paon = kwargs.get("paon")
|
23
|
+
user_postcode = kwargs.get("postcode")
|
24
|
+
web_driver = kwargs.get("web_driver")
|
25
|
+
headless = kwargs.get("headless")
|
26
|
+
check_paon(user_paon)
|
27
|
+
check_postcode(user_postcode)
|
28
|
+
|
29
|
+
# Create Selenium webdriver
|
30
|
+
driver = create_webdriver(web_driver, headless)
|
31
|
+
driver.get(
|
32
|
+
"https://www.westlothian.gov.uk/article/31528/Bin-Collection-Calendar-Dates"
|
33
|
+
)
|
34
|
+
|
35
|
+
# Close feedback banner
|
36
|
+
feedbackBanner = WebDriverWait(driver, 10).until(
|
37
|
+
EC.presence_of_element_located((By.CSS_SELECTOR, ".feedback__link--no"))
|
38
|
+
)
|
39
|
+
feedbackBanner.click()
|
40
|
+
|
41
|
+
# Wait for the postcode field to appear then populate it
|
42
|
+
inputElement_postcode = WebDriverWait(driver, 30).until(
|
43
|
+
EC.presence_of_element_located(
|
44
|
+
(By.ID, "WLBINCOLLECTION_PAGE1_ADDRESSLOOKUPPOSTCODE")
|
45
|
+
)
|
46
|
+
)
|
47
|
+
inputElement_postcode.send_keys(user_postcode)
|
48
|
+
|
49
|
+
# Click search button
|
50
|
+
findAddress = WebDriverWait(driver, 10).until(
|
51
|
+
EC.presence_of_element_located(
|
52
|
+
(By.ID, "WLBINCOLLECTION_PAGE1_ADDRESSLOOKUPSEARCH")
|
53
|
+
)
|
54
|
+
)
|
55
|
+
findAddress.click()
|
56
|
+
|
57
|
+
# Wait for the 'Select address' dropdown to appear and select option matching the house name/number
|
58
|
+
WebDriverWait(driver, 10).until(
|
59
|
+
EC.element_to_be_clickable(
|
60
|
+
(
|
61
|
+
By.XPATH,
|
62
|
+
"//select[@id='WLBINCOLLECTION_PAGE1_ADDRESSLOOKUPADDRESS']//option[contains(., '"
|
63
|
+
+ user_paon
|
64
|
+
+ "')]",
|
65
|
+
)
|
66
|
+
)
|
67
|
+
).click()
|
68
|
+
|
69
|
+
# Wait for the collections table to appear
|
70
|
+
WebDriverWait(driver, 10).until(
|
71
|
+
EC.presence_of_element_located((By.CSS_SELECTOR, ".bin-collections"))
|
72
|
+
)
|
73
|
+
|
74
|
+
soup = BeautifulSoup(driver.page_source, features="html.parser")
|
75
|
+
|
76
|
+
# Get collections
|
77
|
+
for collection in soup.find_all("div", {"class": "bin-collect"}):
|
78
|
+
dict_data = {
|
79
|
+
"type": collection.find("h3").get_text(strip=True),
|
80
|
+
"collectionDate": datetime.strptime(
|
81
|
+
remove_ordinal_indicator_from_date_string(
|
82
|
+
collection.find(
|
83
|
+
"span", {"class": "bin-collect__date"}
|
84
|
+
).get_text(strip=True)
|
85
|
+
),
|
86
|
+
"%A, %B %d %Y",
|
87
|
+
).strftime(date_format),
|
88
|
+
}
|
89
|
+
data["bins"].append(dict_data)
|
90
|
+
|
91
|
+
data["bins"].sort(
|
92
|
+
key=lambda x: datetime.strptime(x.get("collectionDate"), "%d/%m/%Y")
|
93
|
+
)
|
94
|
+
except Exception as e:
|
95
|
+
# Here you can log the exception if needed
|
96
|
+
print(f"An error occurred: {e}")
|
97
|
+
# Optionally, re-raise the exception if you want it to propagate
|
98
|
+
raise
|
99
|
+
finally:
|
100
|
+
# This block ensures that the driver is closed regardless of an exception
|
101
|
+
if driver:
|
102
|
+
driver.quit()
|
103
|
+
return data
|
@@ -0,0 +1,34 @@
|
|
1
|
+
import urllib.parse
|
2
|
+
|
3
|
+
import requests
|
4
|
+
|
5
|
+
from uk_bin_collection.uk_bin_collection.common import *
|
6
|
+
from uk_bin_collection.uk_bin_collection.get_bin_data import AbstractGetBinDataClass
|
7
|
+
|
8
|
+
|
9
|
+
# import the wonderful Beautiful Soup and the URL grabber
|
10
|
+
class CouncilClass(AbstractGetBinDataClass):
|
11
|
+
"""
|
12
|
+
Concrete classes have to implement all abstract operations of the
|
13
|
+
base class. They can also override some operations with a default
|
14
|
+
implementation.
|
15
|
+
"""
|
16
|
+
|
17
|
+
def parse_data(self, page: str, **kwargs) -> dict:
|
18
|
+
user_postcode = kwargs.get("postcode")
|
19
|
+
check_postcode(user_postcode)
|
20
|
+
|
21
|
+
api_url = f"https://mycouncil.northampton.digital/BinRoundFinder?postcode={urllib.parse.quote(user_postcode)}"
|
22
|
+
json_data = requests.get(api_url).json()
|
23
|
+
|
24
|
+
data = {"bins": []}
|
25
|
+
|
26
|
+
dict_data = {
|
27
|
+
"type": json_data["type"].capitalize(),
|
28
|
+
"collectionDate": datetime.strptime(
|
29
|
+
json_data["date"], "%Y%m%d%H%M"
|
30
|
+
).strftime(date_format),
|
31
|
+
}
|
32
|
+
data["bins"].append(dict_data)
|
33
|
+
|
34
|
+
return data
|
@@ -0,0 +1,64 @@
|
|
1
|
+
import itertools
|
2
|
+
|
3
|
+
from bs4 import BeautifulSoup, Tag
|
4
|
+
from dateutil.parser import parse as date_parse
|
5
|
+
|
6
|
+
from uk_bin_collection.uk_bin_collection.common import *
|
7
|
+
from uk_bin_collection.uk_bin_collection.get_bin_data import AbstractGetBinDataClass
|
8
|
+
|
9
|
+
|
10
|
+
class CouncilClass(AbstractGetBinDataClass):
|
11
|
+
|
12
|
+
def parse_data(self, page: str, **kwargs) -> dict:
|
13
|
+
data = {"bins": []}
|
14
|
+
user_uprn = kwargs.get("uprn")
|
15
|
+
|
16
|
+
api_url = (
|
17
|
+
f"https://maps.westsuffolk.gov.uk/MyWestSuffolk.aspx?action=SetAddress&UniqueId={user_uprn}"
|
18
|
+
)
|
19
|
+
|
20
|
+
response = requests.get(api_url)
|
21
|
+
|
22
|
+
soup = BeautifulSoup(response.text, features="html.parser")
|
23
|
+
soup.prettify()
|
24
|
+
|
25
|
+
def panel_search(cur_tag: Tag):
|
26
|
+
"""
|
27
|
+
Helper function to find the correct tag
|
28
|
+
"""
|
29
|
+
if cur_tag.name != "div":
|
30
|
+
return False
|
31
|
+
|
32
|
+
tag_class = cur_tag.attrs.get("class", None)
|
33
|
+
if tag_class is None:
|
34
|
+
return False
|
35
|
+
|
36
|
+
parent_has_header = cur_tag.parent.find_all("h4", string="Bin collection days")
|
37
|
+
if len(parent_has_header) < 1:
|
38
|
+
return False
|
39
|
+
|
40
|
+
return "atPanelData" in tag_class
|
41
|
+
|
42
|
+
collection_tag = soup.body.find_all(panel_search)
|
43
|
+
|
44
|
+
# Parse the resultant div
|
45
|
+
for tag in collection_tag:
|
46
|
+
text_list = list(tag.stripped_strings)
|
47
|
+
# Create and parse the list as tuples of name:date
|
48
|
+
for bin_name, collection_date in itertools.batched(text_list, 2):
|
49
|
+
# Clean-up the bin_name
|
50
|
+
bin_name_clean = bin_name.strip().replace("\r", "").replace("\n", "")
|
51
|
+
bin_name_clean = re.sub(' +', ' ', bin_name_clean)
|
52
|
+
|
53
|
+
# Parse the date
|
54
|
+
next_collection = date_parse(collection_date)
|
55
|
+
next_collection = next_collection.replace(year=datetime.now().year)
|
56
|
+
|
57
|
+
dict_data = {
|
58
|
+
"type": bin_name_clean,
|
59
|
+
"collectionDate": next_collection.strftime(date_format),
|
60
|
+
}
|
61
|
+
|
62
|
+
data["bins"].append(dict_data)
|
63
|
+
|
64
|
+
return data
|
@@ -0,0 +1,97 @@
|
|
1
|
+
from datetime import datetime
|
2
|
+
|
3
|
+
import requests
|
4
|
+
from bs4 import BeautifulSoup
|
5
|
+
from uk_bin_collection.uk_bin_collection.common import *
|
6
|
+
from uk_bin_collection.uk_bin_collection.get_bin_data import AbstractGetBinDataClass
|
7
|
+
|
8
|
+
|
9
|
+
# import the wonderful Beautiful Soup and the URL grabber
|
10
|
+
class CouncilClass(AbstractGetBinDataClass):
|
11
|
+
"""
|
12
|
+
Concrete classes have to implement all abstract operations of the
|
13
|
+
base class. They can also override some operations with a default
|
14
|
+
implementation.
|
15
|
+
"""
|
16
|
+
|
17
|
+
def parse_data(self, page: str, **kwargs) -> dict:
|
18
|
+
# Get and check UPRN
|
19
|
+
user_uprn = kwargs.get("uprn")
|
20
|
+
check_uprn(user_uprn)
|
21
|
+
user_uprn = user_uprn.zfill(
|
22
|
+
12
|
23
|
+
) # Wigan is expecting 12 character UPRN or else it falls over, expects 0 padded UPRNS at the start for any that aren't 12 chars
|
24
|
+
|
25
|
+
user_postcode = kwargs.get("postcode")
|
26
|
+
check_postcode(user_postcode)
|
27
|
+
|
28
|
+
# Start a new session to walk through the form
|
29
|
+
requests.packages.urllib3.disable_warnings()
|
30
|
+
s = requests.session()
|
31
|
+
|
32
|
+
# Get our initial session running
|
33
|
+
response = s.get("https://apps.wigan.gov.uk/MyNeighbourhood/")
|
34
|
+
|
35
|
+
soup = BeautifulSoup(response.text, features="html.parser")
|
36
|
+
soup.prettify()
|
37
|
+
|
38
|
+
# Grab the ASP variables needed to continue
|
39
|
+
payload = {
|
40
|
+
"__VIEWSTATE": (soup.find("input", {"id": "__VIEWSTATE"}).get("value")),
|
41
|
+
"__VIEWSTATEGENERATOR": (
|
42
|
+
soup.find("input", {"id": "__VIEWSTATEGENERATOR"}).get("value")
|
43
|
+
),
|
44
|
+
"__EVENTVALIDATION": (
|
45
|
+
soup.find("input", {"id": "__EVENTVALIDATION"}).get("value")
|
46
|
+
),
|
47
|
+
"ctl00$ContentPlaceHolder1$txtPostcode": (user_postcode),
|
48
|
+
"ctl00$ContentPlaceHolder1$btnPostcodeSearch": ("Search"),
|
49
|
+
}
|
50
|
+
|
51
|
+
# Use the above to get to the next page with address selection
|
52
|
+
response = s.post("https://apps.wigan.gov.uk/MyNeighbourhood/", payload)
|
53
|
+
|
54
|
+
soup = BeautifulSoup(response.text, features="html.parser")
|
55
|
+
soup.prettify()
|
56
|
+
|
57
|
+
# Load the new variables that are constant and can't be gotten from the page
|
58
|
+
payload = {
|
59
|
+
"__EVENTTARGET": ("ctl00$ContentPlaceHolder1$lstAddresses"),
|
60
|
+
"__EVENTARGUMENT": (""),
|
61
|
+
"__LASTFOCUS": (""),
|
62
|
+
"__VIEWSTATE": (soup.find("input", {"id": "__VIEWSTATE"}).get("value")),
|
63
|
+
"__VIEWSTATEGENERATOR": (
|
64
|
+
soup.find("input", {"id": "__VIEWSTATEGENERATOR"}).get("value")
|
65
|
+
),
|
66
|
+
"__EVENTVALIDATION": (
|
67
|
+
soup.find("input", {"id": "__EVENTVALIDATION"}).get("value")
|
68
|
+
),
|
69
|
+
"ctl00$ContentPlaceHolder1$txtPostcode": (user_postcode),
|
70
|
+
"ctl00$ContentPlaceHolder1$lstAddresses": ("UPRN" + user_uprn),
|
71
|
+
}
|
72
|
+
|
73
|
+
# Get the final page with the actual dates
|
74
|
+
response = s.post("https://apps.wigan.gov.uk/MyNeighbourhood/", payload)
|
75
|
+
|
76
|
+
soup = BeautifulSoup(response.text, features="html.parser")
|
77
|
+
soup.prettify()
|
78
|
+
|
79
|
+
data = {"bins": []}
|
80
|
+
|
81
|
+
# Get the dates.
|
82
|
+
for bins in soup.find_all("div", {"class": "BinsRecycling"}):
|
83
|
+
bin_type = bins.find("h2").text
|
84
|
+
binCollection = bins.find("div", {"class": "dateWrapper-next"}).get_text(
|
85
|
+
strip=True
|
86
|
+
)
|
87
|
+
binData = datetime.strptime(
|
88
|
+
re.sub(r"(\d)(st|nd|rd|th)", r"\1", binCollection), "%A%d%b%Y"
|
89
|
+
)
|
90
|
+
if binData:
|
91
|
+
dict_data = {
|
92
|
+
"type": bin_type,
|
93
|
+
"collectionDate": binData.strftime(date_format),
|
94
|
+
}
|
95
|
+
data["bins"].append(dict_data)
|
96
|
+
|
97
|
+
return data
|
@@ -0,0 +1,135 @@
|
|
1
|
+
from bs4 import BeautifulSoup
|
2
|
+
|
3
|
+
from uk_bin_collection.uk_bin_collection.common import *
|
4
|
+
from uk_bin_collection.uk_bin_collection.get_bin_data import AbstractGetBinDataClass
|
5
|
+
|
6
|
+
|
7
|
+
class CouncilClass(AbstractGetBinDataClass):
|
8
|
+
"""
|
9
|
+
Concrete classes have to implement all abstract operations of the
|
10
|
+
base class. They can also override some operations with a default
|
11
|
+
implementation.
|
12
|
+
"""
|
13
|
+
|
14
|
+
def parse_data(self, page: str, **kwargs) -> dict:
|
15
|
+
requests.packages.urllib3.disable_warnings()
|
16
|
+
# Define some months to get from the calendar
|
17
|
+
this_month = datetime.now().month
|
18
|
+
this_year = datetime.now().year
|
19
|
+
one_month = this_month + 1
|
20
|
+
two_month = this_month + 2
|
21
|
+
months = [this_month, one_month, two_month]
|
22
|
+
|
23
|
+
# Get and check the postcode and UPRN values
|
24
|
+
user_postcode = kwargs.get("postcode")
|
25
|
+
check_postcode(user_postcode)
|
26
|
+
user_uprn = kwargs.get("uprn")
|
27
|
+
check_uprn(user_uprn)
|
28
|
+
|
29
|
+
# Some data for the request
|
30
|
+
cookies = {
|
31
|
+
"ARRAffinity": "c5a9db7fe43cef907f06528c3d34a997365656f757206fbdf34193e2c3b6f737",
|
32
|
+
"ARRAffinitySameSite": "c5a9db7fe43cef907f06528c3d34a997365656f757206fbdf34193e2c3b6f737",
|
33
|
+
}
|
34
|
+
headers = {
|
35
|
+
"Accept": "*/*",
|
36
|
+
"Accept-Language": "en-GB,en;q=0.9",
|
37
|
+
"Cache-Control": "no-cache",
|
38
|
+
"Connection": "keep-alive",
|
39
|
+
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
40
|
+
# 'Cookie': 'ARRAffinity=c5a9db7fe43cef907f06528c3d34a997365656f757206fbdf34193e2c3b6f737; ARRAffinitySameSite=c5a9db7fe43cef907f06528c3d34a997365656f757206fbdf34193e2c3b6f737',
|
41
|
+
"Origin": "https://ilambassadorformsprod.azurewebsites.net",
|
42
|
+
"Pragma": "no-cache",
|
43
|
+
"Referer": "https://ilambassadorformsprod.azurewebsites.net/wastecollectiondays/index",
|
44
|
+
"Sec-Fetch-Dest": "empty",
|
45
|
+
"Sec-Fetch-Mode": "cors",
|
46
|
+
"Sec-Fetch-Site": "same-origin",
|
47
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36 OPR/98.0.0.0",
|
48
|
+
"X-Requested-With": "XMLHttpRequest",
|
49
|
+
"sec-ch-ua": '"Chromium";v="112", "Not_A Brand";v="24", "Opera GX";v="98"',
|
50
|
+
"sec-ch-ua-mobile": "?0",
|
51
|
+
"sec-ch-ua-platform": '"Windows"',
|
52
|
+
}
|
53
|
+
|
54
|
+
collections = []
|
55
|
+
|
56
|
+
# For each of the months we defined
|
57
|
+
for cal_month in months:
|
58
|
+
# If we're in Nov/Dec, the calculations won't work since its just adding one, so roll it
|
59
|
+
# to next year correctly
|
60
|
+
if cal_month == 13:
|
61
|
+
cal_month = 1
|
62
|
+
cal_year = this_year + 1
|
63
|
+
elif cal_month == 14:
|
64
|
+
cal_month = 2
|
65
|
+
cal_year = this_year + 1
|
66
|
+
else:
|
67
|
+
cal_year = this_year
|
68
|
+
|
69
|
+
# Data for the calendar
|
70
|
+
data = {
|
71
|
+
"Month": cal_month,
|
72
|
+
"Year": cal_year,
|
73
|
+
"Postcode": user_postcode,
|
74
|
+
"Uprn": user_uprn,
|
75
|
+
}
|
76
|
+
|
77
|
+
# Send it all as a POST
|
78
|
+
response = requests.post(
|
79
|
+
"https://ilambassadorformsprod.azurewebsites.net/wastecollectiondays/collectionlist",
|
80
|
+
cookies=cookies,
|
81
|
+
headers=headers,
|
82
|
+
data=data,
|
83
|
+
)
|
84
|
+
|
85
|
+
# If we don't get a HTTP200, throw an error
|
86
|
+
if response.status_code != 200:
|
87
|
+
raise SystemError(
|
88
|
+
"Error retrieving data! Please try again or raise an issue on GitHub!"
|
89
|
+
)
|
90
|
+
|
91
|
+
soup = BeautifulSoup(response.text, features="html.parser")
|
92
|
+
soup.prettify()
|
93
|
+
|
94
|
+
# Find all the bits of the current calendar that contain an event
|
95
|
+
events = soup.find_all("div", {"class": "rc-event-container"})
|
96
|
+
|
97
|
+
for event in events:
|
98
|
+
# Get the date and type of each bin collection
|
99
|
+
bin_date = datetime.strptime(
|
100
|
+
event.find_next("a").attrs.get("data-original-datetext"),
|
101
|
+
"%A %d %B, %Y",
|
102
|
+
)
|
103
|
+
bin_type = event.find_next("a").attrs.get("data-original-title")
|
104
|
+
# Only process it if it's today or in the future
|
105
|
+
if bin_date.date() >= datetime.now().date():
|
106
|
+
# Split the really long type up into two separate bins
|
107
|
+
if (
|
108
|
+
bin_type
|
109
|
+
== "Mixed dry recycling (blue lidded bin) and glass (black box or basket)"
|
110
|
+
):
|
111
|
+
collections.append(
|
112
|
+
(
|
113
|
+
"Mixed dry recycling (blue lidded bin)",
|
114
|
+
datetime.strftime(bin_date, date_format),
|
115
|
+
)
|
116
|
+
)
|
117
|
+
collections.append(
|
118
|
+
(
|
119
|
+
"Glass (black box or basket)",
|
120
|
+
datetime.strftime(bin_date, date_format),
|
121
|
+
)
|
122
|
+
)
|
123
|
+
else:
|
124
|
+
collections.append(
|
125
|
+
(bin_type, datetime.strftime(bin_date, date_format))
|
126
|
+
)
|
127
|
+
|
128
|
+
data = {"bins": []}
|
129
|
+
|
130
|
+
# Now there's a list of collections, yeet them into the dictionary for nice JSON
|
131
|
+
for item in collections:
|
132
|
+
dict_data = {"type": item[0], "collectionDate": item[1]}
|
133
|
+
data["bins"].append(dict_data)
|
134
|
+
|
135
|
+
return data
|