Repository: alltheplaces/alltheplaces

Issue:
Sleep Number
https://stores.sleepnumber.com/


Discussion:
This looks like the `YextSpider` with an overridden `request_page` method that overwrites the URL with:
`https://www.sleepnumber.com/yext/v2/accounts/me/entities/geosearch?location={latitude}%2C{longitude}8&limit=50&radius=2500&v=20200501&resolvePlaceholders=true&fields=googlePlaceId%2Caddress%2Cdescription%2Chours%2Cc_storePagesURLURL%2Cc_storeDisplayName%2Cc_siebelNumber%2Cc_landmark%2Cc_scheduleAnAppointmentURL%2Cclosed%2CgeocodedCoordinate%2CmainPhone&offset={offset}`

With `{latitude}` and `{longitude}` adjusted according to the 2500mi (or km?) search radius and `{offset}` used to page through search results 50 at a time.

I couldn't find an API key or other evidence that they're using the cloud hosted Yext API endpoint. It looks self-hosted.
Cloudflare protection as well.

Sleep Number

U.S.-based manufacturer of beds, foundations and bedding accessories

### Wikidata ID

Q7447640
https://www.wikidata.org/wiki/Q7447640
https://www.wikidata.org/wiki/Special:EntityData/Q7447640.json

### Store finder url(s)

Official Url(s): http://www.selectcomfort.com/
- [x] I have run the below and if anything in generated, added it.
- [ ] I have manually checked for a storefinder
- [ ] This is not a manufacturer listing outlets operating under their own brands / this will not create duplicates
`pipenv run scrapy sf --brand-wikidata=Q7447640 http://www.selectcomfort.com/`
```
```
Official Url(s): https://www.sleepnumber.com/
- [x] I have run the below and if anything in generated, added it.
- [x] I have manually checked for a storefinder
- [ ] This is not a manufacturer listing outlets operating under their own brands / this will not create duplicates
`pipenv run scrapy sf --brand-wikidata=Q7447640 https://www.sleepnumber.com/`
```
```
https://www.sleepnumber.com/stores/
https://www.sleepnumber.com/api/storefront/store-locations?lat=42.8473561&lng=-106.26166833&limit=1000&radius=25000
https://www.sleepnumber.com/sitemap.xml.gz

Stores links are:
```
https://www.sleepnumber.com/stores/al/mobile
https://www.sleepnumber.com/stores/al/mobile/3255-airport-blvd
https://www.sleepnumber.com/stores/al/montgomery
https://www.sleepnumber.com/stores/al/montgomery/7895-eastchase-pkwy
https://www.sleepnumber.com/stores/al/opelika
https://www.sleepnumber.com/stores/al/opelika/2065-interstate-drive
https://www.sleepnumber.com/stores/al/tuscaloosa
https://www.sleepnumber.com/stores/al/tuscaloosa/1397-mcfarland-blvd-e
```
With only /stores/\w+/\w+/\w+ being relevant.

However, not structured data for a business.

Resolving patch:
diff --git a/locations/spiders/sleep_number_us.py b/locations/spiders/sleep_number_us.py
new file mode 100644
--- /dev/null
+++ b/locations/spiders/sleep_number_us.py
@@ -0,0 +1,38 @@
+from scrapy import Spider
+
+from locations.categories import Categories
+from locations.dict_parser import DictParser
+from locations.hours import OpeningHours
+
+
+class SleepNumberUSSpider(Spider):
+    name = "sleep_number_us"
+    item_attributes = {"brand_wikidata": "Q7447640", "brand": "Sleep Number", "extras": Categories.SHOP_BED.value}
+    start_urls = [
+        "https://www.sleepnumber.com/api/storefront/store-locations?lat=42.8473561&lng=-106.26166833&limit=10000&radius=25000"
+    ]
+
+    def parse(self, response):
+        for store in response.json()["entities"]:
+            item = DictParser.parse(store)
+            item["ref"] = store["cid"]
+            item["website"] = store["c_storePagesURLURL"]
+
+            # This structure appears in multiple other spiders, should refactor.
+            # carhartt_wip, nissan_europe, stbant, primark.
+            oh = OpeningHours()
+            day_data = store.get("hours") or {}
+            day_dict = dict(day_data)
+            for day, value in day_dict.items():
+                value = dict(value)
+                if value == {} or value.get("isClosed"):
+                    continue
+                for interval in value.get("openIntervals"):
+                    oh.add_range(
+                        day=day.title()[:2],
+                        open_time=interval.get("start"),
+                        close_time=interval.get("end"),
+                        time_format="%H:%M",
+                    )
+            item["opening_hours"] = oh.as_opening_hours()
+            yield item


Test patch: