uk_bin_collection 0.76.1__py3-none-any.whl → 0.77.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.
@@ -4,35 +4,66 @@ import requests
4
4
  import sys
5
5
  from tabulate import tabulate
6
6
 
7
- def get_councils_from_files(branch):
8
- url = f"https://api.github.com/repos/robbrad/UKBinCollectionData/contents/uk_bin_collection/uk_bin_collection/councils?ref={branch}"
7
+ def get_councils_from_files(repo, branch):
8
+ url = f"https://api.github.com/repos/{repo}/contents/uk_bin_collection/uk_bin_collection/councils?ref={branch}"
9
+ print(f"Fetching councils from files at URL: {url}")
9
10
  response = requests.get(url)
10
- data = response.json()
11
11
 
12
12
  # Debugging lines to check the response content and type
13
- print(f"Response Status Code: {response.status_code}")
14
- print(f"Response Content: {response.content}")
15
- print(f"Parsed JSON Data: {data}")
16
- print(f"Data Type: {type(data)}")
13
+ print(f"Response Status Code (Files): {response.status_code}")
17
14
 
18
- # Ensure 'data' is a list before proceeding
19
- if isinstance(data, list):
20
- return [item['name'].replace('.py', '') for item in data if item['name'].endswith('.py')]
15
+ if response.status_code == 200:
16
+ try:
17
+ data = response.json()
18
+ #print(f"Parsed JSON Data (Files): {data}")
19
+ # Ensure 'data' is a list before proceeding
20
+ if isinstance(data, list):
21
+ return [item['name'].replace('.py', '') for item in data if item['name'].endswith('.py')]
22
+ else:
23
+ print("Expected a list from the JSON response but got something else.")
24
+ raise ValueError("Expected a list from the JSON response but got something else.")
25
+ except json.JSONDecodeError as e:
26
+ print(f"JSON decoding error: {e}")
27
+ raise
21
28
  else:
22
- raise ValueError("Expected a list from the JSON response but got something else.")
29
+ print(f"Failed to fetch councils from files: {response.content}")
30
+ return []
23
31
 
24
-
25
- def get_councils_from_json(branch):
26
- url = f"https://raw.githubusercontent.com/robbrad/UKBinCollectionData/{branch}/uk_bin_collection/tests/input.json"
32
+ def get_councils_from_json(repo, branch):
33
+ url = f"https://raw.githubusercontent.com/{repo}/{branch}/uk_bin_collection/tests/input.json"
34
+ print(f"Fetching councils from JSON at URL: {url}")
27
35
  response = requests.get(url)
28
- data = json.loads(response.text)
29
- return list(data.keys())
36
+
37
+ # Debugging lines to check the response content and type
38
+ print(f"Response Status Code (JSON): {response.status_code}")
39
+
40
+ if response.status_code == 200:
41
+ try:
42
+ data = json.loads(response.text)
43
+ #print(f"Parsed JSON Data: {data}")
44
+ return list(data.keys())
45
+ except json.JSONDecodeError as e:
46
+ print(f"JSON decoding error: {e}")
47
+ raise
48
+ else:
49
+ print(f"Failed to fetch councils from JSON: {response.content}")
50
+ return []
30
51
 
31
- def get_councils_from_features(branch):
32
- url = f"https://raw.githubusercontent.com/robbrad/UKBinCollectionData/{branch}/uk_bin_collection/tests/features/validate_council_outputs.feature"
52
+ def get_councils_from_features(repo, branch):
53
+ url = f"https://raw.githubusercontent.com/{repo}/{branch}/uk_bin_collection/tests/features/validate_council_outputs.feature"
54
+ print(f"Fetching councils from features at URL: {url}")
33
55
  response = requests.get(url)
34
- content = response.text
35
- return re.findall(r'Examples:\s+(\w+)', content)
56
+
57
+ # Debugging lines to check the response content and type
58
+ print(f"Response Status Code (Features): {response.status_code}")
59
+
60
+ if response.status_code == 200:
61
+ content = response.text
62
+ #print(f"Fetched Features Content: {content[:500]}...") # Print only the first 500 characters for brevity
63
+ return re.findall(r'Examples:\s+(\w+)', content)
64
+ else:
65
+ print(f"Failed to fetch councils from features: {response.content}")
66
+ return []
36
67
 
37
68
  def compare_councils(councils1, councils2, councils3):
38
69
  set1 = set(councils1)
@@ -56,11 +87,16 @@ def compare_councils(councils1, councils2, councils3):
56
87
  discrepancies_found = True
57
88
  return all_council_data, discrepancies_found
58
89
 
59
- def main(branch="master"):
90
+ def main(repo="robbrad/UKBinCollectionData", branch="master"):
60
91
  # Execute and print the comparison
61
- file_councils = get_councils_from_files(branch)
62
- json_councils = get_councils_from_json(branch)
63
- feature_councils = get_councils_from_features(branch)
92
+ print(f"Starting comparison for repo: {repo}, branch: {branch}")
93
+ file_councils = get_councils_from_files(repo, branch)
94
+ json_councils = get_councils_from_json(repo, branch)
95
+ feature_councils = get_councils_from_features(repo, branch)
96
+
97
+ #print(f"Councils from files: {file_councils}")
98
+ #print(f"Councils from JSON: {json_councils}")
99
+ #print(f"Councils from features: {feature_councils}")
64
100
 
65
101
  all_councils_data, discrepancies_found = compare_councils(file_councils, json_councils, feature_councils)
66
102
 
@@ -87,5 +123,6 @@ def main(branch="master"):
87
123
  print("No discrepancies found. Workflow successful.")
88
124
 
89
125
  if __name__ == "__main__":
90
- branch = sys.argv[1] if len(sys.argv) > 1 else "master"
91
- main(branch)
126
+ repo = sys.argv[1] if len(sys.argv) > 1 else "robbrad/UKBinCollectionData"
127
+ branch = sys.argv[2] if len(sys.argv) > 2 else "master"
128
+ main(repo, branch)
@@ -76,6 +76,11 @@ Feature: Test each council output matches expected results
76
76
  | council |
77
77
  | BoltonCouncil |
78
78
 
79
+ @BracknellForestCouncil
80
+ Examples: BracknellForestCouncil
81
+ | council |
82
+ | BracknellForestCouncil |
83
+
79
84
  @BradfordMDC
80
85
  Examples: BradfordMDC
81
86
  | council |
@@ -101,6 +101,13 @@
101
101
  "wiki_name": "Bolton Council",
102
102
  "wiki_note": "To get the UPRN, you will need to use [FindMyAddress](https://www.findmyaddress.co.uk/search). Previously required single field that was UPRN and full address, now requires UPRN and postcode as separate fields."
103
103
  },
104
+ "BracknellForestCouncil": {
105
+ "paon": "57",
106
+ "postcode": "GU47 9BS",
107
+ "skip_get_url": true,
108
+ "url": "https://selfservice.mybfc.bracknell-forest.gov.uk/w/webpage/waste-collection-days",
109
+ "wiki_name": "Bracknell Forest Council"
110
+ },
104
111
  "BradfordMDC": {
105
112
  "custom_component_show_url_field": false,
106
113
  "skip_get_url": true,
@@ -209,8 +216,8 @@
209
216
  "CheshireWestAndChesterCouncil": {
210
217
  "house_number": "Hill View House",
211
218
  "postcode": "CH3 9ER",
212
- "uprn": "100012346655",
213
219
  "skip_get_url": true,
220
+ "uprn": "100012346655",
214
221
  "url": "https://www.cheshirewestandchester.gov.uk/residents/waste-and-recycling/your-bin-collection/collection-day",
215
222
  "web_driver": "http://selenium:4444",
216
223
  "wiki_name": "Cheshire West and Chester Council"
@@ -1027,8 +1034,8 @@
1027
1034
  },
1028
1035
  "WestSuffolkCouncil": {
1029
1036
  "postcode": "IP28 6DR",
1030
- "uprn": "10009739960",
1031
1037
  "skip_get_url": true,
1038
+ "uprn": "10009739960",
1032
1039
  "url": "https://maps.westsuffolk.gov.uk/MyWestSuffolk.aspx",
1033
1040
  "wiki_name": "West Suffolk Council"
1034
1041
  },
@@ -0,0 +1,246 @@
1
+ import time
2
+
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
+
9
+ def get_headers(base_url: str, method: str) -> dict[str, str]:
10
+ """
11
+ Gets request headers
12
+ :rtype: dict[str, str]
13
+ :param base_url: Base URL to use
14
+ :param method: Method to use
15
+ :return: Request headers
16
+ """
17
+ headers = {
18
+ "Accept-Encoding": "gzip, deflate, br",
19
+ "Accept-Language": "en-GB,en-US;q=0.9,en;q=0.8",
20
+ "Cache-Control": "max-age=0",
21
+ "Connection": "keep-alive",
22
+ "Host": "selfservice.mybfc.bracknell-forest.gov.uk",
23
+ "Origin": base_url,
24
+ "sec-ch-ua": '"Not_A Brand";v="99", "Google Chrome";v="109", "Chromium";v="109"',
25
+ "sec-ch-ua-mobile": "?0",
26
+ "sec-ch-ua-platform": "Windows",
27
+ "Sec-Fetch-Dest": "document",
28
+ "Sec-Fetch-User": "?1",
29
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)"
30
+ " Chrome/109.0.0.0 Safari/537.36",
31
+ }
32
+ if method.lower() == "post":
33
+ headers["Accept"] = "application/json, text/javascript, */*; q=0.01"
34
+ headers["Content-Type"] = "application/x-www-form-urlencoded; charset=UTF-8"
35
+ headers["Sec-Fetch-Mode"] = "cors"
36
+ headers["Sec-Fetch-Mode"] = "same-origin"
37
+ headers["X-Requested-With"] = "XMLHttpRequest"
38
+ else:
39
+ headers["Accept"] = (
40
+ "text/html,application/xhtml+xml,application/xml;"
41
+ "q=0.9,image/avif,image/webp,image/apng,*/*;"
42
+ "q=0.8,application/signed-exchange;v=b3;q=0.9"
43
+ )
44
+ headers["Sec-Fetch-Mode"] = "navigate"
45
+ headers["Sec-Fetch-Mode"] = "none"
46
+ return headers
47
+
48
+
49
+ def get_session_storage_global() -> object:
50
+ """
51
+ Gets session storage global object
52
+ :rtype: object
53
+ :return: Session storage global object
54
+ """
55
+ return {
56
+ "destination_stack": [
57
+ "w/webpage/waste-collection-days",
58
+ ],
59
+ "last_context_record_id": "86086077",
60
+ }
61
+
62
+
63
+ def get_csrf_token(s: requests.session, base_url: str) -> str:
64
+ """
65
+ Gets a CSRF token
66
+ :rtype: str
67
+ :param s: requests.session() to use
68
+ :param base_url: Base URL to use
69
+ :return: CSRF token
70
+ """
71
+ csrf_token = ""
72
+ response = s.get(
73
+ base_url + "/w/webpage/waste-collection-days",
74
+ headers=get_headers(base_url, "GET"),
75
+ )
76
+ if response.status_code == 200:
77
+ soup = BeautifulSoup(response.text, features="html.parser")
78
+ soup.prettify()
79
+ app_body = soup.find("div", {"class": "app-body"})
80
+ script = app_body.find("script", {"type": "text/javascript"}).string
81
+ p = re.compile("var CSRF = ('|\")(.*?)('|\");")
82
+ m = p.search(script)
83
+ csrf_token = m.groups()[1]
84
+ else:
85
+ raise ValueError(
86
+ "Code 1: Failed to get a CSRF token. Please ensure the council website is online first,"
87
+ " then open an issue on GitHub."
88
+ )
89
+ return csrf_token
90
+
91
+
92
+ def get_address_id(
93
+ s: requests.session, base_url: str, csrf_token: str, postcode: str, paon: str
94
+ ) -> str:
95
+ """
96
+ Gets the address ID
97
+ :rtype: str
98
+ :param s: requests.session() to use
99
+ :param base_url: Base URL to use
100
+ :param csrf_token: CSRF token to use
101
+ :param postcode: Postcode to use
102
+ :param paon: House number/address to find
103
+ :return: address ID
104
+ """
105
+ address_id = "0"
106
+ # Get the addresses for the postcode
107
+ form_data = {
108
+ "code_action": "find_addresses",
109
+ "code_params": '{"search":"' + postcode + '"}',
110
+ "_session_storage": json.dumps(
111
+ {
112
+ "/w/webpage/waste-collection-days": {},
113
+ "_global": get_session_storage_global(),
114
+ }
115
+ ),
116
+ "action_cell_id": "PCL0003988FEFFB1",
117
+ "action_page_id": "PAG0000570FEFFB1",
118
+ "form_check_ajax": csrf_token,
119
+ }
120
+ response = s.post(
121
+ base_url
122
+ + "/w/webpage/waste-collection-days?webpage_subpage_id=PAG0000570FEFFB1"
123
+ "&webpage_token=390170046582b0e3d7ca68ef1d6b4829ccff0b1ae9c531047219c6f9b5295738"
124
+ "&widget_action=handle_event",
125
+ headers=get_headers(base_url, "POST"),
126
+ data=form_data,
127
+ )
128
+ if response.status_code == 200:
129
+ json_response = json.loads(response.text)
130
+ addresses = json_response["response"]["addresses"]["items"]
131
+ # Find the matching address id for the paon
132
+ for address in addresses:
133
+ # Check for full matches first
134
+ if address.get("Description") == paon:
135
+ address_id = address.get("Id")
136
+ break
137
+ # Check for matching start if no full match found
138
+ if address_id == "0":
139
+ for address in addresses:
140
+ if address.get("Description").split()[0] == paon.strip():
141
+ address_id = address.get("Id")
142
+ break
143
+ # Check match was found
144
+ if address_id == "0":
145
+ raise ValueError(
146
+ "Code 2: No matching address for house number/full address found."
147
+ )
148
+ else:
149
+ raise ValueError("Code 3: No addresses found for provided postcode.")
150
+ return address_id
151
+
152
+
153
+ def get_collection_data(
154
+ s: requests.session, base_url: str, csrf_token: str, address_id: str
155
+ ) -> str:
156
+ """
157
+ Gets the collection data
158
+ :rtype: str
159
+ :param s: requests.session() to use
160
+ :param base_url: Base URL to use
161
+ :param csrf_token: CSRF token to use
162
+ :param address_id: Address id to use
163
+ :param retries: Retries count
164
+ :return: Collection data
165
+ """
166
+ collection_data = ""
167
+ if address_id != "0":
168
+ form_data = {
169
+ "code_action": "find_rounds",
170
+ "code_params": '{"addressId":"' + address_id + '"}',
171
+ "_session_storage": json.dumps(
172
+ {
173
+ "/w/webpage/waste-collection-days": {},
174
+ "_global": get_session_storage_global(),
175
+ }
176
+ ),
177
+ "action_cell_id": "PCL0003988FEFFB1",
178
+ "action_page_id": "PAG0000570FEFFB1",
179
+ "form_check_ajax": csrf_token,
180
+ }
181
+ response = s.post(
182
+ base_url
183
+ + "/w/webpage/waste-collection-days?webpage_subpage_id=PAG0000570FEFFB1"
184
+ "&webpage_token=390170046582b0e3d7ca68ef1d6b4829ccff0b1ae9c531047219c6f9b5295738"
185
+ "&widget_action=handle_event",
186
+ headers=get_headers(base_url, "POST"),
187
+ data=form_data,
188
+ )
189
+ if response.status_code == 200 and len(response.text) > 0:
190
+ json_response = json.loads(response.text)
191
+ collection_data = json_response["response"]["collections"]
192
+ else:
193
+ raise ValueError("Code 4: Failed to get bin data.")
194
+ return collection_data
195
+
196
+
197
+ class CouncilClass(AbstractGetBinDataClass):
198
+ """
199
+ Concrete classes have to implement all abstract operations of the
200
+ base class. They can also override some operations with a default
201
+ implementation.
202
+ """
203
+
204
+ def parse_data(self, page: str, **kwargs) -> dict:
205
+ requests.packages.urllib3.disable_warnings()
206
+ s = requests.session()
207
+ base_url = "https://selfservice.mybfc.bracknell-forest.gov.uk"
208
+ paon = kwargs.get("paon")
209
+ postcode = kwargs.get("postcode")
210
+ check_paon(paon)
211
+ check_postcode(postcode)
212
+
213
+ # Firstly, get a CSRF (cross-site request forgery) token
214
+ csrf_token = get_csrf_token(s, base_url)
215
+ # Next, get the address_id
216
+ address_id = get_address_id(s, base_url, csrf_token, postcode, paon)
217
+ # Finally, use the address_id to get the collection data
218
+ collection_data = get_collection_data(s, base_url, csrf_token, address_id)
219
+ if collection_data != "":
220
+ # Form a JSON wrapper
221
+ data = {"bins": []}
222
+
223
+ for c in collection_data:
224
+ collection_type = c["round"]
225
+ for c_date in c["upcomingCollections"]:
226
+ collection_date = (
227
+ re.search(r"Your (.*) is(.*)", c_date).group(2).strip()
228
+ )
229
+ dict_data = {
230
+ "type": collection_type,
231
+ "collectionDate": datetime.strptime(
232
+ collection_date, "%A %d %B %Y"
233
+ ).strftime(date_format),
234
+ }
235
+ data["bins"].append(dict_data)
236
+
237
+ if len(data["bins"]) == 0:
238
+ raise ValueError(
239
+ "Code 5: No bin data found. Please ensure the council website is showing data first,"
240
+ " then open an issue on GitHub."
241
+ )
242
+
243
+ data["bins"].sort(
244
+ key=lambda x: datetime.strptime(x.get("collectionDate"), date_format)
245
+ )
246
+ return data
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: uk_bin_collection
3
- Version: 0.76.1
3
+ Version: 0.77.0
4
4
  Summary: Python Lib to collect UK Bin Data
