Skip to content

Commit e425260

Browse files
feat: add Python 3.10 support
- Lower requires-python from >=3.11 to >=3.10 (aligns with pydantic-ai) - Add 3.10 to CI test matrix (3.10, 3.11, 3.12, 3.13) - Replace datetime.UTC (3.11+) with datetime.timezone.utc (all versions) across 28 files in src/ and tests/ - Use asyncio.TimeoutError with noqa for ruff UP041 compat - Update ruff target-version to py310 - Update README, classifiers, and docs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent b441d3e commit e425260

29 files changed

Lines changed: 175 additions & 156 deletions

src/chat_sdk/adapters/discord/adapter.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import os
1414
import re
1515
from contextvars import ContextVar
16-
from datetime import UTC, datetime
16+
from datetime import datetime, timezone
1717
from typing import Any
1818
from urllib.parse import quote
1919

@@ -60,6 +60,7 @@
6060
StreamOptions,
6161
ThreadInfo,
6262
WebhookOptions,
63+
_parse_iso,
6364
)
6465

6566
DISCORD_API_BASE = "https://discord.com/api/v10"
@@ -588,9 +589,9 @@ async def _handle_forwarded_message(
588589
is_me=author_data.get("id") == self._application_id,
589590
),
590591
metadata=MessageMetadata(
591-
date_sent=datetime.fromisoformat(data.get("timestamp", ""))
592+
date_sent=_parse_iso(data.get("timestamp", ""))
592593
if data.get("timestamp")
593-
else datetime.now(UTC),
594+
else datetime.now(timezone.utc),
594595
edited=False,
595596
),
596597
attachments=[
@@ -1239,9 +1240,9 @@ def _parse_discord_message(self, raw: dict[str, Any], thread_id: str) -> Message
12391240
is_me=is_me,
12401241
),
12411242
metadata=MessageMetadata(
1242-
date_sent=datetime.fromisoformat(msg["timestamp"]) if msg.get("timestamp") else datetime.now(UTC),
1243+
date_sent=_parse_iso(msg["timestamp"]) if msg.get("timestamp") else datetime.now(timezone.utc),
12431244
edited=msg.get("edited_timestamp") is not None,
1244-
edited_at=datetime.fromisoformat(msg["edited_timestamp"]) if msg.get("edited_timestamp") else None,
1245+
edited_at=_parse_iso(msg["edited_timestamp"]) if msg.get("edited_timestamp") else None,
12451246
),
12461247
attachments=[
12471248
Attachment(
@@ -1284,7 +1285,7 @@ async def _create_discord_thread(
12841285
message_id: str,
12851286
) -> dict[str, str]:
12861287
"""Create a Discord thread from a message."""
1287-
thread_name = f"Thread {datetime.now(UTC).isoformat()}"
1288+
thread_name = f"Thread {datetime.now(timezone.utc).isoformat()}"
12881289

12891290
self._logger.debug(
12901291
"Discord API: POST thread",

src/chat_sdk/adapters/github/adapter.py

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import re
1717
import time
1818
from collections.abc import AsyncIterable
19-
from datetime import UTC, datetime
19+
from datetime import datetime, timezone
2020
from typing import Any
2121

2222
from chat_sdk.adapters.github.cards import card_to_github_markdown
@@ -55,6 +55,7 @@
5555
ThreadInfo,
5656
ThreadSummary,
5757
WebhookOptions,
58+
_parse_iso,
5859
)
5960

6061
REVIEW_COMMENT_THREAD_PATTERN = re.compile(r"^([^/]+)/([^:]+):(\d+):rc:(\d+)$")
@@ -384,11 +385,9 @@ def _parse_issue_comment(
384385
},
385386
author=author,
386387
metadata=MessageMetadata(
387-
date_sent=datetime.fromisoformat(created_at.replace("Z", "+00:00"))
388-
if created_at
389-
else datetime.now(tz=UTC),
388+
date_sent=_parse_iso(created_at) if created_at else datetime.now(tz=timezone.utc),
390389
edited=edited,
391-
edited_at=datetime.fromisoformat(updated_at.replace("Z", "+00:00")) if edited and updated_at else None,
390+
edited_at=_parse_iso(updated_at) if edited and updated_at else None,
392391
),
393392
attachments=[],
394393
)
@@ -426,11 +425,9 @@ def _parse_review_comment(
426425
},
427426
author=author,
428427
metadata=MessageMetadata(
429-
date_sent=datetime.fromisoformat(created_at.replace("Z", "+00:00"))
430-
if created_at
431-
else datetime.now(tz=UTC),
428+
date_sent=_parse_iso(created_at) if created_at else datetime.now(tz=timezone.utc),
432429
edited=edited,
433-
edited_at=datetime.fromisoformat(updated_at.replace("Z", "+00:00")) if edited and updated_at else None,
430+
edited_at=_parse_iso(updated_at) if edited and updated_at else None,
434431
),
435432
attachments=[],
436433
)
@@ -785,17 +782,17 @@ async def list_threads(
785782
},
786783
author=self._parse_author(pr_user),
787784
metadata=MessageMetadata(
788-
date_sent=datetime.fromisoformat(pr.get("created_at", "").replace("Z", "+00:00"))
785+
date_sent=_parse_iso(pr.get("created_at", "").replace("Z", "+00:00"))
789786
if pr.get("created_at")
790-
else datetime.now(tz=UTC),
787+
else datetime.now(tz=timezone.utc),
791788
edited=pr.get("created_at") != pr.get("updated_at"),
792789
),
793790
)
794791
threads.append(
795792
ThreadSummary(
796793
id=tid,
797794
root_message=root_message,
798-
last_reply_at=datetime.fromisoformat(pr.get("updated_at", "").replace("Z", "+00:00"))
795+
last_reply_at=_parse_iso(pr.get("updated_at", "").replace("Z", "+00:00"))
799796
if pr.get("updated_at")
800797
else None,
801798
)
@@ -928,13 +925,9 @@ async def _get_installation_token(self, installation_id: int) -> str:
928925
data = await response.json()
929926

930927
token = data["token"]
931-
# Parse ISO-8601 expiry from GitHub's response
928+
# Parse ISO-8601 expiry; default 1h if absent
932929
expires_at_str = data.get("expires_at", "")
933-
if expires_at_str:
934-
expires_at = datetime.fromisoformat(expires_at_str.replace("Z", "+00:00")).timestamp()
935-
else:
936-
# Default: 1 hour from now (GitHub's default)
937-
expires_at = time.time() + 3600
930+
expires_at = _parse_iso(expires_at_str).timestamp() if expires_at_str else time.time() + 3600
938931

939932
self._installation_token_cache[installation_id] = (token, expires_at)
940933
self._logger.debug(

src/chat_sdk/adapters/google_chat/adapter.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import re
1717
import time
1818
from collections.abc import AsyncIterable, Awaitable, Callable
19-
from datetime import UTC, datetime
19+
from datetime import datetime, timezone
2020
from typing import Any
2121

2222
from chat_sdk.adapters.google_chat.cards import card_to_google_card
@@ -78,6 +78,7 @@
7878
ThreadInfo,
7979
ThreadSummary,
8080
WebhookOptions,
81+
_parse_iso,
8182
)
8283

8384
# Strong-reference set for fire-and-forget tasks to prevent GC collection.
@@ -611,9 +612,7 @@ async def _create_space_subscription_with_cache(
611612

612613
subscription_info = {
613614
"subscription_name": result.name,
614-
"expire_time": int(datetime.fromisoformat(result.expire_time.replace("Z", "+00:00")).timestamp() * 1000)
615-
if result.expire_time
616-
else 0,
615+
"expire_time": int(_parse_iso(result.expire_time).timestamp() * 1000) if result.expire_time else 0,
617616
}
618617

619618
if self._state:
@@ -647,7 +646,7 @@ async def _find_existing_subscription(
647646
for sub in subscriptions:
648647
expire_time_str = sub.get("expire_time", "")
649648
if expire_time_str:
650-
expire_time = int(datetime.fromisoformat(expire_time_str.replace("Z", "+00:00")).timestamp() * 1000)
649+
expire_time = int(_parse_iso(expire_time_str).timestamp() * 1000)
651650
if expire_time > int(time.time() * 1000) + SUBSCRIPTION_REFRESH_BUFFER_MS:
652651
return {
653652
"subscription_name": sub.get("name", ""),
@@ -2310,7 +2309,7 @@ async def list_threads(
23102309
last_reply_at: datetime | None = None
23112310
if create_time:
23122311
with contextlib.suppress(ValueError, AttributeError):
2313-
last_reply_at = datetime.fromisoformat(create_time.replace("Z", "+00:00"))
2312+
last_reply_at = _parse_iso(create_time)
23142313

23152314
threads.append(
23162315
ThreadSummary(
@@ -2725,11 +2724,11 @@ def _parse_message_metadata(message: dict[str, Any]) -> Any:
27252724
date_sent: datetime
27262725
if create_time:
27272726
try:
2728-
date_sent = datetime.fromisoformat(create_time.replace("Z", "+00:00"))
2727+
date_sent = _parse_iso(create_time)
27292728
except (ValueError, AttributeError):
2730-
date_sent = datetime.now(tz=UTC)
2729+
date_sent = datetime.now(tz=timezone.utc)
27312730
else:
2732-
date_sent = datetime.now(tz=UTC)
2731+
date_sent = datetime.now(tz=timezone.utc)
27332732

27342733
return MessageMetadata(
27352734
date_sent=date_sent,

src/chat_sdk/adapters/linear/adapter.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import os
1616
import re
1717
import time
18-
from datetime import UTC, datetime
18+
from datetime import datetime, timezone
1919
from typing import Any
2020

2121
from chat_sdk.adapters.linear.cards import card_to_linear_markdown
@@ -53,6 +53,7 @@
5353
StreamOptions,
5454
ThreadInfo,
5555
WebhookOptions,
56+
_parse_iso,
5657
)
5758

5859
COMMENT_THREAD_PATTERN = re.compile(r"^([^:]+):c:([^:]+)$")
@@ -415,9 +416,9 @@ def _build_message(
415416
raw=LinearRawMessage(comment=comment),
416417
author=author,
417418
metadata=MessageMetadata(
418-
date_sent=datetime.fromisoformat(created_at) if created_at else datetime.now(UTC),
419+
date_sent=_parse_iso(created_at) if created_at else datetime.now(timezone.utc),
419420
edited=created_at != updated_at,
420-
edited_at=datetime.fromisoformat(updated_at) if (created_at != updated_at and updated_at) else None,
421+
edited_at=_parse_iso(updated_at) if (created_at != updated_at and updated_at) else None,
421422
),
422423
attachments=[],
423424
)
@@ -749,10 +750,10 @@ def _comment_node_to_message(
749750
is_me=user_id == self._bot_user_id,
750751
),
751752
metadata=MessageMetadata(
752-
date_sent=datetime.fromisoformat(node["createdAt"]) if node.get("createdAt") else datetime.now(UTC),
753+
date_sent=_parse_iso(node["createdAt"]) if node.get("createdAt") else datetime.now(timezone.utc),
753754
edited=node.get("createdAt") != node.get("updatedAt"),
754755
edited_at=(
755-
datetime.fromisoformat(node["updatedAt"])
756+
_parse_iso(node["updatedAt"])
756757
if node.get("createdAt") != node.get("updatedAt") and node.get("updatedAt")
757758
else None
758759
),
@@ -852,9 +853,9 @@ def parse_message(self, raw: LinearRawMessage) -> Message:
852853
is_me=user_id == self._bot_user_id,
853854
),
854855
metadata=MessageMetadata(
855-
date_sent=(datetime.fromisoformat(created_at) if created_at else datetime.now(UTC)),
856+
date_sent=(_parse_iso(created_at) if created_at else datetime.now(timezone.utc)),
856857
edited=created_at != updated_at,
857-
edited_at=(datetime.fromisoformat(updated_at) if created_at != updated_at and updated_at else None),
858+
edited_at=(_parse_iso(updated_at) if created_at != updated_at and updated_at else None),
858859
),
859860
attachments=[],
860861
raw=raw,

src/chat_sdk/adapters/slack/adapter.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from collections import OrderedDict
2121
from collections.abc import AsyncIterable, Awaitable, Callable
2222
from contextvars import ContextVar
23-
from datetime import UTC, datetime
23+
from datetime import datetime, timezone
2424
from typing import Any
2525
from urllib.parse import parse_qs
2626

@@ -1574,14 +1574,14 @@ async def _parse_slack_message(
15741574

15751575
ts_str = event.get("ts", "0")
15761576
try:
1577-
date_sent = datetime.fromtimestamp(float(ts_str), tz=UTC)
1577+
date_sent = datetime.fromtimestamp(float(ts_str), tz=timezone.utc)
15781578
except (ValueError, TypeError, OSError):
1579-
date_sent = datetime.now(tz=UTC)
1579+
date_sent = datetime.now(tz=timezone.utc)
15801580

15811581
edited_at: datetime | None = None
15821582
if event.get("edited"):
15831583
try:
1584-
edited_at = datetime.fromtimestamp(float(event["edited"].get("ts", "0")), tz=UTC)
1584+
edited_at = datetime.fromtimestamp(float(event["edited"].get("ts", "0")), tz=timezone.utc)
15851585
except (ValueError, TypeError, OSError):
15861586
edited_at = None
15871587

@@ -1616,14 +1616,14 @@ def _parse_slack_message_sync(self, event: dict[str, Any], thread_id: str) -> Me
16161616

16171617
ts_str = event.get("ts", "0")
16181618
try:
1619-
date_sent = datetime.fromtimestamp(float(ts_str), tz=UTC)
1619+
date_sent = datetime.fromtimestamp(float(ts_str), tz=timezone.utc)
16201620
except (ValueError, TypeError, OSError):
1621-
date_sent = datetime.now(tz=UTC)
1621+
date_sent = datetime.now(tz=timezone.utc)
16221622

16231623
edited_at: datetime | None = None
16241624
if event.get("edited"):
16251625
try:
1626-
edited_at = datetime.fromtimestamp(float(event["edited"].get("ts", "0")), tz=UTC)
1626+
edited_at = datetime.fromtimestamp(float(event["edited"].get("ts", "0")), tz=timezone.utc)
16271627
except (ValueError, TypeError, OSError):
16281628
edited_at = None
16291629

@@ -2585,7 +2585,7 @@ async def list_threads(self, channel_id: str, options: ListThreadsOptions | None
25852585
last_reply_at: datetime | None = None
25862586
if msg.get("latest_reply"):
25872587
try:
2588-
last_reply_at = datetime.fromtimestamp(float(msg["latest_reply"]), tz=UTC)
2588+
last_reply_at = datetime.fromtimestamp(float(msg["latest_reply"]), tz=timezone.utc)
25892589
except (ValueError, TypeError, OSError):
25902590
last_reply_at = None
25912591

src/chat_sdk/adapters/teams/adapter.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import json
1414
import os
1515
import re
16-
from datetime import UTC, datetime
16+
from datetime import datetime, timezone
1717
from typing import Any, NoReturn
1818

1919
from chat_sdk.adapters.teams.cards import card_to_adaptive_card
@@ -52,6 +52,7 @@
5252
StreamOptions,
5353
ThreadInfo,
5454
WebhookOptions,
55+
_parse_iso,
5556
)
5657

5758
MESSAGEID_CAPTURE_PATTERN = re.compile(r"messageid=(\d+)")
@@ -527,9 +528,9 @@ def _parse_teams_message(
527528
is_me=is_me,
528529
),
529530
metadata=MessageMetadata(
530-
date_sent=datetime.fromisoformat(activity["timestamp"])
531+
date_sent=_parse_iso(activity["timestamp"])
531532
if activity.get("timestamp")
532-
else datetime.now(UTC),
533+
else datetime.now(timezone.utc),
533534
edited=False,
534535
),
535536
attachments=attachments,
@@ -1430,7 +1431,7 @@ def _map_graph_message(self, msg: dict[str, Any], thread_id: str) -> Message:
14301431
),
14311432
metadata=MessageMetadata(
14321433
date_sent=(
1433-
datetime.fromisoformat(msg["createdDateTime"]) if msg.get("createdDateTime") else datetime.now(UTC)
1434+
_parse_iso(msg["createdDateTime"]) if msg.get("createdDateTime") else datetime.now(timezone.utc)
14341435
),
14351436
edited=bool(msg.get("lastModifiedDateTime")),
14361437
),

src/chat_sdk/adapters/telegram/adapter.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import os
1818
import re
1919
from dataclasses import dataclass
20-
from datetime import UTC, datetime
20+
from datetime import datetime, timezone
2121
from typing import Any
2222

2323
from chat_sdk.adapters.telegram.cards import (
@@ -944,7 +944,7 @@ async def edit_message(
944944
metadata=MessageMetadata(
945945
date_sent=existing.metadata.date_sent,
946946
edited=True,
947-
edited_at=datetime.now(UTC),
947+
edited_at=datetime.now(timezone.utc),
948948
),
949949
attachments=existing.attachments,
950950
is_mention=existing.is_mention,
@@ -1259,9 +1259,9 @@ def parse_telegram_message(
12591259
raw=raw,
12601260
author=author,
12611261
metadata=MessageMetadata(
1262-
date_sent=datetime.fromtimestamp(raw["date"], tz=UTC),
1262+
date_sent=datetime.fromtimestamp(raw["date"], tz=timezone.utc),
12631263
edited=edit_date is not None,
1264-
edited_at=(datetime.fromtimestamp(edit_date, tz=UTC) if edit_date is not None else None),
1264+
edited_at=(datetime.fromtimestamp(edit_date, tz=timezone.utc) if edit_date is not None else None),
12651265
),
12661266
attachments=self.extract_attachments(raw),
12671267
is_mention=self.is_bot_mentioned(raw, plain_text),

src/chat_sdk/adapters/whatsapp/adapter.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import os
1616
import time
1717
from collections.abc import AsyncIterable
18-
from datetime import UTC, datetime
18+
from datetime import datetime, timezone
1919
from typing import Any
2020
from urllib.parse import parse_qs, urlparse
2121

@@ -525,7 +525,7 @@ def _build_message(
525525
metadata=MessageMetadata(
526526
date_sent=datetime.fromtimestamp(
527527
int(inbound.get("timestamp", "0")),
528-
tz=UTC,
528+
tz=timezone.utc,
529529
),
530530
edited=False,
531531
),
@@ -981,7 +981,7 @@ def parse_message(self, raw: WhatsAppRawMessage) -> Message:
981981
metadata=MessageMetadata(
982982
date_sent=datetime.fromtimestamp(
983983
int(raw["message"].get("timestamp", "0")),
984-
tz=UTC,
984+
tz=timezone.utc,
985985
),
986986
edited=False,
987987
),

0 commit comments

Comments
 (0)