Repository: alltheplaces/alltheplaces

Issue:
Inpost parcel lockers (>22 000 POIs)
### Brand name

Inpost

### Wikidata ID

Q3182097

### Store finder url(s)

https://inpost.pl/en/find-location


Discussion:
JSON object parser needed for `https://inpost.pl/sites/default/files/points.json` which returns all locations.

The field names are minimised and obscured but it looks fairly simple to figure it out.
t=1 for parcel lockers (Categories.PARCEL_LOCKER) and t=2 for parcel pickup locations (Categories.PRODUCT_PICKUP)

Resolving patch:
diff --git a/locations/hours.py b/locations/hours.py
--- a/locations/hours.py
+++ b/locations/hours.py
@@ -766,10 +766,12 @@ def time_of_day_regex(time_24h: bool = True) -> str:
         """
         if time_24h is True:
             # Regular expression for extracting 24h times (e.g. 15:45)
-            time_regex = r"(?<!\d)(\d(?!\d)|[01]\d|2[0-4])(?:[:\.]?([0-5]\d))(?:[:\.]?[0-5]\d)?(?!(?:\d|[AP]M))"
+            time_regex = r"(?<!\d)(\d(?!\d)|[01]\d|2[0-4])(?:(?:[:\.]?([0-5]\d))(?:[:\.]?[0-5]\d)?)?(?!(?:\d|:|[AP]M))"
         else:
             # Regular expression for extracting 12h times (e.g. 9:30AM)
-            time_regex = r"(?<!\d)(\d(?!\d)|0\d|1[012])(?:(?:[:\.]?([0-5]\d))(?:[:\.]?[0-5]\d)?)?\s*([AP]M)?(?!\d)"
+            time_regex = (
+                r"(?<!\d)(\d(?!\d)|0\d|1[012])(?:(?:[:\.]?([0-5]\d))(?:[:\.]?[0-5]\d)?)?\s*([AP]M)?(?!(?:\d|:|[AP]M))"
+            )
         return time_regex
 
     @staticmethod
@@ -909,8 +911,14 @@ def extract_hours_from_string(
                     re.IGNORECASE,
                 )
                 for time_range in time_ranges:
+                    time_start_minute = time_range[1]
+                    if not time_range[1]:
+                        time_start_minute = "00"
+                    time_end_minute = time_range[3]
+                    if not time_range[3]:
+                        time_end_minute = "00"
                     results.append(
-                        (days_in_range, f"{time_range[0]}:{time_range[1]}", f"{time_range[2]}:{time_range[3]}")
+                        (days_in_range, f"{time_range[0]}:{time_start_minute}", f"{time_range[2]}:{time_end_minute}")
                     )
         elif len(results_12h) > 0:
             # Parse 12h opening hour information.
@@ -928,10 +936,13 @@ def extract_hours_from_string(
                     re.IGNORECASE,
                 )
                 for time_range in time_ranges:
+                    time_start_hour = time_range[0]
+                    if time_start_hour == "00":
+                        time_start_hour = "12"
                     if time_range[1]:
-                        time_start = f"{time_range[0]}:{time_range[1]}"
+                        time_start = f"{time_start_hour}:{time_range[1]}"
                     else:
-                        time_start = f"{time_range[0]}:00"
+                        time_start = f"{time_start_hour}:00"
                     if time_range[2]:
                         time_start = f"{time_start}{time_range[2].upper()}"
                     else:
@@ -939,14 +950,17 @@ def extract_hours_from_string(
                         time_start = f"{time_start}AM"
                     time_start_24h = time.strptime(time_start, "%I:%M%p")
                     time_start_24h = time.strftime("%H:%M", time_start_24h)
+                    time_end_hour = time_range[3]
+                    if time_end_hour == "00":
+                        time_end_hour = "12"
                     if time_range[4]:
-                        time_end = f"{time_range[3]}:{time_range[4]}"
+                        time_end = f"{time_end_hour}:{time_range[4]}"
                     else:
-                        time_end = f"{time_range[3]}:00"
+                        time_end = f"{time_end_hour}:00"
                     if time_range[5]:
                         time_end = f"{time_end}{time_range[5].upper()}"
                     else:
-                        # If AM/PM is not specified, it is almost always going to be PM for start times.
+                        # If AM/PM is not specified, it is almost always going to be PM for end times.
                         time_end = f"{time_end}PM"
                     time_end_24h = time.strptime(time_end, "%I:%M%p")
                     time_end_24h = time.strftime("%H:%M", time_end_24h)
diff --git a/locations/spiders/inpost_pl.py b/locations/spiders/inpost_pl.py
new file mode 100644
--- /dev/null
+++ b/locations/spiders/inpost_pl.py
@@ -0,0 +1,48 @@
+from scrapy import Spider
+from scrapy.http import JsonRequest
+from unidecode import unidecode
+
+from locations.categories import Categories, apply_category
+from locations.hours import DAYS, DAYS_PL, OpeningHours
+from locations.items import Feature
+
+
+class InPostPLSpider(Spider):
+    name = "inpost_pl"
+    item_attributes = {"brand": "InPost", "brand_wikidata": "Q3182097"}
+    allowed_domains = ["inpost.pl"]
+    start_urls = ["https://inpost.pl/sites/default/files/points.json"]
+
+    def start_requests(self):
+        for url in self.start_urls:
+            yield JsonRequest(url=url)
+
+    def parse(self, response):
+        for location in response.json()["items"]:
+            properties = {
+                "ref": location["n"],
+                "lat": location["l"]["a"],
+                "lon": location["l"]["o"],
+                "housenumber": location["b"],
+                "street": location["e"],
+                "city": location["c"],
+                "state": location["r"].title(),
+                "postcode": location["o"],
+                "image": "https://geowidget.easypack24.net/uploads/pl/images/" + location["n"] + ".jpg",
+                "opening_hours": OpeningHours(),
+            }
+            if location["h"] == "24/7":
+                properties["opening_hours"].add_days_range(DAYS, "00:00", "23:59")
+            else:
+                properties["opening_hours"].add_ranges_from_string(location["h"], days=DAYS_PL)
+            slug_parts = []
+            if location["t"] == 1:
+                apply_category(Categories.PARCEL_LOCKER, properties)
+                slug_parts = ["parcel-locker", location["g"], location["n"], location["e"], "paczkomaty", location["r"]]
+            elif location["t"] == 2:
+                apply_category(Categories.PRODUCT_PICKUP, properties)
+                slug_parts = ["parcel-service-point", location["n"], location["g"], location["e"]]
+            if len(slug_parts) > 0:
+                slug = "-".join(map(lambda x: unidecode(x.strip().lower()), slug_parts))
+                properties["website"] = "https://inpost.pl/en/" + slug
+            yield Feature(**properties)


Test patch:
diff --git a/tests/test_opening_hours.py b/tests/test_opening_hours.py
--- a/tests/test_opening_hours.py
+++ b/tests/test_opening_hours.py
@@ -401,3 +401,9 @@ def test_add_ranges_from_string():
         "{Sun|056:00AM-08:00PM}{Mon|05:00AM-09:00PM}{Tue|05:00AM-09:00PM}{Wed|05:00AM-09:00PM}{Thu|05:00AM-09:00PM}{Fri|05:00AM-09:00PM}{Sat|c}"
     )
     assert o.as_opening_hours() == "Mo-Fr 05:00-21:00"
+
+    o = OpeningHours()
+    o.add_ranges_from_string("Mo-Tu 06-12,We 14-18:30,Th 09-17,Fr 04-24,Sa-Su 00:00-11:59")
+    assert (
+        o.as_opening_hours() == "Mo-Tu 06:00-12:00; We 14:00-18:30; Th 09:00-17:00; Fr 04:00-24:00; Sa-Su 00:00-11:59"
+    )