OperonDBS 0.8.3__tar.gz → 0.8.4__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 (302) hide show
  1. {operondbs-0.8.3 → operondbs-0.8.4}/.github/workflows/publish.yml +42 -1
  2. {operondbs-0.8.3 → operondbs-0.8.4}/AGENTS.md +21 -9
  3. {operondbs-0.8.3 → operondbs-0.8.4}/OperonDBS.egg-info/PKG-INFO +1 -1
  4. {operondbs-0.8.3 → operondbs-0.8.4}/PKG-INFO +1 -1
  5. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/contributor/development-testing.md +22 -9
  6. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/contributor/pypi-release.md +21 -3
  7. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/contributor/development-testing.md +19 -9
  8. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/contributor/pypi-release.md +17 -3
  9. {operondbs-0.8.3 → operondbs-0.8.4}/pyproject.toml +1 -1
  10. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_docs_projects.py +38 -0
  11. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_tui_config.py +54 -26
  12. {operondbs-0.8.3 → operondbs-0.8.4}/.github/workflows/test.yml +0 -0
  13. {operondbs-0.8.3 → operondbs-0.8.4}/.gitignore +0 -0
  14. {operondbs-0.8.3 → operondbs-0.8.4}/LICENSE +0 -0
  15. {operondbs-0.8.3 → operondbs-0.8.4}/MANIFEST.in +0 -0
  16. {operondbs-0.8.3 → operondbs-0.8.4}/OperonDBS.egg-info/SOURCES.txt +0 -0
  17. {operondbs-0.8.3 → operondbs-0.8.4}/OperonDBS.egg-info/dependency_links.txt +0 -0
  18. {operondbs-0.8.3 → operondbs-0.8.4}/OperonDBS.egg-info/entry_points.txt +0 -0
  19. {operondbs-0.8.3 → operondbs-0.8.4}/OperonDBS.egg-info/requires.txt +0 -0
  20. {operondbs-0.8.3 → operondbs-0.8.4}/OperonDBS.egg-info/top_level.txt +0 -0
  21. {operondbs-0.8.3 → operondbs-0.8.4}/README.md +0 -0
  22. {operondbs-0.8.3 → operondbs-0.8.4}/README_ZH.md +0 -0
  23. {operondbs-0.8.3 → operondbs-0.8.4}/benchmarks/qc_representative_entities.tsv +0 -0
  24. {operondbs-0.8.3 → operondbs-0.8.4}/docs/conf_common.py +0 -0
  25. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/.readthedocs.yaml +0 -0
  26. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/architecture/extensibility.md +0 -0
  27. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/architecture/external-analysis.md +0 -0
  28. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/architecture/files-and-storage.md +0 -0
  29. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/architecture/index.md +0 -0
  30. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/architecture/metadata-and-data-model.md +0 -0
  31. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/architecture/overview.md +0 -0
  32. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/architecture/qc-and-rules.md +0 -0
  33. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/architecture/release-lifecycle.md +0 -0
  34. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/architecture/taxonomy-coverage.md +0 -0
  35. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/conf.py +0 -0
  36. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/contributor/defect-tracking.md +0 -0
  37. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/contributor/documentation-deployment.md +0 -0
  38. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/contributor/index.md +0 -0
  39. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/contributor/repository-guide.md +0 -0
  40. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/getting-started/daily-workflow.md +0 -0
  41. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/getting-started/first-project.md +0 -0
  42. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/getting-started/index.md +0 -0
  43. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/getting-started/installation.md +0 -0
  44. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/getting-started/quickstart.md +0 -0
  45. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/guides/backup-migration.md +0 -0
  46. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/guides/curation-lifecycle.md +0 -0
  47. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/guides/external-analysis.md +0 -0
  48. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/guides/file-archiving.md +0 -0
  49. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/guides/index.md +0 -0
  50. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/guides/metadata-import.md +0 -0
  51. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/guides/ncbi-datasets.md +0 -0
  52. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/guides/phylogenetics-workflow.md +0 -0
  53. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/guides/qc-profiles.md +0 -0
  54. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/guides/remote-execution.md +0 -0
  55. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/guides/remote-first.md +0 -0
  56. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/guides/remote-storage.md +0 -0
  57. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/guides/taxonomy-coverage.md +0 -0
  58. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/guides/troubleshooting.md +0 -0
  59. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/index.md +0 -0
  60. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/operations/database-compatibility.md +0 -0
  61. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/operations/index.md +0 -0
  62. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/operations/ncbi-recovery-migration.md +0 -0
  63. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/operations/qc-performance.md +0 -0
  64. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/overview.md +0 -0
  65. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/reference/behaviors-and-limitations.md +0 -0
  66. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/reference/cli-analysis.md +0 -0
  67. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/reference/cli-decisions-reports.md +0 -0
  68. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/reference/cli-files-qc.md +0 -0
  69. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/reference/cli-project-metadata.md +0 -0
  70. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/reference/cli-remote.md +0 -0
  71. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/reference/cli-taxonomy-lifecycle-admin.md +0 -0
  72. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/reference/cli-tui.md +0 -0
  73. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/reference/cli-workflow.md +0 -0
  74. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/reference/data-model.md +0 -0
  75. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/reference/index.md +0 -0
  76. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/reference/recipe-fields.md +0 -0
  77. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/reference/recipe-overview.md +0 -0
  78. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/reference/recipe-parsers-examples.md +0 -0
  79. {operondbs-0.8.3 → operondbs-0.8.4}/docs/en/reference/resolved-issues.md +0 -0
  80. {operondbs-0.8.3 → operondbs-0.8.4}/docs/locales/README.md +0 -0
  81. {operondbs-0.8.3 → operondbs-0.8.4}/docs/requirements.txt +0 -0
  82. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/.readthedocs.yaml +0 -0
  83. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/architecture/extensibility.md +0 -0
  84. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/architecture/external-analysis.md +0 -0
  85. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/architecture/files-and-storage.md +0 -0
  86. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/architecture/index.md +0 -0
  87. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/architecture/metadata-and-data-model.md +0 -0
  88. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/architecture/overview.md +0 -0
  89. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/architecture/qc-and-rules.md +0 -0
  90. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/architecture/release-lifecycle.md +0 -0
  91. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/architecture/taxonomy-coverage.md +0 -0
  92. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/conf.py +0 -0
  93. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/contributor/defect-tracking.md +0 -0
  94. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/contributor/documentation-deployment.md +0 -0
  95. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/contributor/index.md +0 -0
  96. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/contributor/repository-guide.md +0 -0
  97. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/getting-started/daily-workflow.md +0 -0
  98. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/getting-started/first-project.md +0 -0
  99. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/getting-started/index.md +0 -0
  100. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/getting-started/installation.md +0 -0
  101. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/getting-started/quickstart.md +0 -0
  102. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/guides/backup-migration.md +0 -0
  103. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/guides/curation-lifecycle.md +0 -0
  104. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/guides/external-analysis.md +0 -0
  105. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/guides/file-archiving.md +0 -0
  106. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/guides/index.md +0 -0
  107. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/guides/metadata-import.md +0 -0
  108. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/guides/ncbi-datasets.md +0 -0
  109. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/guides/phylogenetics-workflow.md +0 -0
  110. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/guides/qc-profiles.md +0 -0
  111. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/guides/remote-execution.md +0 -0
  112. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/guides/remote-first.md +0 -0
  113. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/guides/remote-storage.md +0 -0
  114. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/guides/taxonomy-coverage.md +0 -0
  115. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/guides/troubleshooting.md +0 -0
  116. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/index.md +0 -0
  117. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/operations/database-compatibility.md +0 -0
  118. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/operations/index.md +0 -0
  119. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/operations/ncbi-recovery-migration.md +0 -0
  120. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/operations/qc-performance.md +0 -0
  121. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/overview.md +0 -0
  122. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/reference/behaviors-and-limitations.md +0 -0
  123. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/reference/cli-analysis.md +0 -0
  124. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/reference/cli-decisions-reports.md +0 -0
  125. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/reference/cli-files-qc.md +0 -0
  126. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/reference/cli-project-metadata.md +0 -0
  127. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/reference/cli-remote.md +0 -0
  128. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/reference/cli-taxonomy-lifecycle-admin.md +0 -0
  129. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/reference/cli-tui.md +0 -0
  130. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/reference/cli-workflow.md +0 -0
  131. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/reference/data-model.md +0 -0
  132. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/reference/index.md +0 -0
  133. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/reference/recipe-fields.md +0 -0
  134. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/reference/recipe-overview.md +0 -0
  135. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/reference/recipe-parsers-examples.md +0 -0
  136. {operondbs-0.8.3 → operondbs-0.8.4}/docs/zh/reference/resolved-issues.md +0 -0
  137. {operondbs-0.8.3 → operondbs-0.8.4}/operon/__init__.py +0 -0
  138. {operondbs-0.8.3 → operondbs-0.8.4}/operon/__main__.py +0 -0
  139. {operondbs-0.8.3 → operondbs-0.8.4}/operon/adapters/__init__.py +0 -0
  140. {operondbs-0.8.3 → operondbs-0.8.4}/operon/adapters/ncbi_datasets.py +0 -0
  141. {operondbs-0.8.3 → operondbs-0.8.4}/operon/adapters/timetree.py +0 -0
  142. {operondbs-0.8.3 → operondbs-0.8.4}/operon/backup.py +0 -0
  143. {operondbs-0.8.3 → operondbs-0.8.4}/operon/classify.py +0 -0
  144. {operondbs-0.8.3 → operondbs-0.8.4}/operon/cli.py +0 -0
  145. {operondbs-0.8.3 → operondbs-0.8.4}/operon/config.py +0 -0
  146. {operondbs-0.8.3 → operondbs-0.8.4}/operon/coverage.py +0 -0
  147. {operondbs-0.8.3 → operondbs-0.8.4}/operon/database.py +0 -0
  148. {operondbs-0.8.3 → operondbs-0.8.4}/operon/demo.py +0 -0
  149. {operondbs-0.8.3 → operondbs-0.8.4}/operon/entity_view.py +0 -0
  150. {operondbs-0.8.3 → operondbs-0.8.4}/operon/environment.py +0 -0
  151. {operondbs-0.8.3 → operondbs-0.8.4}/operon/environment_capture.py +0 -0
  152. {operondbs-0.8.3 → operondbs-0.8.4}/operon/errors.py +0 -0
  153. {operondbs-0.8.3 → operondbs-0.8.4}/operon/execution.py +0 -0
  154. {operondbs-0.8.3 → operondbs-0.8.4}/operon/export.py +0 -0
  155. {operondbs-0.8.3 → operondbs-0.8.4}/operon/fanout.py +0 -0
  156. {operondbs-0.8.3 → operondbs-0.8.4}/operon/files.py +0 -0
  157. {operondbs-0.8.3 → operondbs-0.8.4}/operon/import_wizard.py +0 -0
  158. {operondbs-0.8.3 → operondbs-0.8.4}/operon/lifecycle.py +0 -0
  159. {operondbs-0.8.3 → operondbs-0.8.4}/operon/lineage.py +0 -0
  160. {operondbs-0.8.3 → operondbs-0.8.4}/operon/metadata_files.py +0 -0
  161. {operondbs-0.8.3 → operondbs-0.8.4}/operon/ncbi_reconcile.py +0 -0
  162. {operondbs-0.8.3 → operondbs-0.8.4}/operon/profiles.py +0 -0
  163. {operondbs-0.8.3 → operondbs-0.8.4}/operon/qc/__init__.py +0 -0
  164. {operondbs-0.8.3 → operondbs-0.8.4}/operon/qc/_alignment.pyx +0 -0
  165. {operondbs-0.8.3 → operondbs-0.8.4}/operon/qc/_parsers.pyx +0 -0
  166. {operondbs-0.8.3 → operondbs-0.8.4}/operon/qc/alignment.py +0 -0
  167. {operondbs-0.8.3 → operondbs-0.8.4}/operon/qc/measure.py +0 -0
  168. {operondbs-0.8.3 → operondbs-0.8.4}/operon/qc/parsers.py +0 -0
  169. {operondbs-0.8.3 → operondbs-0.8.4}/operon/release.py +0 -0
  170. {operondbs-0.8.3 → operondbs-0.8.4}/operon/remotes.py +0 -0
  171. {operondbs-0.8.3 → operondbs-0.8.4}/operon/reports.py +0 -0
  172. {operondbs-0.8.3 → operondbs-0.8.4}/operon/rules.py +0 -0
  173. {operondbs-0.8.3 → operondbs-0.8.4}/operon/schema.py +0 -0
  174. {operondbs-0.8.3 → operondbs-0.8.4}/operon/sequence_tools.py +0 -0
  175. {operondbs-0.8.3 → operondbs-0.8.4}/operon/shutdown.py +0 -0
  176. {operondbs-0.8.3 → operondbs-0.8.4}/operon/sql.py +0 -0
  177. {operondbs-0.8.3 → operondbs-0.8.4}/operon/table_import.py +0 -0
  178. {operondbs-0.8.3 → operondbs-0.8.4}/operon/taxonomy.py +0 -0
  179. {operondbs-0.8.3 → operondbs-0.8.4}/operon/timetree.py +0 -0
  180. {operondbs-0.8.3 → operondbs-0.8.4}/operon/tools.py +0 -0
  181. {operondbs-0.8.3 → operondbs-0.8.4}/operon/tui/__init__.py +0 -0
  182. {operondbs-0.8.3 → operondbs-0.8.4}/operon/tui/actions.py +0 -0
  183. {operondbs-0.8.3 → operondbs-0.8.4}/operon/tui/app.py +0 -0
  184. {operondbs-0.8.3 → operondbs-0.8.4}/operon/tui/app.tcss +0 -0
  185. {operondbs-0.8.3 → operondbs-0.8.4}/operon/tui/assets/splash.png +0 -0
  186. {operondbs-0.8.3 → operondbs-0.8.4}/operon/tui/assets/splash.rgb.z +0 -0
  187. {operondbs-0.8.3 → operondbs-0.8.4}/operon/tui/data.py +0 -0
  188. {operondbs-0.8.3 → operondbs-0.8.4}/operon/tui/parity.py +0 -0
  189. {operondbs-0.8.3 → operondbs-0.8.4}/operon/tui/screens/__init__.py +0 -0
  190. {operondbs-0.8.3 → operondbs-0.8.4}/operon/tui/screens/analyze.py +0 -0
  191. {operondbs-0.8.3 → operondbs-0.8.4}/operon/tui/screens/classify.py +0 -0
  192. {operondbs-0.8.3 → operondbs-0.8.4}/operon/tui/screens/common.py +0 -0
  193. {operondbs-0.8.3 → operondbs-0.8.4}/operon/tui/screens/config.py +0 -0
  194. {operondbs-0.8.3 → operondbs-0.8.4}/operon/tui/screens/config_classification.py +0 -0
  195. {operondbs-0.8.3 → operondbs-0.8.4}/operon/tui/screens/coverage.py +0 -0
  196. {operondbs-0.8.3 → operondbs-0.8.4}/operon/tui/screens/decisions.py +0 -0
  197. {operondbs-0.8.3 → operondbs-0.8.4}/operon/tui/screens/derived_ops.py +0 -0
  198. {operondbs-0.8.3 → operondbs-0.8.4}/operon/tui/screens/entities.py +0 -0
  199. {operondbs-0.8.3 → operondbs-0.8.4}/operon/tui/screens/environments.py +0 -0
  200. {operondbs-0.8.3 → operondbs-0.8.4}/operon/tui/screens/files.py +0 -0
  201. {operondbs-0.8.3 → operondbs-0.8.4}/operon/tui/screens/files_ops.py +0 -0
  202. {operondbs-0.8.3 → operondbs-0.8.4}/operon/tui/screens/hits.py +0 -0
  203. {operondbs-0.8.3 → operondbs-0.8.4}/operon/tui/screens/home.py +0 -0
  204. {operondbs-0.8.3 → operondbs-0.8.4}/operon/tui/screens/import_wizard.py +0 -0
  205. {operondbs-0.8.3 → operondbs-0.8.4}/operon/tui/screens/labels.py +0 -0
  206. {operondbs-0.8.3 → operondbs-0.8.4}/operon/tui/screens/publish.py +0 -0
  207. {operondbs-0.8.3 → operondbs-0.8.4}/operon/tui/screens/run_external.py +0 -0
  208. {operondbs-0.8.3 → operondbs-0.8.4}/operon/tui/screens/runs.py +0 -0
  209. {operondbs-0.8.3 → operondbs-0.8.4}/operon/tui/splash.py +0 -0
  210. {operondbs-0.8.3 → operondbs-0.8.4}/operon/tui/splash_terminal.py +0 -0
  211. {operondbs-0.8.3 → operondbs-0.8.4}/operon/utils.py +0 -0
  212. {operondbs-0.8.3 → operondbs-0.8.4}/operon/workflow.py +0 -0
  213. {operondbs-0.8.3 → operondbs-0.8.4}/setup.cfg +0 -0
  214. {operondbs-0.8.3 → operondbs-0.8.4}/setup.py +0 -0
  215. {operondbs-0.8.3 → operondbs-0.8.4}/tests/__init__.py +0 -0
  216. {operondbs-0.8.3 → operondbs-0.8.4}/tests/compatibility/__init__.py +0 -0
  217. {operondbs-0.8.3 → operondbs-0.8.4}/tests/compatibility/test_python_support.py +0 -0
  218. {operondbs-0.8.3 → operondbs-0.8.4}/tests/helpers.py +0 -0
  219. {operondbs-0.8.3 → operondbs-0.8.4}/tests/integration/__init__.py +0 -0
  220. {operondbs-0.8.3 → operondbs-0.8.4}/tests/integration/test_analysis_array.py +0 -0
  221. {operondbs-0.8.3 → operondbs-0.8.4}/tests/integration/test_analysis_resume.py +0 -0
  222. {operondbs-0.8.3 → operondbs-0.8.4}/tests/integration/test_analysis_shutdown.py +0 -0
  223. {operondbs-0.8.3 → operondbs-0.8.4}/tests/integration/test_analysis_tools.py +0 -0
  224. {operondbs-0.8.3 → operondbs-0.8.4}/tests/integration/test_classify_sequences.py +0 -0
  225. {operondbs-0.8.3 → operondbs-0.8.4}/tests/integration/test_execution_backends.py +0 -0
  226. {operondbs-0.8.3 → operondbs-0.8.4}/tests/integration/test_fanout_analyze.py +0 -0
  227. {operondbs-0.8.3 → operondbs-0.8.4}/tests/integration/test_lineage_cascade.py +0 -0
  228. {operondbs-0.8.3 → operondbs-0.8.4}/tests/integration/test_ncbi_datasets_adapter.py +0 -0
  229. {operondbs-0.8.3 → operondbs-0.8.4}/tests/integration/test_pipeline_and_release.py +0 -0
  230. {operondbs-0.8.3 → operondbs-0.8.4}/tests/integration/test_python_packaging.py +0 -0
  231. {operondbs-0.8.3 → operondbs-0.8.4}/tests/integration/test_qc_remote_measure.py +0 -0
  232. {operondbs-0.8.3 → operondbs-0.8.4}/tests/integration/test_taxonomy_coverage.py +0 -0
  233. {operondbs-0.8.3 → operondbs-0.8.4}/tests/regression/__init__.py +0 -0
  234. {operondbs-0.8.3 → operondbs-0.8.4}/tests/regression/test_correctness.py +0 -0
  235. {operondbs-0.8.3 → operondbs-0.8.4}/tests/regression/test_cython_alignment_parity.py +0 -0
  236. {operondbs-0.8.3 → operondbs-0.8.4}/tests/regression/test_cython_parser_parity.py +0 -0
  237. {operondbs-0.8.3 → operondbs-0.8.4}/tests/regression/test_parser_semantics.py +0 -0
  238. {operondbs-0.8.3 → operondbs-0.8.4}/tests/regression/test_sql_identifier_security.py +0 -0
  239. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/__init__.py +0 -0
  240. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_alignment_qc.py +0 -0
  241. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_classify.py +0 -0
  242. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_cli_edges.py +0 -0
  243. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_config_workflow_edges.py +0 -0
  244. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_core_coverage_more.py +0 -0
  245. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_coverage_edges.py +0 -0
  246. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_database_edges_more.py +0 -0
  247. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_defect_registry.py +0 -0
  248. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_defects_cli.py +0 -0
  249. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_docs_versions.py +0 -0
  250. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_environment.py +0 -0
  251. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_environment_capture.py +0 -0
  252. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_execution.py +0 -0
  253. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_execution_edges.py +0 -0
  254. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_export.py +0 -0
  255. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_fanout.py +0 -0
  256. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_files_edges.py +0 -0
  257. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_follow.py +0 -0
  258. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_import_backup_show.py +0 -0
  259. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_import_wizard_edges.py +0 -0
  260. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_lifecycle.py +0 -0
  261. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_lineage.py +0 -0
  262. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_misc_coverage_more.py +0 -0
  263. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_ncbi_coverage_gaps.py +0 -0
  264. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_ncbi_edge_cases.py +0 -0
  265. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_ncbi_reconcile_edges.py +0 -0
  266. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_parser_edge_paths.py +0 -0
  267. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_qc_and_rules.py +0 -0
  268. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_qc_edges_more.py +0 -0
  269. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_qc_measure.py +0 -0
  270. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_qc_remote_only.py +0 -0
  271. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_recipe_history.py +0 -0
  272. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_remote_taxonomy_more.py +0 -0
  273. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_remote_transport_edges.py +0 -0
  274. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_remotes.py +0 -0
  275. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_remotes_edges.py +0 -0
  276. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_rules_schema_edges.py +0 -0
  277. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_schema_2_10.py +0 -0
  278. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_schema_2_11.py +0 -0
  279. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_schema_2_9.py +0 -0
  280. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_schema_and_metadata.py +0 -0
  281. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_sequence_tools.py +0 -0
  282. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_sequences.py +0 -0
  283. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_shutdown.py +0 -0
  284. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_support_edges.py +0 -0
  285. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_table_import_edges.py +0 -0
  286. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_taxonomy_edges.py +0 -0
  287. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_timetree.py +0 -0
  288. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_timetree_snapshots.py +0 -0
  289. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_tools_edges.py +0 -0
  290. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_tools_execution_more.py +0 -0
  291. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_tui.py +0 -0
  292. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_tui_analyze.py +0 -0
  293. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_tui_cli_parity.py +0 -0
  294. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_tui_derived_ops.py +0 -0
  295. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_tui_flows_more.py +0 -0
  296. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_tui_m3_loop.py +0 -0
  297. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_tui_publish.py +0 -0
  298. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_tui_screens_more.py +0 -0
  299. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_tui_splash_terminal.py +0 -0
  300. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_tui_writes.py +0 -0
  301. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_views_release_reports_edges.py +0 -0
  302. {operondbs-0.8.3 → operondbs-0.8.4}/tests/unit/test_workflow_cli.py +0 -0
@@ -8,7 +8,46 @@ permissions:
8
8
  contents: read
9
9
 
10
10
  jobs:
11
+ verify-release:
12
+ runs-on: ubuntu-latest
13
+ permissions:
14
+ contents: read
15
+ actions: read
16
+ steps:
17
+ - uses: actions/checkout@v6
18
+ with:
19
+ ref: ${{ github.event.release.tag_name }}
20
+ fetch-depth: 0
21
+ persist-credentials: false
22
+ - uses: actions/setup-python@v7
23
+ with:
24
+ python-version: "3.12"
25
+ - run: python -m pip install --upgrade pip pyyaml
26
+ - name: The tagged commit passed its own CI run
27
+ env:
28
+ GH_TOKEN: ${{ github.token }}
29
+ RELEASE_TAG: ${{ github.event.release.tag_name }}
30
+ run: |
31
+ sha=$(git rev-parse HEAD)
32
+ conclusion=$(gh api \
33
+ "repos/${{ github.repository }}/actions/workflows/test.yml/runs?head_sha=$sha&per_page=1" \
34
+ --jq '.workflow_runs[0].conclusion // "no run"')
35
+ echo "$RELEASE_TAG -> $sha: test.yml concluded '$conclusion'"
36
+ if [ "$conclusion" != "success" ]; then
37
+ echo "::error::test.yml on $sha concluded '$conclusion'; rerun it (gh run rerun) before releasing"
38
+ exit 1
39
+ fi
40
+ - name: Import the maintainer's published GPG key
41
+ run: |
42
+ curl -sSL "https://github.com/${{ github.repository_owner }}.gpg" | gpg --import
43
+ gpg --list-keys --with-colons | grep '^pub' | wc -l
44
+ - name: Release preflight
45
+ env:
46
+ RELEASE_TAG: ${{ github.event.release.tag_name }}
47
+ run: scripts/release-preflight.sh --ci --tag "$RELEASE_TAG"
48
+
11
49
  source-distribution:
50
+ needs: verify-release
12
51
  runs-on: ubuntu-latest
13
52
  steps:
14
53
  - uses: actions/checkout@v6
@@ -35,6 +74,7 @@ jobs:
35
74
  if-no-files-found: error
36
75
 
37
76
  documentation:
77
+ needs: verify-release
38
78
  runs-on: ubuntu-latest
39
79
  steps:
40
80
  - uses: actions/checkout@v6
@@ -48,6 +88,7 @@ jobs:
48
88
  sphinx-build -W --keep-going -b html docs/zh docs/_build/zh/html
49
89
 
50
90
  wheels:
91
+ needs: verify-release
51
92
  strategy:
52
93
  fail-fast: false
53
94
  matrix:
@@ -86,7 +127,7 @@ jobs:
86
127
  if-no-files-found: error
87
128
 
88
129
  pypi-publish:
89
- needs: [source-distribution, wheels]
130
+ needs: [verify-release, source-distribution, wheels]
90
131
  runs-on: ubuntu-latest
91
132
  environment:
92
133
  name: pypi
@@ -26,7 +26,7 @@ principle-to-implementation mapping.
26
26
 
27
27
  Current version markers (must stay consistent across code and docs):
28
28
 
29
- - `operon` 0.8.3 (`pyproject.toml`)
29
+ - `operon` 0.8.4 (`pyproject.toml`)
30
30
  - database schema 2.11 (`operon/database.py`, `SCHEMA_VERSION`)
31
31
  - metadata schema 1.4 (`operon/schema.py`, `METADATA_SCHEMA_VERSION`)
32
32
 
@@ -171,10 +171,11 @@ The project is licensed AGPL-3.0-or-later (`LICENSE` at the repo root).
171
171
  - `benchmarks/` — representative entity sets for QC performance diagnostics
172
172
  (see `docs/*/operations/qc-performance.md`).
173
173
  - `scripts/` — local developer tooling; `setup-test-matrix.sh` and
174
- `run-test-matrix.sh` build and drive the uv-managed Python 3.10–3.14
174
+ `run-test-matrix.sh` build and drive the uv-managed Python 3.10–3.15
175
175
  matrix under .matrix/ (see `docs/*/contributor/development-testing.md`);
176
- `defects.sh` appends to and queries the defect registry (`defects.yml`,
177
- see "Defect reports" below).
176
+ `release-preflight.sh` is the release gate and the script the publish
177
+ workflow runs on the tag; `defects.sh` appends to and queries the defect
178
+ registry (`defects.yml`, see "Defect reports" below).
178
179
 
179
180
  ## Setup, test, and build
180
181
 
@@ -193,7 +194,13 @@ python -m pytest tests/unit # by category: unit / integration /
193
194
 
194
195
  python -m pytest --lf -q --no-cov # iterate: last failures, no coverage
195
196
  python -m pytest -n 4 --dist loadfile # parallel full suite (xdist, in the dev/test extras)
196
- scripts/run-test-matrix.sh # whole suite on 3.10-3.15 concurrently
197
+ scripts/setup-test-matrix.sh # once per machine: uv-managed 3.10-3.15 venvs
198
+ scripts/run-test-matrix.sh # whole suite on 3.10-3.15, in waves
199
+ # (MATRIX_CONCURRENCY / MATRIX_JOBS override the split)
200
+ scripts/release-preflight.sh # the release gate: version, full suite, both doc
201
+ # trees, defect registry, matrix evidence for HEAD;
202
+ # --run-matrix runs the matrix inside the gate, and
203
+ # --tag vX.Y.Z asserts the tag's name/signature/target
197
204
 
198
205
  python setup.py build_ext --inplace # rebuild only the Cython extension
199
206
 
@@ -205,14 +212,19 @@ Run the relevant test category after any change; run the full suite before
205
212
  considering work done.
206
213
 
207
214
  CI (`.github/workflows/test.yml`) runs pytest on Python 3.10–3.15 and the strict
208
- Sphinx build. Releases are published exclusively to PyPI; see `docs/*/contributor/pypi-release.md`.
215
+ Sphinx build. Releases are published exclusively to PyPI, and
216
+ `.github/workflows/publish.yml` builds nothing and uploads nothing until its
217
+ `verify-release` job has seen a `success` conclusion for the tagged commit's `test`
218
+ run *and* a passing `scripts/release-preflight.sh --ci --tag <tag>`; see
219
+ `docs/*/contributor/pypi-release.md`.
209
220
 
210
221
  The project uses `pytest-xdist` to parallel testing. Avoid sharing state between tests
211
222
  to prevent unexpected or random test results.
212
223
 
213
224
  > Running `python -m pytest` directly takes approx 35 seconds
214
- > (auto: 24 workers). Running the local test matrix takes 3 minutes
215
- > (6 jobs × 4 workers), or 4~5min (6 jobs × 2 workers in default).
225
+ > (auto: 24 workers). Running the local test matrix takes about 5 minutes
226
+ > (6 versions, 3 at a time × 7 workers by default; `MATRIX_CONCURRENCY` /
227
+ > `MATRIX_JOBS` override the split).
216
228
  >
217
229
  > Measured on Intel Core i7-13700HX (16c24t).
218
230
 
@@ -303,7 +315,7 @@ change:
303
315
  `docs/<language>/.readthedocs.yaml`, and the registry in
304
316
  `tests/unit/test_docs_projects.py`
305
317
 
306
- Version markers in docs (`operon` 0.8.3, database schema 2.11, metadata
318
+ Version markers in docs (`operon` 0.8.4, database schema 2.11, metadata
307
319
  schema 1.4) must match `pyproject.toml` and the code. Do not write the
308
320
  current values literally in Markdown sources: use the `myst_substitutions`
309
321
  references `{{ operon_version }}`, `{{ db_schema }}`, and
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: OperonDBS
3
- Version: 0.8.3
3
+ Version: 0.8.4
4
4
  Summary: Operon the Database System: Archive, Quality-Control, Organize, Analyze and Release Your Bio-Data
5
5
  Author-email: hyli360 <lihuanyu2003@gmail.com>
6
6
  License-Expression: AGPL-3.0-or-later
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: OperonDBS
3
- Version: 0.8.3
3
+ Version: 0.8.4
4
4
  Summary: Operon the Database System: Archive, Quality-Control, Organize, Analyze and Release Your Bio-Data
5
5
  Author-email: hyli360 <lihuanyu2003@gmail.com>
6
6
  License-Expression: AGPL-3.0-or-later
@@ -14,7 +14,8 @@ python -m pytest tests/integration
14
14
  python -m pytest tests/regression tests/compatibility
15
15
 
16
16
  # build the Sphinx documentation strictly
17
- sphinx-build -W --keep-going -b html docs docs/_build/html
17
+ sphinx-build -W --keep-going -b html docs/en docs/_build/en/html
18
+ sphinx-build -W --keep-going -b html docs/zh docs/_build/zh/html
18
19
  ```
19
20
 
20
21
  The pytest suite is organized into four categories — `unit`, `integration`, `regression`, `compatibility` — covering:
@@ -67,18 +68,30 @@ The full suite takes about seven minutes serially; the loop below aims to run it
67
68
  is assigned to only one worker, which is required for Textual UI testing.
68
69
  Coverages are the most resource-intensive: use `--no-cov` during iterations and run the test only once at the end.
69
70
  4. **Cross-version matrix in one pass.** `scripts/setup-test-matrix.sh` creates uv-managed CPython 3.10-3.15
70
- interpreters plus one venv per version inside `.matrix/`. `scripts/run-test-matrix.sh` runs all six concurrently:
71
+ interpreters plus one venv per version inside `.matrix/`. `scripts/run-test-matrix.sh` runs them
72
+ `MATRIX_CONCURRENCY` at a time (three by default, so six versions go out as two waves) with
73
+ `MATRIX_JOBS` xdist workers each; the defaults split the machine's cores over the concurrent
74
+ versions and leave one core free:
71
75
 
72
76
  ```bash
73
- scripts/setup-test-matrix.sh # once per machine
74
- scripts/run-test-matrix.sh # whole suite, six versions, 2~3 minutes
75
- scripts/run-test-matrix.sh tests/unit -x # extra pytest arguments are forwarded
76
- MATRIX_JOBS=4 scripts/run-test-matrix.sh # workers per version
77
+ scripts/setup-test-matrix.sh # once per machine
78
+ scripts/run-test-matrix.sh # whole suite, six versions, ~5 min on 24 threads
79
+ scripts/run-test-matrix.sh tests/unit -x # extra pytest arguments are forwarded
80
+ MATRIX_JOBS=4 scripts/run-test-matrix.sh # xdist workers per version
81
+ MATRIX_CONCURRENCY=2 scripts/run-test-matrix.sh # versions running at once
77
82
  ```
78
83
 
84
+ The matrix is six times one suite, so waves trade peak load and nothing else: the wall
85
+ clock follows the cores, not the grouping. What a Linux box cannot run is still CI's
86
+ half — `.github/workflows/test.yml` runs the same versions on macOS.
87
+
79
88
  Each version logs to `.matrix/logs/<version>.log`, so a failure there can be re-run
80
- directly with that interpreter. The scripts work on Linux and macOS. `.matrix/` is
81
- git-ignored.
89
+ directly with that interpreter; the directory is wiped before a run starts, so the
90
+ leftovers of an earlier one never pass for the current run, and a second run refuses
91
+ to start (exit 2) while one is in flight rather than wipe the files it is still
92
+ reporting from. The run also records the commit it tested in `.matrix/logs/head`,
93
+ which is how `scripts/release-preflight.sh` refuses matrix evidence that belongs to
94
+ another commit. The scripts work on Linux and macOS. `.matrix/` is git-ignored.
82
95
  5. **Platform-specific code cannot be verified by one matrix job alone.** Linux and macOS
83
96
  differ in the system utilities the environment probe calls and in `pathlib`/`resource`
84
97
  behaviour. Code guarded by `sys.platform`/`os.name`, or shelling out to system tools,
@@ -175,4 +188,4 @@ Software versions, database schema versions, and metadata schema versions stated
175
188
  {{ operon_version }} {{ db_schema }} {{ metadata_schema }}
176
189
  ```
177
190
 
178
- `docs/conf.py` resolves them from `pyproject.toml` and the code constants at build time. Substitutions expand in paragraph text only, not inside code spans or fenced code blocks; examples there use `<version>` placeholders instead. Intentional historical pins stay literal: they either live on the allowlisted era-pinned pages under `docs/*/operations/` or carry an inline `<!-- version-pin -->` marker. `tests/unit/test_docs_versions.py` enforces the rule.
191
+ `docs/conf_common.py` resolves them from `pyproject.toml` and the code constants at build time. Substitutions expand in paragraph text only, not inside code spans or fenced code blocks; examples there use `<version>` placeholders instead. Intentional historical pins stay literal: they either live on the allowlisted era-pinned pages under `docs/*/operations/` or carry an inline `<!-- version-pin -->` marker. `tests/unit/test_docs_versions.py` enforces the rule.
@@ -16,6 +16,14 @@ It validates that the release tag is exactly `v<project.version>`, then builds:
16
16
  - CPython 3.10-3.14 macOS Intel wheels; and
17
17
  - CPython 3.10-3.14 macOS Apple Silicon wheels.
18
18
 
19
+ A `verify-release` job runs before every other job, and they all need it: it
20
+ checks out the release tag and refuses to continue unless the `test` workflow's run
21
+ for that exact commit concluded `success` and `scripts/release-preflight.sh --ci
22
+ --tag <tag>` passes. It imports the maintainer's published GPG key first, so the
23
+ tag's signature is verified rather than merely present; without that key the gate
24
+ reports the check as skipped instead of failed. A tag on a commit whose CI is red — or cancelled before it ran
25
+ — therefore builds and uploads nothing.
26
+
19
27
  Each wheel is installed in an isolated test environment before upload. The
20
28
  test imports the compiled parser and invokes the CLI. The source distribution
21
29
  is checked with Twine. The final publish job cannot start until all artifacts
@@ -31,13 +39,23 @@ manual approval before the final upload.
31
39
 
32
40
  1. Update `[project].version`, plus `SCHEMA_VERSION` / `METADATA_SCHEMA_VERSION`
33
41
  in the code when they change. Documentation version markers render from
34
- these single sources through `myst_substitutions` in `docs/conf.py`, so no
42
+ these single sources through `myst_substitutions` in `docs/conf_common.py`, so no
35
43
  manual sweep is needed; `tests/unit/test_docs_versions.py` rejects
36
44
  hardcoded current versions in the Markdown sources.
37
- 2. Run the full pytest suite and strict documentation build.
45
+ 2. Run `scripts/release-preflight.sh` — the release gate in one command, one exit
46
+ code: the full pytest suite, the strict documentation build for both language
47
+ trees, the version in `pyproject.toml` against the installed metadata, the defect
48
+ registry (every record shipping in this version is `verified`, names a
49
+ `fix_commit`, and that commit is in the tree), and the cross-version matrix
50
+ evidence for the commit you are about to tag (`--run-matrix` runs the matrix as
51
+ part of the gate; `--tag vX.Y.Z` additionally asserts the created tag is
52
+ annotated, GPG-signed, named `v<version>`, and pointing at `HEAD`). Nothing about
53
+ a release depends on remembering the individual commands.
38
54
  3. Commit the release state and create tag `v<project.version>` on that exact
39
55
  commit. Never reuse a tag that points to older package metadata.
40
- 4. Wait for the `deploy` workflow on the tagged commit to pass.
56
+ 4. Wait for the `test` workflow on the tagged commit to pass. The publish
57
+ workflow refuses to build or upload while that run is missing, cancelled, or red;
58
+ give such a commit its own run with `gh run rerun <run-id>` and publish again.
41
59
  5. Create a GitHub Release from that tag, initially as a draft if release notes
42
60
  still need review, then publish it.
43
61
  6. Verify the files and metadata on the `OperonDBS` PyPI project page.
@@ -13,7 +13,8 @@ python -m pytest tests/integration
13
13
  python -m pytest tests/regression tests/compatibility
14
14
 
15
15
  # 严格构建 Sphinx 文档
16
- sphinx-build -W --keep-going -b html docs docs/_build/html
16
+ sphinx-build -W --keep-going -b html docs/en docs/_build/en/html
17
+ sphinx-build -W --keep-going -b html docs/zh docs/_build/zh/html
17
18
  ```
18
19
 
19
20
  pytest 测试按 `unit`、`integration`、`regression`、`compatibility` 四类组织,覆盖:
@@ -61,17 +62,26 @@ F821 检查仍然启用。Markdown 制表符检查允许围栏代码块中的 TS
61
62
  可将耗时压到 30~40 秒。`--dist loadfile` 保证同一个测试文件只落在一个
62
63
  worker 上,这是 Textual UI 测试所需要的。覆盖率统计最贵:迭代时用 `--no-cov`,最后只测一次。
63
64
  4. **一次跑完跨版本矩阵。** `scripts/setup-test-matrix.sh` 用 uv 安装 CPython 3.10–3.15 并在
64
- `.matrix/` 下为每个版本建 venv。`scripts/run-test-matrix.sh` 可并发跑完六个版本:
65
+ `.matrix/` 下为每个版本建 venv。`scripts/run-test-matrix.sh` 每次并发 `MATRIX_CONCURRENCY`
66
+ 个版本(默认 3 个,六个版本分两波),每个版本给 `MATRIX_JOBS` 个 xdist worker;默认值把机器
67
+ 核心数摊到并发版本上,并留出一个核心:
65
68
 
66
69
  ```bash
67
- scripts/setup-test-matrix.sh # 每台机器执行一次
68
- scripts/run-test-matrix.sh # 全量 × 六个版本,约 2~3 分钟
69
- scripts/run-test-matrix.sh tests/unit -x # 其余 pytest 参数原样透传
70
- MATRIX_JOBS=4 scripts/run-test-matrix.sh # 每个版本的 worker 数
70
+ scripts/setup-test-matrix.sh # 每台机器执行一次
71
+ scripts/run-test-matrix.sh # 全量 × 六个版本,24 线程约 5 分钟
72
+ scripts/run-test-matrix.sh tests/unit -x # 其余 pytest 参数原样透传
73
+ MATRIX_JOBS=4 scripts/run-test-matrix.sh # 每个版本的 worker 数
74
+ MATRIX_CONCURRENCY=2 scripts/run-test-matrix.sh # 同时运行的版本数
71
75
  ```
72
76
 
73
- 每个版本的日志在 `.matrix/logs/<version>.log`,失败后可直接用对应解释器重跑。脚本在 Linux
74
- 与 macOS 上均可运行。`.matrix/` 已被 git 忽略。
77
+ 矩阵相当于把全量测试跑六遍,所以分波只影响峰值占用,不改变总耗时——总耗时由核心数决定。
78
+ Linux 机器跑不到的那一半仍由 CI 覆盖(`.github/workflows/test.yml` 在 macOS 上跑同样版本)。
79
+
80
+ 每个版本的日志在 `.matrix/logs/<version>.log`,失败后可直接用对应解释器重跑;日志目录在每次
81
+ 运行前清空,因此不会把上一轮的残留误当成本轮结果,且同一时间只允许一个运行——第二个会以退出码 2
82
+ 拒绝启动,而不是清空正在汇报的那一轮的日志。运行还会把被测提交写入
83
+ `.matrix/logs/head`,`scripts/release-preflight.sh` 据此拒绝把别的提交的矩阵结果当作
84
+ 本次发布的证据。脚本在 Linux 与 macOS 上均可运行,`.matrix/` 已被 git 忽略。
75
85
  5. **平台相关代码不能只靠单个矩阵任务验证。** Linux 与 macOS 在环境探测所调用的系统工具、以及
76
86
  `pathlib`/`resource` 行为上都不同。凡是用 `sys.platform`/`os.name` 分支或调用系统工具的代码,
77
87
  都应带一个能在 Linux 上复现对端平台条件的测试(例如不含 GNU `timeout` 的空 `PATH`),而不是
@@ -157,7 +167,7 @@ CLI 是规范面:新能力先落在 CLI 与核心函数,TUI 只调用同一
157
167
  {{ operon_version }} {{ db_schema }} {{ metadata_schema }}
158
168
  ```
159
169
 
160
- 由 `docs/conf.py` 在构建时从 `pyproject.toml` 与代码常量解析。替换只在正文段落中展开,行内代码与代码块内不生效,
170
+ 由 `docs/conf_common.py` 在构建时从 `pyproject.toml` 与代码常量解析。替换只在正文段落中展开,行内代码与代码块内不生效,
161
171
  这些位置的示例改用 `<version>` 占位符。有意保留的历史版本号保持字面量:要么位于 `docs/*/operations/`
162
172
  下 allowlist 中的时代绑定页面,要么在行内附带 `<!-- version-pin -->` 标记。`tests/unit/test_docs_versions.py`
163
173
  负责强制此规则。
@@ -16,6 +16,13 @@ tag 必须严格等于 `v<project.version>`,再构建:
16
16
  - CPython 3.10-3.14 的 macOS Intel wheel;
17
17
  - CPython 3.10-3.14 的 macOS Apple Silicon wheel。
18
18
 
19
+ `verify-release` job 会先于其他所有 job 运行,且它们都依赖它:它检出 release tag,
20
+ 并断言"该提交的 `test` workflow 结论为 `success`"以及
21
+ `scripts/release-preflight.sh --ci --tag <tag>` 通过。它会先导入维护者公开的 GPG 公钥,
22
+ 因此 tag 的签名是被**真正验证**过,而不只是"存在签名";若该公钥不可用,这一项报 `skip`
23
+ 而不是 `fail`。因此,tag 落在 CI 为红(或压根
24
+ 被取消、没跑完)的提交上时,什么都不会被构建,更不会被上传。
25
+
19
26
  每个 wheel 都会先安装到隔离测试环境,导入编译后的 parser 并调用 CLI;源码分发包
20
27
  通过 Twine 检查。最终发布 job 必须等待全部构建成功,并通过 PyPI Trusted
21
28
  Publishing 认证,不保存长期 API token。
@@ -27,13 +34,20 @@ workflow 填 `publish.yml`,environment 填 `pypi`。GitHub environment 名称
27
34
  ## 发布步骤
28
35
 
29
36
  1. 更新 `[project].version`;如有变化,同时更新代码中的 `SCHEMA_VERSION` /
30
- `METADATA_SCHEMA_VERSION`。文档版本标记通过 `docs/conf.py` 的
37
+ `METADATA_SCHEMA_VERSION`。文档版本标记通过 `docs/conf_common.py` 的
31
38
  `myst_substitutions` 从这些唯一来源渲染,无需手工批量替换;
32
39
  `tests/unit/test_docs_versions.py` 会拒绝 Markdown 源文件中硬编码的当前版本。
33
- 2. 运行完整 pytest 套件与严格文档构建。
40
+ 2. 运行 `scripts/release-preflight.sh` —— 一条命令、一个退出码,就是发布闸门:
41
+ 全量 pytest 套件、两棵树语言的严格文档构建、`pyproject.toml` 版本与已安装元数据的
42
+ 一致性、缺陷登记库(凡是 `fixed_in` 为本版本的记录都必须 `verified`、带
43
+ `fix_commit`,且该提交包含在当前树中),以及"你即将打 tag 的那个提交"的跨版本矩阵
44
+ 证据(`--run-matrix` 会把矩阵跑在闸门里;`--tag vX.Y.Z` 还会断言 create 出来的 tag
45
+ 是附注、已 GPG 签名、名为 `v<version>` 且指向 `HEAD`)。发布不再依赖谁记得住这些
46
+ 单个命令。
34
47
  3. 提交发布状态,并让 `v<project.version>` tag 指向该提交;不要复用仍指向旧软件包
35
48
  元数据的 tag。
36
- 4. 等待该 tag 对应提交的 `deploy` workflow 全部通过。
49
+ 4. 等待该 tag 对应提交的 `test` workflow 全部通过。若该 run 缺失、被取消或为红,
50
+ 发布链路会拒绝构建与上传;先用 `gh run rerun <run-id>` 给它补一轮,再发布。
37
51
  5. 从该 tag 创建 GitHub Release;如仍需检查发布说明可先保存为 draft,确认后发布。
38
52
  6. 在 PyPI 的 `OperonDBS` 项目页核对文件与元数据。
39
53
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "OperonDBS"
7
- version = "0.8.3"
7
+ version = "0.8.4"
8
8
  description = "Operon the Database System: Archive, Quality-Control, Organize, Analyze and Release Your Bio-Data"
9
9
  authors = [
10
10
  { name = "hyli360", email = "lihuanyu2003@gmail.com" }
@@ -16,6 +16,7 @@ extra installs no Sphinx, so the documentation must stay verifiable without it.
16
16
  from __future__ import annotations
17
17
 
18
18
  import ast
19
+ import os
19
20
  from pathlib import Path
20
21
 
21
22
  import pytest
@@ -186,3 +187,40 @@ def test_every_workflow_builds_both_language_trees():
186
187
  f"{directories or ['docs']} — the pre-split source directory no "
187
188
  "longer holds a conf.py"
188
189
  )
190
+
191
+
192
+ def test_the_publish_workflow_gates_on_the_release_preflight() -> None:
193
+ """A release builds and uploads only after the tagged commit proved itself.
194
+
195
+ The publish workflow used to build whatever the tag pointed at, so a tag on a
196
+ commit whose CI was red — or cancelled before it ever ran — reached PyPI anyway.
197
+ The gate is the ``verify-release`` job: it asserts the ``test`` workflow's
198
+ conclusion for that exact commit and then runs
199
+ ``scripts/release-preflight.sh``, and every job that builds or uploads waits for
200
+ it. A release cannot exercise that path in a test, so this keeps it in place.
201
+ """
202
+
203
+ preflight = REPO_ROOT / "scripts" / "release-preflight.sh"
204
+ assert preflight.is_file(), "the release preflight script is missing"
205
+ assert os.access(preflight, os.X_OK), f"{preflight} is not executable"
206
+
207
+ publish = yaml.safe_load(
208
+ (REPO_ROOT / ".github" / "workflows" / "publish.yml").read_text(encoding="utf-8")
209
+ )
210
+ jobs = publish["jobs"]
211
+ verify = jobs["verify-release"]
212
+ commands = "\n".join(
213
+ step.get("run", "") for step in verify["steps"] if isinstance(step, dict)
214
+ )
215
+ assert "release-preflight.sh" in commands, "verify-release does not run the preflight"
216
+ assert "test.yml" in commands and "conclusion" in commands, (
217
+ "verify-release does not assert the test workflow's conclusion for the tagged commit"
218
+ )
219
+ assert verify.get("permissions", {}).get("actions") == "read", (
220
+ "verify-release reads workflow runs, so it needs actions: read"
221
+ )
222
+
223
+ for job in ("source-distribution", "documentation", "wheels", "pypi-publish"):
224
+ needs = jobs[job]["needs"]
225
+ needs = [needs] if isinstance(needs, str) else list(needs)
226
+ assert "verify-release" in needs, f"{job} does not wait for verify-release"
@@ -137,7 +137,9 @@ async def _click(pilot, selector: str) -> None:
137
137
  if isinstance(widget, Button) and widget.has_class("-active"):
138
138
  await _wait_until(lambda: not widget.has_class("-active"),
139
139
  f"{selector} to settle")
140
- for _ in range(10):
140
+ loop = asyncio.get_running_loop()
141
+ deadline = loop.time() + SETTLE_TIMEOUT
142
+ while True:
141
143
  try:
142
144
  landed = await pilot.click(selector)
143
145
  except OutOfBounds:
@@ -148,9 +150,10 @@ async def _click(pilot, selector: str) -> None:
148
150
  widget.press()
149
151
  await pilot.pause()
150
152
  return
153
+ if loop.time() > deadline:
154
+ raise AssertionError(f"click did not land on {selector}")
151
155
  await pilot.pause()
152
156
  await asyncio.sleep(0.02)
153
- raise AssertionError(f"click did not land on {selector}")
154
157
 
155
158
 
156
159
  # ---------------------------------------------------------------------------
@@ -572,6 +575,7 @@ def test_config_screen_profile_save_end_to_end(project: Project) -> None:
572
575
  assert sorted(row["profile_version"] for row in rows) == [1, 2]
573
576
 
574
577
 
578
+ @pytest.mark.bug("ODR-0027")
575
579
  def test_profile_editor_scrolls_to_all_rules(project: Project) -> None:
576
580
  """The rules editor must scroll: rule containers use height 1fr by default
577
581
  (plain Vertical), which clipped the rules to a fixed non-scrolling window."""
@@ -581,8 +585,15 @@ def test_profile_editor_scrolls_to_all_rules(project: Project) -> None:
581
585
  async with app.run_test(size=(160, 50)) as pilot:
582
586
  panel = await _open_config(app, pilot)
583
587
  _select_profile(panel, "assembly_production_v1")
584
- await pilot.pause()
588
+ await _await_form_ready(pilot, panel)
585
589
  editor = panel.query_one("#profile-editor", VerticalScroll)
590
+ # The rows are in the tree before the layout gives the editor its
591
+ # content height, so wait for the growth the test is about instead
592
+ # of measuring in the mounting turn (ODR-0027).
593
+ await _wait_until(
594
+ lambda: editor.virtual_size.height > editor.scrollable_content_region.height,
595
+ "the rules editor to grow past its viewport",
596
+ )
586
597
  assert editor.virtual_size.height > editor.scrollable_content_region.height
587
598
  assert editor.max_scroll_y > 0
588
599
  editor.scroll_end(animate=False)
@@ -705,6 +716,20 @@ def test_config_screen_recipe_save_end_to_end(project: Project) -> None:
705
716
  # ---------------------------------------------------------------------------
706
717
 
707
718
 
719
+ async def _await_notification(pilot, app, needle: str) -> None:
720
+ """Wait until some raised notification's message contains ``needle``.
721
+
722
+ A save runs on a worker and raises its notification a message-loop turn
723
+ after the modal closes, so reading ``_notifications(app)`` right after a
724
+ single ``pause()`` sees the list before the text is in it — on a slow
725
+ runner that turns a correct save into a failure (ODR-0027).
726
+ """
727
+ await _wait_until(
728
+ lambda: any(needle in message for _, message in _notifications(app)),
729
+ f"the notification {needle!r}",
730
+ )
731
+
732
+
708
733
  def _notifications(app) -> list[tuple[str, str]]:
709
734
  """(severity, message) of every notification raised on the app so far.
710
735
 
@@ -893,8 +918,7 @@ def test_config_screen_profile_unmodeled_keys_survive_save(project: Project) ->
893
918
  await _settled(app)
894
919
  await pilot.pause()
895
920
  assert not isinstance(app.screen, ProfileSaveModal)
896
- assert any("saved probe_extras_v1 version 2" in message
897
- for _, message in _notifications(app))
921
+ await _await_notification(pilot, app, "saved probe_extras_v1 version 2")
898
922
 
899
923
  _run(scenario())
900
924
  loaded = load_profile(project.profiles_dir, "probe_extras_v1", expected_kind="qc")
@@ -1013,10 +1037,8 @@ def test_config_screen_unchanged_profile_save_notifies(project: Project) -> None
1013
1037
  await _settled(app)
1014
1038
  await pilot.pause()
1015
1039
  assert not isinstance(app.screen, ProfileSaveModal)
1016
- assert any(
1017
- "assembly_production_v1: unchanged — version 1 kept" in message
1018
- for _, message in _notifications(app)
1019
- )
1040
+ await _await_notification(
1041
+ pilot, app, "assembly_production_v1: unchanged — version 1 kept")
1020
1042
 
1021
1043
  _run(scenario())
1022
1044
  assert load_profile(
@@ -1103,8 +1125,7 @@ def test_config_screen_new_profile_is_created_as_version_1(project: Project) ->
1103
1125
  await _settled(app)
1104
1126
  await pilot.pause()
1105
1127
  assert not isinstance(app.screen, ProfileSaveModal)
1106
- assert any("saved assembly_strict_v1 version 1" in message
1107
- for _, message in _notifications(app))
1128
+ await _await_notification(pilot, app, "saved assembly_strict_v1 version 1")
1108
1129
 
1109
1130
  _run(scenario())
1110
1131
  loaded = load_profile(project.profiles_dir, "assembly_strict_v1", expected_kind="qc")
@@ -1136,10 +1157,8 @@ def test_config_screen_new_profile_with_existing_name_opens_it(project: Project)
1136
1157
  assert "reads_qc_v1" in _static_text(panel.query_one("#profile-heading", Static))
1137
1158
  assert panel.query_one("#profile-description", Input).value.startswith("Raw read QC")
1138
1159
  assert len(panel._rule_rows("required")) == 4
1139
- assert any(
1140
- "profile 'reads_qc_v1' already exists — opening it instead" in message
1141
- for _, message in _notifications(app)
1142
- )
1160
+ await _await_notification(
1161
+ pilot, app, "profile 'reads_qc_v1' already exists — opening it instead")
1143
1162
 
1144
1163
  _run(scenario())
1145
1164
 
@@ -1310,8 +1329,7 @@ def test_config_screen_recipe_unchanged_max_hits_and_cancel(project: Project) ->
1310
1329
  await _settled(app)
1311
1330
  await pilot.pause()
1312
1331
  assert not isinstance(app.screen, RecipeSaveModal)
1313
- assert any("saved blastn_nt version 2" in message
1314
- for _, message in _notifications(app))
1332
+ await _await_notification(pilot, app, "saved blastn_nt version 2")
1315
1333
 
1316
1334
  # A recipe without max_hits_per_query also saves as a no-op.
1317
1335
  panel._load_recipe("busco_autolineage")
@@ -1980,15 +1998,23 @@ async def _await_rows(pilot, root, selector: str, count: int, child: str) -> lis
1980
1998
  """Wait until ``root`` holds ``count`` ``selector`` rows whose ``child`` exists.
1981
1999
 
1982
2000
  Mounting a row subtree takes more than one message-loop turn, so a single
1983
- ``pilot.pause()`` can observe a row that has not composed yet.
2001
+ ``pilot.pause()`` can observe a row that has not composed yet. The budget is
2002
+ this file's wall-clock settle timeout rather than a fixed number of cycles: a
2003
+ loaded CI runner needs seconds to deliver the click and mount the row it
2004
+ produces, and a cycle count that is generous on a fast machine runs out there
2005
+ (ODR-0027).
1984
2006
  """
1985
- for _ in range(60):
1986
- rows = [row for row in root.query(selector) if len(list(row.query(child))) > 0]
1987
- if len(rows) >= count:
1988
- return rows
1989
- await pilot.pause()
1990
- await asyncio.sleep(0.02)
1991
- raise AssertionError(f"{selector} rows with {child} never reached {count}")
2007
+ def composed() -> list:
2008
+ return [row for row in root.query(selector) if len(list(row.query(child))) > 0]
2009
+
2010
+ try:
2011
+ await _wait_until(lambda: len(composed()) >= count,
2012
+ f"{count} {selector} rows with {child}")
2013
+ except TimeoutError as error:
2014
+ raise AssertionError(
2015
+ f"{selector} rows with {child}: saw {len(composed())}, wanted {count}"
2016
+ ) from error
2017
+ return composed()
1992
2018
 
1993
2019
 
1994
2020
  async def _await_form_ready(pilot, panel) -> None:
@@ -2079,6 +2105,7 @@ def test_save_classification_profile_versions_and_validation(project: Project) -
2079
2105
  assert loaded["rules"][0]["when"][0]["value"] == 1e-05
2080
2106
 
2081
2107
 
2108
+ @pytest.mark.bug("ODR-0027")
2082
2109
  def test_config_classification_editor_end_to_end(project: Project) -> None:
2083
2110
  _write_classification_profile(project, "bhlh_tiers", BHLH_PROFILE)
2084
2111
 
@@ -2160,7 +2187,7 @@ def test_config_classification_editor_end_to_end(project: Project) -> None:
2160
2187
  await _settled(app)
2161
2188
  await pilot.pause()
2162
2189
  assert not isinstance(app.screen, ClassificationSaveModal)
2163
- assert any("saved bhlh_tiers version 2" in message for _, message in _notifications(app))
2190
+ await _await_notification(pilot, app, "saved bhlh_tiers version 2")
2164
2191
  # The editor reloaded from disk: the freshly saved structure renders.
2165
2192
  assert len(list(panel.query(".source-row"))) == 2
2166
2193
  assert len(list(panel.query(".classrule-row"))) == 4
@@ -2183,6 +2210,7 @@ def test_config_classification_editor_end_to_end(project: Project) -> None:
2183
2210
  "WHERE profile_name='bhlh_tiers'")[-1]["profile_version"] == 2
2184
2211
 
2185
2212
 
2213
+ @pytest.mark.bug("ODR-0027")
2186
2214
  def test_config_classification_editor_guards_and_readonly(project: Project) -> None:
2187
2215
  nested = json.loads(json.dumps(BHLH_PROFILE))
2188
2216
  nested["rules"][0]["when"] = [{"any": [{"not": {"field": "x", "operator": "exists"}}]}]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes