Tutorial#

We can illustrate how to use hourly by looking at the hourly repo itself.

git clone https://github.com/asherp/hourly.git
cd hourly
from hourly import get_work_commits

get_work_commits gathers all commits into a pandas array

work, repo = get_work_commits('..')
work.head()
message hash name email
time
2018-10-19 23:40:41-04:00 Initial commit ef5690543bfb354b9325d1fbd1f9abbaf... Asher Pembroke apembroke@gmail.com
2018-10-19 23:57:48-04:00 clock in 5c8f05b57b739ec525291c248ea920065... Asher Pembroke apembroke@gmail.com
2018-10-20 00:21:40-04:00 preparing setup.py 254ecdacb52fc70bc358f8d55be58df3b... Asher Pembroke apembroke@gmail.com
2018-10-20 00:39:11-04:00 clock out - work done for the day 0e33fa3d74f663f954b05dd9f30e0128c... Asher Pembroke apembroke@gmail.com
2018-10-20 01:06:08-04:00 clock in - start adding requireme... dc065b17337b14c2f8e0458de61e6880a... Asher Pembroke apembroke@gmail.com

get_labor calculates hours worked by differencing commit timestamps and raises an error if clock in and clock out are of different lengths.

Getting time card#

First we filter by commit messages containing "clock"

from hourly import get_clocks, get_labor
work.head(8)
message hash name email
time
2018-10-19 23:40:41-04:00 Initial commit ef5690543bfb354b9325d1fbd1f9abbaf... Asher Pembroke apembroke@gmail.com
2018-10-19 23:57:48-04:00 clock in 5c8f05b57b739ec525291c248ea920065... Asher Pembroke apembroke@gmail.com
2018-10-20 00:21:40-04:00 preparing setup.py 254ecdacb52fc70bc358f8d55be58df3b... Asher Pembroke apembroke@gmail.com
2018-10-20 00:39:11-04:00 clock out - work done for the day 0e33fa3d74f663f954b05dd9f30e0128c... Asher Pembroke apembroke@gmail.com
2018-10-20 01:06:08-04:00 clock in - start adding requireme... dc065b17337b14c2f8e0458de61e6880a... Asher Pembroke apembroke@gmail.com
2018-10-20 01:47:01-04:00 clock out 644ad6ebf4c9015fd512ed47b858602d7... Asher Pembroke apembroke@gmail.com
2018-10-20 01:47:45-04:00 clock in - pro bono e6b5f78daa68e3731f82effccb66fd4bd... Asher Pembroke apembroke@gmail.com
2018-10-20 01:51:36-04:00 clock out - pro bono 1aff88af5e9688645966ccd15da8e1530... Asher Pembroke apembroke@gmail.com

Choose start and end dates (timezones have to match)

start_date, end_date = work.index[[0, 25]]
start_date, end_date
(Timestamp('2018-10-19 23:40:41-0400', tz='US/Eastern'),
 Timestamp('2018-11-23 14:14:38-0500', tz='US/Eastern'))

Filter by clocks statements

