Geography: Add 'timezone' command

This commit is contained in:
Valentin Lorentz 2021-11-09 22:32:29 +01:00
parent 696d82ccfe
commit 36ade18319
2 changed files with 137 additions and 11 deletions

View File

@ -28,18 +28,91 @@
### ###
import datetime
from supybot import utils, plugins, ircutils, callbacks from supybot import utils, plugins, ircutils, callbacks
from supybot.commands import * from supybot.commands import *
from supybot.i18n import PluginInternationalization from supybot.i18n import PluginInternationalization
from . import nominatim
from . import wikidata
_ = PluginInternationalization("Geography") _ = PluginInternationalization("Geography")
class Geography(callbacks.Plugin): class Geography(callbacks.Plugin):
"""Provides geography facts, such as timezones.""" """Provides geography facts, such as timezones.
This plugin uses data from `Wikidata <https://wikidata.org/>`_
and `OSM/Nominatim <https://nominatim.openstreetmap.org/>`.
"""
threaded = True threaded = True
@wrap(["text"])
def timezone(self, irc, msg, args, query):
"""<location name to search>
Returns the timezone used in the given location. For example,
the name could be "Paris" or "Paris, France".
This uses data from Wikidata and Nominatim."""
osmids = nominatim.search_osmids(query)
if not osmids:
irc.error(_("Could not find the location"), Raise=True)
now = datetime.datetime.now(tz=datetime.timezone.utc)
for osmid in osmids:
uri = wikidata.uri_from_osmid(osmid)
if not uri:
continue
# Get the timezone object (and handle various errors)
try:
timezone = wikidata.timezone_from_uri(uri)
except utils.time.UnknownTimeZone as e:
irc.error(
format(_("Could not understand timezone: %s"), e.args[0]),
Raise=True,
)
except utils.time.MissingTimezoneLibrary:
irc.error(
_(
"Timezone-related commands are not available. "
"Your administrator need to either upgrade Python to "
"version 3.9 or greater, or install pytz."
),
Raise=True,
)
except utils.time.TimezoneException as e:
irc.error(e.args[0], Raise=True)
# Extract a human-friendly name, depending on the type of
# the timezone object:
if hasattr(timezone, "key"):
# instance of zoneinfo.ZoneInfo
irc.reply(timezone.key)
return
elif hasattr(timezone, "zone"):
# instance of pytz.timezone
irc.reply(timezone.zone)
return
else:
# probably datetime.timezone built from a constant offset
try:
offset = timezone.utcoffset(now).seconds
except NotImplementedError:
continue
hours = int(offset / 3600)
minutes = int(offset / 60 % 60)
irc.reply("UTC+%0.2i:%0.2i" % (hours, minutes))
return
irc.error(
_("Could not find the timezone of this location."), Raise=True
)
Class = Geography Class = Geography

View File

