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,101 @@
1
+ import difflib
2
+ from datetime import date, datetime
3
+
4
+ import requests
5
+ from bs4 import BeautifulSoup
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
+ base_url = "https://lisburn.isl-fusion.com"
19
+
20
+ def parse_data(self, page: str, **kwargs) -> dict:
21
+ """
22
+ This function will make a request to the search endpoint with the postcode, extract the
23
+ house numbers from the responses, then retrieve the ID of the entry with the house number that matches,
24
+ to then retrieve the bin schedule.
25
+
26
+ The API here is a weird combination of HTML in json responses.
27
+ """
28
+ postcode = kwargs.get("postcode")
29
+ paon = kwargs.get("paon")
30
+
31
+ if not postcode:
32
+ raise ValueError("Must provide a postcode")
33
+
34
+ if not paon:
35
+ raise ValueError("Must provide a house number")
36
+
37
+ search_url = f"{self.base_url}/address/{postcode}"
38
+
39
+ requests.packages.urllib3.disable_warnings()
40
+ s = requests.Session()
41
+ response = s.get(search_url)
42
+ response.raise_for_status()
43
+
44
+ address_data = response.json()
45
+
46
+ address_list = address_data["html"]
47
+
48
+ soup = BeautifulSoup(address_list, features="html.parser")
49
+
50
+ address_by_id = {}
51
+
52
+ for li in soup.find_all("li"):
53
+ link = li.find_all("a")[0]
54
+ address_id = link.attrs["href"]
55
+ address = link.text
56
+
57
+ address_by_id[address_id] = address
58
+
59
+ addresses = list(address_by_id.values())
60
+
61
+ common = difflib.SequenceMatcher(
62
+ a=addresses[0], b=addresses[1]
63
+ ).find_longest_match()
64
+ extra_bit = addresses[0][common.a : common.a + common.size]
65
+
66
+ ids_by_paon = {
67
+ a.replace(extra_bit, ""): a_id.replace("/view/", "").replace("/", "")
68
+ for a_id, a in address_by_id.items()
69
+ }
70
+
71
+ property_id = ids_by_paon.get(paon)
72
+ if not property_id:
73
+ raise ValueError(
74
+ f"Invalid house number, valid values are {', '.join(ids_by_paon.keys())}"
75
+ )
76
+
77
+ today = date.today()
78
+ calendar_url = (
79
+ f"{self.base_url}/calendar/{property_id}/{today.strftime('%Y-%m-%d')}"
80
+ )
81
+ response = s.get(calendar_url)
82
+ response.raise_for_status()
83
+ calendar_data = response.json()
84
+ next_collections = calendar_data["nextCollections"]
85
+
86
+ collections = list(next_collections["collections"].values())
87
+
88
+ data = {"bins": []}
89
+
90
+ for collection in collections:
91
+ collection_date = datetime.strptime(collection["date"], "%Y-%m-%d")
92
+ bins = [c["name"] for c in collection["collections"].values()]
93
+
94
+ for bin in bins:
95
+ data["bins"].append(
96
+ {
97
+ "type": bin,
98
+ "collectionDate": collection_date.strftime(date_format),
99
+ }
100
+ )
101
+ return data
@@ -0,0 +1,65 @@
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
+ from dateutil.relativedelta import relativedelta
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
+ # Add in some variables we need
17
+ data = {"bins": []}
18
+ collections = []
19
+ curr_date = datetime.today()
20
+
21
+ # Parse the page
22
+ soup = BeautifulSoup(page.text, features="html.parser")
23
+ soup.prettify()
24
+
25
+ # Get all table rows on the page - enumerate gives us an index, which is handy for to keep a row count.
26
+ # In this case, the first (0th) row is headings, so we can skip it, then parse the other data.
27
+ for idx, row in enumerate(soup.find_all("tr")):
28
+ if idx == 0:
29
+ continue
30
+
31
+ row_type = row.find("th").text.strip()
32
+ row_data = row.find_all("td")
33
+
34
+ # When we get the row data, we can loop through it all and parse it to datetime. Because there are no
35
+ # years, we must add it in, then check if we need to overflow it to the following year.
36
+ for item in row_data:
37
+ item_text = item.text.strip()
38
+
39
+ if item_text == "Today":
40
+ collections.append((row_type, curr_date))
41
+ elif item_text == "Tomorrow":
42
+ collections.append((row_type, curr_date + relativedelta(days=1)))
43
+ else:
44
+ bin_date = datetime.strptime(
45
+ remove_ordinal_indicator_from_date_string(item_text),
46
+ "%A, %d %B",
47
+ ).replace(year=curr_date.year)
48
+
49
+ if curr_date.month == 12 and bin_date.month == 1:
50
+ bin_date = bin_date + relativedelta(years=1)
51
+
52
+ collections.append((row_type, bin_date))
53
+
54
+ # Sort the text and list elements by date
55
+ ordered_data = sorted(collections, key=lambda x: x[1])
56
+
57
+ # Put the elements into the dictionary
58
+ for item in ordered_data:
59
+ dict_data = {
60
+ "type": item[0],
61
+ "collectionDate": item[1].strftime(date_format),
62
+ }
63
+ data["bins"].append(dict_data)
64
+
65
+ return data
@@ -0,0 +1,82 @@
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
+ api_url = "https://www.hounslow.gov.uk/homepage/86/recycling_and_waste_collection_day_finder"
16
+ user_uprn = kwargs.get("uprn")
17
+
18
+ # Check the UPRN is valid
19
+ check_uprn(user_uprn)
20
+
21
+ # Create the form data
22
+ form_data = {
23
+ "UPRN": user_uprn,
24
+ }
25
+
26
+ # Make a request to the API
27
+ requests.packages.urllib3.disable_warnings()
28
+ response = requests.post(api_url, data=form_data)
29
+
30
+ # Make a BS4 object
31
+ soup = BeautifulSoup(response.text, features="html.parser")
32
+ soup.prettify()
33
+
34
+ data = {"bins": []}
35
+
36
+ # Get the div element
37
+ div_element = soup.find("div", {"class": "bin_day_main_wrapper"})
38
+
39
+ # Get all bins with their corresponding dates using list comprehension
40
+ # This creates a list of tuples, where each tuple contains the bin type and collection date
41
+ bins_with_dates = [
42
+ (
43
+ bin.get_text().strip(),
44
+ h4.get_text().replace("This ", "").replace("Next ", ""),
45
+ )
46
+ # This first for loop iterates over each h4 element
47
+ for h4 in div_element.find_all("h4")
48
+ # This nested for loop iterates over each li element within the corresponding ul element
49
+ for bin in h4.find_next_sibling("ul").find_all("li")
50
+ ]
51
+
52
+ for bin_type, collection_date in bins_with_dates:
53
+ if "-" in collection_date:
54
+ date_part = collection_date.split(" - ")[1]
55
+ data["bins"].append(
56
+ {
57
+ "type": bin_type,
58
+ "collectionDate": datetime.strptime(
59
+ date_part, "%d %b %Y"
60
+ ).strftime(date_format),
61
+ }
62
+ )
63
+ elif len(collection_date.split(" ")) == 4:
64
+ data["bins"].append(
65
+ {
66
+ "type": bin_type,
67
+ "collectionDate": datetime.strptime(
68
+ collection_date, "%A %d %b %Y"
69
+ ).strftime(date_format),
70
+ }
71
+ )
72
+ else:
73
+ data["bins"].append(
74
+ {
75
+ "type": bin_type,
76
+ "collectionDate": datetime.strptime(
77
+ collection_date, "%d %b %Y"
78
+ ).strftime(date_format),
79
+ }
80
+ )
81
+
82
+ return data
@@ -0,0 +1,161 @@
1
+ import re
2
+ import requests
3
+ from bs4 import BeautifulSoup
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
+ # This script pulls (in one hit) the data from Bromley Council Bins Data
9
+ import datetime
10
+ from datetime import datetime
11
+ from selenium.webdriver.common.by import By
12
+ from selenium.webdriver.support import expected_conditions as EC
13
+ from selenium.webdriver.support.ui import Select
14
+ from selenium.webdriver.support.wait import WebDriverWait
15
+ from selenium.webdriver.common.keys import Keys
16
+ import time
17
+
18
+
19
+ # import the wonderful Beautiful Soup and the URL grabber
20
+ class CouncilClass(AbstractGetBinDataClass):
21
+ """
22
+ Concrete classes have to implement all abstract operations of the
23
+ base class. They can also override some operations with a default
24
+ implementation.
25
+ """
26
+
27
+ def parse_data(self, page: str, **kwargs) -> dict:
28
+ driver = None
29
+ try:
30
+ data = {"bins": []}
31
+ headers = {"User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64)"}
32
+
33
+ uprn = kwargs.get("uprn")
34
+ postcode = kwargs.get("postcode")
35
+ web_driver = kwargs.get("web_driver")
36
+ headless = kwargs.get("headless")
37
+ driver = create_webdriver(web_driver, headless)
38
+ driver.get(kwargs.get("url"))
39
+
40
+ wait = WebDriverWait(driver, 60)
41
+ post_code_search = wait.until(
42
+ EC.presence_of_element_located(
43
+ (By.XPATH, f"//input[contains(@class, 'searchAddress')]")
44
+ )
45
+ )
46
+ post_code_search.send_keys(postcode)
47
+
48
+ submit_btn = wait.until(
49
+ EC.presence_of_element_located(
50
+ (By.XPATH, f"//button[contains(@class, 'searchAddressButton')]")
51
+ )
52
+ )
53
+
54
+ submit_btn.send_keys(Keys.ENTER)
55
+
56
+ address_link = wait.until(
57
+ EC.presence_of_element_located(
58
+ (By.XPATH, f'//a[contains(@data-uprn,"{uprn}")]')
59
+ )
60
+ )
61
+
62
+ address_link.send_keys(Keys.ENTER)
63
+
64
+ address_results = wait.until(
65
+ EC.presence_of_element_located(
66
+ (By.CLASS_NAME, "your-collection-schedule-container")
67
+ )
68
+ )
69
+
70
+ # Make a BS4 object
71
+ soup = BeautifulSoup(driver.page_source, features="html.parser")
72
+ data = {"bins": []}
73
+
74
+ # Get the current month and year
75
+ current_month = datetime.now().month
76
+ current_year = datetime.now().year
77
+
78
+ # Function to extract collection data
79
+ def extract_collection_data(collection_div, collection_type):
80
+ if collection_div:
81
+ date_element = (
82
+ collection_div.find(class_="recycling-collection-day-numeric")
83
+ or collection_div.find(class_="refuse-collection-day-numeric")
84
+ or collection_div.find(class_="garden-collection-day-numeric")
85
+ )
86
+ month_element = (
87
+ collection_div.find(class_="recycling-collection-month")
88
+ or collection_div.find(class_="refuse-collection-month")
89
+ or collection_div.find(class_="garden-collection-month")
90
+ )
91
+
92
+ if date_element and month_element:
93
+ collection_date = date_element.get_text(strip=True)
94
+ collection_month = month_element.get_text(strip=True)
95
+
96
+ # Combine month, date, and year into a string
97
+ date_string = (
98
+ f"{collection_date} {collection_month} {current_year}"
99
+ )
100
+
101
+ try:
102
+ # Convert the date string to a datetime object
103
+ collection_date_obj = datetime.strptime(
104
+ date_string, "%d %B %Y"
105
+ )
106
+
107
+ # Check if the month is ahead of the current month
108
+ if collection_date_obj.month >= current_month:
109
+ # If the month is ahead, use the current year
110
+ formatted_date = collection_date_obj.strftime(
111
+ date_format
112
+ )
113
+ else:
114
+ # If the month is before the current month, use the next year
115
+ formatted_date = collection_date_obj.replace(
116
+ year=current_year + 1
117
+ ).strftime(date_format)
118
+ # Create a dictionary for each collection entry
119
+ dict_data = {
120
+ "type": collection_type,
121
+ "collectionDate": formatted_date,
122
+ }
123
+
124
+ # Append dictionary data to the 'bins' list in the 'data' dictionary
125
+ data["bins"].append(dict_data)
126
+
127
+ except ValueError as e:
128
+ # Handle the case where the date format is invalid
129
+ formatted_date = "Invalid Date Format"
130
+
131
+ # Extract Recycling collection data
132
+ extract_collection_data(
133
+ soup.find(
134
+ class_="container-fluid RegularCollectionDay"
135
+ ).find_next_sibling("div"),
136
+ "Recycling",
137
+ )
138
+
139
+ # Extract Refuse collection data
140
+ for refuse_div in soup.find_all(
141
+ class_="container-fluid RegularCollectionDay"
142
+ ):
143
+ extract_collection_data(refuse_div, "Refuse")
144
+
145
+ # Extract Garden Waste collection data
146
+ extract_collection_data(
147
+ soup.find(class_="container-fluid gardenwasteCollectionDay"),
148
+ "Garden Waste",
149
+ )
150
+
151
+ # Print the extracted data
152
+ except Exception as e:
153
+ # Here you can log the exception if needed
154
+ print(f"An error occurred: {e}")
155
+ # Optionally, re-raise the exception if you want it to propagate
156
+ raise
157
+ finally:
158
+ # This block ensures that the driver is closed regardless of an exception
159
+ if driver:
160
+ driver.quit()
161
+ return data
@@ -0,0 +1,52 @@
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
+ data = {"bins": []}
16
+ uprn = kwargs.get("uprn")
17
+ check_uprn(uprn)
18
+
19
+ requests.packages.urllib3.disable_warnings()
20
+ response = requests.get(
21
+ f"https://maldon.suez.co.uk/maldon/ServiceSummary?uprn={uprn}",
22
+ headers={"User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64)"},
23
+ )
24
+ if response.status_code != 200:
25
+ raise ValueError("No bin data found for provided UPRN.")
26
+
27
+ soup = BeautifulSoup(response.text, features="html.parser")
28
+ collections = soup.find_all("div", {"class": "panel"})
29
+ for c in collections:
30
+ binType = c.find("div", {"class": "panel-heading"}).get_text(strip=True)
31
+ collectionDate = ""
32
+ rows = c.find("div", {"class": "panel-body"}).find_all(
33
+ "div", {"class": "row"}
34
+ )
35
+ for row in rows:
36
+ if row.find("strong").get_text(strip=True).lower() == "next collection":
37
+ collectionDate = row.find("div", {"class": "col-sm-9"}).get_text(
38
+ strip=True
39
+ )
40
+
41
+ if collectionDate != "":
42
+ collection_data = {
43
+ "type": binType,
44
+ "collectionDate": collectionDate,
45
+ }
46
+ data["bins"].append(collection_data)
47
+
48
+ data["bins"].sort(
49
+ key=lambda x: datetime.strptime(x.get("collectionDate"), date_format)
50
+ )
51
+
52
+ return data
@@ -0,0 +1,57 @@
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
+ api_url = "https://swict.malvernhills.gov.uk/mhdcroundlookup/HandleSearchScreen"
16
+
17
+ user_uprn = kwargs.get("uprn")
18
+ # Check the UPRN is valid
19
+ check_uprn(user_uprn)
20
+
21
+ # Create the form data
22
+ form_data = {"nmalAddrtxt": "", "alAddrsel": user_uprn}
23
+ # expects postcode to be looked up and then uprn used.
24
+ # we can just provide uprn
25
+
26
+ # Make a request to the API
27
+ requests.packages.urllib3.disable_warnings()
28
+ response = requests.post(api_url, data=form_data, verify=False)
29
+
30
+ # Make a BS4 object
31
+ soup = BeautifulSoup(response.text, features="html.parser")
32
+ soup.prettify()
33
+
34
+ # Find results table
35
+ table_element = soup.find("table")
36
+ table_body = table_element.find("tbody")
37
+ rows = table_body.find_all("tr")
38
+
39
+ data = {"bins": []}
40
+
41
+ for row in rows:
42
+ columns = row.find_all("td")
43
+ columns = [ele.text.strip() for ele in columns]
44
+
45
+ thisCollection = [ele for ele in columns if ele] # Get rid of empty values
46
+
47
+ # if not signed up for garden waste, this appears as Not applicable
48
+ if "Not applicable" not in thisCollection[1]:
49
+ bin_type = thisCollection[0].replace("collection", "").strip()
50
+ date = datetime.strptime(thisCollection[1], "%A %d/%m/%Y")
51
+ dict_data = {
52
+ "type": bin_type,
53
+ "collectionDate": date.strftime(date_format),
54
+ }
55
+ data["bins"].append(dict_data)
56
+
57
+ return data
@@ -0,0 +1,106 @@
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
+ 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
+ # Get and check UPRN
18
+ user_uprn = kwargs.get("uprn")
19
+ check_uprn(user_uprn)
20
+
21
+ # Start a new session to walk through the form
22
+ requests.packages.urllib3.disable_warnings()
23
+ s = requests.session()
24
+
25
+ # There's a cookie that makes the whole thing valid when you search for a postcode,
26
+ # but postcode and UPRN is a hassle imo, so this makes a request for the session to get a cookie
27
+ # using a Manchester City Council postcode I hardcoded in the data payload
28
+ postcode_request_header = {
29
+ "authority": "www.manchester.gov.uk",
30
+ "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,"
31
+ "image/webp,image/apng,*/*;q=0.8",
32
+ "accept-language": "en-GB,en;q=0.6",
33
+ "cache-control": "max-age=0",
34
+ # Requests sorts cookies= alphabetically
35
+ "origin": "https://www.manchester.gov.uk",
36
+ "referer": "https://www.manchester.gov.uk/bincollections",
37
+ "sec-fetch-dest": "document",
38
+ "sec-fetch-mode": "navigate",
39
+ "sec-fetch-site": "same-origin",
40
+ "sec-fetch-user": "?1",
41
+ "sec-gpc": "1",
42
+ "upgrade-insecure-requests": "1",
43
+ "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, "
44
+ "like Gecko) Chrome/104.0.5112.102 Safari/537.36",
45
+ }
46
+ postcode_request_data = {
47
+ "mcc_bin_dates_search_term": "M2 5DB",
48
+ "mcc_bin_dates_submit": "Go",
49
+ }
50
+ response = s.post(
51
+ "https://www.manchester.gov.uk/bincollections",
52
+ headers=postcode_request_header,
53
+ data=postcode_request_data,
54
+ )
55
+
56
+ # Make a POST with the same cookie-fied session using the user's UPRN data
57
+ uprn_request_headers = {
58
+ "authority": "www.manchester.gov.uk",
59
+ "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
60
+ "accept-language": "en-GB,en;q=0.6",
61
+ "cache-control": "max-age=0",
62
+ # Requests sorts cookies= alphabetically
63
+ # 'cookie': 'TestCookie=Test; CookieConsent={stamp:%27D8rypjMDBJhpfMWybSMdGXP1hCZWGJYtGETiMTu1UuXTdRIKl8SU5g==%27%2Cnecessary:true%2Cpreferences:true%2Cstatistics:true%2Cmarketing:true%2Cver:6%2Cutc:1661783732090%2Cregion:%27gb%27}; PHPSESSID=kElJxYAt%2Cf-4ZWoskt0s5tn32BUQRXDYUVp3G-NsqOAOaeIcKlm2T4r7ATSgqfz6',
64
+ "origin": "https://www.manchester.gov.uk",
65
+ "referer": "https://www.manchester.gov.uk/bincollections",
66
+ "sec-fetch-dest": "document",
67
+ "sec-fetch-mode": "navigate",
68
+ "sec-fetch-site": "same-origin",
69
+ "sec-fetch-user": "?1",
70
+ "sec-gpc": "1",
71
+ "upgrade-insecure-requests": "1",
72
+ "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",
73
+ }
74
+ uprn_request_data = {
75
+ "mcc_bin_dates_uprn": user_uprn,
76
+ "mcc_bin_dates_submit": "Go",
77
+ }
78
+ response = s.post(
79
+ "https://www.manchester.gov.uk/bincollections",
80
+ headers=uprn_request_headers,
81
+ data=uprn_request_data,
82
+ )
83
+
84
+ # Make that BS4 object and use it to prettify the response
85
+ soup = BeautifulSoup(response.content, features="html.parser")
86
+ soup.prettify()
87
+
88
+ # Get the collection items on the page and strip the bits of text that we don't care for
89
+ collections = []
90
+ for bin in soup.find_all("div", {"class": "collection"}):
91
+ bin_type = bin.find_next("h3").text.replace(" DUE TODAY", "").strip()
92
+ next_collection = bin.find_next("p").text.replace("Next collection ", "")
93
+ next_collection = datetime.strptime(next_collection, "%A %d %b %Y")
94
+ collections.append((bin_type, next_collection))
95
+
96
+ # Sort the collections by date order rather than bin type, then return as a dictionary (with str date)
97
+ ordered_data = sorted(collections, key=lambda x: x[1])
98
+ data = {"bins": []}
99
+ for item in ordered_data:
100
+ dict_data = {
101
+ "type": item[0],
102
+ "collectionDate": item[1].strftime(date_format),
103
+ }
104
+ data["bins"].append(dict_data)
105
+
106
+ return data
@@ -0,0 +1,38 @@
1
+ import requests
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
+ user_uprn = kwargs.get("uprn")
18
+ check_uprn(user_uprn)
19
+
20
+ data = {"bins": []}
21
+ api_url = f'https://portal.mansfield.gov.uk/MDCWhiteSpaceWebService/WhiteSpaceWS.asmx/GetCollectionByUPRNAndDate?apiKey=mDc-wN3-B0f-f4P&UPRN={user_uprn}&coldate={datetime.now().strftime("%d/%m/%Y")}'
22
+
23
+ response = requests.get(api_url)
24
+ if response.status_code != 200:
25
+ raise ConnectionError("Could not get latest data!")
26
+
27
+ json_data = response.json()["Collections"]
28
+ for item in json_data:
29
+
30
+ dict_data = {
31
+ "type": item.get("Service").split(" ")[0] + " bin",
32
+ "collectionDate": datetime.strptime(
33
+ item.get("Date"), "%d/%m/%Y %H:%M:%S"
34
+ ).strftime(date_format),
35
+ }
36
+ data["bins"].append(dict_data)
37
+
38
+ return data