kleinkram 0.39.0.dev20250224101424__py3-none-any.whl → 0.40.0__py3-none-any.whl

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.

kleinkram/api/client.py CHANGED
@@ -148,7 +148,9 @@ class AuthenticatedClient(httpx.Client):
148
148
  raise NotAuthenticated
149
149
 
150
150
  logger.info(f"retrying request {method} {full_url}")
151
- resp = super().request(method, full_url, *args, **kwargs)
151
+ resp = super().request(
152
+ method, full_url, params=httpx_params, *args, **kwargs
153
+ )
152
154
  logger.info(f"got response {resp}")
153
155
  return resp
154
156
  else:
kleinkram/cli/app.py CHANGED
@@ -29,6 +29,7 @@ from kleinkram.cli._upload import upload_typer
29
29
  from kleinkram.cli._verify import verify_typer
30
30
  from kleinkram.cli.error_handling import ErrorHandledTyper
31
31
  from kleinkram.cli.error_handling import display_error
32
+ from kleinkram.config import MAX_TABLE_SIZE
32
33
  from kleinkram.config import Config
33
34
  from kleinkram.config import check_config_compatibility
34
35
  from kleinkram.config import get_config
@@ -179,6 +180,11 @@ def cli(
179
180
  None, "--version", "-v", callback=_version_callback
180
181
  ),
181
182
  log_level: Optional[LogLevel] = typer.Option(None, help="Set log level."),
183
+ max_lines: int = typer.Option(
184
+ MAX_TABLE_SIZE,
185
+ "--max-lines",
186
+ help="Maximum number of lines when pretty printing tables. -1 for unlimited.",
187
+ ),
182
188
  ):
183
189
  if not check_config_compatibility():
184
190
  typer.confirm("found incompatible config file, overwrite?", abort=True)
@@ -189,6 +195,10 @@ def cli(
189
195
  shared_state.verbose = verbose
190
196
  shared_state.debug = debug
191
197
 
198
+ if max_lines < 0 and max_lines != -1:
199
+ raise typer.BadParameter("`--max-lines` must be -1 or positive")
200
+ shared_state.max_table_size = max_lines
201
+
192
202
  if shared_state.debug and log_level is None:
193
203
  log_level = LogLevel.DEBUG
194
204
  if log_level is None:
kleinkram/config.py CHANGED
@@ -24,10 +24,12 @@ from rich.text import Text
24
24
 
25
25
  from kleinkram._version import __local__
26
26
  from kleinkram._version import __version__
27
+ from kleinkram.utils import format_traceback
27
28
 
28
29
  logger = logging.getLogger(__name__)
29
30
 
30
31
  CONFIG_PATH = Path().home() / ".kleinkram.json"
32
+ MAX_TABLE_SIZE = 256
31
33
 
32
34
 
33
35
  class Environment(Enum):
@@ -74,8 +76,8 @@ def get_env() -> Environment:
74
76
 
75
77
 
76
78
  ACTION_API_KEY = "KLEINKRAM_API_KEY"
77
- ACTION_API = "KLEINKRAM_API"
78
- ACTION_S3 = "KLEINKRAM_S3"
79
+ ACTION_API = "KLEINKRAM_API_ENDPOINT"
80
+ ACTION_S3 = "KLEINKRAM_S3_ENDPOINT"
79
81
 
80
82
 
81
83
  def _get_endpoint_from_action_env_vars() -> Optional[Endpoint]:
@@ -166,13 +168,28 @@ def _config_from_dict(dct: Dict[str, Any]) -> Config:
166
168
  )
167
169
 
168
170
 
169
- def save_config(config: Config, path: Path = CONFIG_PATH) -> None:
170
- fd, temp_path = tempfile.mkstemp()
171
+ def _safe_config_write(
172
+ config: Config, path: Path, tmp_dir: Optional[Path] = None
173
+ ) -> None:
174
+ fd, temp_path = tempfile.mkstemp(dir=tmp_dir)
171
175
  with os.fdopen(fd, "w") as f:
172
176
  json.dump(_config_to_dict(config), f)
173
177
  os.replace(temp_path, path)
174
178
 
175
179
 
180
+ def _unsafe_config_write(config: Config, path: Path) -> None:
181
+ with open(path, "w") as f:
182
+ json.dump(_config_to_dict(config), f)
183
+
184
+
185
+ def save_config(config: Config, path: Path = CONFIG_PATH) -> None:
186
+ try:
187
+ _safe_config_write(config, path)
188
+ except Exception as e:
189
+ logger.warning(f"failed to safe write config {format_traceback(e)}")
190
+ _unsafe_config_write(config, path)
191
+
192
+
176
193
  def _load_config_if_compatible(path: Path) -> Optional[Config]:
177
194
  if not path.exists():
178
195
  return None
@@ -245,6 +262,7 @@ class SharedState:
245
262
  log_file: Optional[Path] = None
246
263
  verbose: bool = True
247
264
  debug: bool = False
265
+ max_table_size: int = MAX_TABLE_SIZE
248
266
 
249
267
 
250
268
  SHARED_STATE = SharedState()
kleinkram/printing.py CHANGED
@@ -17,6 +17,7 @@ from rich.console import Console
17
17
  from rich.table import Table
18
18
  from rich.text import Text
19
19
 
20
+ from kleinkram.config import get_shared_state
20
21
  from kleinkram.core import FileVerificationStatus
21
22
  from kleinkram.models import File
22
23
  from kleinkram.models import FileState
@@ -46,6 +47,11 @@ FILE_VERIFICATION_STATUS_STYLES = {
46
47
  }
47
48
 
48
49
 
50
+ def _add_placeholder_row(table: Table, skipped: int) -> None:
51
+ first_column = f"... ({skipped} more)"
52
+ table.add_row(first_column, *["..." for _ in range(len(table.columns) - 1)])
53
+
54
+
49
55
  def file_state_to_text(file_state: FileState) -> Text:
50
56
  return Text(file_state.value, style=FILE_STATE_COLOR[file_state])
51
57
 
@@ -114,8 +120,11 @@ def projects_to_table(projects: Sequence[Project]) -> Table:
114
120
  table.add_column("name")
115
121
  table.add_column("description")
116
122
 
117
- for project in projects:
123
+ max_table_size = get_shared_state().max_table_size
124
+ for project in projects[:max_table_size]:
118
125
  table.add_row(str(project.id), project.name, project.description)
126
+ if len(projects) > max_table_size:
127
+ _add_placeholder_row(table, skipped=len(projects) - max_table_size)
119
128
  return table
120
129
 
121
130
 
@@ -136,10 +145,11 @@ def missions_to_table(missions: Sequence[Mission]) -> Table:
136
145
  if not missions_tp:
137
146
  return table
138
147
  last_project: Optional[str] = None
139
- for project, _, mission in missions_tp:
148
+ max_table_size = get_shared_state().max_table_size
149
+ for project, _, mission in missions_tp[:max_table_size]:
140
150
  # add delimiter row if project changes
141
151
  if last_project is not None and last_project != project:
142
- table.add_row()
152
+ table.add_section()
143
153
  last_project = project
144
154
 
145
155
  table.add_row(
@@ -149,6 +159,9 @@ def missions_to_table(missions: Sequence[Mission]) -> Table:
149
159
  str(mission.number_of_files),
150
160
  format_bytes(mission.size),
151
161
  )
162
+
163
+ if len(missions_tp) > max_table_size:
164
+ _add_placeholder_row(table, skipped=len(missions_tp) - max_table_size)
152
165
  return table
153
166
 
154
167
 
