Skip to content

Commit

Permalink
Filter time entries by description (#18)
Browse files Browse the repository at this point in the history
Add a simple `--description` option to filter the time entries to the
`entries` command group, so it's available to all time entries related
commands.

The API endpoint does not offer filtering capabilities, to we are
performing the filtering on the client side.

Refs: #16
  • Loading branch information
zmoog authored Apr 3, 2023
1 parent ecf8077 commit 73f189d
Show file tree
Hide file tree
Showing 6 changed files with 162 additions and 15 deletions.
28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ For listing the time entries in the last 24 hours, run:
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Total 13:08

Now you can also filter time entries by project ID:
Now you can also filter time entries by project ID or description:

$ tgl entries --project-id 178435728 list
Time Entries
Expand All @@ -55,6 +55,32 @@ Now you can also filter time entries by project ID:
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Total 7:05

$ tgl entries --description SDH list

Time Entries
At Description Start Stop Duration Tags
──────────────────────────────────────────────────────────────────────────
2023-04-01 SDH 3256 05:56 AM 07:01 AM 1:04 type:support
2023-03-31 SDH 3256 03:01 PM 04:18 PM 1:17 type:support
2023-03-31 SDH 3247 09:45 AM 12:04 PM 2:18 type:support
2023-03-31 SDH 3253 08:49 AM 09:45 AM 0:56 type:support
2023-03-31 SDH 3237 07:22 AM 08:30 AM 1:08 type:support
2023-03-30 SDH 3229 01:16 PM 05:35 PM 4:18 type:support
2023-03-30 SDH 3229 09:36 AM 11:31 AM 1:55 type:support
2023-04-03 SDH 3140 08:11 AM 09:19 AM 1:07 type:support
2023-03-29 SDH 3149 01:36 PM 05:04 PM 3:27 type:support
2023-03-28 SDH 3237 05:04 PM 06:03 PM 0:58 type:support
2023-03-28 SDH 3237 02:40 PM 03:01 PM 0:20 type:support
2023-03-28 SDH 3237 01:43 PM 02:21 PM 0:38 type:support
2023-03-28 SDH 3237 10:31 AM 12:17 PM 1:46 type:support
2023-03-27 SDH 3069 08:50 PM 09:29 PM 0:39 type:support
2023-03-27 SDH 3069 05:51 PM 06:56 PM 1:05 type:support
2023-03-27 SDH 3069 04:59 PM 05:21 PM 0:21 type:support
2023-03-27 SDH 3069 01:11 PM 03:09 PM 1:57 type:support
──────────────────────────────────────────────────────────────────────────
Total 25:22

Supports JSON and NDJSON as alternative output format using the `--format` option:

# format result as a list of objects
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
interactions:
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate
Connection:
- keep-alive
User-Agent:
- python-requests/2.28.2
method: GET
uri: https://api.track.toggl.com/api/v9/me/time_entries?start_date=2023-01-26T00%3A00%3A00Z&end_date=2023-01-27T00%3A00%3A00Z
response:
body:
string: !!binary |
H4sIAAAAAAAEA9WZ3Y7jNBTHX8XKLR3JH7GT+G6ZYaWVQCDNSkisViNP47bZpklInC2ziDdgJa4R
Ele8AE/GI3CSJpO6KZluacIg9aZO1Nrndz7+5/jNj04UOpL6JCCU48CbOds0XxeZmuu76gnxCcfE
nzlZnr7Tc7Nb9HyXcY/CslHFul5LyjieOfdRHKv7WDtyoeJCz5zCqNw40qGYsitMrqh4TamkXDL+
GcYSY6d6J816r3BXMvYdPA3LXJkoTWAvHoN/DHUxz6Nst+Rcp5tNmUTmQaIXcZxuUabyIkqWKF2g
V9+8F0iFYa6LQhcoSuCz1IVBWZTpOEo0/LpRy8KRbxzzkGlZlFmWwnbf1utwrOoRodjzscCwCFtJ
k/jBkSYv4Wyqf7B6193BdP5e53ehjrXR4V31/s5MZQHLteEFJYyxmVNa37a26etvrc1/mnXMsE+p
oFMwI9LlkpDuaMeY1VhtZtSl7gCzl9EPj8R0nqc5mseRTgARcEBRge5jlayRSkIA+a4Edgt4x0Qb
fWvUJrs8wAPPnAKgO0nQ7QDiIYANYxsg+T/Qo1eYvcaedIVkQXfEKeiR6ehhvztaP/wawDY9yA3/
mC+fTez9V/QwIxzce/yCh8Erpes9SQ8HNj0W0CGALz6UuUa30TKB0vZVGpaxRqo0K8if0byumXdQ
tedQ/aDuQRUyKooL9KooyotXvtr58HSh5/se1L7TQo/62CUBw2eJFeJLHkg+mDcbvDY7HlRiyg6+
v37/+Ce6iZJE513lssTGCSKj50wXzHKtqfY0hu8Rl2JQKKOHCfEkYSANB8KkouFLeqALBQ0GdeGz
CZN2+90JL8iOHBGIvhA+P1EgtuzPCxMhXQAzFCYtXjtMoLvg/TD59Wd0uyqNAZG+RGvQ4ehem63W
CVqlG0hyIAe3K2U0qOuzA6ndzygwWmPuBxIXAvPTUtYjy/NgcMmhwdrLxj21QHa8qA2DEQGBbues
/Qbr+QRSs/1R2D0a34LnBtwFTx0/C3JQCpK43dH68Bq+NrwjgXSTpxlaaRXqHMXpElU9b9UGX8dp
Gb6EXtYgCKHEFF0QXaQRJj0HHDvPuYJgAhV+CjyYSXeoEYbT1wRtPFSQXmx9EasCpNrXSa3kJPq+
hOkEjDoKtMjTDbrRSfQBXafFukwuT+jAy8YmRD3ieZMEEJOYDI+XCMyWQEYJmxBnbr/ZrSIFtDVM
mNK8KkZX6Fut1zADqkdE3ehoo7WB5/boKAiYgAL3tKhrN9QF/dg4YJbGPXFSwLS17LxaRAGFZEO9
D4g+mAQe0iBuEBzWItDPv6Evy2S+6sz/ifIZ/gzQ8730ekFLt5baLxyE+rwagY2fmajEUBYHJdgO
BrH9Hlyh5/fFQzLvbNwUhWrNMnczGiUnOHjzz9M5OPa48CapCJBs4POE3Ws0tt0pqSqWrbaWCtp3
GHCm217G/5cZpucdF/T7o4oJw2h6kvsEAvEMgmNoOEZ2jA4cvxq9HwJQlcalEsbM9W1C2RsxL1MV
H40CdkIUNNuYLAo8z2e0ur0ZPftg6P5cSQZ7joaTHQUB7uf5i2efnoeM7Pyeh6G+Qv06we4BxzQQ
VZN+xl0azLypL929ctbrFoAMfAi2zU6ZgOpvJx+TLpfxlcnVfC2hV4DLluqiBUGDkEd6r0ewasDT
Xt9uYBSvb623V3I9GHrgAOw5uvHrLpQP+TzwgTkDPhhYEeH3LsXGMn6zgemMz+Am+bS628ql8zxf
VHJnsOxWNbca3x54PnePCMs/fkGf51qtF9CRdcLnUz295xAXzDOttSxPJxga3wk8vbr7He6pwNg1
D9vYjAa98d5Ynt5ziAsa/zHNvP0bd0fgfDIhAAA=
headers:
Alt-Svc:
- h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
Cache-Control:
- no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0
Content-Encoding:
- gzip
Content-Type:
- application/json; charset=utf-8
Date:
- Fri, 03 Feb 2023 07:48:05 GMT
Instance:
- time-public-api2
Referrer-Policy:
- strict-origin-when-cross-origin
Server:
- nginx
Strict-Transport-Security:
- max-age=15552000; includeSubDomains
Transfer-Encoding:
- chunked
Vary:
- Accept-Encoding
Via:
- 1.1 google
X-Content-Type-Options:
- nosniff
X-Request-ID:
- d37a26641b2eb9d772d2e2ad8364d785
X-Service-Level:
- GREEN
X-Toggl-Request-Id:
- d37a26641b2eb9d772d2e2ad8364d785
X-We-are-hiring:
- https://toggl.com/jobs/
status:
code: 200
message: OK
version: 1
32 changes: 32 additions & 0 deletions tests/test_entries_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,38 @@
}


@pytest.mark.vcr
@pytest.mark.block_network
def test_list_filter_by_description(save_to_tmp):
runner = CliRunner()
with runner.isolated_filesystem():
result = runner.invoke(
cli,
["entries", "--description", "toggl-track", "list", "--start-date", "2023-01-26", "--end-date", "2023-01-27"],
env=env,
)
save_to_tmp(result.output, name="test_list_filter_by_description")
assert result.exit_code == 0
assert (
result.output
== """ Time Entries
At Description Start Stop Duration Tags
──────────────────────────────────────────────────────────────────────────────
2023-01-26 toggl-track: list time 07:28 AM 08:08 AM 0:39
entries
2023-01-26 toggl-track: list time 06:48 AM 07:17 AM 0:28
entries
2023-01-26 toggl-track: list time 05:11 AM 06:06 AM 0:54
entries
──────────────────────────────────────────────────────────────────────────────
Total 2:02
"""
)


@pytest.mark.vcr
@pytest.mark.block_network
def test_list(save_to_tmp):
Expand Down
12 changes: 9 additions & 3 deletions toggl_track/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,21 @@ def cli(ctx: click.Context, format: str, json_root: str):
ctx.obj['json_root'] = json_root

@cli.group()
@click.option(
'--description',
'-d',
type=click.STRING,
multiple=False)
@click.option(
'--project-id',
'-p',
type=click.INT,
multiple=True)
@click.pass_context
def entries(ctx: click.Context, project_id: List[int]):
def entries(ctx: click.Context, description: List[str], project_id: List[int]):
"Time entries commands"
ctx.ensure_object(dict)
ctx.obj['description'] = description
ctx.obj['project_id'] = project_id


