kleinkram 0.38.1__tar.gz → 0.38.1.dev20241120100707__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.

Potentially problematic release.


This version of kleinkram might be problematic. Click here for more details.

Files changed (40) hide show
  1. {kleinkram-0.38.1 → kleinkram-0.38.1.dev20241120100707}/PKG-INFO +1 -1
  2. {kleinkram-0.38.1 → kleinkram-0.38.1.dev20241120100707}/kleinkram/api/client.py +35 -5
  3. {kleinkram-0.38.1 → kleinkram-0.38.1.dev20241120100707}/kleinkram/api/file_transfer.py +33 -24
  4. {kleinkram-0.38.1 → kleinkram-0.38.1.dev20241120100707}/kleinkram/api/routes.py +3 -0
  5. {kleinkram-0.38.1 → kleinkram-0.38.1.dev20241120100707}/kleinkram/app.py +44 -13
  6. {kleinkram-0.38.1 → kleinkram-0.38.1.dev20241120100707}/kleinkram/commands/verify.py +4 -2
  7. {kleinkram-0.38.1 → kleinkram-0.38.1.dev20241120100707}/kleinkram/errors.py +6 -0
  8. {kleinkram-0.38.1 → kleinkram-0.38.1.dev20241120100707}/kleinkram/utils.py +16 -6
  9. {kleinkram-0.38.1 → kleinkram-0.38.1.dev20241120100707}/kleinkram.egg-info/PKG-INFO +1 -1
  10. {kleinkram-0.38.1 → kleinkram-0.38.1.dev20241120100707}/setup.cfg +1 -1
  11. {kleinkram-0.38.1 → kleinkram-0.38.1.dev20241120100707}/LICENSE +0 -0
  12. {kleinkram-0.38.1 → kleinkram-0.38.1.dev20241120100707}/README.md +0 -0
  13. {kleinkram-0.38.1 → kleinkram-0.38.1.dev20241120100707}/kleinkram/__init__.py +0 -0
  14. {kleinkram-0.38.1 → kleinkram-0.38.1.dev20241120100707}/kleinkram/__main__.py +0 -0
  15. {kleinkram-0.38.1 → kleinkram-0.38.1.dev20241120100707}/kleinkram/_version.py +0 -0
  16. {kleinkram-0.38.1 → kleinkram-0.38.1.dev20241120100707}/kleinkram/api/__init__.py +0 -0
  17. {kleinkram-0.38.1 → kleinkram-0.38.1.dev20241120100707}/kleinkram/auth.py +0 -0
  18. {kleinkram-0.38.1 → kleinkram-0.38.1.dev20241120100707}/kleinkram/commands/__init__.py +0 -0
  19. {kleinkram-0.38.1 → kleinkram-0.38.1.dev20241120100707}/kleinkram/commands/download.py +0 -0
  20. {kleinkram-0.38.1 → kleinkram-0.38.1.dev20241120100707}/kleinkram/commands/endpoint.py +0 -0
  21. {kleinkram-0.38.1 → kleinkram-0.38.1.dev20241120100707}/kleinkram/commands/list.py +0 -0
  22. {kleinkram-0.38.1 → kleinkram-0.38.1.dev20241120100707}/kleinkram/commands/mission.py +0 -0
  23. {kleinkram-0.38.1 → kleinkram-0.38.1.dev20241120100707}/kleinkram/commands/project.py +0 -0
  24. {kleinkram-0.38.1 → kleinkram-0.38.1.dev20241120100707}/kleinkram/commands/upload.py +0 -0
  25. {kleinkram-0.38.1 → kleinkram-0.38.1.dev20241120100707}/kleinkram/config.py +0 -0
  26. {kleinkram-0.38.1 → kleinkram-0.38.1.dev20241120100707}/kleinkram/consts.py +0 -0
  27. {kleinkram-0.38.1 → kleinkram-0.38.1.dev20241120100707}/kleinkram/core.py +0 -0
  28. {kleinkram-0.38.1 → kleinkram-0.38.1.dev20241120100707}/kleinkram/enums.py +0 -0
  29. {kleinkram-0.38.1 → kleinkram-0.38.1.dev20241120100707}/kleinkram/main.py +0 -0
  30. {kleinkram-0.38.1 → kleinkram-0.38.1.dev20241120100707}/kleinkram/models.py +0 -0
  31. {kleinkram-0.38.1 → kleinkram-0.38.1.dev20241120100707}/kleinkram.egg-info/SOURCES.txt +0 -0
  32. {kleinkram-0.38.1 → kleinkram-0.38.1.dev20241120100707}/kleinkram.egg-info/dependency_links.txt +0 -0
  33. {kleinkram-0.38.1 → kleinkram-0.38.1.dev20241120100707}/kleinkram.egg-info/entry_points.txt +0 -0
  34. {kleinkram-0.38.1 → kleinkram-0.38.1.dev20241120100707}/kleinkram.egg-info/requires.txt +0 -0
  35. {kleinkram-0.38.1 → kleinkram-0.38.1.dev20241120100707}/kleinkram.egg-info/top_level.txt +0 -0
  36. {kleinkram-0.38.1 → kleinkram-0.38.1.dev20241120100707}/pyproject.toml +0 -0
  37. {kleinkram-0.38.1 → kleinkram-0.38.1.dev20241120100707}/requirements.txt +0 -0
  38. {kleinkram-0.38.1 → kleinkram-0.38.1.dev20241120100707}/setup.py +0 -0
  39. {kleinkram-0.38.1 → kleinkram-0.38.1.dev20241120100707}/tests/__init__.py +0 -0
  40. {kleinkram-0.38.1 → kleinkram-0.38.1.dev20241120100707}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: kleinkram
