diff --git a/README.md b/README.md index dd2807f..5c33981 100644 --- a/README.md +++ b/README.md @@ -58,3 +58,7 @@ Now install the dependencies and test dependencies: To run the tests: pytest + +If you need to send other requests to Toggl API, you can capture responses using: + + pytest --record-mode=once diff --git a/setup.py b/setup.py index 9ef98a2..684e366 100644 --- a/setup.py +++ b/setup.py @@ -33,7 +33,6 @@ def get_long_description(): """, install_requires=[ "click", - "humanize", "pydantic", "requests", "rich", diff --git a/tests/cassettes/test_entries/test_entries.yaml b/tests/cassettes/test_entries/test_entries.yaml deleted file mode 100644 index ffeb11d..0000000 --- a/tests/cassettes/test_entries/test_entries.yaml +++ /dev/null @@ -1,81 +0,0 @@ -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 | - H4sIAAAAAAAEA82Z3YrjNhTHX0X4thnQhyXbutvOdGGhpYUpFLosgybRJN44tmvLm86WvkEXel0K - veoL9Mn6CD124iSKUidkbLOQm8gmkc7v/M+X3v7ixTNP0pBEhHIcBRNvnRXLMldT/VA/ISHhmIQT - Ly+y93pqNotB6DMeUFg2qlw2a2mVJBPvMU4S9ZhoTz6ppNQTrzSqMJ70KKbsBpMbKr6nVFIuGf8C - Y4mxV7+T5c4r3JeM/QhPZ1WhTJylsJeAwT/OdDkt4nyz5N1mq1WVxuZZoldJkq1RrooyTucoe0Jv - vvsgkJrNCl2WukRxCp+5Lg3K41wncarh142al55865nnXMuyyvMMtvuuWYdj1Y8IxUGIBYZF2EqW - Js+eNEUFZ1PuwZpd7w+miw+6eJjpRBs9e6jf35ipKmG5MbyghDE28Srr29o2ffOttfmvkz0zHFIq - 6BjMiPS5JGR/tFPMGqw2M+pTv4PZ6/jnHTFdFFmBpkmsU0AEHFBcosdEpUuk0hmAfF8Buyd4x8Qr - fW/UKu8f4JFnjgHQH0V0G4C4C+CWsQ2QjEBvIwqQ4kZym6/nxOZ45BisyHiscHieFbZZQST43+jY - l9JewOrA+QZnhRnh4LrDJzMsWST94CwrHNmsWES7cL36WBUa3cfzFNLWN9msSjRSlVlAbIynTT58 - gIw8hcwGOQ0yjFFxUqI3ZVn1ntUaoeFof8KB4YVhAHntMqHREPskYviqQoSEkkeSH7ilm9S2eG12 - PKoLJVtq//716R90F6epLnZZ6SqtOA7Vo7lbcx3UEGFAfIqhAhlcKiSQhEHpt3ckx9w1kVDSo7pP - 0Kiz7vtspNJuf3/CHtmREwVgKETILywAW/bXSUVIH8B0SaXFa0sFugfuSuWP39D9ojIGivA5WkKd - jR61WWudokW2gkAH5d56oYyG6vlFYmr3NAiQ1qCHYuJCYH5Z6NrxvA4IlxyaqIOo7Ippw4zaQBgR - IHY7dh02UZ+PmLbbH4TdzvgWPD/iPnjr8JGQQ8Ugib8/mgtvy9eGd0JMd0WWo4VWM12gJJujuq+t - W93bJKtmr6FfNQhklJpyJ6R+ml3iOODQsc4XBBPI9GPgwUz6Xc0unL4haOOhgjja+ipRJZRs36ZN - RSfRTxVMIGCcUaKnIluhO53GH9FtVi6rtH9CR142NCEakCAYRUBMYtI9QiIwP4JSStiEOPPdhrZW - CtTYMEXKijoh3aAftF7CnKcZA+3HQyutDTy3x0NRxAQkufPToXZDe9EPjQPmZTwQFwmmzWXX5SIK - KCTr6oGg8INp3zEN4kfRcS6COvpP9HWVThc7819TRsMfAn5+EGJ7tHZrrcPkQWjI61HX8NGJSgyp - sbMU2wAhtu+DOzi+Xz6n052d28RQr50agZILnHz7z+M5OQ64CEbJChBw4HPG7g0a2+6U1FnLrrjm - Clp5GGRmayfqvzDKON7Ro9+frJowjKBHuTcgoGcoOrrGYmTD6Mjx6xH7MQBV17lUwji5uTWonFHy - PFPJSRWwC1Sw3cZoKgiCkNH6lmbw6IOhC/Ql6ew7tpxsFUTYjfW9Rx/HQwZ2/iDAkGMhh11g94hj - Gom6Wb/izgwHcEcn/YN05nQMQAY+5HgkzARUAHbwMdl8ntyYQk2XEvoFuFSpL1QQNAlFrPd9wjVp - t93EIJ7fWvAg7QYwAMER2HRwAE03yrv8HhjBvAEfDa+ICJ0LsCEBbDcxHgAGN8eX5d+2bLpOAaIu - ezrTb51765GuHXgo908UmX//jr4stFo+QXe2K4Cu8njHMXqMOa3FLI8nGBrhETy+vu/t7rHA4A0T - 2+CMRs7Ib0iPdxyjRwC7kPPuP1wjPywqIQAA - 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, 27 Jan 2023 07:32:41 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: - - 30ab28ce94254c279dcb9a3437e2470e - X-Service-Level: - - GREEN - X-Toggl-Request-Id: - - 30ab28ce94254c279dcb9a3437e2470e - X-We-are-hiring: - - https://toggl.com/jobs/ - status: - code: 200 - message: OK -version: 1 diff --git a/tests/cassettes/test_entries_group_by/test_group_by_tags.yaml b/tests/cassettes/test_entries_group_by/test_group_by_tags.yaml new file mode 100644 index 0000000..d66d88d --- /dev/null +++ b/tests/cassettes/test_entries_group_by/test_group_by_tags.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 | + H4sIAAAAAAAEA82Z3YrjNhTHX0X4thnQh+UP3W1nurDQ0sIUCl2WQRNrEm8c27XlTWdL36CFXpdC + r/oCfbI+Qo+deGxFqSeT2mYhN5FNIp3f+Z8vvf3JiSNH0ICEhHIc+gtnlxWbMpdLdVc/IQHhmAQL + Jy+y92qp94t+4DLuU1jWstw0a2mVJAvnPk4SeZ8oRzzIpFQLp9Sy0I5wKKbsCpMr6n1LqaBcMP4Z + xgJjp34ny61XuCsY+x6eRlUhdZylsBefwT9GqlwWcb5fcq6z7bZKY/0o0KskyXYol0UZpyuUPaA3 + 33zwkIyiQpWlKlGcwmelSo3yOFdJnCr4dS1XpSPeOvoxV6Ks8jyD7b5r1uFY9SNCsR9gD8MibCVL + k0dH6KKCs0n7YM2uu4Op4oMq7iKVKK2iu/r9vZmqEpYbw3uUMMYWTmV825mmb761Nv950THDAaUe + nYMZES4XhHRHO8WswWoyoy51B5i9jn98IqaKIivQMolVCoiAA4pLdJ/IdINkGgHI9xWwe4B3dLxV + t1pu8/EBHnnmHADdWUS3B4iHAB4YmwDJHPQMwZ0hNMsb5+BE5uOEg+c5YZMTRIH/jIyjqewyTj2n + m5wTZoSDy06fxLBgoXD9Zznh0OTEQjqE6tXHqlDoNl6lkK6+yqIqUUhWeg0xMV42efAOMvESMhrk + MsgsWsZJid6UZTV6NmtEhsPuhBPDCwIf8tl5IqMBdknI8EUFCAkEDwXvuaWdzA54TXY8rAskU2b/ + /Pnr3+gmTlNVdNnopTqxnGlEU7em6tUNgU9ciqHqmFwmxBeEQbnXOZFl6ppGIOhRrefRcLDW+2Rk + 0m6/O+GI7MiJoi/wvICfWfS17C+TiSdcADMkkxavKRPoGLgtk99/QbfrSmsovFdoA7U1uld6p1SK + 1tkWghyUeLu11Aoq5ouF1O5nEhitMftC4p6H+Xkh64nlZTC44NA09aKxLaQ9L2rCYMQDoZsxq980 + fTpCOmx/EnZPxjfguSF3wVOnj4IcKgVB3O5oNrwDXxPeCSHdFFmO1kpGqkBJtkJ1H1u3ttdJVkWv + oT/VCCSU6rIT0SjNLbEccOo453oEE8jwc+DBTLhDzS2cviFo4qEesbT1RSJLKNW+TptKTqAfKpg4 + wPiiRA9FtkU3Ko0/ouus3FTp+ISOvGxqQtQnvj+LgJjAZHhkRGBeBGWUZxLizLUb2FopUFvD1Cgr + 6mR0hb5TagNznWbs042DtkppeG6Og8KQeZDgnm9S2w11op8aB8zHuO+dJZg2l12WiyigEGyo94Gi + D6Z7xzSIG4bHuQjq5z/Ql1W6XHfmf2H5DH8G6HkvvI5o6dZS/cRBaMDrsdb0kYkKDGlxsATbwyCm + 34MrWH5fPqbLzsaHpFCvGeY+jDvJGQ5++Of5HBz73PNnyQgQbODzjN0bNKbdKakzllltrSS07zC0 + zHZWxP+fEcbyjhH9/mTFhGHcPMsdAQE9Q8ExNAYje0ZHjl+P048ByLrGpQJGx80NQWWNjVeZTE6q + gJ2hgsM2ZlOB7weM1jcyk0cfDN2fK8hgz3HgZKogxHacHz36WB4ysfP7Pob8CvnrDLuHHNPQq5v0 + C+7HsA/3ccLtpTOrWwAy8CHHI2DmQfY3g4/OVqvkShdyuRHQK8AFSn15gqBBKGLV6xGMHPC817cb + mMTrW+v1Uq4PQw8cgj0nN37ThfIhnwc+MGfARwMr4gXWRddUxj9sYD7jM7gdPi/vtuXSZZ7v1eXO + YNqtc249vjUDDuXuicLyr9/Q54WSmwfoyLrC56WebjnEiHGmtZbh6QRD4zuDp9f3ucM9FRi74WEa + m9HQGu9N5emWQ4xo/Kcw8+5fyIRd7gYhAAA= + 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:02:08 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: + - fcc935dc52862c5105bcd3754c37f105 + X-Service-Level: + - GREEN + X-Toggl-Request-Id: + - fcc935dc52862c5105bcd3754c37f105 + X-We-are-hiring: + - https://toggl.com/jobs/ + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/test_entries_list/test_list.yaml b/tests/cassettes/test_entries_list/test_list.yaml new file mode 100644 index 0000000..78fb3db --- /dev/null +++ b/tests/cassettes/test_entries_list/test_list.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 | + H4sIAAAAAAAEA82Z3YrjNhTHX0X4thnQh+UP3W1nurDQ0sIUCl2WQRNrEm8c27XlTWdL36CFXpdC + r/oCfbI+Qo+deGxFqSeT2mYhN5FNIp3f+Z8vvf3JiSNH0ICEhHIc+gtnlxWbMpdLdVc/IQHhmAQL + Jy+y92qp94t+4DLuU1jWstw0a2mVJAvnPk4SeZ8oRzzIpFQLp9Sy0I5wKKbsCpMr6n1LqaBcMP4Z + xgJjp34ny61XuCsY+x6eRlUhdZylsBefwT9GqlwWcb5fcq6z7bZKY/0o0KskyXYol0UZpyuUPaA3 + 33zwkIyiQpWlKlGcwmelSo3yOFdJnCr4dS1XpSPeOvoxV6Ks8jyD7b5r1uFY9SNCsR9gD8MibCVL + k0dH6KKCs0n7YM2uu4Op4oMq7iKVKK2iu/r9vZmqEpYbw3uUMMYWTmV825mmb761Nv950THDAaUe + nYMZES4XhHRHO8WswWoyoy51B5i9jn98IqaKIivQMolVCoiAA4pLdJ/IdINkGgHI9xWwe4B3dLxV + t1pu8/EBHnnmHADdWUS3B4iHAB4YmwDJHPQMwZ0hNMsb5+BE5uOEg+c5YZMTRIH/jIyjqewyTj2n + m5wTZoSDy06fxLBgoXD9Zznh0OTEQjqE6tXHqlDoNl6lkK6+yqIqUUhWeg0xMV42efAOMvESMhrk + MsgsWsZJid6UZTV6NmtEhsPuhBPDCwIf8tl5IqMBdknI8EUFCAkEDwXvuaWdzA54TXY8rAskU2b/ + /Pnr3+gmTlNVdNnopTqxnGlEU7em6tUNgU9ciqHqmFwmxBeEQbnXOZFl6ppGIOhRrefRcLDW+2Rk + 0m6/O+GI7MiJoi/wvICfWfS17C+TiSdcADMkkxavKRPoGLgtk99/QbfrSmsovFdoA7U1uld6p1SK + 1tkWghyUeLu11Aoq5ouF1O5nEhitMftC4p6H+Xkh64nlZTC44NA09aKxLaQ9L2rCYMQDoZsxq980 + fTpCOmx/EnZPxjfguSF3wVOnj4IcKgVB3O5oNrwDXxPeCSHdFFmO1kpGqkBJtkJ1H1u3ttdJVkWv + oT/VCCSU6rIT0SjNLbEccOo453oEE8jwc+DBTLhDzS2cviFo4qEesbT1RSJLKNW+TptKTqAfKpg4 + wPiiRA9FtkU3Ko0/ouus3FTp+ISOvGxqQtQnvj+LgJjAZHhkRGBeBGWUZxLizLUb2FopUFvD1Cgr + 6mR0hb5TagNznWbs042DtkppeG6Og8KQeZDgnm9S2w11op8aB8zHuO+dJZg2l12WiyigEGyo94Gi + D6Z7xzSIG4bHuQjq5z/Ql1W6XHfmf2H5DH8G6HkvvI5o6dZS/cRBaMDrsdb0kYkKDGlxsATbwyCm + 34MrWH5fPqbLzsaHpFCvGeY+jDvJGQ5++Of5HBz73PNnyQgQbODzjN0bNKbdKakzllltrSS07zC0 + zHZWxP+fEcbyjhH9/mTFhGHcPMsdAQE9Q8ExNAYje0ZHjl+P048ByLrGpQJGx80NQWWNjVeZTE6q + gJ2hgsM2ZlOB7weM1jcyk0cfDN2fK8hgz3HgZKogxHacHz36WB4ysfP7Pob8CvnrDLuHHNPQq5v0 + C+7HsA/3ccLtpTOrWwAy8CHHI2DmQfY3g4/OVqvkShdyuRHQK8AFSn15gqBBKGLV6xGMHPC817cb + mMTrW+v1Uq4PQw8cgj0nN37ThfIhnwc+MGfARwMr4gXWRddUxj9sYD7jM7gdPi/vtuXSZZ7v1eXO + YNqtc249vjUDDuXuicLyr9/Q54WSmwfoyLrC56WebjnEiHGmtZbh6QRD4zuDp9f3ucM9FRi74WEa + m9HQGu9N5emWQ4xo/Kcw8+5fyIRd7gYhAAA= + 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:02:09 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: + - a62a2f942c009e485790366413698216 + X-Service-Level: + - GREEN + X-Toggl-Request-Id: + - a62a2f942c009e485790366413698216 + X-We-are-hiring: + - https://toggl.com/jobs/ + status: + code: 200 + message: OK +version: 1 diff --git a/tests/conftest.py b/tests/conftest.py index 3527da0..02d5f3e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,7 +14,7 @@ def vcr_config(): @pytest.fixture() def save_to_tmp(): """Saves the result object to the filesystem for inspection.""" - def wrapper(output: Any): - with open("/tmp/output.txt", "w") as f: + def wrapper(output: Any, name: str = "result"): + with open(f"/tmp/{name}.out.txt", "w") as f: f.write(str(output)) return wrapper diff --git a/tests/test_entries_group_by.py b/tests/test_entries_group_by.py new file mode 100644 index 0000000..20b99fb --- /dev/null +++ b/tests/test_entries_group_by.py @@ -0,0 +1,38 @@ +import pytest +from click.testing import CliRunner + +from toggl_track.cli import cli + + +env = { + "TOGGL_API_TOKEN": "1234567890abcdef1234567890abcdef", # fake token for testing +} + + +@pytest.mark.vcr +@pytest.mark.block_network +def test_group_by_tags(): + runner = CliRunner() + with runner.isolated_filesystem(): + result = runner.invoke( + cli, + ["entries", "group-by", "--field", "tags", "--start-date", "2023-01-26", "--end-date", "2023-01-27"], + env=env, + ) + assert result.exit_code == 0 + assert ( + result.output + == """ Time Entries + + tags Duration + ───────────────────────── + type:support 5:44 + 5:14 + type:sync 3:00 + type:meeting 2:04 + type:goal 0:35 + + +""" + ) + diff --git a/tests/test_entries.py b/tests/test_entries_list.py similarity index 54% rename from tests/test_entries.py rename to tests/test_entries_list.py index d51dcf2..46e8dac 100644 --- a/tests/test_entries.py +++ b/tests/test_entries_list.py @@ -12,7 +12,7 @@ @pytest.mark.vcr @pytest.mark.block_network -def test_entries(): +def test_list(): runner = CliRunner() with runner.isolated_filesystem(): result = runner.invoke( @@ -25,77 +25,75 @@ def test_entries(): result.output == """ Time Entries - At Description Start Stop Duration Tags + At Description Start Stop Duration Tags ────────────────────────────────────────────────────────────────────────────── - 2023-01-26 Community: 10:25 PM 10:54 PM 28 minutes type:support + 2023-01-26 Community: 10:25 PM 10:54 PM 0:28 type:support Allow parsing of IPv6 addresses in - ingest - pipeline - 2023-01-26 Community: 09:45 PM 10:25 PM 40 minutes type:support - Fix parsing - error client - port is blank - and adjust - for timeStamp - 2023-01-26 Community: 09:45 PM 09:45 PM a second - Fix parsing - error client - port is blank - and adjust - for timeStamp - 2023-01-26 Community: 09:45 PM 09:45 PM 2 seconds - Fix parsing - error client - port is blank - and adjust - for timeStamp - 2023-01-26 Community: 08:39 PM 09:45 PM an hour type:support + ingest pipeline + 2023-01-26 Community: Fix 09:45 PM 10:25 PM 0:40 type:support + parsing error + client port is + blank and + adjust for + timeStamp + 2023-01-26 Community: Fix 09:45 PM 09:45 PM 0:00 + parsing error + client port is + blank and + adjust for + timeStamp + 2023-01-26 Community: Fix 09:45 PM 09:45 PM 0:00 + parsing error + client port is + blank and + adjust for + timeStamp + 2023-01-26 Community: 08:39 PM 09:45 PM 1:05 type:support Azure Signin Module - authenticati… + authentication… Issue - 2023-01-26 🍲 Dinner 06:59 PM 08:39 PM an hour - 2023-01-26 Community: 05:13 PM 06:58 PM an hour type:support + 2023-01-26 🍲 Dinner 06:59 PM 08:39 PM 1:39 + 2023-01-26 Community: 05:13 PM 06:58 PM 1:44 type:support Azure Signin Module - authenticati… + authentication… Issue - 2023-01-26 🚌 Shuttling 04:48 PM 05:13 PM 25 minutes + 2023-01-26 🚌 Shuttling 04:48 PM 05:13 PM 0:25 kids between home and whatever - 2023-01-26 Community: 03:55 PM 04:48 PM 52 minutes type:support + 2023-01-26 Community: 03:55 PM 04:48 PM 0:52 type:support Azure Signin Module - authenticati… + authentication… Issue - 2023-01-26 Drop header 03:47 PM 03:55 PM 8 minutes type:support - log line in + 2023-01-26 Drop header log 03:47 PM 03:55 PM 0:08 type:support + line in CloudFront events - 2023-01-26 ElasticOnAzu… 03:03 PM 03:47 PM 43 minutes type:support - questions - from Deniz - Coskun - 2023-01-26 Cloud 01:01 PM 02:30 PM an hour type:meeting + 2023-01-26 ElasticOnAzure: 03:03 PM 03:47 PM 0:43 type:support + questions from + Deniz Coskun + 2023-01-26 Cloud 01:01 PM 02:30 PM 1:29 type:meeting Monitoring - Weekly - 2023-01-26 🍜 Lunch 12:35 PM 01:00 PM 24 minutes - 2023-01-26 sync 12:06 PM 12:35 PM 28 minutes type:sync - 2023-01-26 gather town 11:31 AM 12:06 PM 35 minutes type:meeting - 2023-01-26 azure2: 10:55 AM 11:31 AM 35 minutes type:goal - follow up - 2023-01-26 sync 08:24 AM 10:55 AM 2 hours type:sync - 2023-01-26 toggl-track: 07:28 AM 08:08 AM 39 minutes + 2023-01-26 🍜 Lunch 12:35 PM 01:00 PM 0:24 + 2023-01-26 sync 12:06 PM 12:35 PM 0:28 type:sync + 2023-01-26 gather town 11:31 AM 12:06 PM 0:35 type:meeting + 2023-01-26 azure2: follow 10:55 AM 11:31 AM 0:35 type:goal + up + 2023-01-26 sync 08:24 AM 10:55 AM 2:31 type:sync + 2023-01-26 toggl-track: 07:28 AM 08:08 AM 0:39 list time entries - 2023-01-26 toggl-track: 06:48 AM 07:17 AM 28 minutes + 2023-01-26 toggl-track: 06:48 AM 07:17 AM 0:28 list time entries - 2023-01-26 🥐 Breakfast 06:06 AM 06:48 AM 42 minutes - 2023-01-26 toggl-track: 05:11 AM 06:06 AM 54 minutes + 2023-01-26 🥐 Breakfast 06:06 AM 06:48 AM 0:42 + 2023-01-26 toggl-track: 05:11 AM 06:06 AM 0:54 list time entries diff --git a/tests/test_result.py b/tests/test_result.py index 33cf556..9d68877 100644 --- a/tests/test_result.py +++ b/tests/test_result.py @@ -11,7 +11,7 @@ def test_empty_timeentrieslistresult_as_str(): assert str(result) == "No time entries found." -def test_empty_timeentrieslistresult_as_str(): +def test_empty_timeentrieslistresult_as_str(save_to_tmp): entries = [TimeEntry( id=1, workspace_id=1, @@ -28,10 +28,12 @@ def test_empty_timeentrieslistresult_as_str(): )] result = TimeEntriesListResult(entries) - assert str(result) == """ Time Entries - - At Description Start Stop Duration Tags - ──────────────────────────────────────────────────────────────────────────── - 2021-01-25 community: 11:04 PM 11:27 PM 22 minutes type:support - + save_to_tmp(result, "test_empty_timeentrieslistresult_as_str") + + assert str(result) == """ Time Entries + + At Description Start Stop Duration Tags + ────────────────────────────────────────────────────────────────────────── + 2021-01-25 community: 11:04 PM 11:27 PM 0:22 type:support + """ diff --git a/toggl_track/cli.py b/toggl_track/cli.py index 7998858..d7e7fe1 100644 --- a/toggl_track/cli.py +++ b/toggl_track/cli.py @@ -48,8 +48,10 @@ def list_entries(start_date: datetime, end_date: datetime): client = TimeEntries.from_environment() - click.echo(TimeEntriesListResult( - client.list(start_date, end_date)) + click.echo( + TimeEntriesListResult( + client.list(start_date, end_date) + ) ) diff --git a/toggl_track/result.py b/toggl_track/result.py index bef19e8..410813b 100644 --- a/toggl_track/result.py +++ b/toggl_track/result.py @@ -1,8 +1,8 @@ import io +import datetime as dt from itertools import groupby from typing import Any, List -from humanize import naturaldelta from rich import box from rich.console import Console from rich.table import Table @@ -36,7 +36,7 @@ def __str__(self) -> str: e.description, e.start.strftime("%I:%M %p"), "" if not e.stop else e.stop.strftime("%I:%M %p"), - "" if e.duration < 0 else naturaldelta(e.duration), + format_duration(e.duration), "" if not e.tags else "".join(e.tags), ) @@ -46,6 +46,10 @@ def __str__(self) -> str: return console.file.getvalue() + def ndjson(self) -> str: + """Returns a newline-delimited JSON string.""" + return "\n".join([e.json() for e in self.entries]) + class GroupByCriterion(object): """MISSING DOCSTRING""" @@ -96,7 +100,7 @@ def __str__(self) -> str: # total_duration = sum([e.duration for e in g if e.duration > 0]) table.add_row( k, - "-" if g < 0 else naturaldelta(g), + format_duration(g), ) # turn table into a string using the Console @@ -104,3 +108,17 @@ def __str__(self) -> str: console.print(table) return console.file.getvalue() + + +def format_duration(duration: int) -> str: + """Formats a duration in seconds as a string in the format HH:MM""" + + if duration < 0: # happens when a time entry is still running + return "-" + + # credits: 'inspired' by timedelta.__str__ + mm, _ = divmod(duration, 60) + hh, mm = divmod(mm, 60) + s = "%d:%02d" % (hh, mm) + + return s diff --git a/toggl_track/toggl.py b/toggl_track/toggl.py index 674d043..7ad7bfa 100644 --- a/toggl_track/toggl.py +++ b/toggl_track/toggl.py @@ -50,16 +50,12 @@ def list(self, start_date: datetime, end_date: datetime) -> List[TimeEntry]: end_date=end_date.isoformat() + "Z", ) - # print(params) - url = ( "https://api.track.toggl.com/api/v9/me/time_entries?" + urllib.parse.urlencode(params) ) resp = requests.get(url, auth=(self.api_token, "api_token")) - # print(resp.text) - if resp.status_code != 200: raise Exception(resp.text)