Repository navigation
Raise DataError for time zone values without one instant #1224
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,7 @@ | |
| from typing import Any, ClassVar | ||
| from zoneinfo import ZoneInfo, ZoneInfoNotFoundError | ||
|
|
||
| from pyathena.error import DataError | ||
| from pyathena.parser import ( | ||
| TypedValueConverter, | ||
| TypeNode, | ||
|
|
@@ -94,44 +95,70 @@ def _parse_utc_offset(value: str) -> timezone | None: | |
|
|
||
|
|
||
| @functools.cache | ||
| def _parse_time_zone(value: str) -> tzinfo | None: | ||
| def _parse_time_zone(value: str) -> tzinfo: | ||
| """Parse the time zone of an Athena TIMESTAMP WITH TIME ZONE value. | ||
|
|
||
| Results are cached per text, so an unknown zone name logs a warning only on its | ||
| first lookup. | ||
|
|
||
| Args: | ||
| value: A ``+HH:MM`` or ``-HH:MM`` UTC offset or an IANA time zone name. | ||
|
|
||
| Returns: | ||
| The fixed-offset time zone or ``ZoneInfo``, or None if the IANA time zone | ||
| database available to Python cannot provide a zone with the name. | ||
| The fixed-offset time zone or ``ZoneInfo``. | ||
|
|
||
| Raises: | ||
| DataError: If the IANA time zone database available to Python cannot | ||
| provide a zone with the name. | ||
| """ | ||
| offset = _parse_utc_offset(value) | ||
| if offset is not None: | ||
| return offset | ||
| try: | ||
| return ZoneInfo(value) | ||
| except (ZoneInfoNotFoundError, ValueError, OSError): | ||
| _logger.warning(f"Unknown time zone {value!r}; returning naive datetimes for it.") | ||
| return None | ||
| except (ZoneInfoNotFoundError, ValueError, OSError) as e: | ||
| raise DataError( | ||
| f"Unknown time zone {value!r}. " | ||
| "Update the tzdata package or the system time zone database." | ||
| ) from e | ||
|
|
||
|
|
||
| def _to_datetime_with_tz(varchar_value: str | None) -> datetime | None: | ||
| """Convert an Athena TIMESTAMP WITH TIME ZONE value to an aware datetime. | ||
|
|
||
| Athena writes a value with a zone name as the local time in that zone. A local | ||
| time that a change of the zone's UTC offset, such as DST, repeats or skips does | ||
| not identify one instant, so it is not converted. | ||
|
|
||
| Args: | ||
| varchar_value: The value as text with a trailing zone name or ``+HH:MM`` / | ||
| ``-HH:MM`` UTC offset, or None. An empty string, which pandas and the | ||
| Arrow CSV reader return for NULL, is None. | ||
|
|
||
| Returns: | ||
| The aware datetime, a naive datetime if the zone name is unknown, or None. | ||
| The aware datetime, or None. | ||
|
|
||
| Raises: | ||
| DataError: If the zone name is unknown, or the local time is ambiguous or | ||
| does not exist in the zone. | ||
| """ | ||
| if not varchar_value: | ||
| return None | ||
| datetime_, _, tz = varchar_value.rpartition(" ") | ||
| return _parse_datetime(datetime_).replace(tzinfo=_parse_time_zone(tz)) | ||
| zone = _parse_time_zone(tz) | ||
| value = _parse_datetime(datetime_).replace(tzinfo=zone) | ||
| if isinstance(zone, timezone): | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Independent review (relayed result from Codex CLI,
Reviewer's coverage:
Result: CLEAN. No actionable findings against the stated behavior, including the accepted PandasCursor wrapping and the absence of the column name. Limits: static review only. No builds, tests, or AWS execution. |
||
| return value | ||
| # PEP 495: fold=0 and fold=1 give different offsets only at a transition. At a | ||
| # repeated local time, fold=0 is the earlier instant; at a skipped one, the later. | ||
| folded = value.replace(fold=1) | ||
| if value.utcoffset() != folded.utcoffset(): | ||
| if value.timestamp() < folded.timestamp(): | ||
| reason = "is ambiguous: the local time occurs twice" | ||
| else: | ||
| reason = "does not exist: the local time is skipped" | ||
| raise DataError( | ||
| f"{varchar_value!r} {reason} in {tz}. Convert the value to UTC in SQL, " | ||
| "for example with AT TIME ZONE 'UTC'." | ||
| ) | ||
| return value | ||
|
|
||
|
|
||
| def _parse_time(value: str) -> time: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Self-review round two (claims, callers, and operations)
Base
b2d7aa24b00a204e9e150952c708e3bb1126f9e2, head0ebd096bdd(full pass over the PR body, commit messages, docstrings, and the issue premises they repeat).Claims checked:
fold=0result were reproduced; the new messages are taken from the code.execute()(the tests assert it, including managed results).DataError(checked for each).tzdata2024.2 is IANA 2024b (tzdata.IANA_VERSION);uv lock --checkpasses with uv 0.12.5 and 0.13.1.origin/master.docs/s3fs.md(awaredatetime) anddocs/pandas.mdstay correct.Callers and operations:
DataError.tzdata<2024.2will fail to resolve; this is in the release note.DataErroris possible when Python's tz rules have a transition that Athena's do not; that skew limitation is documented under Document the temporal type contract and gate the release on it #1221.Result: FINDINGS, PR body corrected:
UTC; Hive has no time zone type).No code changes in this round.