3
- Version: 0.38.1
3
+ Version: 0.38.1.dev20241120100707
4
4
  Summary: give me your bags
5
5
  Author: Cyrill Püntener, Dominique Garmier, Johann Schwabe
6
6
  Classifier: Programming Language :: Python :: 3
@@ -1,30 +1,41 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import logging
4
+ from typing import Any
5
+
3
6
  import httpx
4
7
  from kleinkram.auth import Config
5
8
  from kleinkram.config import Credentials
6
9
  from kleinkram.errors import LOGIN_MESSAGE
7
10
  from kleinkram.errors import NotAuthenticatedException
8
11
 
12
+ logger = logging.getLogger(__name__)
13
+
9
14
 
10
15
  COOKIE_AUTH_TOKEN = "authtoken"
11
16
  COOKIE_REFRESH_TOKEN = "refreshtoken"
12
17
  COOKIE_CLI_KEY = "clikey"
13
18
 
14
19
 
20
+ class NotLoggedInException(Exception): ...
21
+
22
+
15
23
  class AuthenticatedClient(httpx.Client):
16
- def __init__(self, *args, **kwargs) -> None:
24
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
17
25
  super().__init__(*args, **kwargs)
18
26
  self.config = Config()
19
27
 
20
28
  if self.config.has_cli_key:
21
29
  assert self.config.cli_key, "unreachable"
30
+ logger.info("using cli key...")
22
31
  self.cookies.set(COOKIE_CLI_KEY, self.config.cli_key)
23
32
 
24
33
  elif self.config.has_refresh_token:
34
+ logger.info("using refresh token...")
25
35
  assert self.config.auth_token is not None, "unreachable"
26
36
  self.cookies.set(COOKIE_AUTH_TOKEN, self.config.auth_token)
27
37
  else:
38
+ logger.info("not authenticated...")
28
39
  raise NotAuthenticatedException(self.config.endpoint)
29
40
 
30
41
  def _refresh_token(self) -> None:
@@ -37,6 +48,7 @@ class AuthenticatedClient(httpx.Client):
37
48
 
38
49
  self.cookies.set(COOKIE_REFRESH_TOKEN, refresh_token)
39
50
 
51
+ logger.info("refreshing token...")
40
52
  response = self.post(
41
53
  "/auth/refresh-token",
42
54
  )
@@ -45,21 +57,39 @@ class AuthenticatedClient(httpx.Client):
45
57
  new_access_token = response.cookies[COOKIE_AUTH_TOKEN]
46
58
  creds = Credentials(auth_token=new_access_token, refresh_token=refresh_token)
47
59
 
60
+ logger.info("saving new tokens...")
48
61
  self.config.save_credentials(creds)
49
62
  self.cookies.set(COOKIE_AUTH_TOKEN, new_access_token)
50
63
 
51
- def request(self, method, url, *args, **kwargs):
64
+ def request(
65
+ self, method: str, url: str | httpx.URL, *args: Any, **kwargs: Any
66
+ ) -> httpx.Response:
67
+ if isinstance(url, httpx.URL):
68
+ raise ValueError("url must be a slug")
69
+
70
+ # try to do a request
52
71
  full_url = f"{self.config.endpoint}{url}"
