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,70 @@
1
+ from datetime import datetime
2
+
3
+ from bs4 import BeautifulSoup
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
+ api_url = "https://www.salford.gov.uk/bins-and-recycling/bin-collection-days/your-bin-collections"
18
+ user_uprn = kwargs.get("uprn")
19
+
20
+ # Check the UPRN is valid
21
+ check_uprn(user_uprn)
22
+
23
+ # Create the form data
24
+ params = {
25
+ "UPRN": user_uprn,
26
+ }
27
+
28
+ # Make a request to the API
29
+ requests.packages.urllib3.disable_warnings()
30
+ response = requests.get(api_url, params=params)
31
+
32
+ # Make a BS4 object
33
+ soup = BeautifulSoup(response.text, features="html.parser")
34
+ soup.prettify()
35
+
36
+ data = {"bins": []}
37
+
38
+ # Get the div element
39
+ div_element = soup.find("div", {"class": "wastefurther"})
40
+
41
+ # Get the bins
42
+ bin_lists = div_element.find_all("ul")
43
+
44
+ # Loop through each <ul> tag to extract the bin information
45
+ for i, bin_list in enumerate(bin_lists):
46
+ # Find the <p> tag containing the bin type string
47
+ bin_type = bin_list.find_previous_sibling("p").find("strong").text.strip()
48
+
49
+ # Loop through each <li> tag in the <ul> tag to extract the collection date
50
+ for li in bin_list.find_all("li"):
51
+ # Convert the collection time to a datetime object
52
+ collection_date = datetime.strptime(li.text, "%A %d %B %Y")
53
+
54
+ # Add the bin to the data dict
55
+ data["bins"].append(
56
+ {
57
+ # remove the ":" from the end of the bin type
58
+ "type": bin_type[:-1],
59
+ "collectionDate": collection_date,
60
+ }
61
+ )
62
+
63
+ # Sort the bins by collection time
64
+ data["bins"] = sorted(data["bins"], key=lambda x: x["collectionDate"])
65
+
66
+ # Convert the datetime objects to strings in the desired format
67
+ for bin in data["bins"]:
68
+ bin["collectionDate"] = bin["collectionDate"].strftime(date_format)
69
+
70
+ return data
@@ -0,0 +1,106 @@
1
+ import time
2
+ from typing import Any
3
+
4
+ from selenium.webdriver.common.by import By
5
+ from selenium.webdriver.common.keys import Keys
6
+ from selenium.common.exceptions import TimeoutException, NoSuchElementException
7
+ from selenium.webdriver.support.ui import Select, WebDriverWait
8
+ from selenium.webdriver.support import expected_conditions as EC
9
+ from dateutil.parser import parse
10
+
11
+ from uk_bin_collection.uk_bin_collection.common import create_webdriver, date_format
12
+ from uk_bin_collection.uk_bin_collection.get_bin_data import AbstractGetBinDataClass
13
+
14
+
15
+ class CouncilClass(AbstractGetBinDataClass):
16
+ def wait_for_element_conditions(self, driver, conditions, timeout: int = 5):
17
+ try:
18
+ WebDriverWait(driver, timeout).until(conditions)
19
+ except TimeoutException:
20
+ print("Timed out waiting for page to load")
21
+ raise
22
+
23
+ def parse_data(self, page: str, **kwargs) -> dict:
24
+ driver = None
25
+ try:
26
+ web_driver = kwargs.get("web_driver")
27
+ headless = kwargs.get("headless")
28
+ page = "https://sevenoaks-dc-host01.oncreate.app/w/webpage/waste-collection-day"
29
+
30
+ # Assign user info
31
+ user_postcode = kwargs.get("postcode")
32
+ user_paon = kwargs.get("paon")
33
+
34
+ # Create Selenium webdriver
35
+ driver = create_webdriver(web_driver, headless)
36
+ driver.get(page)
37
+
38
+ # Enter postcode
39
+ postcode_css_selector = "#address_search_postcode"
40
+ self.wait_for_element_conditions(
41
+ driver,
42
+ EC.presence_of_element_located(
43
+ (By.CSS_SELECTOR, postcode_css_selector)
44
+ ),
45
+ )
46
+ postcode_input_box = driver.find_element(
47
+ By.CSS_SELECTOR, postcode_css_selector
48
+ )
49
+ postcode_input_box.send_keys(user_postcode)
50
+ postcode_input_box.send_keys(Keys.ENTER)
51
+
52
+ # Select the dropdown
53
+ self.wait_for_element_conditions(
54
+ driver, EC.presence_of_element_located((By.XPATH, "//select/option[2]"))
55
+ )
56
+ select_address_dropdown = Select(driver.find_element(By.XPATH, "//select"))
57
+
58
+ if user_paon is not None:
59
+ for option in select_address_dropdown.options:
60
+ if user_paon in option.text:
61
+ select_address_dropdown.select_by_visible_text(option.text)
62
+ break
63
+ else:
64
+ # If we've not been supplied an address, pick the second entry
65
+ select_address_dropdown.select_by_index(1)
66
+
67
+ # Grab the response blocks
68
+ response_xpath_selector = "//div[@data-class_name]//h4/../../../.."
69
+ self.wait_for_element_conditions(
70
+ driver,
71
+ EC.presence_of_element_located((By.XPATH, response_xpath_selector)),
72
+ )
73
+ elements = driver.find_elements(By.XPATH, response_xpath_selector)
74
+
75
+ # Iterate through them
76
+ data = {"bins": []}
77
+ for element in elements:
78
+ try:
79
+ raw_bin_name = element.find_element(By.TAG_NAME, "h4").text
80
+ raw_next_collection_date = element.find_elements(
81
+ By.XPATH, ".//div[input]"
82
+ )[1].text
83
+
84
+ parsed_bin_date = parse(
85
+ raw_next_collection_date, fuzzy_with_tokens=True
86
+ )[0]
87
+
88
+ dict_data = {
89
+ "type": raw_bin_name,
90
+ "collectionDate": parsed_bin_date.strftime(date_format),
91
+ }
92
+
93
+ data["bins"].append(dict_data)
94
+
95
+ except (IndexError, NoSuchElementException):
96
+ print("Error finding element for bin")
97
+ except Exception as e:
98
+ # Here you can log the exception if needed
99
+ print(f"An error occurred: {e}")
100
+ # Optionally, re-raise the exception if you want it to propagate
101
+ raise
102
+ finally:
103
+ # This block ensures that the driver is closed regardless of an exception
104
+ if driver:
105
+ driver.quit()
106
+ return data
@@ -0,0 +1,54 @@
1
+ from bs4 import BeautifulSoup
2
+ from uk_bin_collection.uk_bin_collection.common import *
3
+ from uk_bin_collection.uk_bin_collection.get_bin_data import AbstractGetBinDataClass
4
+
5
+
6
+ # import the wonderful Beautiful Soup and the URL grabber
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
+ # Make a BS4 object
16
+ soup = BeautifulSoup(page.text, features="html.parser")
17
+ soup.prettify()
18
+
19
+ # Form a JSON wrapper
20
+ data = {"bins": []}
21
+
22
+ # Search for the specific table using BS4
23
+ rows = soup.find("table", {"class": re.compile("table")}).find_all("tr")
24
+
25
+ # Loops the Rows
26
+ for row in rows:
27
+ cells = row.find_all(
28
+ "td", {"class": lambda L: L and L.startswith("service-name")}
29
+ )
30
+
31
+ if len(cells) > 0:
32
+ collectionDatesRawData = row.find_all(
33
+ "td", {"class": lambda L: L and L.startswith("next-service")}
34
+ )[0].get_text(strip=True)
35
+ collectionDate = collectionDatesRawData[
36
+ 16 : len(collectionDatesRawData)
37
+ ].split(",")
38
+ bin_type = row.find_all(
39
+ "td", {"class": lambda L: L and L.startswith("service-name")}
40
+ )[0].h4.get_text(strip=True)
41
+
42
+ for collectDate in collectionDate:
43
+ # Make each Bin element in the JSON
44
+ dict_data = {
45
+ "type": bin_type,
46
+ "collectionDate": datetime.strptime(
47
+ collectDate.strip(), "%d %b %Y"
48
+ ).strftime(date_format),
49
+ }
50
+
51
+ # Add data to the main JSON Wrapper
52
+ data["bins"].append(dict_data)
53
+
54
+ return data
@@ -0,0 +1,45 @@
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
+ # Make a BS4 object
16
+ soup = BeautifulSoup(page.text, features="html.parser")
17
+ soup.prettify()
18
+
19
+ # Form a JSON wrapper
20
+ data = {"bins": []}
21
+
22
+ # Find section with bins in
23
+ sections = (
24
+ soup.find("div", {"class": "container results-table-wrapper"})
25
+ .find("tbody")
26
+ .find_all("tr")
27
+ )
28
+
29
+ # For each bin section, get the text and the list elements
30
+ for item in sections:
31
+ words = item.find_next("a").text.split()[1:2]
32
+ bin_type = " ".join(words).capitalize()
33
+ date = (
34
+ item.find("td", {"class": "next-service"})
35
+ .find_next("span")
36
+ .next_sibling.strip()
37
+ )
38
+ next_collection = datetime.strptime(date, "%d/%m/%Y")
39
+ dict_data = {
40
+ "type": bin_type,
41
+ "collectionDate": next_collection.strftime(date_format),
42
+ }
43
+ data["bins"].append(dict_data)
44
+
45
+ return data
@@ -0,0 +1,48 @@
1
+ from bs4 import BeautifulSoup
2
+ from uk_bin_collection.uk_bin_collection.common import *
3
+ from uk_bin_collection.uk_bin_collection.get_bin_data import AbstractGetBinDataClass
4
+
5
+
6
+ # import the wonderful Beautiful Soup and the URL grabber
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
+ # Make a BS4 object
16
+ soup = BeautifulSoup(page.text, features="html.parser")
17
+ soup.prettify()
18
+
19
+ data = {"bins": []}
20
+ collections = []
21
+
22
+ for bin in soup.find_all("div", class_="mb-4 card"):
23
+ bin_type = (
24
+ bin.find("div", class_="card-title")
25
+ .find("h4")
26
+ .get_text()
27
+ .strip()
28
+ .replace("Wheelie ", "")
29
+ )
30
+ bin_date_text = (
31
+ bin.find_all("div", class_="mt-1")[1]
32
+ .find("strong")
33
+ .get_text()
34
+ .strip()
35
+ .replace(",", "")
36
+ )
37
+ bin_date = datetime.strptime(bin_date_text, "%A %d %B %Y")
38
+ collections.append((bin_type, bin_date))
39
+
40
+ ordered_data = sorted(collections, key=lambda x: x[1])
41
+ for item in ordered_data:
42
+ dict_data = {
43
+ "type": item[0].capitalize(),
44
+ "collectionDate": item[1].strftime(date_format),
45
+ }
46
+ data["bins"].append(dict_data)
47
+
48
+ return data
@@ -0,0 +1,203 @@
1
+ from bs4 import BeautifulSoup
2
+ from uk_bin_collection.uk_bin_collection.common import *
3
+ from uk_bin_collection.uk_bin_collection.get_bin_data import AbstractGetBinDataClass
4
+
5
+
6
+ # import the wonderful Beautiful Soup and the URL grabber
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
+ user_postcode = kwargs.get("postcode")
16
+ check_postcode(user_postcode)
17
+ user_uprn = kwargs.get("uprn")
18
+ check_uprn(user_uprn)
19
+
20
+ headers = {
21
+ "User-Agent": "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) "
22
+ "Chrome/87.0.4280.141 Safari/537.36"
23
+ }
24
+
25
+ requests.packages.urllib3.disable_warnings()
26
+ with requests.Session() as s:
27
+ # Set Headers
28
+ s.headers = headers
29
+
30
+ # Get the first page - This is the Search for property by Post Code page
31
+ resource = s.get(
32
+ "https://iweb.itouchvision.com/portal/f?p=customer:BIN_DAYS:::NO:RP:UID:625C791B4D9301137723E9095361401AE8C03934"
33
+ )
34
+ # Create a BeautifulSoup object from the page's HTML
35
+ soup = BeautifulSoup(resource.text, "html.parser")
36
+
37
+ # The page contains a number of values that must be passed into subsequent requests - extract them here
38
+ payload = {
39
+ i["name"]: i.get("value", "") for i in soup.select("input[name]")
40
+ }
41
+ payload2 = {
42
+ i["data-for"]: i.get("value", "")
43
+ for i in soup.select("input[data-for]")
44
+ }
45
+ payload_salt = soup.select_one('input[id="pSalt"]').get("value")
46
+ payload_protected = soup.select_one('input[id="pPageItemsProtected"]').get(
47
+ "value"
48
+ )
49
+
50
+ # Add the PostCode and 'SEARCH' to the payload
51
+ payload["p_request"] = "SEARCH"
52
+ payload["P153_POST_CODE"] = user_postcode
53
+
54
+ # Manipulate the lists and build the JSON that must be submitted in further requests - some data is nested
55
+ merged_list = {**payload, **payload2}
56
+ new_list = []
57
+ other_list = {}
58
+ for key in merged_list.keys():
59
+ temp_list = {}
60
+ val = merged_list[key]
61
+ if key in [
62
+ "P153_UPRN",
63
+ "P153_TEMP",
64
+ "P153_SYSDATE",
65
+ "P0_LANGUAGE",
66
+ "P153_POST_CODE",
67
+ ]:
68
+ temp_list = {"n": key, "v": val}
69
+ new_list.append(temp_list)
70
+ elif key in [
71
+ "p_flow_id",
72
+ "p_flow_step_id",
73
+ "p_instance",
74
+ "p_page_submission_id",
75
+ "p_request",
76
+ "p_reload_on_submit",
77
+ ]:
78
+ other_list[key] = val
79
+ else:
80
+ temp_list = {"n": key, "v": "", "ck": val}
81
+ new_list.append(temp_list)
82
+
83
+ json_builder = {
84
+ "pageItems": {
85
+ "itemsToSubmit": new_list,
86
+ "protected": payload_protected,
87
+ "rowVersion": "",
88
+ "formRegionChecksums": [],
89
+ },
90
+ "salt": payload_salt,
91
+ }
92
+ json_object = json.dumps(json_builder, separators=(",", ":"))
93
+ other_list["p_json"] = json_object
94
+
95
+ # Set Referrer header
96
+ s.headers.update(
97
+ {
98
+ "referer": "https://iweb.itouchvision.com/portal/f?p=customer:BIN_DAYS:::NO:RP:UID:625C791B4D9301137723E9095361401AE8C03934"
99
+ }
100
+ )
101
+
102
+ # Generate POST including all the JSON we just built
103
+ s.post(
104
+ "https://iweb.itouchvision.com/portal/wwv_flow.accept", data=other_list
105
+ )
106
+
107
+ # The second page on the portal would normally allow you to select your property from a dropdown list of
108
+ # those that are at the postcode entered on the previous page
109
+ # The required cookies are stored within the session so re-use the session to keep them
110
+ resource = s.get(
111
+ "https://iweb.itouchvision.com/portal/itouchvision/r/customer/bin_days"
112
+ )
113
+
114
+ # Create a BeautifulSoup object from the page's HTML
115
+ soup = BeautifulSoup(resource.text, "html.parser")
116
+
117
+ # The page contains a number of values that must be passed into subsequent requests - extract them here
118
+ payload = {
119
+ i["name"]: i.get("value", "") for i in soup.select("input[name]")
120
+ }
121
+ payload2 = {
122
+ i["data-for"]: i.get("value", "")
123
+ for i in soup.select("input[data-for]")
124
+ }
125
+ payload_salt = soup.select_one('input[id="pSalt"]').get("value")
126
+ payload_protected = soup.select_one('input[id="pPageItemsProtected"]').get(
127
+ "value"
128
+ )
129
+
130
+ # Add the UPRN and 'SUBMIT' to the payload
131
+ payload["p_request"] = "SUBMIT"
132
+ payload["P153_UPRN"] = user_uprn
133
+
134
+ # Manipulate the lists and build the JSON that must be submitted in further requests - some data is nested
135
+ merged_list = {**payload, **payload2}
136
+ new_list = []
137
+ other_list = {}
138
+ for key in merged_list.keys():
139
+ temp_list = {}
140
+ val = merged_list[key]
141
+ if key in ["P153_UPRN", "P153_TEMP", "P153_SYSDATE", "P0_LANGUAGE"]:
142
+ temp_list = {"n": key, "v": val}
143
+ new_list.append(temp_list)
144
+ elif key in ["P153_ZABY"]:
145
+ temp_list = {"n": key, "v": "1", "ck": val}
146
+ new_list.append(temp_list)
147
+ elif key in ["P153_POST_CODE"]:
148
+ temp_list = {"n": key, "v": user_postcode, "ck": val}
149
+ new_list.append(temp_list)
150
+ elif key in [
151
+ "p_flow_id",
152
+ "p_flow_step_id",
153
+ "p_instance",
154
+ "p_page_submission_id",
155
+ "p_request",
156
+ "p_reload_on_submit",
157
+ ]:
158
+ other_list[key] = val
159
+ else:
160
+ temp_list = {"n": key, "v": "", "ck": val}
161
+ new_list.append(temp_list)
162
+
163
+ json_builder = {
164
+ "pageItems": {
165
+ "itemsToSubmit": new_list,
166
+ "protected": payload_protected,
167
+ "rowVersion": "",
168
+ "formRegionChecksums": [],
169
+ },
170
+ "salt": payload_salt,
171
+ }
172
+
173
+ json_object = json.dumps(json_builder, separators=(",", ":"))
174
+ other_list["p_json"] = json_object
175
+
176
+ # Generate POST including all the JSON we just built
177
+ s.post(
178
+ "https://iweb.itouchvision.com/portal/wwv_flow.accept", data=other_list
179
+ )
180
+
181
+ # The third and final page on the portal shows the detail of the waste collection services
182
+ # The required cookies are stored within the session so re-use the session to keep them
183
+ resource = s.get(
184
+ "https://iweb.itouchvision.com/portal/itouchvision/r/customer/bin_days"
185
+ )
186
+
187
+ # Create a BeautifulSoup object from the page's HTML
188
+ soup = BeautifulSoup(resource.text, "html.parser")
189
+ data = {"bins": []}
190
+
191
+ # Loop through the items on the page and build a JSON object for ingestion
192
+ for item in soup.select(".t-MediaList-item"):
193
+ for value in item.select(".t-MediaList-body"):
194
+ dict_data = {
195
+ "type": value.select("span")[1].get_text(strip=True).title(),
196
+ "collectionDate": datetime.strptime(
197
+ value.select(".t-MediaList-desc")[0].get_text(strip=True),
198
+ "%A, %d %B, %Y",
199
+ ).strftime(date_format),
200
+ }
201
+ data["bins"].append(dict_data)
202
+
203
+ return data
@@ -0,0 +1,73 @@
1
+ import json
2
+ from datetime import timedelta
3
+
4
+ import requests
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 both the passed UPRN and postcode
19
+ user_uprn = kwargs.get("uprn")
20
+ check_uprn(user_uprn)
21
+ user_postcode = kwargs.get("postcode")
22
+ check_postcode(user_postcode)
23
+
24
+ # Build the headers, specify the parameters and then make a GET for the calendar
25
+ headers = {
26
+ "Connection": "Keep-Alive",
27
+ "User-Agent": "okhttp/3.14.9",
28
+ }
29
+ params = {
30
+ "end_date": "2024-01-01",
31
+ "rn": user_uprn,
32
+ "device": "undefined",
33
+ "postcode": user_postcode,
34
+ "OS": "android",
35
+ "OS_ver": "31",
36
+ "app_ver": "35",
37
+ }
38
+ requests.packages.urllib3.disable_warnings()
39
+ response = requests.get(
40
+ "http://www.sac-bins.co.uk/get_calendar.php", params=params, headers=headers
41
+ )
42
+
43
+ # Load the response as JSON
44
+ json_data = json.loads(response.text)
45
+
46
+ # The response loads well over a year's worth of data, so figure out some dates to limit output
47
+ today = datetime.today()
48
+ eight_weeks = datetime.today() + timedelta(days=8 * 7)
49
+ data = {"bins": []}
50
+
51
+ # The bin titles are pretty weird and colours are too basic, so make the names match the app
52
+ bin_friendly_names = {
53
+ "blue": "Blue Bin",
54
+ "red": "Food Caddy",
55
+ "green": "Green Bin",
56
+ "grey": "Grey Bin",
57
+ "purple": "Purple Bin",
58
+ "brown": "Brown Bin",
59
+ }
60
+
61
+ # Loop through the results. When a date is found that's on or greater than today's date AND less than
62
+ # eight weeks away, we want it in the output. So look up the friendly name and add it in.
63
+ for item in json_data:
64
+ bin_date = datetime.strptime(item["start"], "%Y-%m-%d").date()
65
+ if today.date() <= bin_date <= eight_weeks.date():
66
+ bin_type = bin_friendly_names.get(item["className"])
67
+ dict_data = {
68
+ "type": bin_type,
69
+ "collectionDate": bin_date.strftime(date_format),
70
+ }
71
+ data["bins"].append(dict_data)
72
+
73
+ return data
@@ -0,0 +1,65 @@
1
+ import requests
2
+ from bs4 import BeautifulSoup
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
+ # import the wonderful Beautiful Soup and the URL grabber
8
+ class CouncilClass(AbstractGetBinDataClass):
9
+ """
10
+ Concrete classes have to implement all abstract operations of the
11
+ base class. They can also override some operations with a default
12
+ implementation.
13
+ """
14
+
15
+ def parse_data(self, page: str, **kwargs) -> dict:
16
+ API_URLS = {
17
+ "address_search": "https://servicelayer3c.azure-api.net/wastecalendar/address/search/",
18
+ "collection": "https://servicelayer3c.azure-api.net/wastecalendar/collection/search/{}/",
19
+ }
20
+ headers = {
21
+ "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.102 Safari/537.36",
22
+ }
23
+
24
+ user_postcode = kwargs.get("postcode")
25
+ user_paon = kwargs.get("paon")
26
+
27
+ # Establish session
28
+ requests.packages.urllib3.disable_warnings()
29
+ s = requests.Session()
30
+ r = s.get(
31
+ "https://www.scambs.gov.uk/recycling-and-bins/find-your-household-bin-collection-day/",
32
+ headers=headers,
33
+ )
34
+
35
+ # Select address
36
+ r = s.get(
37
+ API_URLS["address_search"],
38
+ headers=headers,
39
+ params={"postCode": user_postcode},
40
+ )
41
+ addresses = r.json()
42
+ address_ids = [
43
+ x["id"] for x in addresses if x["houseNumber"].capitalize() == user_paon
44
+ ]
45
+ if len(address_ids) == 0:
46
+ raise Exception(f"Could not match address {user_paon}, {user_postcode}")
47
+
48
+ # Get the schedule
49
+ r = s.get(
50
+ API_URLS["collection"].format(address_ids[0]),
51
+ headers=headers,
52
+ )
53
+ schedule = r.json()["collections"]
54
+
55
+ data = {"bins": []}
56
+
57
+ for collection in schedule:
58
+ dt = datetime.strptime(collection["date"], "%Y-%m-%dT%H:%M:%SZ").strftime(
59
+ date_format
60
+ )
61
+ for round in collection["roundTypes"]:
62
+ dict_data = {"type": round.title(), "collectionDate": dt}
63
+ data["bins"].append(dict_data)
64
+
65
+ return data