auhau / toggl-cli Goto Github PK
View Code? Open in Web Editor NEWA simple command-line interface for toggl.com
Home Page: https://toggl.uhlir.dev
License: Other
A simple command-line interface for toggl.com
Home Page: https://toggl.uhlir.dev
License: Other
Hi, I'm new to toggl.py. Thanks for writing such a great tool!
From reading the usage output of toggl.py -h
I thought that that the [:WORKSPACE] argument would be optional. However without that argument I get the following:
$ toggl.py add "pmwg weekly" @meetings 06:30am 07:30am
Traceback (most recent call last):
File "/Users/mturquette/.local/bin/toggl.py", line 1175, in <module>
CLI().act()
File "/Users/mturquette/.local/bin/toggl.py", line 981, in act
self._add_time_entry(self.args[1:])
File "/Users/mturquette/.local/bin/toggl.py", line 948, in _add_time_entry
project = ProjectList(workspace["name"]).find_by_name(project_name)
UnboundLocalError: local variable 'workspace' referenced before assignment
By specifying my one and only workspace I can make the command succeed:
$ toggl.py add "pmwg weekly" ":Mike Turquette's workspace" @meetings 06:30am 07:30am
pmwg weekly added
I can always write a bash function which wraps toggl.py and does this stuff for me, but I would rather that ~/.togglrc specify a "default workspace" so that I don't have to do that.
File "./toggl.py", line 811, in
sys.exit(main())
File "./toggl.py", line 792, in main
Logger.info( ProjectList() )
File "./toggl.py", line 286, in info
print msg+end,
TypeError: unsupported operand type(s) for +: 'ProjectList' and 'str'
For easier management of time entries, it would be better to employ special syntax, that would refer to time entry not with ID or its description, but by the order they are presented in the listing.
The listing would have numbered lines:
Description Duration Start Stop
1. Some entry 0:04:09 9:07:32 AM 01/21/2019 running
2. Some entry 0:00:11 8:54:57 AM 8:55:08 AM 01/21/2019
3. Some entry 0:02:00 8:54:08 AM 8:56:08 AM 01/21/2019
Then it would be possible to reference the issues like: toggl continue $2
, toggl rm $3
.
Potential problems:
If there is a change in the state from another source (Toggl's web client, other tool using it API calls) the referenced lines may differ and hence you can modify (or worse - delete) lines that you have not intended.
One solution to this problem, could be to store the state of the last command locally and when a call with this syntax, the ID of the referenced line would have to be retrieved from the previously stored state. This could go hand in hand with #86. But even then it could resolve in problems.
Let me know what you think and if you would be interested in such a feature!
$ python2.7 toggl.py ls
Traceback (most recent call last):
File "toggl.py", line 1265, in <module>
CLI().act()
File "toggl.py", line 1039, in act
Logger.info(TimeEntryList())
File "toggl.py", line 294, in info
print("%s%s" % (msg, end)),
File "toggl.py", line 881, in __str__
s += str(entry) + "\n"
UnicodeEncodeError: 'ascii' codec can't encode characters in position 11-12: ordinal not in range(128)
$ python2.7 toggl.py continue "Posição na rede"
toggl.py:808: UnicodeWarning: Unicode equal comparison failed to convert both arguments to Unicode - interpreting them as being unequal
if entry.get('description') == description:
Traceback (most recent call last):
File "toggl.py", line 1265, in <module>
CLI().act()
File "toggl.py", line 1045, in act
self._continue_entry(self.args[1:])
File "toggl.py", line 1102, in _continue_entry
Logger.info("Did not find '%s' in list of entries." % args[0] )
IndexError: list index out of range
Credit to @StanczakDominik
This allows toggl continue Presentation from the command line to work properly.
On my system, passing that in resulted in:
Error: Invalid value for "[DESCR]": Unknown Time entry under specification 'Presentation'!
While the Presentation entry is definitely found in existing entries (as checked via the -d flag). Without the click type check, it works nicely. I'm not sure if I broke any existing functionality with that change, but it hasn't crashed on me yet.Back to the presentation! 😄
When the last (stopped) entry is "entry desc", executing the following:
toggl continue "entry desc"
continues the previous, existing entry without changing its start time - rather than creating a new one with the same info and a start time of now. I would have expected that it is syntatic sugar for toggl start "entry desc"
with the project auto-attached.
I get errors when running the unit tests:
E..............F.F....Removing toggl entries created by the test...
E
======================================================================
ERROR: test_iterator (__main__.TestClientList)
----------------------------------------------------------------------
Traceback (most recent call last):
File "tests.py", line 33, in test_iterator
num_clients = len(self.list.client_list)
TypeError: object of type 'NoneType' has no len()
======================================================================
ERROR: tearDownModule (__main__)
----------------------------------------------------------------------
Traceback (most recent call last):
File "tests.py", line 361, in tearDownModule
if entry.get('description').startswith('unittest_'):
AttributeError: 'NoneType' object has no attribute 'startswith'
======================================================================
FAIL: test_start_simple (__main__.TestTimeEntry)
----------------------------------------------------------------------
Traceback (most recent call last):
File "tests.py", line 220, in test_start_simple
self.assertRaises(Exception, self.entry.start)
AssertionError: Exception not raised
======================================================================
FAIL: test_stop_simple (__main__.TestTimeEntry)
----------------------------------------------------------------------
Traceback (most recent call last):
File "tests.py", line 248, in test_stop_simple
self.assertRaises(Exception, self.entry.start)
AssertionError: Exception not raised
----------------------------------------------------------------------
Ran 22 tests in 52.641s
FAILED (failures=2, errors=2)
The test entries are also not deleted at the end. When running toggl.py ls I also only get the entries created today. Is this expected?
Currently, all rm
subcommands accept only one ID/Name, it would be more user-friendly if they would accept "infinite" number of arguments.
I think this is because my .togglrc
file is old (which I am about to fix), but nevertheless this is a simple bug that should be fixed:
$ toggl
Sent: None
No option 'prefer_token' in section: 'options'
Traceback (most recent call last):
File "/home/kev/g/devkev/toggl-cli/toggl.py", line 1265, in <module>
CLI().act()
File "/home/kev/g/devkev/toggl-cli/toggl.py", line 1039, in act
Logger.info(TimeEntryList())
File "/home/kev/g/devkev/toggl-cli/toggl.py", line 82, in __call__
cls.instance = super(Singleton, cls).__call__(*args, **kw)
File "/home/kev/g/devkev/toggl-cli/toggl.py", line 791, in __init__
self.reload()
File "/home/kev/g/devkev/toggl-cli/toggl.py", line 849, in reload
entries = json.loads( toggl(url, 'get') )
File "/home/kev/g/devkev/toggl-cli/toggl.py", line 319, in toggl
print(r.text)
UnboundLocalError: local variable 'r' referenced before assignment
c:\git\toggl-cli>python --version
Python 3.4.3
c:\git\toggl-cli>python toggl.py
File "toggl.py", line 928
count=0
^
TabError: inconsistent use of tabs and spaces in indentation
In source I observe some signs that tool had automated zsh/bash completion some time ago?
https://github.com/click-contrib/click-completion
So far I am using my self-written "dirty one" which tab completes almost all routine tasks, but if tool has some built-in completion generator - can you share way to activate it ?
https://github.com/Voronenko/dotfiles/blob/master/completions/_toggl.zsh
#compdef toggl
__toggl-workspaces() {
toggl workspaces
}
__toggl-projects() {
toggl projects | awk '{ print $2 }'
}
__toggl-now() {
echo "''d1:20''"
echo "'`date '+%Y-%m-%d %H:%M:%S'`'"
}
__toggl-duration() {
echo "''d1:20''"
}
__toggl-recent-entries() {
toggl ls | grep @ | awk -F'@' '{gsub(/^[ \t\r\n]+/, "", $1);gsub(/[ \t\r\n]+$/, "", $1);print "\""$1"\""}' | sort | uniq
}
_toggl-commands() {
local -a commands
commands=(
'add: DESCR [:WORKSPACE] [@PROJECT | #PROJECT_ID] START_DATETIME (''d''DURATION | END_DATETIME) creates a completed time entry, DURATION = [[Hours:]Minutes:]Seconds'
'add: DESCR [:WORKSPACE] [@PROJECT | #PROJECT_ID] ''d''DURATION creates a completed time entry, with start time DURATION ago, DURATION = [[Hours:]Minutes:]Seconds'
'clients:lists all clients'
'continue: [from DATETIME | ''d''DURATION] restarts the last entry, DURATION = [[Hours:]Minutes:]Seconds'
'continue: DESCR [from DATETIME | ''d''DURATION] restarts the last entry matching DESCR'
'ls:list recent time entries'
'now:print what you''re working on now'
'workspaces:lists all workspaces'
'projects: [:WORKSPACE] lists all projects'
'rm:delete a time entry by id'
'start: DESCR [:WORKSPACE] [@PROJECT | #PROJECT_ID] [''d''DURATION | DATETIME] starts a new entry , DURATION = [[Hours:]Minutes:]Seconds'
'stop:stops the current entry'
'www:visits toggl.com'
)
_arguments -s : $nul_args && ret=0
_describe -t commands 'toggl command' commands && ret=0
}
_toggl() {
local -a nul_args
nul_args=(
'(-h --help)'{-h,--help}'[show help message and exit.]'
'(-q --quiet)'{-q,--quiet}'[don''t print anything]'
'(-v --verbose)'{-v,--verbose}'[print additional info]'
'(-d --debug)'{-d,--debug}'[print debugging output]'
)
local curcontext=$curcontext ret=1
if ((CURRENT == 2)); then
_toggl-commands
else
shift words
(( CURRENT -- ))
curcontext="${curcontext%:*:*}:toggl-$words[1]:"
case $words[1] in
add)
_arguments "2: :($(__toggl-workspaces))" \
"3: :($(__toggl-projects))" \
"4: :($(__toggl-now))" \
"5: :($(__toggl-duration))"
;;
start)
_arguments "2: :($(__toggl-workspaces))" \
"3: :($(__toggl-projects))" \
"4: :($(__toggl-now))"
;;
stop)
_arguments "1: :($(__toggl-now))"
;;
projects)
_arguments "1: :($(__toggl-workspaces))"
;;
continue)
_arguments "1: :($(__toggl-recent-entries))"
;;
esac fi
}
_toggl "$@"
# Local Variables:
# mode: Shell-Script
# sh-indentation: 2
# indent-tabs-mode: nil
# sh-basic-offset: 2
# End:
# vim: ft=zsh sw=2 ts=2 et
If you localtime is behind the standard time,
you will get the following messages when you stop a task.
400 Client Error: Bad Request
Stop time must be after start time
I think use standard time instead of localtime would be better.
So I put ntp() function in the DateAndTime.
And I add utf-8 support also.
I hope this would be useful.
toggl.py | 47 +++++++++++++++++++++++++++++++++++++++++++----
1 file changed, 43 insertions(+), 4 deletions(-)
diff --git toggl.py toggl.py
index 03761df..de3a453 100755
--- toggl.py
+++ toggl.py
@@ -1,4 +1,5 @@
#!/usr/bin/env python
+# -*- coding: utf-8 -*-
"""
toggl.py
@@ -25,6 +26,10 @@ import requests
import sys
import time
import urllib
+import socket
+import struct
+from socket import AF_INET, SOCK_DGRAM
+
TOGGL_URL = "https://www.toggl.com/api/v8"
VERBOSE = False # verbose output?
@@ -203,7 +208,11 @@ class DateAndTime(object):
"""
Returns "now" as a localized datetime object.
"""
- return self.tz.localize( datetime.datetime.now() )
+ t = self.ntp()
+ if t:
+ return self.tz.localize( datetime.datetime.fromtimestamp(t) )
+ else:
+ return self.tz.localize( datetime.datetime.now() )
def parse_local_datetime_str(self, datetime_str):
"""
@@ -236,6 +245,36 @@ class DateAndTime(object):
datetime.timedelta(days=1) # subtract one day from today at midnight
)
+ def ntp(self):
+ """
+ Returns the POSIX timestamp.
+ """
+ # ref: http://blog.mattcrampton.com/post/88291892461/query-an-ntp-server-from-python
+ # TODO: behind a proxy
+ # TODO: custom ntp conf
+ host = "pool.ntp.org"
+ port = 123
+ buf = 1024
+ address = (host,port)
+ msg = '\x1b' + 47 * '\0'
+
+ # reference time (in seconds since 1900-01-01 00:00:00)
+ TIME1970 = 2208988800L # 1970-01-01 00:00:00
+
+ # connect to server
+ client = socket.socket(AF_INET, SOCK_DGRAM)
+ client.sendto(msg, address)
+ client.settimeout(2)
+
+ for x in range(1, 3):
+ try:
+ msg, address = client.recvfrom( buf )
+ except:
+ pass
+
+ t = struct.unpack("!12I", msg )[10]
+ return t - TIME1970 if t else 0
+
#----------------------------------------------------------------------------
# Logger
#----------------------------------------------------------------------------
@@ -764,7 +803,7 @@ class TimeEntryList(object):
returned.
"""
for entry in reversed(self.time_entries):
- if entry.get('description') == description:
+ if entry.get('description') == description.decode('utf-8'):
return entry
return None
@@ -829,7 +868,7 @@ class TimeEntryList(object):
s += date + "\n"
duration = 0
for entry in days[date]:
- s += str(entry) + "\n"
+ s += unicode(entry) + "\n"
duration += entry.normalized_duration()
s += " (%s)\n" % DateAndTime().elapsed_time(int(duration))
return s.rstrip() # strip trailing \n
@@ -1124,7 +1163,7 @@ class CLI(object):
entry = TimeEntryList().now()
if entry != None:
- Logger.info(str(entry))
+ Logger.info(unicode(entry))
else:
Logger.info("You're not working on anything right now.")
It would be helpful to see the time that an entry was started or continued.
Currently, there is no detailed API wrapper's reference, where detailed signatures are explained for the wrapper classes.
There are few criteria from my side:
Currently, I am aware of pydoc-markdown, but the current version does not support documentation of class's attributes. There is a new version being developed which will support it but for now, I am not aware of any other solutions, so if you know about any please let me know!
def act(self) in toggle.py checks self.args[0] for strings such as 'ls'.
if len(self.args) == 0 or self.args[0] == "ls":
Logger.info(TimeEntryList())
elif self.args[0] == "add":
self._add_time_entry(self.args[1:])
.....
I had to change this to self.args[1] as self.args[0] is the program name (Linux).
Adding print(self.args[0]) to act(self) at line 1041 reveals this.
Is this a Windows/Linux problem?
I ended up simply replacing most occurrences of 'self.args[0]' with 'self.args[1]'.
hi think this program should chmod 600 ~/.togglrc after it has created the file. it contains password, and user may forgot (or not aware of) that this file should be protected for unwanted eyes
Clean 18.04 LTS image, for example digital ocean
python3 --version
Python 3.6.7
pip3 --version
pip 9.0.1 from /usr/lib/python3/dist-packages (python 3.6)
pip3 install togglCli==2.0.2
...
root@ubuntu-s-1vcpu-1gb-ams3-01:~# toggl
Traceback (most recent call last):
File "/usr/local/lib/python3.6/dist-packages/pbr/version.py", line 442, in _get_version_from_pkg_resources
provider = pkg_resources.get_provider(requirement)
File "/usr/lib/python3/dist-packages/pkg_resources/__init__.py", line 344, in get_provider
return working_set.find(moduleOrReq) or require(str(moduleOrReq))[0]
File "/usr/lib/python3/dist-packages/pkg_resources/__init__.py", line 892, in require
needed = self.resolve(parse_requirements(requirements))
File "/usr/lib/python3/dist-packages/pkg_resources/__init__.py", line 778, in resolve
raise DistributionNotFound(req, requirers)
pkg_resources.DistributionNotFound: The 'toggl' distribution was not found and is required by the application
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/usr/local/bin/toggl", line 7, in <module>
from toggl.toggl import main
File "/usr/local/lib/python3.6/dist-packages/toggl/__init__.py", line 3, in <module>
VERSION = VersionInfo('toggl').semantic_version()
File "/usr/local/lib/python3.6/dist-packages/pbr/version.py", line 462, in semantic_version
self._semantic = self._get_version_from_pkg_resources()
File "/usr/local/lib/python3.6/dist-packages/pbr/version.py", line 449, in _get_version_from_pkg_resources
result_string = packaging.get_version(self.package)
File "/usr/local/lib/python3.6/dist-packages/pbr/packaging.py", line 848, in get_version
name=package_name))
Exception: Versioning for this project requires either an sdist tarball, or access to an upstream git repository. It's also possible that there is a mismatch between the package name in setup.cfg and the argument given to pbr.version.VersionInfo. Project name toggl was given, but was not able to be found.
Hi there! Cool tool you have created.
I have implemented pomodoro support, but had difficult time trying to understand duration argument format. My 10+ year of experience in bash/python was useless there, so maybe you can improve duration format in some way?
In docs we have:
add DESCR [:WORKSPACE] [@PROJECT] START_DATETIME ('d'DURATION | END_DATETIME)
[..]
add DESCR [:WORKSPACE] [@PROJECT] 'd'DURATION
[..]
DURATION = [[Hours:]Minutes:]Seconds
So I was like "wat?". I tried examples like:
* 'd'20min (at first I haven't noticed format left in last line of help, so used Toggl syntax like in their app)
* 'd'00:25:00
* 00:25:00
* and a couple of more.
So my suggestion would be:
I think this can be improved in many ways, so my "suggestions" are not final in any way. Hope to discuss this with you in the future.
Hi,
I am testing out the cli and when I try to use the add
command I just see the help menu printed
Example command:
toggl add "Code reviews, and week prep" :Company @Project
Whereas when I start
a new task to be logged:
toggl start "Code reviews, and week prep" :Company @Project
It works.
Seems like the add command line argument is not being picked up. Any ideas?
The error:
./toggl.py ls
Traceback (most recent call last):
File "./toggl.py", line 1265, in <module>
CLI().act()
File "./toggl.py", line 1039, in act
Logger.info(TimeEntryList())
File "./toggl.py", line 294, in info
print("%s%s" % (msg, end)),
File "./toggl.py", line 881, in __str__
s += str(entry) + "\n"
File "./toggl.py", line 740, in __str__
project = ProjectList().find_by_id(self.data['pid'])
File "./toggl.py", line 474, in find_by_id
for project in self:
File "/usr/lib/python2.7/dist-packages/six.py", line 558, in next
return type(self).__next__(self)
File "./toggl.py", line 499, in __next__
if self.iter_index >= len(self.project_list):
TypeError: object of type 'NoneType' has no len()
Continuing an entry across days doesn't seem to work correctly. It assumes that the entry has been in progress since the original start time.
Hi there, nice project!
I'm bumping into what I assume is a 'events no older than 1 month' limit or similar when calling api.TimeEntry.objects.all()
, wondering if there's a way around that?
If a TimeEntry
currently has tags associated to it, they are stored as a generic SetField
. To link the proper Tag model to the time entries the tags
field of TimeEntry
should probably change to a MappingField
, but cardinality 'many'
has not yet been implemented for those.
I'd like to look into it, but I'm stuck with what has to be done to implement that. Any pointers would be very helpful.
Tags currently don't have a separate model, while they do have their own set of api endpoints.
toggl.py now
work fine if not working on a project
when working on a project it does :
Traceback (most recent call last):
File "toggl.py", line 1233, in <module>
CLI().act()
File "toggl.py", line 1015, in act
self._list_current_time_entry()
File "toggl.py", line 1177, in _list_current_time_entry
Logger.info(str(entry))
File "toggl.py", line 716, in __str__
project_name = " @%s " % ProjectList().find_by_id(self.data['pid'])['name']
TypeError: 'NoneType' object has no attribute '__getitem__'
The config should live at $XDG_CONFIG_HOME/togglrc
by default.
Note that a small refactor should be included in this PR to minimize the number of times the config path is hardcoded in: https://github.com/AuHau/toggl-cli/search?l=Python&q=togglrc
To install through pip.
I don't think this configuration variable is being used. This should be read from the toggl API, since it's a global user setting stored in the user's profile.
You chose a copyright license for this tool. So that would mean that I cannot fork unless you give permission. And if I happen to contribute, my PR would also get your property, right? Did you consider swithing to a more liberal license? Or do you just not enforce your copyrights (there are already 14 forks and I could not find any issues asking for permission)?
After the inclusion of the ical
feature in #43, the cli looks as though it supports two different datetime formats per --help
. DATETIME
, the format existing prior to ical
, which is I believe local time and is converted to UTC+TZ via the DateAndTime
object, and the ical
specified starttime/endtime of YYYY-MM-DDThh:mm:ss+TZ:00
.
For consistency there should probably be only one DATETIME format. I'm not sure why ical
needs the full TZ specifier (and I don't know how ical
is supposed to work), perhaps @martinm76 can chime in.
The stop function without parameters always adds 1 hour to the actual duration for no reason.
While the output is correct:
./toggl.sh -v stop
"general" stopped at 02:05PM and lasted for 17.0m48.0s
Toggle desktop and the website then say that the work has a duration of 1 hour plus 17 minutes.
I'm not sure if daylight saving time/summer time is part of the problem but using utcnow() solves the issue for me:
def now(self):
return self.tz.localize( datetime.datetime.now() )
def now(self):
return self.tz.localize( datetime.datetime.utcnow() )
Field created_with
on TimeEntry entity is only write-only, in a sense of the field is not part of fields returned from Toggl API when querying for list/detail operation.
This field serves Toggl developers to identify the creators of entries that are problematic to contact them.
When I try to run toggle.py, I get a KeyError
Traceback (most recent call last):
File "toggl.py", line 1175, in <module>
CLI().act()
File "toggl.py", line 979, in act
Logger.info(TimeEntryList())
File "toggl.py", line 271, in info
print("%s%s" % (msg, end)),
File "toggl.py", line 832, in __str__
s += str(entry) + "\n"
File "toggl.py", line 712, in __str__
s = "%s%s%s%s" % (is_running, self.data['description'], project_name,
KeyError: 'description'
With the introduction of Django ORM like framework, it allows easy usage of the API wrapper classes, but it also introduces quiet bit overhead for fetching resources, which resolve in expensive API calls. Currently, for CLI usage, this should not be a problem, but if somebody would like to use the API wrappers in more intensive ways he could reach the API's throttling limitations quiet soon.
To address this problem, I would like to add support for caching results. This should be most probably on the low level of toggl.utils.other.toggl
calls.
Toggl's API supports bulk fetching of data using https://www.toggl.com/api/v8/me?with_related_data=true
, which could be used for population of the cache.
Also, the same endpoint supports since=<timestamp>
, that fetches changed data, which could be used for preventing the cached data to become stale.
Changing the timezone to Europe/Stockholm in .togglrc gives me this error when trying to run toggl.py:
Traceback (most recent call last):
File "toggl.py", line 1063, in <module>
CLI().act()
File "toggl.py", line 893, in act
Logger.info(TimeEntryList())
File "toggl.py", line 271, in info
print("%s%s" % (msg, end)),
File "toggl.py", line 752, in __str__
s += str(entry) + "\n"
UnicodeEncodeError: 'ascii' codec can't encode character u'\xf6' in position 6: ordinal not in range(128)
toggl add test :MyWorkSpace @"My Awesome - Project" 'd'10:00
fails with a project not found RuntimeError.
Good question!
I have multiple projects with the same Tag Name
@monthly Marketing Work - Client A
@monthly Marketing Work - Client B
But anytime I try to run
toggle start "Fix Site" @monthly
It chooses Client B, and I'm not sure how to switch it to Client A. I tried "@monthly Marketing Work - Client A", but that didn't work either.
My timezone is set for Vancouver and I'm currently in Mexico, 2 hours ahead. If I run continue, a new entry is started with the correct time. If I then run stop, the entry is stopped 2 hours ahead.
I'm from Sweden. In Sweden we use weird letters like åäö. Så when clients are called "Lantmännen" with projects called "Höstkampanj" you run into problems with toggl-cli. This happens when I try to access my company's toggl:
toggl clients
Traceback (most recent call last):
File "toggl.py", line 1181, in
CLI().act()
File "toggl.py", line 989, in act
print(ClientList())
UnicodeEncodeError: 'ascii' codec can't encode character u'\xe4' in position 281: ordinal not in range(128)
I'm not very familiar with python, but this seems to do the trick (line 493):
return s.rstrip().encode('utf-8')
Since I'm no pro at python, nor git, I think it's smarter if someone else solved this issue properly.
Where "n" is a order number for a last task.
Hi there..
I have modified toggl.py a bit to better support outputting utf-8 and also to generate ical entries and being able to select a periode for which to get entries rather than just yesterday and today.
I am unsure how to get the patch uploaded properly. Do I need to download it via git and submit my changes that way? The formatting will probably get lost, but below is a dump of the patch:
--- ../toggl-cli-master/toggl.py 2015-09-24 17:46:44.000000000 +0200
+++ toggl.py 2015-10-16 10:05:55.629874565 +0200
@@ -269,7 +269,7 @@
Prints msg if the current logging level >= INFO.
"""
if Logger.level >= Logger.INFO:
- print("%s%s" % (msg, end)),
+ print("%s%s" % (unicode(msg), unicode(end))),
#----------------------------------------------------------------------------
# toggl
@@ -486,8 +486,8 @@
if 'cid' in project:
for client in clients:
if project['cid'] == client['id']:
- client_name = " - %s" % client['name']
- s = s + ":%s @%s%s\n" % (self.workspace['name'], project['name'], client_name)
+ client_name = " - %s" % unicode(client['name'])
+ s = s + ":%s @%s%s\n" % (unicode(self.workspace['name']), unicode(project['name']), unicode(client_name))
return s.rstrip() # strip trailing \n
#----------------------------------------------------------------------------
@@ -542,6 +542,7 @@
if project == None:
raise RuntimeError("Project '%s' not found." % project_name)
self.data['pid'] = project['id']
+ self.data['project_name'] = project['name']
if duration is not None:
self.data['duration'] = duration
@@ -744,11 +745,11 @@
__metaclass__ = Singleton
- def __init__(self):
+ def __init__(self, start_time=DateAndTime().start_of_yesterday().isoformat('T'), stop_time=DateAndTime().last_minute_today().isoformat('T'), project_name=None):
"""
Fetches time entry data from toggl.
"""
- self.reload()
+ self.reload(start_time,stop_time)
def __iter__(self):
"""
@@ -788,15 +789,15 @@
return entry
return None
- def reload(self):
+ #def reload(self,start_time,stop_time,project_name):
+ def reload(self,start_time,stop_time):
"""
Force reloading time entry data from the server. Returns self for
method chaining.
"""
# Fetch time entries from 00:00:00 yesterday to 23:59:59 today.
url = "%s/time_entries?start_date=%s&end_date=%s" % \
- (TOGGL_URL, urllib.parse.quote(DateAndTime().start_of_yesterday().isoformat('T')), \
- urllib.parse.quote(DateAndTime().last_minute_today().isoformat('T')))
+ (TOGGL_URL, urllib.parse.quote(start_time), urllib.parse.quote(stop_time))
Logger.debug(url)
entries = json.loads( toggl(url, 'get') )
@@ -830,12 +831,120 @@
s += date + "\n"
duration = 0
for entry in days[date]:
- s += str(entry) + "\n"
+ s += unicode(entry) + "\n"
duration += entry.normalized_duration()
s += " (%s)\n" % DateAndTime().elapsed_time(int(duration))
return s.rstrip() # strip trailing \n
#----------------------------------------------------------------------------
+# IcalEntryList
+#----------------------------------------------------------------------------
+class IcalEntryList(object):
+ """
+ A singleton list of recent TimeEntry objects.
+ """
+
+ __metaclass__ = Singleton
+
+ def __init__(self, start_time=DateAndTime().start_of_yesterday().isoformat('T'), stop_time=DateAndTime().last_minute_today().isoformat('T'), project_name=None):
+ """
+ Fetches time entry data from toggl.
+ """
+ self.reload(start_time,stop_time)
+
+ def __iter__(self):
+ """
+ Start iterating over the time entries.
+ """
+ self.iter_index = 0
+ return self
+
+ def find_by_description(self, description):
+ """
+ Searches the list of entries for the one matching the given
+ description, or return None. If more than one entry exists
+ with a matching description, the most recent one is
+ returned.
+ """
+ for entry in reversed(self.time_entries):
+ if entry.get('description') == description:
+ return entry
+ return None
+
+ def next(self):
+ """
+ Returns the next time entry object.
+ """
+ if self.iter_index >= len(self.time_entries):
+ raise StopIteration
+ else:
+ self.iter_index += 1
+ return self.time_entries[self.iter_index-1]
+
+ def now(self):
+ """
+ Returns the current time entry object or None.
+ """
+ for entry in self:
+ if int(entry.get('duration')) < 0:
+ return entry
+ return None
+
+ #def reload(self,start_time,stop_time,project_name):
+ def reload(self,start_time,stop_time):
+ """
+ Force reloading time entry data from the server. Returns self for
+ method chaining.
+ """
+ # Fetch time entries from 00:00:00 yesterday to 23:59:59 today.
+ url = "%s/time_entries?start_date=%s&end_date=%s" % \
+ (TOGGL_URL, urllib.parse.quote(start_time), urllib.parse.quote(stop_time))
+ Logger.debug(url)
+ entries = json.loads( toggl(url, 'get') )
+
+ # Build a list of entries.
+ self.time_entries = []
+ for entry in entries:
+ te = TimeEntry(data_dict=entry)
+ Logger.debug(te.json())
+ Logger.debug('---')
+ self.time_entries.append(te)
+
+ # Sort the list by start time.
+ sorted(self.time_entries, key=lambda entry: entry.data['start'])
+ return self
+
+ def __str__(self):
+ """
+ Returns a human-friendly list of recent time entries.
+ """
+ # Sort the time entries into buckets based on "Month Day" of the entry.
+ days = { }
+ for entry in self.time_entries:
+ start_time = DateAndTime().parse_iso_str(entry.get('start')).strftime("%Y-%m-%d")
+ if start_time not in days:
+ days[start_time] = []
+ days[start_time].append(entry)
+
+ # For each day, create calendar entries for each toggle entry.
+ s="BEGIN:VCALENDAR\nX-WR-CALNAME:Toggl Entries\nVERSION:2.0:CALSCALE:GREGORIAN\nMETHOD:PUBLISH\n"
+ count=0
+ for date in sorted(days.keys()):
+ for entry in days[date]:
+ #print vars(entry) + "\n"
+ s += "BEGIN:VEVENT\nDTSTART:%sZ\n" % entry.get('start')
+ s += "DTEND:%sZ\n" % entry.get('stop')
+ if entry.has('pid') == True:
+ s += "SUMMARY:%s\nDESCRIPTION:%s\nSEQUENCE:%i\nEND:VEVENT\n" % (unicode(ProjectList().find_by_id(entry.data['pid'])['name']), unicode(entry.get('description')), count)
+ else:
+ s += "SUMMARY:%s\nDESCRIPTION:%s\nSEQUENCE:%i\nEND:VEVENT\n" % (unicode(entry.get('description')), unicode(entry.get('description')), count)
+ count += 1
+ #duration += entry.normalized_duration()
+ #s += " (%s)\n" % DateAndTime().elapsed_time(int(duration))
+ s += "END:VCALENDAR\n"
+ return s.rstrip() # strip trailing \n
+
+#----------------------------------------------------------------------------
# User
#----------------------------------------------------------------------------
class User(object):
@@ -898,7 +1007,8 @@
" add DESCR [:WORKSPACE] [@PROJECT] START_DATETIME ('d'DURATION | END_DATETIME)\n\tcreates a completed time entry\n"
" clients\n\tlists all clients\n"
" continue DESCR\n\trestarts the given entry\n"
- " ls\n\tlist recent time entries\n"
+ " ls [starttime endtime]\n\tlist (recent) time entries\n"
+ " ical [starttime endtime]\n\tdump iCal list of (recent) time entries\n"
" now\n\tprint what you're working on now\n"
" workspaces\n\tlists all workspaces\n"
" projects [:WORKSPACE]\n\tlists all projects\n"
@@ -907,7 +1017,9 @@
" stop [DATETIME]\n\tstops the current entry\n"
" www\n\tvisits toggl.com\n"
"\n"
- " DURATION = [[Hours:]Minutes:]Seconds\n")
+ " DURATION = [[Hours:]Minutes:]Seconds\n"
+ " starttime/endtime = YYYY-MM-DDThh:mm:ss+TZ:00\n"
+ " e.g. starttime = 2015-10-15T00:00:00+02:00\n")
self.parser.add_option("-q", "--quiet",
action="store_true", dest="quiet", default=False,
help="don't print anything")
@@ -979,8 +1091,14 @@
"""
Performs the actions described by the list of arguments in self.args.
"""
- if len(self.args) == 0 or self.args[0] == "ls":
+ if len(self.args) == 3 and self.args[0] == "ls":
+ Logger.info(TimeEntryList(self.args[1],self.args[2]))
+ elif len(self.args) == 0 or self.args[0] == "ls":
Logger.info(TimeEntryList())
+ elif len(self.args) == 1 and self.args[0] == "ical":
+ Logger.info(IcalEntryList())
+ elif len(self.args) == 3 or self.args[0] == "ical":
+ Logger.info(IcalEntryList(self.args[1],self.args[2]))
elif self.args[0] == "add":
self._add_time_entry(self.args[1:])
elif self.args[0] == "clients":
@@ -1006,7 +1124,7 @@
def _show_projects(self, args):
workspace_name = self._get_workspace_arg(args, optional=True)
- print(ProjectList(workspace_name))
+ print(unicode(ProjectList(workspace_name)))
def _continue_entry(self, args):
"""
I had issues regarding the ssl certificate, when running the app:
ssl_.py:90: InsecurePlatformWarning: A true SSLContext object is not available. This
prevents urllib3 from configuring SSL appropriately and may cause certain SSL
connections to fail. For more information, see
https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning.
fixed it installing the following:
pip install pyopenssl ndg-httpsclient pyasn1
In case it helps someone.
Refactor the program into objects.
The last in-file configuration option.
Sometimes botches the times:
$ t ls
Nov 03
email 56m42s...
$ t continue email
email continued at 02:31PM
$ t stop
email stopped at 02:37PM
$ t ls
Nov 03
email 7h3m16s
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.