Expand All @@ -72,7 +78,7 @@ def list_entries(ctx: click.Context, start_date: dt.datetime, end_date: dt.datet
click.echo(
render(
TimeEntriesListResult(
client.list(start_date, end_date, project_ids=ctx.obj['project_id'])
client.list(start_date, end_date, project_ids=ctx.obj['project_id'], description=ctx.obj['description'])
),
format=ctx.obj['format'],
json_root=ctx.obj['json_root'],
Expand Down Expand Up @@ -105,7 +111,7 @@ def group_by_entries(ctx: click.Context, start_date: dt.datetime, end_date: dt.d
click.echo(
render(
TimeEntriesGroupByResult(
client.list(start_date, end_date, project_ids=ctx.obj['project_id']),
client.list(start_date, end_date, project_ids=ctx.obj['project_id'], description=ctx.obj['description']),
key_func=GroupByCriterion(field)
),
format=ctx.obj['format'],
Expand Down
12 changes: 6 additions & 6 deletions toggl_track/result.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import json
import datetime as dt
from itertools import groupby
from typing import Any, List
from typing import Any, Iterator, List

from pydantic.json import pydantic_encoder
from rich import box
Expand All @@ -13,10 +13,10 @@


class TimeEntriesListResult(object):
"""Turns a list of TimeEntry objects into a rich table"""
"""Turns a TimeEntry iterator into a rich table"""

def __init__(self, entries: List[TimeEntry]) -> None:
self.entries = entries
def __init__(self, entries: Iterator[TimeEntry]) -> None:
self.entries = list(entries) # we iterate over the entries twice

def __str__(self) -> str:
"""Returns a rich table as a string."""
Expand Down Expand Up @@ -82,9 +82,9 @@ def __call__(self, time_entry: TimeEntry) -> Any:
return v

class TimeEntriesGroupByResult(object):
"""Turns a list of TimeEntry objects into a rich table grouped by a criterion."""
"""Turns a TimeEntry iterator into a rich table grouped by a criterion."""

def __init__(self, entries: List[TimeEntry], key_func: GroupByCriterion) -> None:
def __init__(self, entries: Iterator[TimeEntry], key_func: GroupByCriterion) -> None:
self.key_func = key_func

# group entries by key_func
Expand Down
12 changes: 7 additions & 5 deletions toggl_track/toggl.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import os
import urllib
from datetime import datetime
from typing import Optional, List
from typing import Optional, Iterator, List

import requests
from pydantic import BaseModel, parse_raw_as
from typing import List


class TimeEntry(BaseModel):
Expand Down Expand Up @@ -44,7 +43,7 @@ def from_environment(cls) -> "TimeEntries":
)
return cls(api_token=os.environ["TOGGL_API_TOKEN"])

def list(self, start_date: datetime, end_date: datetime, project_ids: List[int] = ()) -> List[TimeEntry]:
def list(self, start_date: datetime, end_date: datetime, description: str = None, project_ids: List[int] = []) -> Iterator[TimeEntry]:
"""Fetches the time entries between `start_date` and `end_date` dates.
Time Entries API v9
Expand All @@ -67,8 +66,11 @@ def list(self, start_date: datetime, end_date: datetime, project_ids: List[int]
# it looks like the API doesn't support filtering, so I suppose
# we have to do it ourselves
entries = parse_raw_as(List[TimeEntry], resp.text)


if description:
entries = filter(lambda entry: description in entry.initiative, entries)

if project_ids:
return list(filter(lambda entry: entry.project_id in project_ids, entries))
entries = filter(lambda entry: entry.project_id in project_ids, entries)

return entries

0 comments on commit 73f189d

Please sign in to comment.