Repository: alltheplaces/alltheplaces

Issue:
Banfield Pet Hospital
https://www.banfield.com/our-hospitals/hospital-locations


Discussion:

Resolving patch:
diff --git a/locations/hours.py b/locations/hours.py
--- a/locations/hours.py
+++ b/locations/hours.py
@@ -55,7 +55,9 @@ def as_opening_hours(self):
             opening_hours = '24/7'
         else:
             for day_group in day_groups:
-                if day_group['from_day'] == day_group['to_day']:
+                if not day_group['hours']:
+                    continue
+                elif day_group['from_day'] == day_group['to_day']:
                     opening_hours += '{from_day} {hours}; '.format(**day_group)
                 elif day_group['from_day'] == 'Su' and day_group['to_day'] == 'Sa':
                     opening_hours += '{hours}; '.format(**day_group)
diff --git a/locations/spiders/banfield_pet_hospital.py b/locations/spiders/banfield_pet_hospital.py
new file mode 100644
--- /dev/null
+++ b/locations/spiders/banfield_pet_hospital.py
@@ -0,0 +1,71 @@
+import scrapy
+import re
+import urllib.parse
+
+from locations.items import GeojsonPointItem
+from locations.hours import OpeningHours
+
+
+class BanfieldPetHospitalScraper(scrapy.Spider):
+    name = "banfield_pet_hospital"
+    allowed_domains = ["www.banfield.com"]
+    download_delay = 0.5
+    start_urls = (
+        'https://www.banfield.com/our-hospitals/hospital-locations/all-locations',
+    )
+
+    def parse_store(self, response):
+
+        elem = response.xpath('//div[contains(@class, "our-hospitals-location")]')
+
+        script_body = ' '.join(elem.xpath('.//script/text()').extract())
+        match = re.search(r'.*google.maps.LatLng\(([0-9.-]+),\s([0-9.-]+)\)', script_body)
+
+        lat, lon = match.groups()
+
+        # use last 3 elements of the store url as unique identifier (store number does not appear to be unique)
+        ref = "_".join(urllib.parse.urlsplit(response.url).path.split('/')[-3:])
+
+        number = elem.xpath('//div[@class="vcard"]/p[@id="hospitalAddressHospitalNumber"]/text()').extract_first()
+        number = re.search(r'Hospital\sNumber:\s+(\d+)', number).group(1)
+
+        properties = {
+            'name': elem.xpath('//div[@class="vcard"]/p[@class="fn"]/text()').extract_first(),
+            'addr_full': elem.xpath('//div[@class="vcard"]/span[@class="street-address"]/text()').extract_first(),
+            'phone': elem.xpath('//div[@class="vcard"]/p[@id="hospitalAddressPhone"]/text()').extract_first(),
+            'city': elem.xpath('//div[@class="vcard"]/span[@class="region"]/text()').extract_first(),
+            'state': elem.xpath('//div[@class="vcard"]/span[@class="state"]/text()').extract_first(),
+            'postcode': elem.xpath('//div[@class="vcard"]/span[@class="postal-code"]/text()').extract_first(),
+            'ref': ref,
+            'website': response.url,
+            'lat': lat,
+            'lon': lon,
+            'extras': {
+                'number': number
+            }
+        }
+
+        days = elem.xpath('//div[@class="hours"]/div[contains(@class, "day")]/@content').extract()
+        opening_hours = OpeningHours()
+
+        for d in days:
+            match = re.search(r'([A-Za-z]{2})\s([\d:]+)-([\d:]+)', d)
+            if match:
+                day, open, close = match.groups()
+                opening_hours.add_range(day=day, open_time=open, close_time=close)
+
+        hours = opening_hours.as_opening_hours()
+
+        if hours and hours != 'Mo-Su ':
+            properties['opening_hours'] = hours
+
+        yield GeojsonPointItem(**properties)
+
+    def parse(self, response):
+        stores = response.xpath('//li/table')
+        for store in stores:
+            elem = store.xpath('.//td[@class="hospname"]/a')
+            path = elem.xpath('.//@href').extract_first()
+            name = elem.xpath('.//text()').extract_first()
+
+            yield scrapy.Request(response.urljoin(path), callback=self.parse_store)


Test patch:
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
diff --git a/tests/test_opening_hours.py b/tests/test_opening_hours.py
new file mode 100644
--- /dev/null
+++ b/tests/test_opening_hours.py
@@ -0,0 +1,67 @@
+from locations.hours import OpeningHours
+
+
+def test_two_ranges():
+    o = OpeningHours()
+    o.add_range('Mo', '07:00', '17:00')
+    o.add_range('Tu', '07:00', '17:00')
+    o.add_range('We', '07:00', '17:00')
+
+    o.add_range('Fr', '08:00', '17:00')
+    o.add_range('Sa', '08:00', '17:00')
+
+    assert o.as_opening_hours() == "Mo-We 07:00-17:00; Fr-Sa 08:00-17:00"
+
+
+def test_mixed_ranges():
+    o = OpeningHours()
+    o.add_range('Mo', '08:00', '17:00')
+    o.add_range('Tu', '08:00', '17:00')
+    o.add_range('We', '09:00', '18:00')
+    o.add_range('Th', '09:00', '18:00')
+    o.add_range('Fr', '07:00', '17:00')
+    o.add_range('Su', '09:00', '17:00')
+
+    assert o.as_opening_hours() == "Mo-Tu 08:00-17:00; We-Th 09:00-18:00; Fr 07:00-17:00; Su 09:00-17:00"
+
+
+def test_closed_sunday():
+    o = OpeningHours()
+    o.add_range('Mo', '07:00', '17:00')
+    o.add_range('Tu', '07:00', '17:00')
+    o.add_range('We', '07:00', '17:00')
+    o.add_range('Th', '07:00', '17:00')
+    o.add_range('Fr', '07:00', '17:00')
+    o.add_range('Sa', '07:00', '17:00')
+
+    assert o.as_opening_hours() == "Mo-Sa 07:00-17:00"
+
+
+def test_closed_tuesday():
+    o = OpeningHours()
+    o.add_range('Mo', '07:00', '17:00')
+    o.add_range('We', '07:00', '17:00')
+    o.add_range('Th', '07:00', '17:00')
+    o.add_range('Fr', '07:00', '17:00')
+    o.add_range('Sa', '07:00', '17:00')
+    o.add_range('Su', '07:00', '17:00')
+
+    assert o.as_opening_hours() == "Mo 07:00-17:00; We-Su 07:00-17:00"
+
+
+def test_twentyfour_seven():
+    o = OpeningHours()
+    o.add_range('Mo', '0:00', '23:59')
+    o.add_range('Tu', '0:00', '23:59')
+    o.add_range('We', '0:00', '23:59')
+    o.add_range('Th', '0:00', '23:59')
+    o.add_range('Fr', '0:00', '23:59')
+    o.add_range('Sa', '0:00', '23:59')
+    o.add_range('Su', '0:00', '23:59')
+
+    assert o.as_opening_hours() == '24/7'
+
+
+def test_no_opening_hours():
+    o = OpeningHours()
+    assert o.as_opening_hours() == ''