PK!S `timew/__init__.pyfrom .duration import Duration from .exceptions import DurationError, IntervalError, TimeWarriorError from .interval import Interval from .timewarrior import TimeWarrior PK!_timew/__version__.py__version__ = "0.0.21" PK!@A0HHtimew/duration.pyfrom datetime import timedelta from .exceptions import DurationError class Duration: """ Wrapper class for a Timewarrior Duration. """ def __init__(self, delta): if not type(delta) is timedelta: raise DurationError('Duration must be of type datetime.timedelta') if delta < timedelta(0): raise DurationError('Duration cannot be negative') self.__delta = delta def __repr__(self): return 'PT%dS' % self.__delta.total_seconds() def __str__(self): return 'PT%dS' % self.__delta.total_seconds() PK!6~~timew/exceptions.pyimport sys class DurationError(Exception): def __init__(self, message): super().__init__(message) class IntervalError(Exception): def __init__(self, message): super().__init__(message) class TimeWarriorError(Exception): def __init__(self, command, stderr, code): self.command = command self.stderr = stderr self.code = code PK!)!11timew/interval.pyimport re from datetime import datetime, timedelta from dateutil.parser import parse from .exceptions import IntervalError class Interval: """ Wrapper class for a Timewarrior Interval. """ def __init__(self, start_time=None, end_time=None, duration=None): """ 2 of the 3 keyword arguments need to be supplied. If all 3 are supplied, start_time and end_time takes precedence over duration Arguments: start_time (datetime.date/str): Start time for interval end_time (datetime.date/str): End time for interval duration (timew.Duration): Duration of the interval """ # Test for valid input if type(start_time) is str: start_time = parse(start_time) if type(end_time) is str: end_time = parse(end_time) if(duration and not re.match("\\d+[dDhHmMsS]", duration)): raise IntervalError('Duration value "%s" is invalid.' % (duration)) self.__interval = '' if(start_time and end_time): self.__interval = 'from %s - %s' % (start_time.strftime('%Y%m%dT%H%M%S'), end_time.strftime('%Y%m%dT%H%M%S')) elif(start_time and duration): self.__interval = '%s after %s' % (duration, start_time.strftime('%Y%m%dT%H%M%S')) elif(end_time and duration): self.__interval = '%s before %s' % (duration, end_time.strftime('%Y%m%dT%H%M%S')) else: raise IntervalError( 'At least 2 arguments need to be supplied: start time, end time, duration') def __repr__(self): return self.__interval def __str__(self): return self.__interval PK!FU  timew/timewarrior.pyimport json from datetime import datetime, timedelta from subprocess import PIPE, Popen from .exceptions import TimeWarriorError from .interval import Interval class TimeWarrior: """ """ def __init__(self, bin='timew', simulate=False): self.bin = bin self.simulate = simulate def cancel(self): """If there is an open interval, it is abandoned.""" return self.__execute('cancel') def cont(self, id): """Resumes tracking of closed intervals. Args: id (int): The Timewarrior id to be continued """ return self.__execute('continue @%d' % id) def delete(self, id): """Deletes an interval. Args: id (int): The Timewarrior id to be deleted """ return self.__execute('delete', '@%d' % id) def join(self, id1, id2): """Joins two intervals, by using the earlier of the two start times, and the later of the two end times, and the combined set of tags. Args: id1 (int): The first Timewarrior id to be joined id2 (int): The second Timewarrior id to be joined """ return self.__execute('join', '@%d' % id1, '@%d' % id2) def lengthen(self, id, duration): """Defer the end date of a closed interval. Args: id (int): The Timewarrior id duration (timew.Duration): The duration to lengthen the interval by """ return self.__execute('lengthen', '@%d' % id, '%s' % str(duration)) def move(self, id, time): """Reposition an interval at a new start time. Args: id (int): The Timewarrior id time (datetime): The new start time for the interval """ return self.__execute('move', '@%d' % id, self.__strfdatetime(time)) def shorten(self, id, duration): """Advance the end date of a closed interval. Args: id (int): The Timewarrior id duration (timew.Duration): The duration to shorten the interval by """ return self.__execute('shorten', '@%d' % id, '%s' % str(duration)) def list(self, start_time=None, end_time=datetime.now()): """export the timewarrior entries for interval Args: start_time (datetime, optional): start of interval to list entries for. end_time (datetime, optional): start of interval to list entries for. Returns a list of database entries formatted like the following [ {"id" : 1,"start":"20190123T092300Z","tags":["watering new plants","gardening"]}, {"id" : 2,"start":"20190123T085000Z","end":"20190123T092256Z","tags":["helped plant roses","gardening"]} ] the above contains a started (but not ended) entry with id=1; and an ended entry with id=2 """ if not start_time: now = datetime.now() start_time = datetime(now.year, now.month, now.day) end_time = datetime(now.year, now.month, now.day, 23, 59, 59) interval = Interval(start_time=start_time, end_time=end_time) cmd = f'export {interval}' out = self.__execute(*(cmd.split())) if self.simulate: return out data = json.loads(out[0]) data.reverse() counter = 1 for d in data: d["id"] = counter counter += 1 return data def summary(self, start_time=None, end_time=None): """export the timewarrior entries for interval Args: start_time (datetime, optional): start of interval to list entries for. end_time (datetime, optional): start of interval to list entries for. Returns a list of database entries formatted like the following [ {"id" : 1,"start":"20190123T092300Z","tags":["watering new plants","gardening"]}, {"id" : 2,"start":"20190123T085000Z","end":"20190123T092256Z","tags":["helped plant roses","gardening"]} ] the above contains a started (but not ended) entry with id=1; and an ended entry with id=2 """ return self.list(start_time, end_time) def split(self, id): """Splits an interval into two equally sized adjacent intervals, having the same tags. Args: id (int): The Timewarrior id to split """ return self.__execute('split', '@%d' % id) def start(self, time=datetime.now(), tags=None): """Begins tracking using the current time with any specified set of tags. Args: time (datetime): The time to start the interval tags (list): The list of tags to apply to the interval """ args = ['start', self.__strfdatetime(time)] if(tags): for tag in tags: args.append('"%s"' % tag) return self.__execute(*args) def stop(self, tags=None): """Stops tracking time. If tags are specified, then they are no longer tracked. If no tags are specified, all tracking stops. Args: tags (int): The Timewarrior id tags (list): The list of tags to stop tracking """ args = ['stop'] if tags: if isinstance(tags, type(list())): for tag in tags: args.append('"%s"' % tag) else: args.append(f"@{tags}") return self.__execute(*args) def tag(self, id, tags): """Adds a tag to an interval. Args: id (int): The Timewarrior id tags (list): The list of tags to add to the interval """ args = ['tag', '@%d' % id] for tag in tags: args.append('"%s"' % tag) return self.__execute(*args) def track(self, start_time, end_time=None, tags=None): """The track command is used to add tracked time in the past. Perhaps you forgot to record time, or are just filling in old entries. Args: start_time (datetime): The task start time. end_time (datetime, optional): The task end time. (required if duration not given) duration (timew.Timedelta, optional): The task duration. (required if task not given) tags (list of string): The tags Raises: TimewarriorError: Timew command errors """ args = ['track'] interval = Interval(start_time=start_time, end_time=end_time) args.append(str(interval)) if tags: for tag in tags: args.append('"%s"' % tag) return self.__execute(*args) def untag(self, id, tag): """Remove a tag from an interval Args: id (int): The Timewarrior id tag (str): The tag to remove """ return self.__execute(args) def __strftimedelta(self, duration): if type(duration) is timedelta: return 'PT%dS' % duration.total_seconds() else: return duration def __strfdatetime(self, dt): if type(dt) is datetime: return dt.strftime('%Y%m%dT%H%M%S') else: return dt def __export(self): stdout, stderr = self.__execute('export') data = json.loads(stdout) data.reverse() return data def __execute(self, *args): """ Execute a given timewarrior command with arguments Returns a 2-tuple of stdout and stderr (respectively). """ command = [self.bin] + list(args) if(self.simulate): return ' '.join(command) try: proc = Popen( command, stdout=PIPE, stderr=PIPE, ) stdout, stderr = proc.communicate() except OSError as e: if e.errno == errno.ENOENT: raise OSError("Unable to find the '%s' command-line tool." % (self.bin)) raise if proc.returncode != 0: raise TimeWarriorError(command, stderr.strip().decode(), proc.returncode) return stdout.strip().decode(), stderr.strip().decode() PK!DhZ44timew-0.0.21.dist-info/LICENSEMIT License Copyright (c) 2018 Tjaart van der Walt Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!HڽTUtimew-0.0.21.dist-info/WHEEL A н#Z;/"d&F[xzw@Zpy3Fv]\fi4WZ^EgM_-]#0(q7PK!H O timew-0.0.21.dist-info/METADATAUmo6_qh>T,Y8jYbޒհu@P,L$Q#$;R-wqN =fTS&E hbL6ge/LyM1Ln"J{0%{b(sVhXh+q C'tZhV,*HA=l[A|ZL,xBֻ1| TcaQ+r{+Ihٌ W'?1+C;Z4#n.,k!J咩DRN ae] G> 䨑M|6]v4{#4˨ GMz͕P=x(⋳I\SnS~G}W&huV2j",ܴhh?Ex˺䖰lZp]Bnoo@I [{b+7]2ߖ3g4̦v>\L/4/3l7ɤ-lz=X]vL> cc-p <qz@ӕ߼kz%K3*~!  i"x,8%xwv"saG-6ttT{PK!H(-timew-0.0.21.dist-info/RECORDuҹ@἟m,!AHєBE5Oό}tr/?%!35y $W.ŪiHZ<ڮo" Z:1ф[{$Yx7o 6"Y^` yEtӊsknY{ؒUtO/Ankz䄳!_QGj^(^Q'BkLoBz#,Yws|gep& 6.^!l_A߄l%فhhʏU 57SS1/XsYn(Tu%[iP{dˬ\:NS~ f7lIVoͥ oV"l xiNRc Yq:[K~#{m#}FuG 235P\X/KQ0wN~"#t-Ծ9wW`|+acײ.aEYJ("u{ PK!S `timew/__init__.pyPK!_timew/__version__.pyPK!@A0HH"timew/duration.pyPK!6~~timew/exceptions.pyPK!)!11Htimew/interval.pyPK!FU   timew/timewarrior.pyPK!DhZ44,timew-0.0.21.dist-info/LICENSEPK!HڽTUX1timew-0.0.21.dist-info/WHEELPK!H O 1timew-0.0.21.dist-info/METADATAPK!H(-6timew-0.0.21.dist-info/RECORDPK  8