Skip to content

decorative_secrets.utilities

iscoroutinefunction

iscoroutinefunction(function: typing.Any) -> bool

An adaptation of asyncio.iscoroutinefunction

Source code in src/decorative_secrets/utilities.py
13
14
15
16
17
18
19
20
21
22
def iscoroutinefunction(function: Any) -> bool:
    """
    An adaptation of `asyncio.iscoroutinefunction`
    """
    if isinstance(function, partial):
        return iscoroutinefunction(function.func)
    return (
        inspect.iscoroutinefunction(function)
        or type(getattr(function, "_is_coroutine", None)) is object
    )

as_tuple

as_tuple(
    function: collections.abc.Callable[
        ...,
        collections.abc.Iterable[typing.Any]
        | collections.abc.Awaitable[
            collections.abc.Iterable[typing.Any]
        ],
    ],
) -> collections.abc.Callable[..., typing.Any]

This is a decorator which will return an iterable as a tuple.

Examples:

from decorative_secrets.utilities import as_tuple


@as_tuple
def get_numbers() -> Iterable[int]:
    yield 1
    yield 2
    yield 3


assert get_numbers() == (1, 2, 3)
Source code in src/decorative_secrets/utilities.py
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
def as_tuple(
    function: Callable[..., Iterable[Any] | Awaitable[Iterable[Any]]],
) -> Callable[..., Any]:
    """
    This is a decorator which will return an iterable as a tuple.

    Examples:
        ```python
        from decorative_secrets.utilities import as_tuple


        @as_tuple
        def get_numbers() -> Iterable[int]:
            yield 1
            yield 2
            yield 3


        assert get_numbers() == (1, 2, 3)
        ```
    """
    if iscoroutinefunction(function):

        @wraps(function)
        async def wrapper(*args: Any, **kwargs: Any) -> tuple[Any, ...]:
            return tuple(  # --
                await function(*args, **kwargs) or ()  # type: ignore[misc]
            )

    else:

        @wraps(function)
        def wrapper(*args: Any, **kwargs: Any) -> tuple[Any, ...]:
            return tuple(
                function(*args, **kwargs) or ()  # type: ignore[arg-type]
            )

    return wrapper

as_str

as_str(
    function: None = None, separator: str = ""
) -> collections.abc.Callable[
    ..., collections.abc.Callable[..., str]
]
as_str(
    function: collections.abc.Callable[
        ..., collections.abc.Iterable[str]
    ] = ...,
    separator: str = "",
) -> collections.abc.Callable[..., str]
as_str(
    function: (
        collections.abc.Callable[
            ..., collections.abc.Iterable[str]
        ]
        | collections.abc.Awaitable[
            collections.abc.Iterable[typing.Any]
        ]
        | None
    ) = None,
    separator: str = "",
) -> (
    collections.abc.Callable[
        ..., collections.abc.Callable[..., str]
    ]
    | collections.abc.Callable[..., str]
)
This decorator causes a function yielding an iterable of strings to
return a single string with the elements joined by the specified
`separator`.

Parameters:
    function: The function to decorate. If `None`, a decorating
        function is returned.
    separator: The string used to join the iterable elements.

Returns:
    A decorator which joins the iterable elements into a single string.

Examples:
    ```python
    from decorative_secrets.utilities import as_str


    @as_str(separator=", ")
    def get_fruits() -> Iterable[str]:
        yield "apple"
        yield "banana"
        yield "cherry"


    assert get_fruits() == "apple, banana, cherry"
    ```

    ```python
    from decorative_secrets.utilities import as_str


    @as_str
    def get_fruits() -> Iterable[str]:
        yield "apple

" yield "banana " yield "cherry"

    assert get_fruits() == "apple

banana cherry" ```

Source code in src/decorative_secrets/utilities.py
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
def as_str(
    function: Callable[..., Iterable[str]]
    | Awaitable[Iterable[Any]]
    | None = None,
    separator: str = "",
) -> Callable[..., Callable[..., str]] | Callable[..., str]:
    """
    This decorator causes a function yielding an iterable of strings to
    return a single string with the elements joined by the specified
    `separator`.

    Parameters:
        function: The function to decorate. If `None`, a decorating
            function is returned.
        separator: The string used to join the iterable elements.

    Returns:
        A decorator which joins the iterable elements into a single string.

    Examples:
        ```python
        from decorative_secrets.utilities import as_str


        @as_str(separator=", ")
        def get_fruits() -> Iterable[str]:
            yield "apple"
            yield "banana"
            yield "cherry"


        assert get_fruits() == "apple, banana, cherry"
        ```

        ```python
        from decorative_secrets.utilities import as_str


        @as_str
        def get_fruits() -> Iterable[str]:
            yield "apple\n"
            yield "banana\n"
            yield "cherry"


        assert get_fruits() == "apple\nbanana\ncherry"
        ```
    """

    def decorating_function(
        user_function: Callable[..., Iterable[str]],
    ) -> Callable[..., Any]:
        if iscoroutinefunction(user_function):

            @wraps(user_function)
            async def wrapper(*args: Any, **kwargs: Any) -> str:
                return separator.join(
                    await user_function(  # type: ignore[misc]
                        *args, **kwargs
                    )
                    or ()
                )

        else:

            @wraps(user_function)
            def wrapper(*args: Any, **kwargs: Any) -> str:
                return separator.join(user_function(*args, **kwargs) or ())

        return wrapper

    if function is None:
        return decorating_function
    return decorating_function(function)  # type: ignore[arg-type]

as_dict

as_dict(
    function: collections.abc.Callable[
        ...,
        collections.abc.Iterable[
            tuple[typing.Any, typing.Any]
        ]
        | collections.abc.Awaitable[
            collections.abc.Iterable[
                tuple[typing.Any, typing.Any]
            ]
        ],
    ],
) -> collections.abc.Callable[..., typing.Any]

This is a decorator which will return an iterable of key/value pairs as a dictionary.

Examples:

from decorative_secrets.utilities import as_dict


@as_dict
def get_settings() -> Iterable[tuple[str, Any]]:
    yield ("host", "localhost")
    yield ("port", 8080)
    yield ("debug", True)


assert get_settings() == (
    {"host": "localhost", "port": 8080, "debug": True}
)
Source code in src/decorative_secrets/utilities.py
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
def as_dict(
    function: Callable[
        ..., Iterable[tuple[Any, Any]] | Awaitable[Iterable[tuple[Any, Any]]]
    ],
) -> Callable[..., Any]:
    """
    This is a decorator which will return an iterable of key/value pairs
    as a dictionary.

    Examples:
        ```python
        from decorative_secrets.utilities import as_dict


        @as_dict
        def get_settings() -> Iterable[tuple[str, Any]]:
            yield ("host", "localhost")
            yield ("port", 8080)
            yield ("debug", True)


        assert get_settings() == (
            {"host": "localhost", "port": 8080, "debug": True}
        )
        ```
    """

    if iscoroutinefunction(function):

        @wraps(function)
        async def wrapper(*args: Any, **kwargs: Any) -> dict[Any, Any]:
            return dict(
                await function(*args, **kwargs) or ()  # type: ignore[misc]
            )

    else:

        @wraps(function)
        def wrapper(*args: Any, **kwargs: Any) -> dict[Any, Any]:
            return dict(
                function(*args, **kwargs) or ()  # type: ignore[arg-type]
            )

    return wrapper

as_iter

as_iter(
    function: collections.abc.Callable[
        ..., collections.abc.Iterable[typing.Any]
    ],
) -> collections.abc.Callable[..., typing.Any]

This is a decorator which will return an iterator for a function yielding an iterable.

Examples:

from decorative_secrets.utilities import as_iter
from collections.abc import Iterator


@as_iter
def get_settings() -> Iterable[tuple[str, Any]]:
    yield ("host", "localhost")
    yield ("port", 8080)
    yield ("debug", True)


assert issubclass(get_settings(), Iterator)
Source code in src/decorative_secrets/utilities.py
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
def as_iter(
    function: Callable[..., Iterable[Any]],
) -> Callable[..., Any]:
    """
    This is a decorator which will return an iterator for a function
    yielding an iterable.

    Examples:
        ```python
        from decorative_secrets.utilities import as_iter
        from collections.abc import Iterator


        @as_iter
        def get_settings() -> Iterable[tuple[str, Any]]:
            yield ("host", "localhost")
            yield ("port", 8080)
            yield ("debug", True)


        assert issubclass(get_settings(), Iterator)
        ```
    """

    if iscoroutinefunction(function):

        @wraps(function)
        async def wrapper(*args: Any, **kwargs: Any) -> Iterator[Any]:
            return iter(
                await function(*args, **kwargs) or ()  # type: ignore[misc]
            )

    else:

        @wraps(function)
        def wrapper(*args: Any, **kwargs: Any) -> Iterator[Any]:
            return iter(function(*args, **kwargs) or ())

    return wrapper

warn_retry_hook

warn_retry_hook(
    error: Exception,
    attempt_number: int,
    *args: typing.Any,
    **kwargs: typing.Any
) -> bool

This is a retry hook which will issue a warning and retry number whenever an error occurs.

Source code in src/decorative_secrets/utilities.py
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
def warn_retry_hook(
    error: Exception,
    attempt_number: int,
    *args: Any,  # noqa: ARG001
    **kwargs: Any,  # noqa: ARG001
) -> bool:
    """
    This is a retry hook which will issue a warning and retry number
    whenever an error occurs.
    """
    message: str = f"Attempt # {attempt_number} failed with error: {error}"
    warn(
        message,
        stacklevel=2,
    )
    return True

create_log_warning_retry_hook

create_log_warning_retry_hook(
    logger: (
        logging.Logger
        | collections.abc.Callable[[], logging.Logger]
    ),
) -> decorative_secrets.utilities.RetryHook

This factory creates a retry hook which logs warning using the provided logger.

Parameters:

  • logger (logging.Logger | collections.abc.Callable[[], logging.Logger]) –

    The logger to use for logging warnings, or a callable which

Source code in src/decorative_secrets/utilities.py
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
def create_log_warning_retry_hook(
    logger: logging.Logger | Callable[[], logging.Logger],
) -> RetryHook:
    """
    This factory creates a retry hook which logs warning using the provided
    logger.

    Parameters:
        logger: The logger to use for logging warnings, or a callable which
        returns a logger.
    """
    if not isinstance(logger, logging.Logger) and callable(logger):
        logger = logger()
    if not isinstance(logger, logging.Logger):
        raise TypeError(logger)

    def retry_hook(
        error: Exception,
        attempt_number: int,
        *args: Any,  # noqa: ARG001
        **kwargs: Any,  # noqa: ARG001
    ) -> bool:
        logger.warning(
            "Attempt # %d failed with error: %s",
            attempt_number,
            str(error),
            stacklevel=2,
        )
        return True

    return retry_hook

create_async_log_warning_retry_hook

create_async_log_warning_retry_hook(
    logger: logging.Logger,
) -> decorative_secrets.utilities.AsyncRetryHook

This factory creates an async retry hook which logs warning using the provided logger.

!!! Note Please make sure to use a non-blocking logger .

Parameters:

  • logger (logging.Logger) –

    The logger to use for logging warnings.

Source code in src/decorative_secrets/utilities.py
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
def create_async_log_warning_retry_hook(
    logger: logging.Logger,
) -> AsyncRetryHook:
    """
    This factory creates an async retry hook which logs warning using the
    provided logger.

    !!! Note
        Please make sure to use a [non-blocking logger
        ](https://docs.python.org/3/howto/logging-cookbook.html#dealing-with-handlers-that-block).

    Parameters:
        logger: The logger to use for logging warnings.
    """

    async def retry_hook(
        error: Exception,
        attempt_number: int,
        *args: Any,  # noqa: ARG001
        **kwargs: Any,  # noqa: ARG001
    ) -> bool:
        logger.warning(
            "Attempt # %d failed with error: %s",
            attempt_number,
            str(error),
            stacklevel=2,
        )
        return True

    return retry_hook

retry

retry(
    errors: tuple[type[Exception], ...],
    retry_hook: (
        decorative_secrets.utilities.RetryHook
        | decorative_secrets.utilities.AsyncRetryHook
    ) = decorative_secrets.utilities._default_retry_hook,
    number_of_attempts: int = 2,
) -> collections.abc.Callable

This is a decorator which will retry a function a specified number of times, with exponential backoff, if it raises one of the specified errors types.

Parameters:

  • errors (tuple[type[Exception], ...]) –

    A tuple of exception types which should trigger a retry.

  • retry_hook (decorative_secrets.utilities.RetryHook | decorative_secrets.utilities.AsyncRetryHook, default: decorative_secrets.utilities._default_retry_hook ) –

    A function which is called with the exception instance (optionally) and an attempt number when an error occurs. If this function returns False, the exception is re-raised and no further retries are attempted.

  • number_of_attempts (int, default: 2 ) –

    The total number of attempts to make, including the initial attempt.

Source code in src/decorative_secrets/utilities.py
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
def retry(  # noqa: C901
    errors: tuple[type[Exception], ...],
    retry_hook: RetryHook | AsyncRetryHook = _default_retry_hook,
    number_of_attempts: int = 2,
) -> Callable:
    """
    This is a decorator which will retry a function a specified
    number of times, with exponential backoff, if it raises one of the
    specified errors types.

    Parameters:
        errors: A tuple of exception types which should trigger a retry.
        retry_hook: A function which is called with the exception instance
            (optionally) and an attempt number when an error occurs. If this
            function returns `False`, the exception is re-raised and no further
            retries are attempted.
        number_of_attempts: The total number of attempts to make, including
            the initial attempt.
    """

    def decorating_function(function: Callable) -> Callable:
        attempt_number: int = 1
        if iscoroutinefunction(function):

            @wraps(function)
            async def wrapper(*args: Any, **kwargs: Any) -> Any:
                nonlocal attempt_number
                if (number_of_attempts - attempt_number) > 0:
                    # If `number_of_attempts` is greater than `attempt_number`,
                    # we have remaining attempts to try, so catch errors.
                    try:
                        return await function(*args, **kwargs)
                    except errors as error:
                        if not (
                            (
                                await retry_hook(  # type: ignore[misc]
                                    error, attempt_number
                                )
                                if len(
                                    inspect.signature(retry_hook).parameters
                                )
                                > 1
                                else await retry_hook(  # type: ignore[misc]
                                    error
                                )
                            )
                            if iscoroutinefunction(retry_hook)
                            else (
                                retry_hook(error, attempt_number)
                                if len(
                                    inspect.signature(retry_hook).parameters
                                )
                                > 1
                                else retry_hook(error)
                            )
                        ):
                            raise
                        await asyncio.sleep(2**attempt_number)
                        attempt_number += 1
                        return await wrapper(*args, **kwargs)
                # This is our last attempt, so just call the function.
                return await function(*args, **kwargs)

        else:

            @wraps(function)
            def wrapper(*args: Any, **kwargs: Any) -> Any:
                nonlocal attempt_number
                if (number_of_attempts - attempt_number) > 0:
                    try:
                        return function(*args, **kwargs)
                    except errors as error:
                        if not (
                            retry_hook(error, attempt_number)
                            if len(inspect.signature(retry_hook).parameters)
                            > 1
                            else retry_hook(error)
                        ):
                            raise
                        sleep(2**attempt_number)
                        attempt_number += 1
                        return wrapper(*args, **kwargs)
                return function(*args, **kwargs)

        return wrapper

    return decorating_function

get_exception_text

get_exception_text() -> str

When called within an exception, this function returns a text representation of the error matching what is found in traceback.print_exception, but is returned as a string value rather than printing.

Source code in src/decorative_secrets/utilities.py
441
442
443
444
445
446
447
448
def get_exception_text() -> str:
    """
    When called within an exception, this function returns a text
    representation of the error matching what is found in
    `traceback.print_exception`, but is returned as a string value rather than
    printing.
    """
    return "".join(format_exception(*sys.exc_info()))