comicbox 5.2.1__tar.gz → 5.2.2__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 (601) hide show
  1. {comicbox-5.2.1 → comicbox-5.2.2}/.dockerignore +0 -1
  2. {comicbox-5.2.1 → comicbox-5.2.2}/.prettierignore +1 -1
  3. {comicbox-5.2.1 → comicbox-5.2.2}/NEWS.md +8 -0
  4. {comicbox-5.2.1 → comicbox-5.2.2}/PKG-INFO +2 -2
  5. comicbox-5.2.2/bin/ci-reuse-dist.sh +48 -0
  6. comicbox-5.2.2/bin/hub_prune_stale.py +519 -0
  7. comicbox-5.2.2/bin/lint-ci.sh +12 -0
  8. {comicbox-5.2.1 → comicbox-5.2.2}/bin/lint-python.sh +1 -1
  9. comicbox-5.2.2/bin/lint-vulture.sh +9 -0
  10. comicbox-5.2.2/bin/release-tag.sh +392 -0
  11. comicbox-5.2.2/bin/release_info.py +238 -0
  12. {comicbox-5.2.1 → comicbox-5.2.2}/bin/roman.py +32 -22
  13. {comicbox-5.2.1 → comicbox-5.2.2}/bun.lock +60 -515
  14. comicbox-5.2.2/cfg/eslint.config.base.js +256 -0
  15. {comicbox-5.2.1 → comicbox-5.2.2}/cfg/python.mk +6 -0
  16. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/box/archive/pages.py +2 -4
  17. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/box/computed/notes.py +1 -4
  18. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/box/pages/extract.py +5 -9
  19. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/config/online/build.py +1 -1
  20. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/base/online/matcher.py +1 -1
  21. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/base/online/outcome_stats.py +4 -4
  22. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/base/online/rate_gate.py +1 -1
  23. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/comicvine_api/online_source.py +1 -1
  24. {comicbox-5.2.1 → comicbox-5.2.2}/docs/NEWS.md +8 -0
  25. comicbox-5.2.2/generate-schema/gen-schema-docs.sh +13 -0
  26. {comicbox-5.2.1 → comicbox-5.2.2}/package.json +9 -33
  27. {comicbox-5.2.1 → comicbox-5.2.2}/pyproject.toml +18 -31
  28. {comicbox-5.2.1 → comicbox-5.2.2}/pyproject.toml.orig +31 -46
  29. {comicbox-5.2.1 → comicbox-5.2.2}/tests/test_extract.py +14 -0
  30. {comicbox-5.2.1 → comicbox-5.2.2}/tests/test_pages.py +6 -0
  31. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_identifiers.py +1 -1
  32. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_merge_mode_config.py +1 -1
  33. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_prompt_dedup.py +1 -1
  34. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_rar_time_overflow.py +1 -1
  35. {comicbox-5.2.1 → comicbox-5.2.2}/uv.lock +542 -500
  36. comicbox-5.2.1/.remarkignore +0 -99
  37. comicbox-5.2.1/bin/ci-download-dist-if-identical.sh +0 -38
  38. comicbox-5.2.1/bin/lint-ci.sh +0 -11
  39. comicbox-5.2.1/bin/release-tag.sh +0 -11
  40. comicbox-5.2.1/bin/remark-for-claude.sh +0 -7
  41. comicbox-5.2.1/cfg/eslint.config.base.js +0 -179
  42. {comicbox-5.2.1 → comicbox-5.2.2}/.gitignore +0 -0
  43. {comicbox-5.2.1 → comicbox-5.2.2}/.readthedocs.yaml +0 -0
  44. {comicbox-5.2.1 → comicbox-5.2.2}/.shellcheckrc +0 -0
  45. {comicbox-5.2.1 → comicbox-5.2.2}/Makefile +0 -0
  46. {comicbox-5.2.1 → comicbox-5.2.2}/README.md +0 -0
  47. {comicbox-5.2.1 → comicbox-5.2.2}/bin/clean-pycache.sh +0 -0
  48. {comicbox-5.2.1 → comicbox-5.2.2}/bin/cleanup-pypi-alpha.sh +0 -0
  49. {comicbox-5.2.1 → comicbox-5.2.2}/bin/dev-module.sh +0 -0
  50. {comicbox-5.2.1 → comicbox-5.2.2}/bin/docker-compose-exit.sh +0 -0
  51. {comicbox-5.2.1 → comicbox-5.2.2}/bin/docker-tag-latest.sh +0 -0
  52. {comicbox-5.2.1 → comicbox-5.2.2}/bin/fix-docker.sh +0 -0
  53. {comicbox-5.2.1 → comicbox-5.2.2}/bin/fix-python.sh +0 -0
  54. {comicbox-5.2.1 → comicbox-5.2.2}/bin/fix-sh.sh +0 -0
  55. {comicbox-5.2.1 → comicbox-5.2.2}/bin/fix.sh +0 -0
  56. {comicbox-5.2.1 → comicbox-5.2.2}/bin/kill-eslint_d.sh +0 -0
  57. {comicbox-5.2.1 → comicbox-5.2.2}/bin/lint-complexity.sh +0 -0
  58. {comicbox-5.2.1 → comicbox-5.2.2}/bin/lint-darwin.sh +0 -0
  59. {comicbox-5.2.1 → comicbox-5.2.2}/bin/lint-docker.sh +0 -0
  60. {comicbox-5.2.1 → comicbox-5.2.2}/bin/lint-sh.sh +0 -0
  61. {comicbox-5.2.1 → comicbox-5.2.2}/bin/lint.sh +0 -0
  62. {comicbox-5.2.1 → comicbox-5.2.2}/bin/prettier-nginx.sh +0 -0
  63. {comicbox-5.2.1 → comicbox-5.2.2}/bin/publish-pypi.sh +0 -0
  64. {comicbox-5.2.1 → comicbox-5.2.2}/bin/regenerate-pdf-test-pages.py +0 -0
  65. {comicbox-5.2.1 → comicbox-5.2.2}/bin/sort-ignore.sh +0 -0
  66. {comicbox-5.2.1 → comicbox-5.2.2}/bin/test-docker.sh +0 -0
  67. {comicbox-5.2.1 → comicbox-5.2.2}/bin/test-python.sh +0 -0
  68. {comicbox-5.2.1 → comicbox-5.2.2}/bin/type-python.sh +0 -0
  69. {comicbox-5.2.1 → comicbox-5.2.2}/bin/uml.sh +0 -0
  70. {comicbox-5.2.1 → comicbox-5.2.2}/bin/update-deps-node.sh +0 -0
  71. {comicbox-5.2.1 → comicbox-5.2.2}/bin/update-deps-python.sh +0 -0
  72. {comicbox-5.2.1 → comicbox-5.2.2}/bin/version-node.sh +0 -0
  73. {comicbox-5.2.1 → comicbox-5.2.2}/bin/version-python.sh +0 -0
  74. {comicbox-5.2.1 → comicbox-5.2.2}/cfg/ci.mk +0 -0
  75. {comicbox-5.2.1 → comicbox-5.2.2}/cfg/common.mk +0 -0
  76. {comicbox-5.2.1 → comicbox-5.2.2}/cfg/docker.mk +0 -0
  77. {comicbox-5.2.1 → comicbox-5.2.2}/cfg/docs.mk +0 -0
  78. {comicbox-5.2.1 → comicbox-5.2.2}/cfg/gha_std.mk +0 -0
  79. {comicbox-5.2.1 → comicbox-5.2.2}/cfg/help.mk +0 -0
  80. {comicbox-5.2.1 → comicbox-5.2.2}/cfg/node.mk +0 -0
  81. {comicbox-5.2.1 → comicbox-5.2.2}/cfg/node_root.mk +0 -0
  82. {comicbox-5.2.1 → comicbox-5.2.2}/check-jsonschema.sh +0 -0
  83. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/__init__.py +0 -0
  84. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/_pdf.py +0 -0
  85. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/_rar.py +0 -0
  86. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/box/__init__.py +0 -0
  87. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/box/archive/__init__.py +0 -0
  88. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/box/archive/archive.py +0 -0
  89. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/box/archive/archiveinfo.py +0 -0
  90. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/box/archive/filenames.py +0 -0
  91. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/box/archive/init.py +0 -0
  92. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/box/archive/mtime.py +0 -0
  93. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/box/archive/read.py +0 -0
  94. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/box/archive/sniff.py +0 -0
  95. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/box/archive/write.py +0 -0
  96. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/box/computed/__init__.py +0 -0
  97. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/box/computed/date.py +0 -0
  98. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/box/computed/identifiers.py +0 -0
  99. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/box/computed/issue.py +0 -0
  100. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/box/computed/pages.py +0 -0
  101. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/box/computed/stamp.py +0 -0
  102. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/box/computed/stories_title.py +0 -0
  103. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/box/computed/url_identifiers.py +0 -0
  104. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/box/computed/urls.py +0 -0
  105. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/box/dump.py +0 -0
  106. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/box/dump_files.py +0 -0
  107. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/box/init.py +0 -0
  108. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/box/load.py +0 -0
  109. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/box/merge.py +0 -0
  110. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/box/metadata.py +0 -0
  111. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/box/normalize.py +0 -0
  112. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/box/online_covers.py +0 -0
  113. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/box/online_lookup.py +0 -0
  114. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/box/pages/__init__.py +0 -0
  115. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/box/pages/covers.py +0 -0
  116. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/box/print.py +0 -0
  117. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/box/sources.py +0 -0
  118. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/box/types.py +0 -0
  119. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/box/validate/__init__.py +0 -0
  120. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/box/validate/guess_format.py +0 -0
  121. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/cli/__init__.py +0 -0
  122. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/cli/__main__.py +0 -0
  123. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/cli/epilog.py +0 -0
  124. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/cli/parser.py +0 -0
  125. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/config/__init__.py +0 -0
  126. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/config/computed.py +0 -0
  127. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/config/formats.py +0 -0
  128. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/config/online/__init__.py +0 -0
  129. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/config/online/settings.py +0 -0
  130. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/config/online/template.py +0 -0
  131. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/config/paths.py +0 -0
  132. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/config/read.py +0 -0
  133. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/config/settings.py +0 -0
  134. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/config_default.yaml +0 -0
  135. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/constants.py +0 -0
  136. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/empty.py +0 -0
  137. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/enums/__init__.py +0 -0
  138. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/enums/comet.py +0 -0
  139. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/enums/comicbookinfo.py +0 -0
  140. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/enums/comicbox.py +0 -0
  141. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/enums/comicinfo.py +0 -0
  142. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/enums/generic/__init__.py +0 -0
  143. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/enums/generic/age_rating.py +0 -0
  144. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/enums/generic/role.py +0 -0
  145. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/enums/maps/__init__.py +0 -0
  146. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/enums/maps/age_rating.py +0 -0
  147. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/enums/maps/formats.py +0 -0
  148. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/enums/maps/identifiers.py +0 -0
  149. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/enums/maps/reading_direction.py +0 -0
  150. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/enums/maps/roles.py +0 -0
  151. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/enums/metroninfo.py +0 -0
  152. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/events.py +0 -0
  153. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/exceptions.py +0 -0
  154. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/__init__.py +0 -0
  155. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/_base.py +0 -0
  156. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/base/__init__.py +0 -0
  157. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/base/fields/__init__.py +0 -0
  158. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/base/fields/collection_fields.py +0 -0
  159. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/base/fields/comicbox.py +0 -0
  160. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/base/fields/comicinfo.py +0 -0
  161. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/base/fields/enum_fields.py +0 -0
  162. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/base/fields/fields.py +0 -0
  163. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/base/fields/metroninfo.py +0 -0
  164. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/base/fields/number_fields.py +0 -0
  165. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/base/fields/pdf.py +0 -0
  166. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/base/fields/pycountry.py +0 -0
  167. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/base/fields/time_fields.py +0 -0
  168. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/base/fields/union.py +0 -0
  169. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/base/fields/xml_fields.py +0 -0
  170. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/base/online/__init__.py +0 -0
  171. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/base/online/auto_engage.py +0 -0
  172. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/base/online/cli_overrides.py +0 -0
  173. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/base/online/cover_hash.py +0 -0
  174. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/base/online/credentials.py +0 -0
  175. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/base/online/profile.py +0 -0
  176. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/base/online/prompt.py +0 -0
  177. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/base/online/rate_limits.py +0 -0
  178. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/base/online/retry.py +0 -0
  179. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/base/online/sanitize.py +0 -0
  180. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/base/online/selector.py +0 -0
  181. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/base/online/series_cache.py +0 -0
  182. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/base/online/series_filter.py +0 -0
  183. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/base/online/session_state.py +0 -0
  184. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/base/online/signals.py +0 -0
  185. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/base/online/sources/__init__.py +0 -0
  186. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/base/online/sources/base.py +0 -0
  187. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/base/online/transform_helpers.py +0 -0
  188. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/base/online/vacuum.py +0 -0
  189. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/base/online/warn_once.py +0 -0
  190. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/base/schemas/__init__.py +0 -0
  191. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/base/schemas/base.py +0 -0
  192. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/base/schemas/cache.py +0 -0
  193. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/base/schemas/decorators.py +0 -0
  194. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/base/schemas/error_store.py +0 -0
  195. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/base/schemas/json_schemas.py +0 -0
  196. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/base/schemas/xml_schemas.py +0 -0
  197. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/base/schemas/yaml.py +0 -0
  198. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/base/transforms/__init__.py +0 -0
  199. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/base/transforms/base.py +0 -0
  200. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/base/transforms/identifiers.py +0 -0
  201. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/base/transforms/price.py +0 -0
  202. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/base/transforms/publishing_tags.py +0 -0
  203. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/base/transforms/spec.py +0 -0
  204. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/base/transforms/xml_credits.py +0 -0
  205. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/base/transforms/xml_reprints.py +0 -0
  206. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/comet/__init__.py +0 -0
  207. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/comet/schema.py +0 -0
  208. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/comet/transform/__init__.py +0 -0
  209. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/comet/transform/reprints.py +0 -0
  210. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/comic_book_info/__init__.py +0 -0
  211. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/comic_book_info/schema.py +0 -0
  212. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/comic_book_info/transform/__init__.py +0 -0
  213. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/comic_book_info/transform/credits.py +0 -0
  214. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/comic_book_info/transform/rating.py +0 -0
  215. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/comic_info/__init__.py +0 -0
  216. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/comic_info/schema.py +0 -0
  217. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/comic_info/transform/__init__.py +0 -0
  218. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/comic_info/transform/identifiers.py +0 -0
  219. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/comic_info/transform/manga.py +0 -0
  220. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/comic_info/transform/pages.py +0 -0
  221. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/comic_info/transform/storyarcs.py +0 -0
  222. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/comicbox/__init__.py +0 -0
  223. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/comicbox/schema/README.md +0 -0
  224. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/comicbox/schema/__init__.py +0 -0
  225. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/comicbox/schema/cli.py +0 -0
  226. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/comicbox/schema/identifiers.py +0 -0
  227. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/comicbox/schema/json_schema.py +0 -0
  228. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/comicbox/schema/pages.py +0 -0
  229. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/comicbox/schema/publishing.py +0 -0
  230. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/comicbox/schema/yaml.py +0 -0
  231. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/comicbox/transform/__init__.py +0 -0
  232. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/comicbox/transform/cli.py +0 -0
  233. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/comicbox/transform/credits.py +0 -0
  234. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/comicbox/transform/json.py +0 -0
  235. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/comicbox/transform/name_objs.py +0 -0
  236. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/comicbox/transform/yaml.py +0 -0
  237. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/comicvine_api/__init__.py +0 -0
  238. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/comicvine_api/schema.py +0 -0
  239. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/comicvine_api/transform.py +0 -0
  240. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/filename/__init__.py +0 -0
  241. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/filename/schema.py +0 -0
  242. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/filename/transform.py +0 -0
  243. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/metron_api/__init__.py +0 -0
  244. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/metron_api/online_source.py +0 -0
  245. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/metron_api/paced_session.py +0 -0
  246. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/metron_api/schema.py +0 -0
  247. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/metron_api/transform.py +0 -0
  248. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/metron_info/__init__.py +0 -0
  249. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/metron_info/schema/__init__.py +0 -0
  250. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/metron_info/schema/credits.py +0 -0
  251. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/metron_info/schema/identifiers.py +0 -0
  252. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/metron_info/schema/price.py +0 -0
  253. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/metron_info/schema/publishing.py +0 -0
  254. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/metron_info/schema/rating.py +0 -0
  255. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/metron_info/schema/resource.py +0 -0
  256. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/metron_info/transform/__init__.py +0 -0
  257. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/metron_info/transform/const.py +0 -0
  258. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/metron_info/transform/credits.py +0 -0
  259. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/metron_info/transform/identified_name.py +0 -0
  260. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/metron_info/transform/identifier_attribute.py +0 -0
  261. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/metron_info/transform/identifiers.py +0 -0
  262. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/metron_info/transform/publishing_tags.py +0 -0
  263. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/metron_info/transform/rating.py +0 -0
  264. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/metron_info/transform/reprints.py +0 -0
  265. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/metron_info/transform/resources.py +0 -0
  266. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/pdf/__init__.py +0 -0
  267. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/pdf/schema.py +0 -0
  268. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/pdf/transform/__init__.py +0 -0
  269. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/pdf/transform/credits.py +0 -0
  270. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/formats/sources.py +0 -0
  271. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/identifiers/__init__.py +0 -0
  272. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/identifiers/identifiers.py +0 -0
  273. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/identifiers/other.py +0 -0
  274. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/identifiers/urns.py +0 -0
  275. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/logger.py +0 -0
  276. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/merge/__init__.py +0 -0
  277. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/merge/mergedeep.py +0 -0
  278. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/online_estimate.py +0 -0
  279. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/online_session.py +0 -0
  280. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/predict.py +0 -0
  281. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/print.py +0 -0
  282. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/process.py +0 -0
  283. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/py.typed +0 -0
  284. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/run.py +0 -0
  285. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/schemas/CoMet-v1.1.xsd +0 -0
  286. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/schemas/ComicInfo-v2.1-Draft.xsd +0 -0
  287. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/schemas/MetronInfo-v1.1.xsd +0 -0
  288. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/schemas/comic-book-info-v1.0.schema.json +0 -0
  289. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/schemas/v1.0/comicbox-v1.0.schema.json +0 -0
  290. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/schemas/v1.0/page.schema.json +0 -0
  291. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/schemas/v1.0/reprint.schema.json +0 -0
  292. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/schemas/v1.0/series.schema.json +0 -0
  293. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/schemas/v1.0/volume.schema.json +0 -0
  294. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/schemas/v2.0/comicbox-v2.0.schema.json +0 -0
  295. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/schemas/v2.0/comicbox.example.yaml +0 -0
  296. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/schemas/v2.0/credit.schema.json +0 -0
  297. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/schemas/v2.0/date.schema.json +0 -0
  298. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/schemas/v2.0/identified-object.schema.json +0 -0
  299. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/schemas/v2.0/identified-objects.schema.json +0 -0
  300. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/schemas/v2.0/identifiers.schema.json +0 -0
  301. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/schemas/v2.0/named-identified-object.schema.json +0 -0
  302. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/schemas/v2.0/page.schema.json +0 -0
  303. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/schemas/v2.0/reprint.schema.json +0 -0
  304. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/schemas/v2.0/series.schema.json +0 -0
  305. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/schemas/v2.0/volume.schema.json +0 -0
  306. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/schemas/v3.0/alternative-name.schema.json +0 -0
  307. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/schemas/v3.0/comicbox-v3.0.schema.json +0 -0
  308. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/schemas/v3.0/comicbox.example.yaml +0 -0
  309. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/schemas/v3.0/credit.schema.json +0 -0
  310. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/schemas/v3.0/date.schema.json +0 -0
  311. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/schemas/v3.0/identified-object.schema.json +0 -0
  312. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/schemas/v3.0/identified-objects.schema.json +0 -0
  313. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/schemas/v3.0/identifiers.schema.json +0 -0
  314. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/schemas/v3.0/named-identified-object.schema.json +0 -0
  315. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/schemas/v3.0/page.schema.json +0 -0
  316. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/schemas/v3.0/reprint.schema.json +0 -0
  317. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/schemas/v3.0/role.schema.json +0 -0
  318. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/schemas/v3.0/series.schema.json +0 -0
  319. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/schemas/v3.0/volume.schema.json +0 -0
  320. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/validate/__init__.py +0 -0
  321. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/validate/base.py +0 -0
  322. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/validate/json_validator.py +0 -0
  323. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/validate/spec.py +0 -0
  324. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/validate/xml_validator.py +0 -0
  325. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/validate/yaml_validator.py +0 -0
  326. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/version.py +0 -0
  327. {comicbox-5.2.1 → comicbox-5.2.2}/comicbox/write.py +0 -0
  328. {comicbox-5.2.1 → comicbox-5.2.2}/compose.yaml +0 -0
  329. {comicbox-5.2.1 → comicbox-5.2.2}/docs/README.md +0 -0
  330. {comicbox-5.2.1 → comicbox-5.2.2}/docs/requirements.txt +0 -0
  331. {comicbox-5.2.1 → comicbox-5.2.2}/docs/style.css +0 -0
  332. {comicbox-5.2.1 → comicbox-5.2.2}/docs/style.mkdocs.css +0 -0
  333. {comicbox-5.2.1 → comicbox-5.2.2}/eslint.config.js +0 -0
  334. {comicbox-5.2.1 → comicbox-5.2.2}/generate-schema/gen-schema.sh +0 -0
  335. {comicbox-5.2.1 → comicbox-5.2.2}/generate-schema/mjs.py +0 -0
  336. {comicbox-5.2.1 → comicbox-5.2.2}/mkdocs.yml +0 -0
  337. {comicbox-5.2.1 → comicbox-5.2.2}/tests/__init__.py +0 -0
  338. {comicbox-5.2.1 → comicbox-5.2.2}/tests/calibration/.gitignore +0 -0
  339. {comicbox-5.2.1 → comicbox-5.2.2}/tests/calibration/README.md +0 -0
  340. {comicbox-5.2.1 → comicbox-5.2.2}/tests/calibration/__init__.py +0 -0
  341. {comicbox-5.2.1 → comicbox-5.2.2}/tests/calibration/_harness_helpers.py +0 -0
  342. {comicbox-5.2.1 → comicbox-5.2.2}/tests/calibration/bootstrap.py +0 -0
  343. {comicbox-5.2.1 → comicbox-5.2.2}/tests/calibration/compare.py +0 -0
  344. {comicbox-5.2.1 → comicbox-5.2.2}/tests/calibration/debug_search.py +0 -0
  345. {comicbox-5.2.1 → comicbox-5.2.2}/tests/calibration/fixtures.example.json +0 -0
  346. {comicbox-5.2.1 → comicbox-5.2.2}/tests/calibration/label_metron.py +0 -0
  347. {comicbox-5.2.1 → comicbox-5.2.2}/tests/calibration/run.py +0 -0
  348. {comicbox-5.2.1 → comicbox-5.2.2}/tests/calibration/sample.py +0 -0
  349. {comicbox-5.2.1 → comicbox-5.2.2}/tests/calibration/summarize.py +0 -0
  350. {comicbox-5.2.1 → comicbox-5.2.2}/tests/calibration/test_bootstrap.py +0 -0
  351. {comicbox-5.2.1 → comicbox-5.2.2}/tests/calibration/test_compare.py +0 -0
  352. {comicbox-5.2.1 → comicbox-5.2.2}/tests/calibration/test_harness.py +0 -0
  353. {comicbox-5.2.1 → comicbox-5.2.2}/tests/calibration/test_harness_checkpoint.py +0 -0
  354. {comicbox-5.2.1 → comicbox-5.2.2}/tests/calibration/test_harness_diagnostics.py +0 -0
  355. {comicbox-5.2.1 → comicbox-5.2.2}/tests/calibration/test_harness_filter.py +0 -0
  356. {comicbox-5.2.1 → comicbox-5.2.2}/tests/calibration/test_harness_resume.py +0 -0
  357. {comicbox-5.2.1 → comicbox-5.2.2}/tests/calibration/test_label_metron.py +0 -0
  358. {comicbox-5.2.1 → comicbox-5.2.2}/tests/calibration/test_sample.py +0 -0
  359. {comicbox-5.2.1 → comicbox-5.2.2}/tests/calibration/test_summarize.py +0 -0
  360. {comicbox-5.2.1 → comicbox-5.2.2}/tests/cli/__init__.py +0 -0
  361. {comicbox-5.2.1 → comicbox-5.2.2}/tests/cli/test_cli_exit_code.py +0 -0
  362. {comicbox-5.2.1 → comicbox-5.2.2}/tests/cli/test_cli_export.py +0 -0
  363. {comicbox-5.2.1 → comicbox-5.2.2}/tests/cli/test_cli_extract.py +0 -0
  364. {comicbox-5.2.1 → comicbox-5.2.2}/tests/cli/test_cli_online.py +0 -0
  365. {comicbox-5.2.1 → comicbox-5.2.2}/tests/cli/test_cli_print.py +0 -0
  366. {comicbox-5.2.1 → comicbox-5.2.2}/tests/cli/test_cli_rename.py +0 -0
  367. {comicbox-5.2.1 → comicbox-5.2.2}/tests/cli/test_cli_write.py +0 -0
  368. {comicbox-5.2.1 → comicbox-5.2.2}/tests/conftest.py +0 -0
  369. {comicbox-5.2.1 → comicbox-5.2.2}/tests/const.py +0 -0
  370. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/Captain Science #001 (1950) The Beginning - multi.cbz +0 -0
  371. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/Captain Science #001 (1950) The Beginning - nothing.cbz +0 -0
  372. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/Captain Science #001-cbi.cbr +0 -0
  373. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/Captain Science #001-cix-cbi.cbr +0 -0
  374. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/Captain Science #001-cix.cbt +0 -0
  375. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/Captain Science #001-cix.cbz +0 -0
  376. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/Captain Science #001-comet.cbz +0 -0
  377. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/Captain Science #001-metron.cbz +0 -0
  378. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/Captain Science #001.cb7 +0 -0
  379. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/Captain Science #001.cbz +0 -0
  380. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/Captain Science 001/CaptainScience#1_01.jpg +0 -0
  381. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/Captain Science 001/CaptainScience#1_02.jpg +0 -0
  382. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/Captain Science 001/CaptainScience#1_03.jpg +0 -0
  383. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/Captain Science 001/CaptainScience#1_04.jpg +0 -0
  384. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/Captain Science 001/CaptainScience#1_05.jpg +0 -0
  385. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/Captain Science 001/CoMet.xml +0 -0
  386. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/Captain Science 001/comic-book-info.json +0 -0
  387. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/Captain Science 001/comicinfo-original.xml +0 -0
  388. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/Captain Science 001/comicinfo.xml +0 -0
  389. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/Captain Science 001/metroninfo-full.xml +0 -0
  390. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/Captain Science 001/metroninfo.xml +0 -0
  391. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/comicbox-cli.cbz +0 -0
  392. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/comicbox.cbz +0 -0
  393. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/create-archive.sh +0 -0
  394. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/create_cs_archives.py +0 -0
  395. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/create_macos_resource_fork.py +0 -0
  396. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/empty.cbz +0 -0
  397. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/examples/comic-book-info-example.json +0 -0
  398. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/examples/metroninfo-v1.1-valid.xml +0 -0
  399. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/export/comet.xml +0 -0
  400. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/export/comic-book-info.json +0 -0
  401. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/export/comicbox-cli.yaml +0 -0
  402. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/export/comicbox.json +0 -0
  403. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/export/comicbox.yaml +0 -0
  404. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/export/comicinfo.xml +0 -0
  405. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/export/create-export-archive.sh +0 -0
  406. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/export/metroninfo.xml +0 -0
  407. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/export/mupdf.json +0 -0
  408. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/export/pdf-metadata.xml +0 -0
  409. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/export.cbz +0 -0
  410. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/list_recurse_output.txt +0 -0
  411. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/macos_resource_fork.cbz +0 -0
  412. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/metadata/comet-cover-path-list.xml +0 -0
  413. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/metadata/comet-write.xml +0 -0
  414. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/metadata/comet.xml +0 -0
  415. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/metadata/comic-book-info-example.json +0 -0
  416. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/metadata/comic-book-info.json +0 -0
  417. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/metadata/comicbox-cli.yaml +0 -0
  418. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/metadata/comicbox-filename.txt +0 -0
  419. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/metadata/comicbox-write.json +0 -0
  420. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/metadata/comicbox-write.yaml +0 -0
  421. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/metadata/comicbox.json +0 -0
  422. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/metadata/comicbox.xml +0 -0
  423. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/metadata/comicbox.yaml +0 -0
  424. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/metadata/comicinfo-bad-multiple.xml +0 -0
  425. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/metadata/comicinfo-cover-path-list.xml +0 -0
  426. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/metadata/comicinfo-ids-from-tags.xml +0 -0
  427. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/metadata/comicinfo-metron-origin.xml +0 -0
  428. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/metadata/comicinfo-notes-date.xml +0 -0
  429. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/metadata/comicinfo-validation.xml +0 -0
  430. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/metadata/comicinfo-write.xml +0 -0
  431. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/metadata/comicinfo.xml +0 -0
  432. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/metadata/create-archives.sh +0 -0
  433. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/metadata/metroninfo-no-primary-id.xml +0 -0
  434. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/metadata/metroninfo-v1.1-valid.xml +0 -0
  435. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/metadata/metroninfo-write.xml +0 -0
  436. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/metadata/metroninfo.xml +0 -0
  437. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/metadata/mupdf-nested.json +0 -0
  438. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/metadata/mupdf.json +0 -0
  439. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/metadata/pdf-cix.xml +0 -0
  440. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/metadata/pdf-nested.xml +0 -0
  441. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/metadata/pdf.xml +0 -0
  442. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/multi/comet.xml +0 -0
  443. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/multi/comic-book-info.json +0 -0
  444. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/multi/comicbox-filename.txt +0 -0
  445. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/multi/comicbox.json +0 -0
  446. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/multi/comicbox.yaml +0 -0
  447. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/multi/comicinfo.xml +0 -0
  448. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/multi/create-multi-archive.sh +0 -0
  449. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/patchme.cbz +0 -0
  450. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/pdf/0.pdf +0 -0
  451. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/pdf/1.pdf +0 -0
  452. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/pdf/2.pdf +0 -0
  453. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/pdf/3.pdf +0 -0
  454. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/test_cix.pdf +0 -0
  455. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/test_pdf.pdf +0 -0
  456. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/test_pdf_legacy_keywords.pdf +0 -0
  457. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/xml.cbz +0 -0
  458. {comicbox-5.2.1 → comicbox-5.2.2}/tests/files/yaml.cbz +0 -0
  459. {comicbox-5.2.1 → comicbox-5.2.2}/tests/schemas/__init__.py +0 -0
  460. {comicbox-5.2.1 → comicbox-5.2.2}/tests/schemas/test_cbi.py +0 -0
  461. {comicbox-5.2.1 → comicbox-5.2.2}/tests/schemas/test_cix.py +0 -0
  462. {comicbox-5.2.1 → comicbox-5.2.2}/tests/schemas/test_comet.py +0 -0
  463. {comicbox-5.2.1 → comicbox-5.2.2}/tests/schemas/test_filename.py +0 -0
  464. {comicbox-5.2.1 → comicbox-5.2.2}/tests/schemas/test_json.py +0 -0
  465. {comicbox-5.2.1 → comicbox-5.2.2}/tests/schemas/test_json_schema_drift.py +0 -0
  466. {comicbox-5.2.1 → comicbox-5.2.2}/tests/schemas/test_metron.py +0 -0
  467. {comicbox-5.2.1 → comicbox-5.2.2}/tests/schemas/test_multi.py +0 -0
  468. {comicbox-5.2.1 → comicbox-5.2.2}/tests/schemas/test_pdf_json.py +0 -0
  469. {comicbox-5.2.1 → comicbox-5.2.2}/tests/schemas/test_pdf_xml.py +0 -0
  470. {comicbox-5.2.1 → comicbox-5.2.2}/tests/schemas/test_validation.py +0 -0
  471. {comicbox-5.2.1 → comicbox-5.2.2}/tests/schemas/test_yaml.py +0 -0
  472. {comicbox-5.2.1 → comicbox-5.2.2}/tests/stress/.gitignore +0 -0
  473. {comicbox-5.2.1 → comicbox-5.2.2}/tests/stress/README.md +0 -0
  474. {comicbox-5.2.1 → comicbox-5.2.2}/tests/stress/__init__.py +0 -0
  475. {comicbox-5.2.1 → comicbox-5.2.2}/tests/stress/jobs_accuracy.py +0 -0
  476. {comicbox-5.2.1 → comicbox-5.2.2}/tests/stress/prompt_ux.py +0 -0
  477. {comicbox-5.2.1 → comicbox-5.2.2}/tests/stress/run.py +0 -0
  478. {comicbox-5.2.1 → comicbox-5.2.2}/tests/stress/test_jobs_accuracy.py +0 -0
  479. {comicbox-5.2.1 → comicbox-5.2.2}/tests/stress/test_prompt_ux.py +0 -0
  480. {comicbox-5.2.1 → comicbox-5.2.2}/tests/stress/test_run.py +0 -0
  481. {comicbox-5.2.1 → comicbox-5.2.2}/tests/test_codex_api.py +0 -0
  482. {comicbox-5.2.1 → comicbox-5.2.2}/tests/test_compute.py +0 -0
  483. {comicbox-5.2.1 → comicbox-5.2.2}/tests/test_convert_pdf.py +0 -0
  484. {comicbox-5.2.1 → comicbox-5.2.2}/tests/test_import_export.py +0 -0
  485. {comicbox-5.2.1 → comicbox-5.2.2}/tests/test_mtime.py +0 -0
  486. {comicbox-5.2.1 → comicbox-5.2.2}/tests/test_stories_title.py +0 -0
  487. {comicbox-5.2.1 → comicbox-5.2.2}/tests/test_validate_testfiles.py +0 -0
  488. {comicbox-5.2.1 → comicbox-5.2.2}/tests/test_writing.py +0 -0
  489. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/__init__.py +0 -0
  490. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_archive_errors.py +0 -0
  491. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_archive_read.py +0 -0
  492. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_archiveinfo_mtime.py +0 -0
  493. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_auto_engage.py +0 -0
  494. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_box_cache_invalidation.py +0 -0
  495. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_cbi_rating_normalize.py +0 -0
  496. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_cli_choices_enum_drift.py +0 -0
  497. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_cli_dest_folds.py +0 -0
  498. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_cli_dest_template_drift.py +0 -0
  499. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_comicinfo_alternates.py +0 -0
  500. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_comicinfo_series_groups.py +0 -0
  501. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_comicvine.py +0 -0
  502. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_comicvine_client.py +0 -0
  503. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_comicvine_session_lifecycle.py +0 -0
  504. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_computed_pipeline.py +0 -0
  505. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_computed_reprints.py +0 -0
  506. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_config_container_inputs.py +0 -0
  507. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_config_defaults_drift.py +0 -0
  508. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_config_env_tree.py +0 -0
  509. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_config_file_option.py +0 -0
  510. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_config_layering.py +0 -0
  511. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_config_no_path_guard.py +0 -0
  512. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_config_online_leaf.py +0 -0
  513. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_config_online_sources.py +0 -0
  514. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_cover_hash.py +0 -0
  515. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_credit_primary.py +0 -0
  516. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_defer_prompts.py +0 -0
  517. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_destination_occupied_error.py +0 -0
  518. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_error_store_visibility.py +0 -0
  519. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_exceptions.py +0 -0
  520. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_lazy_startup.py +0 -0
  521. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_library_logging.py +0 -0
  522. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_list_field_dedupe.py +0 -0
  523. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_list_field_sort_key_types.py +0 -0
  524. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_manga_and_age_rating.py +0 -0
  525. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_manga_volume.py +0 -0
  526. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_merge_order.py +0 -0
  527. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_merge_precedence.py +0 -0
  528. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_mergedeep.py +0 -0
  529. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_metron_community_rating.py +0 -0
  530. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_metron_id_attribute_fallback.py +0 -0
  531. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_metron_prefetch.py +0 -0
  532. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_metron_session_lifecycle.py +0 -0
  533. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_metron_source.py +0 -0
  534. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_metron_transform.py +0 -0
  535. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_online.py +0 -0
  536. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_online_abort_propagation.py +0 -0
  537. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_online_degradation.py +0 -0
  538. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_online_estimate.py +0 -0
  539. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_online_events.py +0 -0
  540. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_online_lookup.py +0 -0
  541. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_online_matcher.py +0 -0
  542. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_online_matcher_resolution.py +0 -0
  543. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_online_matcher_scoring.py +0 -0
  544. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_online_prompt.py +0 -0
  545. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_online_prompt_loops.py +0 -0
  546. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_online_result_contract.py +0 -0
  547. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_online_retry.py +0 -0
  548. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_online_selector.py +0 -0
  549. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_online_session.py +0 -0
  550. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_online_session_state.py +0 -0
  551. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_outcome_stats.py +0 -0
  552. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_paced_session.py +0 -0
  553. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_parse_decimal.py +0 -0
  554. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_parse_issue.py +0 -0
  555. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_pdf_pages_help.py +0 -0
  556. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_process.py +0 -0
  557. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_quota_reserved_skip.py +0 -0
  558. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_rate_gate.py +0 -0
  559. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_rate_gate_concurrency.py +0 -0
  560. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_rate_limits.py +0 -0
  561. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_read_events.py +0 -0
  562. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_refresh_cache.py +0 -0
  563. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_rename_and_convert_safety.py +0 -0
  564. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_reprints_and_alternative_names.py +0 -0
  565. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_rich_transforms.py +0 -0
  566. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_role_vocabulary.py +0 -0
  567. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_run_parallel.py +0 -0
  568. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_run_recurse.py +0 -0
  569. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_series_cache.py +0 -0
  570. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_series_filter.py +0 -0
  571. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_series_single_flight.py +0 -0
  572. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_session_series_batching.py +0 -0
  573. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_single_valued_tags.py +0 -0
  574. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_sniff.py +0 -0
  575. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_sources.py +0 -0
  576. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_to_metron_age_rating.py +0 -0
  577. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_transform_spec_dest_collisions.py +0 -0
  578. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_transform_spec_missing_keys.py +0 -0
  579. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_transform_spec_scope_phases.py +0 -0
  580. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_urls_and_identifiers.py +0 -0
  581. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_vacuum.py +0 -0
  582. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_validate.py +0 -0
  583. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_write_api.py +0 -0
  584. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_write_destination_prediction.py +0 -0
  585. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_write_destination_safety.py +0 -0
  586. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_write_order_and_claims.py +0 -0
  587. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_xml_cdata_fields.py +0 -0
  588. {comicbox-5.2.1 → comicbox-5.2.2}/tests/unit/test_yes_no_field.py +0 -0
  589. {comicbox-5.2.1 → comicbox-5.2.2}/tests/util/__init__.py +0 -0
  590. {comicbox-5.2.1 → comicbox-5.2.2}/tests/util/compare.py +0 -0
  591. {comicbox-5.2.1 → comicbox-5.2.2}/tests/util/diff.py +0 -0
  592. {comicbox-5.2.1 → comicbox-5.2.2}/tests/util/metadata.py +0 -0
  593. {comicbox-5.2.1 → comicbox-5.2.2}/tests/util/metron_transport.py +0 -0
  594. {comicbox-5.2.1 → comicbox-5.2.2}/tests/util/online_client.py +0 -0
  595. {comicbox-5.2.1 → comicbox-5.2.2}/tests/util/online_matcher.py +0 -0
  596. {comicbox-5.2.1 → comicbox-5.2.2}/tests/util/online_prompt.py +0 -0
  597. {comicbox-5.2.1 → comicbox-5.2.2}/tests/util/parser.py +0 -0
  598. {comicbox-5.2.1 → comicbox-5.2.2}/tests/util/tmp.py +0 -0
  599. {comicbox-5.2.1 → comicbox-5.2.2}/tests/validate_cli.py +0 -0
  600. {comicbox-5.2.1 → comicbox-5.2.2}/validate-schema.sh +0 -0
  601. {comicbox-5.2.1 → comicbox-5.2.2}/vulture_ignorelist.py +0 -0
