Search code examples
pythonboto3mypypython-typing

Type error using combination of mypy and boto3 stubs


I am using typed python in AWS. (mypy and boto3 stubs)

I am new at typing, so I will need your advice and explanation.

I have one function like this:

def select_time_range() -> dict[str, datetime]:

    try:
        current_datetime = datetime.now()
        datetime_one_day_before = current_datetime - timedelta(days=1)
        datetime_24_hours_ago = current_datetime - timedelta(hours=24)

        report_timerange_choice = {
            'previous_day': {
                'from': datetime(datetime_one_day_before.year, datetime_one_day_before.month, datetime_one_day_before.day,
                                 0, 0),
                'to': datetime(current_datetime.year, current_datetime.month, current_datetime.day, 0, 0)
            },
            'last_24_hours': {

                    'from': datetime(datetime_24_hours_ago.year, datetime_24_hours_ago.month, datetime_24_hours_ago.day, datetime_24_hours_ago.hour, datetime_24_hours_ago.minute),
                    'to': datetime(current_datetime.year, current_datetime.month, current_datetime.day, current_datetime.hour, current_datetime.minute),
                }
            }

    except Exception as e:
        LOGGER.error(e)
        raise e

    return report_timerange_choice[TIMEFRAME_PICKER]

This function is returning dict like this: {'from': datetime.datetime(2024, 3, 21, 0, 0) 'to':

 datetime.datetime(2024, 3, 22, 0, 0)}

Then I have function like this where I am passing this dict to the function as a parameter. The thing is, describe_events() function is expecting *timeframe as a *type DateTimeRangeTypeDef, but it is getting dict[str, datetime]". How can I "change a type" or what I need to do to write correctly?

def get_account_events(timeframe: dict[str, datetime]) -> DescribeEventsResponseTypeDef:
    try:
        #DAILY
        daily_events = health_client.describe_events(

            filter={
                'startTimes': [timeframe]
                
            }
        )

    except Exception as e:
        LOGGER.error(e)
        raise e

    return daily_events

When I change the type from

dict[str, datetime]

to

DateTimeRangeTypeDef

in first function, it is complaining that it is a type dict[str, datetime]


Solution

  • (to clarify, I assume that we're talking about health_client = boto3.client('health'))

    In stubs it is defined as

    TimestampTypeDef = Union[datetime, str]
    ...
    DateTimeRangeTypeDef = TypedDict(
        "DateTimeRangeTypeDef",
        {
            "from": NotRequired[TimestampTypeDef],
            "to": NotRequired[TimestampTypeDef],
        },
    )
    

    The easiest way to see the stubs is to pip install corresponding stubs package (e.g. pip install 'boto3-stubs[health]') and examine the contents of its folder (e.g. grep -rn DateTimeRangeTypeDef ./venv/lib/python3.XX/site-packages/mypy_boto3_health/), then go to the file where it is defined and watch. It works for any service, replace health with a service name - the one you use to instantiate a client - in the text above.

    So just use DateTimeRangeTypeDef instead of dict[str, datetime]. You may need to annotate the outer dictionary. The following passes type checking cleanly (I took the liberty to fix a couple of almost-definitely-mistakes like raise e inside handler, but beware that catch-all except Exception is weird here, and LOGGER.error does not print traceback by default, you may want to use LOGGER.exception or LOGGER.error(..., exc_info=True) instead).

    from __future__ import annotations
    
    import logging
    from datetime import datetime, timedelta
    from typing import TYPE_CHECKING, Final, Literal, TypeAlias
    
    import boto3
    
    if TYPE_CHECKING:
        from mypy_boto3_health.type_defs import (
            DateTimeRangeTypeDef,
            DescribeEventsResponseTypeDef,
        )
    
    health_client: Final = boto3.client("health")
    LOGGER: Final = logging.getLogger(__name__)
    TimeFramePickerT: TypeAlias = Literal["previous_day", "last_24_hours"]
    TIMEFRAME_PICKER: Final[TimeFramePickerT] = "last_24_hours"
    
    
    def select_time_range() -> DateTimeRangeTypeDef:
        try:
            current_datetime = datetime.now()
            datetime_one_day_before = current_datetime - timedelta(days=1)
            datetime_24_hours_ago = current_datetime - timedelta(hours=24)
    
            report_timerange_choice: dict[TimeFramePickerT, DateTimeRangeTypeDef] = {
                "previous_day": {
                    "from": datetime(
                        datetime_one_day_before.year,
                        datetime_one_day_before.month,
                        datetime_one_day_before.day,
                        0,
                        0,
                    ),
                    "to": datetime(
                        current_datetime.year,
                        current_datetime.month,
                        current_datetime.day,
                        0,
                        0,
                    ),
                },
                "last_24_hours": {
                    "from": datetime(
                        datetime_24_hours_ago.year,
                        datetime_24_hours_ago.month,
                        datetime_24_hours_ago.day,
                        datetime_24_hours_ago.hour,
                        datetime_24_hours_ago.minute,
                    ),
                    "to": datetime(
                        current_datetime.year,
                        current_datetime.month,
                        current_datetime.day,
                        current_datetime.hour,
                        current_datetime.minute,
                    ),
                },
            }
        except Exception as e:
            LOGGER.exception(e)
            raise
    
        return report_timerange_choice[TIMEFRAME_PICKER]
    
    
    def get_account_events(
        timeframe: DateTimeRangeTypeDef,
    ) -> DescribeEventsResponseTypeDef:
        try:
            daily_events = health_client.describe_events(
                filter={
                    "startTimes": [timeframe],
                }
            )
        except Exception as e:
            LOGGER.error(e)
            raise
    
        return daily_events