Repository: alltheplaces/alltheplaces

Issue:
Ulta
Makeup/Beauty shop

https://www.ulta.com/stores/directory


Discussion:
This issue hasn't been worked on in a while. I'm removing the assignment so others can work on it. If you're still working on it, please reassign yourself and reference a pull request or branch where you're doing your work.

Resolving patch:
diff --git a/locations/hours.py b/locations/hours.py
new file mode 100644
--- /dev/null
+++ b/locations/hours.py
@@ -0,0 +1,66 @@
+from collections import defaultdict
+import time
+
+
+DAYS = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su']
+
+class OpeningHours(object):
+    def __init__(self):
+        self.day_hours = defaultdict(list)
+
+    def add_range(self, day, open_time, close_time):
+        if day not in DAYS:
+            raise ValueError("day must be one of " + ', '.join(DAYS))
+
+        if not isinstance(open_time, time.struct_time):
+            open_time = time.strptime(open_time, '%H:%M')
+        if not isinstance(close_time, time.struct_time):
+            close_time = time.strptime(close_time, '%H:%M')
+
+        self.day_hours[day].append((open_time, close_time))
+
+    def as_opening_hours(self):
+        day_groups = []
+        this_day_group = None
+
+        for day in DAYS:
+            hours = ' '.join(
+                '%s-%s' % (
+                    time.strftime('%H:%M', h[0]),
+                    time.strftime('%H:%M', h[1]),
+                )
+                for h in self.day_hours[day]
+            )
+
+            if not this_day_group:
+                this_day_group = {
+                    'from_day': day,
+                    'to_day': day,
+                    'hours': hours
+                }
+            elif this_day_group['hours'] != hours:
+                day_groups.append(this_day_group)
+                this_day_group = {
+                    'from_day': day,
+                    'to_day': day,
+                    'hours': hours
+                }
+            elif this_day_group['hours'] == hours:
+                this_day_group['to_day'] = day
+
+        day_groups.append(this_day_group)
+
+        opening_hours = ""
+        if len(day_groups) == 1 and day_groups[0]['hours'] in ('00:00-23:59', '00:00-00:00'):
+            opening_hours = '24/7'
+        else:
+            for day_group in day_groups:
+                if 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)
+                else:
+                    opening_hours += '{from_day}-{to_day} {hours}; '.format(**day_group)
+            opening_hours = opening_hours[:-2]
+
+        return opening_hours
diff --git a/locations/spiders/ulta_beauty.py b/locations/spiders/ulta_beauty.py
new file mode 100644
--- /dev/null
+++ b/locations/spiders/ulta_beauty.py
@@ -0,0 +1,76 @@
+# -*- coding: utf-8 -*-
+import json
+import re
+import scrapy
+
+from locations.items import GeojsonPointItem
+from locations.hours import OpeningHours
+
+DAYS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
+
+
+class UltaBeautySpider(scrapy.Spider):
+    name = "ulta-beauty"
+    allowed_domains = ["ulta.com", "sweetiq.com"]
+    start_urls = (
+        'https://api.sweetiq.com/store-locator/public/locations/582b2f2f588e96c131eefa9f?page=1&perPage=0&searchFields%5B0%5D=name&countLocations=true&clientIds%5B0%5D=57b75cf805fbd94379859661',
+    )
+
+    def store_hours(self, store_hours):
+        result = 'Mo'
+        last_day = 'Mo'
+        last_period = store_hours['Mon'][0][0]+'-'+store_hours['Mon'][0][1]
+        for k in DAYS:
+
+            if len(store_hours[k]):
+                period = store_hours[k][0][0]+'-'+store_hours[k][0][1]
+                if period == last_period:
+                    continue
+                if DAYS[DAYS.index(k)-1][:2] == last_day:
+                    result += ' '+last_period+'; '
+                else:
+                    result += '-'+DAYS[DAYS.index(k)-1][:2]+' '+last_period+'; '
+                last_period = period
+                last_day = k[:2]
+                result += k[:2]
+            else:
+                if k == "Mo":
+                    result = ''
+                    last_period = ''
+                if last_period:
+                    if DAYS[DAYS.index(k)-1][:2] == last_day:
+                        result += ' '+last_period+'; '
+                    else:
+                        result += '-'+DAYS[DAYS.index(k)-1][:2]+' '+last_period + '; '
+                last_period = ''
+                last_day = ''
+
+        if last_day != 'Su' and last_period != '':
+            result += '-Su'
+
+        result += ' ' + last_period
+        return result
+
+    def parse(self, response):
+        shops = json.loads(response.text)
+
+        for shop in shops['records']:
+            o = OpeningHours()
+            for d, ranges in shop['hoursOfOperation'].items():
+                for r in ranges:
+                    o.add_range(d[:2], r[0], r[1])
+
+            yield GeojsonPointItem(
+                website=shop['website'],
+                ref=shop['branch'],
+                opening_hours=o.as_opening_hours(),
+                phone=shop['phone'],
+                addr_full=shop['address'],
+                postcode=shop['postalCode'],
+                city=shop['city'],
+                state=shop['province'],
+                country=shop['country'],
+                lat=shop['geo'][1],
+                lon=shop['geo'][0],
+            )
+


Test patch: