Skip to content

General Cache Flow

flowchart TD
    request <--> load
    request <-->|if delete request|delete <--> find
    request <--> |if get or put request|add <--> find
    request <---> |if patch or post request|update  <--> find
    request <---> |if cache is valid and get request| find

    request[request method] --> valid[\valid?/] --> save
    valid ----> returnn
    save -----> returnl[return list of objects]
    save -----> returnn[return none]
    save -----> returno[return single object]

Press "Alt" / "Option" to enable Pan & Zoom

toggl_api.MissingParentError

Bases: AttributeError

Cache object doesn't have a parent endpoint associated and is being called used.

toggl_api.meta.cache.TogglCache

Bases: ABC, Generic[TC]

Abstract class for caching Toggl API data to disk.

Integrates as the backend for the TogglCachedEndpoint in order to store requested models locally.

PARAMETER DESCRIPTION
path

Location where the cache will be saved.

TYPE: Path | PathLike[str]

expire_after

After how much time should the cache expire. Set to None if no expire_date or to 0 seconds for no caching at all. If using an integer it will be assumed as seconds. If set to None its ignored.

TYPE: timedelta | int | None DEFAULT: None

parent

Endpoint which the cache belongs to. Doesn't need to be set through parameters as it will be auto assigned.

TYPE: TogglCachedEndpoint[TC] | None DEFAULT: None

ATTRIBUTE DESCRIPTION
cache_path

Path to the cache file. Will generate the folder if it does not exist.

TYPE: Path

expire_after

Time after which the cache should be refreshed.

TYPE: timedelta | None

parent

Parent TogglCachedEndpoint

TYPE: TogglCachedEndpoint[TC]

METHOD DESCRIPTION
commit

Commits the cache to disk, database or other form. Method for finalising the cache. Abstract.

load_cache

Loads the cache from disk, database or other form. Abstract.

save_cache

Saves and preforms action depending on request type. Abstract.

find_entry

Looks for a TogglClass in the cache. Abstract.

add_entry

Adds a TogglClass to the cache. Abstract.

update_entry

Updates a TogglClass in the cache. Abstract.

delete_entry

Deletes a TogglClass from the cache. Abstract.

find_method

Matches a RequestMethod to cache functionality.

parent_exist

Validates if the parent has been set. The parent will be generally set by the endpoint when assigned. Abstract.

query

Queries the cache for various varibles. Abstract.

RAISES DESCRIPTION
MissingParentError

If the parent is None and any cache method is being accessed.

toggl_api.asyncio.TogglAsyncCache

Bases: ABC, Generic[T]

Abstract class for caching Toggl API data to disk.

Integrates as the backend for the TogglCachedEndpoint in order to store requested models locally.

PARAMETER DESCRIPTION
path

Location where the cache will be saved.

TYPE: Path | PathLike[str]

expire_after

After how much time should the cache expire. Set to None if no expire_date or to 0 seconds for no caching at all. If using an integer it will be assumed as seconds. If set to None its ignored.

TYPE: timedelta | int | None DEFAULT: None

parent

Endpoint which the cache belongs to. Doesn't need to be set through parameters as it will be auto assigned.

TYPE: TogglAsyncCachedEndpoint[T] | None DEFAULT: None

ATTRIBUTE DESCRIPTION
cache_path

Path to the cache file. Will generate the folder if it does not exist.

TYPE: Path

expire_after

Time after which the cache should be refreshed.

TYPE: timedelta | None

parent

Parent TogglCachedEndpoint

TYPE: TogglAsyncCachedEndpoint[T]

METHOD DESCRIPTION
commit

Commits the cache to disk, database or other form. Method for finalising the cache. Abstract.

load_cache

Loads the cache from disk, database or other form. Abstract.

save_cache

Saves and preforms action depending on request type. Abstract.

find_entry

Looks for a TogglClass in the cache. Abstract.

add_entry

Adds a TogglClass to the cache. Abstract.

update_entry

Updates a TogglClass in the cache. Abstract.

delete_entry

Deletes a TogglClass from the cache. Abstract.

find_method

Matches a RequestMethod to cache functionality.

parent_exist

Validates if the parent has been set. The parent will be generally set by the endpoint when assigned. Abstract.

RAISES DESCRIPTION
MissingParentError

If the parent is None and any cache method is being accessed.


Querying

toggl_api.meta.cache.Comparison

Bases: Enum

Source code in src/toggl_api/meta/cache/_base_cache.py
22
23
24
25
26
27
class Comparison(enum.Enum):
    EQUAL = enum.auto()
    LESS_THEN = enum.auto()
    LESS_THEN_OR_EQUAL = enum.auto()
    GREATER_THEN = enum.auto()
    GREATER_THEN_OR_EQUAL = enum.auto()

toggl_api.meta.cache.TogglQuery dataclass

Bases: Generic[T]

Dataclass for querying cached Toggl models.

