prompt: You will be provided with a partial code base and an issue statement explaining a problem to resolve.
  <issue>
  Fix Costa Coffee (US) which has switched from Amasty to a JSON API
  In the latest weekly run, Costa Coffee (costacoffee_us) is now broken with the Amasty "amlocator" AJAX endpoint no longer existing. In it's place appears to be a JSON API that returns store details (locations and addresses):
  
  https://us.costacoffee.com/api/cf/?locale=en-US&include=2&content_type=storeLocatorStore&limit=500&fields.location[near]=33.77804102,-84.38068933
  
  This appears to be a fairly easy fix to rewrite the spider to use the new JSON API (with a higher limit than 500).
  
  </issue>
  <code>
  [start of locations/spiders/costacoffee_us.py]
  1 import json
  2 import re
  3 
  4 import scrapy
  5 from scrapy import Selector
  6 
  7 from locations.categories import Categories, apply_category
  8 from locations.items import Feature
  9 
  10 
  11 class CostaCoffeeUSSpider(scrapy.Spider):
  12     name = "costacoffee_us"
  13     item_attributes = {"brand": "Costa Coffee", "brand_wikidata": "Q608845"}
  14     allowed_domains = ["us.costacoffee.com"]
  15     start_urls = ["https://us.costacoffee.com/amlocator/index/ajax"]
  16 
  17     def parse(self, response):
  18         script = response.xpath('//script[contains(text(), "amLocator")]/text()').extract_first()
  19 
  20         start = script.index("jsonLocations: ") + len("jsonLocations: ")
  21         stop = script.index("imageLocations")
  22 
  23         locations = script[start:stop].strip().strip(",")
  24         items = json.loads(locations)["items"]
  25 
  26         for store in items:
  27             item = Feature()
  28             item["ref"] = store["id"]
  29             item["lat"] = store["lat"]
  30             item["lon"] = store["lng"]
  31 
  32             html = Selector(text=store["popup_html"])
  33 
  34             item["name"] = html.xpath('//*[@class="amlocator-title"]/text()').get()
  35 
  36             for line in html.xpath('//div[@class="amlocator-info-popup"]/text()').getall():
  37                 line = line.strip()
  38                 if m := re.match(r"City: (.*)", line):
  39                     item["city"] = m.group(1)
  40                 elif m := re.match(r"Zip: (.*)", line):
  41                     item["postcode"] = m.group(1)
  42                 elif m := re.match(r"Address: (.*)", line):
  43                     item["street_address"] = m.group(1)
  44                 elif m := re.match(r"State: (.*)", line):
  45                     item["state"] = m.group(1)
  46 
  47             apply_category(Categories.COFFEE_SHOP, item)
  48 
  49             yield item
  50
  [end of locations/spiders/costacoffee_us.py]
  </code>
  
  I need you to solve this issue by generating a single patch file that I can apply directly to this repository using git apply. Please respond with a single patch file in the following format.
  <patch>
  diff --git a/file.py b/file.py
  --- a/file.py
  +++ b/file.py
  @@ -1,27 +1,35 @@
   def euclidean(a, b):
  -    while b:
  -        a, b = b, a % b
  -    return a
  +    if b == 0:
  +        return a
  +    return euclidean(b, a % b)
   
   
   def bresenham(x0, y0, x1, y1):
       points = []
       dx = abs(x1 - x0)
       dy = abs(y1 - y0)
  -    sx = 1 if x0 < x1 else -1
  -    sy = 1 if y0 < y1 else -1
  -    err = dx - dy
  +    x, y = x0, y0
  +    sx = -1 if x0 > x1 else 1
  +    sy = -1 if y0 > y1 else 1
   
  -    while True:
  -        points.append((x0, y0))
  -        if x0 == x1 and y0 == y1:
  -            break
  -        e2 = 2 * err
  -        if e2 > -dy:
  -            err -= dy
  -            x0 += sx
  -        if e2 < dx:
  -            err += dx
  -            y0 += sy
  +    if dx > dy:
  +        err = dx / 2.0
  +        while x != x1:
  +            points.append((x, y))
  +            err -= dy
  +            if err < 0:
  +                y += sy
  +                err += dx
  +            x += sx
  +    else:
  +        err = dy / 2.0
  +        while y != y1:
  +            points.append((x, y))
  +            err -= dx
  +            if err < 0:
  +                x += sx
  +                err += dy
  +            y += sy
  +
  +    points.append((x, y))
       return points
  
  </patch>
