diff --git a/docs/reference/oauth/index.html b/docs/reference/oauth/index.html
index 8fe69e5d0..a7a9199e2 100644
--- a/docs/reference/oauth/index.html
+++ b/docs/reference/oauth/index.html
@@ -54,6 +54,10 @@
OAuth state parameter data store …
@@ -865,6 +869,7 @@ Methods
slack_sdk.oauth.authorize_url_generator
slack_sdk.oauth.installation_store
slack_sdk.oauth.redirect_uri_page_renderer
+slack_sdk.oauth.sqlalchemy_utils
slack_sdk.oauth.state_store
slack_sdk.oauth.state_utils
slack_sdk.oauth.token_rotation
diff --git a/docs/reference/oauth/installation_store/sqlalchemy/index.html b/docs/reference/oauth/installation_store/sqlalchemy/index.html
index 8861dd476..d19743757 100644
--- a/docs/reference/oauth/installation_store/sqlalchemy/index.html
+++ b/docs/reference/oauth/installation_store/sqlalchemy/index.html
@@ -99,6 +99,9 @@
async with self.engine.begin() as conn:
i = installation.to_dict()
i["client_id"] = self.client_id
+ i["installed_at"] = normalize_datetime_for_db(i.get("installed_at"))
+ i["bot_token_expires_at"] = normalize_datetime_for_db(i.get("bot_token_expires_at"))
+ i["user_token_expires_at"] = normalize_datetime_for_db(i.get("user_token_expires_at"))
i_column = self.installations.c
installations_rows = await conn.execute(
@@ -130,6 +133,8 @@
# bots
b = bot.to_dict()
b["client_id"] = self.client_id
+ b["installed_at"] = normalize_datetime_for_db(b.get("installed_at"))
+ b["bot_token_expires_at"] = normalize_datetime_for_db(b.get("bot_token_expires_at"))
b_column = self.bots.c
bots_rows = await conn.execute(
@@ -526,6 +531,9 @@ Inherited members
with self.engine.begin() as conn:
i = installation.to_dict()
i["client_id"] = self.client_id
+ i["installed_at"] = normalize_datetime_for_db(i.get("installed_at"))
+ i["bot_token_expires_at"] = normalize_datetime_for_db(i.get("bot_token_expires_at"))
+ i["user_token_expires_at"] = normalize_datetime_for_db(i.get("user_token_expires_at"))
i_column = self.installations.c
installations_rows = conn.execute(
@@ -557,6 +565,8 @@ Inherited members
# bots
b = bot.to_dict()
b["client_id"] = self.client_id
+ b["installed_at"] = normalize_datetime_for_db(b.get("installed_at"))
+ b["bot_token_expires_at"] = normalize_datetime_for_db(b.get("bot_token_expires_at"))
b_column = self.bots.c
bots_rows = conn.execute(
diff --git a/docs/reference/oauth/sqlalchemy_utils/index.html b/docs/reference/oauth/sqlalchemy_utils/index.html
new file mode 100644
index 000000000..d82260a1d
--- /dev/null
+++ b/docs/reference/oauth/sqlalchemy_utils/index.html
@@ -0,0 +1,130 @@
+
+
+
+
+
+
+slack_sdk.oauth.sqlalchemy_utils API documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Module slack_sdk.oauth.sqlalchemy_utils
+
+
+
+
+
+
+
+
+def normalize_datetime_for_db (dt: datetime.datetime | None) ‑> datetime.datetime | None
+
+
+
+
+Expand source code
+
+def normalize_datetime_for_db(dt: Optional[datetime]) -> Optional[datetime]:
+ """
+ Normalize timezone-aware datetime to naive UTC datetime for database storage.
+
+ Ensures compatibility with existing databases using TIMESTAMP WITHOUT TIME ZONE.
+ SQLAlchemy DateTime columns without timezone=True create naive timestamp columns
+ in databases like PostgreSQL. This function strips timezone information from
+ timezone-aware datetimes (which are already in UTC) to enable safe comparisons.
+
+ Args:
+ dt: A timezone-aware or naive datetime object, or None
+
+ Returns:
+ A naive datetime in UTC, or None if input is None
+
+ Example:
+ >>> from datetime import datetime, timezone
+ >>> aware_dt = datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
+ >>> naive_dt = normalize_datetime_for_db(aware_dt)
+ >>> naive_dt.tzinfo is None
+ True
+ """
+ if dt is None:
+ return None
+ if dt.tzinfo is not None:
+ return dt.replace(tzinfo=None)
+ return dt
+
+Normalize timezone-aware datetime to naive UTC datetime for database storage.
+
Ensures compatibility with existing databases using TIMESTAMP WITHOUT TIME ZONE.
+SQLAlchemy DateTime columns without timezone=True create naive timestamp columns
+in databases like PostgreSQL. This function strips timezone information from
+timezone-aware datetimes (which are already in UTC) to enable safe comparisons.
+
Args
+
+dt
+A timezone-aware or naive datetime object, or None
+
+
Returns
+
A naive datetime in UTC, or None if input is None
+
Example
+
>>> from datetime import datetime, timezone
+>>> aware_dt = datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
+>>> naive_dt = normalize_datetime_for_db(aware_dt)
+>>> naive_dt.tzinfo is None
+True
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/reference/oauth/state_store/sqlalchemy/index.html b/docs/reference/oauth/state_store/sqlalchemy/index.html
index a35fa3171..341fc10be 100644
--- a/docs/reference/oauth/state_store/sqlalchemy/index.html
+++ b/docs/reference/oauth/state_store/sqlalchemy/index.html
@@ -99,7 +99,7 @@
async def async_issue(self, *args, **kwargs) -> str:
state: str = str(uuid4())
- now = datetime.fromtimestamp(time.time() + self.expiration_seconds, tz=timezone.utc)
+ now = normalize_datetime_for_db(datetime.fromtimestamp(time.time() + self.expiration_seconds, tz=timezone.utc))
async with self.engine.begin() as conn:
await conn.execute(
self.oauth_states.insert(),
@@ -109,9 +109,10 @@
async def async_consume(self, state: str) -> bool:
try:
+ now = normalize_datetime_for_db(datetime.now(tz=timezone.utc))
async with self.engine.begin() as conn:
c = self.oauth_states.c
- query = self.oauth_states.select().where(and_(c.state == state, c.expire_at > datetime.now(tz=timezone.utc)))
+ query = self.oauth_states.select().where(and_(c.state == state, c.expire_at > now))
result = await conn.execute(query)
for row in result.mappings():
self.logger.debug(f"consume's query result: {row}")
@@ -189,9 +190,10 @@ Methods
async def async_consume(self, state: str) -> bool:
try:
+ now = normalize_datetime_for_db(datetime.now(tz=timezone.utc))
async with self.engine.begin() as conn:
c = self.oauth_states.c
- query = self.oauth_states.select().where(and_(c.state == state, c.expire_at > datetime.now(tz=timezone.utc)))
+ query = self.oauth_states.select().where(and_(c.state == state, c.expire_at > now))
result = await conn.execute(query)
for row in result.mappings():
self.logger.debug(f"consume's query result: {row}")
@@ -215,7 +217,7 @@ Methods
async def async_issue(self, *args, **kwargs) -> str:
state: str = str(uuid4())
- now = datetime.fromtimestamp(time.time() + self.expiration_seconds, tz=timezone.utc)
+ now = normalize_datetime_for_db(datetime.fromtimestamp(time.time() + self.expiration_seconds, tz=timezone.utc))
async with self.engine.begin() as conn:
await conn.execute(
self.oauth_states.insert(),
@@ -293,7 +295,7 @@ Methods
def issue(self, *args, **kwargs) -> str:
state: str = str(uuid4())
- now = datetime.fromtimestamp(time.time() + self.expiration_seconds, tz=timezone.utc)
+ now = normalize_datetime_for_db(datetime.fromtimestamp(time.time() + self.expiration_seconds, tz=timezone.utc))
with self.engine.begin() as conn:
conn.execute(
self.oauth_states.insert(),
@@ -303,9 +305,10 @@ Methods
def consume(self, state: str) -> bool:
try:
+ now = normalize_datetime_for_db(datetime.now(tz=timezone.utc))
with self.engine.begin() as conn:
c = self.oauth_states.c
- query = self.oauth_states.select().where(and_(c.state == state, c.expire_at > datetime.now(tz=timezone.utc)))
+ query = self.oauth_states.select().where(and_(c.state == state, c.expire_at > now))
result = conn.execute(query)
for row in result.mappings():
self.logger.debug(f"consume's query result: {row}")
@@ -383,9 +386,10 @@ Methods
def consume(self, state: str) -> bool:
try:
+ now = normalize_datetime_for_db(datetime.now(tz=timezone.utc))
with self.engine.begin() as conn:
c = self.oauth_states.c
- query = self.oauth_states.select().where(and_(c.state == state, c.expire_at > datetime.now(tz=timezone.utc)))
+ query = self.oauth_states.select().where(and_(c.state == state, c.expire_at > now))
result = conn.execute(query)
for row in result.mappings():
self.logger.debug(f"consume's query result: {row}")
@@ -422,7 +426,7 @@ Methods
def issue(self, *args, **kwargs) -> str:
state: str = str(uuid4())
- now = datetime.fromtimestamp(time.time() + self.expiration_seconds, tz=timezone.utc)
+ now = normalize_datetime_for_db(datetime.fromtimestamp(time.time() + self.expiration_seconds, tz=timezone.utc))
with self.engine.begin() as conn:
conn.execute(
self.oauth_states.insert(),
diff --git a/slack_sdk/version.py b/slack_sdk/version.py
index a2910c423..f5a37b599 100644
--- a/slack_sdk/version.py
+++ b/slack_sdk/version.py
@@ -1,3 +1,3 @@
"""Check the latest version at https://pypi.org/project/slack-sdk/"""
-__version__ = "3.40.0"
+__version__ = "3.40.1"