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 | ||
---|---|---|---|---|
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 | ||
---|---|---|---|---|
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 | ||
---|---|---|---|---|
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 | ||
---|---|---|---|---|
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