diff --git a/README.md b/README.md index 42a99a4..1c06e8e 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/tests/cassettes/test_entries_list/test_list_filter_by_description.yaml b/tests/cassettes/test_entries_list/test_list_filter_by_description.yaml new file mode 100644 index 0000000..6a01cd5 --- /dev/null +++ b/tests/cassettes/test_entries_list/test_list_filter_by_description.yaml @@ -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 diff --git a/tests/test_entries_list.py b/tests/test_entries_list.py index a3fd45e..66e4a12 100644 --- a/tests/test_entries_list.py +++ b/tests/test_entries_list.py @@ -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): diff --git a/toggl_track/cli.py b/toggl_track/cli.py index 571026a..2d0229f 100644 --- a/toggl_track/cli.py +++ b/toggl_track/cli.py @@ -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 @@ -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'], @@ -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'], diff --git a/toggl_track/result.py b/toggl_track/result.py index d694eeb..b171b6d 100644 --- a/toggl_track/result.py +++ b/toggl_track/result.py @@ -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 @@ -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.""" @@ -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 diff --git a/toggl_track/toggl.py b/toggl_track/toggl.py index 0bdf04e..c466054 100644 --- a/toggl_track/toggl.py +++ b/toggl_track/toggl.py @@ -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): @@ -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 @@ -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