@@ -1,6 +1,5 @@
1
1
  __pycache__
2
2
  .*cache
3
- .circleci
4
3
  .claude
5
4
  .coverage*
6
5
  .DS_Store
@@ -1,7 +1,6 @@
1
1
  __pycache__
2
2
  .*cache
3
3
  .*cache/
4
- .circleci
5
4
  .claude
6
5
  .git
7
6
  .venv
@@ -13,6 +12,7 @@ dist
13
12
  Dockerfile
14
13
  node_modules
15
14
  schemas/*Info*.xsd
15
+ tasks
16
16
  test-results
17
17
  tests/**/*.json
18
18
  tests/**/*.xml
@@ -1,5 +1,13 @@
1
1
  # 📰 News
2
2
 
3
+ ## v5.2.2
4
+
5
+ - Fixes
6
+ - `get_page_by_index()` returns `None` for an index past the last page
7
+ instead of raising `StopIteration`.
8
+ - Extracting covers writes each cover once. A cover found by more than one
9
+ heuristic was written, and counted, once per heuristic.
10
+
3
11
  ## v5.2.1
4
12
 
5
13
  - Fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: comicbox
3
- Version: 5.2.1
3
+ Version: 5.2.2
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
@@ -14,7 +14,7 @@ Classifier: Programming Language :: Python :: 3
14
14
  Requires-Dist: bidict~=0.23
15
15
  Requires-Dist: case-converter~=1.2
16
16
  Requires-Dist: comicfn2dict>=0.3.2,<1.0.0
17
- Requires-Dist: confuse~=2.2.0
17
+ Requires-Dist: confuse~=2.3.0
18
18
  Requires-Dist: glom~=25.12
19
19
  Requires-Dist: httpx~=0.28
20
20
  Requires-Dist: imagehash>=4.3.2
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env bash
2
+ # Reuse python-dist from an earlier CI run that passed on this exact tree.
3
+ #
4
+ # devenv-check's result job uploads a ci-passed-<tree> marker artifact when
5
+ # lint, test and build all pass. Find the newest live marker for HEAD's tree
6
+ # and download that run's python-dist into dist/. On a pull request run the
7
+ # tree is the merge ref's, which is what a squash merge onto an unchanged
8
+ # base produces, so a main push reuses the dist its PR already tested.
9
+ #
10
+ # Runs from fork pull requests are ignored: a fork controls its own workflow
11
+ # and could upload a forged marker for a tree it can predict.
12
+ #
13
+ # Any failure falls through to a full lint, test and build.
14
+ #
15
+ # Environment: GH_REPO, GH_TOKEN, GITHUB_OUTPUT, GITHUB_RUN_ID
16
+ # Outputs: dist_found=true and source_run_id=<run id> on reuse.
17
+ set -euo pipefail
18
+
19
+ TREE=$(git rev-parse 'HEAD^{tree}')
20
+ MARKER="ci-passed-$TREE"
21
+ CONTINUE="Continue with Lint, Test & Build."
22
+
23
+ if ! ARTIFACTS=$(gh api "repos/$GH_REPO/actions/artifacts?name=$MARKER&per_page=100"); then
24
+ echo "::warning::Could not list artifacts for tree $TREE. $CONTINUE"
25
+ exit 0
26
+ fi
27
+
28
+ RUN_ID=$(jq -r --argjson run "${GITHUB_RUN_ID:-0}" '
29
+ [.artifacts[]
30
+ | select(.expired | not)
31
+ | select(.workflow_run.head_repository_id == .workflow_run.repository_id)
32
+ | select(.workflow_run.id != $run)]
33
+ | max_by(.created_at)
34
+ | .workflow_run.id // empty' <<<"$ARTIFACTS")
35
+
36
+ if [[ -z $RUN_ID ]]; then
37
+ echo "No earlier run passed on tree $TREE. $CONTINUE"
38
+ exit 0
39
+ fi
40
+
41
+ echo "Tree $TREE passed in CI run $RUN_ID"
42
+ if gh run download "$RUN_ID" --name python-dist --dir dist; then
43
+ echo "dist_found=true" >>"$GITHUB_OUTPUT"
44
+ echo "source_run_id=$RUN_ID" >>"$GITHUB_OUTPUT"
45
+ echo "Reusing python-dist from run $RUN_ID"
46
+ else
47
+ echo "Could not download python-dist from run $RUN_ID. $CONTINUE"
48
+ fi
@@ -0,0 +1,519 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Delete stale images and image indexes from a Docker Hub repository.
4
+
5
+ Stale means debris: a manifest no tag reaches, which is what an old digest
6
+ becomes when a tag is overwritten, along with the per-architecture images
7
+ that hung beneath it. Deleting those cannot break a pull.
8
+
9
+ Untagged is not the same as stale, and that difference is the whole job. The
10
+ per-arch images and attestations under a live multi-arch tag carry no tags of
11
+ their own; delete those and the tag breaks. So this works out what every tag
12
+ actually reaches -- the tagged digest, the architectures the tag list names,
13
+ and the attestation manifests only the index itself knows about -- and treats
14
+ that set as untouchable.
15
+
16
+ The repository's manifests come from
17
+ /v2/namespaces/{ns}/repositories/{repo}/manifests, which is what Docker Hub's
18
+ Image Management page reads. It is undocumented: absent from the current Hub
19
+ API spec, its currently_tagged and status parameters are ignored, page numbers
20
+ do nothing, and paging runs off a last_evaluated_key cursor at 100 per page.
21
+
22
+ Deletion goes through the registry API, which independently refuses (403)
23
+ anything still referenced, so every delete gets a second opinion. Registry
24
+ tokens expire after about five minutes, which a long prune outlives, so the
25
+ token is reissued before it lapses and again on any 401.
26
+
27
+ Credentials, the same env vars as the rest of the Docker tooling here:
28
+ DOCKER_USER Docker Hub username
29
+ DOCKER_PASS Docker Hub password or personal access token
30
+ A personal access token needs the "Read, Write, Delete" scope.
31
+
32
+ Dry run by default. Pass --execute to actually delete.
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ import os
38
+ import sys
39
+ import time
40
+ from argparse import ArgumentParser, RawDescriptionHelpFormatter
41
+ from dataclasses import dataclass
42
+ from datetime import datetime, timedelta, timezone
43
+ from http import HTTPStatus
44
+ from typing import TYPE_CHECKING, Any
45
+
46
+ import requests
47
+ from requests.auth import HTTPBasicAuth
48
+
49
+ if TYPE_CHECKING:
50
+ from collections.abc import Iterator
51
+
52
+ # ---------------------------------------------------------------------------
53
+ # Constants
54
+ # ---------------------------------------------------------------------------
55
+
56
+ # Overridable so the test harness can point at a local fake registry.
57
+ HUB_API: str = os.environ.get("HUB_API_URL", "https://hub.docker.com/v2")
58
+ REGISTRY_API: str = os.environ.get(
59
+ "REGISTRY_API_URL", "https://registry-1.docker.io/v2"
60
+ )
61
+ REGISTRY_AUTH: str = os.environ.get("REGISTRY_AUTH_URL", "https://auth.docker.io/token")
62
+
63
+ TIMEOUT: int = 30
64
+ PAGE_SIZE: int = 100 # The listing returns nothing at all above 100.
65
+ MAX_PAGES: int = 500 # A cursor that never ends is a bug, not a big repo.
66
+ TOKEN_LEEWAY: int = 30 # Reissue this many seconds before expiry.
67
+ DEFAULT_TOKEN_LIFETIME: int = 300
68
+ MAX_ATTEMPTS: int = 3
69
+ RETRY_WAIT: int = 2
70
+
71
+ ACCEPT_MANIFESTS: str = (
72
+ "application/vnd.oci.image.index.v1+json"
73
+ ", application/vnd.docker.distribution.manifest.list.v2+json"
74
+ ", application/vnd.oci.image.manifest.v1+json"
75
+ ", application/vnd.docker.distribution.manifest.v2+json"
76
+ )
77
+
78
+ DELETED_STATUSES: frozenset[int] = frozenset(
79
+ (HTTPStatus.OK, HTTPStatus.ACCEPTED, HTTPStatus.NOT_FOUND)
80
+ )
81
+ RETRY_STATUSES: frozenset[int] = frozenset(
82
+ (
83
+ HTTPStatus.TOO_MANY_REQUESTS,
84
+ HTTPStatus.BAD_GATEWAY,
85
+ HTTPStatus.SERVICE_UNAVAILABLE,
86
+ )
87
+ )
88
+
89
+
90
+ class PruneError(RuntimeError):
91
+ """Something went wrong that must stop the prune."""
92
+
93
+
94
+ @dataclass(frozen=True)
95
+ class Manifest:
96
+ """One manifest in a repository, as Docker Hub describes it."""
97
+
98
+ digest: str
99
+ media_type: str
100
+ tags: tuple[str, ...]
101
+ platform: str
102
+ size: int
103
+ last_pushed: str
104
+ last_pulled: str
105
+
106
+ @property
107
+ def is_index(self) -> bool:
108
+ """Whether this is an image index rather than a single image."""
109
+ return "index" in self.media_type or "list" in self.media_type
110
+
111
+ @property
112
+ def kind(self) -> str:
113
+ """Human label for the manifest's type."""
114
+ return "image index" if self.is_index else "image "
115
+
116
+ @classmethod
117
+ def from_hub(cls, data: dict[str, Any]) -> Manifest:
118
+ """Build a manifest from a Hub listing entry."""
119
+ # Every field can arrive as JSON null, hence the empty defaults.
120
+ arch = str(data.get("arch") or "")
121
+ return cls(
122
+ digest=str(data.get("manifest_digest") or ""),
123
+ media_type=str(data.get("media_type") or ""),
124
+ tags=tuple(_tag_names(data.get("tags"))),
125
+ platform=f"{data.get('os') or 'linux'}/{arch}" if arch else "",
126
+ size=int(data.get("total_size") or 0),
127
+ last_pushed=str(data.get("last_pushed") or ""),
128
+ last_pulled=str(data.get("last_pulled") or ""),
129
+ )
130
+
131
+
132
+ def _tag_names(tags: Any) -> Iterator[str]:
133
+ """Read tag names, whether the listing gives strings or objects."""
134
+ for tag in tags or ():
135
+ if isinstance(tag, dict):
136
+ name = tag.get("tag")
137
+ if name:
138
+ yield str(name)
139
+ else:
140
+ yield str(tag)
141
+
142
+
143
+ # ---------------------------------------------------------------------------
144
+ # Docker Hub
145
+ # ---------------------------------------------------------------------------
146
+
147
+
148
+ class HubSession:
149
+ """Authenticated client for the Docker Hub API."""
150
+
151
+ def __init__(self, user: str, secret: str) -> None:
152
+ """Log in and keep the bearer token on the session."""
153
+ self._session = requests.Session()
154
+ response = self._session.post(
155
+ f"{HUB_API}/auth/token",
156
+ json={"identifier": user, "secret": secret},
157
+ timeout=TIMEOUT,
158
+ )
159
+ if not response.ok:
160
+ reason = f"Docker Hub login failed (HTTP {response.status_code})"
161
+ raise PruneError(reason)
162
+ token = response.json().get("access_token")
163
+ if not token:
164
+ reason = "Docker Hub returned no access token"
165
+ raise PruneError(reason)
166
+ self._session.headers["Authorization"] = f"Bearer {token}"
167
+
168
+ def get(self, url: str, params: dict[str, str | int]) -> dict[str, Any]:
169
+ """GET a Hub endpoint that answers with JSON."""
170
+ response = self._session.get(url, params=params, timeout=TIMEOUT)
171
+ if not response.ok:
172
+ reason = f"{url} failed (HTTP {response.status_code})"
173
+ raise PruneError(reason)
174
+ return response.json()
175
+
176
+ def manifests(self, repo: str) -> list[Manifest]:
177
+ """Every manifest in the repository, tagged or not."""
178
+ url = f"{HUB_API}/namespaces/{_namespace(repo)}/repositories/{_name(repo)}/manifests"
179
+ params: dict[str, str | int] = {"page_size": PAGE_SIZE}
180
+ manifests: list[Manifest] = []
181
+ for page in range(MAX_PAGES):
182
+ body = self.get(url, params)
183
+ entries = body.get("manifests") or body.get("results") or []
184
+ manifests.extend(Manifest.from_hub(entry) for entry in entries)
185
+ cursor = body.get("last_evaluated_key")
186
+ # Page numbers are ignored here; the cursor is the only way on.
187
+ if not cursor or len(entries) < PAGE_SIZE:
188
+ return manifests
189
+ params = {"page_size": PAGE_SIZE, "last_evaluated_key": cursor}
190
+ if page == MAX_PAGES - 1:
191
+ reason = (
192
+ f"the manifest listing did not end after {MAX_PAGES} pages; "
193
+ "refusing to work from a partial view of the repository"
194
+ )
195
+ raise PruneError(reason)
196
+ return manifests
197
+
198
+ def tags(self, repo: str) -> Iterator[dict[str, Any]]:
199
+ """Every current tag in the repository."""
200
+ url = f"{HUB_API}/namespaces/{_namespace(repo)}/repositories/{_name(repo)}/tags"
201
+ page = 1
202
+ while True:
203
+ body = self.get(url, {"page_size": PAGE_SIZE, "page": page})
204
+ yield from body.get("results") or ()
205
+ if not body.get("next"):
206
+ return
207
+ page += 1
208
+
209
+
210
+ # ---------------------------------------------------------------------------
211
+ # Registry
212
+ # ---------------------------------------------------------------------------
213
+
214
+
215
+ class RegistrySession:
216
+ """Registry client that keeps its short-lived token fresh."""
217
+
218
+ def __init__(self, repo: str, user: str, secret: str) -> None:
219
+ """Prepare a session; the token is fetched on first use."""
220
+ self._repo = repo
221
+ self._auth = HTTPBasicAuth(user, secret)
222
+ self._session = requests.Session()
223
+ self._expires = datetime.now(tz=timezone.utc)
224
+ self._token = ""
225
+
226
+ def _reissue(self) -> None:
227
+ """Get a fresh pull/delete token for this repository."""
228
+ response = self._session.get(
229
+ REGISTRY_AUTH,
230
+ params={
231
+ "service": "registry.docker.io",
232
+ "scope": f"repository:{self._repo}:pull,delete",
233
+ },
234
+ auth=self._auth,
235
+ timeout=TIMEOUT,
236
+ )
237
+ if not response.ok:
238
+ reason = f"registry auth failed (HTTP {response.status_code})"
239
+ raise PruneError(reason)
240
+ body = response.json()
241
+ self._token = body.get("token") or body.get("access_token") or ""
242
+ if not self._token:
243
+ reason = "registry returned no token"
244
+ raise PruneError(reason)
245
+ lifetime = int(body.get("expires_in") or DEFAULT_TOKEN_LIFETIME)
246
+ self._expires = datetime.now(tz=timezone.utc) + timedelta(
247
+ seconds=max(lifetime - TOKEN_LEEWAY, 1)
248
+ )
249
+
250
+ def _request(self, method: str, digest: str, accept: str = "") -> requests.Response:
251
+ """Call the registry, reissuing the token before and after it lapses."""
252
+ if datetime.now(tz=timezone.utc) >= self._expires:
253
+ self._reissue()
254
+ url = f"{REGISTRY_API}/{self._repo}/manifests/{digest}"
255
+ headers = {"Accept": accept} if accept else {}
256
+ response = requests.Response()
257
+ for attempt in range(MAX_ATTEMPTS):
258
+ headers["Authorization"] = f"Bearer {self._token}"
259
+ response = self._session.request(
260
+ method, url, headers=headers, timeout=TIMEOUT
261
+ )
262
+ if response.status_code == HTTPStatus.UNAUTHORIZED:
263
+ # A long prune outlives its token even with the leeway.
264
+ self._reissue()
265
+ continue
266
+ if response.status_code in RETRY_STATUSES:
267
+ time.sleep(RETRY_WAIT * (attempt + 1))
268
+ continue
269
+ break
270
+ return response
271
+
272
+ def manifest(self, digest: str) -> dict[str, Any] | None:
273
+ """Read the manifest at this digest, or None when it is already gone."""
274
+ response = self._request("GET", digest, accept=ACCEPT_MANIFESTS)
275
+ if response.status_code == HTTPStatus.NOT_FOUND:
276
+ return None
277
+ if not response.ok:
278
+ reason = f"could not read manifest {digest} (HTTP {response.status_code})"
279
+ raise PruneError(reason)
280
+ return response.json()
281
+
282
+ def delete(self, digest: str) -> int:
283
+ """Delete the manifest at this digest, returning the status code."""
284
+ return self._request("DELETE", digest).status_code
285
+
286
+
287
+ # ---------------------------------------------------------------------------
288
+ # The prune
289
+ # ---------------------------------------------------------------------------
290
+
291
+
292
+ def _namespace(repo: str) -> str:
293
+ return repo.split("/", 1)[0]
294
+
295
+
296
+ def _name(repo: str) -> str:
297
+ return repo.split("/", 1)[1]
298
+
299
+
300
+ def _tag_reach(hub: HubSession, repo: str) -> tuple[set[str], set[str]]:
301
+ """Read the tag list: the tagged digests, and the architectures beneath them."""
302
+ tagged: set[str] = set()
303
+ children: set[str] = set()
304
+ for tag in hub.tags(repo):
305
+ digest = tag.get("digest")
306
+ if digest:
307
+ tagged.add(digest)
308
+ # The tag list names each architecture but omits attestations.
309
+ for image in tag.get("images") or ():
310
+ child = image.get("digest")
311
+ if child:
312
+ children.add(child)
313
+ return tagged, children
314
+
315
+
316
+ def live_digests(hub: HubSession, registry: RegistrySession, repo: str) -> set[str]:
317
+ """Every digest a tag reaches, directly or through an index."""
318
+ tagged, live = _tag_reach(hub, repo)
319
+ if not tagged:
320
+ reason = (
321
+ f"{repo} has no tags; refusing to prune a repository whose live set "
322
+ "cannot be established -- every manifest would look stale"
323
+ )
324
+ raise PruneError(reason)
325
+ live |= tagged
326
+ for digest in sorted(tagged):
327
+ index = registry.manifest(digest)
328
+ if index is None:
329
+ reason = (
330
+ f"tagged manifest {digest} is missing from the registry; "
331
+ "aborting rather than mistaking its children for stale"
332
+ )
333
+ raise PruneError(reason)
334
+ live.update(
335
+ child["digest"]
336
+ for child in index.get("manifests") or ()
337
+ if child.get("digest")
338
+ )
339
+ return live
340
+
341
+
342
+ def select_stale(
343
+ manifests: list[Manifest], live: set[str], cutoff: str
344
+ ) -> list[Manifest]:
345
+ """Pick the debris: manifests untagged, unreachable, and old enough."""
346
+ return [
347
+ manifest
348
+ for manifest in manifests
349
+ if not manifest.tags
350
+ and manifest.digest not in live
351
+ and (
352
+ not cutoff
353
+ or (manifest.last_pushed < cutoff and manifest.last_pulled < cutoff)
354
+ )
355
+ ]
356
+
357
+
358
+ def human_size(size: float) -> str:
359
+ """Bytes in units a person reads."""
360
+ for unit in ("B", "KB", "MB", "GB"):
361
+ if size < 1024: # noqa: PLR2004
362
+ return f"{size:.0f} {unit}" if unit == "B" else f"{size:.1f} {unit}"
363
+ size /= 1024
364
+ return f"{size:.1f} TB"
365
+
366
+
367
+ def _day(stamp: str) -> str:
368
+ return stamp[:10] if stamp else "never "
369
+
370
+
371
+ def report(stale: list[Manifest]) -> None:
372
+ """Print what would go, and roughly how much room it frees."""
373
+ total = sum(manifest.size for manifest in stale)
374
+ print(f" {len(stale)} stale manifest(s), up to {human_size(total)} reclaimable:")
375
+ for manifest in stale:
376
+ pushed = _day(manifest.last_pushed)
377
+ pulled = _day(manifest.last_pulled)
378
+ digest = manifest.digest[:19]
379
+ print(
380
+ f" {manifest.kind} {digest} {manifest.platform:<12} pushed={pushed} pulled={pulled}"
381
+ )
382
+
383
+
384
+ def delete_stale(registry: RegistrySession, stale: list[Manifest]) -> int:
385
+ """Delete the debris, indexes first. Returns the number that failed."""
386
+ # While an index stands its children are still referenced, and the
387
+ # registry would rightly refuse to delete them.
388
+ ordered = [m for m in stale if m.is_index] + [m for m in stale if not m.is_index]
389
+ failed = 0
390
+ for number, manifest in enumerate(ordered, start=1):
391
+ status = registry.delete(manifest.digest)
392
+ prefix = f" [{number}/{len(ordered)}]"
393
+ if status in DELETED_STATUSES:
394
+ print(f"{prefix} deleted {manifest.digest[:19]}")
395
+ elif status == HTTPStatus.FORBIDDEN:
396
+ note = "the registry says it is still referenced"
397
+ print(f"{prefix} refused {manifest.digest[:19]}: {note}", file=sys.stderr)
398
+ failed += 1
399
+ else:
400
+ print(
401
+ f"{prefix} FAILED {manifest.digest[:19]}: HTTP {status}",
402
+ file=sys.stderr,
403
+ )
404
+ failed += 1
405
+ return failed
406
+
407
+
408
+ # ---------------------------------------------------------------------------
409
+ # CLI
410
+ # ---------------------------------------------------------------------------
411
+
412
+
413
+ def build_parser() -> ArgumentParser:
414
+ """Build cli arg parser."""
415
+ parser = ArgumentParser(
416
+ description=(
417
+ "Delete the stale images and image indexes from a Docker Hub "
418
+ "repository: the manifests no tag reaches, directly or through an "
419
+ "index. Everything a tag reaches is left alone, including the "
420
+ "per-architecture images and attestations that sit untagged "
421
+ "beneath a multi-arch tag."
422
+ ),
423
+ formatter_class=RawDescriptionHelpFormatter,
424
+ epilog=(
425
+ "Credentials come from DOCKER_USER and DOCKER_PASS. A personal\n"
426
+ "access token needs the Read, Write, Delete scope.\n"
427
+ "Dry run unless --execute is given."
428
+ ),
429
+ )
430
+ parser.add_argument(
431
+ "repository",
432
+ metavar="NAMESPACE/REPOSITORY",
433
+ help="e.g. ajslater/codex. A bare name uses $DOCKER_USER as the namespace.",
434
+ )
435
+ parser.add_argument(
436
+ "--min-age",
437
+ type=int,
438
+ default=0,
439
+ metavar="DAYS",
440
+ help=(
441
+ "Only delete manifests whose last push and last pull are both older "
442
+ "than DAYS. Keeps a push that is still in flight out of the blast radius."
443
+ ),
444
+ )
445
+ parser.add_argument(
446
+ "--execute",
447
+ action="store_true",
448
+ help="Actually delete. Without it, only report.",
449
+ )
450
+ return parser
451
+
452
+
453
+ def _credentials() -> tuple[str, str]:
454
+ user = os.environ.get("DOCKER_USER", "")
455
+ secret = os.environ.get("DOCKER_PASS", "")
456
+ if not user or not secret:
457
+ reason = "DOCKER_USER and DOCKER_PASS must be set"
458
+ raise PruneError(reason)
459
+ return user, secret
460
+
461
+
462
+ def prune(repo: str, min_age: int, *, execute: bool) -> int:
463
+ """Find the stale manifests and, when told to, delete them."""
464
+ user, secret = _credentials()
465
+ if "/" not in repo:
466
+ repo = f"{user}/{repo}"
467
+ print(f"[{'EXECUTE' if execute else 'DRY RUN'}] {repo}")
468
+
469
+ hub = HubSession(user, secret)
470
+ registry = RegistrySession(repo, user, secret)
471
+ manifests = hub.manifests(repo)
472
+ if not manifests:
473
+ print(" Repository holds no manifests. Nothing to do.")
474
+ return 0
475
+ live = live_digests(hub, registry, repo)
476
+
477
+ cutoff = ""
478
+ if min_age:
479
+ moment = datetime.now(tz=timezone.utc) - timedelta(days=min_age)
480
+ cutoff = moment.strftime("%Y-%m-%dT%H:%M:%SZ")
481
+ print(f" Age filter: last push and last pull both before {cutoff}")
482
+
483
+ print(
484
+ f" {len(manifests)} manifest(s) in the repository, {len(live)} reachable from a tag."
485
+ )
486
+ stale = select_stale(manifests, live, cutoff)
487
+ if not stale:
488
+ print(" No stale manifests. Nothing to delete.")
489
+ return 0
490
+ report(stale)
491
+
492
+ if not execute:
493
+ print("\nDry run only. Re-run with --execute to delete.")
494
+ return 0
495
+
496
+ print(" Deleting...")
497
+ failed = delete_stale(registry, stale)
498
+ print(f" Deleted {len(stale) - failed} manifest(s).")
499
+ if failed:
500
+ print(f" {failed} deletion(s) did not succeed.", file=sys.stderr)
501
+ return 1
502
+ return 0
503
+
504
+
505
+ def main() -> None:
506
+ """Run program."""
507
+ args = build_parser().parse_args()
508
+ try:
509
+ sys.exit(prune(args.repository, args.min_age, execute=args.execute))
510
+ except PruneError as exc:
511
+ print(f"error: {exc}", file=sys.stderr)
512
+ sys.exit(1)
513
+ except KeyboardInterrupt:
514
+ print("\ninterrupted", file=sys.stderr)
515
+ sys.exit(130)
516
+
517
+
518
+ if __name__ == "__main__":
519
+ main()
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env bash
2
+ # Lint checks for ci
3
+ set -euxo pipefail
4
+
5
+ if [ "$(uname)" != "Darwin" ]; then
6
+ exit 0
7
+ fi
8
+
9
+ # Every workflow, so ci.yml's calls into the devenv-* workflows are checked.
10
+ if [ -d .github/workflows ]; then
11
+ actionlint
12
+ fi
@@ -8,6 +8,6 @@ set -euxo pipefail
8
8
  uv run --group lint ruff check .
9
9
  uv run --group lint ruff format --check .
10
10
  make typecheck
11
- uv run --group lint vulture .
11
+ bin/lint-vulture.sh
12
12
  bin/lint-complexity.sh
13
13
  uv run --group lint codespell .
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env bash
2
+ # Lint dead code with vulture
3
+ set -euo pipefail
4
+
5
+ # vulture fnmatches its excludes against absolute paths, so no pattern can skip
6
+ # the root's dot-dirs (.venv, .claude/worktrees/) without also skipping every
7
+ # file of a checkout under one. Pass the root's other entries instead of ".".
8
+ find . -mindepth 1 -maxdepth 1 ! -name '.*' \( -type d -o -type f -name '*.py' \) \
9
+ -exec uv run --group lint vulture {} +