comicbox 5.1.2__tar.gz → 5.2.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 (595) hide show
  1. {comicbox-5.1.2 → comicbox-5.2.0}/.gitignore +1 -0
  2. {comicbox-5.1.2 → comicbox-5.2.0}/NEWS.md +28 -0
  3. {comicbox-5.1.2 → comicbox-5.2.0}/PKG-INFO +17 -6
  4. {comicbox-5.1.2 → comicbox-5.2.0}/README.md +15 -4
  5. {comicbox-5.1.2 → comicbox-5.2.0}/bun.lock +18 -18
  6. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/box/archive/write.py +39 -25
  7. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/box/dump.py +18 -3
  8. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/box/dump_files.py +7 -4
  9. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/config/online/settings.py +7 -4
  10. comicbox-5.2.0/comicbox/exceptions.py +102 -0
  11. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/base/online/outcome_stats.py +13 -8
  12. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/base/online/profile.py +8 -0
  13. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/base/online/rate_limits.py +5 -3
  14. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/base/online/retry.py +1 -1
  15. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/comicvine_api/online_source.py +20 -4
  16. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/metron_api/online_source.py +89 -15
  17. comicbox-5.2.0/comicbox/formats/metron_api/paced_session.py +306 -0
  18. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/online_session.py +25 -0
  19. comicbox-5.2.0/comicbox/predict.py +72 -0
  20. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/run.py +23 -3
  21. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/write.py +170 -53
  22. {comicbox-5.1.2 → comicbox-5.2.0}/docs/NEWS.md +28 -0
  23. {comicbox-5.1.2 → comicbox-5.2.0}/docs/README.md +15 -4
  24. {comicbox-5.1.2 → comicbox-5.2.0}/package.json +3 -3
  25. {comicbox-5.1.2 → comicbox-5.2.0}/pyproject.toml +2 -2
  26. {comicbox-5.1.2 → comicbox-5.2.0}/pyproject.toml.orig +2 -2
  27. {comicbox-5.1.2 → comicbox-5.2.0}/tests/conftest.py +7 -7
  28. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_comicvine_client.py +73 -0
  29. comicbox-5.2.0/tests/unit/test_destination_occupied_error.py +99 -0
  30. comicbox-5.2.0/tests/unit/test_metron_session_lifecycle.py +183 -0
  31. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_metron_source.py +46 -1
  32. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_outcome_stats.py +34 -0
  33. comicbox-5.2.0/tests/unit/test_paced_session.py +459 -0
  34. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_rate_gate_concurrency.py +77 -57
  35. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_rate_limits.py +10 -8
  36. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_rename_and_convert_safety.py +34 -14
  37. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_write_api.py +6 -1
  38. comicbox-5.2.0/tests/unit/test_write_destination_prediction.py +172 -0
  39. comicbox-5.2.0/tests/unit/test_write_destination_safety.py +376 -0
  40. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_write_order_and_claims.py +8 -6
  41. comicbox-5.2.0/tests/util/metron_transport.py +142 -0
  42. {comicbox-5.1.2 → comicbox-5.2.0}/uv.lock +84 -84
  43. comicbox-5.1.2/comicbox/exceptions.py +0 -49
  44. comicbox-5.1.2/comicbox/formats/metron_api/paced_session.py +0 -273
  45. comicbox-5.1.2/tests/unit/test_paced_session.py +0 -469
  46. {comicbox-5.1.2 → comicbox-5.2.0}/.dockerignore +0 -0
  47. {comicbox-5.1.2 → comicbox-5.2.0}/.prettierignore +0 -0
  48. {comicbox-5.1.2 → comicbox-5.2.0}/.readthedocs.yaml +0 -0
  49. {comicbox-5.1.2 → comicbox-5.2.0}/.remarkignore +0 -0
  50. {comicbox-5.1.2 → comicbox-5.2.0}/.shellcheckrc +0 -0
  51. {comicbox-5.1.2 → comicbox-5.2.0}/Makefile +0 -0
  52. {comicbox-5.1.2 → comicbox-5.2.0}/bin/ci-download-dist-if-identical.sh +0 -0
  53. {comicbox-5.1.2 → comicbox-5.2.0}/bin/clean-pycache.sh +0 -0
  54. {comicbox-5.1.2 → comicbox-5.2.0}/bin/cleanup-pypi-alpha.sh +0 -0
  55. {comicbox-5.1.2 → comicbox-5.2.0}/bin/dev-module.sh +0 -0
  56. {comicbox-5.1.2 → comicbox-5.2.0}/bin/docker-compose-exit.sh +0 -0
  57. {comicbox-5.1.2 → comicbox-5.2.0}/bin/docker-tag-latest.sh +0 -0
  58. {comicbox-5.1.2 → comicbox-5.2.0}/bin/fix-docker.sh +0 -0
  59. {comicbox-5.1.2 → comicbox-5.2.0}/bin/fix-python.sh +0 -0
  60. {comicbox-5.1.2 → comicbox-5.2.0}/bin/fix-sh.sh +0 -0
  61. {comicbox-5.1.2 → comicbox-5.2.0}/bin/fix.sh +0 -0
  62. {comicbox-5.1.2 → comicbox-5.2.0}/bin/kill-eslint_d.sh +0 -0
  63. {comicbox-5.1.2 → comicbox-5.2.0}/bin/lint-ci.sh +0 -0
  64. {comicbox-5.1.2 → comicbox-5.2.0}/bin/lint-complexity.sh +0 -0
  65. {comicbox-5.1.2 → comicbox-5.2.0}/bin/lint-darwin.sh +0 -0
  66. {comicbox-5.1.2 → comicbox-5.2.0}/bin/lint-docker.sh +0 -0
  67. {comicbox-5.1.2 → comicbox-5.2.0}/bin/lint-python.sh +0 -0
  68. {comicbox-5.1.2 → comicbox-5.2.0}/bin/lint-sh.sh +0 -0
  69. {comicbox-5.1.2 → comicbox-5.2.0}/bin/lint.sh +0 -0
  70. {comicbox-5.1.2 → comicbox-5.2.0}/bin/prettier-nginx.sh +0 -0
  71. {comicbox-5.1.2 → comicbox-5.2.0}/bin/publish-pypi.sh +0 -0
  72. {comicbox-5.1.2 → comicbox-5.2.0}/bin/regenerate-pdf-test-pages.py +0 -0
  73. {comicbox-5.1.2 → comicbox-5.2.0}/bin/release-tag.sh +0 -0
  74. {comicbox-5.1.2 → comicbox-5.2.0}/bin/remark-for-claude.sh +0 -0
  75. {comicbox-5.1.2 → comicbox-5.2.0}/bin/roman.py +0 -0
  76. {comicbox-5.1.2 → comicbox-5.2.0}/bin/sort-ignore.sh +0 -0
  77. {comicbox-5.1.2 → comicbox-5.2.0}/bin/test-docker.sh +0 -0
  78. {comicbox-5.1.2 → comicbox-5.2.0}/bin/test-python.sh +0 -0
  79. {comicbox-5.1.2 → comicbox-5.2.0}/bin/type-python.sh +0 -0
  80. {comicbox-5.1.2 → comicbox-5.2.0}/bin/uml.sh +0 -0
  81. {comicbox-5.1.2 → comicbox-5.2.0}/bin/update-deps-node.sh +0 -0
  82. {comicbox-5.1.2 → comicbox-5.2.0}/bin/update-deps-python.sh +0 -0
  83. {comicbox-5.1.2 → comicbox-5.2.0}/bin/version-node.sh +0 -0
  84. {comicbox-5.1.2 → comicbox-5.2.0}/bin/version-python.sh +0 -0
  85. {comicbox-5.1.2 → comicbox-5.2.0}/cfg/ci.mk +0 -0
  86. {comicbox-5.1.2 → comicbox-5.2.0}/cfg/common.mk +0 -0
  87. {comicbox-5.1.2 → comicbox-5.2.0}/cfg/docker.mk +0 -0
  88. {comicbox-5.1.2 → comicbox-5.2.0}/cfg/docs.mk +0 -0
  89. {comicbox-5.1.2 → comicbox-5.2.0}/cfg/eslint.config.base.js +0 -0
  90. {comicbox-5.1.2 → comicbox-5.2.0}/cfg/gha_std.mk +0 -0
  91. {comicbox-5.1.2 → comicbox-5.2.0}/cfg/help.mk +0 -0
  92. {comicbox-5.1.2 → comicbox-5.2.0}/cfg/node.mk +0 -0
  93. {comicbox-5.1.2 → comicbox-5.2.0}/cfg/node_root.mk +0 -0
  94. {comicbox-5.1.2 → comicbox-5.2.0}/cfg/python.mk +0 -0
  95. {comicbox-5.1.2 → comicbox-5.2.0}/check-jsonschema.sh +0 -0
  96. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/__init__.py +0 -0
  97. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/_pdf.py +0 -0
  98. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/_rar.py +0 -0
  99. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/box/__init__.py +0 -0
  100. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/box/archive/__init__.py +0 -0
  101. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/box/archive/archive.py +0 -0
  102. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/box/archive/archiveinfo.py +0 -0
  103. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/box/archive/filenames.py +0 -0
  104. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/box/archive/init.py +0 -0
  105. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/box/archive/mtime.py +0 -0
  106. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/box/archive/pages.py +0 -0
  107. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/box/archive/read.py +0 -0
  108. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/box/archive/sniff.py +0 -0
  109. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/box/computed/__init__.py +0 -0
  110. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/box/computed/date.py +0 -0
  111. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/box/computed/identifiers.py +0 -0
  112. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/box/computed/issue.py +0 -0
  113. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/box/computed/notes.py +0 -0
  114. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/box/computed/pages.py +0 -0
  115. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/box/computed/stamp.py +0 -0
  116. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/box/computed/stories_title.py +0 -0
  117. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/box/computed/url_identifiers.py +0 -0
  118. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/box/computed/urls.py +0 -0
  119. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/box/init.py +0 -0
  120. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/box/load.py +0 -0
  121. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/box/merge.py +0 -0
  122. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/box/metadata.py +0 -0
  123. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/box/normalize.py +0 -0
  124. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/box/online_covers.py +0 -0
  125. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/box/online_lookup.py +0 -0
  126. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/box/pages/__init__.py +0 -0
  127. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/box/pages/covers.py +0 -0
  128. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/box/pages/extract.py +0 -0
  129. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/box/print.py +0 -0
  130. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/box/sources.py +0 -0
  131. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/box/types.py +0 -0
  132. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/box/validate/__init__.py +0 -0
  133. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/box/validate/guess_format.py +0 -0
  134. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/cli/__init__.py +0 -0
  135. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/cli/__main__.py +0 -0
  136. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/cli/epilog.py +0 -0
  137. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/cli/parser.py +0 -0
  138. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/config/__init__.py +0 -0
  139. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/config/computed.py +0 -0
  140. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/config/formats.py +0 -0
  141. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/config/online/__init__.py +0 -0
  142. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/config/online/build.py +0 -0
  143. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/config/online/template.py +0 -0
  144. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/config/paths.py +0 -0
  145. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/config/read.py +0 -0
  146. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/config/settings.py +0 -0
  147. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/config_default.yaml +0 -0
  148. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/constants.py +0 -0
  149. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/empty.py +0 -0
  150. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/enums/__init__.py +0 -0
  151. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/enums/comet.py +0 -0
  152. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/enums/comicbookinfo.py +0 -0
  153. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/enums/comicbox.py +0 -0
  154. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/enums/comicinfo.py +0 -0
  155. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/enums/generic/__init__.py +0 -0
  156. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/enums/generic/age_rating.py +0 -0
  157. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/enums/generic/role.py +0 -0
  158. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/enums/maps/__init__.py +0 -0
  159. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/enums/maps/age_rating.py +0 -0
  160. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/enums/maps/formats.py +0 -0
  161. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/enums/maps/identifiers.py +0 -0
  162. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/enums/maps/reading_direction.py +0 -0
  163. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/enums/maps/roles.py +0 -0
  164. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/enums/metroninfo.py +0 -0
  165. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/events.py +0 -0
  166. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/__init__.py +0 -0
  167. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/_base.py +0 -0
  168. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/base/__init__.py +0 -0
  169. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/base/fields/__init__.py +0 -0
  170. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/base/fields/collection_fields.py +0 -0
  171. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/base/fields/comicbox.py +0 -0
  172. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/base/fields/comicinfo.py +0 -0
  173. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/base/fields/enum_fields.py +0 -0
  174. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/base/fields/fields.py +0 -0
  175. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/base/fields/metroninfo.py +0 -0
  176. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/base/fields/number_fields.py +0 -0
  177. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/base/fields/pdf.py +0 -0
  178. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/base/fields/pycountry.py +0 -0
  179. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/base/fields/time_fields.py +0 -0
  180. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/base/fields/union.py +0 -0
  181. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/base/fields/xml_fields.py +0 -0
  182. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/base/online/__init__.py +0 -0
  183. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/base/online/auto_engage.py +0 -0
  184. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/base/online/cli_overrides.py +0 -0
  185. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/base/online/cover_hash.py +0 -0
  186. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/base/online/credentials.py +0 -0
  187. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/base/online/matcher.py +0 -0
  188. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/base/online/prompt.py +0 -0
  189. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/base/online/rate_gate.py +0 -0
  190. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/base/online/sanitize.py +0 -0
  191. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/base/online/selector.py +0 -0
  192. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/base/online/series_cache.py +0 -0
  193. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/base/online/series_filter.py +0 -0
  194. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/base/online/session_state.py +0 -0
  195. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/base/online/signals.py +0 -0
  196. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/base/online/sources/__init__.py +0 -0
  197. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/base/online/sources/base.py +0 -0
  198. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/base/online/transform_helpers.py +0 -0
  199. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/base/online/vacuum.py +0 -0
  200. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/base/online/warn_once.py +0 -0
  201. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/base/schemas/__init__.py +0 -0
  202. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/base/schemas/base.py +0 -0
  203. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/base/schemas/cache.py +0 -0
  204. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/base/schemas/decorators.py +0 -0
  205. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/base/schemas/error_store.py +0 -0
  206. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/base/schemas/json_schemas.py +0 -0
  207. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/base/schemas/xml_schemas.py +0 -0
  208. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/base/schemas/yaml.py +0 -0
  209. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/base/transforms/__init__.py +0 -0
  210. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/base/transforms/base.py +0 -0
  211. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/base/transforms/identifiers.py +0 -0
  212. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/base/transforms/price.py +0 -0
  213. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/base/transforms/publishing_tags.py +0 -0
  214. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/base/transforms/spec.py +0 -0
  215. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/base/transforms/xml_credits.py +0 -0
  216. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/base/transforms/xml_reprints.py +0 -0
  217. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/comet/__init__.py +0 -0
  218. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/comet/schema.py +0 -0
  219. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/comet/transform/__init__.py +0 -0
  220. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/comet/transform/reprints.py +0 -0
  221. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/comic_book_info/__init__.py +0 -0
  222. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/comic_book_info/schema.py +0 -0
  223. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/comic_book_info/transform/__init__.py +0 -0
  224. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/comic_book_info/transform/credits.py +0 -0
  225. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/comic_book_info/transform/rating.py +0 -0
  226. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/comic_info/__init__.py +0 -0
  227. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/comic_info/schema.py +0 -0
  228. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/comic_info/transform/__init__.py +0 -0
  229. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/comic_info/transform/identifiers.py +0 -0
  230. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/comic_info/transform/manga.py +0 -0
  231. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/comic_info/transform/pages.py +0 -0
  232. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/comic_info/transform/storyarcs.py +0 -0
  233. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/comicbox/__init__.py +0 -0
  234. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/comicbox/schema/README.md +0 -0
  235. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/comicbox/schema/__init__.py +0 -0
  236. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/comicbox/schema/cli.py +0 -0
  237. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/comicbox/schema/identifiers.py +0 -0
  238. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/comicbox/schema/json_schema.py +0 -0
  239. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/comicbox/schema/pages.py +0 -0
  240. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/comicbox/schema/publishing.py +0 -0
  241. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/comicbox/schema/yaml.py +0 -0
  242. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/comicbox/transform/__init__.py +0 -0
  243. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/comicbox/transform/cli.py +0 -0
  244. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/comicbox/transform/credits.py +0 -0
  245. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/comicbox/transform/json.py +0 -0
  246. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/comicbox/transform/name_objs.py +0 -0
  247. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/comicbox/transform/yaml.py +0 -0
  248. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/comicvine_api/__init__.py +0 -0
  249. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/comicvine_api/schema.py +0 -0
  250. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/comicvine_api/transform.py +0 -0
  251. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/filename/__init__.py +0 -0
  252. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/filename/schema.py +0 -0
  253. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/filename/transform.py +0 -0
  254. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/metron_api/__init__.py +0 -0
  255. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/metron_api/schema.py +0 -0
  256. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/metron_api/transform.py +0 -0
  257. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/metron_info/__init__.py +0 -0
  258. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/metron_info/schema/__init__.py +0 -0
  259. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/metron_info/schema/credits.py +0 -0
  260. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/metron_info/schema/identifiers.py +0 -0
  261. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/metron_info/schema/price.py +0 -0
  262. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/metron_info/schema/publishing.py +0 -0
  263. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/metron_info/schema/rating.py +0 -0
  264. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/metron_info/schema/resource.py +0 -0
  265. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/metron_info/transform/__init__.py +0 -0
  266. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/metron_info/transform/const.py +0 -0
  267. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/metron_info/transform/credits.py +0 -0
  268. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/metron_info/transform/identified_name.py +0 -0
  269. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/metron_info/transform/identifier_attribute.py +0 -0
  270. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/metron_info/transform/identifiers.py +0 -0
  271. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/metron_info/transform/publishing_tags.py +0 -0
  272. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/metron_info/transform/rating.py +0 -0
  273. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/metron_info/transform/reprints.py +0 -0
  274. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/metron_info/transform/resources.py +0 -0
  275. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/pdf/__init__.py +0 -0
  276. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/pdf/schema.py +0 -0
  277. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/pdf/transform/__init__.py +0 -0
  278. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/pdf/transform/credits.py +0 -0
  279. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/formats/sources.py +0 -0
  280. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/identifiers/__init__.py +0 -0
  281. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/identifiers/identifiers.py +0 -0
  282. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/identifiers/other.py +0 -0
  283. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/identifiers/urns.py +0 -0
  284. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/logger.py +0 -0
  285. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/merge/__init__.py +0 -0
  286. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/merge/mergedeep.py +0 -0
  287. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/online_estimate.py +0 -0
  288. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/print.py +0 -0
  289. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/process.py +0 -0
  290. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/py.typed +0 -0
  291. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/schemas/CoMet-v1.1.xsd +0 -0
  292. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/schemas/ComicInfo-v2.1-Draft.xsd +0 -0
  293. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/schemas/MetronInfo-v1.1.xsd +0 -0
  294. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/schemas/comic-book-info-v1.0.schema.json +0 -0
  295. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/schemas/v1.0/comicbox-v1.0.schema.json +0 -0
  296. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/schemas/v1.0/page.schema.json +0 -0
  297. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/schemas/v1.0/reprint.schema.json +0 -0
  298. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/schemas/v1.0/series.schema.json +0 -0
  299. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/schemas/v1.0/volume.schema.json +0 -0
  300. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/schemas/v2.0/comicbox-v2.0.schema.json +0 -0
  301. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/schemas/v2.0/comicbox.example.yaml +0 -0
  302. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/schemas/v2.0/credit.schema.json +0 -0
  303. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/schemas/v2.0/date.schema.json +0 -0
  304. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/schemas/v2.0/identified-object.schema.json +0 -0
  305. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/schemas/v2.0/identified-objects.schema.json +0 -0
  306. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/schemas/v2.0/identifiers.schema.json +0 -0
  307. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/schemas/v2.0/named-identified-object.schema.json +0 -0
  308. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/schemas/v2.0/page.schema.json +0 -0
  309. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/schemas/v2.0/reprint.schema.json +0 -0
  310. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/schemas/v2.0/series.schema.json +0 -0
  311. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/schemas/v2.0/volume.schema.json +0 -0
  312. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/schemas/v3.0/alternative-name.schema.json +0 -0
  313. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/schemas/v3.0/comicbox-v3.0.schema.json +0 -0
  314. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/schemas/v3.0/comicbox.example.yaml +0 -0
  315. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/schemas/v3.0/credit.schema.json +0 -0
  316. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/schemas/v3.0/date.schema.json +0 -0
  317. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/schemas/v3.0/identified-object.schema.json +0 -0
  318. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/schemas/v3.0/identified-objects.schema.json +0 -0
  319. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/schemas/v3.0/identifiers.schema.json +0 -0
  320. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/schemas/v3.0/named-identified-object.schema.json +0 -0
  321. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/schemas/v3.0/page.schema.json +0 -0
  322. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/schemas/v3.0/reprint.schema.json +0 -0
  323. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/schemas/v3.0/role.schema.json +0 -0
  324. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/schemas/v3.0/series.schema.json +0 -0
  325. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/schemas/v3.0/volume.schema.json +0 -0
  326. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/validate/__init__.py +0 -0
  327. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/validate/base.py +0 -0
  328. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/validate/json_validator.py +0 -0
  329. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/validate/spec.py +0 -0
  330. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/validate/xml_validator.py +0 -0
  331. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/validate/yaml_validator.py +0 -0
  332. {comicbox-5.1.2 → comicbox-5.2.0}/comicbox/version.py +0 -0
  333. {comicbox-5.1.2 → comicbox-5.2.0}/compose.yaml +0 -0
  334. {comicbox-5.1.2 → comicbox-5.2.0}/docs/requirements.txt +0 -0
  335. {comicbox-5.1.2 → comicbox-5.2.0}/docs/style.css +0 -0
  336. {comicbox-5.1.2 → comicbox-5.2.0}/docs/style.mkdocs.css +0 -0
  337. {comicbox-5.1.2 → comicbox-5.2.0}/eslint.config.js +0 -0
  338. {comicbox-5.1.2 → comicbox-5.2.0}/generate-schema/gen-schema.sh +0 -0
  339. {comicbox-5.1.2 → comicbox-5.2.0}/generate-schema/mjs.py +0 -0
  340. {comicbox-5.1.2 → comicbox-5.2.0}/mkdocs.yml +0 -0
  341. {comicbox-5.1.2 → comicbox-5.2.0}/tests/__init__.py +0 -0
  342. {comicbox-5.1.2 → comicbox-5.2.0}/tests/calibration/.gitignore +0 -0
  343. {comicbox-5.1.2 → comicbox-5.2.0}/tests/calibration/README.md +0 -0
  344. {comicbox-5.1.2 → comicbox-5.2.0}/tests/calibration/__init__.py +0 -0
  345. {comicbox-5.1.2 → comicbox-5.2.0}/tests/calibration/_harness_helpers.py +0 -0
  346. {comicbox-5.1.2 → comicbox-5.2.0}/tests/calibration/bootstrap.py +0 -0
  347. {comicbox-5.1.2 → comicbox-5.2.0}/tests/calibration/compare.py +0 -0
  348. {comicbox-5.1.2 → comicbox-5.2.0}/tests/calibration/debug_search.py +0 -0
  349. {comicbox-5.1.2 → comicbox-5.2.0}/tests/calibration/fixtures.example.json +0 -0
  350. {comicbox-5.1.2 → comicbox-5.2.0}/tests/calibration/label_metron.py +0 -0
  351. {comicbox-5.1.2 → comicbox-5.2.0}/tests/calibration/run.py +0 -0
  352. {comicbox-5.1.2 → comicbox-5.2.0}/tests/calibration/sample.py +0 -0
  353. {comicbox-5.1.2 → comicbox-5.2.0}/tests/calibration/summarize.py +0 -0
  354. {comicbox-5.1.2 → comicbox-5.2.0}/tests/calibration/test_bootstrap.py +0 -0
  355. {comicbox-5.1.2 → comicbox-5.2.0}/tests/calibration/test_compare.py +0 -0
  356. {comicbox-5.1.2 → comicbox-5.2.0}/tests/calibration/test_harness.py +0 -0
  357. {comicbox-5.1.2 → comicbox-5.2.0}/tests/calibration/test_harness_checkpoint.py +0 -0
  358. {comicbox-5.1.2 → comicbox-5.2.0}/tests/calibration/test_harness_diagnostics.py +0 -0
  359. {comicbox-5.1.2 → comicbox-5.2.0}/tests/calibration/test_harness_filter.py +0 -0
  360. {comicbox-5.1.2 → comicbox-5.2.0}/tests/calibration/test_harness_resume.py +0 -0
  361. {comicbox-5.1.2 → comicbox-5.2.0}/tests/calibration/test_label_metron.py +0 -0
  362. {comicbox-5.1.2 → comicbox-5.2.0}/tests/calibration/test_sample.py +0 -0
  363. {comicbox-5.1.2 → comicbox-5.2.0}/tests/calibration/test_summarize.py +0 -0
  364. {comicbox-5.1.2 → comicbox-5.2.0}/tests/cli/__init__.py +0 -0
  365. {comicbox-5.1.2 → comicbox-5.2.0}/tests/cli/test_cli_exit_code.py +0 -0
  366. {comicbox-5.1.2 → comicbox-5.2.0}/tests/cli/test_cli_export.py +0 -0
  367. {comicbox-5.1.2 → comicbox-5.2.0}/tests/cli/test_cli_extract.py +0 -0
  368. {comicbox-5.1.2 → comicbox-5.2.0}/tests/cli/test_cli_online.py +0 -0
  369. {comicbox-5.1.2 → comicbox-5.2.0}/tests/cli/test_cli_print.py +0 -0
  370. {comicbox-5.1.2 → comicbox-5.2.0}/tests/cli/test_cli_rename.py +0 -0
  371. {comicbox-5.1.2 → comicbox-5.2.0}/tests/cli/test_cli_write.py +0 -0
  372. {comicbox-5.1.2 → comicbox-5.2.0}/tests/const.py +0 -0
  373. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/Captain Science #001 (1950) The Beginning - multi.cbz +0 -0
  374. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/Captain Science #001 (1950) The Beginning - nothing.cbz +0 -0
  375. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/Captain Science #001-cbi.cbr +0 -0
  376. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/Captain Science #001-cix-cbi.cbr +0 -0
  377. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/Captain Science #001-cix.cbt +0 -0
  378. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/Captain Science #001-cix.cbz +0 -0
  379. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/Captain Science #001-comet.cbz +0 -0
  380. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/Captain Science #001-metron.cbz +0 -0
  381. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/Captain Science #001.cb7 +0 -0
  382. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/Captain Science #001.cbz +0 -0
  383. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/Captain Science 001/CaptainScience#1_01.jpg +0 -0
  384. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/Captain Science 001/CaptainScience#1_02.jpg +0 -0
  385. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/Captain Science 001/CaptainScience#1_03.jpg +0 -0
  386. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/Captain Science 001/CaptainScience#1_04.jpg +0 -0
  387. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/Captain Science 001/CaptainScience#1_05.jpg +0 -0
  388. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/Captain Science 001/CoMet.xml +0 -0
  389. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/Captain Science 001/comic-book-info.json +0 -0
  390. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/Captain Science 001/comicinfo-original.xml +0 -0
  391. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/Captain Science 001/comicinfo.xml +0 -0
  392. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/Captain Science 001/metroninfo-full.xml +0 -0
  393. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/Captain Science 001/metroninfo.xml +0 -0
  394. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/comicbox-cli.cbz +0 -0
  395. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/comicbox.cbz +0 -0
  396. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/create-archive.sh +0 -0
  397. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/create_cs_archives.py +0 -0
  398. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/create_macos_resource_fork.py +0 -0
  399. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/empty.cbz +0 -0
  400. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/examples/comic-book-info-example.json +0 -0
  401. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/examples/metroninfo-v1.1-valid.xml +0 -0
  402. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/export/comet.xml +0 -0
  403. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/export/comic-book-info.json +0 -0
  404. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/export/comicbox-cli.yaml +0 -0
  405. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/export/comicbox.json +0 -0
  406. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/export/comicbox.yaml +0 -0
  407. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/export/comicinfo.xml +0 -0
  408. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/export/create-export-archive.sh +0 -0
  409. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/export/metroninfo.xml +0 -0
  410. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/export/mupdf.json +0 -0
  411. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/export/pdf-metadata.xml +0 -0
  412. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/export.cbz +0 -0
  413. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/list_recurse_output.txt +0 -0
  414. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/macos_resource_fork.cbz +0 -0
  415. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/metadata/comet-cover-path-list.xml +0 -0
  416. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/metadata/comet-write.xml +0 -0
  417. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/metadata/comet.xml +0 -0
  418. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/metadata/comic-book-info-example.json +0 -0
  419. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/metadata/comic-book-info.json +0 -0
  420. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/metadata/comicbox-cli.yaml +0 -0
  421. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/metadata/comicbox-filename.txt +0 -0
  422. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/metadata/comicbox-write.json +0 -0
  423. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/metadata/comicbox-write.yaml +0 -0
  424. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/metadata/comicbox.json +0 -0
  425. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/metadata/comicbox.xml +0 -0
  426. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/metadata/comicbox.yaml +0 -0
  427. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/metadata/comicinfo-bad-multiple.xml +0 -0
  428. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/metadata/comicinfo-cover-path-list.xml +0 -0
  429. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/metadata/comicinfo-ids-from-tags.xml +0 -0
  430. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/metadata/comicinfo-metron-origin.xml +0 -0
  431. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/metadata/comicinfo-notes-date.xml +0 -0
  432. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/metadata/comicinfo-validation.xml +0 -0
  433. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/metadata/comicinfo-write.xml +0 -0
  434. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/metadata/comicinfo.xml +0 -0
  435. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/metadata/create-archives.sh +0 -0
  436. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/metadata/metroninfo-no-primary-id.xml +0 -0
  437. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/metadata/metroninfo-v1.1-valid.xml +0 -0
  438. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/metadata/metroninfo-write.xml +0 -0
  439. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/metadata/metroninfo.xml +0 -0
  440. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/metadata/mupdf-nested.json +0 -0
  441. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/metadata/mupdf.json +0 -0
  442. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/metadata/pdf-cix.xml +0 -0
  443. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/metadata/pdf-nested.xml +0 -0
  444. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/metadata/pdf.xml +0 -0
  445. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/multi/comet.xml +0 -0
  446. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/multi/comic-book-info.json +0 -0
  447. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/multi/comicbox-filename.txt +0 -0
  448. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/multi/comicbox.json +0 -0
  449. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/multi/comicbox.yaml +0 -0
  450. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/multi/comicinfo.xml +0 -0
  451. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/multi/create-multi-archive.sh +0 -0
  452. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/patchme.cbz +0 -0
  453. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/pdf/0.pdf +0 -0
  454. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/pdf/1.pdf +0 -0
  455. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/pdf/2.pdf +0 -0
  456. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/pdf/3.pdf +0 -0
  457. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/test_cix.pdf +0 -0
  458. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/test_pdf.pdf +0 -0
  459. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/test_pdf_legacy_keywords.pdf +0 -0
  460. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/xml.cbz +0 -0
  461. {comicbox-5.1.2 → comicbox-5.2.0}/tests/files/yaml.cbz +0 -0
  462. {comicbox-5.1.2 → comicbox-5.2.0}/tests/schemas/__init__.py +0 -0
  463. {comicbox-5.1.2 → comicbox-5.2.0}/tests/schemas/test_cbi.py +0 -0
  464. {comicbox-5.1.2 → comicbox-5.2.0}/tests/schemas/test_cix.py +0 -0
  465. {comicbox-5.1.2 → comicbox-5.2.0}/tests/schemas/test_comet.py +0 -0
  466. {comicbox-5.1.2 → comicbox-5.2.0}/tests/schemas/test_filename.py +0 -0
  467. {comicbox-5.1.2 → comicbox-5.2.0}/tests/schemas/test_json.py +0 -0
  468. {comicbox-5.1.2 → comicbox-5.2.0}/tests/schemas/test_json_schema_drift.py +0 -0
  469. {comicbox-5.1.2 → comicbox-5.2.0}/tests/schemas/test_metron.py +0 -0
  470. {comicbox-5.1.2 → comicbox-5.2.0}/tests/schemas/test_multi.py +0 -0
  471. {comicbox-5.1.2 → comicbox-5.2.0}/tests/schemas/test_pdf_json.py +0 -0
  472. {comicbox-5.1.2 → comicbox-5.2.0}/tests/schemas/test_pdf_xml.py +0 -0
  473. {comicbox-5.1.2 → comicbox-5.2.0}/tests/schemas/test_validation.py +0 -0
  474. {comicbox-5.1.2 → comicbox-5.2.0}/tests/schemas/test_yaml.py +0 -0
  475. {comicbox-5.1.2 → comicbox-5.2.0}/tests/stress/.gitignore +0 -0
  476. {comicbox-5.1.2 → comicbox-5.2.0}/tests/stress/README.md +0 -0
  477. {comicbox-5.1.2 → comicbox-5.2.0}/tests/stress/__init__.py +0 -0
  478. {comicbox-5.1.2 → comicbox-5.2.0}/tests/stress/jobs_accuracy.py +0 -0
  479. {comicbox-5.1.2 → comicbox-5.2.0}/tests/stress/prompt_ux.py +0 -0
  480. {comicbox-5.1.2 → comicbox-5.2.0}/tests/stress/run.py +0 -0
  481. {comicbox-5.1.2 → comicbox-5.2.0}/tests/stress/test_jobs_accuracy.py +0 -0
  482. {comicbox-5.1.2 → comicbox-5.2.0}/tests/stress/test_prompt_ux.py +0 -0
  483. {comicbox-5.1.2 → comicbox-5.2.0}/tests/stress/test_run.py +0 -0
  484. {comicbox-5.1.2 → comicbox-5.2.0}/tests/test_codex_api.py +0 -0
  485. {comicbox-5.1.2 → comicbox-5.2.0}/tests/test_compute.py +0 -0
  486. {comicbox-5.1.2 → comicbox-5.2.0}/tests/test_convert_pdf.py +0 -0
  487. {comicbox-5.1.2 → comicbox-5.2.0}/tests/test_extract.py +0 -0
  488. {comicbox-5.1.2 → comicbox-5.2.0}/tests/test_import_export.py +0 -0
  489. {comicbox-5.1.2 → comicbox-5.2.0}/tests/test_mtime.py +0 -0
  490. {comicbox-5.1.2 → comicbox-5.2.0}/tests/test_pages.py +0 -0
  491. {comicbox-5.1.2 → comicbox-5.2.0}/tests/test_stories_title.py +0 -0
  492. {comicbox-5.1.2 → comicbox-5.2.0}/tests/test_validate_testfiles.py +0 -0
  493. {comicbox-5.1.2 → comicbox-5.2.0}/tests/test_writing.py +0 -0
  494. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/__init__.py +0 -0
  495. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_archive_errors.py +0 -0
  496. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_archive_read.py +0 -0
  497. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_archiveinfo_mtime.py +0 -0
  498. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_auto_engage.py +0 -0
  499. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_box_cache_invalidation.py +0 -0
  500. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_cbi_rating_normalize.py +0 -0
  501. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_cli_choices_enum_drift.py +0 -0
  502. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_cli_dest_folds.py +0 -0
  503. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_cli_dest_template_drift.py +0 -0
  504. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_comicinfo_alternates.py +0 -0
  505. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_comicinfo_series_groups.py +0 -0
  506. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_comicvine.py +0 -0
  507. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_computed_pipeline.py +0 -0
  508. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_computed_reprints.py +0 -0
  509. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_config_container_inputs.py +0 -0
  510. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_config_defaults_drift.py +0 -0
  511. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_config_env_tree.py +0 -0
  512. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_config_file_option.py +0 -0
  513. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_config_layering.py +0 -0
  514. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_config_no_path_guard.py +0 -0
  515. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_config_online_leaf.py +0 -0
  516. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_config_online_sources.py +0 -0
  517. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_cover_hash.py +0 -0
  518. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_credit_primary.py +0 -0
  519. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_defer_prompts.py +0 -0
  520. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_error_store_visibility.py +0 -0
  521. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_exceptions.py +0 -0
  522. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_identifiers.py +0 -0
  523. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_lazy_startup.py +0 -0
  524. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_library_logging.py +0 -0
  525. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_list_field_dedupe.py +0 -0
  526. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_list_field_sort_key_types.py +0 -0
  527. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_manga_and_age_rating.py +0 -0
  528. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_manga_volume.py +0 -0
  529. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_merge_mode_config.py +0 -0
  530. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_merge_order.py +0 -0
  531. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_merge_precedence.py +0 -0
  532. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_mergedeep.py +0 -0
  533. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_metron_community_rating.py +0 -0
  534. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_metron_id_attribute_fallback.py +0 -0
  535. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_metron_prefetch.py +0 -0
  536. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_metron_transform.py +0 -0
  537. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_online.py +0 -0
  538. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_online_abort_propagation.py +0 -0
  539. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_online_degradation.py +0 -0
  540. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_online_estimate.py +0 -0
  541. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_online_events.py +0 -0
  542. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_online_lookup.py +0 -0
  543. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_online_matcher.py +0 -0
  544. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_online_matcher_resolution.py +0 -0
  545. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_online_matcher_scoring.py +0 -0
  546. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_online_prompt.py +0 -0
  547. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_online_prompt_loops.py +0 -0
  548. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_online_result_contract.py +0 -0
  549. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_online_retry.py +0 -0
  550. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_online_selector.py +0 -0
  551. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_online_session.py +0 -0
  552. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_online_session_state.py +0 -0
  553. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_parse_decimal.py +0 -0
  554. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_parse_issue.py +0 -0
  555. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_pdf_pages_help.py +0 -0
  556. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_process.py +0 -0
  557. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_prompt_dedup.py +0 -0
  558. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_quota_reserved_skip.py +0 -0
  559. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_rar_time_overflow.py +0 -0
  560. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_rate_gate.py +0 -0
  561. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_read_events.py +0 -0
  562. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_refresh_cache.py +0 -0
  563. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_reprints_and_alternative_names.py +0 -0
  564. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_rich_transforms.py +0 -0
  565. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_role_vocabulary.py +0 -0
  566. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_run_parallel.py +0 -0
  567. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_run_recurse.py +0 -0
  568. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_series_cache.py +0 -0
  569. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_series_filter.py +0 -0
  570. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_series_single_flight.py +0 -0
  571. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_session_series_batching.py +0 -0
  572. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_single_valued_tags.py +0 -0
  573. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_sniff.py +0 -0
  574. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_sources.py +0 -0
  575. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_to_metron_age_rating.py +0 -0
  576. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_transform_spec_dest_collisions.py +0 -0
  577. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_transform_spec_missing_keys.py +0 -0
  578. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_transform_spec_scope_phases.py +0 -0
  579. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_urls_and_identifiers.py +0 -0
  580. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_vacuum.py +0 -0
  581. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_validate.py +0 -0
  582. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_xml_cdata_fields.py +0 -0
  583. {comicbox-5.1.2 → comicbox-5.2.0}/tests/unit/test_yes_no_field.py +0 -0
  584. {comicbox-5.1.2 → comicbox-5.2.0}/tests/util/__init__.py +0 -0
  585. {comicbox-5.1.2 → comicbox-5.2.0}/tests/util/compare.py +0 -0
  586. {comicbox-5.1.2 → comicbox-5.2.0}/tests/util/diff.py +0 -0
  587. {comicbox-5.1.2 → comicbox-5.2.0}/tests/util/metadata.py +0 -0
  588. {comicbox-5.1.2 → comicbox-5.2.0}/tests/util/online_client.py +0 -0
  589. {comicbox-5.1.2 → comicbox-5.2.0}/tests/util/online_matcher.py +0 -0
  590. {comicbox-5.1.2 → comicbox-5.2.0}/tests/util/online_prompt.py +0 -0
  591. {comicbox-5.1.2 → comicbox-5.2.0}/tests/util/parser.py +0 -0
  592. {comicbox-5.1.2 → comicbox-5.2.0}/tests/util/tmp.py +0 -0
  593. {comicbox-5.1.2 → comicbox-5.2.0}/tests/validate_cli.py +0 -0
  594. {comicbox-5.1.2 → comicbox-5.2.0}/validate-schema.sh +0 -0
  595. {comicbox-5.1.2 → comicbox-5.2.0}/vulture_ignorelist.py +0 -0
@@ -113,6 +113,7 @@ share/python-wheels
113
113
  share/python-wheels/
114
114
  target
115
115
  target/
116
+ tasks/
116
117
  test-results
117
118
  TODO.md
118
119
  var
@@ -1,5 +1,33 @@
1
1
  # 📰 News
2
2
 
3
+ ## v5.2.0
4
+
5
+ - Features
6
+ - `comicbox.predict.predict_write_destination()` reports where a write would
7
+ land, and whether that path is taken, before anything is written.
8
+ - A destination collision raises `DestinationOccupiedError`, which names the
9
+ occupied path and says which kind of collision it was.
10
+ - Match candidates carry a full-size cover url alongside the thumbnail.
11
+ - `OnlineSession.close()` releases the pooled Metron connections; the CLI
12
+ does this at the end of every run.
13
+
14
+ - Fixes
15
+ - `bulk_write` checks every destination before doing any work, so a batch
16
+ fails fast instead of repacking an archive and refusing it at the end.
17
+ `preflight=False` restores the old behavior.
18
+ - Two archives in one batch that convert to the same name now lose
19
+ deterministically, by submission order, instead of by thread race.
20
+ - A kept-original CBR whose CBZ twin already exists is refused before its
21
+ metadata is read, on the command line as well as the API.
22
+
23
+ - Performance
24
+ - Metron requests reuse one pooled connection per run instead of a new TLS
25
+ handshake each (mokkari 4.8.0).
26
+
27
+ - Dependencies
28
+ - Requires mokkari 4.8.0. Metron pacing now registers through mokkari's
29
+ `rate_limiter` hook instead of overriding a private method.
30
+
3
31
  ## v5.1.2
