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 @@

Sub-modules

+
slack_sdk.oauth.sqlalchemy_utils
+
+
+
slack_sdk.oauth.state_store

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 @@

    Classes

    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 @@

    Classes

    # 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

    +
    +
    +
    +
    +
    +
    +
    +
    +

    Functions

    +
    +
    +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 @@

    Classes

    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 @@

    Classes

    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"