@ -29,7 +29,19 @@
### ###
import datetime import datetime
import contextlib
from unittest import skipIf from unittest import skipIf
from unittest.mock import patch
try:
import pytz
except ImportError:
pytz = None
try:
import zoneinfo
except ImportError:
zoneinfo = None
from supybot.test import * from supybot.test import *
from supybot import utils from supybot import utils
@ -41,6 +53,43 @@ from . import nominatim
class GeographyTestCase(PluginTestCase): class GeographyTestCase(PluginTestCase):
plugins = ("Geography",) plugins = ("Geography",)
@skipIf(not pytz, "pytz is not available")
@patch.object(nominatim, "search_osmids", return_value=[42])
@patch.object(wikidata, "uri_from_osmid", return_value="foo")
def testTimezonePytz(self, _, __):
tz = pytz.timezone("Europe/Paris")
with patch.object(wikidata, "timezone_from_uri", return_value=tz):
self.assertResponse("timezone Foo Bar", "Europe/Paris")
@skipIf(not zoneinfo, "Python is older than 3.9")
@patch.object(nominatim, "search_osmids", return_value=[42])
@patch.object(wikidata, "uri_from_osmid", return_value="foo")
def testTimezoneZoneinfo(self, _, __):
tz = zoneinfo.ZoneInfo("Europe/Paris")
with patch.object(wikidata, "timezone_from_uri", return_value=tz):
self.assertResponse("timezone Foo Bar", "Europe/Paris")
@skipIf(not zoneinfo, "Python is older than 3.9")
@patch.object(nominatim, "search_osmids", return_value=[42])
@patch.object(wikidata, "uri_from_osmid", return_value="foo")
def testTimezoneAbsolute(self, _, __):
tz = datetime.timezone(datetime.timedelta(hours=4))
with patch.object(wikidata, "timezone_from_uri", return_value=tz):
self.assertResponse("timezone Foo Bar", "UTC+04:00")
tz = datetime.timezone(datetime.timedelta(hours=4, minutes=30))
with patch.object(wikidata, "timezone_from_uri", return_value=tz):
self.assertResponse("timezone Foo Bar", "UTC+04:30")
@skipIf(not network, "Network test")
def testTimezoneIntegration(self):
self.assertResponse("timezone Metz, France", "Europe/Paris")
self.assertResponse("timezone Saint-Denis, La Réunion", "UTC+04:00")
class GeographyWikidataTestCase(SupyTestCase): class GeographyWikidataTestCase(SupyTestCase):
@skipIf(not network, "Network test") @skipIf(not network, "Network test")
@ -56,7 +105,7 @@ class GeographyWikidataTestCase(SupyTestCase):
@skipIf(not network, "Network test") @skipIf(not network, "Network test")
def testDirect(self): def testDirect(self):
"""The queried object directly has a timezone property""" # The queried object directly has a timezone property
self.assertEqual( self.assertEqual(
# New York # New York
wikidata.timezone_from_uri("http://www.wikidata.org/entity/Q1384"), wikidata.timezone_from_uri("http://www.wikidata.org/entity/Q1384"),
@ -65,8 +114,8 @@ class GeographyWikidataTestCase(SupyTestCase):
@skipIf(not network, "Network test") @skipIf(not network, "Network test")
def testParent(self): def testParent(self):
"""The queried object does not have a TZ property # The queried object does not have a TZ property but it is part
but it is part of an object that does""" # of an object that does
self.assertEqual( self.assertEqual(
# Metz, France # Metz, France
wikidata.timezone_from_uri( wikidata.timezone_from_uri(
@ -77,8 +126,8 @@ class GeographyWikidataTestCase(SupyTestCase):
@skipIf(not network, "Network test") @skipIf(not network, "Network test")
def testParentAndIgnoreSelf(self): def testParentAndIgnoreSelf(self):
"""The queried object has a TZ property, but it's useless to us; # The queried object has a TZ property, but it's useless to us;
however it is part of an object that has a useful one.""" # however it is part of an object that has a useful one."""
self.assertEqual( self.assertEqual(
# New York City, NY # New York City, NY
wikidata.timezone_from_uri("http://www.wikidata.org/entity/Q60"), wikidata.timezone_from_uri("http://www.wikidata.org/entity/Q60"),
@ -87,11 +136,12 @@ class GeographyWikidataTestCase(SupyTestCase):
@skipIf(not network, "Network test") @skipIf(not network, "Network test")
def testParentQualifiedIgnorePreferred(self): def testParentQualifiedIgnorePreferred(self):
"""The queried object does not have a TZ property, # The queried object does not have a TZ property,
and is part of an object that does. # and is part of an object that does.
However, this parent's 'preferred' timezone is not the # However, this parent's 'preferred' timezone is not the
right one, so we must make sure to select the right one # right one, so we must make sure to select the right one
based on P518 ('applies to part').""" # based on P518 ('applies to part').
# La Réunion is a French region, but in UTC+4. # La Réunion is a French region, but in UTC+4.
# France has a bunch of timezone statements, and 'Europe/Paris' # France has a bunch of timezone statements, and 'Europe/Paris'
# is marked as Preferred because it is the time of metropolitan # is marked as Preferred because it is the time of metropolitan
@ -113,5 +163,8 @@ class GeographyNominatimTestCase(SupyTestCase):
results = nominatim.search_osmids("Metz, France") results = nominatim.search_osmids("Metz, France")
self.assertEqual(results[0], 450381, results) self.assertEqual(results[0], 450381, results)
results = nominatim.search_osmids("Saint-Denis, La Réunion")
self.assertEqual(results[0], 192468, results)
# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: