Repository: alltheplaces/alltheplaces

Issue:
Little Caesars
https://order.littlecaesars.com/en-us/


Discussion:

Resolving patch:
diff --git a/locations/spiders/little_caesars.py b/locations/spiders/little_caesars.py
new file mode 100644
--- /dev/null
+++ b/locations/spiders/little_caesars.py
@@ -0,0 +1,129 @@
+import json
+import scrapy
+
+from locations.hours import OpeningHours
+from locations.items import GeojsonPointItem
+
+DAY_MAPPING = {
+    'SUN': 'Su',
+    'MON': 'Mo',
+    'TUE': 'Tu',
+    'WED': 'We',
+    'THU': 'Th',
+    'FRI': 'Fr',
+    'SAT': 'Sa',
+}
+
+HEADERS = {
+    'Accept': '*/*',
+    'Accept-Encoding': 'gzip, deflate, br',
+    'Accept-Language': 'en-US',
+    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36'
+}
+
+class LittleCaesarsSpider(scrapy.Spider):
+    name = "little_caesars"
+    allowed_domains = ["littlecaesars.com"]
+    download_delay = 0.1
+
+    def start_requests(self):
+        with open('./locations/searchable_points/us_zcta.csv') as points:
+            next(points) # Ignore the header
+            for point in points:
+                row = point.split(',')
+                zip = row[0].strip().strip('"')
+
+                url = f"https://api.cloud.littlecaesars.com/bff/api/stores?zip={zip}"
+
+                yield scrapy.http.Request(
+                    url,
+                    self.parse,
+                    method='GET',
+                    headers=HEADERS
+                )
+
+    def parse(self, response):
+        body = response.body_as_unicode()
+        if not body:
+            return
+
+        result = json.loads(body)
+        stores = result.get('stores')
+
+        for store in stores:
+            store_id = store.get('storeId')
+            if not store_id:
+                continue
+
+            # We already have some details about each store returned from the search
+            # in our current response, but a superset of these details are available
+            # through another API.
+            yield scrapy.http.Request(
+                url=f"https://api.cloud.littlecaesars.com/bff/api/stores/{store_id}",
+                method='GET',
+                callback=self.parse_store,
+                headers=HEADERS
+            )
+
+    def parse_store(self, response):
+        body = response.body_as_unicode()
+        if not body:
+            return
+
+        result = json.loads(body)
+        store = result.get('store')
+        if not store:
+            return
+
+        address = store.get('address')
+        if not address:
+            return
+
+        addr_full = None
+        street = store.get('street')
+        street2 = store.get('street2')
+        if street:
+            addr_full = street
+        if street2:
+            addr_full += f", {street2}"
+
+        properties = {
+            # 'siteId' appears to be the publicly facing store number.  I would prefer to use it, but
+            # it comes back as null for some stores.  'id' also appears in the data, reliably.
+            # So use it instead.
+            'ref': store.get('locationNumber'),
+            'addr_full': addr_full,
+            'city': address.get('city'),
+            'state': address.get('state'),
+            'postcode': address.get('zip'),
+            'phone': store.get('phone'),
+            'website': response.url,
+            'lon': store.get('longitude'),
+            'lat': store.get('latitude')
+        }
+
+        opening_hours = self.add_hours(result.get('orderingHours'))
+        if opening_hours:
+            properties["opening_hours"] = opening_hours
+
+        yield GeojsonPointItem(**properties)
+
+    def add_hours(self, ordering_hours):
+        opening_hours = OpeningHours()
+
+        for o in ordering_hours:
+            try:
+                opening_hours.add_range(
+                    DAY_MAPPING[o['dayName']],
+                    f"{o['hourOpen']}:{o['minuteOpen']}",
+                    f"{o['hourClose']}:{o['minuteClose']}"
+                )
+            except (KeyError, ValueError):
+                # In rare cases, we've seen the day name is missing from the data.
+                # If we hit any errors at all, don't return opening hours, even if
+                # we were able to get opening hours for different day names.  Otherwise,
+                # we may give the false impression the location is closed on certain days.
+                return
+
+        return opening_hours.as_opening_hours()
+


Test patch: