uk_bin_collection 0.110.0__py3-none-any.whl → 0.112.0__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- uk_bin_collection/tests/input.json +414 -200
- uk_bin_collection/tests/test_collect_data.py +0 -1
- uk_bin_collection/tests/test_common_functions.py +4 -2
- uk_bin_collection/tests/test_conftest.py +13 -2
- uk_bin_collection/tests/test_get_data.py +34 -19
- uk_bin_collection/uk_bin_collection/common.py +1 -0
- uk_bin_collection/uk_bin_collection/councils/BarnsleyMBCouncil.py +6 -5
- uk_bin_collection/uk_bin_collection/councils/BasildonCouncil.py +11 -6
- uk_bin_collection/uk_bin_collection/councils/ChesterfieldBoroughCouncil.py +49 -36
- uk_bin_collection/uk_bin_collection/councils/GlasgowCityCouncil.py +1 -1
- uk_bin_collection/uk_bin_collection/councils/LondonBoroughSutton.py +75 -0
- uk_bin_collection/uk_bin_collection/councils/MidDevonCouncil.py +93 -0
- uk_bin_collection/uk_bin_collection/councils/OxfordCityCouncil.py +63 -0
- {uk_bin_collection-0.110.0.dist-info → uk_bin_collection-0.112.0.dist-info}/METADATA +1 -1
- {uk_bin_collection-0.110.0.dist-info → uk_bin_collection-0.112.0.dist-info}/RECORD +18 -15
- {uk_bin_collection-0.110.0.dist-info → uk_bin_collection-0.112.0.dist-info}/LICENSE +0 -0
- {uk_bin_collection-0.110.0.dist-info → uk_bin_collection-0.112.0.dist-info}/WHEEL +0 -0
- {uk_bin_collection-0.110.0.dist-info → uk_bin_collection-0.112.0.dist-info}/entry_points.txt +0 -0
@@ -430,8 +430,10 @@ def test_string_with_whitespace_and_numbers():
|
|
430
430
|
)
|
431
431
|
def test_get_next_day_of_week(today_str, day_name, expected):
|
432
432
|
mock_today = datetime.strptime(today_str, "%Y-%m-%d")
|
433
|
-
with patch(
|
433
|
+
with patch(
|
434
|
+
"uk_bin_collection.common.datetime"
|
435
|
+
) as mock_datetime: # replace 'your_module' with the actual module name
|
434
436
|
mock_datetime.now.return_value = mock_today
|
435
437
|
mock_datetime.side_effect = lambda *args, **kw: datetime(*args, **kw)
|
436
438
|
result = get_next_day_of_week(day_name, date_format="%m/%d/%Y")
|
437
|
-
assert result == expected
|
439
|
+
assert result == expected
|
@@ -2,26 +2,37 @@ import pytest
|
|
2
2
|
|
3
3
|
# Test the command-line options
|
4
4
|
|
5
|
+
|
5
6
|
def test_headless_mode(pytestconfig):
|
6
7
|
# Simulate pytest command-line option
|
7
8
|
headless_mode_value = pytestconfig.getoption("--headless")
|
8
9
|
assert headless_mode_value == "True" # This should match the default value
|
9
10
|
|
11
|
+
|
10
12
|
def test_local_browser(pytestconfig):
|
11
13
|
local_browser_value = pytestconfig.getoption("--local_browser")
|
12
14
|
assert local_browser_value == "False" # This should match the default value
|
13
15
|
|
16
|
+
|
14
17
|
def test_selenium_url(pytestconfig):
|
15
18
|
selenium_url_value = pytestconfig.getoption("--selenium_url")
|
16
|
-
assert
|
19
|
+
assert (
|
20
|
+
selenium_url_value == "http://localhost:4444"
|
21
|
+
) # This should match the default value
|
22
|
+
|
17
23
|
|
18
24
|
# Test the fixtures
|
19
25
|
|
26
|
+
|
20
27
|
def test_headless_mode_fixture(headless_mode):
|
21
28
|
assert headless_mode == "True" # This should match the default value
|
22
29
|
|
30
|
+
|
23
31
|
def test_local_browser_fixture(local_browser):
|
24
32
|
assert local_browser == "False" # This should match the default value
|
25
33
|
|
34
|
+
|
26
35
|
def test_selenium_url_fixture(selenium_url):
|
27
|
-
assert
|
36
|
+
assert (
|
37
|
+
selenium_url == "http://localhost:4444"
|
38
|
+
) # This should match the default value
|
@@ -105,8 +105,10 @@ def test_output_json():
|
|
105
105
|
assert type(output) == str
|
106
106
|
assert output == '{\n "bin": ""\n}'
|
107
107
|
|
108
|
+
|
108
109
|
class ConcreteGetBinDataClass(agbdc):
|
109
110
|
"""Concrete implementation of the abstract class to test abstract methods."""
|
111
|
+
|
110
112
|
def parse_data(self, page: str, **kwargs) -> dict:
|
111
113
|
return {"mock_key": "mock_value"}
|
112
114
|
|
@@ -114,33 +116,46 @@ class ConcreteGetBinDataClass(agbdc):
|
|
114
116
|
# You can implement the method or delegate it to the abstract class's method
|
115
117
|
super().update_dev_mode_data(council_module_str, this_url, **kwargs)
|
116
118
|
|
119
|
+
|
117
120
|
@pytest.fixture
|
118
121
|
def concrete_class_instance():
|
119
122
|
return ConcreteGetBinDataClass()
|
120
123
|
|
124
|
+
|
121
125
|
def test_get_and_parse_data_no_skip_get_url(concrete_class_instance):
|
122
126
|
mock_page = "mocked page content"
|
123
127
|
mock_parsed_data = {"mock_key": "mock_value"}
|
124
128
|
|
125
|
-
with mock.patch.object(
|
126
|
-
|
127
|
-
|
129
|
+
with mock.patch.object(
|
130
|
+
concrete_class_instance, "get_data", return_value=mock_page
|
131
|
+
) as mock_get_data, mock.patch.object(
|
132
|
+
concrete_class_instance, "parse_data", return_value=mock_parsed_data
|
133
|
+
) as mock_parse_data:
|
134
|
+
|
128
135
|
result = concrete_class_instance.get_and_parse_data("http://example.com")
|
129
136
|
|
130
137
|
mock_get_data.assert_called_once_with("http://example.com")
|
131
138
|
mock_parse_data.assert_called_once_with(mock_page, url="http://example.com")
|
132
139
|
assert result == mock_parsed_data
|
133
140
|
|
141
|
+
|
134
142
|
def test_get_and_parse_data_skip_get_url(concrete_class_instance):
|
135
143
|
mock_parsed_data = {"mock_key": "mock_value"}
|
136
144
|
|
137
|
-
with mock.patch.object(
|
138
|
-
|
139
|
-
|
145
|
+
with mock.patch.object(
|
146
|
+
concrete_class_instance, "parse_data", return_value=mock_parsed_data
|
147
|
+
) as mock_parse_data:
|
148
|
+
|
149
|
+
result = concrete_class_instance.get_and_parse_data(
|
150
|
+
"http://example.com", skip_get_url=True
|
151
|
+
)
|
140
152
|
|
141
|
-
mock_parse_data.assert_called_once_with(
|
153
|
+
mock_parse_data.assert_called_once_with(
|
154
|
+
"", url="http://example.com", skip_get_url=True
|
155
|
+
)
|
142
156
|
assert result == mock_parsed_data
|
143
|
-
|
157
|
+
|
158
|
+
|
144
159
|
@pytest.fixture
|
145
160
|
def setup_test_update_dev_mode_data():
|
146
161
|
"""Fixture to set up and tear down the environment for test_update_dev_mode_data"""
|
@@ -148,7 +163,7 @@ def setup_test_update_dev_mode_data():
|
|
148
163
|
test_dir = tempfile.TemporaryDirectory()
|
149
164
|
|
150
165
|
# Patch os.getcwd() to return the temporary directory
|
151
|
-
cwd_patch = patch(
|
166
|
+
cwd_patch = patch("os.getcwd", return_value=test_dir.name)
|
152
167
|
mock_getcwd = cwd_patch.start()
|
153
168
|
|
154
169
|
# Ensure the nested directory structure exists
|
@@ -171,15 +186,15 @@ def test_update_dev_mode_data(setup_test_update_dev_mode_data):
|
|
171
186
|
obj = ConcreteGetBinDataClass()
|
172
187
|
|
173
188
|
# Define input arguments for the method
|
174
|
-
council_module_str =
|
175
|
-
this_url =
|
189
|
+
council_module_str = "test_council_module"
|
190
|
+
this_url = "https://example.com"
|
176
191
|
kwargs = {
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
192
|
+
"postcode": "12345",
|
193
|
+
"paon": "1A",
|
194
|
+
"uprn": "100012345",
|
195
|
+
"usrn": "200012345",
|
196
|
+
"web_driver": "mocked_web_driver",
|
197
|
+
"skip_get_url": True,
|
183
198
|
}
|
184
199
|
|
185
200
|
# Call the method being tested on the instance
|
@@ -190,8 +205,8 @@ def test_update_dev_mode_data(setup_test_update_dev_mode_data):
|
|
190
205
|
assert os.path.exists(input_file_path)
|
191
206
|
|
192
207
|
# Read the contents of the file and make necessary assertions
|
193
|
-
with open(input_file_path,
|
208
|
+
with open(input_file_path, "r") as f:
|
194
209
|
file_content = f.read()
|
195
210
|
|
196
211
|
# Example assertion - check if certain values exist in the file content (based on your actual file format)
|
197
|
-
assert
|
212
|
+
assert "100012345" in file_content # Checking UPRN as an example
|
@@ -27,13 +27,14 @@ def parse_bin_text(bin_type_str: str, bin_date_str: str) -> List[Dict[str, str]]
|
|
27
27
|
bin_date = datetime.strptime(bin_date_str, "%A, %B %d, %Y")
|
28
28
|
|
29
29
|
for bin_type in bin_type_str.split(", "):
|
30
|
-
bins.append(
|
31
|
-
|
32
|
-
|
33
|
-
|
30
|
+
bins.append(
|
31
|
+
{
|
32
|
+
"type": bin_type.strip() + " bin",
|
33
|
+
"collectionDate": bin_date.strftime(date_format),
|
34
|
+
}
|
35
|
+
)
|
34
36
|
|
35
37
|
return bins
|
36
|
-
|
37
38
|
|
38
39
|
class CouncilClass(AbstractGetBinDataClass):
|
39
40
|
"""
|
@@ -1,7 +1,10 @@
|
|
1
1
|
import requests
|
2
2
|
import json
|
3
3
|
from datetime import datetime
|
4
|
-
from uk_bin_collection.uk_bin_collection.common import
|
4
|
+
from uk_bin_collection.uk_bin_collection.common import (
|
5
|
+
check_uprn,
|
6
|
+
date_format as DATE_FORMAT,
|
7
|
+
)
|
5
8
|
from uk_bin_collection.uk_bin_collection.get_bin_data import AbstractGetBinDataClass
|
6
9
|
|
7
10
|
|
@@ -11,15 +14,15 @@ class CouncilClass(AbstractGetBinDataClass):
|
|
11
14
|
"""
|
12
15
|
|
13
16
|
def parse_data(self, page: str, **kwargs) -> dict:
|
14
|
-
url_base =
|
17
|
+
url_base = (
|
18
|
+
"https://basildonportal.azurewebsites.net/api/getPropertyRefuseInformation"
|
19
|
+
)
|
15
20
|
|
16
21
|
uprn = kwargs.get("uprn")
|
17
22
|
# Check the UPRN is valid
|
18
23
|
check_uprn(uprn)
|
19
24
|
|
20
|
-
payload = {
|
21
|
-
"uprn": uprn
|
22
|
-
}
|
25
|
+
payload = {"uprn": uprn}
|
23
26
|
|
24
27
|
headers = {"Content-Type": "application/json"}
|
25
28
|
|
@@ -36,7 +39,9 @@ class CouncilClass(AbstractGetBinDataClass):
|
|
36
39
|
bins.append(
|
37
40
|
{
|
38
41
|
"type": service_name,
|
39
|
-
"collectionDate": collection_data.get(
|
42
|
+
"collectionDate": collection_data.get(
|
43
|
+
"current_collection_date"
|
44
|
+
),
|
40
45
|
}
|
41
46
|
)
|
42
47
|
|
@@ -15,6 +15,7 @@ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
15
15
|
|
16
16
|
_LOGGER = logging.getLogger(__name__)
|
17
17
|
|
18
|
+
|
18
19
|
class CouncilClass(AbstractGetBinDataClass):
|
19
20
|
"""
|
20
21
|
Implementation for Chesterfield Borough Council waste collection data retrieval.
|
@@ -56,7 +57,9 @@ class CouncilClass(AbstractGetBinDataClass):
|
|
56
57
|
session.get(API_URLS["session"], headers=HEADERS, verify=False)
|
57
58
|
|
58
59
|
# Step 2: Get fwuid
|
59
|
-
fwuid_response = session.get(
|
60
|
+
fwuid_response = session.get(
|
61
|
+
API_URLS["fwuid"], headers=HEADERS, verify=False
|
62
|
+
)
|
60
63
|
fwuid_data = fwuid_response.json()
|
61
64
|
fwuid = fwuid_data.get("auraConfig", {}).get("context", {}).get("fwuid")
|
62
65
|
|
@@ -66,50 +69,58 @@ class CouncilClass(AbstractGetBinDataClass):
|
|
66
69
|
|
67
70
|
# Step 3: Prepare payload for UPRN search
|
68
71
|
payload = {
|
69
|
-
"message": json.dumps(
|
70
|
-
|
71
|
-
"
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
72
|
+
"message": json.dumps(
|
73
|
+
{
|
74
|
+
"actions": [
|
75
|
+
{
|
76
|
+
"id": "4;a",
|
77
|
+
"descriptor": "aura://ApexActionController/ACTION$execute",
|
78
|
+
"callingDescriptor": "UNKNOWN",
|
79
|
+
"params": {
|
80
|
+
"namespace": "",
|
81
|
+
"classname": "CBC_VE_CollectionDays",
|
82
|
+
"method": "getServicesByUPRN",
|
83
|
+
"params": {
|
84
|
+
"propertyUprn": user_uprn,
|
85
|
+
"executedFrom": "Main Website",
|
86
|
+
},
|
87
|
+
"cacheable": False,
|
88
|
+
"isContinuation": False,
|
89
|
+
},
|
90
|
+
}
|
91
|
+
]
|
92
|
+
}
|
93
|
+
),
|
94
|
+
"aura.context": json.dumps(
|
95
|
+
{
|
96
|
+
"mode": "PROD",
|
97
|
+
"fwuid": fwuid,
|
98
|
+
"app": "c:cbc_VE_CollectionDaysLO",
|
99
|
+
"loaded": {
|
100
|
+
"APPLICATION@markup://c:cbc_VE_CollectionDaysLO": "pqeNg7kPWCbx1pO8sIjdLA"
|
101
|
+
},
|
102
|
+
"dn": [],
|
103
|
+
"globals": {},
|
104
|
+
"uad": True,
|
105
|
+
}
|
106
|
+
),
|
98
107
|
"aura.pageURI": "/bins-and-recycling/bin-collections/check-bin-collections.aspx",
|
99
108
|
"aura.token": "null",
|
100
109
|
}
|
101
110
|
|
102
111
|
# Step 4: Make POST request to fetch collection data
|
103
112
|
search_response = session.post(
|
104
|
-
API_URLS["search"],
|
105
|
-
data=payload,
|
106
|
-
headers=HEADERS,
|
107
|
-
verify=False
|
113
|
+
API_URLS["search"], data=payload, headers=HEADERS, verify=False
|
108
114
|
)
|
109
115
|
search_data = search_response.json()
|
110
116
|
|
111
117
|
# Step 5: Extract service units
|
112
|
-
service_units =
|
118
|
+
service_units = (
|
119
|
+
search_data.get("actions", [])[0]
|
120
|
+
.get("returnValue", {})
|
121
|
+
.get("returnValue", {})
|
122
|
+
.get("serviceUnits", [])
|
123
|
+
)
|
113
124
|
|
114
125
|
if not service_units:
|
115
126
|
_LOGGER.warning("No service units found for the given UPRN.")
|
@@ -141,7 +152,9 @@ class CouncilClass(AbstractGetBinDataClass):
|
|
141
152
|
|
142
153
|
# Extract the next scheduled date
|
143
154
|
try:
|
144
|
-
dt_zulu = item["serviceTasks"][0]["serviceTaskSchedules"][0][
|
155
|
+
dt_zulu = item["serviceTasks"][0]["serviceTaskSchedules"][0][
|
156
|
+
"nextInstance"
|
157
|
+
]["currentScheduledDate"]
|
145
158
|
dt_utc = datetime.strptime(dt_zulu, "%Y-%m-%dT%H:%M:%S.%f%z")
|
146
159
|
dt_local = dt_utc.astimezone(None)
|
147
160
|
collection_date = dt_local.date()
|
@@ -0,0 +1,75 @@
|
|
1
|
+
from time import sleep
|
2
|
+
|
3
|
+
import requests
|
4
|
+
from bs4 import BeautifulSoup
|
5
|
+
|
6
|
+
from uk_bin_collection.uk_bin_collection.common import *
|
7
|
+
from uk_bin_collection.uk_bin_collection.get_bin_data import AbstractGetBinDataClass
|
8
|
+
|
9
|
+
|
10
|
+
# import the wonderful Beautiful Soup and the URL grabber
|
11
|
+
class CouncilClass(AbstractGetBinDataClass):
|
12
|
+
"""
|
13
|
+
Concrete classes have to implement all abstract operations of the
|
14
|
+
base class. They can also override some operations with a default
|
15
|
+
implementation.
|
16
|
+
"""
|
17
|
+
|
18
|
+
def parse_data(self, page: str, **kwargs) -> dict:
|
19
|
+
|
20
|
+
user_uprn = kwargs.get("uprn")
|
21
|
+
# check_uprn(user_uprn)
|
22
|
+
bindata = {"bins": []}
|
23
|
+
|
24
|
+
URI = f"https://waste-services.sutton.gov.uk/waste/{user_uprn}"
|
25
|
+
|
26
|
+
s = requests.Session()
|
27
|
+
|
28
|
+
r = s.get(URI)
|
29
|
+
while "Loading your bin days..." in r.text:
|
30
|
+
sleep(2)
|
31
|
+
r = s.get(URI)
|
32
|
+
r.raise_for_status()
|
33
|
+
|
34
|
+
soup = BeautifulSoup(r.content, "html.parser")
|
35
|
+
|
36
|
+
current_year = datetime.now().year
|
37
|
+
next_year = current_year + 1
|
38
|
+
|
39
|
+
services = soup.find_all("h3", class_="govuk-heading-m waste-service-name")
|
40
|
+
|
41
|
+
for service in services:
|
42
|
+
bin_type = service.get_text(
|
43
|
+
strip=True
|
44
|
+
) # Bin type name (e.g., 'Food waste', 'Mixed recycling')
|
45
|
+
if bin_type == "Bulky waste":
|
46
|
+
continue
|
47
|
+
service_details = service.find_next("div", class_="govuk-grid-row")
|
48
|
+
|
49
|
+
next_collection = (
|
50
|
+
service_details.find("dt", string="Next collection")
|
51
|
+
.find_next_sibling("dd")
|
52
|
+
.get_text(strip=True)
|
53
|
+
)
|
54
|
+
|
55
|
+
next_collection = datetime.strptime(
|
56
|
+
remove_ordinal_indicator_from_date_string(next_collection),
|
57
|
+
"%A, %d %B",
|
58
|
+
)
|
59
|
+
|
60
|
+
if next_collection.month == 1:
|
61
|
+
next_collection = next_collection.replace(year=next_year)
|
62
|
+
else:
|
63
|
+
next_collection = next_collection.replace(year=current_year)
|
64
|
+
|
65
|
+
dict_data = {
|
66
|
+
"type": bin_type,
|
67
|
+
"collectionDate": next_collection.strftime("%d/%m/%Y"),
|
68
|
+
}
|
69
|
+
bindata["bins"].append(dict_data)
|
70
|
+
|
71
|
+
bindata["bins"].sort(
|
72
|
+
key=lambda x: datetime.strptime(x.get("collectionDate"), "%d/%m/%Y")
|
73
|
+
)
|
74
|
+
|
75
|
+
return bindata
|
@@ -0,0 +1,93 @@
|
|
1
|
+
import time
|
2
|
+
|
3
|
+
import requests
|
4
|
+
|
5
|
+
from uk_bin_collection.uk_bin_collection.common import *
|
6
|
+
from uk_bin_collection.uk_bin_collection.get_bin_data import AbstractGetBinDataClass
|
7
|
+
|
8
|
+
|
9
|
+
# import the wonderful Beautiful Soup and the URL grabber
|
10
|
+
class CouncilClass(AbstractGetBinDataClass):
|
11
|
+
"""
|
12
|
+
Concrete classes have to implement all abstract operations of the
|
13
|
+
base class. They can also override some operations with a default
|
14
|
+
implementation.
|
15
|
+
"""
|
16
|
+
|
17
|
+
def parse_data(self, page: str, **kwargs) -> dict:
|
18
|
+
|
19
|
+
user_uprn = kwargs.get("uprn")
|
20
|
+
check_uprn(user_uprn)
|
21
|
+
bindata = {"bins": []}
|
22
|
+
|
23
|
+
SESSION_URL = "https://my.middevon.gov.uk/authapi/isauthenticated?uri=https%253A%252F%252Fmy.middevon.gov.uk%252Fen%252FAchieveForms%252F%253Fform_uri%253Dsandbox-publish%253A%252F%252FAF-Process-2289dd06-9a12-4202-ba09-857fe756f6bd%252FAF-Stage-eb382015-001c-415d-beda-84f796dbb167%252Fdefinition.json%2526redirectlink%253D%25252Fen%2526cancelRedirectLink%253D%25252Fen%2526consentMessage%253Dyes&hostname=my.middevon.gov.uk&withCredentials=true"
|
24
|
+
|
25
|
+
API_URL = "https://my.middevon.gov.uk/apibroker/runLookup"
|
26
|
+
|
27
|
+
payload = {
|
28
|
+
"formValues": {
|
29
|
+
"Your Address": {
|
30
|
+
"listAddress": {"value": user_uprn},
|
31
|
+
},
|
32
|
+
},
|
33
|
+
}
|
34
|
+
|
35
|
+
headers = {
|
36
|
+
"Content-Type": "application/json",
|
37
|
+
"Accept": "application/json",
|
38
|
+
"User-Agent": "Mozilla/5.0",
|
39
|
+
"X-Requested-With": "XMLHttpRequest",
|
40
|
+
"Referer": "https://my.middevon.gov.uk/fillform/?iframe_id=fillform-frame-1&db_id=",
|
41
|
+
}
|
42
|
+
|
43
|
+
ids = [
|
44
|
+
"6423144f50ec0",
|
45
|
+
"641c7ae9b4c96",
|
46
|
+
"645e13a01dba1",
|
47
|
+
"642315aacb919",
|
48
|
+
"64231699483cf",
|
49
|
+
"642421bab7478",
|
50
|
+
"6424229605d13",
|
51
|
+
"645e14020c9cc",
|
52
|
+
]
|
53
|
+
|
54
|
+
rows_data = []
|
55
|
+
|
56
|
+
for id in ids:
|
57
|
+
s = requests.session()
|
58
|
+
r = s.get(SESSION_URL)
|
59
|
+
r.raise_for_status()
|
60
|
+
session_data = r.json()
|
61
|
+
sid = session_data["auth-session"]
|
62
|
+
|
63
|
+
params = {
|
64
|
+
"id": id,
|
65
|
+
"repeat_against": "",
|
66
|
+
"noRetry": "false",
|
67
|
+
"getOnlyTokens": "undefined",
|
68
|
+
"log_id": "",
|
69
|
+
"app_name": "AF-Renderer::Self",
|
70
|
+
# unix_timestamp
|
71
|
+
"_": str(int(time.time() * 1000)),
|
72
|
+
"sid": sid,
|
73
|
+
}
|
74
|
+
r = s.post(API_URL, json=payload, headers=headers, params=params)
|
75
|
+
r.raise_for_status()
|
76
|
+
data = r.json()
|
77
|
+
rows_data = data["integration"]["transformed"]["rows_data"]
|
78
|
+
if isinstance(rows_data, dict):
|
79
|
+
date = datetime.strptime(rows_data["0"]["display"], "%d-%b-%y")
|
80
|
+
bin_types = (rows_data["0"]["CollectionItems"]).split(" and ")
|
81
|
+
|
82
|
+
for bin_type in bin_types:
|
83
|
+
dict_data = {
|
84
|
+
"type": bin_type,
|
85
|
+
"collectionDate": date.strftime(date_format),
|
86
|
+
}
|
87
|
+
bindata["bins"].append(dict_data)
|
88
|
+
|
89
|
+
bindata["bins"].sort(
|
90
|
+
key=lambda x: datetime.strptime(x.get("collectionDate"), "%d/%m/%Y")
|
91
|
+
)
|
92
|
+
|
93
|
+
return bindata
|
@@ -0,0 +1,63 @@
|
|
1
|
+
import requests
|
2
|
+
from bs4 import BeautifulSoup
|
3
|
+
|
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
|
+
|
18
|
+
user_uprn = kwargs.get("uprn")
|
19
|
+
user_postcode = kwargs.get("postcode")
|
20
|
+
check_uprn(user_uprn)
|
21
|
+
check_postcode(user_postcode)
|
22
|
+
bindata = {"bins": []}
|
23
|
+
|
24
|
+
session_uri = "https://www.oxford.gov.uk/mybinday"
|
25
|
+
URI = "https://www.oxford.gov.uk/xfp/form/142"
|
26
|
+
|
27
|
+
session = requests.Session()
|
28
|
+
token_response = session.get(session_uri)
|
29
|
+
soup = BeautifulSoup(token_response.text, "html.parser")
|
30
|
+
token = soup.find("input", {"name": "__token"}).attrs["value"]
|
31
|
+
|
32
|
+
form_data = {
|
33
|
+
"__token": token,
|
34
|
+
"page": "12",
|
35
|
+
"locale": "en_GB",
|
36
|
+
"q6ad4e3bf432c83230a0347a6eea6c805c672efeb_0_0": user_postcode,
|
37
|
+
"q6ad4e3bf432c83230a0347a6eea6c805c672efeb_1_0": user_uprn,
|
38
|
+
"next": "Next",
|
39
|
+
}
|
40
|
+
|
41
|
+
collection_response = session.post(URI, data=form_data)
|
42
|
+
|
43
|
+
collection_soup = BeautifulSoup(collection_response.text, "html.parser")
|
44
|
+
for paragraph in collection_soup.find("div", class_="editor").find_all("p"):
|
45
|
+
matches = re.match(r"^(\w+) Next Collection: (.*)", paragraph.text)
|
46
|
+
if matches:
|
47
|
+
collection_type, date_string = matches.groups()
|
48
|
+
try:
|
49
|
+
date = datetime.strptime(date_string, "%A %d %B %Y").date()
|
50
|
+
except ValueError:
|
51
|
+
date = datetime.strptime(date_string, "%A %d %b %Y").date()
|
52
|
+
|
53
|
+
dict_data = {
|
54
|
+
"type": collection_type,
|
55
|
+
"collectionDate": date.strftime("%d/%m/%Y"),
|
56
|
+
}
|
57
|
+
bindata["bins"].append(dict_data)
|
58
|
+
|
59
|
+
bindata["bins"].sort(
|
60
|
+
key=lambda x: datetime.strptime(x.get("collectionDate"), "%d/%m/%Y")
|
61
|
+
)
|
62
|
+
|
63
|
+
return bindata
|