ATTRIBUTE DESCRIPTION
key

Name of the target column to compare against.

TYPE: str

value

Value to compare against.

TYPE: T | Sequence[T]

comparison

The way the value should be compared. None 'EQUALS' comparisons for None numeric or time based values.

TYPE: Comparison

Source code in src/toggl_api/meta/cache/_base_cache.py
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
63
64
65
66
67
68
69
@dataclass
class TogglQuery(Generic[T]):
    """Dataclass for querying cached Toggl models."""

    key: str = field()
    """Name of the target column to compare against."""
    value: T | Sequence[T] = field()
    """Value to compare against."""
    comparison: Comparison = field(default=Comparison.EQUAL)
    """The way the value should be compared. None 'EQUALS' comparisons for None numeric or time based values."""

    def __post_init__(self) -> None:
        if not isinstance(self.value, date | int | timedelta) and self.comparison != Comparison.EQUAL:
            msg = "None 'EQUAL' comparisons only available for time or numeric based values."
            raise TypeError(msg)

        if isinstance(self.value, date) and not isinstance(
            self.value,
            datetime,
        ):
            if self.comparison in {
                Comparison.LESS_THEN,
                Comparison.GREATER_THEN_OR_EQUAL,
            }:
                self.value = datetime.combine(  # type: ignore[assignment]
                    self.value,
                    datetime.min.time(),
                    tzinfo=timezone.utc,
                )
            else:
                self.value = datetime.combine(  # type: ignore[assignment]
                    self.value,
                    datetime.max.time(),
                    tzinfo=timezone.utc,
                )

key class-attribute instance-attribute

key: str = field()

Name of the target column to compare against.

value class-attribute instance-attribute

value: T | Sequence[T] = field()

Value to compare against.

comparison class-attribute instance-attribute

comparison: Comparison = field(default=EQUAL)

The way the value should be compared. None 'EQUALS' comparisons for None numeric or time based values.


SQLite

Info

Make sure to install SQLAlchemy if using SqliteCache with pip install toggl-api-wrapper[sqlite]

toggl_api.meta.cache.SqliteCache

Bases: TogglCache[T]

Class for caching data to a SQLite database.

Disconnects database on deletion or exit.

PARAMETER DESCRIPTION
path

Where the SQLite database will be stored. Ignored if engine parameter is not None.

TYPE: Path | PathLike[str]

expire_after

Time after which the cache should be refreshed. If using an integer it will be assumed as seconds. If set to None the cache will never expire.

TYPE: timedelta | int | None DEFAULT: None

parent

Parent endpoint that will use the cache. Assigned automatically when supplied to a cached endpoint.

TYPE: TogglCachedEndpoint[T] | None DEFAULT: None

engine

Supply an existing database engine or otherwise one is created. This may be used to supply an entirely different DB, but SQLite is the one that is tested & supported.

TYPE: Engine | None DEFAULT: None

ATTRIBUTE DESCRIPTION
expire_after

Time after which the cache should be refreshed.

TYPE: timedelta | None

database

Sqlalchemy database engine.

metadata

Sqlalchemy metadata.

session

Sqlalchemy session.

METHOD DESCRIPTION
load_cache

Loads the data from disk and stores it in the data attribute. Invalidates any entries older than expire argument.

query

Querying method that uses SQL to query cached objects.

