diff --git a/plugins/Geography/plugin.py b/plugins/Geography/plugin.py
index 59cc2d3d3..fca150e40 100644
--- a/plugins/Geography/plugin.py
+++ b/plugins/Geography/plugin.py
@@ -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 `_
+ and `OSM/Nominatim `.
+ """
threaded = True
+ @wrap(["text"])
+ def timezone(self, irc, msg, args, query):
+ """
+
+ 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
diff --git a/plugins/Geography/test.py b/plugins/Geography/test.py
index 330fa6626..035e424f2 100644
--- a/plugins/Geography/test.py
+++ b/plugins/Geography/test.py
@@ -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: