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.commands import *
from supybot.i18n import PluginInternationalization
from . import nominatim
from . import wikidata
_ = PluginInternationalization("Geography")
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
@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

View File

@ -29,7 +29,19 @@
###
import datetime
import contextlib
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 import utils
@ -41,6 +53,43 @@ from . import nominatim
class GeographyTestCase(PluginTestCase):
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):
@skipIf(not network, "Network test")
@ -56,7 +105,7 @@ class GeographyWikidataTestCase(SupyTestCase):
@skipIf(not network, "Network test")
def testDirect(self):
"""The queried object directly has a timezone property"""
# The queried object directly has a timezone property
self.assertEqual(
# New York
wikidata.timezone_from_uri("http://www.wikidata.org/entity/Q1384"),
@ -65,8 +114,8 @@ class GeographyWikidataTestCase(SupyTestCase):
@skipIf(not network, "Network test")
def testParent(self):
"""The queried object does not have a TZ property
but it is part of an object that does"""
# The queried object does not have a TZ property but it is part
# of an object that does
self.assertEqual(
# Metz, France
wikidata.timezone_from_uri(
@ -77,8 +126,8 @@ class GeographyWikidataTestCase(SupyTestCase):
@skipIf(not network, "Network test")
def testParentAndIgnoreSelf(self):
"""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."""
# 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."""
self.assertEqual(
# New York City, NY
wikidata.timezone_from_uri("http://www.wikidata.org/entity/Q60"),
@ -87,11 +136,12 @@ class GeographyWikidataTestCase(SupyTestCase):
@skipIf(not network, "Network test")
def testParentQualifiedIgnorePreferred(self):
"""The queried object does not have a TZ property,
and is part of an object that does.
However, this parent's 'preferred' timezone is not the
right one, so we must make sure to select the right one
based on P518 ('applies to part')."""
# The queried object does not have a TZ property,
# and is part of an object that does.
# However, this parent's 'preferred' timezone is not the
# right one, so we must make sure to select the right one
# based on P518 ('applies to part').
# La Réunion is a French region, but in UTC+4.
# France has a bunch of timezone statements, and 'Europe/Paris'
# is marked as Preferred because it is the time of metropolitan
@ -113,5 +163,8 @@ class GeographyNominatimTestCase(SupyTestCase):
results = nominatim.search_osmids("Metz, France")
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: