1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
|
#!/usr/bin/env python
# timeclock, a time-keeping program based on the Ledger library
#
# Copyright (c) 2003-2004, New Artisans LLC. All rights reserved.
#
# This program is made available under the terms of the BSD Public
# License. See the LICENSE file included with the distribution for
# details and disclaimer.
#
# This program implements a simple timeclock, using the identical
# format as my timeclock.el module for Emacs (which is part of the
# Emacs 21 distribution). This allows you to use either this script
# or that module for creating time events.
#
# Usage is very simple: Set the environment variable TIMELOG to the
# path to your timelog file (if this variable is unset, any events
# created will simply be printed to stdout). Once this variable is
# set:
#
# timeclock in "project" what aspect of the project will I do today
# timeclock out what did I accomplish
#
# The description text is optional, but the project is required when
# clocking in. This project should be a account name, which means it
# can use ":" to separate the project from the task, for example:
#
# timeclock in Client:Meetings at the code review meeting
#
# To generate a balance report of time spent, use "timeclock" with no
# arguments, or "timeclock balance". The options available are the
# same as those used for ledger.
import os
import sys
import string
import time
true, false = 1, 0
from ledger import *
home = os.getenv ("HOME")
config.init_file = home + "/.timeclockrc";
config.cache_file = home + "/.timeclock-cache";
# Define some functions for reporting time quantities
workday = 8 * 60 * 60 # length of a nominal workday
def secstr (secs):
return "%d:%02d" % (abs (secs) / 60 / 60, (abs (secs) / 60) % 60)
def daystr (amt):
dy = int (amt) / int (workday)
amt = amt - (dy * workday)
if dy >= 5:
wk = dy / 5
dy = dy % 5
if dy: amt = "%sw %sd %s" % (wk, dy, secstr (amt))
else: amt = "%sw %s" % (wk, secstr (amt))
else:
if dy: amt = "%sd %s" % (dy, secstr (amt))
else: amt = secstr (amt)
return amt
def adaystr (details):
result = ""
for amt in account_xdata(details.account).total:
if amt.commodity ().symbol == "s":
result = daystr (float (amt))
break
return result
config.amount_expr = "{1.00h}*(a/{3600.00h})"
config.total_expr = "{1.00h}*(O/{3600.00h})"
config.balance_format = "%20@adaystr() %8T %2_%-a\n";
# Help text specific to timeclock
def show_version (arg):
print """Timeclock, a command-line timekeeping tool
Copyright (c) 2003-2004, New Artisans LLC. All rights reserved.
This program is made available under the terms of the BSD Public
License. See the LICENSE file included with the distribution for
details and disclaimer."""
sys.exit (0)
def option_help (arg):
print """usage: timeclock [options] COMMAND [ACCT REGEX]...
Basic options:
-h, --help display this help text
-v, --version show version information
-i, --init FILE initialize ledger by loading FILE (def: ~/.ledgerrc)
--cache FILE use FILE as a binary cache when --file is not used
-f, --file FILE read ledger data from FILE
-o, --output FILE write output to FILE
Report filtering:
-b, --begin DATE set report begin date
-e, --end DATE set report end date
-c, --current show only current and past entries (not future)
-C, --cleared consider only cleared transactions
-U, --uncleared consider only uncleared transactions
-R, --real consider only real (non-virtual) transactions
-Z, --actual consider only actual (non-automated) transactions
-r, --related calculate report using related transactions
Output customization:
-F, --format STR use STR as the format; for each report type, use:
--balance-format --register-format
--plot-amount-format --plot-total-format
-y, --date-format STR use STR as the date format (def: %Y/%m/%d)
--wide for the default register report, use 132 columns
-E, --empty balance: show accounts with zero balance
-n, --collapse register: collapse entries with multiple transactions
-s, --subtotal balance: show sub-accounts; register: show subtotals
-S, --sort EXPR sort report according to the value expression EXPR
-p, --period STR report using the given period
--period-sort EXPR sort each report period's entries by EXPR
--dow show a days-of-the-week report
-W, --weekly show weekly sub-totals
-M, --monthly show monthly sub-totals
-Y, --yearly show yearly sub-totals
-l, --limit EXPR calculate only transactions matching EXPR
-d, --display EXPR display only transactions matching EXPR
-t, --amount EXPR use EXPR to calculate the displayed amount
-T, --total EXPR use EXPR to calculate the displayed total
-j, --amount-data print only raw amount data (useful for scripting)
-J, --total-data print only raw total data
Commodity reporting:
-A, --average report average transaction amount
-D, --deviation report deviation from the average
Commands:
balance [REGEXP]... show balance totals for matching accounts
register [REGEXP]... show register of matching events"""
sys.exit (0)
# This call registers all of the default command-line options that
# Ledger supports into the option handling mechanism. Skip this call
# if you wish to do all of your own processing -- in which case simply
# modify the 'config' object however you like.
add_config_option_handlers ()
add_option_handler ("help", "h", option_help)
add_option_handler ("version", "v", show_version)
# Process the command-line arguments, test whether caching should be
# enabled, and then process any option settings from the execution
# environment. Some historical environment variable names are also
# supported.
args = process_arguments (sys.argv[1:])
config.use_cache = not config.data_file
process_environment (os.environ, "TIMECLOCK_")
if os.environ.has_key ("TIMELOG"):
process_option ("file", os.getenv ("TIMELOG"))
# The command word is in the first argument. Canonicalize it to a
# unique, simple form that the remaining code can use to find out
# which command was specified.
if len (args) == 0:
args = ["balance"]
command = args.pop (0);
if command == "balance" or command == "bal" or command == "b":
command = "b"
elif command == "register" or command == "reg" or command == "r":
command = "r"
elif command == "entry":
command = "e"
elif command == "in" or command == "out":
if config.data_file:
log = open (config.data_file, "a")
else:
log = sys.stdout
if command == "in":
if len (args) == 0:
print "A project name is required when clocking in."
sys.exit (1)
log.write ("i %s %s" % (time.strftime ("%Y/%m/%d %H:%M:%S"),
args.pop (0)))
if len (args) > 0:
log.write (" %s\n" % string.join (args, " "))
else:
log.write ("o %s" % time.strftime ("%Y/%m/%d %H:%M:%S"))
if len (args) > 0:
log.write (" %s" % string.join (args, " "))
log.write ("\n")
log.close ()
sys.exit (0)
else:
print "Unrecognized command:", command
sys.exit (1)
# Create the main journal object, into which all entries will be
# recorded. Once done, the 'journal' may be iterated to yield those
# entries, in the same order as which they appeared in the journal
# file.
journal = Journal ()
# This parser is intended only for timelog files.
class Event:
def __init__(self, kind, when, desc):
self.kind = kind
self.when = when
self.desc = desc
class Interval:
def __init__(self, begin, end):
self.begin = begin
self.end = end
def length(self):
"Return the length of the interval in seconds."
return self.end.when - self.begin.when
def parse_timelog(path, journal):
import re
if not os.path.exists (path):
print "Cannot read timelog file '%s'" % path
sys.exit (1)
file = open(path)
history = []
begin = None
linenum = 0
for line in file:
linenum += 1
match = re.match("([iIoO])\s+([0-9/]+\s+[0-9:]+)\s*(.+)", line)
if match:
(kind, when, desc) = match.groups()
when = time.strptime(when, "%Y/%m/%d %H:%M:%S")
when = time.mktime(when)
event = Event(kind, when, desc)
if kind == "i" or kind == "I":
begin = event
else:
if begin.desc:
match = re.match ("(.+?) (.+)", begin.desc)
if match:
acct = match.group (1)
desc = match.group (2)
else:
acct = begin.desc
desc = ""
else:
acct = "Misc"
desc = event.desc
l = Interval(begin, event).length ()
e = Entry ()
e.date = int (begin.when)
e.payee = desc
x = Transaction (journal.find_account (acct),
Amount ("%ss" % l), TRANSACTION_VIRTUAL)
e.add_transaction (x)
if not journal.add_entry (e):
print "%s, %d: Failed to entry" % (path, linenum)
sys.exit (1)
parse_timelog (config.data_file, journal)
# Now that everything has been correctly parsed (parse_ledger_data
# would have thrown an exception if not), we can take time to further
# process the configuration options. This changes the configuration a
# bit based on previous option settings, the command word, and the
# remaining arguments.
if command == "b" and \
config.amount_expr == "{1.00h}*(a/{3600.00h})":
config.amount_expr = "a"
config.process_options (command, args);
# Determine the format string to used, based on the command.
if config.format_string:
format = config.format_string
elif command == "b":
format = config.balance_format
elif command == "r":
format = config.register_format
else:
format = config.print_format
# The following two classes are responsible for outputing transactions
# and accounts to the user. There are corresponding C++ versions to
# these, but they rely on I/O streams, which Boost.Python does not
# provide a conversion layer for.
class FormatTransactions (TransactionHandler):
last_entry = None
output = None
def __init__ (self, fmt):
try:
i = string.index (fmt, '%/')
self.formatter = Format (fmt[: i])
self.nformatter = Format (fmt[i + 2 :])
except ValueError:
self.formatter = Format (fmt)
self.nformatter = None
self.last_entry = None
if config.output_file:
self.output = open (config.output_file, "w")
else:
self.output = sys.stdout
TransactionHandler.__init__ (self)
def __del__ (self):
if config.output_file:
self.output.close ()
def flush (self):
self.output.flush ()
def __call__ (self, xact):
if not transaction_has_xdata (xact) or \
not transaction_xdata (xact).dflags & TRANSACTION_DISPLAYED:
if self.nformatter is not None and \
self.last_entry is not None and \
xact.entry == self.last_entry:
self.output.write (self.nformatter.format (xact))
else:
self.output.write (self.formatter.format (xact))
self.last_entry = xact.entry
transaction_xdata (xact).dflags |= TRANSACTION_DISPLAYED
class FormatAccounts (AccountHandler):
output = None
def __init__ (self, fmt, pred):
self.formatter = Format (fmt)
self.predicate = AccountPredicate (pred)
if config.output_file:
self.output = open (config.output_file, "w")
else:
self.output = sys.stdout
AccountHandler.__init__ (self)
def __del__ (self):
if config.output_file:
self.output.close ()
def final (self, account):
if account_has_xdata (account):
xdata = account_xdata (account)
if xdata.dflags & ACCOUNT_TO_DISPLAY:
print "-------------------- ---------"
xdata.value = xdata.total
self.output.write (self.formatter.format (account))
def flush (self):
self.output.flush ()
def __call__ (self, account):
if display_account (account, self.predicate):
if not account.parent:
account_xdata (account).dflags |= ACCOUNT_TO_DISPLAY
else:
self.output.write (self.formatter.format (account))
account_xdata (account).dflags |= ACCOUNT_DISPLAYED
# Set the final transaction handler: for balances and equity reports,
# it will simply add the value of the transaction to the account's
# xdata, which is used a bit later to report those totals. For all
# other reports, the transaction data is sent to the configured output
# location (default is sys.stdout).
if command == "b":
handler = SetAccountValue ()
else:
handler = FormatTransactions (format)
# Chain transaction filters on top of the base handler. Most of these
# filters customize the output for reporting. None of this is done
# for balance or equity reports, which don't need it.
if command != "b":
if config.display_predicate:
handler = FilterTransactions (handler, config.display_predicate)
handler = CalcTransactions (handler)
if config.sort_string:
handler = SortTransactions (handler, config.sort_string)
if config.show_revalued:
handler = ChangedValueTransactions (handler, config.show_revalued_only)
if config.show_collapsed:
handler = CollapseTransactions (handler);
if config.show_subtotal and not (command == "b" or command == "E"):
handler = SubtotalTransactions (handler)
if config.days_of_the_week:
handler = DowTransactions (handler)
elif config.by_payee:
handler = ByPayeeTransactions (handler)
if config.report_period:
handler = IntervalTransactions (handler, config.report_period,
config.report_period_sort)
handler = SortTransactions (handler, "d")
# The next two transaction filters are used by all reports.
if config.show_inverted:
handler = InvertTransactions (handler)
if config.show_related:
handler = RelatedTransactions (handler, config.show_all_related)
if config.predicate:
handler = FilterTransactions (handler, config.predicate)
if config.comm_as_payee:
handler = SetCommAsPayee (handler)
# Walk the journal's entries, and pass each entry's transaction to the
# handler chain established above.
walk_entries (journal, handler)
# Flush the handlers, causing them to output whatever data is still
# pending.
handler.flush ()
# For the balance and equity reports, the account totals now need to
# be displayed. This is different from outputting transactions, in
# that we are now outputting account totals to display a summary of
# the transactions that were just walked.
if command == "b":
acct_formatter = FormatAccounts (format, config.display_predicate)
sum_accounts (journal.master)
walk_accounts (journal.master, acct_formatter, config.sort_string)
acct_formatter.final (journal.master)
acct_formatter.flush ()
|