4
32
 
5
33
  - Features
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: comicbox
3
- Version: 5.1.2
3
+ Version: 5.2.0
4
4
  Summary: Comic book archive multi format metadata read/write/transform tool and image extractor.
5
5
  Keywords: cb7,cbr,cbt,cbz,comet,comic,comicbookinfo,comicinfo,metroninfo,pdf
6
6
  Author: AJ Slater
@@ -23,7 +23,7 @@ Requires-Dist: keyring~=25.6
23
23
  Requires-Dist: loguru~=0.7
24
24
  Requires-Dist: marshmallow-union>=0.1.15.post1
25
25
  Requires-Dist: marshmallow~=4.3
26
- Requires-Dist: mokkari>=4.5.0,<5
26
+ Requires-Dist: mokkari>=4.8.0,<5
27
27
  Requires-Dist: nh3>=0.3.5
28
28
  Requires-Dist: pillow>=12.2.0
29
29
  Requires-Dist: platformdirs>=4.9.6
@@ -314,12 +314,23 @@ result = write_metadata(
314
314
  print(result.written)
315
315
  ```
316
316
 
317
+ `comicbox.predict.predict_write_destination` reports where a write would land —
318
+ in place, or the CBZ an unwritable archive repacks to — and whether that path is
319
+ already taken, without opening the archive or reading any metadata:
320
+
321
+ ```python
322
+ from comicbox.predict import predict_write_destination
323
+
324
+ dest = predict_write_destination("comic.cbr")
325
+ print(dest.destination, dest.converts, dest.occupied)
326
+ ```
327
+
317
328
  Every operational error these APIs raise derives from
318
329
  `comicbox.exceptions.ComicboxError` — `ArchiveError`, `ArchiveWriteError`,
319
- `MetadataError`, `ExportError`, `WriteValidationError`,
320
- `OnlineConfigurationError`, `OnlineLookupAbortedError`, and
321
- `UnsupportedArchiveTypeError` — so consumers can `except ComicboxError` without
322
- swallowing unrelated programming errors.
330
+ `DestinationOccupiedError`, `MetadataError`, `ExportError`,
331
+ `WriteValidationError`, `OnlineConfigurationError`, `OnlineLookupAbortedError`,
332
+ and `UnsupportedArchiveTypeError` — so consumers can `except ComicboxError`
333
+ without swallowing unrelated programming errors.
323
334
 
324
335
  ## ⚙️ Configuration
325
336
 
@@ -262,12 +262,23 @@ result = write_metadata(
262
262
  print(result.written)
263
263
  ```
264
264
 
265
+ `comicbox.predict.predict_write_destination` reports where a write would land —
266
+ in place, or the CBZ an unwritable archive repacks to — and whether that path is
267
+ already taken, without opening the archive or reading any metadata:
268
+
269
+ ```python
270
+ from comicbox.predict import predict_write_destination
271
+
272
+ dest = predict_write_destination("comic.cbr")
273
+ print(dest.destination, dest.converts, dest.occupied)
274
+ ```
275
+
265
276
  Every operational error these APIs raise derives from
266
277
  `comicbox.exceptions.ComicboxError` — `ArchiveError`, `ArchiveWriteError`,
267
- `MetadataError`, `ExportError`, `WriteValidationError`,
268
- `OnlineConfigurationError`, `OnlineLookupAbortedError`, and
269
- `UnsupportedArchiveTypeError` — so consumers can `except ComicboxError` without
270
- swallowing unrelated programming errors.
278
+ `DestinationOccupiedError`, `MetadataError`, `ExportError`,
279
+ `WriteValidationError`, `OnlineConfigurationError`, `OnlineLookupAbortedError`,
280
+ and `UnsupportedArchiveTypeError` — so consumers can `except ComicboxError`
281
+ without swallowing unrelated programming errors.
271
282
 
272
283
  ## ⚙️ Configuration
273
284
 
@@ -10,7 +10,7 @@
10
10
  "@fsouza/prettierd": "^0.29.0",
11
11
  "@prettier/plugin-xml": "^3.4.2",
12
12
  "@stylistic/eslint-plugin": "^5.10.0",
13
- "eslint": "^10.10.0",
13
+ "eslint": "^10.11.0",
14
14
  "eslint-config-prettier": "^10.1.8",
15
15
  "eslint-plugin-array-func": "^5.1.1",
16
16
  "eslint-plugin-compat": "^7.0.2",
@@ -23,11 +23,11 @@
23
23
  "eslint-plugin-no-secrets": "^2.3.3",
24
24
  "eslint-plugin-no-unsanitized": "^4.1.5",
25
25
  "eslint-plugin-no-use-extend-native": "^0.7.3",
26
- "eslint-plugin-package-json": "^1.8.1",
26
+ "eslint-plugin-package-json": "^1.9.0",
27
27
  "eslint-plugin-perfectionist": "^5.11.1",
28
28
  "eslint-plugin-prettier": "^5.5.6",
29
29
  "eslint-plugin-promise": "^7.3.0",
30
- "eslint-plugin-regexp": "^3.3.0",
30
+ "eslint-plugin-regexp": "^3.3.1",
31
31
  "eslint-plugin-security": "^4.0.1",
32
32
  "eslint-plugin-sonarjs": "^4.2.1",
33
33
  "eslint-plugin-toml": "^1.5.0",
@@ -52,13 +52,13 @@
52
52
 
53
53
  "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
54
54
 
55
- "@babel/helper-string-parser": ["@babel/helper-string-parser@8.0.0", "", {}, "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg=="],
55
+ "@babel/helper-string-parser": ["@babel/helper-string-parser@8.0.6", "", {}, "sha512-+6HYEBnsDRZHJtUTAEfMQd0zMDqdFHS7C+5vAsEZqBUr3vEMWOWlHJ0Jz1gS8jJIlCvLsuA6ivmeoqzH936YcA=="],
56
56
 
57
- "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@8.0.4", "", {}, "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg=="],
57
+ "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@8.0.6", "", {}, "sha512-HYx0YmuchRxRSpnjvcNffnPS8DCauKNmnumIuHG3jnQAVewRA7kfJGwF13ayEpSZs7s4R/oBH4eppuXcAQd5JQ=="],
58
58
 
