timedctl 5.2.0__tar.gz → 5.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: timedctl
3
- Version: 5.2.0
3
+ Version: 5.3.0
4
4
  Summary: CLI for timed
5
5
  License: AGPL-3.0-only
6
6
  Author: Arthur Deierlein
@@ -35,12 +35,31 @@ People on other distributions can use pip to install the package from pypi:
35
35
  $ pip install timedctl
36
36
  ```
37
37
 
38
+ ### Shell completion
39
+ `timedctl` support shell completion for the unaliased commands:
40
+
41
+ **bash**
42
+ ```bash
43
+ _TIMEDCTL_COMPLETE=bash_source timedctl >> ~/.bashrc`
44
+ ```
45
+
46
+ **zsh**
47
+ ```bash
48
+ _TIMEDCTL_COMPLETE=zsh_source timedctl >> ~/.zshrc`
49
+ ```
50
+
51
+ **fish**
52
+ ```bash
53
+ _TIMEDCTL_COMPLETE=fish_source timedctl > ~/.config/fish/completions/timedctl.fish`
54
+ ```
55
+
38
56
  ## Local development
39
57
  Clone the repository and install the dependencies with `poetry install`. You can now run the project with `poetry run timedctl`. For building wheels, you can use `poetry build`.
58
+ Run tests with `poetry run pytest --cov --cov-fail-under 100`.
40
59
 
41
60
  ## Known issues
42
61
  * Make sure to have a polkit-agent running, otherwise the poetry installation during the installation on arch might fail.
43
- * You need a keyring installed in order for timedctl to store the SSO token, for example `gnome-keyring`.
62
+ * You need a keyring installed in order for timedctl to store the SSO token, for example `gnome-keyring`.
44
63
 
45
64
  ## Feature roadmap
46
65
  - [x] SSO auth
@@ -14,12 +14,31 @@ People on other distributions can use pip to install the package from pypi:
14
14
  $ pip install timedctl
15
15
  ```
16
16
 
17
+ ### Shell completion
18
+ `timedctl` support shell completion for the unaliased commands:
19
+
20
+ **bash**
21
+ ```bash
22
+ _TIMEDCTL_COMPLETE=bash_source timedctl >> ~/.bashrc`
23
+ ```
24
+
25
+ **zsh**
26
+ ```bash
27
+ _TIMEDCTL_COMPLETE=zsh_source timedctl >> ~/.zshrc`
28
+ ```
29
+
30
+ **fish**
31
+ ```bash
32
+ _TIMEDCTL_COMPLETE=fish_source timedctl > ~/.config/fish/completions/timedctl.fish`
33
+ ```
34
+
17
35
  ## Local development
18
36
  Clone the repository and install the dependencies with `poetry install`. You can now run the project with `poetry run timedctl`. For building wheels, you can use `poetry build`.
37
+ Run tests with `poetry run pytest --cov --cov-fail-under 100`.
19
38
 
20
39
  ## Known issues
21
40
  * Make sure to have a polkit-agent running, otherwise the poetry installation during the installation on arch might fail.
22
- * You need a keyring installed in order for timedctl to store the SSO token, for example `gnome-keyring`.
41
+ * You need a keyring installed in order for timedctl to store the SSO token, for example `gnome-keyring`.
23
42
 
24
43
  ## Feature roadmap
25
44
  - [x] SSO auth
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "timedctl"
3
- version = "5.2.0"
3
+ version = "5.3.0"
4
4
  description = "CLI for timed"
5
5
  authors = ["Arthur Deierlein <arthur.deierlein@adfinis.com>", "Gian Klug <gian.klug@adfinis.com>"]
6
6
  readme = "README.md"
@@ -22,6 +22,8 @@ black = "^23.3.0"
22
22
  pytest = "^7.3.2"
23
23
  isort = "^5.12.0"
24
24
  flake8 = "^6.0.0"
25
+ ruff = "^0.0.282"
26
+ pytest-cov = "^4.1.0"
25
27
 
26
28
  [build-system]
27
29
  requires = ["poetry-core"]
@@ -37,3 +39,16 @@ version_toml = [
37
39
  major_on_zero = false
38
40
  branch = "main"
39
41
  build_command = "pip install poetry && poetry build"
42
+
43
+ [tool.ruff]
44
+ # TODO: add "ANN" again in the future
45
+ select = ["E", "F", "C4", "PL", "C90", "I", "N", "UP", "B", "S", "A", "COM", "PT", "Q", "T20", "SLF", "TD", "FIX", "PIE"]
46
+ # same as black
47
+ line-length = 88
48
+ format = "github"
49
+ fixable = ["ALL"]
50
+ # ignore these for now
51
+ ignore = ["PLR0913"]
52
+
53
+ [tool.ruff.per-file-ignores]
54
+ "tests/*" = ["S101"]
@@ -1,14 +1,14 @@
1
+ """
2
+ API unrelated helper functions.
3
+ """
4
+ import datetime
1
5
  import json
2
- import rich
3
6
  import re
4
- import pyfzf
5
- import click
6
- import datetime
7
7
  import sys
8
8
 
9
- """
10
- API unrelated helper functions.
11
- """
9
+ import click
10
+ import pyfzf
11
+ import rich
12
12
 
13
13
 
14
14
  def msg(message, nonl=False):
@@ -70,21 +70,21 @@ def time_sum(arr):
70
70
  return str(total)
71
71
 
72
72
 
73
- def output_formatted(data, format):
73
+ def output_formatted(data, output_format):
74
74
  """Output data in a specified format."""
75
- match format:
75
+ match output_format:
76
76
  case "json":
77
- print(json.dumps(data, indent=4))
77
+ rich.print(json.dumps(data, indent=4))
78
78
  case "csv":
79
79
  keys = data[0].keys()
80
80
  output = ",".join(keys) + "\n"
81
81
  for obj in data:
82
82
  output += ",".join(obj.values()) + "\n"
83
- print(output)
83
+ rich.print(output)
84
84
  case "text":
85
85
  for obj in data:
86
86
  for key, val in obj.items():
87
- print(f"[{key}]: {val}, ", end="")
88
- print("")
87
+ rich.print(f"[{key}]: {val}, ", end="")
88
+ rich.print("")
89
89
  case _:
90
- print("Invalid format")
90
+ rich.print("Invalid format")
@@ -7,6 +7,7 @@ import re
7
7
 
8
8
  import click
9
9
  import pyfzf
10
+ import requests
10
11
  import terminaltables
11
12
  import tomllib
12
13
  from click_aliases import ClickAliasedGroup
@@ -15,12 +16,12 @@ from libtimed.oidc import OIDCClient
15
16
  from tomlkit import dump
16
17
 
17
18
  from timedctl.helpers import (
18
- msg,
19
19
  error_handler,
20
20
  fzf_wrapper,
21
+ msg,
22
+ output_formatted,
21
23
  time_picker,
22
24
  time_sum,
23
- output_formatted,
24
25
  )
25
26
 
26
27
 
@@ -36,17 +37,18 @@ def load_config():
36
37
 
37
38
  # Get the path to the config file based on the $XDG_config_HOME environment variable
38
39
  if not os.getenv("HOME"):
39
- raise EnvironmentError("$HOME is not set")
40
+ raise OSError("$HOME is not set")
40
41
 
41
42
  xdg_config_home = os.getenv(
42
- "XDG_CONFIG_HOME", os.path.join(os.getenv("HOME"), ".config")
43
+ "XDG_CONFIG_HOME",
44
+ os.path.join(os.getenv("HOME"), ".config"),
43
45
  )
44
46
  config_dir = os.path.join(xdg_config_home, "timedctl")
45
47
  config_file = os.path.join(config_dir, "config.toml")
46
48
 
47
49
  if not os.path.isfile(config_file):
48
50
  os.makedirs(config_dir, exist_ok=True)
49
- print("No config file found. Please enter the following infos.")
51
+ click.echo("No config file found. Please enter the following infos.")
50
52
  for key in cfg:
51
53
  cfg[key] = input(f"{key} ({cfg[key]}): ")
52
54
  with open(config_file, "w", encoding="utf-8") as file:
@@ -81,7 +83,8 @@ def client_setup():
81
83
  def select_report(date):
82
84
  """FZF prompt to select a report."""
83
85
  reports = timed.reports.get(
84
- filters={"date": date}, include="task,task.project,task.project.customer"
86
+ filters={"date": date},
87
+ include="task,task.project,task.project.customer",
85
88
  )
86
89
  report_view = []
87
90
  for report in reports:
@@ -93,10 +96,10 @@ def select_report(date):
93
96
  str(report["attributes"]["duration"]),
94
97
  task["id"],
95
98
  report["id"],
96
- ]
99
+ ],
97
100
  )
98
101
  # get longest key per value
99
- max_key_lengths = [max(map(len, col)) for col in zip(*report_view)]
102
+ max_key_lengths = [max(map(len, col)) for col in zip(*report_view, strict=False)]
100
103
  # pad all the values
101
104
  report_view = [
102
105
  [
@@ -109,7 +112,7 @@ def select_report(date):
109
112
  fzf_obj = []
110
113
  for row in report_view:
111
114
  fzf_obj.append(
112
- [" | ".join([row[0], row[1], row[2]]), row[1], row[2], row[3], row[4]]
115
+ [" | ".join([row[0], row[1], row[2]]), row[1], row[2], row[3], row[4]],
113
116
  )
114
117
 
115
118
  report = fzf_wrapper(fzf_obj, [0], "Select a report: ")
@@ -121,9 +124,9 @@ def select_activity(date):
121
124
  activities = timed.activities.get(filters={"date": date})
122
125
  activity_view = []
123
126
  # loop through all activities
124
- for activity in activities:
127
+ for activity_obj in activities:
125
128
  # check if there is an actual task, else use an unknown task
126
- task_data = activity["relationships"]["task"]["data"]
129
+ task_data = activity_obj["relationships"]["task"]["data"]
127
130
  if task_data:
128
131
  task = timed.tasks.get(id=task_data["id"], cached=True)
129
132
  else:
@@ -131,16 +134,16 @@ def select_activity(date):
131
134
  activity_view.append(
132
135
  [
133
136
  task["attributes"]["name"],
134
- activity["attributes"]["comment"],
135
- activity["attributes"]["from-time"].strftime("%H:%M:%S")
137
+ activity_obj["attributes"]["comment"],
138
+ activity_obj["attributes"]["from-time"].strftime("%H:%M:%S")
136
139
  + " - "
137
- + activity["attributes"]["to-time"].strftime("%H:%M:%S"),
140
+ + activity_obj["attributes"]["to-time"].strftime("%H:%M:%S"),
138
141
  task["id"],
139
- activity["id"],
140
- ]
142
+ activity_obj["id"],
143
+ ],
141
144
  )
142
145
  # get longest key per value
143
- max_key_lengths = [max(map(len, col)) for col in zip(*activity_view)]
146
+ max_key_lengths = [max(map(len, col)) for col in zip(*activity_view, strict=False)]
144
147
  # pad all the values
145
148
  activity_view = [
146
149
  [
@@ -153,92 +156,190 @@ def select_activity(date):
153
156
  fzf_obj = []
154
157
  for row in activity_view:
155
158
  fzf_obj.append(
156
- [" | ".join([row[0], row[1], row[2]]), row[1], row[2], row[3], row[4]]
159
+ [" | ".join([row[0], row[1], row[2]]), row[1], row[2], row[3], row[4]],
157
160
  )
158
161
 
159
- activity = fzf_wrapper(fzf_obj, [0], "Select an activity: ")
160
- return activity
162
+ activity_obj = fzf_wrapper(fzf_obj, [0], "Select an activity: ")
163
+ return activity_obj
161
164
 
162
165
 
163
- def format_activity(activity):
166
+ def format_activity(activity_obj):
164
167
  """Format an activity for display."""
165
- task_obj = activity["relationships"]["task"]
168
+ task_obj = activity_obj["relationships"]["task"]
166
169
 
167
170
  task = task_obj["attributes"]["name"]
168
171
 
169
172
  project_obj = timed.projects.get(
170
- id=task_obj["relationships"]["project"]["id"], cached=True
173
+ id=task_obj["relationships"]["project"]["id"],
174
+ cached=True,
171
175
  )
172
176
  project = project_obj["attributes"]["name"]
173
177
 
174
178
  customer_obj = timed.customers.get(
175
- id=project_obj["relationships"]["customer"]["data"]["id"], cached=True
179
+ id=project_obj["relationships"]["customer"]["data"]["id"],
180
+ cached=True,
176
181
  )
177
182
  customer = customer_obj["attributes"]["name"]
178
183
  return f"{customer} > {project} > {task}"
179
184
 
180
185
 
186
+ def get_customer_by_name(customers, name, archived):
187
+ """Get customer by name."""
188
+ customers = timed.customers.get(cached=True, filters={"archived": archived})
189
+ customer = [c for c in customers if c["attributes"]["name"] == name]
190
+ if len(customer) == 0:
191
+ error_handler("ERR_CUSTOMER_NOT_FOUND")
192
+ customer_id = customer[0]["id"]
193
+ return customer_id
194
+
195
+
196
+ def get_project_by_name(projects, name, customer_id, archived):
197
+ """Get project by name."""
198
+ projects = timed.projects.get(
199
+ cached=True,
200
+ filters={"customer": customer_id, "archived": archived},
201
+ )
202
+ project = [c for c in projects if c["attributes"]["name"] == name]
203
+ if len(project) == 0:
204
+ error_handler("ERR_PROJECT_NOT_FOUND")
205
+ project_id = project[0]["id"]
206
+ return project_id
207
+
208
+
209
+ def get_task_by_name(tasks, name, project_id, archived):
210
+ """Get task by name."""
211
+ tasks = timed.tasks.get(
212
+ cached=True,
213
+ filters={"project": project_id, "archived": archived},
214
+ )
215
+ task = [c for c in tasks if c["attributes"]["name"] == name]
216
+ if len(task) == 0:
217
+ error_handler("ERR_TASK_NOT_FOUND")
218
+ task_id = task[0]["id"]
219
+ return task_id
220
+
221
+
222
+ def select_task(customer, project, task, show_archived):
223
+ """Select a task ID with fzf."""
224
+ # select a customer
225
+ customers = timed.customers.get(filters={"archived": show_archived}, cached=True)
226
+ if customer:
227
+ customer_id = get_customer_by_name(customers, customer, show_archived)
228
+ else:
229
+ customer_id = fzf_wrapper(
230
+ customers,
231
+ ["attributes", "name"],
232
+ "Select a customer: ",
233
+ )["id"]
234
+ # get projects
235
+ projects = timed.projects.get(
236
+ filters={"customer": customer_id, "archived": show_archived},
237
+ cached=True,
238
+ )
239
+ # select a project
240
+ if project:
241
+ project_id = get_project_by_name(projects, project, customer_id, show_archived)
242
+ else:
243
+ project_id = fzf_wrapper(
244
+ projects,
245
+ ["attributes", "name"],
246
+ "Select a project: ",
247
+ )["id"]
248
+ # get tasks
249
+ tasks = timed.tasks.get(
250
+ filters={"project": project_id, "archived": show_archived},
251
+ cached=True,
252
+ )
253
+ # select a task
254
+ if task:
255
+ task_id = get_task_by_name(tasks, task, project_id, show_archived)
256
+ else:
257
+ task_id = fzf_wrapper(tasks, ["attributes", "name"], "Select a task: ")["id"]
258
+ return task_id
259
+
260
+
181
261
  timed = client_setup()
182
262
 
183
263
 
184
264
  @click.group(cls=ClickAliasedGroup)
185
265
  def timedctl():
186
266
  """Use timedctl."""
187
- pass # pylint: disable=W0107
267
+ # pylint: disable=W0107
188
268
 
189
269
 
190
270
  @timedctl.group(cls=ClickAliasedGroup, aliases=["g", "show", "describe"])
191
271
  def get():
192
272
  """Get different things."""
193
- pass # pylint: disable=W0107
273
+ # pylint: disable=W0107
194
274
 
195
275
 
196
276
  @get.group(cls=ClickAliasedGroup)
197
277
  def data():
198
278
  """Get raw data for building custom scripts."""
199
- pass # pylint: disable=W0107
279
+ # pylint: disable=W0107
200
280
 
201
281
 
202
282
  @data.command("customers")
203
- @click.option("--format", default="json", type=click.Choice(["json", "csv", "text"]))
204
- def get_customers(format):
283
+ @click.option(
284
+ "--format",
285
+ "output_format",
286
+ default="json",
287
+ type=click.Choice(["json", "csv", "text"]),
288
+ )
289
+ def get_customers(output_format):
205
290
  """Get customers."""
206
291
  customers = timed.customers.get(cached=True)
207
- data = []
292
+ output = []
208
293
  for customer in customers:
209
- data.append({"id": customer["id"], "name": customer["attributes"]["name"]})
210
- output_formatted(data, format)
294
+ output.append({"id": customer["id"], "name": customer["attributes"]["name"]})
295
+ output_formatted(output, output_format)
211
296
 
212
297
 
213
298
  @data.command("projects")
214
- @click.option("--format", default="json", type=click.Choice(["json", "csv", "text"]))
299
+ @click.option(
300
+ "--format",
301
+ "output_format",
302
+ default="json",
303
+ type=click.Choice(["json", "csv", "text"]),
304
+ )
215
305
  @click.option("--customer-id", default=None, type=int)
216
306
  @click.option("--customer-name", default=None, type=str)
217
- def get_projects(format, customer_id, customer_name):
307
+ @click.option("--archived", default=False, is_flag=True)
308
+ def get_projects(output_format, customer_id, customer_name, archived):
218
309
  """Get projects."""
219
310
  if not (customer_id or customer_name):
220
311
  error_handler("ERR_MISSING_ARGUMENTS")
221
312
  # Get customer ID if name is specified
222
313
  if not customer_id:
223
314
  customers = timed.customers.get(cached=True)
224
- customer = [c for c in customers if c["attributes"]["name"] == customer_name]
225
- if len(customer) == 0:
226
- error_handler("ERR_CUSTOMER_NOT_FOUND")
227
- customer_id = customer[0]["id"]
315
+ customer_id = get_customer_by_name(customers, customer_name, archived)
228
316
  projects = timed.projects.get(cached=True, filters={"customer": customer_id})
229
- data = []
317
+ output = []
230
318
  for project in projects:
231
- data.append({"id": project["id"], "name": project["attributes"]["name"]})
232
- output_formatted(data, format)
319
+ output.append({"id": project["id"], "name": project["attributes"]["name"]})
320
+ output_formatted(output, output_format)
233
321
 
234
322
 
235
323
  @data.command("tasks")
236
- @click.option("--format", default="json", type=click.Choice(["json", "csv", "text"]))
324
+ @click.option(
325
+ "--format",
326
+ "output_format",
327
+ default="json",
328
+ type=click.Choice(["json", "csv", "text"]),
329
+ )
237
330
  @click.option("--customer-id", default=None, type=int)
238
331
  @click.option("--customer-name", default=None, type=str)
239
332
  @click.option("--project-id", default=None, type=int)
240
333
  @click.option("--project-name", default=None, type=str)
241
- def get_tasks(format, customer_id, customer_name, project_id, project_name):
334
+ @click.option("--archived", default=False, is_flag=True)
335
+ def get_tasks(
336
+ output_format,
337
+ customer_id,
338
+ customer_name,
339
+ project_id,
340
+ project_name,
341
+ archived,
342
+ ):
242
343
  """Get tasks."""
243
344
  if project_name and not (customer_id or customer_name):
244
345
  error_handler("ERR_CUSTOMER_INFO_MISSING")
@@ -249,24 +350,16 @@ def get_tasks(format, customer_id, customer_name, project_id, project_name):
249
350
  # we need an id for the customer
250
351
  if not customer_id:
251
352
  customers = timed.customers.get(cached=True)
252
- customer = [
253
- c for c in customers if c["attributes"]["name"] == customer_name
254
- ]
255
- if len(customer) == 0:
256
- error_handler("ERR_CUSTOMER_NOT_FOUND")
257
- customer_id = customer[0]["id"]
353
+ customer_id = get_customer_by_name(customers, customer_name, archived)
258
354
  # get the project id
259
355
  projects = timed.projects.get(cached=True, filters={"customer": customer_id})
260
- project = [c for c in projects if c["attributes"]["name"] == project_name]
261
- if len(project) == 0:
262
- error_handler("ERR_PROJECT_NOT_FOUND")
263
- project_id = project[0]["id"]
356
+ project_id = get_project_by_name(projects, project_name, customer_id, archived)
264
357
  # get the tasks for the specified project
265
358
  tasks = timed.tasks.get(cached=True, filters={"project": project_id})
266
- data = []
359
+ output = []
267
360
  for task in tasks:
268
- data.append({"id": task["id"], "name": task["attributes"]["name"]})
269
- output_formatted(data, format)
361
+ output.append({"id": task["id"], "name": task["attributes"]["name"]})
362
+ output_formatted(output, output_format)
270
363
 
271
364
 
272
365
  @get.command("overtime", aliases=["t", "ot", "undertime"])
@@ -275,7 +368,7 @@ def get_overtime(date):
275
368
  """Get overtime of user."""
276
369
  user = timed.users.me["id"]
277
370
  overtime = timed.overtime.get({"user": user, "date": date})
278
- msg(f"Currrent overtime is: {overtime}")
371
+ msg(f"Current overtime is: {overtime}")
279
372
 
280
373
 
281
374
  @get.command("reports", aliases=["report", "r"])
@@ -283,32 +376,23 @@ def get_overtime(date):
283
376
  def get_reports(date):
284
377
  """Get reports."""
285
378
  reports = timed.reports.get(
286
- filters={"date": date}, include="task,task.project,task.project.customer"
379
+ filters={"date": date},
380
+ include="task,task.project,task.project.customer",
287
381
  )
288
382
  table = [["Customer", "Project", "Task", "Comment", "Duration"]]
289
383
  for report in reports:
290
384
  task_obj = report["relationships"]["task"]
291
- task = task_obj["attributes"]["name"]
292
-
293
- project_obj = timed.projects.get(
294
- id=task_obj["relationships"]["project"]["id"], cached=True
385
+ project_obj = task_obj["relationships"]["project"]
386
+ customer_obj = project_obj["relationships"]["customer"]
387
+ # get name attributes
388
+ task, project, customer = (
389
+ x["attributes"]["name"] for x in [task_obj, project_obj, customer_obj]
295
390
  )
296
- project = project_obj["attributes"]["name"]
391
+ comment = report["attributes"]["comment"]
392
+ duration = report["attributes"]["duration"]
297
393
 
298
- customer_obj = timed.customers.get(
299
- id=project_obj["relationships"]["customer"]["data"]["id"], cached=True
300
- )
301
- customer = customer_obj["attributes"]["name"]
302
-
303
- table.append(
304
- [
305
- customer,
306
- project,
307
- task,
308
- report["attributes"]["comment"],
309
- report["attributes"]["duration"],
310
- ]
311
- )
394
+ table.append([customer, project, task, comment, duration])
395
+ # create the output
312
396
  output = terminaltables.SingleTable(table)
313
397
  msg(f"Reports for {date if date is not None else 'today'}:")
314
398
  click.echo(output.table)
@@ -333,7 +417,7 @@ def get_activities(date):
333
417
  activity_obj["attributes"]["to-time"].strftime("%H:%M:%S")
334
418
  if activity_obj["attributes"]["to-time"] is not None
335
419
  else "active",
336
- ]
420
+ ],
337
421
  )
338
422
  output = terminaltables.SingleTable(table)
339
423
  msg(f"Activities for {date if date is not None else 'today'}:")
@@ -343,12 +427,13 @@ def get_activities(date):
343
427
  @get.command("absences", aliases=["abs"])
344
428
  def get_absences():
345
429
  """Get absences."""
430
+ error_handler("ERR_NOT_IMPLEMENTED")
346
431
 
347
432
 
348
433
  @timedctl.group(cls=ClickAliasedGroup, aliases=["rm", "d", "remove", "del"])
349
434
  def delete():
350
435
  """Delete different things."""
351
- pass # pylint: disable=W0107
436
+ # pylint: disable=W0107
352
437
 
353
438
 
354
439
  @delete.command("report", aliases=["r"])
@@ -357,11 +442,12 @@ def delete_report(date):
357
442
  """Delete report(s)."""
358
443
  report = select_report(date)
359
444
  res = pyfzf.FzfPrompt().prompt(
360
- ["Yes", "No"], f"--prompt 'Are you sure? Delete \"{report[1]}\"?'"
445
+ ["Yes", "No"],
446
+ f"--prompt 'Are you sure? Delete \"{report[1]}\"?'",
361
447
  )
362
448
  if res[0] == "Yes":
363
449
  req = timed.reports.delete(report[-1])
364
- if req.status_code == 204:
450
+ if req.status_code == requests.codes["no_content"]:
365
451
  msg(f'Deleted report "{report[1]}"')
366
452
  else:
367
453
  error_handler("ERR_DELETION_FAILED")
@@ -384,7 +470,7 @@ def delete_absence():
384
470
  @timedctl.group(cls=ClickAliasedGroup, aliases=["a", "create"])
385
471
  def add():
386
472
  """Add different things."""
387
- pass # pylint: disable=W0107
473
+ # pylint: disable=W0107
388
474
 
389
475
 
390
476
  @add.command("report", aliases=["r"])
@@ -395,44 +481,16 @@ def add():
395
481
  @click.option("--duration", default=None)
396
482
  @click.option("--show-archived", default=False, is_flag=True)
397
483
  def add_report(
398
- customer, project, task, description, duration, show_archived
399
- ): # pylint: disable=R0912
484
+ customer,
485
+ project,
486
+ task,
487
+ description,
488
+ duration,
489
+ show_archived,
490
+ ): # ruff: noqa: PLR0913
400
491
  """Add report(s)."""
401
- # ask the user to select a customer
402
- msg("Select a customer")
403
- # select a customer
404
- customers = timed.customers.get(filters={"archived": show_archived}, cached=True)
405
- if customer:
406
- customer = [c for c in customers if c["attributes"]["name"] == customer]
407
- if len(customer) == 0:
408
- error_handler("ERR_CUSTOMER_NOT_FOUND")
409
- customer = customer[0]
410
- else:
411
- customer = fzf_wrapper(customers, ["attributes", "name"], "Select a customer: ")
412
- # get projects
413
- projects = timed.projects.get(
414
- filters={"customer": customer["id"], "archived": show_archived}, cached=True
415
- )
416
- # select a project
417
- if project:
418
- project = [p for p in projects if p["attributes"]["name"] == project]
419
- if len(project) == 0:
420
- error_handler("ERR_PROJECT_NOT_FOUND")
421
- project = project[0]
422
- else:
423
- project = fzf_wrapper(projects, ["attributes", "name"], "Select a project: ")
424
- # get tasks
425
- tasks = timed.tasks.get(
426
- filters={"project": project["id"], "archived": show_archived}, cached=True
427
- )
428
492
  # select a task
429
- if task:
430
- task = [t for t in tasks if t["attributes"]["name"] == task]
431
- if len(task) == 0:
432
- error_handler("ERR_TASK_NOT_FOUND")
433
- task = task[0]
434
- else:
435
- task = fzf_wrapper(tasks, ["attributes", "name"], "Select a task: ")
493
+ task_id = select_task(customer, project, task, show_archived)
436
494
  # ask the user to enter a description
437
495
  if not description:
438
496
  msg("Enter a description")
@@ -448,10 +506,10 @@ def add_report(
448
506
  res = timed.reports.post(
449
507
  {"duration": duration, "comment": description},
450
508
  {
451
- "task": task["id"],
509
+ "task": task_id,
452
510
  },
453
511
  )
454
- if res.status_code == 201:
512
+ if res.status_code == requests.codes["created"]:
455
513
  msg("Report created successfully")
456
514
  return
457
515
  # handle exception
@@ -473,7 +531,7 @@ def add_absence():
473
531
  @timedctl.group(cls=ClickAliasedGroup, aliases=["e", "edit", "update"])
474
532
  def edit():
475
533
  """Edit different things."""
476
- pass # pylint: disable=W0107
534
+ # pylint: disable=W0107
477
535
 
478
536
 
479
537
  @edit.command("report", aliases=["r"])
@@ -488,9 +546,11 @@ def edit_report(date):
488
546
  res = pyfzf.FzfPrompt().prompt(["No", "Yes"], "--prompt 'Are you sure?'")
489
547
  if res == ["Yes"]:
490
548
  res = timed.reports.patch(
491
- report[-1], {"comment": comment, "duration": duration}, {"task": report[-2]}
549
+ report[-1],
550
+ {"comment": comment, "duration": duration},
551
+ {"task": report[-2]},
492
552
  )
493
- if res.status_code == 200:
553
+ if res.status_code == requests.codes["ok"]:
494
554
  msg("Report updated successfully")
495
555
  return
496
556
  # handle exception
@@ -514,7 +574,7 @@ def edit_absence():
514
574
  @timedctl.group(cls=ClickAliasedGroup, aliases=["ac"])
515
575
  def activity():
516
576
  """Do stuff with activities."""
517
- pass # pylint: disable=W0107
577
+ # pylint: disable=W0107
518
578
 
519
579
 
520
580
  @activity.command("start", aliases=["add", "a"])
@@ -525,47 +585,14 @@ def activity():
525
585
  @click.option("--show-archived", default=False, is_flag=True)
526
586
  def start_activity(comment, customer, project, task, show_archived):
527
587
  """Start recording activity."""
528
- customers = timed.customers.get(filters={"archived": show_archived}, cached=True)
529
- # ask the user to select a customer
530
- msg("Select a customer")
531
- # select a customer
532
- if customer:
533
- customer = [c for c in customers if c["attributes"]["name"] == customer]
534
- if len(customer) == 0:
535
- error_handler("ERR_CUSTOMER_NOT_FOUND")
536
- customer = customer[0]
537
- else:
538
- customer = fzf_wrapper(customers, ["attributes", "name"], "Select a customer: ")
539
- # get projects
540
- projects = timed.projects.get(
541
- filters={"customer": customer["id"], "archived": show_archived}, cached=True
542
- )
543
- # select a project
544
- if project:
545
- project = [p for p in projects if p["attributes"]["name"] == project]
546
- if len(project) == 0:
547
- error_handler("ERR_PROJECT_NOT_FOUND")
548
- project = project[0]
549
- else:
550
- project = fzf_wrapper(projects, ["attributes", "name"], "Select a project: ")
551
- # get tasks
552
- tasks = timed.tasks.get(
553
- filters={"project": project["id"], "archived": show_archived}, cached=True
554
- )
555
- # select a task
556
- if task:
557
- task = [t for t in tasks if t["attributes"]["name"] == task]
558
- if len(task) == 0:
559
- error_handler("ERR_TASK_NOT_FOUND")
560
- task = task[0]
561
- else:
562
- task = fzf_wrapper(tasks, ["attributes", "name"], "Select a task: ")
588
+ task_id = select_task(customer, project, task, show_archived)
563
589
  # create the activity
564
590
  res = timed.activities.start(
565
- attributes={"comment": comment}, relationships={"task": task["id"]}
591
+ attributes={"comment": comment},
592
+ relationships={"task": task_id},
566
593
  )
567
594
 
568
- if res.status_code == 201:
595
+ if res.status_code == requests.codes["created"]:
569
596
  msg(f"Activity {comment} started successfully.")
570
597
  return
571
598
  # handle exception
@@ -586,17 +613,17 @@ def stop_activity():
586
613
  @click.option("--short", default=False, is_flag=True)
587
614
  def show_activity(short):
588
615
  """Show current activity."""
589
- # TODO: Fix in libtimed so the full object gets returned
590
- current_activity = timed.activities.get(
591
- filters={"id": timed.activities.current["id"]},
592
- include="task,task.project,task.project.customer",
593
- )[0]
594
- comment = " > " + current_activity["attributes"]["comment"] if not short else ""
595
- start = current_activity["attributes"]["from-time"].strftime("%H:%M:%S")
616
+ current_activity = timed.activities.current
596
617
  if current_activity:
618
+ activity_obj = timed.activities.get(
619
+ filters={"id": current_activity["id"]},
620
+ include="task,task.project,task.project.customer",
621
+ )[0]
622
+ comment = " > " + activity_obj["attributes"]["comment"] if not short else ""
623
+ start = activity_obj["attributes"]["from-time"].strftime("%H:%M:%S")
597
624
  msg(
598
- f"Current activity: {format_activity(current_activity)}{comment} (Since "
599
- + f"{start})"
625
+ f"Current activity: {format_activity(activity_obj)}{comment} (Since "
626
+ + f"{start})",
600
627
  )
601
628
  else:
602
629
  error_handler("ERR_NO_CURRENT_ACTIVITY")
@@ -611,14 +638,15 @@ def restart_activity(date):
611
638
  timed.activities.stop()
612
639
  msg("Stopped current activity.")
613
640
  # select an activity
614
- activity = select_activity(date)
641
+ activity_obj = select_activity(date)
615
642
  # grab attributes
616
- comment = activity[1]
617
- task_id = activity[3]
643
+ comment = activity_obj[1]
644
+ task_id = activity_obj[3]
618
645
  res = timed.activities.start(
619
- attributes={"comment": comment}, relationships={"task": task_id}
646
+ attributes={"comment": comment},
647
+ relationships={"task": task_id},
620
648
  )
621
- if res.status_code == 201:
649
+ if res.status_code == requests.codes["created"]:
622
650
  msg(f'Activity "{comment}" restarted successfully.')
623
651
  return
624
652
  # handle exception
@@ -630,9 +658,9 @@ def restart_activity(date):
630
658
  def delete_activity(date):
631
659
  """Delete an activity."""
632
660
  # select an activity
633
- activity = select_activity(date)
634
- if timed.activities.delete(activity[-1]):
635
- msg(f"Activity {activity[1]} deleted successfully.")
661
+ activity_obj = select_activity(date)
662
+ if timed.activities.delete(activity_obj[-1]):
663
+ msg(f"Activity {activity_obj[1]} deleted successfully.")
636
664
  return
637
665
  error_handler("ERR_ACTIVITY_DELETE_FAILED")
638
666
 
@@ -665,10 +693,12 @@ def activity_generate_timesheet():
665
693
  report = report[0]
666
694
  # deserialize the timedelta
667
695
  hours, minutes, seconds = report["attributes"]["duration"].split(
668
- ":"
696
+ ":",
669
697
  )
670
698
  old_duration = datetime.timedelta(
671
- hours=int(hours), minutes=int(minutes), seconds=int(seconds)
699
+ hours=int(hours),
700
+ minutes=int(minutes),
701
+ seconds=int(seconds),
672
702
  )
673
703
  # calculate the new duration
674
704
  report["attributes"]["duration"] = old_duration + duration
@@ -680,7 +710,7 @@ def activity_generate_timesheet():
680
710
  )
681
711
  else:
682
712
  # create report
683
- r = timed.reports.post(
713
+ res = timed.reports.post(
684
714
  {
685
715
  "duration": duration,
686
716
  "comment": attr["comment"],
@@ -688,7 +718,7 @@ def activity_generate_timesheet():
688
718
  {"task": task},
689
719
  )
690
720
  # append the report to the known reports
691
- reports.append(r.json()["data"])
721
+ reports.append(res.json()["data"])
692
722
  # update activity to be transferred
693
723
  attr["transferred"] = True
694
724
  timed.activities.patch(activity_obj["id"], attr, {"task": task})
File without changes
File without changes