PyHardLinkBackup 1.8.4__tar.gz → 1.9.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.
Files changed (64) hide show
  1. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/.pre-commit-config.yaml +1 -1
  2. pyhardlinkbackup-1.9.0/.venv-app/lib/python3.14/site-packages/cli_base/tests/shell_complete_snapshots/.gitignore +1 -0
  3. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/PKG-INFO +8 -5
  4. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/__init__.py +1 -1
  5. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/backup.py +3 -2
  6. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/cli_app/__init__.py +3 -2
  7. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/tests/test_backup.py +25 -19
  8. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/tests/test_project_setup.py +1 -1
  9. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/tests/test_readme.py +0 -6
  10. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/utilities/file_hash_database.py +3 -10
  11. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/utilities/tests/test_file_hash_database.py +27 -7
  12. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/README.md +6 -3
  13. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/docs/README.md +9 -4
  14. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/pyproject.toml +3 -2
  15. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/uv.lock +77 -76
  16. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/.editorconfig +0 -0
  17. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/.github/workflows/tests.yml +0 -0
  18. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/.gitignore +0 -0
  19. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/.idea/.gitignore +0 -0
  20. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/.pre-commit-hooks.yaml +0 -0
  21. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/.run/Template Python tests.run.xml +0 -0
  22. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/.run/Unittests - __all__.run.xml +0 -0
  23. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/.run/cli.py --help.run.xml +0 -0
  24. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/.run/dev-cli update.run.xml +0 -0
  25. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/.run/only DocTests.run.xml +0 -0
  26. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/.run/only DocWrite.run.xml +0 -0
  27. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/.venv-app/.gitignore +0 -0
  28. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/LICENSE +0 -0
  29. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/__main__.py +0 -0
  30. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/cli_app/phlb.py +0 -0
  31. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/cli_dev/__init__.py +0 -0
  32. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/cli_dev/__main__.py +0 -0
  33. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/cli_dev/benchmark.py +0 -0
  34. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/cli_dev/code_style.py +0 -0
  35. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/cli_dev/debugging.py +0 -0
  36. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/cli_dev/packaging.py +0 -0
  37. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/cli_dev/testing.py +0 -0
  38. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/cli_dev/update_readme_history.py +0 -0
  39. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/compare_backup.py +0 -0
  40. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/constants.py +0 -0
  41. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/logging_setup.py +0 -0
  42. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/rebuild_databases.py +0 -0
  43. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/tests/__init__.py +0 -0
  44. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/tests/test_compare_backup.py +0 -0
  45. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/tests/test_doc_write.py +0 -0
  46. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/tests/test_doctests.py +0 -0
  47. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/tests/test_readme_history.py +0 -0
  48. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/tests/test_rebuild_database.py +0 -0
  49. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/utilities/__init__.py +0 -0
  50. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/utilities/file_size_database.py +0 -0
  51. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/utilities/filesystem.py +0 -0
  52. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/utilities/humanize.py +0 -0
  53. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/utilities/rich_utils.py +0 -0
  54. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/utilities/sha256sums.py +0 -0
  55. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/utilities/tee.py +0 -0
  56. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/utilities/tests/__init__.py +0 -0
  57. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/utilities/tests/test_file_size_database.py +0 -0
  58. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/utilities/tests/test_filesystem.py +0 -0
  59. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/utilities/tests/unittest_utilities.py +0 -0
  60. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/utilities/tyro_cli_shared_args.py +0 -0
  61. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/cli.py +0 -0
  62. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/dev-cli.py +0 -0
  63. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/dist/.gitignore +0 -0
  64. {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.0}/noxfile.py +0 -0
@@ -2,6 +2,6 @@
2
2
  # See https://pre-commit.com for more information
3
3
  repos:
4
4
  - repo: https://github.com/jedie/cli-base-utilities
5
- rev: v0.29.1
5
+ rev: v0.30.0
6
6
  hooks:
7
7
  - id: update-readme-history
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyHardLinkBackup
3
- Version: 1.8.4
3
+ Version: 1.9.0
4
4
  Summary: HardLink/Deduplication Backups with Python
5
5
  Project-URL: Documentation, https://github.com/jedie/PyHardLinkBackup
6
6
  Project-URL: Source, https://github.com/jedie/PyHardLinkBackup
@@ -9,7 +9,7 @@ License: GPL-3.0-or-later
9
9
  License-File: LICENSE
10
10
  Requires-Python: >=3.12
11
11
  Requires-Dist: bx-py-utils
12
- Requires-Dist: cli-base-utilities>=0.27.1
12
+ Requires-Dist: cli-base-utilities
13
13
  Requires-Dist: rich
14
14
  Requires-Dist: tyro
15
15
  Description-Content-Type: text/markdown
@@ -298,6 +298,9 @@ Overview of main changes:
298
298
 
299
299
  [comment]: <> (✂✂✂ auto generated history start ✂✂✂)
300
300
 