clocks = get_clocks(work, start_date = start_date, end_date = end_date)
clocks
message hash name email
time
2018-10-19 23:57:48-04:00 clock in 5c8f05b57b739ec525291c248ea920065... Asher Pembroke apembroke@gmail.com
2018-10-20 00:39:11-04:00 clock out - work done for the day 0e33fa3d74f663f954b05dd9f30e0128c... Asher Pembroke apembroke@gmail.com
2018-10-20 01:06:08-04:00 clock in - start adding requireme... dc065b17337b14c2f8e0458de61e6880a... Asher Pembroke apembroke@gmail.com
2018-10-20 01:47:01-04:00 clock out 644ad6ebf4c9015fd512ed47b858602d7... Asher Pembroke apembroke@gmail.com
2018-10-20 01:47:45-04:00 clock in - pro bono e6b5f78daa68e3731f82effccb66fd4bd... Asher Pembroke apembroke@gmail.com
2018-10-20 01:51:36-04:00 clock out - pro bono 1aff88af5e9688645966ccd15da8e1530... Asher Pembroke apembroke@gmail.com
2018-10-20 02:03:56-04:00 clock in - finishing tutorial 53bd7316e579d8582c46af09277b40fbb... Asher Pembroke apembroke@gmail.com
2018-10-20 02:11:54-04:00 clock out - converted notebook fo... d55b5718a3178ab6161f7e3a148c6561a... Asher Pembroke apembroke@gmail.com
2018-10-20 02:14:21-04:00 had to clock out so notebook exam... d9ec537b36475b565df6b28d0cab6edc3... Asher Pembroke apembroke@gmail.com
2018-10-20 11:53:00-04:00 clock in - handling errant messages fa615994ba6b771594d711dea6087cc7b... Asher Pembroke apembroke@gmail.com
2018-10-20 13:16:13-04:00 clock out - converting to pd.Time... ed7aab29e43e7120428816481216198a2... Asher Pembroke apembroke@gmail.com
2018-10-20 13:47:56-04:00 clock in - adding work log 5b398037bf24cd503a7fc88c3b078913f... Asher Pembroke apembroke@gmail.com
2018-10-20 14:33:35-04:00 clock out - see WorkLog.md 93c2aa04aeba7cfe1573205abec053c7d... Asher Pembroke apembroke@gmail.com
2018-10-28 13:44:48-04:00 clock in c4e95f59dc0c8ce296a40300760ab6880... Asher Pembroke apembroke@gmail.com
2018-10-28 13:56:35-04:00 clock out f5200e718c062e828d436506286fd05e5... Asher Pembroke apembroke@gmail.com

Handling errant clock in/out messages#

If you mistakenly put "clock out" in a message, hourly will interpret the message as a legitimate end time. This will likely raise an error when computing the labor. For example, there is a problematic commit in the this repo's history:

clocks[clocks.hash == 'd9ec537b36475b565df6b28d0cab6edc3a89f2da']
message hash name email
time
2018-10-20 02:14:21-04:00 had to clock out so notebook exam... d9ec537b36475b565df6b28d0cab6edc3... Asher Pembroke apembroke@gmail.com

When we include this in our labor calculation, we get the following error:

try:
    get_labor(clocks)
except ValueError as e:
    print(e)
pay period: 2018-10-19 23:57:48-04:00 -> 2018-10-28 13:56:35-04:00
In/Out logs do not match: clock ins:7, clock outs:8

We can skip this errant commit by setting errant_clocks

clocks = get_clocks(work,
                    start_date = start_date, 
                    end_date = end_date,
                    errant_clocks = ['d9ec537b36475b565df6b28d0cab6edc3a89f2da'],
                   )

Finally we can generate a timesheet:

labor = get_labor(clocks)
labor
pay period: 2018-10-19 23:57:48-04:00 -> 2018-10-28 13:56:35-04:00
TimeIn LogIn ... TimeDelta Hours
0 2018-10-19 23:57:48-04:00 clock in ... 00:41:23 0.689722
1 2018-10-20 01:06:08-04:00 clock in - start adding requireme... ... 00:40:53 0.681389
2 2018-10-20 01:47:45-04:00 clock in - pro bono ... 00:03:51 0.064167
3 2018-10-20 02:03:56-04:00 clock in - finishing tutorial ... 00:07:58 0.132778
4 2018-10-20 11:53:00-04:00 clock in - handling errant messages ... 01:23:13 1.386944
5 2018-10-20 13:47:56-04:00 clock in - adding work log ... 00:45:39 0.760833
6 2018-10-28 13:44:48-04:00 clock in ... 00:11:47 0.196389

7 rows × 10 columns

Filtering work session keywords#

Use the "ignore" key word to skip any work you don't want to include in your invoices.

labor = get_labor(clocks, ignore = 'pro bono')
labor
pay period: 2018-10-19 23:57:48-04:00 -> 2018-10-28 13:56:35-04:00
ignoring pro bono
TimeIn LogIn ... TimeDelta Hours
0 2018-10-19 23:57:48-04:00 clock in ... 00:41:23 0.689722
1 2018-10-20 01:06:08-04:00 clock in - start adding requireme... ... 00:40:53 0.681389
3 2018-10-20 02:03:56-04:00 clock in - finishing tutorial ... 00:07:58 0.132778
4 2018-10-20 11:53:00-04:00 clock in - handling errant messages ... 01:23:13 1.386944
5 2018-10-20 13:47:56-04:00 clock in - adding work log ... 00:45:39 0.760833
6 2018-10-28 13:44:48-04:00 clock in ... 00:11:47 0.196389

6 rows × 10 columns

Get total earnings#

Total earnings can be found using this function. Currency is just a string for printing, but in the future we can add unit conversion.

from hourly import get_hours_worked, get_earnings
get_hours_worked(labor)
3.848055555555556
get_earnings(get_hours_worked(labor), wage = 80, currency = 'USD')
307.84 USD





307.84

Time adjustment#

If you forget to clock in or clock out, you can correct your time sheet by adjusting your clock time.

work, repo = get_work_commits('..') # reports actual work time, according to commit message
work = work[work.message.str.contains('T-')].drop(['name', 'email'], axis = 1)
work
message hash
time
2019-02-25 11:19:10-05:00 clock in T-1hr d7add63b4d2e3e1ca1423296aaed25d9c...
2019-02-25 12:54:51-05:00 clock out T-5m acfb8596317786e38177345aa25310980...
2019-03-16 19:12:25-04:00 clock in T-10m aa96bfaf35ab22c24c8ab8dfa3f2580ca...
2019-03-20 20:56:27-04:00 clock in T-10m 7484e4679022a65b4e24e54fb35f8904b...
2019-04-09 00:50:52-04:00 clock in T-5m 88b7abade22835c17c32c47959b5bb50f...
2019-04-11 03:12:13-04:00 clock-in: T-1h 12580fe0f7e96e24987b023dba3ee5556...
2019-04-13 03:24:44-04:00 clock-in: T-45m c6b53a3ecbf0f76f7e174088178511515...
2019-06-19 15:56:37-04:00 clock-out: T-5m 30de457a2dc24ca1cc922caaca1bbff6b...
2019-08-08 00:51:23-04:00 clock-out: T-15m 7f217173d54ac7074514b5d284aa4ed9a...
2019-08-14 01:03:57-04:00 clock-in: T-1h22m 11f6c9adeee806bbc65f4ea2c76ed49e1...
2019-08-14 01:12:41-04:00 clock-in: T-7m 07a334a28d89d108c1ffdd0ebf2f0bde8...
2019-12-23 13:06:57-05:00 clock-in: T-30m f4c47e6484a22fb2231da68a6addd4af0...

The above work commits contain time adjustments. By default, get_clocks adjusts time stamps automatically when a commit message contains the keyword T-.

clocks = get_clocks(work)
clocks
message hash
time
2019-02-25 10:19:10-05:00 clock in T-1hr d7add63b4d2e3e1ca1423296aaed25d9c...
2019-02-25 12:49:51-05:00 clock out T-5m acfb8596317786e38177345aa25310980...
2019-03-16 19:02:25-04:00 clock in T-10m aa96bfaf35ab22c24c8ab8dfa3f2580ca...
2019-03-20 20:46:27-04:00 clock in T-10m 7484e4679022a65b4e24e54fb35f8904b...
2019-04-09 00:45:52-04:00 clock in T-5m 88b7abade22835c17c32c47959b5bb50f...
2019-04-11 02:12:13-04:00 clock-in: T-1h 12580fe0f7e96e24987b023dba3ee5556...
2019-04-13 02:39:44-04:00 clock-in: T-45m c6b53a3ecbf0f76f7e174088178511515...
2019-06-19 15:51:37-04:00 clock-out: T-5m 30de457a2dc24ca1cc922caaca1bbff6b...
2019-08-08 00:36:23-04:00 clock-out: T-15m 7f217173d54ac7074514b5d284aa4ed9a...
2019-08-13 23:41:57-04:00 clock-in: T-1h22m 11f6c9adeee806bbc65f4ea2c76ed49e1...
2019-08-14 01:05:41-04:00 clock-in: T-7m 07a334a28d89d108c1ffdd0ebf2f0bde8...
2019-12-23 12:36:57-05:00 clock-in: T-30m f4c47e6484a22fb2231da68a6addd4af0...

We can verify that the time adjustment works:

clocks['adjustment'] = (work.index - clocks.index)
clocks
message hash adjustment
time
2019-02-25 10:19:10-05:00 clock in T-1hr d7add63b4d2e3e1ca1423296aaed25d9c... 01:00:00
2019-02-25 12:49:51-05:00 clock out T-5m acfb8596317786e38177345aa25310980... 00:05:00
2019-03-16 19:02:25-04:00 clock in T-10m aa96bfaf35ab22c24c8ab8dfa3f2580ca... 00:10:00
2019-03-20 20:46:27-04:00 clock in T-10m 7484e4679022a65b4e24e54fb35f8904b... 00:10:00
2019-04-09 00:45:52-04:00 clock in T-5m 88b7abade22835c17c32c47959b5bb50f... 00:05:00
2019-04-11 02:12:13-04:00 clock-in: T-1h 12580fe0f7e96e24987b023dba3ee5556... 01:00:00
2019-04-13 02:39:44-04:00 clock-in: T-45m c6b53a3ecbf0f76f7e174088178511515... 00:45:00
2019-06-19 15:51:37-04:00 clock-out: T-5m 30de457a2dc24ca1cc922caaca1bbff6b... 00:05:00
2019-08-08 00:36:23-04:00 clock-out: T-15m 7f217173d54ac7074514b5d284aa4ed9a... 00:15:00
2019-08-13 23:41:57-04:00 clock-in: T-1h22m 11f6c9adeee806bbc65f4ea2c76ed49e1... 01:22:00
2019-08-14 01:05:41-04:00 clock-in: T-7m 07a334a28d89d108c1ffdd0ebf2f0bde8... 00:07:00
2019-12-23 12:36:57-05:00 clock-in: T-30m f4c47e6484a22fb2231da68a6addd4af0... 00:30:00
get_labor(clocks, match_logs = False)
pay period: 2019-02-25 10:19:10-05:00 -> 2019-12-23 12:36:57-05:00
TimeIn LogIn ... TimeDelta Hours
0 2019-02-25 10:19:10-05:00 clock in T-1hr ... 0 days 02:30:41 2.511389
1 2019-03-16 19:02:25-04:00 clock in T-10m ... 94 days 20:49:12 2276.820000
2 2019-03-20 20:46:27-04:00 clock in T-10m ... 140 days 03:49:56 3363.832222

3 rows × 8 columns

The default behavior can be changed by setting adjust_clocks to False.

clocks_raw = get_clocks(work, adjust_clocks=False)
clocks_raw['adjustment'] = (work.index - clocks_raw.index)
clocks_raw
message hash adjustment
time
2019-02-25 11:19:10-05:00 clock in T-1hr d7add63b4d2e3e1ca1423296aaed25d9c... 0 days
2019-02-25 12:54:51-05:00 clock out T-5m acfb8596317786e38177345aa25310980... 0 days
2019-03-16 19:12:25-04:00 clock in T-10m aa96bfaf35ab22c24c8ab8dfa3f2580ca... 0 days
2019-03-20 20:56:27-04:00 clock in T-10m 7484e4679022a65b4e24e54fb35f8904b... 0 days
2019-04-09 00:50:52-04:00 clock in T-5m 88b7abade22835c17c32c47959b5bb50f... 0 days
2019-04-11 03:12:13-04:00 clock-in: T-1h 12580fe0f7e96e24987b023dba3ee5556... 0 days
2019-04-13 03:24:44-04:00 clock-in: T-45m c6b53a3ecbf0f76f7e174088178511515... 0 days
2019-06-19 15:56:37-04:00 clock-out: T-5m 30de457a2dc24ca1cc922caaca1bbff6b... 0 days
2019-08-08 00:51:23-04:00 clock-out: T-15m 7f217173d54ac7074514b5d284aa4ed9a... 0 days
2019-08-14 01:03:57-04:00 clock-in: T-1h22m 11f6c9adeee806bbc65f4ea2c76ed49e1... 0 days
2019-08-14 01:12:41-04:00 clock-in: T-7m 07a334a28d89d108c1ffdd0ebf2f0bde8... 0 days
2019-12-23 13:06:57-05:00 clock-in: T-30m f4c47e6484a22fb2231da68a6addd4af0... 0 days

Note

Only time subtractions are supported for now: it's easy to forget to clock in/out, but I can't think of a reason to clock-in sometime in the future.

Tip

Hourly uses pandas' Timedelta format to modify times. This allows for syntax like clock in T-1hr45