72
+ logger.info(f"requesting {method} {full_url}")
53
73
  response = super().request(method, full_url, *args, **kwargs)
74
+ logger.info(f"got response {response}")
54
75
 
76
+ # if the requesting a refresh token fails, we are not logged in
55
77
  if (url == "/auth/refresh-token") and response.status_code == 401:
56
- raise RuntimeError(LOGIN_MESSAGE)
78
+ logger.info("got 401, not logged in...")
79
+ raise NotLoggedInException(LOGIN_MESSAGE)
57
80
 
81
+ # otherwise we try to refresh the token
58
82
  if response.status_code == 401:
83
+ logger.info("got 401, trying to refresh token...")
59
84
  try:
60
85
  self._refresh_token()
61
86
  except Exception:
62
- raise RuntimeError(LOGIN_MESSAGE)
63
- return super().request(method, full_url, *args, **kwargs)
87
+ raise NotLoggedInException(LOGIN_MESSAGE)
88
+
89
+ logger.info(f"retrying request {method} {full_url}")
90
+ resp = super().request(method, full_url, *args, **kwargs)
91
+ logger.info(f"got response {resp}")
92
+ return resp
93
+
64
94
  else:
65
95
  return response
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import logging
3
4
  import os
4
5
  import sys
5
6
  from concurrent.futures import ThreadPoolExecutor
@@ -20,12 +21,19 @@ from kleinkram.config import Config
20
21
  from kleinkram.config import LOCAL_S3
21
22
  from kleinkram.errors import AccessDeniedException
22
23
  from kleinkram.errors import CorruptedFile
24
+ from kleinkram.errors import NotValidUUID
25
+ from kleinkram.errors import UploadCredentialsFailed
23
26
  from kleinkram.errors import UploadFailed
24
27
  from kleinkram.utils import b64_md5
25
- from kleinkram.utils import raw_rich
26
- from rich.text import Text
28
+ from kleinkram.utils import format_error
29
+ from kleinkram.utils import format_traceback
30
+ from kleinkram.utils import styled_string
31
+ from rich.console import Console
27
32
  from tqdm import tqdm
28
33
 
34
+
35
+ logger = logging.getLogger(__name__)
36
+
29
37
  UPLOAD_CREDS = "/file/temporaryAccess"
30
38
  UPLOAD_CONFIRM = "/queue/confirmUpload"
31
39
  UPLOAD_CANCEL = "/file/cancelUpload"
@@ -71,7 +79,7 @@ def _confirm_file_upload(
71
79
  resp = client.post(UPLOAD_CONFIRM, json=data)
72
80
 
73
81
  if 400 <= resp.status_code < 500:
74
- raise CorruptedFile()
82
+ raise CorruptedFile("failed to confirm upload")
75
83
  resp.raise_for_status()
76
84
 
77
85
 
@@ -96,7 +104,7 @@ def _get_file_download(client: AuthenticatedClient, id: UUID) -> str:
96
104
  if 400 <= resp.status_code < 500:
97
105
  raise AccessDeniedException(
98
106
  f"Failed to download file: {resp.json()['message']}",
99
- "Status Code: " + str(resp.status_code),
107
+ f"Status Code: {resp.status_code}",
100
108
  )
101
109
 
102
110
  resp.raise_for_status()
@@ -108,7 +116,7 @@ def _get_upload_creditials(
108
116
  client: AuthenticatedClient, internal_filenames: List[str], mission_id: UUID
109
117
  ) -> Dict[str, UploadCredentials]:
110
118
  if mission_id.version != 4:
111
- raise ValueError("Mission ID must be a UUIDv4")
119
+ raise NotValidUUID("Mission ID must be a UUIDv4")
112
120
  dct = {
113
121
  "filenames": internal_filenames,
114
122
  "missionUUID": str(mission_id),
@@ -116,9 +124,8 @@ def _get_upload_creditials(
116
124
  resp = client.post(UPLOAD_CREDS, json=dct)
117
125
 
118
126
  if resp.status_code >= 400:
119
- raise ValueError(
120
- "Failed to get temporary credentials. Status Code: "
121
- f"{resp.status_code}\n{resp.json()['message'][0]}"
127
+ raise UploadCredentialsFailed(
128
+ f"Failed to get temporary credentials {internal_filenames}"
122
129
  )
123
130
 
124
131
  data = resp.json()
@@ -178,8 +185,8 @@ def _s3_upload(
178
185
  Callback=pbar.update,
179
186
  )
180
187
  except Exception as e:
181
- err = f"error uploading file: {local_path}: {type(e).__name__}"
182
- pbar.write(raw_rich(Text(err, style="red")))
188
+ logger.error(format_traceback(e))
189
+ pbar.write(format_error(f"error uploading file {local_path}", e))
183
190
  return False
184
191
  return True
185
192
 
@@ -212,6 +219,7 @@ def _upload_file(
212
219
  # upload file
213
220
  creds = access[job.name]
214
221
  except Exception as e:
222
+ logger.error(format_traceback(e))
215
223
  pbar.write(f"unable to get upload credentials for file {job.path.name}: {e}")
216
224
  pbar.close()
217
225
  if global_pbar is not None:
@@ -226,8 +234,8 @@ def _upload_file(
226
234
  try:
227
235
  _cancel_file_upload(client, creds.file_id, job.mission_id)
228
236
  except Exception as e:
229
- msg = Text(f"failed to cancel upload: {type(e).__name__}", style="red")
230
- pbar.write(raw_rich(msg))
237
+ logger.error(format_traceback(e))
238
+ pbar.write(format_error(f"error cancelling upload {job.path}", e))
231
239
  else:
232
240
  # tell backend that upload is complete
233
241
  try:
@@ -235,15 +243,14 @@ def _upload_file(
235
243
  _confirm_file_upload(client, creds.file_id, local_hash)
236
244
 
237
245
  if global_pbar is not None:
238
- msg = Text(f"uploaded {job.path}", style="green")
239
- global_pbar.write(raw_rich(msg))
246
+ msg = f"uploaded {job.path}"
247
+ logger.info(msg)
248
+ global_pbar.write(styled_string(msg, style="green"))
240
249
  global_pbar.update()
241
250
 
242
251
  except Exception as e:
243
- msg = Text(
244
- f"error confirming upload {job.path}: {type(e).__name__}", style="red"
245
- )
246
- pbar.write(raw_rich(msg))
252
+ msg = format_error(f"error confirming upload {job.path}", e)
253
+ pbar.write(msg)
247
254
 
248
255
  pbar.close()
249
256
  return (job.path.stat().st_size, job.path)
@@ -292,14 +299,16 @@ def upload_files(
292
299
 
293
300
  total_size += size
294
301
  except Exception as e:
302
+ logger.error(format_traceback(e))
295
303
  errors.append(e)
296
304
 
297
305
  pbar.close()
298
306
 
299
307
  time = monotonic() - start
300
- print(f"upload took {time:.2f} seconds", file=sys.stderr)
301
- print(f"total size: {int(total_size)} MB", file=sys.stderr)
302
- print(f"average speed: {total_size / time:.2f} MB/s", file=sys.stderr)
308
+ c = Console(file=sys.stderr)
309
+ c.print(f"upload took {time:.2f} seconds")
310
+ c.print(f"total size: {int(total_size)} MB")
311
+ c.print(f"average speed: {total_size / time:.2f} MB/s")
303
312
 
304
313
  if errors:
305
314
  raise UploadFailed(f"got unhandled errors: {errors} when uploading files")
@@ -307,12 +316,12 @@ def upload_files(
307
316
 
308
317
  def _url_download(url: str, path: Path, size: int, overwrite: bool = False) -> None:
309
318
  if path.exists() and not overwrite:
310
- raise FileExistsError(f"File already exists: {path}")
319
+ raise FileExistsError(f"file already exists: {path}")
311
320
 
312
321
  with httpx.stream("GET", url) as response:
313
322
  with open(path, "wb") as f:
314
323
  with tqdm(
315
- total=size, desc=f"Downloading {path.name}", unit="B", unit_scale=True
324
+ total=size, desc=f"downloading {path.name}", unit="B", unit_scale=True
316
325
  ) as pbar:
317
326
  for chunk in response.iter_bytes(chunk_size=DOWNLOAD_CHUNK_SIZE):
318
327
  f.write(chunk)
@@ -334,4 +343,4 @@ def download_file(
334
343
  observed_hash = b64_md5(file_path)
335
344
 
336
345
  if observed_hash != hash:
337
- raise CorruptedFile("file hash does not match")
346
+ raise CorruptedFile(f"file hash does not match: {dest}")
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import logging
3
4
  from typing import Any
4
5
  from typing import cast
5
6
  from typing import Dict
@@ -28,6 +29,8 @@ from kleinkram.models import TagType
28
29
  from kleinkram.utils import filtered_by_patterns
29
30
  from kleinkram.utils import is_valid_uuid4
30
31
 
32
+ logger = logging.getLogger(__name__)
33
+
31
34
 
32
35
  MAX_PAGINATION = 10_000
33
36
 
@@ -1,7 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import logging
4
+ import os
5
+ import sys
6
+ import time
3
7
  from collections import OrderedDict
4
8
  from enum import Enum
9
+ from pathlib import Path
5
10
  from typing import Any
6
11
  from typing import Callable
7
12
  from typing import List
@@ -25,10 +30,18 @@ from kleinkram.commands.verify import verify_typer
25
30
  from kleinkram.config import Config
26
31
  from kleinkram.config import get_shared_state
27
32
  from kleinkram.errors import InvalidCLIVersion
33
+ from kleinkram.utils import format_traceback
28
34
  from kleinkram.utils import get_supported_api_version
29
35
  from rich.console import Console
30
36
  from typer.core import TyperGroup
31
37
 
38
+ LOG_DIR = Path() / "logs"
39
+ LOG_FILE = LOG_DIR / f"{time.time_ns()}.log"
40
+ LOG_FORMAT = "%(asctime)s | %(name)s | %(levelname)s | %(message)s"
41
+
42
+ # setup default logging
43
+ logger = logging.getLogger(__name__)
44
+
32
45
 
33
46
  CLI_HELP = """\
34
47
  Kleinkram CLI
@@ -40,6 +53,14 @@ for more information.
40
53
  """
41
54
 
42
55
 
56
+ class LogLevel(str, Enum):
57
+ DEBUG = "DEBUG"
58
+ INFO = "INFO"
59
+ WARNING = "WARNING"
60
+ ERROR = "ERROR"
61
+ CRITICAL = "CRITICAL"
62
+
63
+
43
64
  class CommandTypes(str, Enum):
44
65
  AUTH = "Authentication Commands"
45
66
  CORE = "Core Commands"
@@ -97,10 +118,9 @@ app = ErrorHandledTyper(
97
118
  @app.error_handler(Exception)
98
119
  def base_handler(exc: Exception) -> int:
99
120
  if not get_shared_state().debug:
100
- console = Console()
101
- console.print(f"{type(exc).__name__}: {exc}", style="red")
121
+ Console(file=sys.stderr).print(f"{type(exc).__name__}: {exc}", style="red")
122
+ logger.error(format_traceback(exc))
102
123
  return 1
103
-
104
124
  raise exc
105
125
 
106
126
 
@@ -134,7 +154,7 @@ def claim():
134
154
  print("admin rights claimed successfully.")
135
155
 
136
156
 
137
- def _version_cb(value: bool) -> None:
157
+ def _version_callback(value: bool) -> None:
138
158
  if value:
139
159
  typer.echo(__version__)
140
160
  raise typer.Exit()
@@ -151,11 +171,9 @@ def check_version_compatiblity() -> None:
151
171
  )
152
172
 
153
173
  if cli_version[1] != api_version[1]:
154
- console = Console()
155
- console.print(
156
- f"CLI version {__version__} might not be compatible with API version {api_vers_str}",
157
- style="red",
158
- )
174
+ msg = f"CLI version {__version__} might not be compatible with API version {api_vers_str}"
175
+ Console(file=sys.stderr).print(msg, style="red")
176
+ logger.warning(msg)
159
177
 
160
178
 
161
179
  @app.callback()
@@ -163,18 +181,31 @@ def cli(
163
181
  verbose: bool = typer.Option(True, help="Enable verbose mode."),
164
182
  debug: bool = typer.Option(False, help="Enable debug mode."),
165
183
  version: Optional[bool] = typer.Option(
166
- None, "--version", "-v", callback=_version_cb
184
+ None, "--version", "-v", callback=_version_callback
167
185
  ),
186
+ log_level: Optional[LogLevel] = typer.Option(None, help="Set log level."),
168
187
  ):
169
188
  _ = version # suppress unused variable warning
170
189
  shared_state = get_shared_state()
171
190
  shared_state.verbose = verbose
172
191
  shared_state.debug = debug
173
192
 
193
+ if shared_state.debug:
194
+ log_level = LogLevel.DEBUG
195
+
196
+ if log_level is not None:
197
+ LOG_DIR.mkdir(parents=True, exist_ok=True)
198
+ level = logging.getLevelName(log_level)
199
+ logging.basicConfig(level=level, filename=LOG_FILE, format=LOG_FORMAT)
200
+
201
+ logger.info(f"CLI version: {__version__}")
202
+
174
203
  try:
175
204
  check_version_compatiblity()
176
- except InvalidCLIVersion:
205
+ except InvalidCLIVersion as e:
206
+ logger.error(format_traceback(e))
177
207
  raise
178
208
  except Exception:
179
- console = Console()
180
- console.print("failed to check version compatibility", style="yellow")
209
+ err = "failed to check version compatibility"
210
+ Console(file=sys.stderr).print(err, style="yellow")
211
+ logger.error(err)
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import logging
3
4
  import sys
4
5
  from enum import Enum
5
6
  from pathlib import Path
@@ -22,6 +23,8 @@ from rich.table import Table
22
23
  from rich.text import Text
23
24
  from tqdm import tqdm
24
25
 
26
+ logger = logging.getLogger(__name__)
27
+
25
28
 
26
29
  class FileVerificationStatus(str, Enum):
27
30
  UPLAODED = "uploaded"
@@ -107,8 +110,7 @@ def verify(
107
110
  for path, status in status_dct.items():
108
111
  table.add_row(str(path), Text(status, style=FILE_STATUS_STYLES[status]))
109
112
 
110
- console = Console()
111
- console.print(table)
113
+ Console().print(table)
112
114
  else:
113
115
  for path, status in status_dct.items():
114
116
  stream = (
@@ -18,6 +18,9 @@ class MissionDoesNotExist(Exception): ...
18
18
  class NoPermission(Exception): ...
19
19
 
20
20
 
21
+ class UploadCredentialsFailed(Exception): ...
22
+
23
+
21
24
  class AccessDeniedException(Exception):
22
25
  def __init__(self, message: str, api_error: str):
23
26
  self.message = message
@@ -38,6 +41,9 @@ class CorruptedFile(Exception): ...
38
41
  class NameIsValidUUID(Exception): ...
39
42
 
40
43
 
44
+ class NotValidUUID(Exception): ...
45
+
46
+
41
47
  class UploadFailed(Exception): ...
42
48
 
43
49
 
@@ -3,14 +3,13 @@ from __future__ import annotations
3
3
  import base64
4
4
  import fnmatch
5
5
  import hashlib
6
- import os
7
6
  import string
7
+ import traceback
8
8
  from hashlib import md5
9
9
  from pathlib import Path
10
10
  from typing import Any
11
11
  from typing import Dict
12
12
  from typing import List
13
- from typing import NamedTuple
14
13
  from typing import Optional
15
14
  from typing import Sequence
16
15
  from typing import Tuple
@@ -44,6 +43,20 @@ def check_file_paths(files: Sequence[Path]) -> None:
44
43
  )
45
44
 
46
45
 
46
+ def format_error(msg: str, exc: Exception, *, verbose: bool = False) -> str:
47
+ if not verbose:
48
+ ret = f"{msg}: {type(exc).__name__}"
49
+ else:
50
+ ret = f"{msg}: {exc}"
51
+ return styled_string(ret, style="red")
52
+
53
+
54
+ def format_traceback(exc: Exception) -> str:
55
+ return "".join(
56
+ traceback.format_exception(etype=type(exc), value=exc, tb=exc.__traceback__)
57
+ )
58
+
59
+
47
60
  def filtered_by_patterns(names: Sequence[str], patterns: List[str]) -> List[str]:
48
61
  filtered = []
49
62
  for name in names:
@@ -52,17 +65,14 @@ def filtered_by_patterns(names: Sequence[str], patterns: List[str]) -> List[str]
52
65
  return filtered
53
66
 
54
67
 
55
- def raw_rich(*objects: Any, **kwargs: Any) -> str:
68
+ def styled_string(*objects: Any, **kwargs: Any) -> str:
56
69
  """\
57
70
  accepts any object that Console.print can print
58
71
  returns the raw string output
59
72
  """
60
-
61
73
  console = Console()
62
-
63
74
  with console.capture() as capture:
64
75
  console.print(*objects, **kwargs, end="")
65
-
66
76
  return capture.get()
67
77
 
68
78
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: kleinkram
3
- Version: 0.38.1
3
+ Version: 0.38.1.dev20241120100707
4
4
  Summary: give me your bags
5
5
  Author: Cyrill Püntener, Dominique Garmier, Johann Schwabe
6
6
  Classifier: Programming Language :: Python :: 3
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = kleinkram
3
- version = 0.38.1
3
+ version = 0.38.1-dev20241120100707
4
4
  description = give me your bags
5
5
  long_description = file: README.md
6
6
  long_description_content_type = text/markdown