protspace 4.16.2__tar.gz → 4.16.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 (247) hide show
  1. {protspace-4.16.2 → protspace-4.16.4}/CHANGELOG.md +54 -0
  2. {protspace-4.16.2 → protspace-4.16.4}/CLAUDE.md +3 -2
  3. {protspace-4.16.2 → protspace-4.16.4}/PKG-INFO +1 -1
  4. {protspace-4.16.2 → protspace-4.16.4}/packages/protlabel/pyproject.toml +1 -1
  5. {protspace-4.16.2 → protspace-4.16.4}/pyproject.toml +7 -1
  6. protspace-4.16.4/src/protspace/__init__.py +1 -0
  7. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/main.py +6 -7
  8. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/utils/add_annotation_style.py +9 -26
  9. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/utils/arrow_reader.py +122 -81
  10. {protspace-4.16.2 → protspace-4.16.4}/tests/test_annotation_manager.py +38 -40
  11. protspace-4.16.4/tests/test_arrow_reader.py +132 -0
  12. {protspace-4.16.2 → protspace-4.16.4}/tests/test_backend_switch.py +7 -4
  13. {protspace-4.16.2 → protspace-4.16.4}/tests/test_base_data_processor.py +4 -0
  14. {protspace-4.16.2 → protspace-4.16.4}/tests/test_biocentral_embedder.py +14 -14
  15. {protspace-4.16.2 → protspace-4.16.4}/tests/test_biocentral_retriever.py +2 -2
  16. {protspace-4.16.2 → protspace-4.16.4}/tests/test_bundle_version.py +1 -1
  17. {protspace-4.16.2 → protspace-4.16.4}/tests/test_cath_names.py +1 -1
  18. {protspace-4.16.2 → protspace-4.16.4}/tests/test_fasta.py +1 -1
  19. {protspace-4.16.2 → protspace-4.16.4}/tests/test_interpro_annotation_retriever.py +17 -29
  20. {protspace-4.16.2 → protspace-4.16.4}/tests/test_legacy_cache_refresh.py +7 -16
  21. {protspace-4.16.2 → protspace-4.16.4}/tests/test_pfam_clan.py +1 -1
  22. {protspace-4.16.2 → protspace-4.16.4}/tests/test_taxonomy_annotation_retriever.py +1 -1
  23. {protspace-4.16.2 → protspace-4.16.4}/tests/test_ted_retriever.py +3 -7
  24. {protspace-4.16.2 → protspace-4.16.4}/tests/test_transfer_cli.py +8 -10
  25. {protspace-4.16.2 → protspace-4.16.4}/tests/test_transformer.py +2 -2
  26. {protspace-4.16.2 → protspace-4.16.4}/tests/test_uniprot_annotation_retriever.py +8 -10
  27. {protspace-4.16.2 → protspace-4.16.4}/tests/test_uniprot_parser_encoding.py +1 -1
  28. protspace-4.16.2/src/protspace/__init__.py +0 -1
  29. {protspace-4.16.2 → protspace-4.16.4}/.env.example +0 -0
  30. {protspace-4.16.2 → protspace-4.16.4}/.githooks/pre-commit +0 -0
  31. {protspace-4.16.2 → protspace-4.16.4}/.gitignore +0 -0
  32. {protspace-4.16.2 → protspace-4.16.4}/LICENSE +0 -0
  33. {protspace-4.16.2 → protspace-4.16.4}/README.md +0 -0
  34. {protspace-4.16.2 → protspace-4.16.4}/data/3FTx/3FTx.csv +0 -0
  35. {protspace-4.16.2 → protspace-4.16.4}/data/3FTx/3FTx_raw_data.xlsx +0 -0
  36. {protspace-4.16.2 → protspace-4.16.4}/data/3FTx/parquetbundle/3FTx_accession.csv +0 -0
  37. {protspace-4.16.2 → protspace-4.16.4}/data/3FTx/parquetbundle/3ftx_with_db_styled.parquetbundle +0 -0
  38. {protspace-4.16.2 → protspace-4.16.4}/data/3FTx/parquetbundle/styles.json +0 -0
  39. {protspace-4.16.2 → protspace-4.16.4}/data/Pla2g2/Pla2g2.csv +0 -0
  40. {protspace-4.16.2 → protspace-4.16.4}/data/Pla2g2/Pla2g2.fasta +0 -0
  41. {protspace-4.16.2 → protspace-4.16.4}/data/Pla2g2/Pla2g2_pdb.zip +0 -0
  42. {protspace-4.16.2 → protspace-4.16.4}/data/Pla2g2/protspace_files/Pla2g2.json +0 -0
  43. {protspace-4.16.2 → protspace-4.16.4}/data/Pla2g2/protspace_files/Pla2g2_customized.json +0 -0
  44. {protspace-4.16.2 → protspace-4.16.4}/data/Pla2g2/style.json +0 -0
  45. {protspace-4.16.2 → protspace-4.16.4}/data/jmb_2025/README.md +0 -0
  46. {protspace-4.16.2 → protspace-4.16.4}/data/jmb_2025/toxprot/rebuild_mature_fasta.py +0 -0
  47. {protspace-4.16.2 → protspace-4.16.4}/data/jmb_2025/toxprot/toxins.csv +0 -0
  48. {protspace-4.16.2 → protspace-4.16.4}/data/jmb_2025/toxprot/toxins.json +0 -0
  49. {protspace-4.16.2 → protspace-4.16.4}/data/jmb_2025/toxprot/toxins_full.fasta +0 -0
  50. {protspace-4.16.2 → protspace-4.16.4}/data/jmb_2025/toxprot/toxins_mature.fasta +0 -0
  51. {protspace-4.16.2 → protspace-4.16.4}/data/jmb_2025/toxprot/toxins_style.json +0 -0
  52. {protspace-4.16.2 → protspace-4.16.4}/data/toxins/data.parquetbundle +0 -0
  53. {protspace-4.16.2 → protspace-4.16.4}/docs/annotations.md +0 -0
  54. {protspace-4.16.2 → protspace-4.16.4}/docs/cli.md +0 -0
  55. {protspace-4.16.2 → protspace-4.16.4}/docs/protspace_example.png +0 -0
  56. {protspace-4.16.2 → protspace-4.16.4}/docs/styling.md +0 -0
  57. {protspace-4.16.2 → protspace-4.16.4}/notebooks/ClickThrough_GenerateEmbeddings.ipynb +0 -0
  58. {protspace-4.16.2 → protspace-4.16.4}/notebooks/ProtSpace_Preparation.ipynb +0 -0
  59. {protspace-4.16.2 → protspace-4.16.4}/notebooks/ProtSpace_Transfer.ipynb +0 -0
  60. {protspace-4.16.2 → protspace-4.16.4}/package.json +0 -0
  61. {protspace-4.16.2 → protspace-4.16.4}/packages/protlabel/README.md +0 -0
  62. {protspace-4.16.2 → protspace-4.16.4}/packages/protlabel/benchmarks/bench_knn.py +0 -0
  63. {protspace-4.16.2 → protspace-4.16.4}/packages/protlabel/benchmarks/bench_memory.py +0 -0
  64. {protspace-4.16.2 → protspace-4.16.4}/packages/protlabel/src/protlabel/__init__.py +0 -0
  65. {protspace-4.16.2 → protspace-4.16.4}/packages/protlabel/src/protlabel/backends.py +0 -0
  66. {protspace-4.16.2 → protspace-4.16.4}/packages/protlabel/src/protlabel/lookup.py +0 -0
  67. {protspace-4.16.2 → protspace-4.16.4}/packages/protlabel/src/protlabel/reliability.py +0 -0
  68. {protspace-4.16.2 → protspace-4.16.4}/packages/protlabel/src/protlabel/transfer.py +0 -0
  69. {protspace-4.16.2 → protspace-4.16.4}/packages/protlabel/tests/test_protlabel_backends.py +0 -0
  70. {protspace-4.16.2 → protspace-4.16.4}/packages/protlabel/tests/test_protlabel_boundary.py +0 -0
  71. {protspace-4.16.2 → protspace-4.16.4}/packages/protlabel/tests/test_protlabel_lookup.py +0 -0
  72. {protspace-4.16.2 → protspace-4.16.4}/packages/protlabel/tests/test_protlabel_reliability.py +0 -0
  73. {protspace-4.16.2 → protspace-4.16.4}/packages/protlabel/tests/test_protlabel_transfer.py +0 -0
  74. {protspace-4.16.2 → protspace-4.16.4}/packages/protlabel/tests/test_protlabel_version.py +0 -0
  75. {protspace-4.16.2 → protspace-4.16.4}/scripts/count_h5_rows.py +0 -0
  76. {protspace-4.16.2 → protspace-4.16.4}/scripts/count_parquetbundle_entries.py +0 -0
  77. {protspace-4.16.2 → protspace-4.16.4}/scripts/download_clan_fastas.py +0 -0
  78. {protspace-4.16.2 → protspace-4.16.4}/scripts/generate_examples/README.md +0 -0
  79. {protspace-4.16.2 → protspace-4.16.4}/scripts/generate_examples/build_showcase.py +0 -0
  80. {protspace-4.16.2 → protspace-4.16.4}/scripts/generate_examples/datasets.toml +0 -0
  81. {protspace-4.16.2 → protspace-4.16.4}/scripts/generate_examples/generate.py +0 -0
  82. {protspace-4.16.2 → protspace-4.16.4}/scripts/generate_examples/inputs/three-finger-toxins.accessions.txt +0 -0
  83. {protspace-4.16.2 → protspace-4.16.4}/scripts/generate_examples/showcase.toml +0 -0
  84. {protspace-4.16.2 → protspace-4.16.4}/scripts/generate_examples/stage_perf.py +0 -0
  85. {protspace-4.16.2 → protspace-4.16.4}/scripts/generate_examples/styles/beta-lactamase.json +0 -0
  86. {protspace-4.16.2 → protspace-4.16.4}/scripts/generate_examples/styles/demo.json +0 -0
  87. {protspace-4.16.2 → protspace-4.16.4}/scripts/generate_examples/styles/human-fly.json +0 -0
  88. {protspace-4.16.2 → protspace-4.16.4}/scripts/generate_examples/styles/swissprot.json +0 -0
  89. {protspace-4.16.2 → protspace-4.16.4}/scripts/generate_examples/styles/three-finger-toxins.json +0 -0
  90. {protspace-4.16.2 → protspace-4.16.4}/scripts/generate_examples/write_manifest.py +0 -0
  91. {protspace-4.16.2 → protspace-4.16.4}/scripts/generate_toxprot_demo.py +0 -0
  92. {protspace-4.16.2 → protspace-4.16.4}/scripts/generate_v3_fixture.py +0 -0
  93. {protspace-4.16.2 → protspace-4.16.4}/scripts/inspect_bundle.py +0 -0
  94. {protspace-4.16.2 → protspace-4.16.4}/scripts/pfam_clans/README.md +0 -0
  95. {protspace-4.16.2 → protspace-4.16.4}/scripts/pfam_clans/create_pfam_clan_query.py +0 -0
  96. {protspace-4.16.2 → protspace-4.16.4}/scripts/pfam_clans/merge_h5_batches.py +0 -0
  97. {protspace-4.16.2 → protspace-4.16.4}/scripts/sync_lock_version.py +0 -0
  98. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/analysis/__init__.py +0 -0
  99. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/analysis/classification.py +0 -0
  100. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/app.py +0 -0
  101. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/assets/__init__.py +0 -0
  102. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/assets/annotated_image.png +0 -0
  103. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/assets/custom.css +0 -0
  104. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/assets/help_content/__init__.py +0 -0
  105. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/assets/help_content/help_faq.md +0 -0
  106. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/assets/help_content/help_how_it_works.md +0 -0
  107. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/assets/help_content/help_json.md +0 -0
  108. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/assets/help_content/help_overview.md +0 -0
  109. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/assets/rostlab_logo.png +0 -0
  110. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/cli/__init__.py +0 -0
  111. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/cli/annotate.py +0 -0
  112. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/cli/app.py +0 -0
  113. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/cli/bundle.py +0 -0
  114. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/cli/common_options.py +0 -0
  115. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/cli/convert.py +0 -0
  116. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/cli/embed.py +0 -0
  117. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/cli/prepare.py +0 -0
  118. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/cli/project.py +0 -0
  119. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/cli/serve.py +0 -0
  120. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/cli/stats.py +0 -0
  121. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/cli/style.py +0 -0
  122. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/cli/transfer.py +0 -0
  123. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/core/__init__.py +0 -0
  124. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/core/config.py +0 -0
  125. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/core/constants.py +0 -0
  126. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/data/__init__.py +0 -0
  127. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/data/annotations/__init__.py +0 -0
  128. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/data/annotations/cache.py +0 -0
  129. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/data/annotations/configuration.py +0 -0
  130. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/data/annotations/encoding.py +0 -0
  131. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/data/annotations/manager.py +0 -0
  132. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/data/annotations/merging.py +0 -0
  133. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/data/annotations/retrievers/__init__.py +0 -0
  134. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/data/annotations/retrievers/base_retriever.py +0 -0
  135. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/data/annotations/retrievers/biocentral_retriever.py +0 -0
  136. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/data/annotations/retrievers/cath_names.py +0 -0
  137. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/data/annotations/retrievers/http_utils.py +0 -0
  138. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/data/annotations/retrievers/interpro_retriever.py +0 -0
  139. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/data/annotations/retrievers/taxonomy_retriever.py +0 -0
  140. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/data/annotations/retrievers/ted_retriever.py +0 -0
  141. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/data/annotations/retrievers/uniprot_retriever.py +0 -0
  142. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/data/annotations/scores.py +0 -0
  143. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/data/annotations/transformers/__init__.py +0 -0
  144. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/data/annotations/transformers/interpro_transforms.py +0 -0
  145. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/data/annotations/transformers/transformer.py +0 -0
  146. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/data/annotations/transformers/uniprot_transforms.py +0 -0
  147. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/data/biocentral_connection.py +0 -0
  148. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/data/embedding/__init__.py +0 -0
  149. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/data/embedding/biocentral.py +0 -0
  150. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/data/embedding/local.py +0 -0
  151. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/data/embedding/store.py +0 -0
  152. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/data/io/__init__.py +0 -0
  153. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/data/io/atomic.py +0 -0
  154. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/data/io/bundle.py +0 -0
  155. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/data/io/bundle_v3.py +0 -0
  156. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/data/io/fasta.py +0 -0
  157. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/data/io/formatters.py +0 -0
  158. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/data/io/predictions.py +0 -0
  159. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/data/io/settings_converter.py +0 -0
  160. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/data/io/writers.py +0 -0
  161. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/data/loaders/__init__.py +0 -0
  162. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/data/loaders/embedding_set.py +0 -0
  163. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/data/loaders/fasta.py +0 -0
  164. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/data/loaders/h5.py +0 -0
  165. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/data/loaders/query.py +0 -0
  166. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/data/loaders/similarity.py +0 -0
  167. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/data/parsers/__init__.py +0 -0
  168. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/data/parsers/uniprot_parser.py +0 -0
  169. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/data/processors/__init__.py +0 -0
  170. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/data/processors/base_processor.py +0 -0
  171. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/data/processors/pipeline.py +0 -0
  172. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/stats/__init__.py +0 -0
  173. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/stats/_sampling.py +0 -0
  174. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/stats/annotation_select.py +0 -0
  175. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/stats/base.py +0 -0
  176. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/stats/carriage.py +0 -0
  177. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/stats/cluster/__init__.py +0 -0
  178. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/stats/cluster/kmeans_elbow.py +0 -0
  179. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/stats/driver.py +0 -0
  180. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/stats/metrics/__init__.py +0 -0
  181. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/stats/metrics/annotation_validity.py +0 -0
  182. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/stats/metrics/faithfulness.py +0 -0
  183. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/stats/metrics/validity.py +0 -0
  184. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/ui/__init__.py +0 -0
  185. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/ui/callbacks.py +0 -0
  186. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/ui/layout.py +0 -0
  187. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/ui/styles.py +0 -0
  188. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/utils/__init__.py +0 -0
  189. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/utils/constants.py +0 -0
  190. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/utils/reducers.py +0 -0
  191. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/visualization/__init__.py +0 -0
  192. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/visualization/molstar.py +0 -0
  193. {protspace-4.16.2 → protspace-4.16.4}/src/protspace/visualization/plotting.py +0 -0
  194. {protspace-4.16.2 → protspace-4.16.4}/tests/README.md +0 -0
  195. {protspace-4.16.2 → protspace-4.16.4}/tests/__init__.py +0 -0
  196. {protspace-4.16.2 → protspace-4.16.4}/tests/bundle_v3_helpers.py +0 -0
  197. {protspace-4.16.2 → protspace-4.16.4}/tests/cli_output.py +0 -0
  198. {protspace-4.16.2 → protspace-4.16.4}/tests/conftest.py +0 -0
  199. {protspace-4.16.2 → protspace-4.16.4}/tests/prep_source.py +0 -0
  200. {protspace-4.16.2 → protspace-4.16.4}/tests/test_annotate_cache_dir.py +0 -0
  201. {protspace-4.16.2 → protspace-4.16.4}/tests/test_annotate_cli.py +0 -0
  202. {protspace-4.16.2 → protspace-4.16.4}/tests/test_annotation_checkpoints.py +0 -0
  203. {protspace-4.16.2 → protspace-4.16.4}/tests/test_annotation_encoding.py +0 -0
  204. {protspace-4.16.2 → protspace-4.16.4}/tests/test_annotation_retrieval_e2e.py +0 -0
  205. {protspace-4.16.2 → protspace-4.16.4}/tests/test_annotation_select.py +0 -0
  206. {protspace-4.16.2 → protspace-4.16.4}/tests/test_annotation_validity.py +0 -0
  207. {protspace-4.16.2 → protspace-4.16.4}/tests/test_atomic_publication.py +0 -0
  208. {protspace-4.16.2 → protspace-4.16.4}/tests/test_biocentral_connection.py +0 -0
  209. {protspace-4.16.2 → protspace-4.16.4}/tests/test_biocentral_live.py +0 -0
  210. {protspace-4.16.2 → protspace-4.16.4}/tests/test_build_showcase.py +0 -0
  211. {protspace-4.16.2 → protspace-4.16.4}/tests/test_bundle_internal_columns.py +0 -0
  212. {protspace-4.16.2 → protspace-4.16.4}/tests/test_bundle_overlay.py +0 -0
  213. {protspace-4.16.2 → protspace-4.16.4}/tests/test_bundle_settings.py +0 -0
  214. {protspace-4.16.2 → protspace-4.16.4}/tests/test_bundle_v3_container.py +0 -0
  215. {protspace-4.16.2 → protspace-4.16.4}/tests/test_bundle_v3_decode.py +0 -0
  216. {protspace-4.16.2 → protspace-4.16.4}/tests/test_bundle_v3_encode.py +0 -0
  217. {protspace-4.16.2 → protspace-4.16.4}/tests/test_bundle_v3_fixture.py +0 -0
  218. {protspace-4.16.2 → protspace-4.16.4}/tests/test_classification.py +0 -0
  219. {protspace-4.16.2 → protspace-4.16.4}/tests/test_cli_no_frontend.py +0 -0
  220. {protspace-4.16.2 → protspace-4.16.4}/tests/test_cli_no_similarity.py +0 -0
  221. {protspace-4.16.2 → protspace-4.16.4}/tests/test_config_validation.py +0 -0
  222. {protspace-4.16.2 → protspace-4.16.4}/tests/test_convert.py +0 -0
  223. {protspace-4.16.2 → protspace-4.16.4}/tests/test_display_decode.py +0 -0
  224. {protspace-4.16.2 → protspace-4.16.4}/tests/test_docs_extras_sync.py +0 -0
  225. {protspace-4.16.2 → protspace-4.16.4}/tests/test_embed_completeness.py +0 -0
  226. {protspace-4.16.2 → protspace-4.16.4}/tests/test_failed_source_cache.py +0 -0
  227. {protspace-4.16.2 → protspace-4.16.4}/tests/test_formatters.py +0 -0
  228. {protspace-4.16.2 → protspace-4.16.4}/tests/test_h5_parse_identifier.py +0 -0
  229. {protspace-4.16.2 → protspace-4.16.4}/tests/test_http_retry.py +0 -0
  230. {protspace-4.16.2 → protspace-4.16.4}/tests/test_local_embedder.py +0 -0
  231. {protspace-4.16.2 → protspace-4.16.4}/tests/test_notebooks.py +0 -0
  232. {protspace-4.16.2 → protspace-4.16.4}/tests/test_pipeline_utils.py +0 -0
  233. {protspace-4.16.2 → protspace-4.16.4}/tests/test_predictions_overlay.py +0 -0
  234. {protspace-4.16.2 → protspace-4.16.4}/tests/test_protein_families_parser.py +0 -0
  235. {protspace-4.16.2 → protspace-4.16.4}/tests/test_query.py +0 -0
  236. {protspace-4.16.2 → protspace-4.16.4}/tests/test_reducers.py +0 -0
  237. {protspace-4.16.2 → protspace-4.16.4}/tests/test_run_log.py +0 -0
  238. {protspace-4.16.2 → protspace-4.16.4}/tests/test_scores_ted.py +0 -0
  239. {protspace-4.16.2 → protspace-4.16.4}/tests/test_settings_converter.py +0 -0
  240. {protspace-4.16.2 → protspace-4.16.4}/tests/test_stage_perf.py +0 -0
  241. {protspace-4.16.2 → protspace-4.16.4}/tests/test_stats.py +0 -0
  242. {protspace-4.16.2 → protspace-4.16.4}/tests/test_stats_bundle.py +0 -0
  243. {protspace-4.16.2 → protspace-4.16.4}/tests/test_stats_carriage.py +0 -0
  244. {protspace-4.16.2 → protspace-4.16.4}/tests/test_stats_cli.py +0 -0
  245. {protspace-4.16.2 → protspace-4.16.4}/tests/test_style_warnings.py +0 -0
  246. {protspace-4.16.2 → protspace-4.16.4}/tests/test_toxprot_demo.py +0 -0
  247. {protspace-4.16.2 → protspace-4.16.4}/tests/test_write_manifest.py +0 -0
@@ -1,6 +1,57 @@
1
1
  # CHANGELOG
2
2
 
3
3
 
4
+ ## v4.16.4 (2026-10-15)
5
+
6
+ ### Bug Fixes
7
+
8
+ - **reader**: Refuse only a bundle file as save_data's default target
9
+ ([`d91ff68`](https://github.com/tsenoner/protspace/commit/d91ff68fa4b82dc02e006e7633a140728e60ce7b))
10
+
11
+ - **reader**: Refuse save_data without a directory to write into
12
+ ([`b8c8b8c`](https://github.com/tsenoner/protspace/commit/b8c8b8c6a242baef45b577ef7ef5283a79d8b448))
13
+
14
+ - **reader**: Save annotations where ArrowReader reloads them
15
+ ([`6b5baa8`](https://github.com/tsenoner/protspace/commit/6b5baa84b4c450d6c0547bfc5a99f294bbb12494))
16
+
17
+ ### Refactoring
18
+
19
+ - **reader**: Write save_data's tables in one loop, test via the shared fixture
20
+ ([`2e3ed84`](https://github.com/tsenoner/protspace/commit/2e3ed8478b0b7cdec3a461c530403626c1fbe729))
21
+
22
+
23
+ ## v4.16.3 (2026-10-13)
24
+
25
+ ### Bug Fixes
26
+
27
+ - **reader**: Read a .parquetbundle directly instead of refusing it
28
+ ([`ed1d127`](https://github.com/tsenoner/protspace/commit/ed1d12737089df465b896b4ac3c85c405a1c73cf))
29
+
30
+ ### Chores
31
+
32
+ - **protspace**: Ban src.* imports with ruff TID251
33
+ ([`cca83a1`](https://github.com/tsenoner/protspace/commit/cca83a173c127462c4d019cae3879290837861be))
34
+
35
+ ### Refactoring
36
+
37
+ - **reader**: Fold the path check into _load_data and test it
38
+ ([`d662690`](https://github.com/tsenoner/protspace/commit/d662690b5869f0361bd23342b2c61264beb194d9))
39
+
40
+ ### Testing
41
+
42
+ - **protspace**: Import protspace, not src.protspace, in the tests
43
+ ([`238d432`](https://github.com/tsenoner/protspace/commit/238d432afda2adc43d23c2a9c44722cd751d4a51))
44
+
45
+ - **protspace**: Match the remaining transfer CLI errors through plain()
46
+ ([`aa15d57`](https://github.com/tsenoner/protspace/commit/aa15d57a6bf0979d35616a6ce6a0bad08f0e14b8))
47
+
48
+ - **protspace**: Pin the embed usage errors and the MDS max(s) shift
49
+ ([`07a8403`](https://github.com/tsenoner/protspace/commit/07a8403fc4b3dde50475a0ffc473a4c80dec9639))
50
+
51
+ - **protspace**: Reuse plain() and one _forbid helper in the audit tests
52
+ ([`7aa03f1`](https://github.com/tsenoner/protspace/commit/7aa03f1da2d0749035fabc96673ae7437040b01e))
53
+
54
+
4
55
  ## v4.16.2 (2026-10-07)
5
56
 
6
57
  ### Bug Fixes
@@ -886,6 +937,9 @@
886
937
  - **prepare**: Make -f reach every HDF5 input and reject a path that is not there
887
938
  ([`0591944`](https://github.com/tsenoner/protspace/commit/0591944008d7a79786a36ccab125f8970a004d57))
888
939
 
940
+ - **reader**: Raise instead of returning an empty dataset for a bad path
941
+ ([`a6b00ec`](https://github.com/tsenoner/protspace/commit/a6b00ec52d7d2fbe586ba8a963ceca2a6b30c5da))
942
+
889
943
  ### Build System
890
944
 
891
945
  - **deps**: Give protobuf a floor instead of leaving it unconstrained
@@ -317,13 +317,13 @@ For a live count run `uv run pytest tests/ --collect-only -q`.
317
317
  | `test_config_validation.py` | DimensionReductionConfig parameter validation |
318
318
  | `test_style_warnings.py` | `protspace style` warnings: numeric-column detection (tsenoner/protspace-legacy#67) + `selectedPaletteId` validation (categorical vs gradient palette, per column type) + pinned palette-catalog contract |
319
319
  | `test_h5_parse_identifier.py` | HDF5 key parsing, identifier extraction |
320
- | `test_base_data_processor.py` | BaseProcessor: reduction, output creation, save (incl. settings in unbundled output) |
320
+ | `test_base_data_processor.py` | BaseProcessor: reduction (the MDS input actually fed: a similarity matrix converted with `sqrt(max(s) - s)`, a distance matrix passed through), output creation, save (incl. settings in unbundled output) |
321
321
  | `test_ted_retriever.py` | TED domain retriever (mocked AlphaFold API, CATH names), final retry pass for failed lookups (10-in-a-row cut-off, 404 never retried), parallel lookups equal to one at a time under jitter, one session, CATH names loaded once, a slow first-pass lookup not holding up the others, a tripped final-pass breaker stopping lookups still retrying |
322
322
  | `test_pfam_clan.py` | Pfam CLAN transformer (mapping, dedup, edge cases) |
323
323
  | `test_formatters.py` | ProteinAnnotations → DataFrame formatting |
324
324
  | `test_bundle_settings.py` | Parquetbundle settings read/write |
325
325
  | `test_annotation_encoding.py` | Percent-encoding round-trip, reserved-char-only encoding, schema-metadata stamping through parquet, `upgrade_cell_grammar` taking the version from the caller |
326
- | `test_transfer_cli.py` | Transfer orchestration core and CLI registration |
326
+ | `test_transfer_cli.py` | Transfer orchestration core (k>1 confidence, cosine metric, self-transfer) and the `transfer` CLI: bad input (no matching embedding, no query match, both id columns) as a usage error with its message, `--help` rendering, `protein_id` and legacy v1 bundles end to end |
327
327
  | `test_predictions_overlay.py` | Building the per-cell prediction overlay columns |
328
328
  | `test_display_decode.py` | Display-side decoding of encoded values, multi-hit rendering, gated-off passthrough |
329
329
  | `test_toxprot_demo.py` | Signal-peptide bound parsing, mature-FASTA stripping, bundle post-processing (column filter/reorder) |
@@ -334,6 +334,7 @@ For a live count run `uv run pytest tests/ --collect-only -q`.
334
334
  | `test_atomic_publication.py` | `data/io/atomic.py`: staged rename keeps the previous content on failure, and a published file (bundle, statistics parquet, retained FASTA) carries the process umask rather than `mkstemp`'s owner-only mode |
335
335
  | `test_classification.py` | Query/reference rules: id-prefix and case-insensitive `where` substring, query-over-reference precedence, empty-match and missing-column errors |
336
336
  | `test_bundle_version.py` | `format_version=2` stamped on the table the `prepare` factory hands to `write_bundle`; a `prepare` bundle reads back its proteins, annotation cells and coordinates; `bundle -a` passes stamped annotate output and the `prepare` annotation cache through (an unstamped cache is recognised by its cache-version attribute), reads an unstamped table as v1 plain text (a pandas `category` column too), stores a list column as hits and reports an unstorable column as a usage error; `ArrowReader.save_data` keeps the stamp it read |
337
+ | `test_arrow_reader.py` | `ArrowReader(Path)` reads a `.parquetbundle` in memory exactly as its extracted directory (data, settings, format version), and refuses a path that holds no ProtSpace data (a non-bundle file, a missing path, a directory with no core table) instead of reading it as an empty dataset; an annotations-only directory still loads; `save_data` writes a directory that reloads with its annotations, projections and restyled colours (the `protspace style` path for an unbundled parquet directory) and recreates its source directory when saved in place, a bundle saves to a directory that reads back alike, while `save_data` without an output directory (bundle source) or on a dict-built reader is refused before anything is written |
337
338
  | `test_bundle_v3_encode.py` | v3 encoder: the physical contract behind the browser's zero-copy read (non-nullable PLAIN columns, one row group, little-endian payloads) and v2-reader-parity classification and code order (part 1 rows and projections in the v2 browser's order, numeric inference over the placed proteins with the `placedNumeric` mark, a v1 hit split at its last pipe); NaN for uncovered projections, added unannotated rows, dimension from the data, `true`/`false` booleans, list columns as one hit per element, unstamped and v1 tables refused |
338
339
  | `test_bundle_v3_decode.py` | v3 decoder: `decode_v3(encode_v3(T)) == T` on pipeline-shaped tables, plus the deliberate canonicalisations; `sourceType` restoration (64-bit integers past 2^53 exactly), finite-only projection rows, manifest dimensions, payload tiling checks |
339
340
  | `test_bundle_v3_container.py` | Six-part container boundary: every write emits v3 with part 6 pinned, every read hands back v2-shaped tables, legacy bundles read as written, the delimiter guard covers part 6; detection by `protspace_container_version`, `style` writing a legacy input as v3, `replace_annotations` keeping label columns labels and the `placedNumeric` mark, a corrupt part 1 as a bundle error |
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: protspace
3
- Version: 4.16.2
3
+ Version: 4.16.4
4
4
  Summary: Prepare protein language model (pLM) embeddings for interactive exploration at protspace.app — embed, project, annotate, transfer labels (EAT), and bundle.
5
5
  Project-URL: Homepage, https://protspace.app
6
6
  Project-URL: Repository, https://github.com/tsenoner/protspace
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "protlabel"
3
- version = "4.16.2"
3
+ version = "4.16.4"
4
4
  description = "Embedding Annotation Transfer (EAT): nearest-neighbour label transfer in protein-language-model embedding space"
5
5
  authors = [{ name = "Tobias Senoner", email = "tobias.senoner@tum.de" }]
6
6
  readme = "README.md"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "protspace"
3
- version = "4.16.2"
3
+ version = "4.16.4"
4
4
  description = "Prepare protein language model (pLM) embeddings for interactive exploration at protspace.app — embed, project, annotate, transfer labels (EAT), and bundle."
5
5
  authors = [{ name = "Tobias Senoner", email = "tobias.senoner@tum.de" }]
6
6
  readme = "README.md"
@@ -136,6 +136,7 @@ select = [
136
136
  "ARG001", # unused-function-args
137
137
  "F401", # unused-import
138
138
  "F841", # unused-variable
139
+ "TID251", # banned-api: see flake8-tidy-imports.banned-api below
139
140
  ]
140
141
  ignore = [
141
142
  "E501", # line too long, handled by black
@@ -143,6 +144,11 @@ ignore = [
143
144
  "C901", # too complex
144
145
  ]
145
146
 
147
+ [tool.ruff.lint.flake8-tidy-imports.banned-api]
148
+ # Importing through `src.` loads a second copy of every protspace module next to the
149
+ # installed one, so a `patch("src.protspace...")` misses the copy the code under test uses.
150
+ "src".msg = "Import from `protspace`, not `src.protspace`."
151
+
146
152
  [tool.ruff.lint.isort]
147
153
  # protlabel lives in the packages/protlabel workspace member (not src/), so
148
154
  # declare both packages first-party for stable import grouping.
@@ -0,0 +1 @@
1
+ __version__ = "4.16.4"
@@ -3,7 +3,6 @@ import warnings
3
3
  from pathlib import Path
4
4
 
5
5
  from protspace.app import ProtSpace
6
- from protspace.data.io.bundle import extract_bundle_to_dir
7
6
 
8
7
  DEFAULT_PORT = 8050
9
8
 
@@ -11,18 +10,18 @@ warnings.filterwarnings("ignore", category=SyntaxWarning)
11
10
 
12
11
 
13
12
  def detect_data_type(data_path: str) -> str:
14
- """Resolve *data_path* to a directory of Parquet files.
13
+ """Resolve *data_path* to something ``ArrowReader`` reads.
15
14
 
16
- Accepts a ``.parquetbundle`` file (extracted to a temp dir) or a
17
- directory containing ``.parquet`` files.
15
+ Accepts a ``.parquetbundle`` file or a directory containing ``.parquet``
16
+ files (or a ``data.parquetbundle``).
18
17
 
19
18
  Returns:
20
- Path to the Parquet directory.
19
+ Path to the bundle file or the Parquet directory.
21
20
  """
22
21
  path = Path(data_path)
23
22
 
24
23
  if path.is_file() and path.suffix.lower() == ".parquetbundle":
25
- return extract_bundle_to_dir(path)
24
+ return str(path)
26
25
  elif path.is_dir():
27
26
  parquet_files = list(path.glob("*.parquet"))
28
27
  if parquet_files:
@@ -30,7 +29,7 @@ def detect_data_type(data_path: str) -> str:
30
29
 
31
30
  bundle_file = path / "data.parquetbundle"
32
31
  if bundle_file.exists():
33
- return extract_bundle_to_dir(bundle_file)
32
+ return str(bundle_file)
34
33
 
35
34
  raise ValueError(
36
35
  f"Directory '{data_path}' does not contain .parquet files or data.parquetbundle"
@@ -281,15 +281,8 @@ def generate_template(input_file: str) -> dict:
281
281
  with values in frequency-descending order. ``<NA>`` gets a default
282
282
  light-gray color.
283
283
  """
284
- data_format = detect_data_format(input_file)
285
-
286
- if data_format == "parquetbundle":
287
- from protspace.data.io.bundle import extract_bundle_to_dir
288
-
289
- temp_dir = extract_bundle_to_dir(Path(input_file))
290
- reader = ArrowReader(Path(temp_dir))
291
- elif data_format == "parquet":
292
- reader = ArrowReader(Path(input_file))
284
+ detect_data_format(input_file)
285
+ reader = ArrowReader(Path(input_file))
293
286
 
294
287
  frequencies = compute_value_frequencies(reader)
295
288
  template: dict = {}
@@ -366,7 +359,7 @@ def add_annotation_styles_bundle(
366
359
  ) -> None:
367
360
  """Add annotation styles to a .parquetbundle file.
368
361
 
369
- Extracts the bundle to a temp dir, applies styles via ArrowReader, converts
362
+ Reads the bundle with ArrowReader, applies styles, converts
370
363
  the visualization_state back to settings_json, then writes the output
371
364
  bundle with updated settings.
372
365
 
@@ -375,25 +368,15 @@ def add_annotation_styles_bundle(
375
368
  ``selectedPaletteId``) alongside ``colors`` and ``shapes``. These are
376
369
  forwarded to the settings converter.
377
370
  """
378
- from protspace.data.io.bundle import (
379
- SETTINGS_FILENAME,
380
- extract_bundle_to_dir,
381
- read_settings_from_file,
382
- replace_settings_in_bundle,
383
- )
371
+ from protspace.data.io.bundle import replace_settings_in_bundle
384
372
  from protspace.data.io.settings_converter import visualization_state_to_settings
385
373
 
386
- # Extract bundle so ArrowReader can load the data
387
- temp_dir = extract_bundle_to_dir(Path(bundle_file))
388
- reader = ArrowReader(Path(temp_dir))
374
+ reader = ArrowReader(Path(bundle_file))
389
375
 
390
- # Read existing settings (if any) to preserve extra fields. From the
391
- # extracted copy, not the bundle again: this command reads the bundle once
392
- # (and so logs a legacy-format warning once).
393
- settings_path = Path(temp_dir) / SETTINGS_FILENAME
394
- existing_settings = (
395
- read_settings_from_file(settings_path) if settings_path.exists() else None
396
- )
376
+ # Existing settings (if any), to preserve extra fields. From the reader,
377
+ # not the bundle again: this command reads the bundle once (and so logs a
378
+ # legacy-format warning once).
379
+ existing_settings = reader.settings
397
380
 
398
381
  # Collect settings-level overrides from the styles input
399
382
  style_overrides: dict[str, dict] = {}
@@ -28,27 +28,30 @@ def _read_format_version(schema_metadata: dict | None) -> int:
28
28
  class ArrowReader:
29
29
  """Read and manipulate ProtSpace data from Parquet files or a dict.
30
30
 
31
- Accepts either a ``Path`` to a directory of Parquet files or a
32
- pre-built ``dict`` (the same structure previously handled by the
33
- legacy ``JsonReader``).
31
+ Accepts a ``Path`` to a ``.parquetbundle`` file or to a directory of
32
+ Parquet files, or a pre-built ``dict`` (the same structure previously
33
+ handled by the legacy ``JsonReader``).
34
34
  """
35
35
 
36
36
  def __init__(self, source: Path | dict):
37
37
  """
38
38
  Args:
39
- source: Path to directory containing .parquet files, or a
40
- pre-built data dict (protein_data, projections, …).
39
+ source: Path to a .parquetbundle file or to a directory
40
+ containing .parquet files, or a pre-built data dict
41
+ (protein_data, projections, …).
41
42
  """
42
43
  if isinstance(source, dict):
43
44
  # Direct dict input
44
45
  self.data = source
45
46
  self.data_path = None
47
+ self.settings = None
46
48
  self._protein_annotations_df = None
47
49
  self._projections_metadata_df = None
48
50
  self._projections_data_df = None
49
51
  else:
50
52
  # Parquet file loading
51
53
  self.data_path = Path(source)
54
+ self.settings = None
52
55
  self._protein_annotations_df = None
53
56
  self._projections_metadata_df = None
54
57
  self._projections_data_df = None
@@ -61,43 +64,61 @@ class ArrowReader:
61
64
  self._build_data_structure()
62
65
 
63
66
  def _load_data(self):
64
- """Load data from Parquet files."""
65
- try:
66
- protein_annotations_path = self.data_path / "selected_annotations.parquet"
67
- projections_metadata_path = self.data_path / "projections_metadata.parquet"
68
- projections_data_path = self.data_path / "projections_data.parquet"
69
-
70
- if protein_annotations_path.exists():
71
- annotations_table = pq.read_table(str(protein_annotations_path))
72
- self._protein_annotations_df = annotations_table.to_pandas()
73
- self.data["format_version"] = _read_format_version(
74
- annotations_table.schema.metadata
75
- )
76
- else:
77
- self._protein_annotations_df = pd.DataFrame(columns=["protein_id"])
78
-
79
- if projections_metadata_path.exists():
80
- self._projections_metadata_df = pq.read_table(
81
- str(projections_metadata_path)
82
- ).to_pandas()
83
- else:
84
- self._projections_metadata_df = pd.DataFrame(
85
- columns=["projection_name", "dimensions", "info_json"]
86
- )
67
+ """Load the core tables from a .parquetbundle file or a directory."""
68
+ path = self.data_path
69
+ if path.is_file():
70
+ tables = self._read_bundle(path)
71
+ elif path.is_dir():
72
+ tables = self._read_directory(path)
73
+ else:
74
+ raise FileNotFoundError(f"No such file or directory: '{path}'")
87
75
 
88
- if projections_data_path.exists():
89
- self._projections_data_df = pq.read_table(
90
- str(projections_data_path)
91
- ).to_pandas()
92
- else:
93
- self._projections_data_df = pd.DataFrame(
94
- columns=["projection_name", "identifier", "x", "y", "z"]
95
- )
76
+ annotations, metadata, projections = tables
77
+ if annotations is not None:
78
+ self._protein_annotations_df = annotations.to_pandas()
79
+ self.data["format_version"] = _read_format_version(
80
+ annotations.schema.metadata
81
+ )
82
+ else:
83
+ self._protein_annotations_df = pd.DataFrame(columns=["protein_id"])
84
+ self._projections_metadata_df = (
85
+ metadata.to_pandas()
86
+ if metadata is not None
87
+ else pd.DataFrame(columns=["projection_name", "dimensions", "info_json"])
88
+ )
89
+ self._projections_data_df = (
90
+ projections.to_pandas()
91
+ if projections is not None
92
+ else pd.DataFrame(columns=["projection_name", "identifier", "x", "y", "z"])
93
+ )
96
94
 
97
- except Exception as e:
95
+ def _read_bundle(self, path: Path) -> tuple[pa.Table, pa.Table, pa.Table]:
96
+ """Read a bundle in memory, whatever its container version."""
97
+ from protspace.data.io.bundle import read_bundle_contents
98
+
99
+ contents = read_bundle_contents(path)
100
+ self.settings = contents.settings
101
+ return contents.annotations, contents.metadata, contents.projections
102
+
103
+ def _read_directory(self, path: Path) -> tuple[pa.Table | None, ...]:
104
+ """Read the core tables a directory holds; a missing one reads as None.
105
+
106
+ An annotations-only directory is valid, but one holding none of the core
107
+ tables is not ProtSpace data and must not read as an empty dataset.
108
+ """
109
+ from protspace.data.io.bundle import CORE_FILENAMES
110
+
111
+ core_paths = [path / name for name in CORE_FILENAMES]
112
+ if not any(p.exists() for p in core_paths):
98
113
  raise ValueError(
99
- f"Error loading Arrow data from {self.data_path}: {e}"
100
- ) from e
114
+ f"Directory '{path}' contains none of {', '.join(CORE_FILENAMES)}."
115
+ )
116
+ try:
117
+ return tuple(
118
+ pq.read_table(str(p)) if p.exists() else None for p in core_paths
119
+ )
120
+ except Exception as e:
121
+ raise ValueError(f"Error loading Arrow data from {path}: {e}") from e
101
122
 
102
123
  def _build_data_structure(self):
103
124
  """Build the data structure matching the ProtSpace data schema."""
@@ -158,65 +179,85 @@ class ArrowReader:
158
179
  def _load_visualization_state(self):
159
180
  """Load visualization state.
160
181
 
161
- Priority: visualization_state.json > settings.parquet (from bundle).
182
+ A bundle carries it in its settings part. A directory may hold
183
+ visualization_state.json, which wins over settings.parquet.
162
184
  """
163
- viz_state_path = self.data_path / "visualization_state.json"
164
- if viz_state_path.exists():
165
- try:
166
- with open(viz_state_path) as f:
167
- viz_state = json.load(f)
168
- self.data["visualization_state"] = viz_state
169
- return
170
- except (json.JSONDecodeError, FileNotFoundError):
171
- pass
172
-
173
- # Fall back to settings.parquet (extracted from a 4-part bundle)
174
- settings_path = self.data_path / "settings.parquet"
175
- if settings_path.exists():
176
- try:
177
- from protspace.data.io.bundle import read_settings_from_file
178
- from protspace.data.io.settings_converter import (
179
- settings_to_visualization_state,
180
- )
185
+ if self.data_path.is_dir():
186
+ viz_state_path = self.data_path / "visualization_state.json"
187
+ if viz_state_path.exists():
188
+ try:
189
+ with open(viz_state_path) as f:
190
+ self.data["visualization_state"] = json.load(f)
191
+ return
192
+ except (json.JSONDecodeError, FileNotFoundError):
193
+ pass
194
+
195
+ try:
196
+ from protspace.data.io.bundle import (
197
+ SETTINGS_FILENAME,
198
+ read_settings_from_file,
199
+ )
200
+ from protspace.data.io.settings_converter import (
201
+ settings_to_visualization_state,
202
+ )
181
203
 
182
- settings = read_settings_from_file(settings_path)
204
+ settings_path = self.data_path / SETTINGS_FILENAME
205
+ if self.data_path.is_dir() and settings_path.exists():
206
+ self.settings = read_settings_from_file(settings_path)
207
+ if self.settings:
183
208
  self.data["visualization_state"] = settings_to_visualization_state(
184
- settings
185
- )
186
- except Exception:
187
- logger.debug(
188
- "Failed to load visualization state from settings", exc_info=True
209
+ self.settings
189
210
  )
211
+ except Exception:
212
+ logger.debug(
213
+ "Failed to load visualization state from settings", exc_info=True
214
+ )
215
+
216
+ def save_data(self, output_path: Path | None = None):
217
+ """Save the current data back to parquet files.
190
218
 
191
- def save_data(self, output_path: Path = None):
192
- """Save the current data back to parquet files."""
219
+ Defaults to the directory the data was read from. A reader built from
220
+ a .parquetbundle needs an explicit ``output_path``: the bundle is a
221
+ file, not a directory the tables could be written into.
222
+ """
223
+ if self.data_path is None:
224
+ raise ValueError(
225
+ "save_data writes the parquet tables a reader loaded; this "
226
+ "reader was built from a dict and holds none."
227
+ )
193
228
  if output_path is None:
229
+ if self.data_path.is_file():
230
+ raise ValueError(
231
+ f"'{self.data_path}' is a bundle file, not a directory; "
232
+ "pass save_data an output directory."
233
+ )
194
234
  output_path = self.data_path
195
235
  else:
196
236
  output_path = Path(output_path)
197
237
 
198
238
  output_path.mkdir(parents=True, exist_ok=True)
199
239
 
200
- # Save protein annotations
201
- protein_annotations_path = output_path / "protein_annotations.parquet"
202
- protein_annotations_table = pa.Table.from_pandas(self._protein_annotations_df)
240
+ # The names _read_directory reads back (and a bundle's parts extract to)
241
+ from protspace.data.io.bundle import CORE_FILENAMES
242
+
243
+ annotations, metadata, projections = (
244
+ pa.Table.from_pandas(df)
245
+ for df in (
246
+ self._protein_annotations_df,
247
+ self._projections_metadata_df,
248
+ self._projections_data_df,
249
+ )
250
+ )
203
251
  if self.should_decode():
204
252
  # The cells are written back as they were read, so they keep the
205
253
  # grammar stamp that says so; unstamped they would read as v1.
206
254
  from protspace.data.annotations.encoding import stamp_format_version
207
255
 
208
- protein_annotations_table = stamp_format_version(protein_annotations_table)
209
- pq.write_table(protein_annotations_table, str(protein_annotations_path))
210
-
211
- # Save projections metadata
212
- projections_metadata_path = output_path / "projections_metadata.parquet"
213
- projections_metadata_table = pa.Table.from_pandas(self._projections_metadata_df)
214
- pq.write_table(projections_metadata_table, str(projections_metadata_path))
215
-
216
- # Save projections data
217
- projections_data_path = output_path / "projections_data.parquet"
218
- projections_data_table = pa.Table.from_pandas(self._projections_data_df)
219
- pq.write_table(projections_data_table, str(projections_data_path))
256
+ annotations = stamp_format_version(annotations)
257
+ for name, table in zip(
258
+ CORE_FILENAMES, (annotations, metadata, projections), strict=True
259
+ ):
260
+ pq.write_table(table, str(output_path / name))
220
261
 
221
262
  # Save visualization state as a separate JSON file
222
263
  viz_state_path = output_path / "visualization_state.json"