Package: osm-opening-hours-humanized
Humanized Opening Hours - A parser for the opening_hours fields from OSMHumanized Opening Hoursis a Python 3 module allowing a simple usage of the opening_hours fields used in OpenStreetMap.Due to a lack of free time, the developpement of this module is paused. You can of course use it, but its features won't evolve before a (long) moment. If you want to become maintainer, don't hesitate to create an issue!>>>importosm_opening_hours_humanizedashoh>>>field="Mo-Fr 06:00-21:00; Sa,Su 08:00-12:00">>>oh=hoh.OHParser(field,locale="en")>>>oh.is_open()True>>>oh.next_change()datetime.datetime(2017,12,24,12,0)>>>print('\n'.join(oh.description()))"""From Monday to Friday: 6:00 AM – 9:00 PM.From Saturday to Sunday: 8:00 AM – 12:00 PM."""This module is in beta. It should be production ready, but some bugs or minor modifications are still possible. Don't hesitate to create an issue!Table of contentsInstallationDependenciesHow to use itBasic methodsSolar hoursHave nice schedulesSupported field formatsAlternativesPerformancesLicenceInstallationThis library is so small, you can include it directly into your project.
Also, it is available on PyPi.$ pip3 install osm-humanized-opening-hoursDependenciesThis module requires the following modules, which should be automatically installed when installing HOH withpip.lark-parserbabelastralHow to use itThe only mandatory argument to give to the constructor is the field, which must be a string.
It can also take alocaleargument, which can be any valid locale name. You can change it later by changing thelocaleattribute (which is, in fact, aproperty).
However, to be able to use the most of the rendering methods, it must be inhoh.AVAILABLE_LOCALES(a warning will be printed otherwise).>>>importosm_opening_hours_humanizedashoh>>>field="Mo-Fr 06:00-21:00; Sa,Su 07:00-21:00">>>oh=hoh.OHParser(field)If you have a GeoJSON, you can use a dedicated classmethod:from_geojson(), which returns anOHParserinstance.
It takes the GeoJSON, and optionally the following arguments:timezone_getter(callable): A function to call, which takes two arguments (latitude and longitude, as floats), and returns a timezone name or None, allowing to get solar hours for the facility;locale(str): the locale to use ("en" default).Basic methodsTo know if the facility is open at the present time. Returns a boolean.
Can take a datetime.datetime moment to check for another time.>>>oh.is_open()TrueTo know at which time the facility status (open / closed) will change.
Returns a datetime.datetime object.
It can take a datetime.datetime moment to get next change from another time.
If we are on December 24 before 21:00 / 09:00PM...>>>oh.next_change()datetime.datetime(2017,12,24,21,0)For fields with consecutive days fully open,next_change()will try to get the true next change by recursion.
You can change this behavior with themax_recursionargument, which is set to31default, meaningnext_change()will try a maximum of 31 recursions (i.e.31 days, or a month) to get the true next change.
If this limit is reached, aNextChangeRecursionErrorwill be raised.
You can deny recursion by setting themax_recursionargument to0.TheNextChangeRecursionErrorhas alast_changeattribute, containing the last change got just before raising of the exception.
You can get it with aexcept NextChangeRecursionError as e:block.>>>oh=hoh.OHParser("Mo-Fr 00:00-24:00")>>>oh.next_change(dt=datetime.datetime(2018,1,8,0,0))datetime.datetime(2018,1,11,23,59,59,999999)To get a list of the opening periods between to dates, you can the useopening_periods_between()method.
It takes two arguments, which can bedatetime.dateordatetime.datetimeobjects.
If you passdatetime.dateobjects, it will return all opening periods between these dates (inclusive).
If you passdatetime.datetime, the returned opening periods will be "choped" on these times.The returned opening periods are tuples of twodatetime.datetimeobjects, representing the beginning and the end of the period.>>>oh=hoh.OHParser("Mo-Fr 06:00-21:00; Sa,Su 07:00-21:00")>>>oh.opening_periods_between(datetime.date(2018,1,1),datetime.date(2018,1,7))[(datetime.datetime(2018,1,1,6,0),datetime.datetime(2018,1,1,21,0)),(datetime.datetime(2018,1,2,6,0),datetime.datetime(2018,1,2,21,0)),(datetime.datetime(2018,1,3,6,0),datetime.datetime(2018,1,3,21,0)),(datetime.datetime(2018,1,4,6,0),datetime.datetime(2018,1,4,21,0)),(datetime.datetime(2018,1,5,6,0),datetime.datetime(2018,1,5,21,0)),(datetime.datetime(2018,1,6,7,0),datetime.datetime(2018,1,6,21,0)),(datetime.datetime(2018,1,7,7,0),datetime.datetime(2018,1,7,21,0))]You can also set themergeparameter to True, to merge continuous opening periods.You can get a sanitized version of the field given to the constructor with thesanitize()function or thefieldattribute.>>>field="mo-su 09:30-20h;jan off">>>print(hoh.sanitize(field))"Mo-Su 09:30-20:00; Jan off"If sanitization is the only thing you need, use HOH for this is probably overkill.
You might be interested in theOH Sanitizermodule, or you can copy directly the code of the sanitize function in your project.If you try to parse a field which is invalid or contains a pattern which is not supported, anosm_opening_hours_humanized.exceptions.ParseError(inheriting fromosm_opening_hours_humanized.exceptions.HOHError) will be raised.If a field contains only a comment (like"on appointment"), aCommentOnlyFieldexception (inheriting fromParseError) will be raised.
It contains acommentattribute, allowing you to display it instead of the opening hours.TheOHParsercontains anis_24_7attribute, which is true if the field is simply24/7or00:00-24:00, and false either.
Thenext_change()method won't try recursion if this attribute is true and will directly raise aNextChangeRecursionError(except if you setmax_recursionto zero, in this case it will just return the last time of the current day).You can check equality between twoOHParserinstances.
It will be true if both have the same field and the same location.>>>importosm_opening_hours_humanizedashoh>>>>>>oh1=hoh.OHParser("Mo 10:00-20:00")>>>oh2=hoh.OHParser("Mo 10:00-20:00")>>>oh3=hoh.OHParser("Mo 09:00-21:00")>>>oh1==oh2True>>>oh1==oh3FalseTheOHParserobject contains two other attributes:PH_datesandSH_dates, which are empty lists default.
To indicate a date is a public or a school holiday, you can pass itsdatetime.dateinto these lists.
You can also use thepython-holidaysmodule to get dynamic dictionnary (which updates the year) to replace these lists.
In fact, any iterable object with a__contains__method (receivingdatetime.dateobjects) will work.
If you have GPS coordinates and want to have a country name, you can use thecountriesmodule.Solar hoursIf the field contains solar hours, here is how to deal with them.First of all, you can easily know if you need to set them by checking theOHParser.needs_solar_hours_settingvariable.
If one of its values isTrue, it appears in the field and you should give to HOH a mean to retrive its time.You have to ways to do this.
The first is to give to theOHParserthe location of the facility, to allow it to calculate solar hours.
The second is to use theSolarHoursobject (which inherits fromdict),viatheOHParser.solar_hoursattribute.# First method. You can use either an 'astral.location.Location' object or a tuple.location=astral.location.Location(["Greenwich","England",51.168,0.0,"Europe/London",24])location=(51.168,0.0,"Europe/London",24)oh=hoh.OHParser(field,location=location)# Second method.solar_hours={"sunrise":datetime.time(8,0),"sunset":datetime.time(20,0),"dawn":datetime.time(7,30),"dusk":datetime.time(20,30)}oh.solar_hours[datetime.date.today()]=solar_hoursAttention, except if the facility is on the equator, this setting will be valid only for a short period (except if you provide coordinates, because they will be automatically updated).If you try to do something with a field containing solar hours without providing a location, aosm_opening_hours_humanized.exceptions.SolarHoursErrorexception will be raised.In some very rare cases, it might be impossible to get solar hours.
For example, in Antactica, the sun may never reach the dawn / dusk location in the sky, so theastralmodule can't return the down time.
So, if you try to get, for example, the next change with a field containing solar hours and located in such location, aosm_opening_hours_humanized.exceptions.SolarHoursErrorexception will also be raised.Sometimes, especially if you work with numerous fields, you may want to apply the same methods to the same field but for different locations.
To do so, you can use a dedicated method calledthis_location(), which is intended to be used as a context manager.
It allows you to temporarily set a specific location to the OHParser instance.oh=hoh.OHParser("Mo-Fr sunrise-sunset",location=(51.168,0.0,"Europe/London",24))str(oh.solar_hours.location)=='Location/Region, tz=Europe/London, lat=51.17, lon=0.00'withoh.temporary_location("Paris"):str(oh.solar_hours.location)=='Paris/France, tz=Europe/Paris, lat=48.83, lon=2.33'str(oh.solar_hours.location)=='Location/Region, tz=Europe/London, lat=51.17, lon=0.00'Have nice schedulesYou can pass any valid locale name toOHParser, it will work for the majority of methods, cause they only need Babel's translations.
However, thedescription()andplaintext_week_description()methods need more translations, so it works only with a few locales, whose list is available withhoh.AVAILABLE_LOCALES.
Use another one will make methods return inconsistent sentences.Currently, the following locales are supported:en: english (default);fr_FR: french;de: deutsch;nl: dutch;pl: polish;pt: portuguese;it: italian;ru_RU: russian.Theget_localized_names()method returns a dict of lists with the names of months and weekdays in the current locale.Example:>>>oh.get_localized_names(){'months':['January','February','March','April','May','June','July','August','September','October','November','December'],'days':['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday']}time_before_next_change()returns a humanized delay before the next change in opening status.
Likenext_change(), it can take adatetime.datetimemoment to get next change from another time.>>>oh.time_before_next_change()"in 3 hours">>>oh.time_before_next_change(word=False)"3 hours"description()returns a list of strings (sentences) describing the whole field.# Field: "Mo-Fr 10:00-19:00; Sa 10:00-12:00; Dec 25 off">>>print(oh.description())['From Monday to Friday: 10:00 AM – 7:00 PM.','On Saturday: 10:00 AM – 12:00 PM.','December 25: closed.']>>>print('\n'.join(oh.description()))"""From Monday to Friday: 10:00 AM – 7:00 PM.On Saturday: 10:00 AM – 12:00 PM.December 25: closed."""plaintext_week_description()returns a plaintext description of the opening periods of a week.
This method takes ayearand aweeknumber(bothint).
You can also specify the first day of the week with thefirst_weekdayparameter (asint).
Its default value is0, meaning "Monday".It can also take no parameter, so the described week will be the current one.>>>print(oh.plaintext_week_description(year=2018,weeknumber=1,first_weekday=0))"""Monday: 8:00 AM – 7:00 PMTuesday: 8:00 AM – 7:00 PMWednesday: 8:00 AM – 7:00 PMThursday: 8:00 AM – 7:00 PMFriday: 8:00 AM – 7:00 PMSaturday: 8:00 AM – 12:00 PMSunday: closed"""This method uses thedays_of_week()function to get the datetimes of the days of the requested week.
It is accessible directly through the HOH namespace, and takes the same parameters.get_day()returns aDayobject, which contains opening periods and useful methods for a day.
It can take adatetime.dateargument to get the day you want.The returned object contains the following attributes.ohparser(OHParser) : the OHParser instance where the object come from;date(datetime.date) : the date of the day;weekday_name(str) : the name of the day (ex: "Monday");timespans: (list[ComputedTimeSpan]) : the computed timespans of the day (containingdatetime.datetimeobjects);locale(babel.Locale) : the locale given to OHParser.Attention, thedatetime.datetimeobjects in the computed timespans may be in another day, if it contains a period which spans over midnight (likeMo-Fr 20:00-02:00).Supported field formatsHere are the field formats officialy supported and tested (examples).24/7
Mo 10:00-20:00
Mo-Fr 10:00-20:00
Sa,Su 10:00-20:00
Su,PH off  # or "closed"
10:00-20:00
20:00-02:00
sunrise-sunset  # or "dawn" / "dusk"
(sunrise+01:00)-20:00
Jan 10:00-20:00
Jan-Feb 10:00-20:00
Jan,Dec 10:00-20:00
Jan Mo 10:00-20:00
Jan,Feb Mo 10:00-20:00
Jan-Feb Mo 10:00-20:00
Jan Mo-Fr 10:00-20:00
Jan,Feb Mo-Fr 10:00-20:00
Jan-Feb Mo-Fr 10:00-20:00
SH Mo 10:00-20:00
SH Mo-Fr 10:00-20:00
easter 10:00-20:00
SH,PH Mo-Fr 10:00-20:00
SH,PH Mo-Fr,Su 10:00-20:00
Jan-Feb,Aug Mo-Fr,Su 10:00-20:00
week 1 Mo 09:00-12:00
week 1-10 Su 09:00-12:00
week 1-10/2 Sa-Su 09:00-12:00
2018 Mo-Fr 10:00-20:00
2018-2022 Mo-Fr 10:00-20:00
2018-2022/2 Mo-Fr 10:00-20:00The following formats are NOT supported yet and their parsing will raise a ParseError.Su[1] 10:00-20:00
easter +1 day 10:00-20:00
easter +2 days 10:00-20:00
Mo-Fr 10:00+
Mo-Fr 10:00,12:00,20:00  # Does not support points in time.For fields like24/7; Su 10:00-13:00 off, Sundays are considered as entirely closed.
This should be fixed in a later version.AlternativesIf you want to parseopening_hoursfields but HOH doesn't fit your needs, here are a few other libraries which might interest you.opening_hours.js: The main library to parse these fields, but written in JS.pyopening_hours: A Python implementation of the previous library.simple-opening-hours: Another small JS library which can parse simple fields.PerformancesHOH uses the moduleLark(with the Earley parser) to parse the fields.It is very optimized (about 20 times faster) for the simplest fields (likeMo-Fr 10:00-20:00), so their parsing will be very fast:0.0002 seconds for a single field;0.023 seconds for a hundred;0.23 seconds for a thousand.For more complex fields (likeJan-Feb Mo-Fr 08:00-19:00), the parsing is slower:0.006 seconds for a single field;0.57 seconds for a hundred;5.7 seconds for a thousand.LicenceThis module is published under the AGPLv3 license, the terms of which can be found in theLICENCEfile.