golden_diff: diff --git a/locations/spiders/costacoffee_us.py b/locations/spiders/costacoffee_us.py
  --- a/locations/spiders/costacoffee_us.py
  +++ b/locations/spiders/costacoffee_us.py
  @@ -1,49 +1,38 @@
  -import json
  -import re
  -
  -import scrapy
  -from scrapy import Selector
  +from scrapy import Spider
  +from scrapy.http import JsonRequest
   
   from locations.categories import Categories, apply_category
  -from locations.items import Feature
  +from locations.dict_parser import DictParser
  +from locations.hours import DAYS_FULL, OpeningHours
   
   
  -class CostaCoffeeUSSpider(scrapy.Spider):
  +class CostaCoffeeUSSpider(Spider):
       name = "costacoffee_us"
       item_attributes = {"brand": "Costa Coffee", "brand_wikidata": "Q608845"}
       allowed_domains = ["us.costacoffee.com"]
  -    start_urls = ["https://us.costacoffee.com/amlocator/index/ajax"]
  -
  -    def parse(self, response):
  -        script = response.xpath('//script[contains(text(), "amLocator")]/text()').extract_first()
  -
  -        start = script.index("jsonLocations: ") + len("jsonLocations: ")
  -        stop = script.index("imageLocations")
  -
  -        locations = script[start:stop].strip().strip(",")
  -        items = json.loads(locations)["items"]
  -
  -        for store in items:
  -            item = Feature()
  -            item["ref"] = store["id"]
  -            item["lat"] = store["lat"]
  -            item["lon"] = store["lng"]
  +    start_urls = ["https://us.costacoffee.com/api/cf/?content_type=storeLocatorStore"]
  +    page_size = 1000
   
  -            html = Selector(text=store["popup_html"])
  -
  -            item["name"] = html.xpath('//*[@class="amlocator-title"]/text()').get()
  -
  -            for line in html.xpath('//div[@class="amlocator-info-popup"]/text()').getall():
  -                line = line.strip()
  -                if m := re.match(r"City: (.*)", line):
  -                    item["city"] = m.group(1)
  -                elif m := re.match(r"Zip: (.*)", line):
  -                    item["postcode"] = m.group(1)
  -                elif m := re.match(r"Address: (.*)", line):
  -                    item["street_address"] = m.group(1)
  -                elif m := re.match(r"State: (.*)", line):
  -                    item["state"] = m.group(1)
  +    def start_requests(self):
  +        for url in self.start_urls:
  +            yield JsonRequest(url=f"{url}&limit={self.page_size}")
   
  +    def parse(self, response):
  +        for location in response.json()["items"]:
  +            item = DictParser.parse(location["fields"])
  +            item["ref"] = location["sys"]["id"]
  +            item["addr_full"] = location["fields"]["storeAddress"]
  +            item["opening_hours"] = OpeningHours()
  +            for day_name in [s.lower() for s in DAYS_FULL]:
  +                open_time = location["fields"].get(f"{day_name}Opening")
  +                close_time = location["fields"].get(f"{day_name}Closing")
  +                if open_time and "24 HOURS" in open_time.upper():
  +                    item["opening_hours"].add_range(day_name, "00:00", "24:00")
  +                elif open_time and close_time:
  +                    item["opening_hours"].add_range(day_name, open_time, close_time)
               apply_category(Categories.COFFEE_SHOP, item)
  -
               yield item
  +
  +        offset = response.json()["skip"]
  +        if offset + response.json()["limit"] < response.json()["total"]:
  +            yield JsonRequest(url=f"{response.request.url}&limit={self.page_size}&offset={offset}")
  
verification_info: {"golden_diff": "diff --git a/locations/spiders/costacoffee_us.py b/locations/spiders/costacoffee_us.py\n--- a/locations/spiders/costacoffee_us.py\n+++ b/locations/spiders/costacoffee_us.py\n@@ -1,49 +1,38 @@\n-import json\n-import re\n-\n-import scrapy\n-from scrapy import Selector\n+from scrapy import Spider\n+from scrapy.http import JsonRequest\n \n from locations.categories import Categories, apply_category\n-from locations.items import Feature\n+from locations.dict_parser import DictParser\n+from locations.hours import DAYS_FULL, OpeningHours\n \n \n-class CostaCoffeeUSSpider(scrapy.Spider):\n+class CostaCoffeeUSSpider(Spider):\n     name = \"costacoffee_us\"\n     item_attributes = {\"brand\": \"Costa Coffee\", \"brand_wikidata\": \"Q608845\"}\n     allowed_domains = [\"us.costacoffee.com\"]\n-    start_urls = [\"https://us.costacoffee.com/amlocator/index/ajax\"]\n-\n-    def parse(self, response):\n-        script = response.xpath('//script[contains(text(), \"amLocator\")]/text()').extract_first()\n-\n-        start = script.index(\"jsonLocations: \") + len(\"jsonLocations: \")\n-        stop = script.index(\"imageLocations\")\n-\n-        locations = script[start:stop].strip().strip(\",\")\n-        items = json.loads(locations)[\"items\"]\n-\n-        for store in items:\n-            item = Feature()\n-            item[\"ref\"] = store[\"id\"]\n-            item[\"lat\"] = store[\"lat\"]\n-            item[\"lon\"] = store[\"lng\"]\n+    start_urls = [\"https://us.costacoffee.com/api/cf/?content_type=storeLocatorStore\"]\n+    page_size = 1000\n \n-            html = Selector(text=store[\"popup_html\"])\n-\n-            item[\"name\"] = html.xpath('//*[@class=\"amlocator-title\"]/text()').get()\n-\n-            for line in html.xpath('//div[@class=\"amlocator-info-popup\"]/text()').getall():\n-                line = line.strip()\n-                if m := re.match(r\"City: (.*)\", line):\n-                    item[\"city\"] = m.group(1)\n-                elif m := re.match(r\"Zip: (.*)\", line):\n-                    item[\"postcode\"] = m.group(1)\n-                elif m := re.match(r\"Address: (.*)\", line):\n-                    item[\"street_address\"] = m.group(1)\n-                elif m := re.match(r\"State: (.*)\", line):\n-                    item[\"state\"] = m.group(1)\n+    def start_requests(self):\n+        for url in self.start_urls:\n+            yield JsonRequest(url=f\"{url}&limit={self.page_size}\")\n \n+    def parse(self, response):\n+        for location in response.json()[\"items\"]:\n+            item = DictParser.parse(location[\"fields\"])\n+            item[\"ref\"] = location[\"sys\"][\"id\"]\n+            item[\"addr_full\"] = location[\"fields\"][\"storeAddress\"]\n+            item[\"opening_hours\"] = OpeningHours()\n+            for day_name in [s.lower() for s in DAYS_FULL]:\n+                open_time = location[\"fields\"].get(f\"{day_name}Opening\")\n+                close_time = location[\"fields\"].get(f\"{day_name}Closing\")\n+                if open_time and \"24 HOURS\" in open_time.upper():\n+                    item[\"opening_hours\"].add_range(day_name, \"00:00\", \"24:00\")\n+                elif open_time and close_time:\n+                    item[\"opening_hours\"].add_range(day_name, open_time, close_time)\n             apply_category(Categories.COFFEE_SHOP, item)\n-\n             yield item\n+\n+        offset = response.json()[\"skip\"]\n+        if offset + response.json()[\"limit\"] < response.json()[\"total\"]:\n+            yield JsonRequest(url=f\"{response.request.url}&limit={self.page_size}&offset={offset}\")\n", "issue": "Fix Costa Coffee (US) which has switched from Amasty to a JSON API\nIn the latest weekly run, Costa Coffee (costacoffee_us) is now broken with the Amasty \"amlocator\" AJAX endpoint no longer existing. In it's place appears to be a JSON API that returns store details (locations and addresses):\r\n\r\nhttps://us.costacoffee.com/api/cf/?locale=en-US&include=2&content_type=storeLocatorStore&limit=500&fields.location[near]=33.77804102,-84.38068933\r\n\r\nThis appears to be a fairly easy fix to rewrite the spider to use the new JSON API (with a higher limit than 500).\n", "before_files": [{"content": "import json\nimport re\n\nimport scrapy\nfrom scrapy import Selector\n\nfrom locations.categories import Categories, apply_category\nfrom locations.items import Feature\n\n\nclass CostaCoffeeUSSpider(scrapy.Spider):\n    name = \"costacoffee_us\"\n    item_attributes = {\"brand\": \"Costa Coffee\", \"brand_wikidata\": \"Q608845\"}\n    allowed_domains = [\"us.costacoffee.com\"]\n    start_urls = [\"https://us.costacoffee.com/amlocator/index/ajax\"]\n\n    def parse(self, response):\n        script = response.xpath('//script[contains(text(), \"amLocator\")]/text()').extract_first()\n\n        start = script.index(\"jsonLocations: \") + len(\"jsonLocations: \")\n        stop = script.index(\"imageLocations\")\n\n        locations = script[start:stop].strip().strip(\",\")\n        items = json.loads(locations)[\"items\"]\n\n        for store in items:\n            item = Feature()\n            item[\"ref\"] = store[\"id\"]\n            item[\"lat\"] = store[\"lat\"]\n            item[\"lon\"] = store[\"lng\"]\n\n            html = Selector(text=store[\"popup_html\"])\n\n            item[\"name\"] = html.xpath('//*[@class=\"amlocator-title\"]/text()').get()\n\n            for line in html.xpath('//div[@class=\"amlocator-info-popup\"]/text()').getall():\n                line = line.strip()\n                if m := re.match(r\"City: (.*)\", line):\n                    item[\"city\"] = m.group(1)\n                elif m := re.match(r\"Zip: (.*)\", line):\n                    item[\"postcode\"] = m.group(1)\n                elif m := re.match(r\"Address: (.*)\", line):\n                    item[\"street_address\"] = m.group(1)\n                elif m := re.match(r\"State: (.*)\", line):\n                    item[\"state\"] = m.group(1)\n\n            apply_category(Categories.COFFEE_SHOP, item)\n\n            yield item\n", "path": "locations/spiders/costacoffee_us.py"}]}