diff --git a/gallery_dl/extractor/8chan.py b/gallery_dl/extractor/8chan.py index 51163326..f174ad99 100644 --- a/gallery_dl/extractor/8chan.py +++ b/gallery_dl/extractor/8chan.py @@ -9,9 +9,9 @@ """Extractors for https://8chan.moe/""" from .common import Extractor, Message -from .. import text +from .. import text, util from ..cache import memcache -from datetime import datetime, timedelta +from datetime import timedelta import itertools BASE_PATTERN = r"(?:https?://)?8chan\.(moe|se|cc)" @@ -34,14 +34,14 @@ class _8chanExtractor(Extractor): def cookies_prepare(self): # fetch captcha cookies # (necessary to download without getting interrupted) - now = datetime.utcnow() + now = util.datetime_utcnow() url = self.root + "/captcha.js" params = {"d": now.strftime("%a %b %d %Y %H:%M:%S GMT+0000 (UTC)")} self.request(url, params=params).content # adjust cookies # - remove 'expires' timestamp - # - move 'captchaexpiration' value forward by 1 month) + # - move 'captchaexpiration' value forward by 1 month domain = self.root.rpartition("/")[2] for cookie in self.cookies: if cookie.domain.endswith(domain): diff --git a/gallery_dl/extractor/pixiv.py b/gallery_dl/extractor/pixiv.py index 3479b88f..c908e448 100644 --- a/gallery_dl/extractor/pixiv.py +++ b/gallery_dl/extractor/pixiv.py @@ -448,7 +448,8 @@ class PixivRankingExtractor(PixivExtractor): self.log.warning("invalid date '%s'", date) date = None if not date: - date = (datetime.utcnow() - timedelta(days=1)).strftime("%Y-%m-%d") + now = util.datetime_utcnow() + date = (now - timedelta(days=1)).strftime("%Y-%m-%d") self.date = date return {"ranking": { @@ -887,7 +888,7 @@ class PixivAppAPI(): "get_secure_url": "1", } - time = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S+00:00") + time = util.datetime_utcnow().strftime("%Y-%m-%dT%H:%M:%S+00:00") headers = { "X-Client-Time": time, "X-Client-Hash": hashlib.md5( diff --git a/gallery_dl/text.py b/gallery_dl/text.py index 92581875..8517cdf5 100644 --- a/gallery_dl/text.py +++ b/gallery_dl/text.py @@ -9,7 +9,9 @@ """Collection of functions that work on strings/text""" import re +import sys import html +import time import datetime import urllib.parse @@ -247,12 +249,23 @@ def parse_query(qs): return result -def parse_timestamp(ts, default=None): - """Create a datetime object from a unix timestamp""" - try: - return datetime.datetime.utcfromtimestamp(int(ts)) - except Exception: - return default +if sys.hexversion < 0x30c0000: + # Python <= 3.11 + def parse_timestamp(ts, default=None): + """Create a datetime object from a Unix timestamp""" + try: + return datetime.datetime.utcfromtimestamp(int(ts)) + except Exception: + return default +else: + # Python >= 3.12 + def parse_timestamp(ts, default=None): + """Create a datetime object from a Unix timestamp""" + try: + Y, m, d, H, M, S, _, _, _ = time.gmtime(int(ts)) + return datetime.datetime(Y, m, d, H, M, S) + except Exception: + return default def parse_datetime(date_string, format="%Y-%m-%dT%H:%M:%S%z", utcoffset=0): diff --git a/gallery_dl/util.py b/gallery_dl/util.py index 967479da..128f48b7 100644 --- a/gallery_dl/util.py +++ b/gallery_dl/util.py @@ -218,18 +218,34 @@ def to_string(value): def datetime_to_timestamp(dt): - """Convert naive UTC datetime to timestamp""" + """Convert naive UTC datetime to Unix timestamp""" return (dt - EPOCH) / SECOND def datetime_to_timestamp_string(dt): - """Convert naive UTC datetime to timestamp string""" + """Convert naive UTC datetime to Unix timestamp string""" try: return str((dt - EPOCH) // SECOND) except Exception: return "" +if sys.hexversion < 0x30c0000: + # Python <= 3.11 + datetime_utcfromtimestamp = datetime.datetime.utcfromtimestamp + datetime_utcnow = datetime.datetime.utcnow + datetime_from_timestamp = datetime_utcfromtimestamp +else: + # Python >= 3.12 + def datetime_from_timestamp(ts=None): + """Convert Unix timestamp to naive UTC datetime""" + Y, m, d, H, M, S, _, _, _ = time.gmtime(ts) + return datetime.datetime(Y, m, d, H, M, S) + + datetime_utcfromtimestamp = datetime_from_timestamp + datetime_utcnow = datetime_from_timestamp + + def json_default(obj): if isinstance(obj, CustomNone): return None diff --git a/test/test_extractor.py b/test/test_extractor.py index abf122b7..a45f5c9e 100644 --- a/test/test_extractor.py +++ b/test/test_extractor.py @@ -243,9 +243,12 @@ class TestExtractorWait(unittest.TestCase): def test_wait_until_datetime(self): extr = extractor.find("generic:https://example.org/") - until = datetime.utcnow() + timedelta(seconds=5) + until = util.datetime_utcnow() + timedelta(seconds=5) until_local = datetime.now() + timedelta(seconds=5) + if not until.microsecond: + until = until.replace(microsecond=until_local.microsecond) + with patch("time.sleep") as sleep, patch.object(extr, "log") as log: extr.wait(until=until) diff --git a/test/test_text.py b/test/test_text.py index c99729c0..084436b1 100644 --- a/test/test_text.py +++ b/test/test_text.py @@ -14,7 +14,7 @@ import unittest import datetime sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from gallery_dl import text # noqa E402 +from gallery_dl import text, util # noqa E402 INVALID = ((), [], {}, None, 1, 2.3) @@ -414,8 +414,8 @@ class TestText(unittest.TestCase): self.assertEqual(f(value), {}) def test_parse_timestamp(self, f=text.parse_timestamp): - null = datetime.datetime.utcfromtimestamp(0) - value = datetime.datetime.utcfromtimestamp(1555816235) + null = util.datetime_utcfromtimestamp(0) + value = util.datetime_utcfromtimestamp(1555816235) self.assertEqual(f(0) , null) self.assertEqual(f("0") , null) @@ -427,7 +427,7 @@ class TestText(unittest.TestCase): self.assertEqual(f(value, "foo"), "foo") def test_parse_datetime(self, f=text.parse_datetime): - null = datetime.datetime.utcfromtimestamp(0) + null = util.datetime_utcfromtimestamp(0) self.assertEqual(f("1970-01-01T00:00:00+00:00"), null) self.assertEqual(f("1970-01-01T00:00:00+0000") , null) diff --git a/test/test_util.py b/test/test_util.py index fd2ff8ba..e2db29bf 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -738,6 +738,22 @@ def hash(value): self.assertEqual(f(datetime.datetime(2010, 1, 1)), "1262304000") self.assertEqual(f(None), "") + def test_datetime_from_timestamp( + self, f=util.datetime_from_timestamp): + self.assertEqual(f(0.0), util.EPOCH) + self.assertEqual(f(1262304000.0), datetime.datetime(2010, 1, 1)) + self.assertEqual(f(1262304000.128000).replace(microsecond=0), + datetime.datetime(2010, 1, 1, 0, 0, 0)) + + def test_datetime_utcfromtimestamp( + self, f=util.datetime_utcfromtimestamp): + self.assertEqual(f(0.0), util.EPOCH) + self.assertEqual(f(1262304000.0), datetime.datetime(2010, 1, 1)) + + def test_datetime_utcnow( + self, f=util.datetime_utcnow): + self.assertIsInstance(f(), datetime.datetime) + def test_universal_none(self): obj = util.NONE