forked from wycomco/kimai-cli
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcli.py
359 lines (270 loc) · 9.19 KB
/
cli.py
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
import click
import tabulate
import kimai
import config
import dates
import favorites as fav
from prompt_toolkit import prompt
from prompt_toolkit.completion import Completer, Completion
from fuzzyfinder import fuzzyfinder
def print_success(message):
"""Print success message to the console."""
click.echo(click.style(message, fg='green'))
def print_error(message):
"""Print error message to the console."""
click.echo(click.style(message, fg='red'), err=True)
def print_table(rows, columns=None):
"""Print a table to the console."""
if columns is not None:
rows = map(lambda r: {k: r[k] for k in columns if k in r}, rows)
click.echo(tabulate.tabulate(rows, headers='keys', tablefmt="grid"))
def prompt_with_autocomplete(prompt_title, collection_name, resolve_title=True):
cached_collection = config.get(collection_name, {})
if not cached_collection:
click.echo('Falling back to ids. If you want to have fuzzy '
'autocompletion , please run "kimai configure" first.')
return prompt(prompt_title)
title = None
while title not in cached_collection:
title = prompt(prompt_title, completer=FuzzyCompleter(cached_collection.keys()))
if resolve_title:
return cached_collection[title]
return title
@click.group()
def cli():
pass
@cli.command()
@click.option('--kimai-url', prompt='Kimai URL')
@click.option('--username', prompt='Username')
@click.option('--password', prompt='Password', hide_input=True)
@click.pass_context
def configure(ctx, kimai_url, username, password):
"""Configure the Kimai-CLI"""
config.set('KimaiUrl', kimai_url)
r = kimai.authenticate(username, password)
if not r.successful:
print_error('Authentication failed.')
return
config.set('ApiKey', r.apiKey)
ctx.invoke(download_projects)
ctx.invoke(download_tasks)
print_success('Configuration complete')
@cli.group()
@click.pass_context
def projects(ctx):
if config.get('ApiKey') is None:
print_error(
'''kimai-cli has not yet been configured. Use \'kimai configure\'
first before using any other command'''
)
ctx.abort()
@projects.command('list')
def list_projects():
"""Lists all available projects"""
print_table(
kimai.get_projects(),
columns=['projectID', 'name', 'customerName'],
)
@projects.command('download')
def download_projects():
"""Downloads all existing projects to disk so they can be used
for autocompletion"""
remote_projects = kimai.get_projects()
project_map = {}
for project in remote_projects:
project_map[project['name']] = project['projectID']
config.set('Projects', project_map)
print_success('Successfully downloaded projects.')
@cli.group()
@click.pass_context
def tasks(ctx):
if config.get('ApiKey') is None:
print_error(
'''kimai-cli has not yet been configured. Use \'kimai configure\'
first before using any other command'''
)
ctx.abort()
@tasks.command('list')
def list_tasks():
"""Lists all available tasks"""
print_table(kimai.get_tasks())
@tasks.command('download')
def download_tasks():
"""Downloads all existing tasks to disk so they can be used for autocompletion"""
remote_tasks = kimai.get_tasks()
task_map = {}
for task in remote_tasks:
task_map[task['name']] = task['activityID']
config.set('Tasks', task_map)
print_success('Successfully downloaded tasks.')
@cli.group()
@click.pass_context
def record(ctx):
if config.get('ApiKey') is None:
print_error(
'''kimai-cli has not yet been configured. Use \'kimai configure\'
first before using any other command'''
)
ctx.abort()
@record.command('start')
@click.option('--task-id', prompt='Task Id', type=int)
@click.option('--project-id', prompt='Project Id', type=int)
def start_record(task_id, project_id):
"""Start a new time recording"""
response = kimai.start_recording(task_id, project_id)
if response.successful:
print_success(
'Started recording. To stop recording type \'kimai record stop\''
)
else:
print_error('Could not start recording: "%s"' % response.error)
@record.command('stop')
def stop_record():
"""Stops the currently running recording (if there is one)"""
response = kimai.stop_recording()
if not response:
print_success('No recording running.')
return
if response.successful:
print_success('Stopped recording.')
else:
print_error('Could not stop recording: "%s"' % response.error)
@record.command('get-current')
def get_current():
"""Get the currently running time recording."""
current = kimai.get_current()
if not current:
return
print_table([current], columns=[
'timeEntryID',
'start',
'end',
'customerName',
'projectName',
'activityName'
])
@record.command('get-today')
def get_today():
"""Returns all recorded entries for today"""
records = kimai.get_todays_records()
print_table(records, columns=[
'timeEntryID',
'start',
'end',
'customerName',
'projectName',
'activityName'
])
@record.command('add')
@click.option('--start-time', prompt="Start Time", type=str)
@click.option('--end-time', type=str)
@click.option('--duration', type=str)
@click.option('--project-id', type=int)
@click.option('--task-id', type=int)
@click.option('--favorite', type=str)
@click.option('--comment', default='', type=str)
def add_record(start_time, end_time, duration, favorite, project_id, task_id, comment):
if not end_time and not duration:
print_error('Need either an end time or a duration.')
return
if not favorite and not (project_id and task_id):
print_error('Need either the name of a favorite or a task id and project id')
return
start_time = dates.parse(start_time)
if start_time is None:
print_error('Could not parse start date')
return
if duration:
# We assume that any duration should be added to the start time
# since it doesn't make sence to have the end time be before the
# start time
end_time = dates.parse('+' + duration, start_time)
else:
end_time = dates.parse(end_time)
if end_time is None:
print_error('Could not parse end date')
return
if favorite:
try:
favorite = fav.get_favorite(favorite)
project_id = favorite.Project
task_id = favorite.Task
except RuntimeError as e:
print_error(str(e))
return
kimai.add_record(
start_time,
end_time,
project_id,
task_id,
comment=comment
)
@record.command('delete')
@click.option('--id', prompt='Entry Id', type=int)
def delete_record(id):
response = kimai.delete_record(id)
if not response.successful:
print_error(response.error)
else:
print_success('Record successfully deleted')
@cli.group()
@click.pass_context
def favorites(ctx):
if config.get('ApiKey') is None:
print_error(
'''kimai-cli has not yet been configured. Use \'kimai configure\'
first before using any other command'''
)
ctx.abort()
@favorites.command('list')
def list_favorites():
"""List all favorites"""
print_table(fav.list_favorites())
@favorites.command('add')
@click.option('--project-id', type=int)
@click.option('--task-id', type=int)
@click.option('--name', prompt='Favorite name', type=str)
def add_favorite(project_id, task_id, name):
"""Adds a favorite."""
if not project_id:
project_id = prompt_with_autocomplete('Project: ', 'Projects')
if not task_id:
task_id = prompt_with_autocomplete('Task: ', 'Tasks')
try:
fav.add_favorite(name, project_id, task_id)
except RuntimeError as e:
print_error(str(e))
return
print_success('Successfully added favorite "%s"' % name)
@favorites.command('delete')
@click.option('--name', type=str)
def delete_favorite(name):
"""Deletes a favorite"""
if not name:
name = prompt_with_autocomplete('Favorite: ', 'Favorites', resolve_title=False)
fav.delete_favorite(name)
print_success('Successfully removed favorite "%s"' % name)
@favorites.command('start')
@click.option('--name', type=str)
@click.pass_context
def start_recording_favorite(ctx, name):
if not name:
name = prompt_with_autocomplete('Favorite: ', 'Favorites', resolve_title=False)
try:
favorite = fav.get_favorite(name)
except RuntimeError as e:
print_error(str(e))
return
ctx.invoke(
start_record,
task_id=favorite.Task,
project_id=favorite.Project
)
class FuzzyCompleter(Completer):
def __init__(self, projects):
self.projects = projects
def get_completions(self, document, complete_event):
word_before_cursor = document.get_word_before_cursor(WORD=True)
matches = fuzzyfinder(word_before_cursor, self.projects)
for m in matches:
yield Completion(m, start_position=-len(word_before_cursor))