59
- "@babel/parser": ["@babel/parser@8.0.5", "", { "dependencies": { "@babel/types": "^8.0.5" }, "bin": "./bin/babel-parser.js" }, "sha512-51RXvQNFakaS0bTpYiGkxNbUVwkPO4kONv6EVLorZABxsx+KZ6Z7uSYvi/wmKS/+X+rfj9RvOw0/ZNh+cmI0Rw=="],
59
+ "@babel/parser": ["@babel/parser@8.0.6", "", { "dependencies": { "@babel/types": "^8.0.6" }, "bin": "./bin/babel-parser.js" }, "sha512-LpGDIYJAzc3Y3PT9x+FxBrGYu2PlWxLmyN7SwPTzaX0LSwEmFJBsRnPBqmaXF2r1v+Zhz6lNiEJ+7yKtM5+Ugg=="],
60
60
 
61
- "@babel/types": ["@babel/types@8.0.5", "", { "dependencies": { "@babel/helper-string-parser": "^8.0.0", "@babel/helper-validator-identifier": "^8.0.4" } }, "sha512-eVdMqi3ej5aHhyQ2Si6yD2cAWeV8FJK9UrhK5aL0Sd8hu5GhT+YswhVNbVheOGVYMg8kuGuMaUpkB3stjj4z8A=="],
61
+ "@babel/types": ["@babel/types@8.0.6", "", { "dependencies": { "@babel/helper-string-parser": "^8.0.6", "@babel/helper-validator-identifier": "^8.0.6" } }, "sha512-c+xgSWboV2pdwXP67BUWDVRHi2BO5Q3KwxyFVs7taVTY+fsK47/L51ZXQoV9rozHJDNHvJ4v4WEsI/IlxV2l2A=="],
62
62
 