Source code in src/toggl_api/meta/cache/_sqlite_cache.py
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 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
153
154
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
199
200
201
202
203
204
205
206
207
208
@_requires("sqlalchemy")
class SqliteCache(TogglCache[T]):
    """Class for caching data to a SQLite database.

    Disconnects database on deletion or exit.

    Params:
        path: Where the SQLite database will be stored.
            Ignored if `engine` parameter is not None.
        expire_after: Time after which the cache should be refreshed.
            If using an integer it will be assumed as seconds.
            If set to None the cache will never expire.
        parent: Parent endpoint that will use the cache. Assigned
            automatically when supplied to a cached endpoint.
        engine: Supply an existing database engine or otherwise one is created.
            This may be used to supply an entirely different DB, but SQLite is
            the one that is tested & supported.

    Attributes:
        expire_after: Time after which the cache should be refreshed.
        database: Sqlalchemy database engine.
        metadata: Sqlalchemy metadata.
        session: Sqlalchemy session.

    Methods:
        load_cache: Loads the data from disk and stores it in the data
            attribute. Invalidates any entries older than expire argument.
        query: Querying method that uses SQL to query cached objects.
    """

    __slots__ = ("database", "metadata", "session")

    def __init__(
        self,
        path: Path | PathLike[str],
        expire_after: timedelta | int | None = None,
        parent: TogglCachedEndpoint[T] | None = None,
        *,
        engine: Engine | None = None,
    ) -> None:
        super().__init__(path, expire_after, parent)
        self.database = engine or db.create_engine(
            f"sqlite:///{self.cache_path}",
        )
        self.metadata = register_tables(self.database)

        self.session = Session(self.database)
        atexit.register(self.session.close)

    def commit(self) -> None:
        self.session.commit()

    def load(self) -> Query[T]:
        query = self.session.query(self.model)
        if self.expire_after is not None:
            min_ts = datetime.now(timezone.utc) - self.expire_after
            query.filter(
                cast("ColumnElement[bool]", self.model.timestamp > min_ts),
            )
        return query

    def add(self, *entries: T) -> None:
        for item in entries:
            if self.find(item):
                self.update(item)
                continue
            self.session.add(item)
        self.commit()

    def update(self, *entries: T) -> None:
        for item in entries:
            self.session.merge(item)
        self.commit()

    def delete(self, *entries: T) -> None:
        for entry in entries:
            if self.find(entry):
                self.session.query(
                    self.model,
                ).filter_by(id=entry.id).delete()
        self.commit()

    def find(self, query: T | dict[str, Any]) -> T | None:
        if isinstance(query, TogglClass):
            query = {"id": query.id}

        search = self.session.query(self.model)
        if self._expire_after is not None:
            min_ts = datetime.now(timezone.utc) - self._expire_after
            search = search.filter(
                cast("ColumnElement[bool]", self.model.timestamp > min_ts),
            )
        return search.filter_by(**query).first()

    def query(
        self,
        *query: TogglQuery[Any],
        distinct: bool = False,
    ) -> Query[T]:
        """Query method for filtering models from cache.

        Filters cached model by set of supplied queries.

        Supports queries with various comparisons with the [Comparison][toggl_api.meta.cache.Comparison]
        enumeration.

        Args:
            query: Any positional argument that is used becomes query argument.
            distinct: Whether to keep equivalent values around.

        Returns:
            A SQLAlchemy query object with parameters filtered.
        """
        search = self.session.query(self.model)
        if isinstance(self.expire_after, timedelta):
            min_ts = datetime.now(timezone.utc) - self.expire_after
            search = search.filter(
                cast("ColumnElement[bool]", self.model.timestamp > min_ts),
            )

        search = self._query_helper(list(query), search)
        if distinct:
            data = [q.key for q in query]
            with warnings.catch_warnings():
                warnings.simplefilter("ignore", DeprecationWarning)
                search = search.distinct(*data).group_by(*data)  # type: ignore[arg-type] # FIX: Remove and use selec tobject instead.
        return search

    def _query_helper(
        self,
        query: list[TogglQuery[Any]],
        query_obj: Query[T],
    ) -> Query[T]:
        if query:
            query_obj = self._match_query(query.pop(0), query_obj)
            return self._query_helper(query, query_obj)
        return query_obj

    def _match_query(
        self,
        query: TogglQuery[Any],
        query_obj: Query[T],
    ) -> Query[T]:
        value = getattr(self.model, query.key)
        if query.comparison == Comparison.EQUAL:
            if isinstance(query.value, Sequence) and not isinstance(
                query.value,
                str,
            ):
                return query_obj.filter(value.in_(query.value))
            return query_obj.filter(value == query.value)
        if query.comparison == Comparison.LESS_THEN:
            return query_obj.filter(value < query.value)
        if query.comparison == Comparison.LESS_THEN_OR_EQUAL:
            return query_obj.filter(value <= query.value)
        if query.comparison == Comparison.GREATER_THEN:
            return query_obj.filter(value > query.value)
        if query.comparison == Comparison.GREATER_THEN_OR_EQUAL:
            return query_obj.filter(value >= query.value)
        msg = f"{query.comparison} is not implemented!"
        raise NotImplementedError(msg)

    @property
    def cache_path(self) -> Path:
        return super().cache_path / "cache.sqlite"

    def __del__(self) -> None:
        with contextlib.suppress(AttributeError, TypeError):
            self.session.close()

query

query(*query: TogglQuery[Any], distinct: bool = False) -> Query[T]

Query method for filtering models from cache.

Filters cached model by set of supplied queries.

Supports queries with various comparisons with the Comparison enumeration.

PARAMETER DESCRIPTION
query

Any positional argument that is used becomes query argument.

TYPE: TogglQuery[Any] DEFAULT: ()

distinct

Whether to keep equivalent values around.

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
Query[T]

A SQLAlchemy query object with parameters filtered.

Source code in src/toggl_api/meta/cache/_sqlite_cache.py
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
def query(
    self,
    *query: TogglQuery[Any],
    distinct: bool = False,
) -> Query[T]:
    """Query method for filtering models from cache.

    Filters cached model by set of supplied queries.

    Supports queries with various comparisons with the [Comparison][toggl_api.meta.cache.Comparison]
    enumeration.

    Args:
        query: Any positional argument that is used becomes query argument.
        distinct: Whether to keep equivalent values around.

    Returns:
        A SQLAlchemy query object with parameters filtered.
    """
    search = self.session.query(self.model)
    if isinstance(self.expire_after, timedelta):
        min_ts = datetime.now(timezone.utc) - self.expire_after
        search = search.filter(
            cast("ColumnElement[bool]", self.model.timestamp > min_ts),
        )

    search = self._query_helper(list(query), search)
    if distinct:
        data = [q.key for q in query]
        with warnings.catch_warnings():
            warnings.simplefilter("ignore", DeprecationWarning)
            search = search.distinct(*data).group_by(*data)  # type: ignore[arg-type] # FIX: Remove and use selec tobject instead.
    return search

