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, ValueError

Raised when a cache object doesn't have a parent and is being called.

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.

Parameters:

  • path (Path) –

    Location where the cache will be saved.

  • expire_after (Optional[timedelta | int], default: None ) –

    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.

  • parent (Optional[TogglCachedEndpoint], default: None ) –

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

Attributes:

  • cache_path (Path) –

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

  • expire_after (timedelta | None) –

    Time after which the cache should be refreshed.

  • parent (TogglCachedEndpoint[TC]) –

    Parent TogglCachedEndpoint

Methods:

  • 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:


Querying

toggl_api.Comparison

Bases: Enum

Source code in toggl_api/meta/cache/base_cache.py
32
33
34
35
36
37
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.TogglQuery dataclass

Bases: Generic[T]

Dataclass for querying cached Toggl models.

Source code in toggl_api/meta/cache/base_cache.py
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
@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: str = field() class-attribute instance-attribute

Name of the target column to compare against.

value: T | Sequence[T] = field() class-attribute instance-attribute

Value to compare against.

comparison: Comparison = field(default=Comparison.EQUAL) class-attribute instance-attribute

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.sqlite_cache.SqliteCache

Bases: TogglCache[T]

Class for caching data to a SQLite database.

Disconnects database on deletion or exit.

Parameters:

  • expire_after (Optional[timedelta | int], default: None ) –

    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 (Optional[TogglCachedEndpoint[T]], default: None ) –

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

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.

Source code in toggl_api/meta/cache/sqlite_cache.py
 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
 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
@_requires("sqlalchemy")
class SqliteCache(TogglCache[T]):
    """Class for caching data to a SQLite database.

    Disconnects database on deletion or exit.

    Params:
        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.

    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,
        expire_after: Optional[timedelta | int] = None,
        parent: Optional[TogglCachedEndpoint[T]] = None,
    ) -> None:
        super().__init__(path, expire_after, parent)
        self.database = 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 save_cache(self, entry: list[T] | T, method: RequestMethod) -> None:
        func = self.find_method(method)
        if func is None:
            return
        func(entry)

    def load_cache(self) -> Query[T]:
        query = self.session.query(self.parent.model)
        if self.expire_after is not None:
            min_ts = datetime.now(timezone.utc) - self.expire_after
            query.filter(self.parent.model.timestamp > min_ts)  # type: ignore[arg-type]
        return query

    def add_entries(self, entry: Iterable[T] | T) -> None:
        if not isinstance(entry, Iterable):
            entry = (entry,)

        for item in entry:
            if self.find_entry(item):
                self.update_entries(item)
                continue
            self.session.add(item)
        self.commit()

    def update_entries(self, entry: Iterable[T] | T) -> None:
        if not isinstance(entry, Iterable):
            entry = (entry,)

        for item in entry:
            self.session.merge(item)
        self.commit()

    def delete_entries(self, entry: Iterable[T] | T) -> None:
        if not isinstance(entry, Iterable):
            entry = (entry,)

        for item in entry:
            if self.find_entry(item):
                self.session.query(
                    self.parent.model,  # type: ignore[union-attr]
                ).filter_by(id=item.id).delete()
        self.commit()

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

        search = self.session.query(self.parent.model)
        if self._expire_after is not None:
            min_ts = datetime.now(timezone.utc) - self._expire_after
            search = search.filter(self.parent.model.timestamp > min_ts)  # type: ignore[arg-type]
        return search.filter_by(**query).first()

    def query(self, *query: TogglQuery, 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.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.parent.model)
        if isinstance(self.expire_after, timedelta):
            min_ts = datetime.now(timezone.utc) - self.expire_after
            search = search.filter(self.parent.model.timestamp > min_ts)  # type: ignore[arg-type]

        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]
        return search

    def _query_helper(self, query: list[TogglQuery], 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, query_obj: Query[T]) -> Query[T]:
        value = getattr(self.parent.model, query.key)  # type: ignore[union-attr]
        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: TogglQuery, 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.

Parameters:

  • query (TogglQuery, default: () ) –

    Any positional argument that is used becomes query argument.

  • distinct (bool, default: False ) –

    Whether to keep equivalent values around.

Returns:

  • Query[T]

    A SQLAlchemy query object with parameters filtered.

Source code in 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
def query(self, *query: TogglQuery, 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.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.parent.model)
    if isinstance(self.expire_after, timedelta):
        min_ts = datetime.now(timezone.utc) - self.expire_after
        search = search.filter(self.parent.model.timestamp > min_ts)  # type: ignore[arg-type]

    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]
    return search

JSON

toggl_api.meta.cache.json_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)

Parameters:

  • max_length (int, default: 10000 ) –

    Max length of the data to be stored.

Attributes:

  • max_length (int) –

    Max length of the data to be stored.

  • version (str) –

    Version of the data structure.

  • data (list[T]) –

    List of Toggl objects stored in memory.

  • modified (int) –

    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.

Source code in toggl_api/meta/cache/json_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
@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]):
        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 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.JSONCache

Bases: TogglCache, Generic[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)

Parameters:

  • path (Path) –

    Path to the cache file

  • expire_after (Optional[timedelta | int], default: None ) –

    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 (Optional[TogglCachedEndpoint[T]], default: None ) –

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

  • max_length (int, default: 10000 ) –

    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_cache

    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_cache

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

Source code in toggl_api/meta/cache/json_cache.py
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
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
class JSONCache(TogglCache, Generic[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_cache: 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_cache: Loads the data from the cache and returns the data to the
            caller discarding expired entries.
    """

    __slots__ = ("session",)

    def __init__(
        self,
        path: Path,
        expire_after: Optional[timedelta | int] = None,
        parent: Optional[TogglCachedEndpoint[T]] = 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_cache(self, update: Iterable[T] | T, method: RequestMethod) -> None:
        self.session.refresh(self.cache_path)
        func = self.find_method(method)
        if func is not None:
            func(update)
        self.commit()

    def load_cache(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]  # type: ignore[operator]

    def find_entry(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.parent.model):
                return item
        return None

    def add_entry(self, item: T) -> None:
        find_entry = self.find_entry(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_entries(self, update: list[T] | T, **kwargs: Any) -> None:
        if not isinstance(update, list):
            return self.add_entry(update)
        for item in update:
            self.add_entry(item)
        return None

    def update_entries(self, update: list[T] | T, **kwargs: Any) -> None:
        self.add_entries(update)

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

    def delete_entries(self, update: list[T] | T, **kwargs: Any) -> None:
        if not isinstance(update, list):
            return self.delete_entry(update)
        for entry in update:
            self.delete_entry(entry)
        return None

    def query(self, *query: TogglQuery, 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.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, ...],
        existing: dict[str, set[Any]],
        min_ts: Optional[datetime],
        *,
        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) -> 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 model[query.key] == query.value

    @staticmethod
    def _match_query(model: T, query: TogglQuery) -> bool:
        if query.comparison == Comparison.EQUAL:
            return JSONCache._match_equal(model, query)
        if query.comparison == Comparison.LESS_THEN:
            return model[query.key] < query.value
        if query.comparison == Comparison.LESS_THEN_OR_EQUAL:
            return model[query.key] <= query.value
        if query.comparison == Comparison.GREATER_THEN:
            return model[query.key] > query.value
        if query.comparison == Comparison.GREATER_THEN_OR_EQUAL:
            return 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.parent.model.__tablename__}.json"

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

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

query(*query: TogglQuery, 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.

Parameters:

  • query (TogglQuery, default: () ) –

    Any positional argument that is used becomes query argument.

  • distinct (bool, default: False ) –

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

Returns:

  • list[T]

    A list of models with the query parameters that matched.

Source code in toggl_api/meta/cache/json_cache.py
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
def query(self, *query: TogglQuery, 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.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,
        )
    ]