63
63
  "@cacheable/memory": ["@cacheable/memory@2.2.0", "", { "dependencies": { "@cacheable/utils": "^2.5.0", "@keyv/bigmap": "^1.3.1", "hookified": "^1.15.1", "keyv": "^5.6.0" } }, "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ=="],
64
64
 
@@ -166,7 +166,7 @@
166
166
 
167
167
  "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
168
168
 
169
- "@types/node": ["@types/node@26.6.1", "", { "dependencies": { "undici-types": "~8.9.0" } }, "sha512-VqGJBMCtdhqkBUCcBLvywI0NJ+KLuVzgNnlBUNFOQjqVxzo2lxLUNg1DSey8+u2u6ktswSAxg+s68QLzWHNOuA=="],
169
+ "@types/node": ["@types/node@26.6.2", "", { "dependencies": { "undici-types": "~8.9.0" } }, "sha512-X1P21scMv4zGKLYqjdGjaKa7COa0RKVYYZZN/NfvLQ1JegxFhdhpZG/Lyn8AXx6CDUavKAd11v6BvfpkDByK8g=="],
170
170
 
171
171
  "@types/supports-color": ["@types/supports-color@8.1.3", "", {}, "sha512-Hy6UMpxhE3j1tLpl27exp1XqHD7n8chAiNPzWfz16LPZoMMoSc4dzLl6w9qijkEb/r5O1ozdu1CWGA2L83ZeZg=="],
172
172
 
@@ -338,11 +338,11 @@
338
338
 
339
339
  "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
340
340
 
