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.
- uk_bin_collection/README.rst +0 -0
- uk_bin_collection/tests/council_feature_input_parity.py +79 -0
- uk_bin_collection/tests/features/environment.py +7 -0
- uk_bin_collection/tests/features/validate_council_outputs.feature +767 -0
- uk_bin_collection/tests/input.json +1077 -0
- uk_bin_collection/tests/output.schema +41 -0
- uk_bin_collection/tests/step_defs/step_helpers/file_handler.py +46 -0
- uk_bin_collection/tests/step_defs/test_validate_council.py +87 -0
- uk_bin_collection/tests/test_collect_data.py +104 -0
- uk_bin_collection/tests/test_common_functions.py +342 -0
- uk_bin_collection/uk_bin_collection/collect_data.py +133 -0
- uk_bin_collection/uk_bin_collection/common.py +292 -0
- uk_bin_collection/uk_bin_collection/councils/AdurAndWorthingCouncils.py +43 -0
- uk_bin_collection/uk_bin_collection/councils/ArunCouncil.py +97 -0
- uk_bin_collection/uk_bin_collection/councils/AylesburyValeCouncil.py +69 -0
- uk_bin_collection/uk_bin_collection/councils/BCPCouncil.py +51 -0
- uk_bin_collection/uk_bin_collection/councils/BarnetCouncil.py +180 -0
- uk_bin_collection/uk_bin_collection/councils/BarnsleyMBCouncil.py +109 -0
- uk_bin_collection/uk_bin_collection/councils/BasingstokeCouncil.py +72 -0
- uk_bin_collection/uk_bin_collection/councils/BathAndNorthEastSomersetCouncil.py +100 -0
- uk_bin_collection/uk_bin_collection/councils/BedfordBoroughCouncil.py +49 -0
- uk_bin_collection/uk_bin_collection/councils/BedfordshireCouncil.py +70 -0
- uk_bin_collection/uk_bin_collection/councils/BexleyCouncil.py +147 -0
- uk_bin_collection/uk_bin_collection/councils/BirminghamCityCouncil.py +119 -0
- uk_bin_collection/uk_bin_collection/councils/BlackburnCouncil.py +105 -0
- uk_bin_collection/uk_bin_collection/councils/BoltonCouncil.py +104 -0
- uk_bin_collection/uk_bin_collection/councils/BradfordMDC.py +103 -0
- uk_bin_collection/uk_bin_collection/councils/BrightonandHoveCityCouncil.py +137 -0
- uk_bin_collection/uk_bin_collection/councils/BristolCityCouncil.py +141 -0
- uk_bin_collection/uk_bin_collection/councils/BromleyBoroughCouncil.py +115 -0
- uk_bin_collection/uk_bin_collection/councils/BroxtoweBoroughCouncil.py +107 -0
- uk_bin_collection/uk_bin_collection/councils/BuckinghamshireCouncil.py +95 -0
- uk_bin_collection/uk_bin_collection/councils/BuryCouncil.py +65 -0
- uk_bin_collection/uk_bin_collection/councils/CalderdaleCouncil.py +123 -0
- uk_bin_collection/uk_bin_collection/councils/CannockChaseDistrictCouncil.py +65 -0
- uk_bin_collection/uk_bin_collection/councils/CardiffCouncil.py +172 -0
- uk_bin_collection/uk_bin_collection/councils/CastlepointDistrictCouncil.py +96 -0
- uk_bin_collection/uk_bin_collection/councils/CharnwoodBoroughCouncil.py +54 -0
- uk_bin_collection/uk_bin_collection/councils/ChelmsfordCityCouncil.py +127 -0
- uk_bin_collection/uk_bin_collection/councils/CheshireEastCouncil.py +32 -0
- uk_bin_collection/uk_bin_collection/councils/CheshireWestAndChesterCouncil.py +125 -0
- uk_bin_collection/uk_bin_collection/councils/ChorleyCouncil.py +134 -0
- uk_bin_collection/uk_bin_collection/councils/ConwyCountyBorough.py +27 -0
- uk_bin_collection/uk_bin_collection/councils/CrawleyBoroughCouncil.py +61 -0
- uk_bin_collection/uk_bin_collection/councils/CroydonCouncil.py +291 -0
- uk_bin_collection/uk_bin_collection/councils/DerbyshireDalesDistrictCouncil.py +100 -0
- uk_bin_collection/uk_bin_collection/councils/DoncasterCouncil.py +77 -0
- uk_bin_collection/uk_bin_collection/councils/DorsetCouncil.py +58 -0
- uk_bin_collection/uk_bin_collection/councils/DoverDistrictCouncil.py +41 -0
- uk_bin_collection/uk_bin_collection/councils/DurhamCouncil.py +49 -0
- uk_bin_collection/uk_bin_collection/councils/EastCambridgeshireCouncil.py +44 -0
- uk_bin_collection/uk_bin_collection/councils/EastDevonDC.py +74 -0
- uk_bin_collection/uk_bin_collection/councils/EastLindseyDistrictCouncil.py +108 -0
- uk_bin_collection/uk_bin_collection/councils/EastRidingCouncil.py +142 -0
- uk_bin_collection/uk_bin_collection/councils/EastSuffolkCouncil.py +112 -0
- uk_bin_collection/uk_bin_collection/councils/EastleighBoroughCouncil.py +70 -0
- uk_bin_collection/uk_bin_collection/councils/EnvironmentFirst.py +48 -0
- uk_bin_collection/uk_bin_collection/councils/ErewashBoroughCouncil.py +61 -0
- uk_bin_collection/uk_bin_collection/councils/FenlandDistrictCouncil.py +65 -0
- uk_bin_collection/uk_bin_collection/councils/ForestOfDeanDistrictCouncil.py +113 -0
- uk_bin_collection/uk_bin_collection/councils/GatesheadCouncil.py +118 -0
- uk_bin_collection/uk_bin_collection/councils/GedlingBoroughCouncil.py +1580 -0
- uk_bin_collection/uk_bin_collection/councils/GlasgowCityCouncil.py +55 -0
- uk_bin_collection/uk_bin_collection/councils/GuildfordCouncil.py +150 -0
- uk_bin_collection/uk_bin_collection/councils/HaltonBoroughCouncil.py +142 -0
- uk_bin_collection/uk_bin_collection/councils/HaringeyCouncil.py +59 -0
- uk_bin_collection/uk_bin_collection/councils/HarrogateBoroughCouncil.py +63 -0
- uk_bin_collection/uk_bin_collection/councils/HighPeakCouncil.py +134 -0
- uk_bin_collection/uk_bin_collection/councils/HullCityCouncil.py +48 -0
- uk_bin_collection/uk_bin_collection/councils/HuntingdonDistrictCouncil.py +44 -0
- uk_bin_collection/uk_bin_collection/councils/KingstonUponThamesCouncil.py +84 -0
- uk_bin_collection/uk_bin_collection/councils/KirkleesCouncil.py +130 -0
- uk_bin_collection/uk_bin_collection/councils/KnowsleyMBCouncil.py +139 -0
- uk_bin_collection/uk_bin_collection/councils/LancasterCityCouncil.py +71 -0
- uk_bin_collection/uk_bin_collection/councils/LeedsCityCouncil.py +137 -0
- uk_bin_collection/uk_bin_collection/councils/LisburnCastlereaghCityCouncil.py +101 -0
- uk_bin_collection/uk_bin_collection/councils/LiverpoolCityCouncil.py +65 -0
- uk_bin_collection/uk_bin_collection/councils/LondonBoroughHounslow.py +82 -0
- uk_bin_collection/uk_bin_collection/councils/LondonBoroughRedbridge.py +161 -0
- uk_bin_collection/uk_bin_collection/councils/MaldonDistrictCouncil.py +52 -0
- uk_bin_collection/uk_bin_collection/councils/MalvernHillsDC.py +57 -0
- uk_bin_collection/uk_bin_collection/councils/ManchesterCityCouncil.py +106 -0
- uk_bin_collection/uk_bin_collection/councils/MansfieldDistrictCouncil.py +38 -0
- uk_bin_collection/uk_bin_collection/councils/MertonCouncil.py +58 -0
- uk_bin_collection/uk_bin_collection/councils/MidAndEastAntrimBoroughCouncil.py +128 -0
- uk_bin_collection/uk_bin_collection/councils/MidSussexDistrictCouncil.py +80 -0
- uk_bin_collection/uk_bin_collection/councils/MiltonKeynesCityCouncil.py +54 -0
- uk_bin_collection/uk_bin_collection/councils/MoleValleyDistrictCouncil.py +98 -0
- uk_bin_collection/uk_bin_collection/councils/NeathPortTalbotCouncil.py +139 -0
- uk_bin_collection/uk_bin_collection/councils/NewarkAndSherwoodDC.py +52 -0
- uk_bin_collection/uk_bin_collection/councils/NewcastleCityCouncil.py +57 -0
- uk_bin_collection/uk_bin_collection/councils/NewhamCouncil.py +58 -0
- uk_bin_collection/uk_bin_collection/councils/NewportCityCouncil.py +203 -0
- uk_bin_collection/uk_bin_collection/councils/NorthEastDerbyshireDistrictCouncil.py +115 -0
- uk_bin_collection/uk_bin_collection/councils/NorthEastLincs.py +53 -0
- uk_bin_collection/uk_bin_collection/councils/NorthKestevenDistrictCouncil.py +45 -0
- uk_bin_collection/uk_bin_collection/councils/NorthLanarkshireCouncil.py +46 -0
- uk_bin_collection/uk_bin_collection/councils/NorthLincolnshireCouncil.py +58 -0
- uk_bin_collection/uk_bin_collection/councils/NorthNorfolkDistrictCouncil.py +108 -0
- uk_bin_collection/uk_bin_collection/councils/NorthNorthamptonshireCouncil.py +72 -0
- uk_bin_collection/uk_bin_collection/councils/NorthSomersetCouncil.py +76 -0
- uk_bin_collection/uk_bin_collection/councils/NorthTynesideCouncil.py +220 -0
- uk_bin_collection/uk_bin_collection/councils/NorthWestLeicestershire.py +114 -0
- uk_bin_collection/uk_bin_collection/councils/NorthYorkshire.py +58 -0
- uk_bin_collection/uk_bin_collection/councils/NorthumberlandCouncil.py +123 -0
- uk_bin_collection/uk_bin_collection/councils/NottinghamCityCouncil.py +36 -0
- uk_bin_collection/uk_bin_collection/councils/OldhamCouncil.py +51 -0
- uk_bin_collection/uk_bin_collection/councils/PortsmouthCityCouncil.py +131 -0
- uk_bin_collection/uk_bin_collection/councils/PrestonCityCouncil.py +97 -0
- uk_bin_collection/uk_bin_collection/councils/ReadingBoroughCouncil.py +30 -0
- uk_bin_collection/uk_bin_collection/councils/ReigateAndBansteadBoroughCouncil.py +81 -0
- uk_bin_collection/uk_bin_collection/councils/RenfrewshireCouncil.py +135 -0
- uk_bin_collection/uk_bin_collection/councils/RhonddaCynonTaffCouncil.py +80 -0
- uk_bin_collection/uk_bin_collection/councils/RochdaleCouncil.py +69 -0
- uk_bin_collection/uk_bin_collection/councils/RochfordCouncil.py +60 -0
- uk_bin_collection/uk_bin_collection/councils/RugbyBoroughCouncil.py +93 -0
- uk_bin_collection/uk_bin_collection/councils/RushcliffeBoroughCouncil.py +100 -0
- uk_bin_collection/uk_bin_collection/councils/RushmoorCouncil.py +81 -0
- uk_bin_collection/uk_bin_collection/councils/SalfordCityCouncil.py +70 -0
- uk_bin_collection/uk_bin_collection/councils/SevenoaksDistrictCouncil.py +106 -0
- uk_bin_collection/uk_bin_collection/councils/SheffieldCityCouncil.py +54 -0
- uk_bin_collection/uk_bin_collection/councils/ShropshireCouncil.py +45 -0
- uk_bin_collection/uk_bin_collection/councils/SolihullCouncil.py +48 -0
- uk_bin_collection/uk_bin_collection/councils/SomersetCouncil.py +203 -0
- uk_bin_collection/uk_bin_collection/councils/SouthAyrshireCouncil.py +73 -0
- uk_bin_collection/uk_bin_collection/councils/SouthCambridgeshireCouncil.py +65 -0
- uk_bin_collection/uk_bin_collection/councils/SouthGloucestershireCouncil.py +74 -0
- uk_bin_collection/uk_bin_collection/councils/SouthLanarkshireCouncil.py +78 -0
- uk_bin_collection/uk_bin_collection/councils/SouthNorfolkCouncil.py +91 -0
- uk_bin_collection/uk_bin_collection/councils/SouthOxfordshireCouncil.py +93 -0
- uk_bin_collection/uk_bin_collection/councils/SouthTynesideCouncil.py +98 -0
- uk_bin_collection/uk_bin_collection/councils/StAlbansCityAndDistrictCouncil.py +43 -0
- uk_bin_collection/uk_bin_collection/councils/StHelensBC.py +56 -0
- uk_bin_collection/uk_bin_collection/councils/StaffordshireMoorlandsDistrictCouncil.py +112 -0
- uk_bin_collection/uk_bin_collection/councils/StockportBoroughCouncil.py +39 -0
- uk_bin_collection/uk_bin_collection/councils/StokeOnTrentCityCouncil.py +79 -0
- uk_bin_collection/uk_bin_collection/councils/StratfordUponAvonCouncil.py +94 -0
- uk_bin_collection/uk_bin_collection/councils/SunderlandCityCouncil.py +100 -0
- uk_bin_collection/uk_bin_collection/councils/SwaleBoroughCouncil.py +52 -0
- uk_bin_collection/uk_bin_collection/councils/TamesideMBCouncil.py +62 -0
- uk_bin_collection/uk_bin_collection/councils/TandridgeDistrictCouncil.py +60 -0
- uk_bin_collection/uk_bin_collection/councils/TelfordAndWrekinCouncil.py +50 -0
- uk_bin_collection/uk_bin_collection/councils/TestValleyBoroughCouncil.py +203 -0
- uk_bin_collection/uk_bin_collection/councils/TonbridgeAndMallingBC.py +101 -0
- uk_bin_collection/uk_bin_collection/councils/TorbayCouncil.py +51 -0
- uk_bin_collection/uk_bin_collection/councils/TorridgeDistrictCouncil.py +154 -0
- uk_bin_collection/uk_bin_collection/councils/ValeofGlamorganCouncil.py +119 -0
- uk_bin_collection/uk_bin_collection/councils/ValeofWhiteHorseCouncil.py +103 -0
- uk_bin_collection/uk_bin_collection/councils/WakefieldCityCouncil.py +89 -0
- uk_bin_collection/uk_bin_collection/councils/WarwickDistrictCouncil.py +34 -0
- uk_bin_collection/uk_bin_collection/councils/WaverleyBoroughCouncil.py +119 -0
- uk_bin_collection/uk_bin_collection/councils/WealdenDistrictCouncil.py +86 -0
- uk_bin_collection/uk_bin_collection/councils/WelhatCouncil.py +73 -0
- uk_bin_collection/uk_bin_collection/councils/WestBerkshireCouncil.py +134 -0
- uk_bin_collection/uk_bin_collection/councils/WestLindseyDistrictCouncil.py +118 -0
- uk_bin_collection/uk_bin_collection/councils/WestLothianCouncil.py +103 -0
- uk_bin_collection/uk_bin_collection/councils/WestNorthamptonshireCouncil.py +34 -0
- uk_bin_collection/uk_bin_collection/councils/WestSuffolkCouncil.py +64 -0
- uk_bin_collection/uk_bin_collection/councils/WiganBoroughCouncil.py +97 -0
- uk_bin_collection/uk_bin_collection/councils/WiltshireCouncil.py +135 -0
- uk_bin_collection/uk_bin_collection/councils/WindsorAndMaidenheadCouncil.py +134 -0
- uk_bin_collection/uk_bin_collection/councils/WokingBoroughCouncil.py +114 -0
- uk_bin_collection/uk_bin_collection/councils/WyreCouncil.py +89 -0
- uk_bin_collection/uk_bin_collection/councils/YorkCouncil.py +45 -0
- uk_bin_collection/uk_bin_collection/councils/council_class_template/councilclasstemplate.py +33 -0
- uk_bin_collection/uk_bin_collection/get_bin_data.py +165 -0
- uk_bin_collection-0.74.0.dist-info/LICENSE +21 -0
- uk_bin_collection-0.74.0.dist-info/METADATA +247 -0
- uk_bin_collection-0.74.0.dist-info/RECORD +171 -0
- uk_bin_collection-0.74.0.dist-info/WHEEL +4 -0
- 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
|