| 48 | |
| 49 | |
| 50 | def parse_rfc3339(s): |
| 51 | if isinstance(s, datetime.datetime): |
| 52 | if not s.tzinfo: |
| 53 | return s.replace(tzinfo=UTC) |
| 54 | return s |
| 55 | |
| 56 | m = _re_rfc3339.fullmatch(s.strip()) |
| 57 | if m is None: |
| 58 | raise ValueError( |
| 59 | f"Invalid RFC3339 datetime: {s!r} " |
| 60 | "(expected YYYY-MM-DDTHH:MM:SS[.frac][Z|±HH:MM])" |
| 61 | ) |
| 62 | |
| 63 | groups = m.groups() |
| 64 | dt = [0] * 7 |
| 65 | for x in range(6): |
| 66 | dt[x] = int(groups[x]) |
| 67 | |
| 68 | us = 0 |
| 69 | if groups[6] is not None: |
| 70 | partial_sec = float(groups[6].replace(",", ".")) |
| 71 | us = int(MICROSEC_PER_SEC * partial_sec) |
| 72 | |
| 73 | tz = UTC |
| 74 | if groups[7] is not None and groups[7] not in ('Z', 'z', ' '): |
| 75 | tz_match = _re_timezone.search(groups[7]) |
| 76 | if tz_match is None: |
| 77 | raise ValueError( |
| 78 | f"Invalid timezone format in RFC3339 string {s!r}: " |
| 79 | f"timezone part {groups[7]!r} does not match expected " |
| 80 | f"format (±HH:MM)" |
| 81 | ) |
| 82 | tz_groups = tz_match.groups() |
| 83 | hour = int(tz_groups[1]) |
| 84 | minute = 0 |
| 85 | if tz_groups[0] == "-": |
| 86 | hour *= -1 |
| 87 | if tz_groups[2]: |
| 88 | minute = int(tz_groups[2]) |
| 89 | tz = TimezoneInfo(hour, minute) |
| 90 | |
| 91 | try: |
| 92 | return datetime.datetime( |
| 93 | year=dt[0], month=dt[1], day=dt[2], |
| 94 | hour=dt[3], minute=dt[4], second=dt[5], |
| 95 | microsecond=us, tzinfo=tz) |
| 96 | except ValueError as e: |
| 97 | raise ValueError( |
| 98 | f"Invalid date/time values in RFC3339 string {s!r}: {e}" |
| 99 | ) from e |
| 100 | |
| 101 | |
| 102 | |