Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Filter time entries by description #18

Merged
merged 1 commit into from
Apr 3, 2023
Merged
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
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