301
+ * [v1.9.0](https://github.com/jedie/PyHardLinkBackup/compare/v1.8.4...v1.9.0)
302
+ * 2026-04-14 - Update existing links in has database
303
+ * 2026-04-14 - Update requirements
301
304
  * [v1.8.4](https://github.com/jedie/PyHardLinkBackup/compare/v1.8.3...v1.8.4)
302
305
  * 2026-04-09 - Update requirements
303
306
  * 2026-04-09 - Apply project updates
@@ -311,6 +314,9 @@ Overview of main changes:
311
314
  * 2026-03-28 - Update requirements
312
315
  * 2026-03-28 - apply manageprojects updates
313
316
  * 2026-03-25 - fix some code styles
317
+
318
+ <details><summary>Expand older history entries ...</summary>
319
+
314
320
  * [v1.8.1](https://github.com/jedie/PyHardLinkBackup/compare/v1.8.0...v1.8.1)
315
321
  * 2026-01-24 - Update packaging commands related to new direct "uv" usage
316
322
  * 2026-01-24 - Bugfix "rebuild" command
@@ -320,9 +326,6 @@ Overview of main changes:
320
326
  * 2026-01-22 - rebuid command: skip hashing same files by check the inode uniqueness
321
327
  * 2026-01-22 - Add "fs-info" in dev cli
322
328
  * 2026-01-22 - rebuild command: fix wrong progress bar
323
-
324
- <details><summary>Expand older history entries ...</summary>
325
-
326
329
  * [v1.8.0](https://github.com/jedie/PyHardLinkBackup/compare/v1.7.3...v1.8.0)
327
330
  * 2026-01-22 - Add optional "--name" to enforce a name for the backup sub directory
328
331
  * 2026-01-22 - Do not cross filesystem boundaries as default
@@ -3,5 +3,5 @@
3
3
  """
4
4
 
5
5
  # See https://packaging.python.org/en/latest/specifications/version-specifiers/
6
- __version__ = '1.8.4'
6
+ __version__ = '1.9.0'
7
7
  __author__ = 'Jens Diemer <PyHardLinkBackup@jensdiemer.de>'
@@ -130,7 +130,6 @@ def backup_one_file(
130
130
  else:
131
131
  logger.info('Store unique file: %s to %s', src_path, dst_path)
132
132
  dst_path.write_bytes(file_content)
133
- hash_db[file_hash] = dst_path
134
133
  backup_result.copied_files += 1
135
134
  backup_result.copied_size += size
136
135
 
@@ -146,10 +145,12 @@ def backup_one_file(
146
145
  else:
147
146
  logger.info('Copy unique file: %s to %s', src_path, dst_path)
148
147
  copy_with_progress(src_path, dst_path, progress=progress, total_size=size)
149
- hash_db[file_hash] = dst_path
150
148
  backup_result.copied_files += 1
151
149
  backup_result.copied_size += size
152
150
 
151
+ # Store new file in hash database or update existing entry to latest backuped file:
152
+ hash_db[file_hash] = dst_path
153
+
153
154
  # Keep original file metadata (permission bits, time stamps, and flags)
154
155
  shutil.copystat(src_path, dst_path)
155
156
  else:
@@ -31,9 +31,10 @@ def version():
31
31
 
32
32
 
33
33
  def main(args: Sequence[str] | None = None):
34
- print_version(PyHardLinkBackup)
34
+ project_name = 'phlb' # Enforce program name if pipx used
35
+ print_version(PyHardLinkBackup, project_name=project_name)
35
36
  app.cli(
36
- prog='phlb', # Enforce program name if pipx used
37
+ prog=project_name,
37
38
  description=constants.CLI_EPILOG,
38
39
  use_underscores=False, # use hyphens instead of underscores
39
40
  sort_subcommands=True,
@@ -274,6 +274,7 @@ class BackupTreeTestCase(
274
274
  'wb backups/source/2026-01-01-123456/min_sized_file1.bin',
275
275
  'w backups/.phlb/hash-lookup/bb/c4/bbc4de2ca238d1ec41fb622b75b5cf7d31a6d2ac92405043dd8f8220364fefc8',
276
276
  'a backups/source/2026-01-01-123456/SHA256SUMS',
277
+ 'w backups/.phlb/hash-lookup/bb/c4/bbc4de2ca238d1ec41fb622b75b5cf7d31a6d2ac92405043dd8f8220364fefc8',
277
278
  'a backups/source/2026-01-01-123456/SHA256SUMS',
278
279
  'w backups/source/2026-01-01-123456-summary.txt',
279
280
  ],
@@ -320,7 +321,7 @@ class BackupTreeTestCase(
320
321
  assert_hash_db_info(
321
322
  backup_root=self.backup_root,
322
323
  expected="""
323
- bb/c4/bbc4de2ca238d1… -> source/2026-01-01-123456/min_sized_file1.bin
324
+ bb/c4/bbc4de2ca238d1… -> source/2026-01-01-123456/min_sized_file2.bin
324
325
  e3/71/e3711d0eacddeb… -> source/2026-01-01-123456/large_file1.bin
325
326
  """,
326
327
  )
@@ -416,15 +417,15 @@ class BackupTreeTestCase(
416
417
  redirected_out.stdout,
417
418
  )
418
419
 
419
- # The FileHashDatabase remains the same:
420
+ # The FileHashDatabase always points to the latest backed-up files:
420
421
  with self.assertLogs('PyHardLinkBackup', level=logging.DEBUG):
421
422
  assert_hash_db_info(
422
423
  backup_root=self.backup_root,
423
424
  expected="""
424
425
  23/d2/23d2ce40d26211… -> source/2026-01-02-123456/min_sized_file_newA.bin
425
426
  9a/56/9a567077114134… -> source/2026-01-02-123456/min_sized_file_newB.bin
426
- bb/c4/bbc4de2ca238d1… -> source/2026-01-01-123456/min_sized_file1.bin
427
- e3/71/e3711d0eacddeb… -> source/2026-01-01-123456/large_file1.bin
427
+ bb/c4/bbc4de2ca238d1… -> source/2026-01-02-123456/min_sized_file2.bin
428
+ e3/71/e3711d0eacddeb… -> source/2026-01-02-123456/large_file2.bin
428
429
  """,
429
430
  )
430
431
 
@@ -463,9 +464,13 @@ class BackupTreeTestCase(
463
464
  'a backups/source/2026-01-02-123456/SHA256SUMS',
464
465
  'wb backups/source/2026-01-02-123456/hardlink2file1',
465
466
  'a backups/source/2026-01-02-123456/SHA256SUMS',
467
+ 'w backups/.phlb/hash-lookup/e3/71/e3711d0eacddeb105af4ad9b0d63069d759acf32e49712663419e68dc294a94a',
466
468
  'a backups/source/2026-01-02-123456/SHA256SUMS',
469
+ 'w backups/.phlb/hash-lookup/e3/71/e3711d0eacddeb105af4ad9b0d63069d759acf32e49712663419e68dc294a94a',
467
470
  'a backups/source/2026-01-02-123456/SHA256SUMS',
471
+ 'w backups/.phlb/hash-lookup/bb/c4/bbc4de2ca238d1ec41fb622b75b5cf7d31a6d2ac92405043dd8f8220364fefc8',
468
472
  'a backups/source/2026-01-02-123456/SHA256SUMS',
473
+ 'w backups/.phlb/hash-lookup/bb/c4/bbc4de2ca238d1ec41fb622b75b5cf7d31a6d2ac92405043dd8f8220364fefc8',
469
474
  'a backups/source/2026-01-02-123456/SHA256SUMS',
470
475
  'wb backups/source/2026-01-02-123456/min_sized_file_newA.bin',
471
476
  'w backups/.phlb/hash-lookup/23/d2/23d2ce40d26211a9ffe8096fd1f927f2abd094691839d24f88440f7c5168d500',
@@ -499,8 +504,8 @@ class BackupTreeTestCase(
499
504
  # Don't create broken hardlinks!
500
505
 
501
506
  """DocWrite: README.md ## FileHashDatabase - Missing hardlink target file
502
- If a hardlink source from a old backup is missing, we cannot create a hardlink to it.
503
- But it still works to hardlink same files within the current backup.
507
+ Deleting files from old backups is safe: the hash DB entry always points to the
508
+ most recently backed-up file, so subsequent backups can still create hardlinks.
504
509
  """
505
510
 
506
511
  # Let's remove one of the files used for hardlinking from the first backup:
@@ -515,8 +520,8 @@ class BackupTreeTestCase(
515
520
  self.assertIn('Backup complete', redirected_out.stdout)
516
521
  backup_dir = result.backup_dir
517
522
 
518
- # Note: min_sized_file1.bin and min_sized_file2.bin are hardlinked,
519
- # but not with the first backup anymore! So it's only nlink=2 now!
523
+ # Note: min_sized_file1.bin and min_sized_file2.bin accumulate hardlinks
524
+ # because hash_db always points to the latest backup file.
520
525
  with self.assertLogs('PyHardLinkBackup', level=logging.DEBUG):
521
526
  assert_fs_tree_overview(
522
527
  root=backup_dir,
@@ -527,8 +532,8 @@ class BackupTreeTestCase(
527
532
  hardlink2file1 12:00:00 file 1 14 8a11514a
528
533
  large_file1.bin 12:00:00 hardlink 5 1001 fb3014ff
529
534
  large_file2.bin 12:00:00 hardlink 5 1001 fb3014ff
530
- min_sized_file1.bin 12:00:00 hardlink 2 1000 f0d93de4
531
- min_sized_file2.bin 12:00:00 hardlink 2 1000 f0d93de4
535
+ min_sized_file1.bin 12:00:00 hardlink 5 1000 f0d93de4
536
+ min_sized_file2.bin 12:00:00 hardlink 5 1000 f0d93de4
532
537
  min_sized_file_newA.bin 12:00:00 hardlink 2 1001 a48f0e33
533
538
  min_sized_file_newB.bin 12:00:00 hardlink 2 1000 7d9c564d
534
539
  small_file_newA.txt 12:00:00 file 1 10 76d1acf1
@@ -547,26 +552,26 @@ class BackupTreeTestCase(
547
552
  backup_count=12,
548
553
  backup_size=6091,
549
554
  symlink_files=1,
550
- hardlinked_files=5,
551
- hardlinked_size=5003,
552
- copied_files=6,
553
- copied_size=1074,
555
+ hardlinked_files=6,
556
+ hardlinked_size=6003,
557
+ copied_files=5,
558
+ copied_size=74,
554
559
  copied_small_files=5,
555
560
  copied_small_size=74,
556
561
  error_count=0,
557
562
  ),
558
563
  )
559
564
 
560
- # Note: min_sized_file1.bin is now from the 2026-01-03 backup!
565
+ # All files points now to "2026-01-03" and non of them to the first "2026-01-01" backup:
561
566
  self.assertEqual(backup_dir.name, '2026-01-03-123456') # Latest backup dir name
562
567
  with self.assertLogs('PyHardLinkBackup', level=logging.DEBUG):
563
568
  assert_hash_db_info(
564
569
  backup_root=self.backup_root,
565
570
  expected="""
566
- 23/d2/23d2ce40d26211… -> source/2026-01-02-123456/min_sized_file_newA.bin
567
- 9a/56/9a567077114134… -> source/2026-01-02-123456/min_sized_file_newB.bin
568
- bb/c4/bbc4de2ca238d1… -> source/2026-01-03-123456/min_sized_file1.bin
569
- e3/71/e3711d0eacddeb… -> source/2026-01-01-123456/large_file1.bin
571
+ 23/d2/23d2ce40d26211… -> source/2026-01-03-123456/min_sized_file_newA.bin
572
+ 9a/56/9a567077114134… -> source/2026-01-03-123456/min_sized_file_newB.bin
573
+ bb/c4/bbc4de2ca238d1… -> source/2026-01-03-123456/min_sized_file2.bin
574
+ e3/71/e3711d0eacddeb… -> source/2026-01-03-123456/large_file2.bin
570
575
  """,
571
576
  )
572
577
 
@@ -916,6 +921,7 @@ class BackupTreeTestCase(
916
921
  [
917
922
  'w backups/.phlb_test',
918
923
  'a backups/source/2026-02-22-123456-backup.log',
924
+ 'w backups/.phlb/hash-lookup/23/d2/23d2ce40d26211a9ffe8096fd1f927f2abd094691839d24f88440f7c5168d500',
919
925
  'a backups/source/2026-02-22-123456/SHA256SUMS',
920
926
  'wb backups/source/2026-02-22-123456/large_fileB.txt',
921
927
  'w backups/.phlb/hash-lookup/2a/92/2a925556d3ec9e4258624a324cd9300a9a3d9c86dac6bbbb63071bdb7787afd2',
@@ -22,7 +22,7 @@ class ProjectSetupTestCase(TestCase):
22
22
  assert_is_file(cli_bin)
23
23
 
24
24
  output = subprocess.check_output([cli_bin, 'version'], text=True)
25
- self.assertIn(f'PyHardLinkBackup v{__version__}', output)
25
+ self.assertIn(f'phlb v{__version__}', output)
26
26
 
27
27
  dev_cli_bin = PACKAGE_ROOT / 'dev-cli.py'
28
28
  assert_is_file(dev_cli_bin)
@@ -47,9 +47,6 @@ class ReadmeTestCase(BaseTestCase):
47
47
  ),
48
48
  )
49
49
 
50
- # Installed via pipx is called 'phlb', not 'cli.py':
51
- stdout = stdout.replace('./cli.py', 'phlb')
52
-
53
50
  assert_cli_help_in_readme(text_block=stdout, marker='main help')
54
51
 
55
52
  def test_backup_help(self):
@@ -63,9 +60,6 @@ class ReadmeTestCase(BaseTestCase):
63
60
  ),
64
61
  )
65
62
 
66
- # Installed via pipx is called 'phlb', not 'cli.py':
67
- stdout = stdout.replace('./cli.py', 'phlb')
68
-
69
63
  assert_cli_help_in_readme(text_block=stdout, marker='backup help')
70
64
 
71
65
  def test_dev_help(self):
@@ -5,10 +5,6 @@ from pathlib import Path
5
5
  logger = logging.getLogger(__name__)
6
6
 
7
7
 
8
- class HashAlreadyExistsError(ValueError):
9
- pass
10
-
11
-
12
8
  class FileHashDatabase:
13
9
  """DocWrite: README.md ## FileHashDatabase
14
10
  A simple "database" to store file content hash <-> relative path mappings.
@@ -54,12 +50,9 @@ class FileHashDatabase:
54
50
  return abs_file_path
55
51
 
56
52
  def __setitem__(self, hash: str, abs_file_path: Path):
53
+ """
54
+ Create or update the hash entry with the given absolute file path.
55
+ """
57
56
  hash_path = self._get_hash_path(hash)
58
57
  hash_path.parent.mkdir(parents=True, exist_ok=True)
59
-
60
- # File should be found before and results in hardlink creation!
61
- # So deny change of existing hashes:
62
- if hash_path.exists():
63
- raise HashAlreadyExistsError(f'Hash {hash} already exists in the database!')
64
-
65
58
  hash_path.write_text(str(abs_file_path.relative_to(self.backup_root)))
@@ -8,7 +8,7 @@ from bx_py_utils.test_utils.assertion import assert_text_equal
8
8
  from bx_py_utils.test_utils.log_utils import NoLogs
9
9
  from cli_base.cli_tools.test_utils.base_testcases import BaseTestCase
10
10
 
11
- from PyHardLinkBackup.utilities.file_hash_database import FileHashDatabase, HashAlreadyExistsError
11
+ from PyHardLinkBackup.utilities.file_hash_database import FileHashDatabase
12
12
  from PyHardLinkBackup.utilities.filesystem import iter_scandir_files
13
13
 
14
14
 
@@ -127,10 +127,28 @@ class FileHashDatabaseTestCase(BaseTestCase):
127
127
  )
128
128
 
129
129
  ########################################################################################
130
- # Deny "overwrite" of existing hash:
130
+ # Update existing hash to point to a newer file:
131
131
 
132
- with self.assertRaises(HashAlreadyExistsError):
133
- hash_db['12abcd345678abcdef'] = 'foo/bar/baz' # already exists!
132
+ """DocWrite: README.md ## FileHashDatabase
133
+ The entry for each hash is always updated to point to the most recently backed-up file.
134
+ This means you can safely delete old backups: the hash DB will still point to a valid
135
+ file in the most recent backup, so deduplication continues to work correctly.
136
+ """
137
+
138
+ file_c_path = backup_root_path / 'rel/path/to/file-C'
139
+ file_c_path.parent.mkdir(parents=True, exist_ok=True)
140
+ file_c_path.touch()
141
+
142
+ hash_db['12abcd345678abcdef'] = file_c_path
143
+ self.assertEqual(hash_db.get('12abcd345678abcdef'), file_c_path)
144
+ with self.assertLogs('PyHardLinkBackup', level=logging.DEBUG):
145
+ assert_hash_db_info(
146
+ backup_root=hash_db.backup_root,
147
+ expected="""
148
+ 12/34/12345678abcdef… -> rel/path/to/file-A
149
+ 12/ab/12abcd345678ab… -> rel/path/to/file-C
150
+ """,
151
+ )
134
152
 
135
153
  ########################################################################################
136
154
  # Don't use stale entries pointing to missing files:
@@ -139,8 +157,10 @@ class FileHashDatabaseTestCase(BaseTestCase):
139
157
  file_a_path.unlink()
140
158
 
141
159
  """DocWrite: README.md ## FileHashDatabase - Missing hardlink target file
142
- We check if the hardlink source file still exists. If not, we remove the hash entry from the database.
143
- A warning is logged in this case."""
160
+ The `get()` method checks whether the referenced file still exists.
161
+ If not, the stale entry is removed and a warning is logged.
162
+ On the next backup run, the file is then copied fresh instead of hardlinked.
163
+ """
144
164
  with self.assertLogs(level=logging.WARNING) as logs:
145
165
  self.assertIs(hash_db.get('12345678abcdef'), None)
146
166
  self.assertIn('Hash database entry found, but file does not exist', ''.join(logs.output))
@@ -148,6 +168,6 @@ class FileHashDatabaseTestCase(BaseTestCase):
148
168
  assert_hash_db_info(
149
169
  backup_root=hash_db.backup_root,
150
170
  expected="""
151
- 12/ab/12abcd345678ab… -> rel/path/to/file-B
171
+ 12/ab/12abcd345678ab… -> rel/path/to/file-C
152
172
  """,
153
173
  )
@@ -282,6 +282,9 @@ Overview of main changes:
282
282
 
283
283
  [comment]: <> (✂✂✂ auto generated history start ✂✂✂)
284
284
 
285
+ * [v1.9.0](https://github.com/jedie/PyHardLinkBackup/compare/v1.8.4...v1.9.0)
286
+ * 2026-04-14 - Update existing links in has database
287
+ * 2026-04-14 - Update requirements
285
288
  * [v1.8.4](https://github.com/jedie/PyHardLinkBackup/compare/v1.8.3...v1.8.4)
286
289
  * 2026-04-09 - Update requirements
287
290
  * 2026-04-09 - Apply project updates
@@ -295,6 +298,9 @@ Overview of main changes:
295
298
  * 2026-03-28 - Update requirements
296
299
  * 2026-03-28 - apply manageprojects updates
297
300
  * 2026-03-25 - fix some code styles
301
+
302
+ <details><summary>Expand older history entries ...</summary>
303
+
298
304
  * [v1.8.1](https://github.com/jedie/PyHardLinkBackup/compare/v1.8.0...v1.8.1)
299
305
  * 2026-01-24 - Update packaging commands related to new direct "uv" usage
300
306
  * 2026-01-24 - Bugfix "rebuild" command
@@ -304,9 +310,6 @@ Overview of main changes:
304
310
  * 2026-01-22 - rebuid command: skip hashing same files by check the inode uniqueness
305
311
  * 2026-01-22 - Add "fs-info" in dev cli
306
312
  * 2026-01-22 - rebuild command: fix wrong progress bar
307
-
308
- <details><summary>Expand older history entries ...</summary>
309
-
310
313
  * [v1.8.0](https://github.com/jedie/PyHardLinkBackup/compare/v1.7.3...v1.8.0)
311
314
  * 2026-01-22 - Add optional "--name" to enforce a name for the backup sub directory
312
315
  * 2026-01-22 - Do not cross filesystem boundaries as default
@@ -36,13 +36,18 @@ Notes:
36
36
  * The "relative path" that will be stored is not validated, so it can be any string.
37
37
  * We don't "cache" anything in Memory, to avoid high memory consumption for large datasets.
38
38
 
39
+ The entry for each hash is always updated to point to the most recently backed-up file.
40
+ This means you can safely delete old backups: the hash DB will still point to a valid
41
+ file in the most recent backup, so deduplication continues to work correctly.
42
+
39
43
  ## FileHashDatabase - Missing hardlink target file
40
44
 
41
- If a hardlink source from a old backup is missing, we cannot create a hardlink to it.
42
- But it still works to hardlink same files within the current backup.
45
+ Deleting files from old backups is safe: the hash DB entry always points to the
46
+ most recently backed-up file, so subsequent backups can still create hardlinks.
43
47
 
44
- We check if the hardlink source file still exists. If not, we remove the hash entry from the database.
45
- A warning is logged in this case.
48
+ The `get()` method checks whether the referenced file still exists.
49
+ If not, the stale entry is removed and a warning is logged.
50
+ On the next backup run, the file is then copied fresh instead of hardlinked.
46
51
 
47
52
  ## FileSizeDatabase
48
53
 
@@ -9,7 +9,7 @@ authors = [
9
9
  ]
10
10
  requires-python = ">=3.12"
11
11
  dependencies = [
12
- "cli-base-utilities>=0.27.1", # https://github.com/jedie/cli-base-utilities
12
+ "cli-base-utilities", # https://github.com/jedie/cli-base-utilities
13
13
  "bx_py_utils", # https://github.com/boxine/bx_py_utils
14
14
  "tyro", # https://github.com/brentyi/tyro
15
15
  "rich", # https://github.com/Textualize/rich
@@ -43,8 +43,9 @@ exclude-newer = "1 week"
43
43
  [tool.uv.exclude-newer-package]
44
44
  # Exclude own packages from the "exclude-newer" rule and
45
45
  # add external packages temporarily to fix known issues or current CVEs
46
+ uv = "2026-04-13T12:00:00Z"
47
+ cli-base-utilities = "2026-04-13T12:00:00Z"
46
48
  cryptography = "2026-04-08T12:00:00Z"
47
- django = "2026-04-08T12:00:00Z"
48
49
 
49
50
 
50
51
  [tool.cli_base.pip_audit]
@@ -3,11 +3,12 @@ revision = 3
3
3
  requires-python = ">=3.12"
4
4
 
5
5
  [options]
6
- exclude-newer = "2026-04-02T15:57:08.688116652Z"
6
+ exclude-newer = "2026-04-07T13:11:37.779330069Z"
7
7
  exclude-newer-span = "P1W"
8
8
 
9
9
  [options.exclude-newer-package]
10
- django = "2026-04-08T12:00:00Z"
10
+ uv = "2026-04-13T12:00:00Z"
11
+ cli-base-utilities = "2026-04-13T12:00:00Z"
11
12
  cryptography = "2026-04-08T12:00:00Z"
12
13
 
13
14
  [[package]]
@@ -208,7 +209,7 @@ wheels = [
208
209
 
209
210
  [[package]]
210
211
  name = "cli-base-utilities"
211
- version = "0.29.0"
212
+ version = "0.30.0"
212
213
  source = { registry = "https://pypi.org/simple" }
213
214
  dependencies = [
214
215
  { name = "bx-py-utils" },
@@ -218,21 +219,21 @@ dependencies = [
218
219
  { name = "tomlkit" },
219
220
  { name = "tyro" },
220
221
  ]
221
- sdist = { url = "https://files.pythonhosted.org/packages/88/6c/49fb51ab88452d2da639771ea08aa36f43f055245469936fcb8f8efeb351/cli_base_utilities-0.29.0.tar.gz", hash = "sha256:8b5d4caeedb2219ab443befd6641a72e7201b9774e86454da5abafad78382bff", size = 154656, upload-time = "2026-03-28T09:38:54.791Z" }
222
+ sdist = { url = "https://files.pythonhosted.org/packages/d5/f5/93c8df59459440ef34e24812a5d7e76aea445b8966b777b9ca7174f8ca44/cli_base_utilities-0.30.0.tar.gz", hash = "sha256:a27966515c61604f1cdce5ab221d6c8f92a85f36e7426091fb2cb40d61f109e5", size = 156201, upload-time = "2026-04-11T10:50:14.473Z" }
222
223
  wheels = [
223
- { url = "https://files.pythonhosted.org/packages/95/ad/e39a5490291d8124099d9ac6ff9aad2d021142d1594ce8ed6cc743fbaadc/cli_base_utilities-0.29.0-py3-none-any.whl", hash = "sha256:42dd0215960f5c9ece453c3a4eaddd79d2426c7da012ef1747f45f58c70acfe8", size = 107547, upload-time = "2026-03-28T09:38:52.944Z" },
224
+ { url = "https://files.pythonhosted.org/packages/c4/7f/656f57ac19564b759729b0ff94dcbca5bfd9a53fe0a15633c4fd523117fb/cli_base_utilities-0.30.0-py3-none-any.whl", hash = "sha256:7952a5bfc667e2c974e3215ad3c4e52e785d338f7f9ab2bc4678d2f70c6c2574", size = 107684, upload-time = "2026-04-11T10:50:12.664Z" },
224
225
  ]
225
226
 
226
227
  [[package]]
227
228
  name = "click"
228
- version = "8.3.1"
229
+ version = "8.3.2"
229
230
  source = { registry = "https://pypi.org/simple" }
230
231
  dependencies = [
231
232
  { name = "colorama", marker = "sys_platform == 'win32'" },
232
233
  ]
233
- sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
234
+ sdist = { url = "https://files.pythonhosted.org/packages/57/75/31212c6bf2503fdf920d87fee5d7a86a2e3bcf444984126f13d8e4016804/click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", size = 302856, upload-time = "2026-04-03T19:14:45.118Z" }
234
235
  wheels = [
235
- { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
236
+ { url = "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d", size = 108379, upload-time = "2026-04-03T19:14:43.505Z" },
236
237
  ]
237
238
 
238
239
  [[package]]
@@ -803,11 +804,11 @@ wheels = [
803
804
 
804
805
  [[package]]
805
806
  name = "more-itertools"
806
- version = "11.0.0"
807
+ version = "11.0.1"
807
808
  source = { registry = "https://pypi.org/simple" }
808
- sdist = { url = "https://files.pythonhosted.org/packages/c3/41/181494479e399292ac5b4c6ee9e4688642d4591a50a317e8180c2335ad2a/more_itertools-11.0.0.tar.gz", hash = "sha256:1fca6853f57fbfabc36ad31b3c3d72490340a414f1cc0eec78436fb847837304", size = 144524, upload-time = "2026-04-02T15:06:59.29Z" }
809
+ sdist = { url = "https://files.pythonhosted.org/packages/24/24/e0acc4bf54cba50c1d432c70a72a3df96db4a321b2c4c68432a60759044f/more_itertools-11.0.1.tar.gz", hash = "sha256:fefaf25b7ab08f0b45fa9f1892cae93b9fc0089ef034d39213bce15f1cc9e199", size = 144739, upload-time = "2026-04-02T16:17:45.061Z" }
809
810
  wheels = [
810
- { url = "https://files.pythonhosted.org/packages/dd/70/e2bf8abdcb3f352befbaf74313590c25d69936f8dbb6d6ad1d279988ca1b/more_itertools-11.0.0-py3-none-any.whl", hash = "sha256:88d9f11b0231f65df574a2e626dc37ec60814a9609296aa102755041cca0d00b", size = 72067, upload-time = "2026-04-02T15:06:57.882Z" },
811
+ { url = "https://files.pythonhosted.org/packages/d8/f4/5e52c7319b8087acef603ed6e50dc325c02eaa999355414830468611f13c/more_itertools-11.0.1-py3-none-any.whl", hash = "sha256:eaf287826069452a8f61026c597eae2428b2d1ba2859083abbf240b46842ce6d", size = 72182, upload-time = "2026-04-02T16:17:43.724Z" },
811
812
  ]
812
813
 
813
814
  [[package]]
@@ -1146,7 +1147,7 @@ dev = [
1146
1147
  [package.metadata]
1147
1148
  requires-dist = [
1148
1149
  { name = "bx-py-utils" },
1149
- { name = "cli-base-utilities", specifier = ">=0.27.1" },
1150
+ { name = "cli-base-utilities" },
1150
1151
  { name = "rich" },
1151
1152
  { name = "tyro" },
1152
1153
  ]
@@ -1337,27 +1338,27 @@ wheels = [
1337
1338
 
1338
1339
  [[package]]
1339
1340
  name = "ruff"
1340
- version = "0.15.8"
1341
- source = { registry = "https://pypi.org/simple" }
1342
- sdist = { url = "https://files.pythonhosted.org/packages/14/b0/73cf7550861e2b4824950b8b52eebdcc5adc792a00c514406556c5b80817/ruff-0.15.8.tar.gz", hash = "sha256:995f11f63597ee362130d1d5a327a87cb6f3f5eae3094c620bcc632329a4d26e", size = 4610921, upload-time = "2026-03-26T18:39:38.675Z" }
1343
- wheels = [
1344
- { url = "https://files.pythonhosted.org/packages/4a/92/c445b0cd6da6e7ae51e954939cb69f97e008dbe750cfca89b8cedc081be7/ruff-0.15.8-py3-none-linux_armv6l.whl", hash = "sha256:cbe05adeba76d58162762d6b239c9056f1a15a55bd4b346cfd21e26cd6ad7bc7", size = 10527394, upload-time = "2026-03-26T18:39:41.566Z" },
1345
- { url = "https://files.pythonhosted.org/packages/eb/92/f1c662784d149ad1414cae450b082cf736430c12ca78367f20f5ed569d65/ruff-0.15.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d3e3d0b6ba8dca1b7ef9ab80a28e840a20070c4b62e56d675c24f366ef330570", size = 10905693, upload-time = "2026-03-26T18:39:30.364Z" },
1346
- { url = "https://files.pythonhosted.org/packages/ca/f2/7a631a8af6d88bcef997eb1bf87cc3da158294c57044aafd3e17030613de/ruff-0.15.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6ee3ae5c65a42f273f126686353f2e08ff29927b7b7e203b711514370d500de3", size = 10323044, upload-time = "2026-03-26T18:39:33.37Z" },
1347
- { url = "https://files.pythonhosted.org/packages/67/18/1bf38e20914a05e72ef3b9569b1d5c70a7ef26cd188d69e9ca8ef588d5bf/ruff-0.15.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdce027ada77baa448077ccc6ebb2fa9c3c62fd110d8659d601cf2f475858d94", size = 10629135, upload-time = "2026-03-26T18:39:44.142Z" },
1348
- { url = "https://files.pythonhosted.org/packages/d2/e9/138c150ff9af60556121623d41aba18b7b57d95ac032e177b6a53789d279/ruff-0.15.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12e617fc01a95e5821648a6df341d80456bd627bfab8a829f7cfc26a14a4b4a3", size = 10348041, upload-time = "2026-03-26T18:39:52.178Z" },
1349
- { url = "https://files.pythonhosted.org/packages/02/f1/5bfb9298d9c323f842c5ddeb85f1f10ef51516ac7a34ba446c9347d898df/ruff-0.15.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:432701303b26416d22ba696c39f2c6f12499b89093b61360abc34bcc9bf07762", size = 11121987, upload-time = "2026-03-26T18:39:55.195Z" },
1350
- { url = "https://files.pythonhosted.org/packages/10/11/6da2e538704e753c04e8d86b1fc55712fdbdcc266af1a1ece7a51fff0d10/ruff-0.15.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d910ae974b7a06a33a057cb87d2a10792a3b2b3b35e33d2699fdf63ec8f6b17a", size = 11951057, upload-time = "2026-03-26T18:39:19.18Z" },
1351
- { url = "https://files.pythonhosted.org/packages/83/f0/c9208c5fd5101bf87002fed774ff25a96eea313d305f1e5d5744698dc314/ruff-0.15.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2033f963c43949d51e6fdccd3946633c6b37c484f5f98c3035f49c27395a8ab8", size = 11464613, upload-time = "2026-03-26T18:40:06.301Z" },
1352
- { url = "https://files.pythonhosted.org/packages/f8/22/d7f2fabdba4fae9f3b570e5605d5eb4500dcb7b770d3217dca4428484b17/ruff-0.15.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f29b989a55572fb885b77464cf24af05500806ab4edf9a0fd8977f9759d85b1", size = 11257557, upload-time = "2026-03-26T18:39:57.972Z" },
1353
- { url = "https://files.pythonhosted.org/packages/71/8c/382a9620038cf6906446b23ce8632ab8c0811b8f9d3e764f58bedd0c9a6f/ruff-0.15.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:ac51d486bf457cdc985a412fb1801b2dfd1bd8838372fc55de64b1510eff4bec", size = 11169440, upload-time = "2026-03-26T18:39:22.205Z" },
1354
- { url = "https://files.pythonhosted.org/packages/4d/0d/0994c802a7eaaf99380085e4e40c845f8e32a562e20a38ec06174b52ef24/ruff-0.15.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c9861eb959edab053c10ad62c278835ee69ca527b6dcd72b47d5c1e5648964f6", size = 10605963, upload-time = "2026-03-26T18:39:46.682Z" },
1355
- { url = "https://files.pythonhosted.org/packages/19/aa/d624b86f5b0aad7cef6bbf9cd47a6a02dfdc4f72c92a337d724e39c9d14b/ruff-0.15.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8d9a5b8ea13f26ae90838afc33f91b547e61b794865374f114f349e9036835fb", size = 10357484, upload-time = "2026-03-26T18:39:49.176Z" },
1356
- { url = "https://files.pythonhosted.org/packages/35/c3/e0b7835d23001f7d999f3895c6b569927c4d39912286897f625736e1fd04/ruff-0.15.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c2a33a529fb3cbc23a7124b5c6ff121e4d6228029cba374777bd7649cc8598b8", size = 10830426, upload-time = "2026-03-26T18:40:03.702Z" },
1357
- { url = "https://files.pythonhosted.org/packages/f0/51/ab20b322f637b369383adc341d761eaaa0f0203d6b9a7421cd6e783d81b9/ruff-0.15.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:75e5cd06b1cf3f47a3996cfc999226b19aa92e7cce682dcd62f80d7035f98f49", size = 11345125, upload-time = "2026-03-26T18:39:27.799Z" },
1358
- { url = "https://files.pythonhosted.org/packages/37/e6/90b2b33419f59d0f2c4c8a48a4b74b460709a557e8e0064cf33ad894f983/ruff-0.15.8-py3-none-win32.whl", hash = "sha256:bc1f0a51254ba21767bfa9a8b5013ca8149dcf38092e6a9eb704d876de94dc34", size = 10571959, upload-time = "2026-03-26T18:39:36.117Z" },
1359
- { url = "https://files.pythonhosted.org/packages/1f/a2/ef467cb77099062317154c63f234b8a7baf7cb690b99af760c5b68b9ee7f/ruff-0.15.8-py3-none-win_amd64.whl", hash = "sha256:04f79eff02a72db209d47d665ba7ebcad609d8918a134f86cb13dd132159fc89", size = 11743893, upload-time = "2026-03-26T18:39:25.01Z" },
1360
- { url = "https://files.pythonhosted.org/packages/15/e2/77be4fff062fa78d9b2a4dea85d14785dac5f1d0c1fb58ed52331f0ebe28/ruff-0.15.8-py3-none-win_arm64.whl", hash = "sha256:cf891fa8e3bb430c0e7fac93851a5978fc99c8fa2c053b57b118972866f8e5f2", size = 11048175, upload-time = "2026-03-26T18:40:01.06Z" },
1341
+ version = "0.15.9"
1342
+ source = { registry = "https://pypi.org/simple" }
1343
+ sdist = { url = "https://files.pythonhosted.org/packages/e6/97/e9f1ca355108ef7194e38c812ef40ba98c7208f47b13ad78d023caa583da/ruff-0.15.9.tar.gz", hash = "sha256:29cbb1255a9797903f6dde5ba0188c707907ff44a9006eb273b5a17bfa0739a2", size = 4617361, upload-time = "2026-04-02T18:17:20.829Z" }
1344
+ wheels = [
1345
+ { url = "https://files.pythonhosted.org/packages/0b/1f/9cdfd0ac4b9d1e5a6cf09bedabdf0b56306ab5e333c85c87281273e7b041/ruff-0.15.9-py3-none-linux_armv6l.whl", hash = "sha256:6efbe303983441c51975c243e26dff328aca11f94b70992f35b093c2e71801e1", size = 10511206, upload-time = "2026-04-02T18:16:41.574Z" },
1346
+ { url = "https://files.pythonhosted.org/packages/3d/f6/32bfe3e9c136b35f02e489778d94384118bb80fd92c6d92e7ccd97db12ce/ruff-0.15.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4965bac6ac9ea86772f4e23587746f0b7a395eccabb823eb8bfacc3fa06069f7", size = 10923307, upload-time = "2026-04-02T18:17:08.645Z" },
1347
+ { url = "https://files.pythonhosted.org/packages/ca/25/de55f52ab5535d12e7aaba1de37a84be6179fb20bddcbe71ec091b4a3243/ruff-0.15.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eaf05aad70ca5b5a0a4b0e080df3a6b699803916d88f006efd1f5b46302daab8", size = 10316722, upload-time = "2026-04-02T18:16:44.206Z" },
1348
+ { url = "https://files.pythonhosted.org/packages/48/11/690d75f3fd6278fe55fff7c9eb429c92d207e14b25d1cae4064a32677029/ruff-0.15.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9439a342adb8725f32f92732e2bafb6d5246bd7a5021101166b223d312e8fc59", size = 10623674, upload-time = "2026-04-02T18:16:50.951Z" },
1349
+ { url = "https://files.pythonhosted.org/packages/bd/ec/176f6987be248fc5404199255522f57af1b4a5a1b57727e942479fec98ad/ruff-0.15.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9c5e6faf9d97c8edc43877c3f406f47446fc48c40e1442d58cfcdaba2acea745", size = 10351516, upload-time = "2026-04-02T18:16:57.206Z" },
1350
+ { url = "https://files.pythonhosted.org/packages/b2/fc/51cffbd2b3f240accc380171d51446a32aa2ea43a40d4a45ada67368fbd2/ruff-0.15.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b34a9766aeec27a222373d0b055722900fbc0582b24f39661aa96f3fe6ad901", size = 11150202, upload-time = "2026-04-02T18:17:06.452Z" },
1351
+ { url = "https://files.pythonhosted.org/packages/d6/d4/25292a6dfc125f6b6528fe6af31f5e996e19bf73ca8e3ce6eb7fa5b95885/ruff-0.15.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89dd695bc72ae76ff484ae54b7e8b0f6b50f49046e198355e44ea656e521fef9", size = 11988891, upload-time = "2026-04-02T18:17:18.575Z" },
1352
+ { url = "https://files.pythonhosted.org/packages/13/e1/1eebcb885c10e19f969dcb93d8413dfee8172578709d7ee933640f5e7147/ruff-0.15.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce187224ef1de1bd225bc9a152ac7102a6171107f026e81f317e4257052916d5", size = 11480576, upload-time = "2026-04-02T18:16:52.986Z" },
1353
+ { url = "https://files.pythonhosted.org/packages/ff/6b/a1548ac378a78332a4c3dcf4a134c2475a36d2a22ddfa272acd574140b50/ruff-0.15.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b0c7c341f68adb01c488c3b7d4b49aa8ea97409eae6462d860a79cf55f431b6", size = 11254525, upload-time = "2026-04-02T18:17:02.041Z" },
1354
+ { url = "https://files.pythonhosted.org/packages/42/aa/4bb3af8e61acd9b1281db2ab77e8b2c3c5e5599bf2a29d4a942f1c62b8d6/ruff-0.15.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:55cc15eee27dc0eebdfcb0d185a6153420efbedc15eb1d38fe5e685657b0f840", size = 11204072, upload-time = "2026-04-02T18:17:13.581Z" },
1355
+ { url = "https://files.pythonhosted.org/packages/69/48/d550dc2aa6e423ea0bcc1d0ff0699325ffe8a811e2dba156bd80750b86dc/ruff-0.15.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a6537f6eed5cda688c81073d46ffdfb962a5f29ecb6f7e770b2dc920598997ed", size = 10594998, upload-time = "2026-04-02T18:16:46.369Z" },
1356
+ { url = "https://files.pythonhosted.org/packages/63/47/321167e17f5344ed5ec6b0aa2cff64efef5f9e985af8f5622cfa6536043f/ruff-0.15.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6d3fcbca7388b066139c523bda744c822258ebdcfbba7d24410c3f454cc9af71", size = 10359769, upload-time = "2026-04-02T18:17:10.994Z" },
1357
+ { url = "https://files.pythonhosted.org/packages/67/5e/074f00b9785d1d2c6f8c22a21e023d0c2c1817838cfca4c8243200a1fa87/ruff-0.15.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:058d8e99e1bfe79d8a0def0b481c56059ee6716214f7e425d8e737e412d69677", size = 10850236, upload-time = "2026-04-02T18:16:48.749Z" },
1358
+ { url = "https://files.pythonhosted.org/packages/76/37/804c4135a2a2caf042925d30d5f68181bdbd4461fd0d7739da28305df593/ruff-0.15.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8e1ddb11dbd61d5983fa2d7d6370ef3eb210951e443cace19594c01c72abab4c", size = 11358343, upload-time = "2026-04-02T18:16:55.068Z" },
1359
+ { url = "https://files.pythonhosted.org/packages/88/3d/1364fcde8656962782aa9ea93c92d98682b1ecec2f184e625a965ad3b4a6/ruff-0.15.9-py3-none-win32.whl", hash = "sha256:bde6ff36eaf72b700f32b7196088970bf8fdb2b917b7accd8c371bfc0fd573ec", size = 10583382, upload-time = "2026-04-02T18:17:04.261Z" },
1360
+ { url = "https://files.pythonhosted.org/packages/4c/56/5c7084299bd2cacaa07ae63a91c6f4ba66edc08bf28f356b24f6b717c799/ruff-0.15.9-py3-none-win_amd64.whl", hash = "sha256:45a70921b80e1c10cf0b734ef09421f71b5aa11d27404edc89d7e8a69505e43d", size = 11744969, upload-time = "2026-04-02T18:16:59.611Z" },
1361
+ { url = "https://files.pythonhosted.org/packages/03/36/76704c4f312257d6dbaae3c959add2a622f63fcca9d864659ce6d8d97d3d/ruff-0.15.9-py3-none-win_arm64.whl", hash = "sha256:0694e601c028fd97dc5c6ee244675bc241aeefced7ef80cd9c6935a871078f53", size = 11005870, upload-time = "2026-04-02T18:17:15.773Z" },
1361
1362
  ]
1362
1363
 
1363
1364
  [[package]]
@@ -1503,26 +1504,26 @@ wheels = [
1503
1504
 
1504
1505
  [[package]]
1505
1506
  name = "ty"
1506
- version = "0.0.27"
1507
+ version = "0.0.29"
1507
1508
  source = { registry = "https://pypi.org/simple" }
1508
- sdist = { url = "https://files.pythonhosted.org/packages/f4/de/e5cf1f151cf52fe1189e42d03d90909d7d1354fdc0c1847cbb63a0baa3da/ty-0.0.27.tar.gz", hash = "sha256:d7a8de3421d92420b40c94fe7e7d4816037560621903964dd035cf9bd0204a73", size = 5424130, upload-time = "2026-03-31T19:07:20.806Z" }
1509
+ sdist = { url = "https://files.pythonhosted.org/packages/47/d5/853561de49fae38c519e905b2d8da9c531219608f1fccc47a0fc2c896980/ty-0.0.29.tar.gz", hash = "sha256:e7936cca2f691eeda631876c92809688dbbab68687c3473f526cd83b6a9228d8", size = 5469221, upload-time = "2026-04-05T15:01:21.328Z" }
1509
1510
  wheels = [
1510
- { url = "https://files.pythonhosted.org/packages/fa/20/2a9ea661758bd67f2bfd54ce9daacb5a26c56c5f8b49fbd9a43b365a8a7d/ty-0.0.27-py3-none-linux_armv6l.whl", hash = "sha256:eb14456b8611c9e8287aa9b633f4d2a0d9f3082a31796969e0b50bdda8930281", size = 10571211, upload-time = "2026-03-31T19:07:23.28Z" },
1511
- { url = "https://files.pythonhosted.org/packages/da/b2/8887a51f705d075ddbe78ae7f0d4755ef48d0a90235f67aee289e9cee950/ty-0.0.27-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:02e662184703db7586118df611cf24a000d35dae38d950053d1dd7b6736fd2c4", size = 10427576, upload-time = "2026-03-31T19:07:15.499Z" },
1512
- { url = "https://files.pythonhosted.org/packages/1d/c3/79d88163f508fb709ce19bc0b0a66c7c64b53d372d4caa56172c3d9b3ae8/ty-0.0.27-py3-none-macosx_11_0_arm64.whl", hash = "sha256:be5fc2899441f7f8f7ef40f9ffd006075a5ff6b06c44e8d2aa30e1b900c12f51", size = 9870359, upload-time = "2026-03-31T19:07:36.852Z" },
1513
- { url = "https://files.pythonhosted.org/packages/dc/4d/ed1b0db0e1e46b5ed4976bbfe0d1825faf003b4e3774ef28c785ed73e4bb/ty-0.0.27-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30231e652b14742a76b64755e54bf0cb1cd4c128bcaf625222e0ca92a2094887", size = 10380488, upload-time = "2026-03-31T19:07:31.268Z" },
1514
- { url = "https://files.pythonhosted.org/packages/b1/f2/20372f6d510b01570028433064880adec2f8abe68bf0c4603be61a560bef/ty-0.0.27-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5a119b1168f64261b3205a37e40b5b6c4aac8fd58e4587988f4e4b22c3c79847", size = 10390248, upload-time = "2026-03-31T19:07:28.345Z" },
1515
- { url = "https://files.pythonhosted.org/packages/45/4b/46b31a7311306be1a560f7f20fdc37b5bf718787f60626cd265d9b637554/ty-0.0.27-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e38f4e187b6975d2cbebf0f1eb1221f8f64f6e509bad14d7bb2a91afc97e4956", size = 10878479, upload-time = "2026-03-31T19:07:39.393Z" },
1516
- { url = "https://files.pythonhosted.org/packages/42/ba/5231a2a1fb1cebe053a25de8fded95e1a30a1e77d3628a9e58487297bafc/ty-0.0.27-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a07b1a8fbb23844f6d22091275430d9ac617175f34aa99159b268193de210389", size = 11461232, upload-time = "2026-03-31T19:07:02.518Z" },
1517
- { url = "https://files.pythonhosted.org/packages/c3/37/558abab3e1f6670493524f61280b4dfcc3219555f13889223e733381dfab/ty-0.0.27-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d3ec4033031f240836bb0337274bac5c49dde312c7c6d7575451ed719bf8ffa3", size = 11133002, upload-time = "2026-03-31T19:07:18.371Z" },
1518
- { url = "https://files.pythonhosted.org/packages/32/38/188c14a57f52160407ce62c6abb556011718fd0bcbe1dca690529ce84c46/ty-0.0.27-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:924a8849afd500d260bf5b7296165a05b7424fbb6b19113f30f3b999d682873f", size = 10986624, upload-time = "2026-03-31T19:07:13.066Z" },
1519
- { url = "https://files.pythonhosted.org/packages/9f/f1/667a71393f47d2cd6ba9ed07541b8df3eb63aab1f2ee658e77d91b8362fa/ty-0.0.27-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d8270026c07e7423a1b3a3fd065b46ed1478748f0662518b523b57744f3fa025", size = 10366721, upload-time = "2026-03-31T19:07:00.131Z" },
1520
- { url = "https://files.pythonhosted.org/packages/8b/aa/8edafe41be898bda774249abc5be6edd733e53fb1777d59ea9331e38537d/ty-0.0.27-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e26e9735d3bdfd95d881111ad1cf570eab8188d8c3be36d6bcaad044d38984d8", size = 10412239, upload-time = "2026-03-31T19:07:05.297Z" },
1521
- { url = "https://files.pythonhosted.org/packages/53/ff/8bafaed4a18d38264f46bdfc427de7ea2974cf9064e4e0bdb1b6e6c724e3/ty-0.0.27-py3-none-musllinux_1_2_i686.whl", hash = "sha256:7c09cc9a699810609acc0090af8d0db68adaee6e60a7c3e05ab80cc954a83db7", size = 10573507, upload-time = "2026-03-31T19:06:57.064Z" },
1522
- { url = "https://files.pythonhosted.org/packages/16/2e/63a8284a2fefd08ab56ecbad0fde7dd4b2d4045a31cf24c1d1fcd9643227/ty-0.0.27-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:2d3e02853bb037221a456e034b1898aaa573e6374fbb53884e33cb7513ccb85a", size = 11090233, upload-time = "2026-03-31T19:07:34.139Z" },
1523
- { url = "https://files.pythonhosted.org/packages/14/d3/d6fa1cafdfa2b34dbfa304fc6833af8e1669fc34e24d214fa76d2a2e5a25/ty-0.0.27-py3-none-win32.whl", hash = "sha256:34e7377f2047c14dbbb7bf5322e84114db7a5f2cb470db6bee63f8f3550cfc1e", size = 9984415, upload-time = "2026-03-31T19:07:07.98Z" },
1524
- { url = "https://files.pythonhosted.org/packages/85/e6/dd4e27da9632b3472d5711ca49dbd3709dbd3e8c73f3af6db9c254235ca9/ty-0.0.27-py3-none-win_amd64.whl", hash = "sha256:3f7e4145aad8b815ed69b324c93b5b773eb864dda366ca16ab8693ff88ce6f36", size = 10961535, upload-time = "2026-03-31T19:07:10.566Z" },
1525
- { url = "https://files.pythonhosted.org/packages/0e/1a/824b3496d66852ed7d5d68d9787711131552b68dce8835ce9410db32e618/ty-0.0.27-py3-none-win_arm64.whl", hash = "sha256:95bf8d01eb96bb2ba3ffc39faff19da595176448e80871a7b362f4d2de58476c", size = 10376689, upload-time = "2026-03-31T19:07:25.732Z" },
1511
+ { url = "https://files.pythonhosted.org/packages/03/b7/911f9962115acfa24e3b2ec9d4992dd994c38e8769e1b1d7680bb4d28a51/ty-0.0.29-py3-none-linux_armv6l.whl", hash = "sha256:b8a40955f7660d3eaceb0d964affc81b790c0765e7052921a5f861ff8a471c30", size = 10568206, upload-time = "2026-04-05T15:01:19.165Z" },
1512
+ { url = "https://files.pythonhosted.org/packages/fe/c3/fcae2167d4c77a97269f92f11d1b43b03617f81de1283d5d05b43432110c/ty-0.0.29-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6b6849adae15b00bbe2d3c5b078967dcb62eba37d38936b8eeb4c81a82d2e3b8", size = 10442530, upload-time = "2026-04-05T15:01:28.471Z" },
1513
+ { url = "https://files.pythonhosted.org/packages/97/33/5a6bfa240cfcb9c36046ae2459fa9ea23238d20130d8656ff5ac4d6c012a/ty-0.0.29-py3-none-macosx_11_0_arm64.whl", hash = "sha256:dcdd9b17209788152f7b7ea815eda07989152325052fe690013537cc7904ce49", size = 9915735, upload-time = "2026-04-05T15:01:10.365Z" },
1514
+ { url = "https://files.pythonhosted.org/packages/b3/1e/318f45fae232118e81a6306c30f50de42c509c412128d5bd231eab699ffb/ty-0.0.29-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d8ed4789bae78ffaf94462c0d25589a734cab0366b86f2bbcb1bb90e1a7a169", size = 10419748, upload-time = "2026-04-05T15:01:32.375Z" },
1515
+ { url = "https://files.pythonhosted.org/packages/a9/a8/5687872e2ab5a0f7dd4fd8456eac31e9381ad4dc74961f6f29965ad4dd91/ty-0.0.29-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:91ec374b8565e0ad0900011c24641ebbef2da51adbd4fb69ff3280c8a7eceb02", size = 10394738, upload-time = "2026-04-05T15:01:06.473Z" },
1516
+ { url = "https://files.pythonhosted.org/packages/de/68/015d118097eeb95e6a44c4abce4c0a28b7b9dfb3085b7f0ee48e4f099633/ty-0.0.29-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:298a8d5faa2502d3810bbbb47a030b9455495b9921594206043c785dd61548cf", size = 10910613, upload-time = "2026-04-05T15:01:17.17Z" },
1517
+ { url = "https://files.pythonhosted.org/packages/1c/01/47ce3c6c53e0670eadbe80756b167bf80ed6681d1ba57cfde2e8065a13d1/ty-0.0.29-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c8fba1a3524c6109d1e020d92301c79d41bf442fa8d335b9fa366239339cb70", size = 11475750, upload-time = "2026-04-05T15:01:30.461Z" },
1518
+ { url = "https://files.pythonhosted.org/packages/c4/cf/e361845b1081c9264ad5b7c963231bab03f2666865a9f2a115c4233f2137/ty-0.0.29-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4c48adf88a70d264128c39ee922ed14a947817fced1e93c08c1a89c9244edcde", size = 11190055, upload-time = "2026-04-05T15:01:12.369Z" },
1519
+ { url = "https://files.pythonhosted.org/packages/79/12/0fb0857e9a62cb11586e9a712103877bbf717f5fb570d16634408cfdefee/ty-0.0.29-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ce0a7a0e96bc7b42518cd3a1a6a6298ef64ff40ca4614355c1aa807059b5c6f", size = 11020539, upload-time = "2026-04-05T15:01:37.022Z" },
1520
+ { url = "https://files.pythonhosted.org/packages/20/36/5a26753802083f80cd125db6c4348ad42b3c982ec36e718e0bf4c18f75e5/ty-0.0.29-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a6ac86a05b4a3731d45365ab97780acc7b8146fa62fccb3cbe94fe6546c67a97", size = 10396399, upload-time = "2026-04-05T15:01:26.167Z" },
1521
+ { url = "https://files.pythonhosted.org/packages/00/e6/b4e75b5752239ab3ab400f19faef4dbef81d05aab5d3419fda0c062a3765/ty-0.0.29-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6bbbf53141af0f3150bf288d716263f1a3550054e4b3551ca866d38192ba9891", size = 10421461, upload-time = "2026-04-05T15:01:08.367Z" },
1522
+ { url = "https://files.pythonhosted.org/packages/c0/21/1084b5b609f9abed62070ec0b31c283a403832a6310c8bbc208bd45ee1e6/ty-0.0.29-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1c9e06b770c1d0ff5efc51e34312390db31d53fcf3088163f413030b42b74f84", size = 10599187, upload-time = "2026-04-05T15:01:23.52Z" },
1523
+ { url = "https://files.pythonhosted.org/packages/ab/a1/ce19a2ca717bbcc1ee11378aba52ef70b6ce5b87245162a729d9fdc2360f/ty-0.0.29-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0307fe37e3f000ef1a4ae230bbaf511508a78d24a5e51b40902a21b09d5e6037", size = 11121198, upload-time = "2026-04-05T15:01:15.22Z" },
1524
+ { url = "https://files.pythonhosted.org/packages/6b/6b/f1430b279af704321566ce7ec2725d3d8258c2f815ebd93e474c64cd4543/ty-0.0.29-py3-none-win32.whl", hash = "sha256:7a2a898217960a825f8bc0087e1fdbaf379606175e98f9807187221d53a4a8ed", size = 9995331, upload-time = "2026-04-05T15:01:01.32Z" },
1525
+ { url = "https://files.pythonhosted.org/packages/d2/ef/3ef01c17785ff9a69378465c7d0faccd48a07b163554db0995e5d65a5a23/ty-0.0.29-py3-none-win_amd64.whl", hash = "sha256:fc1294200226b91615acbf34e0a9ad81caf98c081e9c6a912a31b0a7b603bc3f", size = 11023644, upload-time = "2026-04-05T15:01:04.432Z" },
1526
+ { url = "https://files.pythonhosted.org/packages/2c/55/87280a994d6a2d2647c65e12abbc997ed49835794366153c04c4d9304d76/ty-0.0.29-py3-none-win_arm64.whl", hash = "sha256:f9794bbd1bb3ce13f78c191d0c89ae4c63f52c12b6daa0c6fe220b90d019d12c", size = 10428165, upload-time = "2026-04-05T15:01:34.665Z" },
1526
1527
  ]
1527
1528
 
1528
1529
  [[package]]
@@ -1562,11 +1563,11 @@ wheels = [
1562
1563
 
1563
1564
  [[package]]
1564
1565
  name = "tzdata"
1565
- version = "2025.3"
1566
+ version = "2026.1"
1566
1567
  source = { registry = "https://pypi.org/simple" }
1567
- sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" }
1568
+ sdist = { url = "https://files.pythonhosted.org/packages/19/f5/cd531b2d15a671a40c0f66cf06bc3570a12cd56eef98960068ebbad1bf5a/tzdata-2026.1.tar.gz", hash = "sha256:67658a1903c75917309e753fdc349ac0efd8c27db7a0cb406a25be4840f87f98", size = 197639, upload-time = "2026-04-03T11:25:22.002Z" }
1568
1569
  wheels = [
1569
- { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" },
1570
+ { url = "https://files.pythonhosted.org/packages/b0/70/d460bd685a170790ec89317e9bd33047988e4bce507b831f5db771e142de/tzdata-2026.1-py2.py3-none-any.whl", hash = "sha256:4b1d2be7ac37ceafd7327b961aa3a54e467efbdb563a23655fbfe0d39cfc42a9", size = 348952, upload-time = "2026-04-03T11:25:20.313Z" },
1570
1571
  ]
1571
1572
 
1572
1573
  [[package]]
@@ -1580,28 +1581,28 @@ wheels = [
1580
1581
 
1581
1582
  [[package]]
1582
1583
  name = "uv"
1583
- version = "0.11.3"
1584
- source = { registry = "https://pypi.org/simple" }
1585
- sdist = { url = "https://files.pythonhosted.org/packages/88/ed/f11c558e8d2e02fba6057dacd9e92a71557359a80bd5355452310b89f40f/uv-0.11.3.tar.gz", hash = "sha256:6a6fcaf1fec28bbbdf0dfc5a0a6e34be4cea08c6287334b08c24cf187300f20d", size = 4027684, upload-time = "2026-04-01T21:47:22.096Z" }
1586
- wheels = [
1587
- { url = "https://files.pythonhosted.org/packages/cb/93/4f04c49fd6046a18293de341d795ded3b9cbd95db261d687e26db0f11d1e/uv-0.11.3-py3-none-linux_armv6l.whl", hash = "sha256:deb533e780e8181e0859c68c84f546620072cd1bd827b38058cb86ebfba9bb7d", size = 23337334, upload-time = "2026-04-01T21:46:47.545Z" },
1588
- { url = "https://files.pythonhosted.org/packages/7a/4b/c44fd3fbc80ac2f81e2ad025d235c820aac95b228076da85be3f5d509781/uv-0.11.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d2b3b0fa1693880ca354755c216ae1c65dd938a4f1a24374d0c3f4b9538e0ee6", size = 22940169, upload-time = "2026-04-01T21:47:32.72Z" },
1589
- { url = "https://files.pythonhosted.org/packages/ba/c7/7d01be259a47d42fa9e80adcb7a829d81e7c376aa8fa1b714f31d7dfc226/uv-0.11.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:71f5d0b9e73daa5d8a7e2db3fa2e22a4537d24bb4fe78130db797280280d4edc", size = 21473579, upload-time = "2026-04-01T21:47:25.063Z" },
1590
- { url = "https://files.pythonhosted.org/packages/9a/71/fffcd890290a4639a3799cf3f3e87947c10d1b0de19eba3cf837cb418dd8/uv-0.11.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:55ba578752f29a3f2b22879b22a162edad1454e3216f3ca4694fdbd4093a6822", size = 23132691, upload-time = "2026-04-01T21:47:44.587Z" },
1591
- { url = "https://files.pythonhosted.org/packages/d1/7b/1ac9e1f753a19b6252434f0bbe96efdcc335cd74677f4c6f431a7c916114/uv-0.11.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:3b1fe09d5e1d8e19459cd28d7825a3b66ef147b98328345bad6e17b87c4fea48", size = 22955764, upload-time = "2026-04-01T21:46:51.721Z" },
1592
- { url = "https://files.pythonhosted.org/packages/ff/51/1a6010a681a3c3e0a8ec99737ba2d0452194dc372a5349a9267873261c02/uv-0.11.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:088165b9eed981d2c2a58566cc75dd052d613e47c65e2416842d07308f793a6f", size = 22966245, upload-time = "2026-04-01T21:47:07.403Z" },
1593
- { url = "https://files.pythonhosted.org/packages/38/74/1a1b0712daead7e85f56d620afe96fe166a04b615524c14027b4edd39b82/uv-0.11.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef0ae8ee2988928092616401ec7f473612b8e9589fe1567452c45dbc56840f85", size = 24623370, upload-time = "2026-04-01T21:47:03.59Z" },
1594
- { url = "https://files.pythonhosted.org/packages/b6/62/5c3aa5e7bd2744810e50ad72a5951386ec84a513e109b1b5cb7ec442f3b6/uv-0.11.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6708827ecb846d00c5512a7e4dc751c2e27b92e9bd55a0be390561ac68930c32", size = 25142735, upload-time = "2026-04-01T21:46:55.756Z" },
1595
- { url = "https://files.pythonhosted.org/packages/88/ab/6266a04980e0877af5518762adfe23a0c1ab0b801ae3099a2e7b74e34411/uv-0.11.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8df030ea7563e99c09854e1bc82ab743dfa2d0ba18976e6861979cb40d04dba7", size = 24512083, upload-time = "2026-04-01T21:46:43.531Z" },
1596
- { url = "https://files.pythonhosted.org/packages/4e/be/7c66d350f833eb437f9aa0875655cc05e07b441e3f4a770f8bced56133f7/uv-0.11.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fde893b5ab9f6997fe357138e794bac09d144328052519fbbe2e6f72145e457", size = 24589293, upload-time = "2026-04-01T21:47:11.379Z" },
1597
- { url = "https://files.pythonhosted.org/packages/18/4f/22ada41564a8c8c36653fc86f89faae4c54a4cdd5817bda53764a3eb352d/uv-0.11.3-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:45006bcd9e8718248a23ab81448a5beb46a72a9dd508e3212d6f3b8c63aeb88a", size = 23214854, upload-time = "2026-04-01T21:46:59.491Z" },
1598
- { url = "https://files.pythonhosted.org/packages/aa/18/8669840657fea9fd668739dec89643afe1061c023c1488228b02f79a2399/uv-0.11.3-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:089b9d338a64463956b6fee456f03f73c9a916479bdb29009600781dc1e1d2a7", size = 23914434, upload-time = "2026-04-01T21:47:29.164Z" },
1599
- { url = "https://files.pythonhosted.org/packages/08/0d/c59f24b3a1ae5f377aa6fd9653562a0968ea6be946fe35761871a0072919/uv-0.11.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:3ff461335888336467402cc5cb792c911df95dd0b52e369182cfa4c902bb21f4", size = 23971481, upload-time = "2026-04-01T21:47:48.551Z" },
1600
- { url = "https://files.pythonhosted.org/packages/66/7d/f83ed79921310ef216ed6d73fcd3822dff4b66749054fb97e09b7bd5901e/uv-0.11.3-py3-none-musllinux_1_1_i686.whl", hash = "sha256:a62e29277efd39c35caf4a0fe739c4ebeb14d4ce4f02271f3f74271d608061ff", size = 23784797, upload-time = "2026-04-01T21:47:40.588Z" },
1601
- { url = "https://files.pythonhosted.org/packages/35/19/3ff3539c44ca7dc2aa87b021d4a153ba6a72866daa19bf91c289e4318f95/uv-0.11.3-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:ebccdcdebd2b288925f0f7c18c39705dc783175952eacaf94912b01d3b381b86", size = 24794606, upload-time = "2026-04-01T21:47:36.814Z" },
1602
- { url = "https://files.pythonhosted.org/packages/79/e5/e676454bb7cc5dcf5c4637ed3ef0ff97309d84a149b832a4dea53f04c0ab/uv-0.11.3-py3-none-win32.whl", hash = "sha256:794aae3bab141eafbe37c51dc5dd0139658a755a6fa9cc74d2dbd7c71dcc4826", size = 22573432, upload-time = "2026-04-01T21:47:15.143Z" },
1603
- { url = "https://files.pythonhosted.org/packages/ff/a0/95d22d524bd3b4708043d65035f02fc9656e5fb6e0aaef73510313b1641b/uv-0.11.3-py3-none-win_amd64.whl", hash = "sha256:68fda574f2e5e7536a2b747dcea88329a71aad7222317e8f4717d0af8f99fbd4", size = 24969508, upload-time = "2026-04-01T21:47:19.515Z" },
1604
- { url = "https://files.pythonhosted.org/packages/f8/6d/3f0b90a06e8c4594e11f813651756d6896de6dd4461f554fd7e4984a1c4f/uv-0.11.3-py3-none-win_arm64.whl", hash = "sha256:92ffc4d521ab2c4738ef05d8ef26f2750e26d31f3ad5611cdfefc52445be9ace", size = 23488911, upload-time = "2026-04-01T21:47:52.427Z" },
1584
+ version = "0.11.6"
1585
+ source = { registry = "https://pypi.org/simple" }
1586
+ sdist = { url = "https://files.pythonhosted.org/packages/dd/f3/8aceeab67ea69805293ab290e7ca8cc1b61a064d28b8a35c76d8eba063dd/uv-0.11.6.tar.gz", hash = "sha256:e3b21b7e80024c95ff339fcd147ac6fc3dd98d3613c9d45d3a1f4fd1057f127b", size = 4073298, upload-time = "2026-04-09T12:09:01.738Z" }
1587
+ wheels = [
1588
+ { url = "https://files.pythonhosted.org/packages/1f/fe/4b61a3d5ad9d02e8a4405026ccd43593d7044598e0fa47d892d4dafe44c9/uv-0.11.6-py3-none-linux_armv6l.whl", hash = "sha256:ada04dcf89ddea5b69d27ac9cdc5ef575a82f90a209a1392e930de504b2321d6", size = 23780079, upload-time = "2026-04-09T12:08:56.609Z" },
1589
+ { url = "https://files.pythonhosted.org/packages/52/db/d27519a9e1a5ffee9d71af1a811ad0e19ce7ab9ae815453bef39dd479389/uv-0.11.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5be013888420f96879c6e0d3081e7bcf51b539b034a01777041934457dfbedf3", size = 23214721, upload-time = "2026-04-09T12:09:32.228Z" },
1590
+ { url = "https://files.pythonhosted.org/packages/a6/8f/4399fa8b882bd7e0efffc829f73ab24d117d490a93e6bc7104a50282b854/uv-0.11.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ffa5dc1cbb52bdce3b8447e83d1601a57ad4da6b523d77d4b47366db8b1ceb18", size = 21750109, upload-time = "2026-04-09T12:09:24.357Z" },
1591
+ { url = "https://files.pythonhosted.org/packages/32/07/5a12944c31c3dda253632da7a363edddb869ed47839d4d92a2dc5f546c93/uv-0.11.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:bfb107b4dade1d2c9e572992b06992d51dd5f2136eb8ceee9e62dd124289e825", size = 23551146, upload-time = "2026-04-09T12:09:10.439Z" },
1592
+ { url = "https://files.pythonhosted.org/packages/79/5b/2ec8b0af80acd1016ed596baf205ddc77b19ece288473b01926c4a9cf6db/uv-0.11.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:9e2fe7ce12161d8016b7deb1eaad7905a76ff7afec13383333ca75e0c4b5425d", size = 23331192, upload-time = "2026-04-09T12:09:34.792Z" },
1593
+ { url = "https://files.pythonhosted.org/packages/62/7d/eea35935f2112b21c296a3e42645f3e4b1aa8bcd34dcf13345fbd55134b7/uv-0.11.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7ed9c6f70c25e8dfeedddf4eddaf14d353f5e6b0eb43da9a14d3a1033d51d915", size = 23337686, upload-time = "2026-04-09T12:09:18.522Z" },
1594
+ { url = "https://files.pythonhosted.org/packages/21/47/2584f5ab618f6ebe9bdefb2f765f2ca8540e9d739667606a916b35449eec/uv-0.11.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d68a013e609cebf82077cbeeb0809ed5e205257814273bfd31e02fc0353bbfc2", size = 25008139, upload-time = "2026-04-09T12:09:03.983Z" },
1595
+ { url = "https://files.pythonhosted.org/packages/95/81/497ae5c1d36355b56b97dc59f550c7e89d0291c163a3f203c6f341dff195/uv-0.11.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:93f736dddca03dae732c6fdea177328d3bc4bf137c75248f3d433c57416a4311", size = 25712458, upload-time = "2026-04-09T12:09:07.598Z" },
1596
+ { url = "https://files.pythonhosted.org/packages/3c/1c/74083238e4fab2672b63575b9008f1ea418b02a714bcfcf017f4f6a309b6/uv-0.11.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e96a66abe53fced0e3389008b8d2eff8278cfa8bb545d75631ae8ceb9c929aba", size = 24915507, upload-time = "2026-04-09T12:08:50.892Z" },
1597
+ { url = "https://files.pythonhosted.org/packages/5a/ee/e14fe10ba455a823ed18233f12de6699a601890905420b5c504abf115116/uv-0.11.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b096311b2743b228df911a19532b3f18fa420bf9530547aecd6a8e04bbfaccd", size = 24971011, upload-time = "2026-04-09T12:08:54.016Z" },
1598
+ { url = "https://files.pythonhosted.org/packages/3c/a1/7b9c83eaadf98e343317ff6384a7227a4855afd02cdaf9696bcc71ee6155/uv-0.11.6-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:904d537b4a6e798015b4a64ff5622023bd4601b43b6cd1e5f423d63471f5e948", size = 23640234, upload-time = "2026-04-09T12:09:15.735Z" },
1599
+ { url = "https://files.pythonhosted.org/packages/d6/51/75ccdd23e76ff1703b70eb82881cd5b4d2a954c9679f8ef7e0136ef2cfab/uv-0.11.6-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:4ed8150c26b5e319381d75ae2ce6aba1e9c65888f4850f4e3b3fa839953c90a5", size = 24452664, upload-time = "2026-04-09T12:09:26.875Z" },
1600
+ { url = "https://files.pythonhosted.org/packages/4d/86/ace80fe47d8d48b5e3b5aee0b6eb1a49deaacc2313782870250b3faa36f5/uv-0.11.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1c9218c8d4ac35ca6e617fb0951cc0ab2d907c91a6aea2617de0a5494cf162c0", size = 24494599, upload-time = "2026-04-09T12:09:37.368Z" },
1601
+ { url = "https://files.pythonhosted.org/packages/05/2d/4b642669b56648194f026de79bc992cbfc3ac2318b0a8d435f3c284934e8/uv-0.11.6-py3-none-musllinux_1_1_i686.whl", hash = "sha256:9e211c83cc890c569b86a4183fcf5f8b6f0c7adc33a839b699a98d30f1310d3a", size = 24159150, upload-time = "2026-04-09T12:09:13.17Z" },
1602
+ { url = "https://files.pythonhosted.org/packages/ae/24/7eecd76fe983a74fed1fc700a14882e70c4e857f1d562a9f2303d4286c12/uv-0.11.6-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:d2a1d2089afdf117ad19a4c1dd36b8189c00ae1ad4135d3bfbfced82342595cf", size = 25164324, upload-time = "2026-04-09T12:08:59.56Z" },
1603
+ { url = "https://files.pythonhosted.org/packages/27/e0/bbd4ba7c2e5067bbba617d87d306ec146889edaeeaa2081d3e122178ca08/uv-0.11.6-py3-none-win32.whl", hash = "sha256:6e8344f38fa29f85dcfd3e62dc35a700d2448f8e90381077ef393438dcd5012e", size = 22865693, upload-time = "2026-04-09T12:09:21.415Z" },
1604
+ { url = "https://files.pythonhosted.org/packages/a5/33/1983ce113c538a856f2d620d16e39691962ecceef091a84086c5785e32e5/uv-0.11.6-py3-none-win_amd64.whl", hash = "sha256:a28bea69c1186303d1200f155c7a28c449f8a4431e458fcf89360cc7ef546e40", size = 25371258, upload-time = "2026-04-09T12:09:40.52Z" },
1605
+ { url = "https://files.pythonhosted.org/packages/35/01/be0873f44b9c9bc250fcbf263367fcfc1f59feab996355bcb6b52fff080d/uv-0.11.6-py3-none-win_arm64.whl", hash = "sha256:a78f6d64b9950e24061bc7ec7f15ff8089ad7f5a976e7b65fcadce58fe02f613", size = 23869585, upload-time = "2026-04-09T12:09:29.425Z" },
1605
1606
  ]
1606
1607
 
1607
1608
  [[package]]