toggl_api.models.register_tables

register_tables(engine: Engine) -> MetaData

Register all Toggl dataclasses to the database.

Examples:

from sqlalchemy import create_engine
from toggl_api.models import register_tables

engine = create_engine("sqlite:///database.sqlite")
metadata = register_tables(engine)
PARAMETER DESCRIPTION
engine

An SQLAlchemy Engine connected to a database.

TYPE: Engine

RETURNS DESCRIPTION
MetaData

MetaData instance with all the info about the registered tables.

Source code in src/toggl_api/models/_schema.py
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
@_requires("sqlalchemy")
def register_tables(engine: Engine) -> MetaData:
    """Register all Toggl dataclasses to the database.

    Examples:
        ```py
        from sqlalchemy import create_engine
        from toggl_api.models import register_tables

        engine = create_engine("sqlite:///database.sqlite")
        metadata = register_tables(engine)
        ```

    Args:
        engine: An SQLAlchemy `Engine` connected to a database.

    Returns:
        `MetaData` instance with all the info about the registered tables.
    """
    metadata = MetaData()

    _create_mappings(metadata)

    metadata.create_all(engine)

    return metadata

toggl_api.asyncio.AsyncSqliteCache

Bases: TogglAsyncCache[T]

Class for caching data to a SQLite database.

Disconnects database on deletion or exit.

PARAMETER DESCRIPTION
path

Where the SQLite database will be stored. Ignored if engine parameter is not None.

TYPE: Path | PathLike[str]

expire_after

Time after which the cache should be refreshed. If using an integer it will be assumed as seconds. If set to None the cache will never expire.

TYPE: timedelta | int | None DEFAULT: None

parent

Parent endpoint that will use the cache. Assigned automatically when supplied to a cached endpoint.

TYPE: TogglAsyncCachedEndpoint[T] | None DEFAULT: None

engine

Supply an existing database engine or otherwise one is created. This may be used to supply an entirely different DB, but SQLite is the one that is tested & supported.

TYPE: AsyncEngine | None DEFAULT: None

echo_db

Turns on database logging for debugging.

TYPE: bool DEFAULT: False

ATTRIBUTE DESCRIPTION
expire_after

Time after which the cache should be refreshed.

TYPE: timedelta | None

database

Async database engine that created or can also be supplied.

metadata

Metadata generated on object construction.

TYPE: MetaData

METHOD DESCRIPTION
load

Loads and expires entries from the database.

add

Adds new entries to the database.

update

Updates one or multiple existing entries.

delete

Deletes one or multiple existing entries.

find

Find an existing entry.

Source code in src/toggl_api/asyncio/_async_sqlite_cache.py
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 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
153
154
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
class AsyncSqliteCache(TogglAsyncCache[T]):
    """Class for caching data to a SQLite database.

    Disconnects database on deletion or exit.

    Params:
        path: Where the SQLite database will be stored.
            Ignored if `engine` parameter is not None.
        expire_after: Time after which the cache should be refreshed.
            If using an integer it will be assumed as seconds.
            If set to None the cache will never expire.
        parent: Parent endpoint that will use the cache. Assigned
            automatically when supplied to a cached endpoint.
        engine: Supply an existing database engine or otherwise one is created.
            This may be used to supply an entirely different DB, but SQLite is
            the one that is tested & supported.
        echo_db: Turns on database logging for debugging.

    Attributes:
        expire_after: Time after which the cache should be refreshed.
        database: Async database engine that created or can also be supplied.
        metadata: Metadata generated on object construction.

    Methods:
        load: Loads and expires entries from the database.
        add: Adds new entries to the database.
        update: Updates one or multiple existing entries.
        delete: Deletes one or multiple existing entries.
        find: Find an existing entry.
    """

    __slots__ = ("database", "metadata")

    metadata: MetaData

    def __init__(
        self,
        path: Path | PathLike[str],
        expire_after: timedelta | int | None = None,
        parent: TogglAsyncCachedEndpoint[T] | None = None,
        *,
        engine: AsyncEngine | None = None,
        echo_db: bool = False,
    ) -> None:
        super().__init__(path, expire_after, parent)
        self.database = engine = engine or create_async_engine(
            f"sqlite+aiosqlite:///{self.cache_path}",
        )
        self.database.echo = echo_db

        # NOTE: Tests for an existing loop otherwise gets/creates a new one.
        try:
            asyncio.get_running_loop()
            task = asyncio.create_task(async_register_tables(engine))
            task.add_done_callback(
                lambda x: setattr(self, "metadata", x.result()),
            )
        except RuntimeError:
            self.metadata = asyncio.run(async_register_tables(engine))

    async def load(self) -> list[T]:
        """Load data from the database, discarding items if they are past expiration.

        Rather crude load method as it will load all items into memory.

        Returns:
            A flattend list of models from the database.
        """
        stmt = select(self.model)
        if self.expire_after is not None:
            # TODO: Routine that checks for expiration
            # discards instead of ignoring on load.
            min_ts = datetime.now(timezone.utc) - self.expire_after
            stmt = stmt.filter(
                cast("ColumnElement[bool]", self.model.timestamp > min_ts),
            )

        async with AsyncSession(
            self.database,
            expire_on_commit=False,
        ) as session:
            return list(
                chain.from_iterable((await session.execute(stmt)).fetchall()),
            )

    async def add(self, *entries: T) -> None:
        """Add multiple entries to the database."""
        await self.update(*entries)

    async def update(self, *entries: T) -> None:
        """Update entries in the database."""
        async with AsyncSession(
            self.database,
            expire_on_commit=False,
        ) as session:
            for entry in entries:
                await session.merge(entry)
            await session.commit()

    async def delete(self, *entries: T) -> None:
        """Delete multiple entries in the database."""
        async with AsyncSession(
            self.database,
            expire_on_commit=False,
        ) as session:
            for entry in entries:
                if new := await session.get(self.model, entry.id):
                    await session.delete(new)
            await session.commit()

    async def find(self, pk: int) -> T | None:
        """Find a model based on a primary key.

        Args:
            pk: Primary integer key of the model.

        Returns:
            The found model or None if not found.
        """
        async with AsyncSession(
            self.database,
            expire_on_commit=False,
        ) as session:
            return await session.get(self.model, pk)

    @property
    def cache_path(self) -> Path:
        """Full path to the SQLLite database."""
        return super().cache_path / "cache.sqlite"

cache_path property

cache_path: Path

Full path to the SQLLite database.

load async

load() -> list[T]

Load data from the database, discarding items if they are past expiration.

Rather crude load method as it will load all items into memory.

RETURNS DESCRIPTION
list[T]

A flattend list of models from the database.

Source code in src/toggl_api/asyncio/_async_sqlite_cache.py
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
async def load(self) -> list[T]:
    """Load data from the database, discarding items if they are past expiration.

    Rather crude load method as it will load all items into memory.

    Returns:
        A flattend list of models from the database.
    """
    stmt = select(self.model)
    if self.expire_after is not None:
        # TODO: Routine that checks for expiration
        # discards instead of ignoring on load.
        min_ts = datetime.now(timezone.utc) - self.expire_after
        stmt = stmt.filter(
            cast("ColumnElement[bool]", self.model.timestamp > min_ts),
        )

    async with AsyncSession(
        self.database,
        expire_on_commit=False,
    ) as session:
        return list(
            chain.from_iterable((await session.execute(stmt)).fetchall()),
        )

add async

add(*entries: T) -> None

Add multiple entries to the database.

Source code in src/toggl_api/asyncio/_async_sqlite_cache.py
140
141
142
async def add(self, *entries: T) -> None:
    """Add multiple entries to the database."""
    await self.update(*entries)

update async

update(*entries: T) -> None

Update entries in the database.

Source code in src/toggl_api/asyncio/_async_sqlite_cache.py
144
145
146
147
148
149
150
151
152
async def update(self, *entries: T) -> None:
    """Update entries in the database."""
    async with AsyncSession(
        self.database,
        expire_on_commit=False,
    ) as session:
        for entry in entries:
            await session.merge(entry)
        await session.commit()

delete async

delete(*entries: T) -> None

Delete multiple entries in the database.

Source code in src/toggl_api/asyncio/_async_sqlite_cache.py
154
155
156
157
158
159
160
161
162
163
async def delete(self, *entries: T) -> None:
    """Delete multiple entries in the database."""
    async with AsyncSession(
        self.database,
        expire_on_commit=False,
    ) as session:
        for entry in entries:
            if new := await session.get(self.model, entry.id):
                await session.delete(new)
        await session.commit()

find async

find(pk: int) -> T | None

Find a model based on a primary key.

PARAMETER DESCRIPTION
pk

Primary integer key of the model.

TYPE: int

RETURNS DESCRIPTION
T | None

The found model or None if not found.

Source code in src/toggl_api/asyncio/_async_sqlite_cache.py
165
166
167
168
169
170
171
172
173
174
175
176
177
178
async def find(self, pk: int) -> T | None:
    """Find a model based on a primary key.

    Args:
        pk: Primary integer key of the model.

    Returns:
        The found model or None if not found.
    """
    async with AsyncSession(
        self.database,
        expire_on_commit=False,
    ) as session:
        return await session.get(self.model, pk)

toggl_api.asyncio.async_register_tables async

async_register_tables(engine: AsyncEngine) -> MetaData

Set up the database with SQLAlchemy models.

PARAMETER DESCRIPTION
engine

The engine to use when registering tables.

TYPE: AsyncEngine

RETURNS DESCRIPTION
MetaData

Engine metadata with the table implemented.

Source code in src/toggl_api/asyncio/_async_sqlite_cache.py
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
async def async_register_tables(engine: AsyncEngine) -> MetaData:
    """Set up the database with SQLAlchemy models.

    Args:
        engine: The engine to use when registering tables.

    Returns:
        Engine metadata with the table implemented.

    """
    meta = MetaData()

    _create_mappings(meta)

    async with engine.begin() as conn:
        await conn.run_sync(meta.create_all)

    return meta

JSON

toggl_api.meta.cache.JSONSession dataclass

Bases: Generic[T]

Data structure for storing JSON in memory.

Similar to a SQL session as its meant to have the same/similar interface.

This dataclass doesn't require interaction from the library user and will be created in the json cache object.

Examples:

>>> cache = JSONSession(max_length=5000)
PARAMETER DESCRIPTION
max_length

Max length of the data to be stored.

TYPE: int DEFAULT: 10000

ATTRIBUTE DESCRIPTION
max_length

Max length of the data to be stored.

TYPE: int

version

Version of the data structure.

TYPE: str

data

List of Toggl objects stored in memory.

TYPE: list[T]

modified

Timestamp of when the cache was last modified in nanoseconds. Used for checking if another cache object has updated it recently.

TYPE: int

METHOD DESCRIPTION
save

Saves the data to a JSON file. Setting current timestamp and version.

load

Loads the data from disk and stores it in the data attribute. Invalidates any entries older than expire argument.

refresh

Utility method that checks if cache has been updated.

process_data

Processes models according to set attributes.

Source code in src/toggl_api/meta/cache/_json_cache.py
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 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
@dataclass
class JSONSession(Generic[T]):
    """Data structure for storing JSON in memory.

    Similar to a SQL session as its meant to have the same/similar interface.

    This dataclass doesn't require interaction from the library user and will
    be created in the json cache object.

    Examples:
        >>> cache = JSONSession(max_length=5000)

    Params:
        max_length: Max length of the data to be stored.

    Attributes:
        max_length: Max length of the data to be stored.
        version: Version of the data structure.
        data: List of Toggl objects stored in memory.
        modified: Timestamp of when the cache was last modified in nanoseconds.
            Used for checking if another cache object has updated it recently.

    Methods:
        save: Saves the data to a JSON file. Setting current timestamp and
            version.
        load: Loads the data from disk and stores it in the data attribute.
            Invalidates any entries older than expire argument.
        refresh: Utility method that checks if cache has been updated.
        process_data: Processes models according to set attributes.
    """

    max_length: int = field(default=10_000)
    version: str = field(init=False, default=__version__)
    data: list[T] = field(default_factory=list)
    modified: int = field(init=False, default=0)

    def refresh(self, path: Path) -> bool:
        if path.exists() and path.stat().st_mtime_ns > self.modified:
            self.modified = path.stat().st_mtime_ns
            self.data = self._diff(self._load(path)["data"], self.modified)
            return True
        return False

    def _save(self, path: Path, data: dict[str, Any]) -> None:
        with path.open("w", encoding="utf-8") as f:
            json.dump(data, f, cls=CustomEncoder)

    def commit(self, path: Path) -> None:
        self.refresh(path)
        self.version = __version__
        data = {
            "version": self.version,
            "data": self.process_data(self.data),
        }
        self._save(path, data)

        self.modified = path.stat().st_mtime_ns

    def _diff(self, comp: list[T], mtime: int) -> list[T]:
        old_models = {m.id: m for m in self.data}
        new_models = {m.id: m for m in comp}

        model_ids: set[int] = set(old_models)
        model_ids.update(new_models)

        new_data: list[T] = []
        for mid in model_ids:
            old = old_models.get(mid)
            new = new_models.get(mid)
            if (old is None and new is not None) or (new and old and new.timestamp >= old.timestamp):
                new_data.append(new)
            elif old and old.timestamp.timestamp() * 10**9 >= mtime:
                new_data.append(old)

        return new_data

    def _load(self, path: Path) -> dict[str, Any]:
        with path.open("r", encoding="utf-8") as f:
            return cast("dict[str, Any]", json.load(f, cls=CustomDecoder))

    def load(self, path: Path) -> None:
        if path.exists():
            data = self._load(path)
            self.modified = path.stat().st_mtime_ns
            self.version = data["version"]
            self.data = self.process_data(data["data"])
        else:
            self.version = __version__
            self.modified = time.time_ns()

    def process_data(self, data: list[T]) -> list[T]:
        data.sort(key=lambda x: x.timestamp or datetime.now(timezone.utc))
        return data[: self.max_length]

toggl_api.meta.cache.JSONCache

Bases: TogglCache[T]

Class for caching Toggl data to disk in JSON format.

Examples:

>>> JSONCache(Path("cache"))
>>> cache = JSONCache(Path("cache"), 3600)
>>> cache = JSONCache(Path("cache"), timedelta(weeks=2))
>>> tracker_endpoint = TrackerEndpoint(231231, BasicAuth(...), cache)
PARAMETER DESCRIPTION
path

Path to the cache file.

TYPE: Path | PathLike[str]

expire_after

Time after which the cache should be refreshed. If using an integer it will be assumed as seconds. If set to None the cache will never expire.

TYPE: timedelta | int | None DEFAULT: None

parent

Parent endpoint that will use the cache. Assigned automatically when supplied to a cached endpoint.

TYPE: TogglCachedEndpoint[T] | None DEFAULT: None

max_length

Max length list of the data to be stored permanently.

TYPE: int DEFAULT: 10000

ATTRIBUTE DESCRIPTION
expire_after

Time after which the cache should be refreshed.

TYPE: timedelta | None

session(JSONSession)

Store the current json data in memory while handling the cache.

TYPE: timedelta | None

METHOD DESCRIPTION
commit

Wrapper for JSONSession.save() that saves the current json data to disk.

save

Saves the given data to the cache. Takes a list of Toggl objects or a single Toggl object as an argument and process the change before saving.

load

Loads the data from the cache and returns the data to the caller discarding expired entries.

Source code in src/toggl_api/meta/cache/_json_cache.py
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
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
199
200
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
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
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
318
319
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
class JSONCache(TogglCache[T]):
    """Class for caching Toggl data to disk in JSON format.

    Examples:
        >>> JSONCache(Path("cache"))

        >>> cache = JSONCache(Path("cache"), 3600)

        >>> cache = JSONCache(Path("cache"), timedelta(weeks=2))
        >>> tracker_endpoint = TrackerEndpoint(231231, BasicAuth(...), cache)

    Params:
        path: Path to the cache file.
        expire_after: Time after which the cache should be refreshed.
            If using an integer it will be assumed as seconds.
            If set to None the cache will never expire.
        parent: Parent endpoint that will use the cache. Assigned automatically
            when supplied to a cached endpoint.
        max_length: Max length list of the data to be stored permanently.

    Attributes:
        expire_after: Time after which the cache should be refreshed.
        session(JSONSession): Store the current json data in memory while
            handling the cache.

    Methods:
        commit: Wrapper for JSONSession.save() that saves the current json data
            to disk.
        save: Saves the given data to the cache. Takes a list of Toggl
            objects or a single Toggl object as an argument and process the
            change before saving.
        load: Loads the data from the cache and returns the data to the
            caller discarding expired entries.
    """

    __slots__ = ("session",)

    def __init__(
        self,
        path: Path | PathLike[str],
        expire_after: timedelta | int | None = None,
        parent: TogglCachedEndpoint[T] | None = None,
        *,
        max_length: int = 10_000,
    ) -> None:
        super().__init__(path, expire_after, parent)
        self.session: JSONSession[T] = JSONSession(max_length=max_length)

    def commit(self) -> None:
        log.debug("Saving cache to disk!")
        self.session.commit(self.cache_path)

    def save(self, update: Iterable[T] | T, method: RequestMethod) -> None:
        self.session.refresh(self.cache_path)
        super().save(update, method)

    def load(self) -> list[T]:
        self.session.load(self.cache_path)
        if self.expire_after is None:
            return self.session.data
        min_ts = datetime.now(timezone.utc) - self.expire_after
        return [m for m in self.session.data if m.timestamp >= min_ts]

    def find(self, entry: T | dict[str, int], **_kwargs: Any) -> T | None:
        self.session.refresh(self.cache_path)
        if not self.session.data:
            return None
        for item in self.session.data:
            if item is not None and item["id"] == entry["id"] and isinstance(item, self.model):
                return item
        return None

    def _add_entry(self, item: T) -> None:
        find_entry = self.find(item)
        if find_entry is None:
            return self.session.data.append(item)
        index = self.session.data.index(find_entry)
        item.timestamp = datetime.now(timezone.utc)
        self.session.data[index] = item
        return None

    def add(self, *entries: T) -> None:
        for entry in entries:
            self._add_entry(entry)

    def update(self, *entries: T) -> None:
        self.add(*entries)

    def _delete_entry(self, entry: T) -> None:
        find_entry = self.find(entry)
        if not find_entry:
            return
        index = self.session.data.index(find_entry)
        self.session.data.pop(index)

    def delete(self, *entries: T) -> None:
        for entry in entries:
            self._delete_entry(entry)

    def query(
        self,
        *query: TogglQuery[Any],
        distinct: bool = False,
    ) -> list[T]:
        """Query method for filtering Toggl objects from cache.

        Filters cached Toggl objects by set of supplied queries.

        Supports queries with various comparisons with the [Comparison][toggl_api.meta.cache.Comparison]
        enumeration.

        Args:
            query: Any positional argument that is used becomes query argument.
            distinct: Whether to keep the same values around. This doesn't work
                with unhashable fields such as lists.

        Returns:
            A list of models with the query parameters that matched.
        """
        log.debug(
            "Querying cache with %s parameters.",
            len(query),
            extra={"query": query},
        )

        min_ts = datetime.now(timezone.utc) - self.expire_after if self.expire_after else None
        self.session.load(self.cache_path)
        search = self.session.data
        existing: defaultdict[str, set[Any]] = defaultdict(set)

        return [
            model
            for model in search
            if self._query_helper(
                model,
                query,
                existing,
                min_ts,
                distinct=distinct,
            )
        ]

    def _query_helper(
        self,
        model: T,
        queries: tuple[TogglQuery[Any], ...],
        existing: dict[str, set[Any]],
        min_ts: datetime | None,
        *,
        distinct: bool,
    ) -> bool:
        if self.expire_after and min_ts and model.timestamp and min_ts >= model.timestamp:
            return False

        for query in queries:
            if (
                distinct and not isinstance(query.value, list) and model[query.key] in existing[query.key]
            ) or not self._match_query(model, query):
                return False

        if distinct:
            for query in queries:
                value = model[query.key]
                if isinstance(value, Hashable):
                    existing[query.key].add(value)

        return True

    @staticmethod
    def _match_equal(model: T, query: TogglQuery[Any]) -> bool:
        if isinstance(query.value, Sequence) and not isinstance(
            query.value,
            str,
        ):
            value = model[query.key]

            if isinstance(value, Sequence) and not isinstance(value, str):
                return any(v == comp for comp in query.value for v in value)

            return any(value == comp for comp in query.value)

        return bool(model[query.key] == query.value)

    @staticmethod
    def _match_query(model: T, query: TogglQuery[Any]) -> bool:
        if query.comparison == Comparison.EQUAL:
            return JSONCache._match_equal(model, query)
        if query.comparison == Comparison.LESS_THEN:
            return bool(model[query.key] < query.value)
        if query.comparison == Comparison.LESS_THEN_OR_EQUAL:
            return bool(model[query.key] <= query.value)
        if query.comparison == Comparison.GREATER_THEN:
            return bool(model[query.key] > query.value)
        if query.comparison == Comparison.GREATER_THEN_OR_EQUAL:
            return bool(model[query.key] >= query.value)
        msg = f"{query.comparison} is not implemented!"
        raise NotImplementedError(msg)

    @property
    def cache_path(self) -> Path:
        if self.parent is None:
            return self._cache_path / "cache.json"
        return self._cache_path / f"cache_{self.model.__tablename__}.json"

    @property
    def parent(self) -> TogglCachedEndpoint[T]:
        return super().parent

    @parent.setter
    def parent(self, parent: TogglCachedEndpoint[T] | None) -> None:
        self._parent = parent
        if parent is not None:
            self.session.load(self.cache_path)

query

query(*query: TogglQuery[Any], distinct: bool = False) -> list[T]

Query method for filtering Toggl objects from cache.

Filters cached Toggl objects by set of supplied queries.

Supports queries with various comparisons with the Comparison enumeration.

PARAMETER DESCRIPTION
query

Any positional argument that is used becomes query argument.

TYPE: TogglQuery[Any] DEFAULT: ()

distinct

Whether to keep the same values around. This doesn't work with unhashable fields such as lists.

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
list[T]

A list of models with the query parameters that matched.

Source code in src/toggl_api/meta/cache/_json_cache.py
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
def query(
    self,
    *query: TogglQuery[Any],
    distinct: bool = False,
) -> list[T]:
    """Query method for filtering Toggl objects from cache.

    Filters cached Toggl objects by set of supplied queries.

    Supports queries with various comparisons with the [Comparison][toggl_api.meta.cache.Comparison]
    enumeration.

    Args:
        query: Any positional argument that is used becomes query argument.
        distinct: Whether to keep the same values around. This doesn't work
            with unhashable fields such as lists.

    Returns:
        A list of models with the query parameters that matched.
    """
    log.debug(
        "Querying cache with %s parameters.",
        len(query),
        extra={"query": query},
    )

    min_ts = datetime.now(timezone.utc) - self.expire_after if self.expire_after else None
    self.session.load(self.cache_path)
    search = self.session.data
    existing: defaultdict[str, set[Any]] = defaultdict(set)

    return [
        model
        for model in search
        if self._query_helper(
            model,
            query,
            existing,
            min_ts,
            distinct=distinct,
        )
    ]