aipscan 0.9.0.dev10__tar.gz → 0.9.0rc2__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 (182) hide show
  1. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/.gitignore +1 -0
  2. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Aggregator/database_helpers.py +22 -6
  3. aipscan-0.9.0rc2/AIPscan/Aggregator/downloads.py +24 -0
  4. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Aggregator/forms.py +9 -2
  5. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Aggregator/mets_parse_helpers.py +23 -19
  6. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Aggregator/task_helpers.py +3 -3
  7. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Aggregator/tasks.py +63 -43
  8. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Aggregator/templates/edit_storage_service.html +8 -2
  9. aipscan-0.9.0rc2/AIPscan/Aggregator/tests/fixtures/legacy_mets/README.md +23 -0
  10. aipscan-0.9.0rc2/AIPscan/Aggregator/tests/fixtures/legacy_mets/production-aip-mets-file.xml +833 -0
  11. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Aggregator/tests/fixtures/package_json/packages.json +1 -1
  12. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Aggregator/tests/test_database_helpers.py +162 -31
  13. aipscan-0.9.0rc2/AIPscan/Aggregator/tests/test_fetch_cli.py +107 -0
  14. aipscan-0.9.0rc2/AIPscan/Aggregator/tests/test_forms.py +42 -0
  15. aipscan-0.9.0rc2/AIPscan/Aggregator/tests/test_import_failures.py +142 -0
  16. aipscan-0.9.0rc2/AIPscan/Aggregator/tests/test_mets.py +118 -0
  17. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Aggregator/tests/test_task_helpers.py +17 -9
  18. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Aggregator/tests/test_tasks.py +152 -1
  19. aipscan-0.9.0rc2/AIPscan/Aggregator/tests/test_views.py +290 -0
  20. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Aggregator/views.py +30 -15
  21. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Data/data.py +11 -26
  22. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Data/report_data.py +90 -28
  23. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Data/report_data_typesense.py +54 -66
  24. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Data/tests/test_aip_contents.py +50 -0
  25. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Data/tests/test_format_versions_count.py +2 -2
  26. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Data/tests/test_formats_count.py +2 -2
  27. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Reporter/report_aip_contents.py +7 -3
  28. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Reporter/report_formats_count.py +15 -5
  29. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Reporter/report_ingest_log.py +3 -1
  30. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Reporter/report_storage_locations.py +1 -1
  31. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Reporter/templates/file.html +1 -1
  32. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Reporter/templates/report_aips_by_puid.html +2 -2
  33. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Reporter/templates/report_format_versions_count.html +1 -1
  34. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Reporter/templates/report_formats_count.html +1 -1
  35. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Reporter/templates/report_ingest_log_tabular.html +2 -2
  36. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Reporter/templates/report_largest_aips.html +1 -1
  37. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Reporter/templates/report_storage_locations_usage_over_time.html +1 -1
  38. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Reporter/templates/reports.html +3 -3
  39. aipscan-0.9.0rc2/AIPscan/Reporter/tests/test_aip_contents.py +90 -0
  40. aipscan-0.9.0rc2/AIPscan/Reporter/tests/test_formats_count.py +205 -0
  41. aipscan-0.9.0rc2/AIPscan/Reporter/tests/test_indexed_metadata.py +98 -0
  42. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Reporter/tests/test_ingest_log.py +5 -2
  43. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Reporter/tests/test_largest_aips.py +19 -0
  44. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Reporter/tests/test_views.py +19 -0
  45. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Reporter/views.py +4 -2
  46. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/__init__.py +18 -5
  47. aipscan-0.9.0rc2/AIPscan/celery.py +46 -0
  48. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/config.py +21 -24
  49. aipscan-0.9.0rc2/AIPscan/conftest.py +572 -0
  50. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/helpers.py +1 -1
  51. aipscan-0.9.0rc2/AIPscan/migrations/README +1 -0
  52. aipscan-0.9.0rc2/AIPscan/migrations/alembic.ini +50 -0
  53. aipscan-0.9.0rc2/AIPscan/migrations/env.py +130 -0
  54. aipscan-0.9.0rc2/AIPscan/migrations/script.py.mako +24 -0
  55. aipscan-0.9.0rc2/AIPscan/migrations/versions/c605f2284613_initial_migration.py +299 -0
  56. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/models.py +21 -41
  57. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/static/css/custom.css +3 -2
  58. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/static/js/aggregator/storage_service.js +25 -8
  59. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/static/js/reporter/plot_formats_count.js +2 -2
  60. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/static/js/reporter/reports.js +34 -34
  61. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/test_helpers.py +72 -15
  62. aipscan-0.9.0rc2/AIPscan/tests/test_app.py +46 -0
  63. aipscan-0.9.0rc2/AIPscan/tests/test_celery_config.py +55 -0
  64. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/tests/test_cli.py +8 -26
  65. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/tests/test_typesense_helpers.py +51 -8
  66. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/typesense_helpers.py +39 -8
  67. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/typesense_test_helpers.py +15 -5
  68. aipscan-0.9.0rc2/PKG-INFO +123 -0
  69. aipscan-0.9.0rc2/README.md +87 -0
  70. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/build_config/base.config.js +8 -10
  71. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/build_config/chart_formats_count.config.js +8 -10
  72. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/build_config/entry/shared/base_shared.js +0 -2
  73. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/build_config/plot_formats_count.config.js +10 -12
  74. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/build_config/report_base.config.js +8 -10
  75. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/package-lock.json +746 -1128
  76. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/package.json +6 -7
  77. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/pyproject.toml +25 -11
  78. aipscan-0.9.0.dev10/AIPscan/Aggregator/tests/test_mets.py +0 -54
  79. aipscan-0.9.0.dev10/AIPscan/Aggregator/tests/test_views.py +0 -142
  80. aipscan-0.9.0.dev10/AIPscan/Reporter/tests/test_aip_contents.py +0 -23
  81. aipscan-0.9.0.dev10/AIPscan/Reporter/tests/test_formats_count.py +0 -92
  82. aipscan-0.9.0.dev10/AIPscan/celery.py +0 -27
  83. aipscan-0.9.0.dev10/AIPscan/conftest.py +0 -609
  84. aipscan-0.9.0.dev10/PKG-INFO +0 -381
  85. aipscan-0.9.0.dev10/README.md +0 -342
  86. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/API/__init__.py +0 -0
  87. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/API/fields.py +0 -0
  88. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/API/namespace_data.py +0 -0
  89. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/API/namespace_infos.py +0 -0
  90. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/API/namespace_report_data.py +0 -0
  91. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/API/views.py +0 -0
  92. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Aggregator/__init__.py +0 -0
  93. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Aggregator/celery_helpers.py +0 -0
  94. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Aggregator/templates/confirm.html +0 -0
  95. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Aggregator/templates/result_message.html +0 -0
  96. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Aggregator/templates/storage_service.html +0 -0
  97. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Aggregator/templates/storage_services.html +0 -0
  98. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Aggregator/tests/__init__.py +0 -0
  99. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Aggregator/tests/fixtures/features_mets/features-mets-added-agents.xml +0 -0
  100. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Aggregator/tests/fixtures/features_mets/features-mets.xml +0 -0
  101. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Aggregator/tests/fixtures/images_mets/images.xml +0 -0
  102. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Aggregator/tests/fixtures/iso_mets/iso_mets.xml +0 -0
  103. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Aggregator/tests/fixtures/original_name_mets/dataverse_example.xml +0 -0
  104. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Aggregator/tests/fixtures/original_name_mets/document-empty-dirs.xml +0 -0
  105. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Aggregator/tests/test_types.py +0 -0
  106. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Aggregator/types.py +0 -0
  107. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Data/__init__.py +0 -0
  108. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Data/fields.py +0 -0
  109. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Data/tests/__init__.py +0 -0
  110. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Data/tests/test_agents_transfers.py +0 -0
  111. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Data/tests/test_aips_by_format.py +0 -0
  112. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Data/tests/test_aips_by_puid.py +0 -0
  113. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Data/tests/test_largest_aips.py +0 -0
  114. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Data/tests/test_largest_files.py +0 -0
  115. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Data/tests/test_preservation_derivatives.py +0 -0
  116. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Data/tests/test_storage_locations.py +0 -0
  117. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Home/__init__.py +0 -0
  118. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Home/views.py +0 -0
  119. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Reporter/__init__.py +0 -0
  120. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Reporter/database_helpers.py +0 -0
  121. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Reporter/helpers.py +0 -0
  122. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Reporter/report_aips_by_format.py +0 -0
  123. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Reporter/report_aips_by_puid.py +0 -0
  124. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Reporter/report_format_versions_count.py +0 -0
  125. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Reporter/report_largest_aips.py +0 -0
  126. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Reporter/report_largest_files.py +0 -0
  127. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Reporter/report_preservation_derivatives.py +0 -0
  128. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Reporter/request_params.py +0 -0
  129. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Reporter/templates/aip.html +0 -0
  130. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Reporter/templates/aips.html +0 -0
  131. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Reporter/templates/chart_formats_count.html +0 -0
  132. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Reporter/templates/plot_formats_count.html +0 -0
  133. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Reporter/templates/report_aip_contents.html +0 -0
  134. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Reporter/templates/report_aips_by_format.html +0 -0
  135. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Reporter/templates/report_buttons.html +0 -0
  136. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Reporter/templates/report_buttons_print_only.html +0 -0
  137. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Reporter/templates/report_ingest_log_gantt.html +0 -0
  138. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Reporter/templates/report_largest_files.html +0 -0
  139. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Reporter/templates/report_preservation_derivatives.html +0 -0
  140. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Reporter/templates/report_storage_locations.html +0 -0
  141. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Reporter/tests/__init__.py +0 -0
  142. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Reporter/tests/fixtures/storage_location_usage_cumulative_aips.csv +0 -0
  143. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Reporter/tests/fixtures/storage_location_usage_cumulative_files.csv +0 -0
  144. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Reporter/tests/fixtures/storage_location_usage_cumulative_size.csv +0 -0
  145. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Reporter/tests/fixtures/storage_location_usage_differential_aips.csv +0 -0
  146. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Reporter/tests/fixtures/storage_location_usage_differential_files.csv +0 -0
  147. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Reporter/tests/fixtures/storage_location_usage_differential_size.csv +0 -0
  148. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Reporter/tests/test_aips_by_file_format.py +0 -0
  149. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Reporter/tests/test_aips_by_puid.py +0 -0
  150. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Reporter/tests/test_format_versions_count.py +0 -0
  151. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Reporter/tests/test_helpers.py +0 -0
  152. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Reporter/tests/test_largest_files.py +0 -0
  153. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Reporter/tests/test_preservation_derivatives.py +0 -0
  154. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Reporter/tests/test_storage_locations.py +0 -0
  155. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/Reporter/tests/test_update_dates.py +0 -0
  156. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/User/__init__.py +0 -0
  157. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/User/forms.py +0 -0
  158. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/User/views.py +0 -0
  159. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/cli.py +0 -0
  160. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/decorators.py +0 -0
  161. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/navbar.py +0 -0
  162. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/run.py +0 -0
  163. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/static/css/style_ingest_log.css +0 -0
  164. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/static/favicon.ico +0 -0
  165. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/static/js/report.js +0 -0
  166. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/static/js/reporter/aips.js +0 -0
  167. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/static/js/reporter/chart_formats_count.js +0 -0
  168. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/templates/base.html +0 -0
  169. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/templates/error/404.html +0 -0
  170. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/templates/partials/navbar.html +0 -0
  171. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/templates/report_base.html +0 -0
  172. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/tests/__init__.py +0 -0
  173. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/tests/test_helpers.py +0 -0
  174. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/tests/test_models.py +0 -0
  175. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/tests/test_typsense_test_helpers.py +0 -0
  176. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/AIPscan/worker.py +0 -0
  177. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/LICENSE +0 -0
  178. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/build.py +0 -0
  179. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/build_config/entry/base.entry.js +0 -0
  180. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/build_config/entry/chart_formats_count.entry.js +0 -0
  181. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/build_config/entry/plot_formats_count.entry.js +0 -0
  182. {aipscan-0.9.0.dev10 → aipscan-0.9.0rc2}/build_config/entry/report_base.entry.js +0 -0
@@ -10,3 +10,4 @@ node_modules/
10
10
  __pycache__/
11
11
  *.py[codz]
12
12
  .DS_Store
13
+ /output
@@ -2,13 +2,15 @@
2
2
  database.
3
3
  """
4
4
 
5
- from datetime import datetime
5
+ import os
6
6
 
7
7
  from celery.utils.log import get_task_logger
8
+ from dateutil.parser import parse
8
9
  from lxml import etree
9
10
 
10
11
  from AIPscan import db
11
12
  from AIPscan.Aggregator import tasks
13
+ from AIPscan.Aggregator.downloads import get_download_root
12
14
  from AIPscan.Aggregator.task_helpers import _tz_neutral_date
13
15
  from AIPscan.Aggregator.task_helpers import get_storage_service_api_url
14
16
  from AIPscan.models import AIP
@@ -247,6 +249,16 @@ def create_or_update_pipeline(origin_pipeline, storage_service):
247
249
  return pipeline
248
250
 
249
251
 
252
+ def _optional_file_date(value):
253
+ """Keep absent or unusable PREMIS file dates unknown."""
254
+ if value is None or value == () or value == "":
255
+ return None
256
+ try:
257
+ return parse(value).replace(tzinfo=None, microsecond=0)
258
+ except ValueError, TypeError, OverflowError:
259
+ return None
260
+
261
+
250
262
  def _get_file_properties(fs_entry):
251
263
  """Retrieve file properties from FSEntry
252
264
 
@@ -270,14 +282,17 @@ def _get_file_properties(fs_entry):
270
282
 
271
283
  try:
272
284
  for premis_object in fs_entry.get_premis_objects():
273
- file_info["size"] = premis_object.size
285
+ size = premis_object.size
286
+ file_info["size"] = None if size == () else size
274
287
  key_alias = premis_object.format_registry_key
275
- file_info["date_created"] = _tz_neutral_date(
288
+ file_info["date_created"] = _optional_file_date(
276
289
  premis_object.date_created_by_application
277
290
  )
278
291
  if not isinstance(key_alias, tuple):
279
292
  file_info["puid"] = key_alias
280
- file_info["file_format"] = premis_object.format_name
293
+ name_alias = premis_object.format_name
294
+ if not isinstance(name_alias, tuple):
295
+ file_info["file_format"] = name_alias
281
296
  version_alias = premis_object.format_version
282
297
  if not isinstance(version_alias, tuple):
283
298
  file_info["format_version"] = version_alias
@@ -406,13 +421,15 @@ def process_aip_data(aip, mets):
406
421
 
407
422
 
408
423
  def create_fetch_job(datetime_obj_start, timestamp_str, storage_server_id):
424
+ download_root = get_download_root()
425
+ download_directory = os.path.join(download_root, timestamp_str, "")
409
426
  fetch_job = FetchJob(
410
427
  total_packages=None,
411
428
  total_deleted_aips=None,
412
429
  total_aips=None,
413
430
  download_start=datetime_obj_start,
414
431
  download_end=None,
415
- download_directory=f"AIPscan/Aggregator/downloads/{timestamp_str}/",
432
+ download_directory=download_directory,
416
433
  storage_service_id=storage_server_id,
417
434
  )
418
435
  db.session.add(fetch_job)
@@ -453,7 +470,6 @@ def update_fetch_job(fetch_job_id, processed_packages, total_packages_count):
453
470
  obj.total_sips = total_sips
454
471
  obj.total_replicas = total_replicas
455
472
  obj.total_deleted_aips = total_deleted_aips
456
- obj.download_end = datetime.now().replace(microsecond=0)
457
473
  db.session.commit()
458
474
 
459
475
  return obj
@@ -0,0 +1,24 @@
1
+ """Helpers for working with Aggregator download directories."""
2
+
3
+ import os
4
+ from importlib import resources
5
+
6
+ from flask import current_app
7
+
8
+ DEFAULT_DOWNLOAD_ROOT = os.fspath(resources.files(__package__).joinpath("downloads"))
9
+
10
+
11
+ def get_download_root(app=None) -> str:
12
+ """Return the base directory used to stage download artifacts."""
13
+ if app is not None:
14
+ return app.config.get("AGGREGATOR_DOWNLOAD_ROOT", DEFAULT_DOWNLOAD_ROOT)
15
+
16
+ try:
17
+ application = current_app._get_current_object()
18
+ except RuntimeError:
19
+ env_override = os.getenv("AGGREGATOR_DOWNLOAD_ROOT")
20
+ if env_override:
21
+ return env_override
22
+ return DEFAULT_DOWNLOAD_ROOT
23
+
24
+ return application.config.get("AGGREGATOR_DOWNLOAD_ROOT", DEFAULT_DOWNLOAD_ROOT)
@@ -1,7 +1,10 @@
1
1
  from flask_wtf import FlaskForm
2
2
  from wtforms import BooleanField
3
+ from wtforms import IntegerField
3
4
  from wtforms import StringField
4
5
  from wtforms.validators import DataRequired
6
+ from wtforms.validators import InputRequired
7
+ from wtforms.validators import NumberRange
5
8
  from wtforms.widgets import PasswordInput
6
9
 
7
10
 
@@ -12,6 +15,10 @@ class StorageServiceForm(FlaskForm):
12
15
  api_key = StringField(
13
16
  "API key", validators=[DataRequired()], widget=PasswordInput(hide_value=False)
14
17
  )
15
- download_limit = StringField("Download limit", default="20")
16
- download_offset = StringField("Download offset", default="0")
18
+ download_limit = IntegerField(
19
+ "Download limit", default=20, validators=[InputRequired(), NumberRange(min=1)]
20
+ )
21
+ download_offset = IntegerField(
22
+ "Download offset", default=0, validators=[InputRequired(), NumberRange(min=0)]
23
+ )
17
24
  default = BooleanField("Default")
@@ -40,17 +40,14 @@ def parse_mets_with_metsrw(mets_file):
40
40
  return mets
41
41
 
42
42
 
43
- def get_aip_original_name(mets):
44
- """Retrieve PREMIS original name from a METSDocument object.
43
+ def get_aip_original_name(mets, package_uuid):
44
+ """Retrieve the package name from PREMIS or its physical directory label.
45
45
 
46
46
  If the original name cannot be reliably retrieved from the METS file
47
47
  a METSError exception is returned to be handled by the caller as
48
48
  desired.
49
49
  """
50
50
 
51
- # Negated as we're going to want to remove this length of values.
52
- NAMESUFFIX = -len("-00000000-0000-0000-0000-000000000000")
53
-
54
51
  # The transfer directory prefix is a directory prefix that can also
55
52
  # exist in a dmdSec intellectual entity and we want to identify and
56
53
  # ignore those.
@@ -66,16 +63,22 @@ def get_aip_original_name(mets):
66
63
  full_name = dmd_element.find(
67
64
  ELEM_ORIGINAL_NAME_PATTERN, namespaces=NAMESPACES
68
65
  )
69
- if full_name is not None and full_name.text.startswith(TRANSFER_DIR_PREFIX):
66
+ if full_name is None or not full_name.text:
67
+ continue
68
+ if full_name.text.startswith(TRANSFER_DIR_PREFIX):
70
69
  # We don't want this value, it will usually represent an
71
70
  # directory entity.
72
71
  continue
73
- try:
74
- original_name = full_name.text[:NAMESUFFIX]
75
- except AttributeError:
76
- continue
72
+ original_name = full_name.text
77
73
 
78
- # There should be a transfer name in every METS.
74
+ # Older Archivematica METS only identify the package in the physical
75
+ # structure map. METSRW represents its top-level divs as parentless entries.
76
+ if not original_name:
77
+ roots = [entry for entry in mets.all_files() if entry.parent is None]
78
+ if len(roots) == 1 and roots[0].type == "Directory":
79
+ original_name = roots[0].label or ""
80
+
81
+ original_name = original_name.removesuffix(f"-{package_uuid}")
79
82
  if original_name == "":
80
83
  raise METSError("Cannot locate transfer name in METS")
81
84
 
@@ -94,14 +97,15 @@ def download_mets(
94
97
  numbered_subdir = create_numbered_subdirs(timestamp, package_list_no)
95
98
  download_file = os.path.join(numbered_subdir, f"METS.{package_uuid}.xml")
96
99
 
97
- mets_response = requests.get(
98
- get_mets_url(storage_service, package_uuid, relative_path_to_mets),
99
- stream=True,
100
- )
101
-
102
100
  try:
103
- mets_hash = stream_write_and_hash(mets_response, download_file)
104
- finally:
105
- mets_response.close()
101
+ with requests.get(
102
+ get_mets_url(storage_service, package_uuid, relative_path_to_mets),
103
+ stream=True,
104
+ timeout=(10, 120),
105
+ ) as mets_response:
106
+ mets_response.raise_for_status()
107
+ mets_hash = stream_write_and_hash(mets_response, download_file)
108
+ except requests.RequestException:
109
+ raise METSError(f"Cannot download METS for AIP {package_uuid}") from None
106
110
 
107
111
  return download_file, mets_hash
@@ -9,6 +9,7 @@ from datetime import datetime
9
9
  from dateutil.parser import ParserError
10
10
  from dateutil.parser import parse
11
11
 
12
+ from AIPscan.Aggregator.downloads import get_download_root
12
13
  from AIPscan.Aggregator.types import StorageServicePackage
13
14
 
14
15
 
@@ -27,7 +28,7 @@ def get_packages_directory(timestamp):
27
28
  """Create a path which we will use to store packages downloaded from
28
29
  the storage service plus other metadata.
29
30
  """
30
- return os.path.join("AIPscan", "Aggregator", "downloads", timestamp, "packages")
31
+ return os.path.join(get_download_root(), timestamp, "packages")
31
32
 
32
33
 
33
34
  def parse_package_list_file(filepath, logger=None, remove_after_parsing=False):
@@ -129,12 +130,11 @@ def create_numbered_subdirs(timestamp, package_list_number):
129
130
  """Check for the existence and create a container folder for our
130
131
  METS files as required.
131
132
  """
132
- AGGREGATOR_DOWNLOADS = os.path.join("AIPscan", "Aggregator", "downloads")
133
133
  METS_FOLDER = "mets"
134
134
 
135
135
  # Create a package list numbered subdirectory if it doesn't exist.
136
136
  numbered_subdir = os.path.join(
137
- AGGREGATOR_DOWNLOADS, timestamp, METS_FOLDER, str(package_list_number)
137
+ get_download_root(), timestamp, METS_FOLDER, str(package_list_number)
138
138
  )
139
139
  if not os.path.exists(numbered_subdir):
140
140
  os.makedirs(numbered_subdir)
@@ -1,10 +1,12 @@
1
1
  import json
2
2
  import os
3
3
  import shutil
4
+ import time
4
5
  from datetime import datetime
5
6
 
6
7
  import requests
7
8
  from celery.utils.log import get_task_logger
9
+ from flask import current_app
8
10
 
9
11
  from AIPscan import db
10
12
  from AIPscan import typesense_helpers
@@ -127,15 +129,25 @@ def workflow_coordinator(
127
129
  # Wait for package lists task to finish downloading all package
128
130
  # lists.
129
131
  task = package_lists_request.AsyncResult(package_lists_task.id, app=celery)
130
- while True:
131
- if (task.state == "SUCCESS") or (task.state == "FAILURE"):
132
- break
133
-
134
- if isinstance(package_lists_task.info, TaskError):
135
- # Re-raise.
136
- raise (package_lists_task.info)
132
+ idle_timeout = current_app.config["PACKAGE_LIST_IDLE_TIMEOUT_SECONDS"]
133
+ deadline = time.monotonic() + idle_timeout
134
+ completed_lists = 0
135
+ while task.state not in {"SUCCESS", "FAILURE"}:
136
+ now = time.monotonic()
137
+ info = task.info
138
+ progress = info if isinstance(info, dict) else {}
139
+ if progress.get("completedPackageLists", 0) > completed_lists:
140
+ completed_lists = progress["completedPackageLists"]
141
+ deadline = now + idle_timeout
142
+ if now >= deadline:
143
+ raise TaskError(
144
+ f"Package listing made no progress for {idle_timeout} seconds"
145
+ )
146
+ time.sleep(0.2)
147
+ if task.state == "FAILURE":
148
+ raise TaskError("Package listing failed; check the worker logs")
137
149
 
138
- total_package_lists = package_lists_task.info["totalPackageLists"]
150
+ total_package_lists = task.info["totalPackageLists"]
139
151
 
140
152
  all_packages = []
141
153
  for package_list_no in range(1, total_package_lists + 1):
@@ -152,7 +164,7 @@ def workflow_coordinator(
152
164
 
153
165
  all_packages = all_packages + packages
154
166
 
155
- total_packages_count = package_lists_task.info["totalPackages"]
167
+ total_packages_count = task.info["totalPackages"]
156
168
 
157
169
  obj = database_helpers.update_fetch_job(
158
170
  fetch_job_id, all_packages, total_packages_count
@@ -165,7 +177,11 @@ def make_request(request_url, request_url_without_api_key):
165
177
  """Make our request to the storage service and return a valid
166
178
  response to our caller or raise a TaskError for celery.
167
179
  """
168
- response = requests.get(request_url)
180
+ try:
181
+ response = requests.get(request_url, timeout=(10, 120))
182
+ except requests.RequestException:
183
+ # Request exceptions may contain the URL, including its API key.
184
+ raise TaskError(f"Cannot connect to: {request_url_without_api_key}") from None
169
185
  if response.status_code != requests.codes.ok:
170
186
  err = f"Check the URL and API details, cannot connect to: `{request_url_without_api_key}`"
171
187
  logger.error(err)
@@ -225,7 +241,8 @@ def package_lists_request(self, storage_service_id, timestamp, packages_director
225
241
  self.update_state(
226
242
  state=IN_PROGRESS,
227
243
  meta={
228
- "message": f"Total packages: {total_packages} Total package lists: {total_package_lists}"
244
+ "completedPackageLists": packages_count,
245
+ "message": f"Total packages: {total_packages} Total package lists: {total_package_lists}",
229
246
  },
230
247
  )
231
248
 
@@ -292,8 +309,6 @@ def get_mets(
292
309
  This function relies on being able to use mets-reader-writer which
293
310
  is the primary object we will be passing about.
294
311
 
295
- TODO: Log METS errors.
296
-
297
312
  The "customlogger" argument allows an external logger to be specified when
298
313
  the task's logic is executed, using the task's "apply" method, by an
299
314
  external application like a batch script.
@@ -305,13 +320,16 @@ def get_mets(
305
320
 
306
321
  # Download METS file
307
322
  storage_service = db.session.get(StorageService, storage_service_id)
308
- download_file, mets_hash = download_mets(
309
- storage_service,
310
- package_uuid,
311
- relative_path_to_mets,
312
- timestamp_str,
313
- package_list_no,
314
- )
323
+ try:
324
+ download_file, mets_hash = download_mets(
325
+ storage_service,
326
+ package_uuid,
327
+ relative_path_to_mets,
328
+ timestamp_str,
329
+ package_list_no,
330
+ )
331
+ except METSError as err:
332
+ raise TaskError(str(err)) from err
315
333
  mets_name = os.path.basename(download_file)
316
334
 
317
335
  # If METS file's hash matches an existing value, this is a duplicate of an
@@ -331,12 +349,11 @@ def get_mets(
331
349
 
332
350
  try:
333
351
  mets = parse_mets_with_metsrw(download_file)
334
- except METSError:
335
- # An error we need to log and report back to the user.
336
- return
352
+ except METSError as err:
353
+ raise TaskError(f"Cannot parse METS for AIP {package_uuid}: {err}") from err
337
354
 
338
355
  try:
339
- original_name = get_aip_original_name(mets)
356
+ original_name = get_aip_original_name(mets, package_uuid)
340
357
  except METSError:
341
358
  # Some other error with the METS file that we might want to
342
359
  # log and act upon.
@@ -344,26 +361,29 @@ def get_mets(
344
361
 
345
362
  # Delete records of any previous versions of this AIP, which will shortly
346
363
  # be replaced by new records from the updated METS.
347
- previous_aips = AIP.query.filter_by(uuid=package_uuid).all()
348
- for previous_aip in previous_aips:
349
- tasklogger.info(
350
- f"Deleting record for AIP {package_uuid} to replace from newer METS"
351
- )
352
- database_helpers.delete_aip_object(previous_aip)
353
-
354
- aip = database_helpers.create_aip_object(
355
- package_uuid=package_uuid,
356
- transfer_name=original_name,
357
- create_date=mets.createdate,
358
- mets_sha256=mets_hash,
359
- size=aip_size,
360
- storage_service_id=storage_service_id,
361
- storage_location_id=storage_location_id,
362
- fetch_job_id=fetch_job_id,
363
- origin_pipeline_id=origin_pipeline_id,
364
- )
364
+ try:
365
+ previous_aips = AIP.query.filter_by(uuid=package_uuid).all()
366
+ for previous_aip in previous_aips:
367
+ tasklogger.info(
368
+ f"Deleting record for AIP {package_uuid} to replace from newer METS"
369
+ )
370
+ database_helpers.delete_aip_object(previous_aip)
365
371
 
366
- database_helpers.process_aip_data(aip, mets)
372
+ aip = database_helpers.create_aip_object(
373
+ package_uuid=package_uuid,
374
+ transfer_name=original_name,
375
+ create_date=mets.createdate,
376
+ mets_sha256=mets_hash,
377
+ size=aip_size,
378
+ storage_service_id=storage_service_id,
379
+ storage_location_id=storage_location_id,
380
+ fetch_job_id=fetch_job_id,
381
+ origin_pipeline_id=origin_pipeline_id,
382
+ )
383
+ database_helpers.process_aip_data(aip, mets)
384
+ except Exception as err:
385
+ db.session.rollback()
386
+ raise TaskError(f"Cannot import METS for AIP {package_uuid}") from err
367
387
 
368
388
  # Delete downloaded METS file.
369
389
  try:
@@ -23,8 +23,14 @@
23
23
  {% for error in form.api_key.errors %}
24
24
  <span style="color: red;">[{{ error }}]</span>
25
25
  {% endfor %}</td></tr>
26
- <tr><td><strong>{{ form.download_limit.label }}</strong></td><td>{{ form.download_limit(size=48) }} </td></tr>
27
- <tr><td><strong>{{ form.download_offset.label }}</strong></td><td>{{ form.download_offset(size=48) }} </td></tr>
26
+ <tr><td><strong>{{ form.download_limit.label }}</strong></td><td>{{ form.download_limit(size=48) }}
27
+ {% for error in form.download_limit.errors %}
28
+ <span style="color: red;">[{{ error }}]</span>
29
+ {% endfor %}</td></tr>
30
+ <tr><td><strong>{{ form.download_offset.label }}</strong></td><td>{{ form.download_offset(size=48) }}
31
+ {% for error in form.download_offset.errors %}
32
+ <span style="color: red;">[{{ error }}]</span>
33
+ {% endfor %}</td></tr>
28
34
  <tr><td><strong>{{ form.default.label }}</strong></td><td>{{ form.default() }}</td></tr>
29
35
  </table>
30
36
  <button type="submit" class="btn btn-success">Save</button>
@@ -0,0 +1,23 @@
1
+ # Legacy METS fixture
2
+
3
+ `production-aip-mets-file.xml` is copied unchanged from
4
+ [mets-reader-writer] at revision `0affd1133b3fee11c1ee911eb445ba8504c9be1c`.
5
+ Its introducing commit, [1a1fcc93], describes it as generated by an
6
+ Archivematica QA deployment. The document identifies Archivematica 1.6 and
7
+ uses PREMIS 2.2. Its original file, `abc.txt`, has size and format metadata
8
+ but no `dateCreatedByApplication`.
9
+
10
+ The upstream suite uses this fixture for METS XMLSchema and Archivematica
11
+ Schematron validation. AIPscan's import tests vary only the original file's
12
+ optional creation date in memory; the fixture on disk remains unchanged.
13
+
14
+ AIP name tests also use this fixture to recover the package name from the
15
+ physical structure map when no PREMIS package name is available. They vary
16
+ the root directory label in memory to exercise UUID suffix handling.
17
+
18
+ The Dagger scenario packages this METS unchanged and also creates a variant
19
+ with the original file's format identification removed. It verifies both
20
+ packages through Storage Service, Celery, CSV exports, and browser charts.
21
+
22
+ [mets-reader-writer]: https://github.com/artefactual-labs/mets-reader-writer/blob/0affd1133b3fee11c1ee911eb445ba8504c9be1c/fixtures/production-aip-mets-file.xml
23
+ [1a1fcc93]: https://github.com/artefactual-labs/mets-reader-writer/commit/1a1fcc93e73237e1a7c252b416dc4b6e443e9185