@@ -174,9 +187,10 @@ def files_to_table(
174
187
  return table
175
188
 
176
189
  last_mission: Optional[str] = None
177
- for _, mission, _, file in files_tp:
190
+ max_table_size = get_shared_state().max_table_size
191
+ for _, mission, _, file in files_tp[:max_table_size]:
178
192
  if last_mission is not None and last_mission != mission and delimiters:
179
- table.add_row()
193
+ table.add_section()
180
194
  last_mission = mission
181
195
 
182
196
  table.add_row(
@@ -188,6 +202,10 @@ def files_to_table(
188
202
  format_bytes(file.size),
189
203
  ", ".join(file.categories),
190
204
  )
205
+
206
+ if len(files_tp) > max_table_size:
207
+ _add_placeholder_row(table, skipped=len(files_tp) - max_table_size)
208
+
191
209
  return table
192
210
 
193
211
 
@@ -273,7 +291,8 @@ def print_file_verification_status(
273
291
  either using pprint or as a list for piping
274
292
  """
275
293
  if pprint:
276
- Console().print(file_verification_status_table(file_status))
294
+ table = file_verification_status_table(file_status)
295
+ Console().print(table)
277
296
  else:
278
297
  for path, status in file_status.items():
279
298
  stream = (
@@ -288,7 +307,8 @@ def print_files(files: Sequence[File], *, pprint: bool) -> None:
288
307
  either using pprint or as a list for piping
289
308
  """
290
309
  if pprint:
291
- Console().print(files_to_table(files))
310
+ table = files_to_table(files)
311
+ Console().print(table)
292
312
  else:
293
313
  for file in files:
294
314
  stream = sys.stdout if file.state == FileState.OK else sys.stderr
@@ -301,7 +321,8 @@ def print_missions(missions: Sequence[Mission], *, pprint: bool) -> None:
301
321
  either using pprint or as a list for piping
302
322
  """
303
323
  if pprint:
304
- Console().print(missions_to_table(missions))
324
+ table = missions_to_table(missions)
325
+ Console().print(table)
305
326
  else:
306
327
  for mission in missions:
307
328
  print(mission.id)
@@ -313,7 +334,8 @@ def print_projects(projects: Sequence[Project], *, pprint: bool) -> None:
313
334
  either using pprint or as a list for piping
314
335
  """
315
336
  if pprint:
316
- Console().print(projects_to_table(projects))
337
+ table = projects_to_table(projects)
338
+ Console().print(table)
317
339
  else:
318
340
  for project in projects:
319
341
  print(project.id)
@@ -325,7 +347,8 @@ def print_file_info(file: File, *, pprint: bool) -> None:
325
347
  either using pprint or as a list for piping
326
348
  """
327
349
  if pprint:
328
- Console().print(file_info_table(file))
350
+ table = file_info_table(file)
351
+ Console().print(table)
329
352
  else:
330
353
  file_dct = asdict(file)
331
354
  for key in file_dct:
kleinkram/utils.py CHANGED
@@ -40,10 +40,7 @@ def file_paths_from_files(
40
40
  determines the destinations for a sequence of `File` objects,
41
41
  possibly nested by project and mission
42
42
  """
43
- if (
44
- len(set([(file.project_id, file.mission_id) for file in files])) > 1
45
- and not allow_nested
46
- ):
43
+ if len(set([file.mission_id for file in files])) > 1 and not allow_nested:
47
44
  raise ValueError("files from multiple missions were selected")
48
45
  elif not allow_nested:
49
46
  return {dest / file.name: file for file in files}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: kleinkram
3
- Version: 0.39.0.dev20250224101424
3
+ Version: 0.40.0
4
4
  Summary: give me your bags
5
5
  Author: Cyrill Püntener, Dominique Garmier, Johann Schwabe
6
6
  Classifier: Programming Language :: Python :: 3
@@ -2,18 +2,18 @@ kleinkram/__init__.py,sha256=xIJqTJw2kbCGryGlCeAdpmtR1FTxmrW1MklUNQEaj74,1061
2
2
  kleinkram/__main__.py,sha256=B9RiZxfO4jpCmWPUHyKJ7_EoZlEG4sPpH-nz7T_YhhQ,125
3
3
  kleinkram/_version.py,sha256=QYJyRTcqFcJj4qWYpqs7WcoOP6jxDMqyvxLY-cD6KcE,129
4
4
  kleinkram/auth.py,sha256=XD_rHOyJmYYfO7QJf3TLYH5qXA22gXGWi7PT3jujlVs,2968
5
- kleinkram/config.py,sha256=cGBr5dex6XzoktkLKnXBSQiThxFR4wZjUUsFGu5cqpA,6839
5
+ kleinkram/config.py,sha256=_ZfOSfEZaK8ZbgoNRZL2h3ozZfxHnKqmLJ9mUIYOxBU,7413
6
6
  kleinkram/core.py,sha256=Q7OYIKPN9K6kxf9Eq7r5XRHPJ3RtT7SBZp_3_CS8yuY,8429
7
7
  kleinkram/errors.py,sha256=4mygNxkf6IBgaiRWY95qu0v6z4TAXA3G6CUcXC9FU3s,772
8
8
  kleinkram/main.py,sha256=BTE0mZN__xd46wBhFi6iBlK9eGGQvJ1LdUMsbnysLi0,172
9
9
  kleinkram/models.py,sha256=8nJlPrKVLSmehspeuQSFV6nUo76JzehUn6KIZYH1xy4,1832
10
- kleinkram/printing.py,sha256=OLApOtXytGZGzdpQ9Kd-swMrPAjdnvCo5jCpLoa2lIE,11238
10
+ kleinkram/printing.py,sha256=fpkJFeVp5pzZ_cZY6nzYDV_qDFWHKHxiW-DaH2PPNeY,12160
11
11
  kleinkram/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
12
  kleinkram/types.py,sha256=nfDjj8TB1Jn5vqO0Xg6qhLOuKom9DDhe62BrngqnVGM,185
13
- kleinkram/utils.py,sha256=2lUk0y0KE6kkM6gKAfwcdxJsx0lF66ztHbkhm_CSxvI,6264
13
+ kleinkram/utils.py,sha256=fFQsq5isLjDC2Z-XUTiJzz30Wt9rFUi4391WXstusG0,6221
14
14
  kleinkram/wrappers.py,sha256=4xXU43eNnvMG2sssU330MmTLSSRdurOpnZ-zNGOGmt0,11342
15
15
  kleinkram/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
- kleinkram/api/client.py,sha256=yFanAc8VkpcyO5rVXIPyGaYHmF8_tBeC0ZMRUx0StWU,5061
16
+ kleinkram/api/client.py,sha256=1mQuguyYM3ghrADTlivQi9ybWjJzlb9emto5rPtJRkM,5112
17
17
  kleinkram/api/deser.py,sha256=xRpYUFKZ0Luoo7XyAtYblJvprmpjNSZOiFVnFKmOzcM,4819
18
18
  kleinkram/api/file_transfer.py,sha256=3wNlVQdjnRtxOzih5HhCTF18xPbYClFIDxCqbwkLl6c,12985
19
19
  kleinkram/api/pagination.py,sha256=P_zPsBKlMWkmAv-YfUNHaGW-XLB_4U8BDMrKyiDFIXk,1370
@@ -28,23 +28,23 @@ kleinkram/cli/_mission.py,sha256=zDFnOozOFckpuREFgIPt1IzG5q3b1bsNxYlWQoHoz5A,530
28
28
  kleinkram/cli/_project.py,sha256=N0C96NC_onCEwTteYp2wgkkwkdJt-1q43LFdqNXfjC8,3398
29
29
  kleinkram/cli/_upload.py,sha256=gOhbjbmqhmwW7p6bWlSvI53vLHvBFO9QqD1kdU92I2k,2813
30
30
  kleinkram/cli/_verify.py,sha256=0ABVa4U_WzaV36ClR8NsOIG7KAMRlnFmsbtnHhbWVj4,1742
31
- kleinkram/cli/app.py,sha256=pQwyX6Bilg3-GJwkfs5K49KtwnTnl8pzsa7urbR3TMg,6657
31
+ kleinkram/cli/app.py,sha256=m2qq4z95QllvXnxh3koPp0kq06I5R9etsJV8qSV8TMk,7037
32
32
  kleinkram/cli/error_handling.py,sha256=wK3tzeKVSrZm-xmiyzGLnGT2E4TRpyxhaak6GWGP7P8,1921
33
33
  testing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
34
  testing/backend_fixtures.py,sha256=t5QWwyezHUhxxAlbUuE_eFmpyRaGbnWNNcGPwrO17JM,1571
35
35
  tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
36
  tests/conftest.py,sha256=5MLYQOtQoXWl0TRkYntYKNdqpd4hl9m0XTRi5OXanYI,104
37
- tests/test_config.py,sha256=2hyk-CY4D_Q1bLDwcpdEs2tpYeR8zdWxW7VvrckoEVk,5225
37
+ tests/test_config.py,sha256=BGIXTX0XRF634_NMG-ya8v0zruw4PZCZtFGhYVK4DCM,5867
38
38
  tests/test_core.py,sha256=JbzB05LWmaaP77uXeTOQtCJD2AJT0zO9zhDfcZ3GNH8,5139
39
39
  tests/test_end_to_end.py,sha256=0W5pUES5hek-pXq4NZtpPZqKTORkGCRsDv5_D3rDMjY,3372
40
40
  tests/test_error_handling.py,sha256=qPSMKF1qsAHyUME0-krxbIrk38iGKkhAyAah-KwN4NE,1300
41
41
  tests/test_fixtures.py,sha256=UlPmGbEsGvrDPsaStGMRjNvrVPGjCqOB0RMfLJq2VRA,1071
42
- tests/test_printing.py,sha256=qCr04OJVl5ouht9FoeWGKOi8MZXevVV1EDghzV1JaMc,1903
42
+ tests/test_printing.py,sha256=Jz1AjqmqBRjp1JLm6H1oVJyvGaMPlahVXdKnd7UDQFc,2231
43
43
  tests/test_query.py,sha256=fExmCKXLA7-9j2S2sF_sbvRX_2s6Cp3a7OTcqE25q9g,3864
44
44
  tests/test_utils.py,sha256=eUBYrn3xrcgcaxm1X4fqZaX4tRvkbI6rh6BUbNbu9T0,4784
45
45
  tests/test_wrappers.py,sha256=TbcTyO2L7fslbzgfDdcVZkencxNQ8cGPZm_iB6c9d6Q,2673
46
- kleinkram-0.39.0.dev20250224101424.dist-info/METADATA,sha256=7XLgjtoiCqnMyJYoJPcay1gdv4gh9ahm1htV_HxFHJs,2760
47
- kleinkram-0.39.0.dev20250224101424.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
48
- kleinkram-0.39.0.dev20250224101424.dist-info/entry_points.txt,sha256=SaB2l5aqhSr8gmaMw2kvQU90a8Bnl7PedU8cWYxkfYo,46
49
- kleinkram-0.39.0.dev20250224101424.dist-info/top_level.txt,sha256=N3-sJagEHu1Tk1X6Dx1X1q0pLDNbDZpLzRxVftvepds,24
50
- kleinkram-0.39.0.dev20250224101424.dist-info/RECORD,,
46
+ kleinkram-0.40.0.dist-info/METADATA,sha256=7Ju-psj6tQUURUoDiWIhRtsRw-DqAK7_cFD9lTuSy4g,2742
47
+ kleinkram-0.40.0.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
48
+ kleinkram-0.40.0.dist-info/entry_points.txt,sha256=SaB2l5aqhSr8gmaMw2kvQU90a8Bnl7PedU8cWYxkfYo,46
49
+ kleinkram-0.40.0.dist-info/top_level.txt,sha256=N3-sJagEHu1Tk1X6Dx1X1q0pLDNbDZpLzRxVftvepds,24
50
+ kleinkram-0.40.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.8.0)
2
+ Generator: setuptools (75.8.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
tests/test_config.py CHANGED
@@ -7,6 +7,7 @@ from unittest import mock
7
7
 
8
8
  import pytest
9
9
 
10
+ import kleinkram.config
10
11
  from kleinkram.config import ACTION_API
11
12
  from kleinkram.config import ACTION_API_KEY
12
13
  from kleinkram.config import ACTION_S3
@@ -102,9 +103,26 @@ def test_save_and_load_config(config_path):
102
103
  config = Config(version="foo")
103
104
 
104
105
  assert not config_path.exists()
105
- save_config(config, path=config_path)
106
+ with mock.patch.object(kleinkram.config.logger, "warning") as mock_warning:
107
+ save_config(config, path=config_path)
108
+ mock_warning.assert_not_called()
109
+
106
110
  assert config_path.exists()
111
+ loaded_config = _load_config(path=config_path)
112
+ assert loaded_config == config
113
+
107
114
 
115
+ def test_save_and_load_config_when_tmpfile_fails(config_path):
116
+ config = Config(version="foo")
117
+
118
+ assert not config_path.exists()
119
+ with mock.patch("tempfile.mkstemp", side_effect=Exception), mock.patch.object(
120
+ kleinkram.config.logger, "warning"
121
+ ) as mock_warning:
122
+ save_config(config, path=config_path)
123
+ mock_warning.assert_called_once()
124
+
125
+ assert config_path.exists()
108
126
  loaded_config = _load_config(path=config_path)
109
127
  assert loaded_config == config
110
128
 
tests/test_printing.py CHANGED
@@ -3,9 +3,11 @@ from __future__ import annotations
3
3
  import datetime
4
4
 
5
5
  import pytest
6
+ from rich.table import Table
6
7
 
7
8
  from kleinkram.models import MetadataValue
8
9
  from kleinkram.models import MetadataValueType
10
+ from kleinkram.printing import _add_placeholder_row
9
11
  from kleinkram.printing import format_bytes
10
12
  from kleinkram.printing import parse_metadata_value
11
13
 
@@ -23,6 +25,15 @@ def test_format_bytes():
23
25
  assert format_bytes(2**50) == "1.00 PB"
24
26
 
25
27
 
28
+ def test_add_placeholder_row():
29
+ table = Table("foo", "bar")
30
+ _add_placeholder_row(table, skipped=1)
31
+
32
+ assert table.row_count == 1
33
+ assert table.columns[0]._cells[-1] == "... (1 more)"
34
+ assert table.columns[1]._cells[-1] == "..."
35
+
36
+
26
37
  def test_parse_metadata_value():
27
38
  mv = MetadataValue(type_=MetadataValueType.STRING, value="foo")
28
39
  assert parse_metadata_value(mv) == "foo"