rekordbox-edit 0.4.0.dev21__tar.gz → 0.4.0.dev22__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.
Files changed (45) hide show
  1. {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/CHANGELOG.md +6 -1
  2. {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/PKG-INFO +1 -1
  3. {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/pyproject.toml +1 -1
  4. {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/rekordbox_edit/commands/edit.py +29 -9
  5. {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/tests/commands/test_edit.py +181 -0
  6. {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/uv.lock +1 -1
  7. {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/.agent-style/RULES.md +0 -0
  8. {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/.agent-style/claude-code.md +0 -0
  9. {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/.github/actions/commitizen-bump/action.yml +0 -0
  10. {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/.github/actions/commitizen-bump/commitizen-bump.sh +0 -0
  11. {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/.github/actions/install/action.yml +0 -0
  12. {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/.github/actions/lint/action.yml +0 -0
  13. {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/.github/actions/test/action.yml +0 -0
  14. {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/.github/workflows/cd.yml +0 -0
  15. {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/.github/workflows/ci.yml +0 -0
  16. {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/.github/workflows/publish.yml +0 -0
  17. {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/.github/workflows/release.yml +0 -0
  18. {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/.gitignore +0 -0
  19. {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/.pre-commit-config.yaml +0 -0
  20. {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/AGENTS.md +0 -0
  21. {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/CLAUDE.md +0 -0
  22. {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/CONTRIBUTING.md +0 -0
  23. {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/LICENSE +0 -0
  24. {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/Makefile +0 -0
  25. {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/README.md +0 -0
  26. {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/codecov.yml +0 -0
  27. {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/rekordbox_edit/__init__.py +0 -0
  28. {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/rekordbox_edit/_click.py +0 -0
  29. {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/rekordbox_edit/cli.py +0 -0
  30. {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/rekordbox_edit/commands/__init__.py +0 -0
  31. {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/rekordbox_edit/commands/convert.py +0 -0
  32. {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/rekordbox_edit/commands/search.py +0 -0
  33. {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/rekordbox_edit/logger.py +0 -0
  34. {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/rekordbox_edit/query.py +0 -0
  35. {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/rekordbox_edit/utils.py +0 -0
  36. {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/renovate.json5 +0 -0
  37. {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/ruff.toml +0 -0
  38. {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/tests/__init__.py +0 -0
  39. {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/tests/commands/__init__.py +0 -0
  40. {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/tests/commands/test_convert.py +0 -0
  41. {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/tests/commands/test_search.py +0 -0
  42. {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/tests/conftest.py +0 -0
  43. {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/tests/test_logger.py +0 -0
  44. {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/tests/test_query.py +0 -0
  45. {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/tests/test_utils.py +0 -0
@@ -1,6 +1,11 @@
1
- ## v0.4.0.dev21 (2026-05-16)
1
+ ## v0.4.0.dev22 (2026-05-17)
2
2
 
3
3
 
4
+ - feat(edit): add --match for literal find/replace within field value
5
+ - Introduces a _compute_new_value helper and --match option so that
6
+ rbe edit can replace a substring of the current field value instead of
7
+ performing a wholesale replacement; None current values and non-matching
8
+ patterns are treated as no-ops.
4
9
  - chore: add max-complexity lint rule
5
10
  - docs: update AGENTS.md
6
11
  - test: add coverage for --interactive + --yes skipping all confirms
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rekordbox-edit
3
- Version: 0.4.0.dev21
3
+ Version: 0.4.0.dev22
4
4
  Summary: Tools for managing and modifying a RekordBox library en-masse
5
5
  Project-URL: Homepage, https://github.com/jviall/rekordbox-edit
6
6
  Project-URL: Repository, https://github.com/jviall/rekordbox-edit
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "rekordbox-edit"
7
- version = "0.4.0.dev21"
7
+ version = "0.4.0.dev22"
8
8
  description = "Tools for managing and modifying a RekordBox library en-masse"
9
9
  authors = [{ name = "James Viall", email= "jamesviall@pm.me"}]
10
10
  license = "MIT"
@@ -26,6 +26,18 @@ FIELD_COLUMNS = {
26
26
  }
27
27
 
28
28
 
29
+ def _compute_new_value(
30
+ current: str | int | None,
31
+ match_pattern: str | None,
32
+ replace_value: str | int,
33
+ ) -> str | int | None:
34
+ """Derive the new field value."""
35
+ if current is None:
36
+ return None
37
+ if match_pattern is not None:
38
+ return str(current).replace(match_pattern, str(replace_value))
39
+ return replace_value
40
+
29
41
 
30
42
  @click.command(
31
43
  epilog=f"Debug logs for each run can be found at:\n{get_debug_file_path().parent}"
@@ -54,6 +66,13 @@ FIELD_COLUMNS = {
54
66
  required=True,
55
67
  help="The new value to write to the field",
56
68
  )
69
+ @click.option(
70
+ "--match",
71
+ "match_pattern",
72
+ default=None,
73
+ metavar="PATTERN",
74
+ help="Find this literal string within the field value and replace only that portion",
75
+ )
57
76
  @track_ids_argument
58
77
  @click.argument(
59
78
  "field",
@@ -62,10 +81,11 @@ FIELD_COLUMNS = {
62
81
  def edit_command(
63
82
  field: str,
64
83
  replace_value: str,
84
+ match_pattern: str | None,
65
85
  dry_run: bool,
66
86
  yes: bool,
67
87
  interactive: bool,
68
- track_ids: tuple,
88
+ track_ids: List[str] | None,
69
89
  track_id: List[str] | None,
70
90
  playlist: List[str] | None,
71
91
  exact_playlist: List[str] | None,
@@ -99,9 +119,7 @@ def edit_command(
99
119
  )
100
120
 
101
121
  if piped_stdin and not (dry_run or yes):
102
- raise click.UsageError(
103
- "Piping track IDs into edit requires --dry-run or --yes"
104
- )
122
+ raise click.UsageError("Piping track IDs into edit requires --dry-run or --yes")
105
123
 
106
124
  db = Rekordbox6Database()
107
125
  if not db.session:
@@ -127,11 +145,13 @@ def edit_command(
127
145
  tracks = result.scalars().all()
128
146
 
129
147
  col_name = FIELD_COLUMNS[field]
130
- edits = [
131
- (track, replace_value)
132
- for track in tracks
133
- if getattr(track, col_name) != replace_value
134
- ]
148
+ edits = []
149
+ for track in tracks:
150
+ current = getattr(track, col_name)
151
+ new_value = _compute_new_value(current, match_pattern, replace_value)
152
+ if new_value is None or new_value == current:
153
+ continue
154
+ edits.append((track, new_value))
135
155
 
136
156
  if not edits:
137
157
  logger.info("No changes to make.")
@@ -246,3 +246,184 @@ class TestEditCommandPhase1:
246
246
 
247
247
  assert result.exit_code == 0
248
248
  assert "99999" in result.output
249
+
250
+
251
+ class TestEditCommandPhase3:
252
+ """Phase 3: --match flag for literal find/replace within field value."""
253
+
254
+ @patch("rekordbox_edit.commands.edit.confirm")
255
+ @patch("rekordbox_edit.commands.edit.get_filtered_content")
256
+ @patch("rekordbox_edit.commands.edit.Rekordbox6Database")
257
+ def test_match_replaces_substring(
258
+ self, mock_db_class, mock_gfc, mock_confirm, make_djmd_content_item
259
+ ):
260
+ """--match substitutes the pattern within the current value."""
261
+ track = make_djmd_content_item(Title="Dark Matter (feat. Jane)")
262
+ mock_db, mock_result = _make_db_and_result([track])
263
+ mock_db_class.return_value = mock_db
264
+ mock_gfc.return_value = mock_result
265
+ mock_confirm.return_value = True
266
+
267
+ from click.testing import CliRunner
268
+ result = CliRunner().invoke(
269
+ edit_command,
270
+ ["Title", "--match", "feat.", "--replace", "ft."],
271
+ )
272
+
273
+ assert result.exit_code == 0
274
+ assert track.Title == "Dark Matter (ft. Jane)"
275
+ mock_db.session.commit.assert_called_once()
276
+
277
+ @patch("rekordbox_edit.commands.edit.get_filtered_content")
278
+ @patch("rekordbox_edit.commands.edit.Rekordbox6Database")
279
+ def test_match_no_match_is_noop(
280
+ self, mock_db_class, mock_gfc, make_djmd_content_item
281
+ ):
282
+ """When --match pattern is not found in current value, track is a no-op."""
283
+ track = make_djmd_content_item(Title="Dark Matter")
284
+ mock_db, mock_result = _make_db_and_result([track])
285
+ mock_db_class.return_value = mock_db
286
+ mock_gfc.return_value = mock_result
287
+
288
+ from click.testing import CliRunner
289
+ result = CliRunner().invoke(
290
+ edit_command,
291
+ ["Title", "--match", "feat.", "--replace", "ft.", "--yes"],
292
+ )
293
+
294
+ assert result.exit_code == 0
295
+ mock_db.session.commit.assert_not_called()
296
+
297
+ @patch("rekordbox_edit.commands.edit.get_filtered_content")
298
+ @patch("rekordbox_edit.commands.edit.Rekordbox6Database")
299
+ def test_match_with_none_current_value_is_noop(
300
+ self, mock_db_class, mock_gfc, make_djmd_content_item
301
+ ):
302
+ """--match on a track where the field is None is silently skipped."""
303
+ track = make_djmd_content_item(Title=None)
304
+ mock_db, mock_result = _make_db_and_result([track])
305
+ mock_db_class.return_value = mock_db
306
+ mock_gfc.return_value = mock_result
307
+
308
+ from click.testing import CliRunner
309
+ result = CliRunner().invoke(
310
+ edit_command,
311
+ ["Title", "--match", "feat.", "--replace", "ft.", "--yes"],
312
+ )
313
+
314
+ assert result.exit_code == 0
315
+ mock_db.session.commit.assert_not_called()
316
+
317
+ @patch("rekordbox_edit.commands.edit.get_filtered_content")
318
+ @patch("rekordbox_edit.commands.edit.Rekordbox6Database")
319
+ def test_match_treats_pattern_as_literal(
320
+ self, mock_db_class, mock_gfc, make_djmd_content_item
321
+ ):
322
+ """--match treats the pattern as a literal string, not a regex."""
323
+ # "1.0" as a regex would also match "1X0"; as a literal it should not
324
+ track = make_djmd_content_item(Title="Version 1X0")
325
+ mock_db, mock_result = _make_db_and_result([track])
326
+ mock_db_class.return_value = mock_db
327
+ mock_gfc.return_value = mock_result
328
+
329
+ from click.testing import CliRunner
330
+ result = CliRunner().invoke(
331
+ edit_command,
332
+ ["Title", "--match", "1.0", "--replace", "2.0", "--yes"],
333
+ )
334
+
335
+ assert result.exit_code == 0
336
+ mock_db.session.commit.assert_not_called()
337
+
338
+
339
+ class TestEditCommandUnicode:
340
+ """Unicode and multibyte character handling in edit command fields."""
341
+
342
+ @patch("rekordbox_edit.commands.edit.confirm")
343
+ @patch("rekordbox_edit.commands.edit.get_filtered_content")
344
+ @patch("rekordbox_edit.commands.edit.Rekordbox6Database")
345
+ def test_wholesale_replace_with_unicode_value(
346
+ self, mock_db_class, mock_gfc, mock_confirm, make_djmd_content_item
347
+ ):
348
+ """--replace accepts and writes multibyte unicode titles correctly."""
349
+ track = make_djmd_content_item(Title="Original Title")
350
+ mock_db, mock_result = _make_db_and_result([track])
351
+ mock_db_class.return_value = mock_db
352
+ mock_gfc.return_value = mock_result
353
+ mock_confirm.return_value = True
354
+
355
+ from click.testing import CliRunner
356
+ result = CliRunner().invoke(
357
+ edit_command,
358
+ ["Title", "--replace", "日本語タイトル"],
359
+ )
360
+
361
+ assert result.exit_code == 0
362
+ assert track.Title == "日本語タイトル"
363
+ mock_db.session.commit.assert_called_once()
364
+
365
+ @patch("rekordbox_edit.commands.edit.confirm")
366
+ @patch("rekordbox_edit.commands.edit.get_filtered_content")
367
+ @patch("rekordbox_edit.commands.edit.Rekordbox6Database")
368
+ def test_wholesale_replace_of_unicode_current_value(
369
+ self, mock_db_class, mock_gfc, mock_confirm, make_djmd_content_item
370
+ ):
371
+ """A track with a unicode title can be replaced wholesale."""
372
+ track = make_djmd_content_item(Title="Ü-Bahn Nights (feat. Ångström)")
373
+ mock_db, mock_result = _make_db_and_result([track])
374
+ mock_db_class.return_value = mock_db
375
+ mock_gfc.return_value = mock_result
376
+ mock_confirm.return_value = True
377
+
378
+ from click.testing import CliRunner
379
+ result = CliRunner().invoke(
380
+ edit_command,
381
+ ["Title", "--replace", "U-Bahn Nights (feat. Angstrom)"],
382
+ )
383
+
384
+ assert result.exit_code == 0
385
+ assert track.Title == "U-Bahn Nights (feat. Angstrom)"
386
+ mock_db.session.commit.assert_called_once()
387
+
388
+ @patch("rekordbox_edit.commands.edit.confirm")
389
+ @patch("rekordbox_edit.commands.edit.get_filtered_content")
390
+ @patch("rekordbox_edit.commands.edit.Rekordbox6Database")
391
+ def test_match_replace_within_unicode_title(
392
+ self, mock_db_class, mock_gfc, mock_confirm, make_djmd_content_item
393
+ ):
394
+ """--match correctly finds and replaces a substring within a unicode title."""
395
+ track = make_djmd_content_item(Title="夜 feat. 山田太郎")
396
+ mock_db, mock_result = _make_db_and_result([track])
397
+ mock_db_class.return_value = mock_db
398
+ mock_gfc.return_value = mock_result
399
+ mock_confirm.return_value = True
400
+
401
+ from click.testing import CliRunner
402
+ result = CliRunner().invoke(
403
+ edit_command,
404
+ ["Title", "--match", "feat.", "--replace", "ft."],
405
+ )
406
+
407
+ assert result.exit_code == 0
408
+ assert track.Title == "夜 ft. 山田太郎"
409
+ mock_db.session.commit.assert_called_once()
410
+
411
+ @patch("rekordbox_edit.commands.edit.get_filtered_content")
412
+ @patch("rekordbox_edit.commands.edit.Rekordbox6Database")
413
+ def test_noop_when_unicode_values_are_equal(
414
+ self, mock_db_class, mock_gfc, make_djmd_content_item
415
+ ):
416
+ """No edit is made when the current unicode value already equals --replace."""
417
+ track = make_djmd_content_item(Title="Ø (Disambiguation)")
418
+ mock_db, mock_result = _make_db_and_result([track])
419
+ mock_db_class.return_value = mock_db
420
+ mock_gfc.return_value = mock_result
421
+
422
+ from click.testing import CliRunner
423
+ result = CliRunner().invoke(
424
+ edit_command,
425
+ ["Title", "--replace", "Ø (Disambiguation)", "--yes"],
426
+ )
427
+
428
+ assert result.exit_code == 0
429
+ mock_db.session.commit.assert_not_called()
@@ -985,7 +985,7 @@ wheels = [
985
985
 
986
986
  [[package]]
987
987
  name = "rekordbox-edit"
988
- version = "0.4.0.dev21"
988
+ version = "0.4.0.dev22"
989
989
  source = { editable = "." }
990
990
  dependencies = [
991
991
  { name = "click" },