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.
Files changed (171) hide show
  1. uk_bin_collection/README.rst +0 -0
  2. uk_bin_collection/tests/council_feature_input_parity.py +79 -0
  3. uk_bin_collection/tests/features/environment.py +7 -0
  4. uk_bin_collection/tests/features/validate_council_outputs.feature +767 -0
  5. uk_bin_collection/tests/input.json +1077 -0
  6. uk_bin_collection/tests/output.schema +41 -0
  7. uk_bin_collection/tests/step_defs/step_helpers/file_handler.py +46 -0
  8. uk_bin_collection/tests/step_defs/test_validate_council.py +87 -0
  9. uk_bin_collection/tests/test_collect_data.py +104 -0
  10. uk_bin_collection/tests/test_common_functions.py +342 -0
  11. uk_bin_collection/uk_bin_collection/collect_data.py +133 -0
  12. uk_bin_collection/uk_bin_collection/common.py +292 -0
  13. uk_bin_collection/uk_bin_collection/councils/AdurAndWorthingCouncils.py +43 -0
  14. uk_bin_collection/uk_bin_collection/councils/ArunCouncil.py +97 -0
  15. uk_bin_collection/uk_bin_collection/councils/AylesburyValeCouncil.py +69 -0
  16. uk_bin_collection/uk_bin_collection/councils/BCPCouncil.py +51 -0
  17. uk_bin_collection/uk_bin_collection/councils/BarnetCouncil.py +180 -0
  18. uk_bin_collection/uk_bin_collection/councils/BarnsleyMBCouncil.py +109 -0
  19. uk_bin_collection/uk_bin_collection/councils/BasingstokeCouncil.py +72 -0
  20. uk_bin_collection/uk_bin_collection/councils/BathAndNorthEastSomersetCouncil.py +100 -0
  21. uk_bin_collection/uk_bin_collection/councils/BedfordBoroughCouncil.py +49 -0
  22. uk_bin_collection/uk_bin_collection/councils/BedfordshireCouncil.py +70 -0
  23. uk_bin_collection/uk_bin_collection/councils/BexleyCouncil.py +147 -0
  24. uk_bin_collection/uk_bin_collection/councils/BirminghamCityCouncil.py +119 -0
  25. uk_bin_collection/uk_bin_collection/councils/BlackburnCouncil.py +105 -0
  26. uk_bin_collection/uk_bin_collection/councils/BoltonCouncil.py +104 -0
  27. uk_bin_collection/uk_bin_collection/councils/BradfordMDC.py +103 -0
  28. uk_bin_collection/uk_bin_collection/councils/BrightonandHoveCityCouncil.py +137 -0
  29. uk_bin_collection/uk_bin_collection/councils/BristolCityCouncil.py +141 -0
  30. uk_bin_collection/uk_bin_collection/councils/BromleyBoroughCouncil.py +115 -0
  31. uk_bin_collection/uk_bin_collection/councils/BroxtoweBoroughCouncil.py +107 -0
  32. uk_bin_collection/uk_bin_collection/councils/BuckinghamshireCouncil.py +95 -0
  33. uk_bin_collection/uk_bin_collection/councils/BuryCouncil.py +65 -0
  34. uk_bin_collection/uk_bin_collection/councils/CalderdaleCouncil.py +123 -0
  35. uk_bin_collection/uk_bin_collection/councils/CannockChaseDistrictCouncil.py +65 -0
  36. uk_bin_collection/uk_bin_collection/councils/CardiffCouncil.py +172 -0
  37. uk_bin_collection/uk_bin_collection/councils/CastlepointDistrictCouncil.py +96 -0
  38. uk_bin_collection/uk_bin_collection/councils/CharnwoodBoroughCouncil.py +54 -0
  39. uk_bin_collection/uk_bin_collection/councils/ChelmsfordCityCouncil.py +127 -0
  40. uk_bin_collection/uk_bin_collection/councils/CheshireEastCouncil.py +32 -0
  41. uk_bin_collection/uk_bin_collection/councils/CheshireWestAndChesterCouncil.py +125 -0
  42. uk_bin_collection/uk_bin_collection/councils/ChorleyCouncil.py +134 -0
  43. uk_bin_collection/uk_bin_collection/councils/ConwyCountyBorough.py +27 -0
  44. uk_bin_collection/uk_bin_collection/councils/CrawleyBoroughCouncil.py +61 -0
  45. uk_bin_collection/uk_bin_collection/councils/CroydonCouncil.py +291 -0
  46. uk_bin_collection/uk_bin_collection/councils/DerbyshireDalesDistrictCouncil.py +100 -0
  47. uk_bin_collection/uk_bin_collection/councils/DoncasterCouncil.py +77 -0
  48. uk_bin_collection/uk_bin_collection/councils/DorsetCouncil.py +58 -0
  49. uk_bin_collection/uk_bin_collection/councils/DoverDistrictCouncil.py +41 -0
  50. uk_bin_collection/uk_bin_collection/councils/DurhamCouncil.py +49 -0
  51. uk_bin_collection/uk_bin_collection/councils/EastCambridgeshireCouncil.py +44 -0
  52. uk_bin_collection/uk_bin_collection/councils/EastDevonDC.py +74 -0
  53. uk_bin_collection/uk_bin_collection/councils/EastLindseyDistrictCouncil.py +108 -0
  54. uk_bin_collection/uk_bin_collection/councils/EastRidingCouncil.py +142 -0
  55. uk_bin_collection/uk_bin_collection/councils/EastSuffolkCouncil.py +112 -0
  56. uk_bin_collection/uk_bin_collection/councils/EastleighBoroughCouncil.py +70 -0
  57. uk_bin_collection/uk_bin_collection/councils/EnvironmentFirst.py +48 -0
  58. uk_bin_collection/uk_bin_collection/councils/ErewashBoroughCouncil.py +61 -0
  59. uk_bin_collection/uk_bin_collection/councils/FenlandDistrictCouncil.py +65 -0
  60. uk_bin_collection/uk_bin_collection/councils/ForestOfDeanDistrictCouncil.py +113 -0
  61. uk_bin_collection/uk_bin_collection/councils/GatesheadCouncil.py +118 -0
  62. uk_bin_collection/uk_bin_collection/councils/GedlingBoroughCouncil.py +1580 -0
  63. uk_bin_collection/uk_bin_collection/councils/GlasgowCityCouncil.py +55 -0
  64. uk_bin_collection/uk_bin_collection/councils/GuildfordCouncil.py +150 -0
  65. uk_bin_collection/uk_bin_collection/councils/HaltonBoroughCouncil.py +142 -0
  66. uk_bin_collection/uk_bin_collection/councils/HaringeyCouncil.py +59 -0
  67. uk_bin_collection/uk_bin_collection/councils/HarrogateBoroughCouncil.py +63 -0
  68. uk_bin_collection/uk_bin_collection/councils/HighPeakCouncil.py +134 -0
  69. uk_bin_collection/uk_bin_collection/councils/HullCityCouncil.py +48 -0
  70. uk_bin_collection/uk_bin_collection/councils/HuntingdonDistrictCouncil.py +44 -0
  71. uk_bin_collection/uk_bin_collection/councils/KingstonUponThamesCouncil.py +84 -0
  72. uk_bin_collection/uk_bin_collection/councils/KirkleesCouncil.py +130 -0
  73. uk_bin_collection/uk_bin_collection/councils/KnowsleyMBCouncil.py +139 -0
  74. uk_bin_collection/uk_bin_collection/councils/LancasterCityCouncil.py +71 -0
  75. uk_bin_collection/uk_bin_collection/councils/LeedsCityCouncil.py +137 -0
  76. uk_bin_collection/uk_bin_collection/councils/LisburnCastlereaghCityCouncil.py +101 -0
  77. uk_bin_collection/uk_bin_collection/councils/LiverpoolCityCouncil.py +65 -0
  78. uk_bin_collection/uk_bin_collection/councils/LondonBoroughHounslow.py +82 -0
  79. uk_bin_collection/uk_bin_collection/councils/LondonBoroughRedbridge.py +161 -0
  80. uk_bin_collection/uk_bin_collection/councils/MaldonDistrictCouncil.py +52 -0
  81. uk_bin_collection/uk_bin_collection/councils/MalvernHillsDC.py +57 -0
  82. uk_bin_collection/uk_bin_collection/councils/ManchesterCityCouncil.py +106 -0
  83. uk_bin_collection/uk_bin_collection/councils/MansfieldDistrictCouncil.py +38 -0
  84. uk_bin_collection/uk_bin_collection/councils/MertonCouncil.py +58 -0
  85. uk_bin_collection/uk_bin_collection/councils/MidAndEastAntrimBoroughCouncil.py +128 -0
  86. uk_bin_collection/uk_bin_collection/councils/MidSussexDistrictCouncil.py +80 -0
  87. uk_bin_collection/uk_bin_collection/councils/MiltonKeynesCityCouncil.py +54 -0
  88. uk_bin_collection/uk_bin_collection/councils/MoleValleyDistrictCouncil.py +98 -0
  89. uk_bin_collection/uk_bin_collection/councils/NeathPortTalbotCouncil.py +139 -0
  90. uk_bin_collection/uk_bin_collection/councils/NewarkAndSherwoodDC.py +52 -0
  91. uk_bin_collection/uk_bin_collection/councils/NewcastleCityCouncil.py +57 -0
  92. uk_bin_collection/uk_bin_collection/councils/NewhamCouncil.py +58 -0
  93. uk_bin_collection/uk_bin_collection/councils/NewportCityCouncil.py +203 -0
  94. uk_bin_collection/uk_bin_collection/councils/NorthEastDerbyshireDistrictCouncil.py +115 -0
  95. uk_bin_collection/uk_bin_collection/councils/NorthEastLincs.py +53 -0
  96. uk_bin_collection/uk_bin_collection/councils/NorthKestevenDistrictCouncil.py +45 -0
  97. uk_bin_collection/uk_bin_collection/councils/NorthLanarkshireCouncil.py +46 -0
  98. uk_bin_collection/uk_bin_collection/councils/NorthLincolnshireCouncil.py +58 -0
  99. uk_bin_collection/uk_bin_collection/councils/NorthNorfolkDistrictCouncil.py +108 -0
  100. uk_bin_collection/uk_bin_collection/councils/NorthNorthamptonshireCouncil.py +72 -0
  101. uk_bin_collection/uk_bin_collection/councils/NorthSomersetCouncil.py +76 -0
  102. uk_bin_collection/uk_bin_collection/councils/NorthTynesideCouncil.py +220 -0
  103. uk_bin_collection/uk_bin_collection/councils/NorthWestLeicestershire.py +114 -0
  104. uk_bin_collection/uk_bin_collection/councils/NorthYorkshire.py +58 -0
  105. uk_bin_collection/uk_bin_collection/councils/NorthumberlandCouncil.py +123 -0
  106. uk_bin_collection/uk_bin_collection/councils/NottinghamCityCouncil.py +36 -0
  107. uk_bin_collection/uk_bin_collection/councils/OldhamCouncil.py +51 -0
  108. uk_bin_collection/uk_bin_collection/councils/PortsmouthCityCouncil.py +131 -0
  109. uk_bin_collection/uk_bin_collection/councils/PrestonCityCouncil.py +97 -0
  110. uk_bin_collection/uk_bin_collection/councils/ReadingBoroughCouncil.py +30 -0
  111. uk_bin_collection/uk_bin_collection/councils/ReigateAndBansteadBoroughCouncil.py +81 -0
  112. uk_bin_collection/uk_bin_collection/councils/RenfrewshireCouncil.py +135 -0
  113. uk_bin_collection/uk_bin_collection/councils/RhonddaCynonTaffCouncil.py +80 -0
  114. uk_bin_collection/uk_bin_collection/councils/RochdaleCouncil.py +69 -0
  115. uk_bin_collection/uk_bin_collection/councils/RochfordCouncil.py +60 -0
  116. uk_bin_collection/uk_bin_collection/councils/RugbyBoroughCouncil.py +93 -0
  117. uk_bin_collection/uk_bin_collection/councils/RushcliffeBoroughCouncil.py +100 -0
  118. uk_bin_collection/uk_bin_collection/councils/RushmoorCouncil.py +81 -0
  119. uk_bin_collection/uk_bin_collection/councils/SalfordCityCouncil.py +70 -0
  120. uk_bin_collection/uk_bin_collection/councils/SevenoaksDistrictCouncil.py +106 -0
  121. uk_bin_collection/uk_bin_collection/councils/SheffieldCityCouncil.py +54 -0
  122. uk_bin_collection/uk_bin_collection/councils/ShropshireCouncil.py +45 -0
  123. uk_bin_collection/uk_bin_collection/councils/SolihullCouncil.py +48 -0
  124. uk_bin_collection/uk_bin_collection/councils/SomersetCouncil.py +203 -0
  125. uk_bin_collection/uk_bin_collection/councils/SouthAyrshireCouncil.py +73 -0
  126. uk_bin_collection/uk_bin_collection/councils/SouthCambridgeshireCouncil.py +65 -0
  127. uk_bin_collection/uk_bin_collection/councils/SouthGloucestershireCouncil.py +74 -0
  128. uk_bin_collection/uk_bin_collection/councils/SouthLanarkshireCouncil.py +78 -0
  129. uk_bin_collection/uk_bin_collection/councils/SouthNorfolkCouncil.py +91 -0
  130. uk_bin_collection/uk_bin_collection/councils/SouthOxfordshireCouncil.py +93 -0
  131. uk_bin_collection/uk_bin_collection/councils/SouthTynesideCouncil.py +98 -0
  132. uk_bin_collection/uk_bin_collection/councils/StAlbansCityAndDistrictCouncil.py +43 -0
  133. uk_bin_collection/uk_bin_collection/councils/StHelensBC.py +56 -0
  134. uk_bin_collection/uk_bin_collection/councils/StaffordshireMoorlandsDistrictCouncil.py +112 -0
  135. uk_bin_collection/uk_bin_collection/councils/StockportBoroughCouncil.py +39 -0
  136. uk_bin_collection/uk_bin_collection/councils/StokeOnTrentCityCouncil.py +79 -0
  137. uk_bin_collection/uk_bin_collection/councils/StratfordUponAvonCouncil.py +94 -0
  138. uk_bin_collection/uk_bin_collection/councils/SunderlandCityCouncil.py +100 -0
  139. uk_bin_collection/uk_bin_collection/councils/SwaleBoroughCouncil.py +52 -0
  140. uk_bin_collection/uk_bin_collection/councils/TamesideMBCouncil.py +62 -0
  141. uk_bin_collection/uk_bin_collection/councils/TandridgeDistrictCouncil.py +60 -0
  142. uk_bin_collection/uk_bin_collection/councils/TelfordAndWrekinCouncil.py +50 -0
  143. uk_bin_collection/uk_bin_collection/councils/TestValleyBoroughCouncil.py +203 -0
  144. uk_bin_collection/uk_bin_collection/councils/TonbridgeAndMallingBC.py +101 -0
  145. uk_bin_collection/uk_bin_collection/councils/TorbayCouncil.py +51 -0
  146. uk_bin_collection/uk_bin_collection/councils/TorridgeDistrictCouncil.py +154 -0
  147. uk_bin_collection/uk_bin_collection/councils/ValeofGlamorganCouncil.py +119 -0
  148. uk_bin_collection/uk_bin_collection/councils/ValeofWhiteHorseCouncil.py +103 -0
  149. uk_bin_collection/uk_bin_collection/councils/WakefieldCityCouncil.py +89 -0
  150. uk_bin_collection/uk_bin_collection/councils/WarwickDistrictCouncil.py +34 -0
  151. uk_bin_collection/uk_bin_collection/councils/WaverleyBoroughCouncil.py +119 -0
  152. uk_bin_collection/uk_bin_collection/councils/WealdenDistrictCouncil.py +86 -0
  153. uk_bin_collection/uk_bin_collection/councils/WelhatCouncil.py +73 -0
  154. uk_bin_collection/uk_bin_collection/councils/WestBerkshireCouncil.py +134 -0
  155. uk_bin_collection/uk_bin_collection/councils/WestLindseyDistrictCouncil.py +118 -0
  156. uk_bin_collection/uk_bin_collection/councils/WestLothianCouncil.py +103 -0
  157. uk_bin_collection/uk_bin_collection/councils/WestNorthamptonshireCouncil.py +34 -0
  158. uk_bin_collection/uk_bin_collection/councils/WestSuffolkCouncil.py +64 -0
  159. uk_bin_collection/uk_bin_collection/councils/WiganBoroughCouncil.py +97 -0
  160. uk_bin_collection/uk_bin_collection/councils/WiltshireCouncil.py +135 -0
  161. uk_bin_collection/uk_bin_collection/councils/WindsorAndMaidenheadCouncil.py +134 -0
  162. uk_bin_collection/uk_bin_collection/councils/WokingBoroughCouncil.py +114 -0
  163. uk_bin_collection/uk_bin_collection/councils/WyreCouncil.py +89 -0
  164. uk_bin_collection/uk_bin_collection/councils/YorkCouncil.py +45 -0
  165. uk_bin_collection/uk_bin_collection/councils/council_class_template/councilclasstemplate.py +33 -0
  166. uk_bin_collection/uk_bin_collection/get_bin_data.py +165 -0
  167. uk_bin_collection-0.74.0.dist-info/LICENSE +21 -0
  168. uk_bin_collection-0.74.0.dist-info/METADATA +247 -0
  169. uk_bin_collection-0.74.0.dist-info/RECORD +171 -0
  170. uk_bin_collection-0.74.0.dist-info/WHEEL +4 -0
  171. 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