341
- "electron-to-chromium": ["electron-to-chromium@1.5.430", "", {}, "sha512-e1QEj72Y4zd8RlNZVmoTg+iCOSVwpk05IOiiQwdrkwCSVlZfPthevErhE+nckGd2YbsXfp1SkisznhGVIXP2NQ=="],
341
+ "electron-to-chromium": ["electron-to-chromium@1.5.433", "", {}, "sha512-5lCAbyZBjtmUt/RAGHRqrL2q0oEFRThDAsZHHDn9XHa89Qw7gMYOeSicBTy+AHfvo0r6vwsZvqNJTQIQy1BLzA=="],
342
342
 
343
343
  "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
344
344
 
345
- "empathic": ["empathic@2.0.1", "", {}, "sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q=="],
345
+ "empathic": ["empathic@2.1.0", "", {}, "sha512-AnfC1ATldl49/cvZdLPDjBfrRNwbDO05aibiOtzQu3qtlbJtomNLhF30HEtn/7iBz50dlMECqATo3fG0LrdEgw=="],
346
346
 
347
347
  "entities": ["entities@8.1.0", "", {}, "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA=="],
348
348
 
@@ -354,11 +354,11 @@
354
354
 
355
355
  "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
356
356
 
357
- "eslint": ["eslint@10.10.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.7.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.3", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "11.1.5 || >11.1.6 <12", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-NPXn6r5zl4uET1DAVPaOwzX3rut4c0wcmw3dWJAfOsTM5+TogXo0DDjz8pwm/hL8cyVNpHqeK4JpN0NjnyFFNw=="],
357
+ "eslint": ["eslint@10.11.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.7.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.3", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "11.1.5 || >11.1.6 <12", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-P7a6UEEqb9G95MYAtqkmsTbVXIYyzIfl6NGOIJk162PaahFxFyeGcrlXYFSiagECg4sEm8IseJdZBKR3rx6MsQ=="],
358
358
 
359
359
  "eslint-config-prettier": ["eslint-config-prettier@10.1.8", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": "bin/cli.js" }, "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w=="],
360
360
 
361
- "eslint-fix-utils": ["eslint-fix-utils@0.4.3", "", { "peerDependencies": { "@types/estree": ">=1", "eslint": ">=8" }, "optionalPeers": ["@types/estree"] }, "sha512-EKzNhsNavV5DdkHVltHBM9TqfsonCmiUhOm1Ra97zo03M9IAx3wtM1eC27eWGMj/D3LrALmHHNe1QlQH1P7Nxw=="],
361
+ "eslint-fix-utils": ["eslint-fix-utils@0.4.4", "", { "peerDependencies": { "@types/estree": ">=1", "eslint": ">=8" }, "optionalPeers": ["@types/estree"] }, "sha512-9reP0fw++eZv0qs0ji8U1G9a+j8yWQW1RT8Wrlw32JlhQJDiweVX98kyDrJq6G7jdOLgKwmj0nPghyMadsOgHQ=="],
362
362
 
363
363
  "eslint-import-context": ["eslint-import-context@0.1.9", "", { "dependencies": { "get-tsconfig": "^4.10.1", "stable-hash-x": "^0.2.0" }, "peerDependencies": { "unrs-resolver": "^1.0.0" } }, "sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg=="],
364
364
 
@@ -388,7 +388,7 @@
388
388
 
389
389
  "eslint-plugin-no-use-extend-native": ["eslint-plugin-no-use-extend-native@0.7.3", "", { "dependencies": { "is-get-set-prop": "^2.0.0", "is-js-type": "^3.0.0", "is-obj-prop": "^2.0.0", "is-proto-prop": "^3.0.1" }, "peerDependencies": { "eslint": "^9.3.0 || ^10.0.0" } }, "sha512-kYJhgZkiZIavu/wIwrO+n4GemQcMX53kWCNZNr7nGMkRD1aBFLkDpBivEYP7nIJINCo9fzPbFjrpeX5kr2Qbww=="],
390
390
 
391
- "eslint-plugin-package-json": ["eslint-plugin-package-json@1.8.1", "", { "dependencies": { "@altano/repository-tools": "^2.0.1", "@types/estree": "^1.0.0", "change-case": "^5.4.4", "detect-indent": "^7.0.2", "detect-newline": "^4.0.1", "eslint-fix-utils": "~0.4.3", "eslint-json-compat-utils": "^0.2.3", "jsonc-eslint-parser": "^3.1.0", "package-json-validator": "^1.6.0", "semver": "^7.7.3", "sort-object-keys": "^2.0.0", "sort-package-json": "^4.0.0" }, "peerDependencies": { "@eslint/json": ">=1.0.0", "eslint": ">=9.0.0" }, "optionalPeers": ["@eslint/json"] }, "sha512-/Adb7EHfwIM2t13zdBuB6IjIsNSDkbPT+T9ZlIK0N6aAnj9hiekqFlFbUpEIsiQX4Zv7bASBDP2C3hu362e2kw=="],
391
+ "eslint-plugin-package-json": ["eslint-plugin-package-json@1.9.0", "", { "dependencies": { "@altano/repository-tools": "^2.0.1", "@types/estree": "^1.0.0", "change-case": "^5.4.4", "detect-indent": "^7.0.2", "detect-newline": "^4.0.1", "eslint-fix-utils": "~0.4.3", "eslint-json-compat-utils": "^0.2.3", "jsonc-eslint-parser": "^3.1.0", "package-json-validator": "^1.6.0", "semver": "^7.7.3", "sort-object-keys": "^2.0.0", "sort-package-json": "^4.0.0" }, "peerDependencies": { "@eslint/json": ">=1.0.0", "eslint": ">=9.0.0" }, "optionalPeers": ["@eslint/json"] }, "sha512-Q18c0/YIAYhtlLoBzqA6qNO2dJmNHARlIARPROH9MZ/l0B1u/JafEtc0XNcuTXgYdvFUrhv649Z3sd5T4mO9ww=="],
392
392
 
393
393
  "eslint-plugin-perfectionist": ["eslint-plugin-perfectionist@5.11.1", "", { "dependencies": { "@typescript-eslint/utils": "^8.70.0", "natural-orderby": "^5.0.0" }, "peerDependencies": { "eslint": "^8.45.0 || ^9.0.0 || ^10.0.0" } }, "sha512-xloHScnOpin9Jy3CegfzoBeY33ASk0spLyM2sJs0LyNHQjv9m0osUD2PYNT7NrJieZbUR2Ebn4A5Q0+0dOcv+A=="],
394
394
 
@@ -396,7 +396,7 @@
396
396
 
397
397
  "eslint-plugin-promise": ["eslint-plugin-promise@7.3.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0" }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0" } }, "sha512-6uGiOR0INuujr6PEQmeSSP7GbIMJ/ebEXXiEzb/nOj68LknH5Pxzb/AbZivmr6VE6TkTE8rTjRK9zhKpK6HsRA=="],
398
398
 
399
- "eslint-plugin-regexp": ["eslint-plugin-regexp@3.3.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.11.0", "comment-parser": "^1.4.0", "jsdoc-type-pratt-parser": "^9.0.0", "refa": "^0.12.1", "regexp-ast-analysis": "^0.7.1", "scslre": "^0.3.0" }, "peerDependencies": { "eslint": ">=9.38.0" } }, "sha512-TT0JTQbW7CRKohntpGAsMdQ1K3MsmJK4gdAhKJtIwlJ0JvuYxauymFgJdvqxIY4lqNj0EuRZWVrymQtEdJYEFg=="],
399
+ "eslint-plugin-regexp": ["eslint-plugin-regexp@3.3.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.11.0", "comment-parser": "^1.4.0", "jsdoc-type-pratt-parser": "^9.0.0", "refa": "^0.12.1", "regexp-ast-analysis": "^0.7.1", "scslre": "^0.3.0" }, "peerDependencies": { "eslint": ">=9.38.0" } }, "sha512-58bGUci/AaWAo1ETj6Lsnmy6QXwnSyVONRA805828nnqp7vrTwdaQMc02vtowmYB3A5OB3SLr3r5HgmfxX4dLg=="],
400
400
 
401
401
  "eslint-plugin-security": ["eslint-plugin-security@4.0.1", "", { "dependencies": { "safe-regex": "^2.1.1" } }, "sha512-/lZCkOxPOWaf1jXAqgICrS8St3BMBccIPvhOSUYuV6VCr1o5nFVG998FnTLt6w2Nxb8Uo0nM8fzmnhp+GY/aEg=="],
402
402
 
@@ -404,7 +404,7 @@
404
404
 
405
405
  "eslint-plugin-toml": ["eslint-plugin-toml@1.5.0", "", { "dependencies": { "@eslint/core": "^1.0.1", "@eslint/plugin-kit": "^0.7.0", "@ota-meshi/ast-token-store": "^0.3.0", "debug": "^4.1.1", "toml-eslint-parser": "^1.0.1" }, "peerDependencies": { "eslint": ">=9.38.0" } }, "sha512-qBjRywEkKxO2uYOjus//6GVF1r+Hg5QDkRO8RTY6XcaXgWfBU0DhwpFmJa2Ljf0Sz49r7DdZlpKwwHmJ4nmH1Q=="],
406
406
 
407
- "eslint-plugin-unicorn": ["eslint-plugin-unicorn@75.0.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.10.1", "@eslint/css-tree": "^4.0.5", "browserslist": "^4.28.8", "change-case": "^5.4.4", "ci-info": "^4.4.0", "core-js-compat": "^3.50.0", "detect-indent": "^7.0.2", "entities": "^8.0.0", "find-up-simple": "^1.0.1", "globals": "^17.11.0", "identifier-regex": "^1.1.0", "indent-string": "^5.0.0", "is-builtin-module": "^5.0.0", "pluralize": "^8.0.0", "quote-js-string": "^0.1.0", "regjsparser": "^0.13.2", "reserved-identifiers": "^1.2.0", "semver": "^7.8.5", "strip-indent": "^4.1.1", "yaml": "^2.9.0" }, "peerDependencies": { "eslint": ">=10.4" } }, "sha512-72I7eSn+/z5CpgValFBl9mp2JE5UPD+nUUyB/VIGUMF1rCP1i37mA05zfIAVIU312A+qCZ38T8tSZr3oaMv4Mw=="],
407
+ "eslint-plugin-unicorn": ["eslint-plugin-unicorn@76.0.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.10.1", "@eslint/css-tree": "^4.0.5", "browserslist": "^4.28.8", "change-case": "^5.4.4", "ci-info": "^4.4.0", "core-js-compat": "^3.50.0", "detect-indent": "^7.0.2", "entities": "^8.0.0", "find-up-simple": "^1.0.1", "globals": "^17.11.0", "identifier-regex": "^1.1.0", "indent-string": "^5.0.0", "is-builtin-module": "^5.0.0", "pluralize": "^8.0.0", "quote-js-string": "^0.1.0", "regjsparser": "^0.13.2", "reserved-identifiers": "^1.2.0", "semver": "^7.8.5", "strip-indent": "^4.1.1", "yaml": "^2.9.0" }, "peerDependencies": { "eslint": ">=10.4" } }, "sha512-3ywJrCMHKYhcBP5yFzGaOOX3dY7+iR3hvpuoS/0EnhRFgVWLejly+t4fOdJvWQxJdHZl1C+QBW7hybXPDld4Pw=="],
408
408
 
