Skip to content

Commit

Permalink
List time entries (#5)
Browse files Browse the repository at this point in the history
* Mock a simple list of time entries

refs #4

* Add TimeEntries client

Ref: #4

* Switch from hard-coded to actual Toggl API calls

- We use the actual TimeEntries API to fetch data from Toggl
- Record and replay network traffic using pytest-recording

refs: #4

* Add missing TOGGL_API_TOKEN environment variable

* Move output formatting into a result module

refs: #4

* Add a 'entries list' command usage example

Simple demonstration of the only available command :)

refs: #4

* Improve help messages add clean up default options

Default values for date-related options were a little messy. I hope
this change makes the date logic a little more readable.
  • Loading branch information
zmoog authored Jan 28, 2023
1 parent 3a61668 commit 0a45b43
Show file tree
Hide file tree
Showing 10 changed files with 437 additions and 11 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ venv
.pytest_cache
*.egg-info
.DS_Store
.idea
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,26 @@ Install this tool using `pip`:

## Usage

For listing the time entries in the last 24 hours, run:

$ tgl entries list
Time Entries
At Description Start Stop Duration Tags
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
2023-01-28 toggl-track: list time entries 04:33 AM
2023-01-27 ESF: telemetry 05:23 PM 07:19 PM an hour type:goal
2023-01-27 ESF: telemetry 03:28 PM 05:00 PM an hour type:goal
2023-01-27 Community: Fix parsing error client port is blank and adjust for timeStamp 03:13 PM 03:28 PM 15 minutes type:support
2023-01-27 Community: Azure Signin Module authentication_processing_details Issue 02:47 PM 03:13 PM 25 minutes type:support
2023-01-27 sync 02:31 PM 02:47 PM 16 minutes type:sync
2023-01-27 🍜 Lunch 01:11 PM 02:31 PM an hour
2023-01-27 Community: Fix parsing error client port is blank and adjust for timeStamp 12:19 PM 12:29 PM 10 minutes type:support
2023-01-27 Community: Fix parsing error client port is blank and adjust for timeStamp 11:54 AM 12:06 PM 11 minutes type:support
2023-01-27 Community: Azure Signin Module authentication_processing_details Issue 09:30 AM 11:54 AM 2 hours type:support
2023-01-27 sync 08:34 AM 09:30 AM 56 minutes type:sync
2023-01-27 toggl-track: list time entries 07:04 AM 08:34 AM an hour

For help, run:

toggl-track --help
Expand Down
13 changes: 11 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,18 @@ def get_long_description():
[console_scripts]
tgl=toggl_track.cli:cli
""",
install_requires=["click"],
install_requires=[
"click",
"humanize",
"pydantic",
"requests",
"rich",
],
extras_require={
"test": ["pytest"]
"test": [
"pytest",
"pytest-recording",
]
},
python_requires=">=3.7",
)
81 changes: 81 additions & 0 deletions tests/cassettes/test_entries/test_entries.yaml
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 |
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
20 changes: 20 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from typing import Any

import pytest
from click.testing import Result


@pytest.fixture(scope="module")
def vcr_config():
return {
"filter_headers": ["authorization"],
}


@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:
f.write(str(output))
return wrapper
105 changes: 105 additions & 0 deletions tests/test_entries.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import click.testing
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_entries():
runner = CliRunner()
with runner.isolated_filesystem():
result = runner.invoke(
cli,
["entries", "list", "--start-date", "2023-01-26", "--end-date", "2023-01-27"],
env=env,
)
assert result.exit_code == 0
assert (
result.output
== """ Time Entries
At Description Start Stop Duration Tags
──────────────────────────────────────────────────────────────────────────────
2023-01-26 Community: 10:25 PM 10:54 PM 28 minutes 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
Azure Signin
Module
authenticati…
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
Azure Signin
Module
authenticati…
Issue
2023-01-26 🚌 Shuttling 04:48 PM 05:13 PM 25 minutes
kids between
home and
whatever
2023-01-26 Community: 03:55 PM 04:48 PM 52 minutes type:support
Azure Signin
Module
authenticati…
Issue
2023-01-26 Drop header 03:47 PM 03:55 PM 8 minutes type:support
log 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
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
list time
entries
2023-01-26 toggl-track: 06:48 AM 07:17 AM 28 minutes
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
list time
entries
"""
)

37 changes: 37 additions & 0 deletions tests/test_result.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from datetime import datetime

from toggl_track.result import TimeEntriesListResult
from toggl_track.toggl import TimeEntry


def test_empty_timeentrieslistresult_as_str():
entries = []
result = TimeEntriesListResult(entries)

assert str(result) == "No time entries found."


def test_empty_timeentrieslistresult_as_str():
entries = [TimeEntry(
id=1,
workspace_id=1,
user_id=1,
project_id=1,
task_id=None,
billable=False,
at=datetime(2021, 1, 25, 0, 0),
description="community:",
start=datetime(2021, 1, 25, 23, 4),
stop=datetime(2021, 1, 25, 23, 27),
duration=1379,
tags=["type:support"]
)]
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
"""
51 changes: 42 additions & 9 deletions toggl_track/cli.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,54 @@
import datetime
from datetime import timedelta

import click

from .toggl import TimeEntries
from .result import TimeEntriesListResult


# default reference date for all date options
now = datetime.datetime.now()

def as_str(reference_date: datetime = now) -> str:
"""Formats a `reference_date` into a string.
Helper function to be used as a default value for click options.
:param reference_date: a datetime object"""
return reference_date.strftime("%Y-%m-%dT%H:%M:%S")


@click.group()
@click.version_option()
def cli():
"CLI tool and Python library to access Toggl Track https://toggl.com/track/"


@cli.command(name="command")
@click.argument(
"example"
@cli.group()
def entries():
"Time entries commands"
pass



@entries.command(name="list")
@click.option(
"--start-date",
type=click.DateTime(),
default=as_str(now - timedelta(hours=24)),
help="Start date (default: 24 hours ago)"
)
@click.option(
"-o",
"--option",
help="An example option",
"--end-date",
type=click.DateTime(),
default=as_str(now)
)
def first_command(example, option):
"Command description goes here"
click.echo("Here is some output")
def list_entries(start_date: datetime, end_date: datetime):
"""Returns a list of the latest time entries (default: last 24 hours)"""

client = TimeEntries.from_environment()

click.echo(TimeEntriesListResult(
client.list(start_date, end_date))
)
Loading

0 comments on commit 0a45b43

Please sign in to comment.