Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions todoist_api_python/_core/http_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from requests.status_codes import codes

from todoist_api_python._core.http_headers import create_headers
from todoist_api_python._core.utils import log_calls

if TYPE_CHECKING:
from requests import Session
Expand All @@ -22,7 +23,7 @@

T = TypeVar("T")


@log_calls
def get(
session: Session,
url: str,
Expand All @@ -45,7 +46,7 @@ def get(
response.raise_for_status()
return cast("T", response.ok)


@log_calls
def post(
session: Session,
url: str,
Expand Down Expand Up @@ -73,7 +74,7 @@ def post(
response.raise_for_status()
return cast("T", response.ok)


@log_calls
def delete(
session: Session,
url: str,
Expand Down
57 changes: 57 additions & 0 deletions todoist_api_python/_core/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
import asyncio
import sys
import uuid
import inspect
import logging
from functools import wraps
from datetime import date, datetime, timezone
from typing import TYPE_CHECKING, TypeVar, cast

Expand Down Expand Up @@ -75,3 +78,57 @@ def parse_datetime(datetime_str: str) -> datetime:
def default_request_id_fn() -> str:
"""Generate random UUIDv4s as the default request ID."""
return str(uuid.uuid4())


def log_calls(func: Callable[..., T]) -> Callable[..., T]:
"""
Decorator to log calls to a callable. Arguments and returned values are included in the log
"""
# Create a 'call count' variable to differentiate between multiple calls to the same function
# Useful for recursion
func._call_count = 0

# Use inspect to get the module of the caller and the appropriate logger
funcs_module = inspect.getmodule(func)
logger = logging.getLogger(funcs_module.__name__ if funcs_module else "")
logger.debug(f"Wrapping function {func.__name__} with logger {logger}")

# Create the wrapped function which logs on function entry, with the arguments
# and on exit, with the return value
# Function calls are numbered to make the log file easier to search
@wraps(func)
def wrapper(*args, **kwargs):
func._call_count += 1
logger.debug(f"Call to function {func.__name__} (#{func._call_count}): Entering with args={args}, kwargs={kwargs}")
result = func(*args, **kwargs)
logger.debug(f"Call to function {func.__name__} (#{func._call_count}): Exiting with result={result}")
return result

return wrapper


def log_method_calls(
exclude_properties: bool = True, exclude_private: bool = True, exclude_dunder: bool = True
) -> Callable[[type], type]:
"""
Class decorator to log calls to all methods of a class. Arguments and returned values are
included in the log.

Args:
exclude_dunder (bool): If True, exclude dunder methods (methods with names starting and
ending with '__') from logging. Default is True.
"""
def class_decorator(cls: type) -> type:
for attr_name, attr_value in cls.__dict__.items():
if callable(attr_value):
if exclude_dunder and attr_name.startswith("__") and attr_name.endswith("__"):
continue
if exclude_private and attr_name.startswith("_"):
continue
if exclude_properties and isinstance(getattr(cls, attr_name, None), property):
continue
decorated_attr = log_calls(attr_value)
setattr(cls, attr_name, decorated_attr)
return cls

return class_decorator
2 changes: 2 additions & 0 deletions todoist_api_python/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
default_request_id_fn,
format_date,
format_datetime,
log_method_calls
)
from todoist_api_python.models import (
Attachment,
Expand Down Expand Up @@ -84,6 +85,7 @@
ViewStyle = Annotated[str, Predicate(lambda x: x in ("list", "board", "calendar"))]


@log_method_calls()
class TodoistAPI:
"""
Client for the Todoist API.
Expand Down