409
409
  "eslint-plugin-yml": ["eslint-plugin-yml@3.8.1", "", { "dependencies": { "@eslint/core": "^1.0.1", "@eslint/plugin-kit": "^0.7.0", "@ota-meshi/ast-token-store": "^0.3.0", "diff-sequences": "^29.0.0", "escape-string-regexp": "5.0.0", "natural-compare": "^1.4.0", "yaml-eslint-parser": "^2.0.0" }, "peerDependencies": { "eslint": ">=9.38.0" } }, "sha512-E/70psRwxz5EJ8dBtzrFfqSiiInuSytbdtFCjueb/GcKjsWzPBfxfhtQePImMAPjHwMA/VDurO7DJwStDJ4wWg=="],
410
410
 
@@ -572,7 +572,7 @@
572
572
 
573
573
  "lowercase-keys": ["lowercase-keys@3.0.0", "", {}, "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ=="],
574
574
 
575
- "lru-cache": ["lru-cache@11.5.2", "", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="],
575
+ "lru-cache": ["lru-cache@11.5.3", "", {}, "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg=="],
576
576
 
577
577
  "markdown-extensions": ["markdown-extensions@2.0.0", "", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="],
578
578
 
@@ -788,7 +788,7 @@
788
788
 
789
789
  "regexp-tree": ["regexp-tree@0.1.27", "", { "bin": "bin/regexp-tree" }, "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA=="],
790
790
 
791
- "regjsparser": ["regjsparser@0.13.2", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NgRBy2Nx/bE+9F27nVHnqcN5HjyLmecqsqx2PJHu3/IEtADD4WuxuXIVExD5PoSDFVrl78dOonfcOe5O+5nbzQ=="],
791
+ "regjsparser": ["regjsparser@0.13.3", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-ycwFAS14Jw4mppvmK4GR/J6u3WpWpjkEApehuHtLc/8VpPNpDMbQ4WjqwplXifGeyKOzHSFLmSPqzksDQE2Sfg=="],
792
792
 
793
793
  "remark": ["remark@15.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A=="],
794
794
 
@@ -1140,7 +1140,7 @@
1140
1140
 
1141
1141
  "toml-eslint-parser/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
1142
1142
 
1143
- "unified-engine/@types/node": ["@types/node@22.20.3", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-DZmzkmwHzXrLPAXPyKNDzlIwMMUZCVacoD25ywdy5YTKGbOx/2ld+Q38Im2zJ0vBuZP5Prd3VZutKZyXwkOS8A=="],
1143
+ "unified-engine/@types/node": ["@types/node@22.20.4", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-zJRE40jpHtKqE/C4fgHrAKQLJuSpzEnP9ff9Y7YtoR3Wd2pwqzlekDeEuUQXjRd+QCYnVnNwuJYmhdk9XV8gvA=="],
1144
1144
 
1145
1145
  "unified-engine/ignore": ["ignore@6.0.2", "", {}, "sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A=="],
1146
1146
 
@@ -10,7 +10,7 @@ from zipremove import ZIP_DEFLATED, ZIP_STORED, ZipFile
10
10
  from comicbox._pdf import PAGE_FORMAT_PIXMAP_JPEG
11
11
  from comicbox.box.archive.archiveinfo import ArchiveInfo, InfoType
12
12
  from comicbox.box.archive.read import ComicboxArchiveRead
13
- from comicbox.exceptions import ArchiveWriteError
13
+ from comicbox.exceptions import ArchiveWriteError, DestinationOccupiedError
14
14
  from comicbox.formats.sources import MetadataSources
15
15
 
16
16
  _RECOMPRESS_SUFFIX = ".comicbox_tmp_zip"
@@ -26,13 +26,12 @@ _INFLIGHT_DESTINATIONS: set[Path] = set()
26
26
  _INFLIGHT_GUARD = Lock()
27
27
 
28
28
 
29
- def _claim_destination(path: Path) -> None:
29
+ def _claim_destination(destination: Path, source: Path) -> None:
30
30
  """Take a conversion destination, refusing one already in flight."""
31
31
  with _INFLIGHT_GUARD:
32
- if path in _INFLIGHT_DESTINATIONS:
33
- reason = f"{path} is already being written by another archive."
34
- raise ArchiveWriteError(reason)
35
- _INFLIGHT_DESTINATIONS.add(path)
32
+ if destination in _INFLIGHT_DESTINATIONS:
33
+ raise DestinationOccupiedError(source, destination, "inflight")
34
+ _INFLIGHT_DESTINATIONS.add(destination)
36
35
 
37
36
 
38
37
  def _release_destination(path: Path) -> None:
@@ -53,15 +52,36 @@ _ALL_ARCHIVE_METADATA_FILENAMES = frozenset(
53
52
  class ComicboxArchiveWrite(ComicboxArchiveRead):
54
53
  """Comicboxs methods for writing to the archive."""
55
54
 
56
- def _get_new_archive_path(self) -> Path:
55
+ def get_write_destination(self) -> Path:
56
+ """
57
+ Where a write of this archive lands: in place, or the CBZ it repacks to.
58
+
59
+ Decided from the sniffed archive type (``_archive_cls`` /
60
+ ``_archive_is_pdf``) and ``convert.cbz`` -- never from the suffix. A
61
+ zip is rewritten in place whatever it is called; a PDF is updated in
62
+ place unless ``convert.cbz`` asks for a repack; everything else
63
+ (CBR/CBT/CB7) is unwritable and repacks to ``<stem>.cbz``.
64
+ """
57
65
  if not self._path:
58
- reason = "Cannot write zipfile metadata without a path."
59
- raise ArchiveWriteError(reason)
60
- new_path = self._path.with_suffix(_CBZ_SUFFIX)
61
- if new_path.is_file() and new_path != self._path:
62
- reason = f"{new_path} already exists."
66
+ reason = "Cannot write archive metadata without a path."
63
67
  raise ArchiveWriteError(reason)
64
- return new_path
68
+ is_zip = self._archive_cls == ZipFile
69
+ is_pdf_in_place = self._archive_is_pdf and not self._config.convert.cbz
70
+ if is_zip or is_pdf_in_place:
71
+ return self._path
72
+ return self._path.with_suffix(_CBZ_SUFFIX)
73
+
74
+ def _get_new_archive_path(self) -> Path:
75
+ """Return the conversion destination, refusing one already on disk."""
76
+ destination = self.get_write_destination()
77
+ if destination != self._path and destination.is_file():
78
+ # self._path is non-None: get_write_destination raised otherwise.
79
+ raise DestinationOccupiedError(
80
+ self._path, # pyright: ignore[reportArgumentType], # ty: ignore[invalid-argument-type]
81
+ destination,
82
+ "convert",
83
+ )
84
+ return destination
65
85
 
66
86
  def _cleanup_tmp_archive(self, tmp_path: Path, new_path: Path) -> None:
67
87
  if not self._archive_cls or not self._path:
@@ -234,26 +254,20 @@ class ComicboxArchiveWrite(ComicboxArchiveRead):
234
254
 
235
255
  Every write claims its destination, not just the conversions. An
236
256
  in-place rewrite is as destructive as a conversion when two writers
237
- overlap on it -- ``bulk_write`` does not deduplicate paths, so one
238
- archive named twice in a batch used to be repacked by both threads
239
- at once -- and the finished-file check cannot see a write that is
240
- still in flight.
257
+ overlap on it -- ``bulk_write``'s preflight refuses a path named
258
+ twice; the claim is the backstop for concurrent callers -- and the
259
+ finished-file check cannot see a write that is still in flight.
241
260
  """
242
261
  if not self._path:
243
262
  reason = "Cannot write archive metadata without a path."
244
263
  raise ArchiveWriteError(reason)
264
+ destination = self.get_write_destination()
245
265
  is_zip = self._archive_cls == ZipFile
246
- is_pdf_in_place = self._archive_is_pdf and not self._config.convert.cbz
247
- destination = (
248
- self._path
249
- if is_zip or is_pdf_in_place
250
- else self._path.with_suffix(_CBZ_SUFFIX)
251
- )
252
- _claim_destination(destination)
266
+ _claim_destination(destination, self._path)
253
267
  try:
254
268
  if is_zip:
255
269
  self._patch_zipfile(files, comment)
256
- elif is_pdf_in_place:
270
+ elif destination == self._path:
257
271
  self._update_pdffile(files, mupdf_metadata)
258
272
  else:
259
273
  self._create_zipfile(files, comment)
@@ -146,16 +146,31 @@ class ComicboxDump(ComicboxPages):
146
146
  }
147
147
  self._reset_archive(None, None, keep_sources)
148
148
 
149
+ def _is_write_disabled(self) -> bool:
150
+ """Return whether the config asks for no write at all."""
151
+ write = self._config.write
152
+ return self._config.general.dry_run or not (
153
+ write.formats or self._config.convert.cbz or write.delete_all_tags
154
+ )
155
+
149
156
  def dump(self, formats: frozenset[MetadataFormats] | None = None) -> None:
150
157
  """Write metadata according to config.write settings."""
151
158
  write = self._config.write
152
159
  convert = self._config.convert
153
- if self._config.general.dry_run or not (
154
- write.formats or convert.cbz or write.delete_all_tags
155
- ):
160
+ if self._is_write_disabled():
156
161
  logger.info(f"Not writing metadata for: {self._path}")
157
162
  return None
158
163
 
164
+ if self._path:
165
+ # Refuse an occupied conversion destination before any metadata
166
+ # is read. _get_dump_formats() below loads and merges every
167
+ # source, which is the expensive part of a write that was only
168
+ # ever going to be refused -- a kept-original CBR whose CBZ twin
169
+ # already exists is the common case, and it reaches here from
170
+ # the CLI as well as the API. _create_zipfile repeats the check
171
+ # (it is idempotent) so direct callers stay guarded.
172
+ self._get_new_archive_path()
173
+
159
174
  # Must get metadata *before* get write formats
160
175
  if formats is None:
161
176
  formats = self._get_dump_formats()
@@ -7,7 +7,11 @@ from loguru import logger
7
7
 
8
8
  from comicbox.box.archive.write import _claim_destination, _release_destination
9
9
  from comicbox.box.dump import ComicboxDump
10
- from comicbox.exceptions import ArchiveWriteError, ExportError
10
+ from comicbox.exceptions import (
11
+ ArchiveWriteError,
12
+ DestinationOccupiedError,
13
+ ExportError,
14
+ )
11
15
  from comicbox.formats import MetadataFormats
12
16
 
13
17
 
@@ -96,11 +100,10 @@ class ComicboxDumpToFiles(ComicboxDump):
96
100
  """
97
101
  if self._is_same_file(old_path, new_path):
98
102
  return
99
- _claim_destination(new_path)
103
+ _claim_destination(new_path, old_path)
100
104
  try:
101
105
  if new_path.exists():
102
- reason = f"{new_path} already exists."
103
- raise ArchiveWriteError(reason)
106
+ raise DestinationOccupiedError(old_path, new_path, "rename")
104
107
  old_path.rename(new_path)
105
108
  finally:
106
109
  _release_destination(new_path)
@@ -168,10 +168,13 @@ class OnlineSourceLimits:
168
168
  ``comicbox.formats.base.online.rate_limits`` for citation / audit.
169
169
  """
170
170
 
171
- # Historical Metron overrides. mokkari>=4.0.1 tracks Metron's actual
172
- # per-user limits from `X-RateLimit-*` response headers instead of a
173
- # fixed local bucket, with no injection point for a custom one; these
174
- # are accepted but ignored with a warning at client build.
171
+ # Metron overrides. mokkari tracks Metron's actual per-user limits
172
+ # from `X-RateLimit-*` response headers rather than a fixed local
173
+ # bucket. `per_minute` has been comicbox's `RateGate` ceiling since
174
+ # 5.1.0 -- it can lower the server-reported burst limit, never raise
175
+ # it, which is how several processes on one token split the window.
176
+ # `per_day` is still accepted and ignored with a warning at client
177
+ # build (see `_warn_ignored_rate_limit_overrides`).
175
178
  per_minute: int | None = None
176
179
  per_day: int | None = None
177
180
  # Historical ComicVine overrides. simyan builds its rate limiter
@@ -0,0 +1,102 @@
1
+ """
2
+ Exceptions for comicbox.
3
+
4
+ Every operational error comicbox raises on a public path derives from
5
+ :class:`ComicboxError`, so library consumers can write ``except
6
+ ComicboxError`` without also swallowing unrelated programming errors
7
+ (``ValueError``, ``KeyError``, …) from their own code.
8
+
9
+ This module is a leaf: it must not import anything from comicbox, so any
10
+ module (including the format packages and the box mixins) can import it
11
+ without load-order concerns. The write/online modules re-export their
12
+ exceptions from here under their historical import paths.
13
+ """
14
+
15
+ from pathlib import Path
16
+ from typing import Literal
17
+
18
+ from typing_extensions import override
19
+
20
+ DestinationKind = Literal["convert", "rename", "inflight"]
21
+
22
+
23
+ class ComicboxError(Exception):
24
+ """Base class for all operational errors comicbox raises."""
25
+
26
+
27
+ class UnsupportedArchiveTypeError(ComicboxError):
28
+ """Unsupported Archive Type."""
29
+
30
+
31
+ class ArchiveError(ComicboxError):
32
+ """An archive could not be opened or read."""
33
+
34
+
35
+ class ArchiveWriteError(ArchiveError):
36
+ """An archive could not be written, repacked, or renamed."""
37
+
38
+
39
+ class DestinationOccupiedError(ArchiveWriteError):
40
+ """
41
+ A write's destination path is held by another file or writer.
42
+
43
+ ``kind`` says which collision: ``"convert"`` -- the CBZ this archive
44
+ would repack to already exists (or another archive in the same batch
45
+ claims it, in which case ``occupant`` names it); ``"rename"`` -- the
46
+ predicted filename is taken; ``"inflight"`` -- another writer in this
47
+ process holds the destination right now (transient; retry later).
48
+ """
49
+
50
+ def __init__(
51
+ self,
52
+ source: Path,
53
+ destination: Path,
54
+ kind: DestinationKind,
55
+ occupant: Path | None = None,
56
+ ) -> None:
57
+ """Record the two paths, the collision kind, and any rival source."""
58
+ self.source = source
59
+ self.destination = destination
60
+ self.kind = kind
61
+ self.occupant = occupant
62
+ super().__init__(self._message())
63
+
64
+ def _message(self) -> str:
65
+ """Render the frozen message text for this collision kind."""
66
+ if self.kind == "inflight":
67
+ return f"{self.destination} is already being written by another archive."
68
+ if self.occupant is not None:
69
+ return f"{self.destination} is also the destination of {self.occupant}."
70
+ return f"{self.destination} already exists."
71
+
72
+ @override
73
+ def __reduce__(self) -> tuple:
74
+ """
75
+ Rebuild with the real signature when crossing a process boundary.
76
+
77
+ Python unpickles an exception as ``cls(*self.args)``, which here
78
+ is ``cls(message)`` -- a TypeError that would replace the real
79
+ error with a bogus one on the far side. WriteResult.error is a
80
+ public field, so keep it picklable.
81
+ """
82
+ return (type(self), (self.source, self.destination, self.kind, self.occupant))
83
+
84
+
85
+ class MetadataError(ComicboxError):
86
+ """Metadata could not be loaded or routed through the source pipeline."""
87
+
88
+
89
+ class ExportError(ComicboxError):
90
+ """A metadata file export failed its preconditions."""
91
+
92
+
93
+ class WriteValidationError(ComicboxError):
94
+ """Raised when write_metadata inputs are inconsistent or invalid."""
95
+
96
+
97
+ class OnlineConfigurationError(ComicboxError):
98
+ """Raised when OnlineSession inputs are inconsistent or incomplete."""
99
+
100
+
101
+ class OnlineLookupAbortedError(ComicboxError):
102
+ """Raised when the selector callback returns ('abort', None)."""
@@ -32,9 +32,11 @@ class _ApiCounts:
32
32
 
33
33
  Distinct from ``OnlineSource.api_call_counts``, which counts calls at
34
34
  comicbox's wrapper level: that number includes response-cache hits and
35
- counts a paginated result as one. These are real HTTP sends, recorded
36
- at the one place a request actually leaves the process, so the number
37
- a user pastes into a bug report lines up with the server's own logs.
35
+ counts a paginated result as one. ``requests`` counts responses
36
+ RECEIVED, per endpoint, recorded where the raw response arrives, so
37
+ the number a user pastes into a bug report lines up with the server's
38
+ own logs. A send that never answered is counted only in
39
+ ``connection_failures``.
38
40
  """
39
41
 
40
42
  requests: dict[str, int] = field(default_factory=dict)
@@ -138,14 +140,16 @@ class _OutcomeStats:
138
140
  self._per_source_api[source_name] = bucket
139
141
  return bucket
140
142
 
141
- def record_http_request(
142
- self, source_name: str, endpoint: str, blocked_seconds: float = 0.0
143
- ) -> None:
144
- """Record one real HTTP send and what it waited at the rate gate."""
143
+ def record_http_request(self, source_name: str, endpoint: str) -> None:
144
+ """Record one response received, by endpoint."""
145
145
  with self._lock:
146
146
  bucket = self._api_bucket_for(source_name)
147
147
  bucket.requests[endpoint] = bucket.requests.get(endpoint, 0) + 1
148
- bucket.blocked_seconds += blocked_seconds
148
+
149
+ def record_gate_wait(self, source_name: str, seconds: float) -> None:
150
+ """Record what one send waited at the rate gate before going out."""
151
+ with self._lock:
152
+ self._api_bucket_for(source_name).blocked_seconds += seconds
149
153
 
150
154
  def record_rate_limit_rejection(self, source_name: str) -> None:
151
155
  """Record one server rate-limit rejection (a 429 we still paid for)."""
@@ -379,6 +383,7 @@ record_skip = _STATS.record_skip
379
383
  record_no_match = _STATS.record_no_match
380
384
  record_explicit_id = _STATS.record_explicit_id
381
385
  record_http_request = _STATS.record_http_request
386
+ record_gate_wait = _STATS.record_gate_wait
382
387
  record_rate_limit_rejection = _STATS.record_rate_limit_rejection
383
388
  record_unthrottled_response = _STATS.record_unthrottled_response
384
389
  record_connection_failure = _STATS.record_connection_failure
@@ -65,6 +65,14 @@ class CandidateSummary:
65
65
  # to reproduce client-side the precedence its old six-call cascade
66
66
  # got from the order it made the calls in.
67
67
  volume: int | None = None
68
+ # A genuinely larger image of the same cover, for a frontend that
69
+ # wants a hover or lightbox next to the thumbnail. Display only: the
70
+ # matcher keeps hashing `cover_url`, because hashing a larger image
71
+ # moves every score and would need a fresh calibration run (see
72
+ # `matcher.py`'s audit note). None means this record carries no
73
+ # larger tier than `cover_url` already is, which is what a frontend
74
+ # keys "no hover" on.
75
+ cover_url_full: str | None = None
68
76
 
69
77
 
70
78
  @dataclass(frozen=True, slots=True)
@@ -1,9 +1,11 @@
1
1
  """
2
2
  Rate limits for online sources, for citation / audit.
3
3
 
4
- Neither upstream library takes a local override: mokkari reads Metron's
5
- per-user limits off the `X-RateLimit-*` response headers, and simyan
6
- builds its ComicVine limiter internally with no injection point at all.
4
+ mokkari reads Metron's per-user limits off the `X-RateLimit-*` response
5
+ headers; since 4.8.0 it also takes an injectable `rate_limiter`, which
6
+ is where comicbox plugs its `RateGate` in (see
7
+ `comicbox/formats/metron_api/paced_session.py`). simyan builds its
8
+ ComicVine limiter internally with no injection point at all.
7
9
  This module exists so the numbers are visible in our codebase (rather
8
10
  than hidden in transitive dependencies) and can be cited / audited, and
9
11
  so the consumers below have a stable constant to work from.