5
5
  Author: Robert Bradley
6
6
  Author-email: robbrad182@gmail.com
@@ -1,8 +1,8 @@
1
1
  uk_bin_collection/README.rst,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- uk_bin_collection/tests/council_feature_input_parity.py,sha256=66MrXc-3SnkricUV7LyAs9GFux7wEH31xcs9xbdB5Qo,3515
2
+ uk_bin_collection/tests/council_feature_input_parity.py,sha256=_BINFpWMsMg91Jf1MYydPykOi8gu06x8C-E2AdT0A3Q,5205
3
3
  uk_bin_collection/tests/features/environment.py,sha256=VQZjJdJI_kZn08M0j5cUgvKT4k3iTw8icJge1DGOkoA,127
4
- uk_bin_collection/tests/features/validate_council_outputs.feature,sha256=XIYnckDPBwBd7jTX2CN7Nv4mhUQsiJYOnYzvXVGfpdk,16246
5
- uk_bin_collection/tests/input.json,sha256=lbJZCvGd0kvIvI-0dD7Q9clMnkk_zX2UWbp8j2Ji1ow,53474
4
+ uk_bin_collection/tests/features/validate_council_outputs.feature,sha256=JfARfiOGbj6ENoieIpFw0XKmkGgm4krWg8sbofgx9k4,16357
5
+ uk_bin_collection/tests/input.json,sha256=PaincPdmWl4MqajMT9-79qGNOK2Zz2cgAY2_bTuy4gk,53745
6
6
  uk_bin_collection/tests/output.schema,sha256=ZwKQBwYyTDEM4G2hJwfLUVM-5v1vKRvRK9W9SS1sd18,1086
7
7
  uk_bin_collection/tests/step_defs/step_helpers/file_handler.py,sha256=Ygzi4V0S1MIHqbdstUlIqtRIwnynvhu4UtpweJ6-5N8,1474
8
8
  uk_bin_collection/tests/step_defs/test_validate_council.py,sha256=Pg3Z7c3zrebx1w-yIZ9xTJ3E-okVndlNIAJL-F46HKU,2962
@@ -24,6 +24,7 @@ uk_bin_collection/uk_bin_collection/councils/BexleyCouncil.py,sha256=9MrbpfR17R6
24
24
  uk_bin_collection/uk_bin_collection/councils/BirminghamCityCouncil.py,sha256=wIJgp7gjUL2zVALZNEz0YapDpvAXmriOswE03EqH49o,4130
25
25
  uk_bin_collection/uk_bin_collection/councils/BlackburnCouncil.py,sha256=jHbCK8sL09vdmdP7Xnh8lIrU5AHTnJLEZfOLephPvWg,4090
26
26
  uk_bin_collection/uk_bin_collection/councils/BoltonCouncil.py,sha256=MrSzfK3EukRVasgP1nxAqg3lTkZzlAkDLJ2Nu5dJJ-0,4023
27
+ uk_bin_collection/uk_bin_collection/councils/BracknellForestCouncil.py,sha256=35P9f4j6LpteYMaUhpE-NdfzupTRg0wEQyojH1n-OUw,9015
27
28
  uk_bin_collection/uk_bin_collection/councils/BradfordMDC.py,sha256=9EDTdhbGb8BQ5PLEw9eiWZ3BrS0TtoFpYOHQU44wc2k,4308
28
29
  uk_bin_collection/uk_bin_collection/councils/BrightonandHoveCityCouncil.py,sha256=k6qt4cds-Ejd97Z-__pw2BYvGVbFdc9SUfF73PPrTNA,5823
29
30
  uk_bin_collection/uk_bin_collection/councils/BristolCityCouncil.py,sha256=VT7k0AzcWL_h6E7Yr84NXs-FJ7qbYed7giR4HClHYE0,5575
@@ -164,8 +165,8 @@ uk_bin_collection/uk_bin_collection/councils/WyreCouncil.py,sha256=zDDa7n4K_zm5P
164
165
  uk_bin_collection/uk_bin_collection/councils/YorkCouncil.py,sha256=I2kBYMlsD4bIdsvmoSzBjJAvTTi6yPfJa8xjJx1ys2w,1490
165
166
  uk_bin_collection/uk_bin_collection/councils/council_class_template/councilclasstemplate.py,sha256=4s9ODGPAwPqwXc8SrTX5Wlfmizs3_58iXUtHc4Ir86o,1162
166
167
  uk_bin_collection/uk_bin_collection/get_bin_data.py,sha256=9qppF2oPkhmOoK8-ZkRIU1M6vhBh-yUCWAZEEd07iLk,5414
167
- uk_bin_collection-0.76.1.dist-info/LICENSE,sha256=vABBUOzcrgfaTKpzeo-si9YVEun6juDkndqA8RKdKGs,1071
168
- uk_bin_collection-0.76.1.dist-info/METADATA,sha256=hxUuy3JDHnEOpdpGmTvly1ME2eNlTxOBM5n9ln6EmEg,12594
169
- uk_bin_collection-0.76.1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
170
- uk_bin_collection-0.76.1.dist-info/entry_points.txt,sha256=36WCSGMWSc916S3Hi1ZkazzDKHaJ6CD-4fCEFm5MIao,90
171
- uk_bin_collection-0.76.1.dist-info/RECORD,,
168
+ uk_bin_collection-0.77.0.dist-info/LICENSE,sha256=vABBUOzcrgfaTKpzeo-si9YVEun6juDkndqA8RKdKGs,1071
169
+ uk_bin_collection-0.77.0.dist-info/METADATA,sha256=dWmzwfpIqThI-9oJf3jASJodK51-kB5TO1bgM_XJdY0,12594
170
+ uk_bin_collection-0.77.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
171
+ uk_bin_collection-0.77.0.dist-info/entry_points.txt,sha256=36WCSGMWSc916S3Hi1ZkazzDKHaJ6CD-4fCEFm5MIao,90
172
+ uk_bin_collection-0.77.0.dist-info/RECORD,,