• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

hasgeek / coaster / 9244418196

26 May 2024 03:39PM UTC coverage: 84.467% (-4.8%) from 89.263%
9244418196

push

github

web-flow
Add async support for Quart+Flask (#470)

This commit bumps the version number from 0.7 to 0.8 as it has extensive changes:

* Ruff replaces black, isort and flake8 for linting and formatting
* All decorators now support async functions and provide async wrapper implementations
* Some obsolete modules have been removed
* Pagination from Flask-SQLAlchemy is now included, removing that dependency (but still used in tests)
* New `compat` module provides wrappers to both Quart and Flake and is used by all other modules
* Some tests run using Quart. The vast majority of tests are not upgraded, nor are there tests for async decorators, so overall line coverage has dropped significantly. Comprehensive test coverage is still pending; for now we are using Funnel's tests as the extended test suite

648 of 1023 new or added lines in 29 files covered. (63.34%)

138 existing lines in 17 files now uncovered.

3948 of 4674 relevant lines covered (84.47%)

3.38 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.92
/src/coaster/utils/datetime.py
1
"""Date, time and timezone utilities."""
4✔
2

3
# spell-checker:ignore isoweek
4
from __future__ import annotations
4✔
5

6
from datetime import date, datetime, timedelta, tzinfo
4✔
7
from typing import Optional, Union
4✔
8

9
import isoweek
4✔
10
import pytz
4✔
11
from aniso8601 import parse_datetime, parse_duration
4✔
12
from aniso8601.exceptions import ISOFormatError as ParseError
4✔
13
from pytz import BaseTzInfo, utc
4✔
14

15
__all__ = [
4✔
16
    'utcnow',
17
    'parse_isoformat',
18
    'parse_duration',
19
    'isoweek_datetime',
20
    'midnight_to_utc',
21
    'sorted_timezones',
22
    'ParseError',
23
]
24

25

26
def utcnow() -> datetime:
4✔
27
    """Return the current time at UTC with `tzinfo` set."""
28
    return datetime.now(utc)
4✔
29

30

31
def parse_isoformat(text: str, naive: bool = True, delimiter: str = 'T') -> datetime:
4✔
32
    """
33
    Parse an ISO 8601 timestamp as generated by `datetime.isoformat()`.
34

35
    Timestamps without a timezone are assumed to be at UTC. Raises :exc:`ParseError` if
36
    the timestamp cannot be parsed.
37

38
    :param bool naive: If `True`, strips timezone and returns datetime at UTC.
39
    """
40
    try:
4✔
41
        dt = parse_datetime(text, delimiter)
4✔
42
    except NotImplementedError:
4✔
43
        # `aniso8601` misinterprets junk data and returns NotImplementedError with
44
        # "ISO 8601 extended year representation not supported"
45
        raise ParseError(f"Cannot parse datetime {text}") from None
4✔
46
    if dt.tzinfo is not None and naive:
4✔
47
        dt = dt.astimezone(utc).replace(tzinfo=None)
4✔
48
    return dt
4✔
49

50

51
def isoweek_datetime(
4✔
52
    year: int,
53
    week: int,
54
    timezone: Union[tzinfo, BaseTzInfo, str] = 'UTC',
55
    naive: bool = False,
56
) -> datetime:
57
    """
58
    Return a datetime matching the starting point of a specified ISO week.
59

60
    The return value is in the specified timezone, or in the specified timezone (default
61
    `UTC`). Returns a naive datetime in UTC if requested (default `False`).
62

63
    >>> isoweek_datetime(2017, 1)
64
    datetime.datetime(2017, 1, 2, 0, 0, tzinfo=<UTC>)
65
    >>> isoweek_datetime(2017, 1, 'Asia/Kolkata')
66
    datetime.datetime(2017, 1, 1, 18, 30, tzinfo=<UTC>)
67
    >>> isoweek_datetime(2017, 1, 'Asia/Kolkata', naive=True)
68
    datetime.datetime(2017, 1, 1, 18, 30)
69
    >>> isoweek_datetime(2008, 1, 'Asia/Kolkata')
70
    datetime.datetime(2007, 12, 30, 18, 30, tzinfo=<UTC>)
71
    """
72
    naivedt = datetime.combine(isoweek.Week(year, week).day(0), datetime.min.time())
4✔
73
    if isinstance(timezone, str):
4✔
74
        tz: tzinfo = pytz.timezone(timezone)
4✔
75
    else:
UNCOV
76
        tz = timezone
×
77
    if isinstance(tz, BaseTzInfo):
4✔
78
        dt = tz.localize(naivedt).astimezone(utc)
4✔
79
    else:
UNCOV
80
        dt = naivedt.replace(tzinfo=tz).astimezone(utc)
×
81
    if naive:
4✔
82
        return dt.replace(tzinfo=None)
4✔
83
    return dt
4✔
84

85

86
def midnight_to_utc(
4✔
87
    dt: Union[date, datetime],
88
    timezone: Optional[Union[tzinfo, BaseTzInfo, str]] = None,
89
    naive: bool = False,
90
) -> datetime:
91
    """
92
    Return a UTC datetime matching the midnight for the given date or datetime.
93

94
    >>> from datetime import date
95
    >>> midnight_to_utc(datetime(2017, 1, 1))
96
    datetime.datetime(2017, 1, 1, 0, 0, tzinfo=<UTC>)
97
    >>> midnight_to_utc(pytz.timezone('Asia/Kolkata').localize(datetime(2017, 1, 1)))
98
    datetime.datetime(2016, 12, 31, 18, 30, tzinfo=<UTC>)
99
    >>> midnight_to_utc(datetime(2017, 1, 1), naive=True)
100
    datetime.datetime(2017, 1, 1, 0, 0)
101
    >>> midnight_to_utc(
102
    ...     pytz.timezone('Asia/Kolkata').localize(datetime(2017, 1, 1)), naive=True
103
    ... )
104
    datetime.datetime(2016, 12, 31, 18, 30)
105
    >>> midnight_to_utc(date(2017, 1, 1))
106
    datetime.datetime(2017, 1, 1, 0, 0, tzinfo=<UTC>)
107
    >>> midnight_to_utc(date(2017, 1, 1), naive=True)
108
    datetime.datetime(2017, 1, 1, 0, 0)
109
    >>> midnight_to_utc(date(2017, 1, 1), timezone='Asia/Kolkata')
110
    datetime.datetime(2016, 12, 31, 18, 30, tzinfo=<UTC>)
111
    >>> midnight_to_utc(datetime(2017, 1, 1), timezone='Asia/Kolkata')
112
    datetime.datetime(2016, 12, 31, 18, 30, tzinfo=<UTC>)
113
    >>> midnight_to_utc(
114
    ...     pytz.timezone('Asia/Kolkata').localize(datetime(2017, 1, 1)), timezone='UTC'
115
    ... )
116
    datetime.datetime(2017, 1, 1, 0, 0, tzinfo=<UTC>)
117
    """
118
    tz: Union[tzinfo, BaseTzInfo]
119
    if timezone:
4✔
120
        tz = pytz.timezone(timezone) if isinstance(timezone, str) else timezone
4✔
121
    elif isinstance(dt, datetime) and dt.tzinfo:
4✔
122
        tz = dt.tzinfo
4✔
123
    else:
124
        tz = utc
4✔
125

126
    if isinstance(tz, BaseTzInfo):
4✔
127
        utc_dt = tz.localize(datetime.combine(dt, datetime.min.time())).astimezone(utc)
4✔
128
    else:
UNCOV
129
        utc_dt = datetime.combine(dt, datetime.min.time()).astimezone(utc)
×
130
    if naive:
4✔
131
        return utc_dt.replace(tzinfo=None)
4✔
132
    return utc_dt
4✔
133

134

135
def sorted_timezones() -> list[tuple[str, str]]:
4✔
136
    """Return a list of timezones sorted by offset from UTC."""
137

138
    def hourmin(delta: timedelta) -> tuple[int, int]:
4✔
139
        if delta.days < 0:
4✔
140
            hours, remaining = divmod(86400 - delta.seconds, 3600)
4✔
141
        else:
142
            hours, remaining = divmod(delta.seconds, 3600)
4✔
143
        minutes, remaining = divmod(remaining, 60)
4✔
144
        return hours, minutes
4✔
145

146
    now = datetime.utcnow()
4✔
147
    # Make a list of country code mappings
148
    timezone_country = {}
4✔
149
    for countrycode in pytz.country_timezones:
4✔
150
        for timezone in pytz.country_timezones[countrycode]:
4✔
151
            timezone_country[timezone] = countrycode
4✔
152

153
    # Make a list of timezones, discarding the US/* and Canada/* zones since they aren't
154
    # reliable for DST, and discarding UTC and GMT since timezones in that zone have
155
    # their own names
156
    timezones = [
4✔
157
        (
158
            pytz.timezone(tzname).utcoffset(  # type: ignore[call-arg]
159
                now, is_dst=False
160
            ),
161
            tzname,
162
        )
163
        for tzname in pytz.common_timezones
164
        if not tzname.startswith(('US/', 'Canada/')) and tzname not in ('GMT', 'UTC')
165
    ]
166
    # Sort timezones by offset from UTC and their human-readable name
167
    presorted = [
4✔
168
        (
169
            delta,
170
            # pylint: disable=consider-using-f-string
171
            '{sign}{offset} – {country}{zone} ({tzname})'.format(
172
                sign=(
173
                    (delta.days < 0 and '-')
174
                    or (delta.days == 0 and delta.seconds == 0 and ' ')
175
                    or '+'
176
                ),
177
                offset='{:02d}:{:02d}'.format(*hourmin(delta)),
178
                country=(
179
                    (f'{pytz.country_names[timezone_country[name]]}: ')
180
                    if name in timezone_country
181
                    else ''
182
                ),
183
                zone=name.replace('_', ' '),
184
                tzname=pytz.timezone(name).tzname(  # type: ignore[call-arg]
185
                    now, is_dst=False
186
                ),
187
            ),
188
            name,
189
        )
190
        for delta, name in timezones
191
    ]
192
    presorted.sort()
4✔
193
    # Return a list of (timezone, label) with the timezone offset included in the label.
194
    return [(name, label) for (delta, label, name) in presorted]
4✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc