gramps-webapi 3.22.0__tar.gz → 3.22.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (286) hide show
  1. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/Dockerfile.base +3 -0
  2. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/PKG-INFO +2 -2
  3. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/ROADMAP.md +4 -4
  4. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/_version.py +1 -1
  5. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/__init__.py +50 -1
  6. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/deprecations.py +4 -1
  7. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/image.py +31 -2
  8. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/media.py +1 -0
  9. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/report.py +17 -2
  10. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/base.py +55 -10
  11. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/filters.py +86 -10
  12. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/import_media.py +10 -1
  13. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/importers.py +30 -27
  14. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/metadata.py +8 -2
  15. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/schemas.py +3 -1
  16. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/sort.py +65 -1
  17. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/token.py +15 -3
  18. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/transactions.py +39 -5
  19. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/trees.py +1 -1
  20. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/tasks.py +30 -23
  21. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/util.py +11 -3
  22. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/auth/__init__.py +68 -42
  23. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/auth/const.py +9 -0
  24. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/profiler.py +237 -39
  25. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi.egg-info/PKG-INFO +2 -2
  26. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi.egg-info/SOURCES.txt +4 -0
  27. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi.egg-info/requires.txt +1 -1
  28. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/pyproject.toml +1 -1
  29. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/requirements-dev.txt +1 -1
  30. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_deprecations.py +13 -0
  31. gramps_webapi-3.22.2/tests/test_endpoints/test_db_handle.py +72 -0
  32. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_filters.py +253 -63
  33. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_import_media.py +17 -0
  34. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_importers.py +34 -0
  35. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_metadata.py +44 -3
  36. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_ocr.py +8 -1
  37. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_oidc.py +74 -0
  38. gramps_webapi-3.22.2/tests/test_endpoints/test_paging_sort.py +163 -0
  39. gramps_webapi-3.22.2/tests/test_endpoints/test_profiler.py +70 -0
  40. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_reports.py +10 -0
  41. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_s3.py +2 -2
  42. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_token.py +52 -2
  43. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_transactions.py +74 -0
  44. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_image.py +95 -2
  45. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_openapi_operation_ids.py +31 -0
  46. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_s3.py +2 -2
  47. gramps_webapi-3.22.2/tests/test_sort_raw_data.py +119 -0
  48. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_tasks.py +16 -4
  49. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/.devcontainer/Dockerfile +0 -0
  50. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/.devcontainer/devcontainer.json +0 -0
  51. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/.devcontainer/docker-compose.yml +0 -0
  52. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/.dockerignore +0 -0
  53. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/.editorconfig +0 -0
  54. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/.flake8 +0 -0
  55. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  56. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  57. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  58. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/.github/workflows/base-image.yml +0 -0
  59. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/.github/workflows/image.yml +0 -0
  60. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/.github/workflows/pypi.yml +0 -0
  61. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/.github/workflows/test.yml +0 -0
  62. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/.gitignore +0 -0
  63. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/.pre-commit-config.yaml +0 -0
  64. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/.pylintrc +0 -0
  65. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/.vscode/extensions.json +0 -0
  66. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/.vscode/launch.json +0 -0
  67. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/.vscode/settings.json +0 -0
  68. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/.vscode/tasks.json +0 -0
  69. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/CLAUDE.md +0 -0
  70. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/CODE_OF_CONDUCT.md +0 -0
  71. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/CONTRIBUTING.md +0 -0
  72. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/Dockerfile +0 -0
  73. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/LICENSE +0 -0
  74. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/MANIFEST.in +0 -0
  75. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/README.md +0 -0
  76. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/alembic.ini +0 -0
  77. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/alembic_users/README +0 -0
  78. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/alembic_users/env.py +0 -0
  79. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/alembic_users/script.py.mako +0 -0
  80. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/alembic_users/versions/2082445b0769_add_oidc_accounts_table_for_secure_.py +0 -0
  81. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/alembic_users/versions/22c8d1fba959_add_trees_enabled.py +0 -0
  82. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/alembic_users/versions/66e56620891a_add_trees_table.py +0 -0
  83. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/alembic_users/versions/6d8f3cb50b71_add_persistent_access_tokens_table.py +0 -0
  84. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/alembic_users/versions/84960b7d968c_use_bigint_for_quota_media.py +0 -0
  85. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/alembic_users/versions/a8e57fe0d82e_add_columns_for_ai_quota.py +0 -0
  86. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/alembic_users/versions/b0582f54029c_use_bigint_for_usage_media.py +0 -0
  87. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/alembic_users/versions/b2c3d4e5f6a7_add_tree_config_column.py +0 -0
  88. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/alembic_users/versions/c1d2e3f4a5b6_merge_task_tree_and_tree_config.py +0 -0
  89. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/alembic_users/versions/c89728e71264_.py +0 -0
  90. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/alembic_users/versions/d4e9a1c7b3f2_drop_unique_constraint_on_user_email.py +0 -0
  91. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/alembic_users/versions/e176543c72a8_add_tree_column_to_user.py +0 -0
  92. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/alembic_users/versions/e5e738d09fa7_added_configuration_table.py +0 -0
  93. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/alembic_users/versions/f3a1c8e92b47_add_task_tree_table.py +0 -0
  94. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/docker-entrypoint.sh +0 -0
  95. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/__init__.py +0 -0
  96. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/__main__.py +0 -0
  97. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/auth.py +0 -0
  98. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/blueprint.py +0 -0
  99. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/cache.py +0 -0
  100. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/check.py +0 -0
  101. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/dna.py +0 -0
  102. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/emails.py +0 -0
  103. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/export.py +0 -0
  104. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/file.py +0 -0
  105. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/html.py +0 -0
  106. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/llm/__init__.py +0 -0
  107. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/llm/agent.py +0 -0
  108. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/llm/deps.py +0 -0
  109. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/llm/tools.py +0 -0
  110. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/media_importer.py +0 -0
  111. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/people_families_cache.py +0 -0
  112. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/ratelimiter.py +0 -0
  113. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/__init__.py +0 -0
  114. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/access_tokens.py +0 -0
  115. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/anniversaries.py +0 -0
  116. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/bookmarks.py +0 -0
  117. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/chat.py +0 -0
  118. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/citations.py +0 -0
  119. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/config.py +0 -0
  120. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/db_backend.py +0 -0
  121. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/delete.py +0 -0
  122. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/dna.py +0 -0
  123. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/emit.py +0 -0
  124. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/events.py +0 -0
  125. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/export_media.py +0 -0
  126. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/exporters.py +0 -0
  127. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/face_detection.py +0 -0
  128. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/facts.py +0 -0
  129. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/families.py +0 -0
  130. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/file.py +0 -0
  131. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/history.py +0 -0
  132. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/holidays.py +0 -0
  133. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/living.py +0 -0
  134. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/match.py +0 -0
  135. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/media.py +0 -0
  136. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/merge.py +0 -0
  137. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/name_formats.py +0 -0
  138. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/name_groups.py +0 -0
  139. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/notes.py +0 -0
  140. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/object_query.py +0 -0
  141. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/objects.py +0 -0
  142. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/ocr.py +0 -0
  143. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/oidc.py +0 -0
  144. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/people.py +0 -0
  145. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/places.py +0 -0
  146. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/relations.py +0 -0
  147. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/reports.py +0 -0
  148. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/repositories.py +0 -0
  149. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/restore.py +0 -0
  150. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/search.py +0 -0
  151. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/sources.py +0 -0
  152. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/tags.py +0 -0
  153. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/tasks.py +0 -0
  154. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/timeline.py +0 -0
  155. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/translations.py +0 -0
  156. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/types.py +0 -0
  157. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/user.py +0 -0
  158. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/util.py +0 -0
  159. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/verify.py +0 -0
  160. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/resources/ydna.py +0 -0
  161. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/s3.py +0 -0
  162. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/search/__init__.py +0 -0
  163. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/search/embeddings.py +0 -0
  164. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/search/indexer.py +0 -0
  165. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/search/metadata.py +0 -0
  166. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/search/text.py +0 -0
  167. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/search/text_semantic.py +0 -0
  168. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/api/telemetry.py +0 -0
  169. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/app.py +0 -0
  170. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/auth/oidc.py +0 -0
  171. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/auth/oidc_helpers.py +0 -0
  172. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/auth/passwords.py +0 -0
  173. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/auth/sql_guid.py +0 -0
  174. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/celery.py +0 -0
  175. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/config.py +0 -0
  176. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/const.py +0 -0
  177. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/data/empty_gramps_auth.cfg +0 -0
  178. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/data/example_gramps_auth.cfg +0 -0
  179. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/data/face_detection_yunet_2023mar.onnx +0 -0
  180. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/data/test_auth.cfg +0 -0
  181. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/dbloader.py +0 -0
  182. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/dbmanager.py +0 -0
  183. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/logging_utils.py +0 -0
  184. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/sentry.py +0 -0
  185. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/static/index.html +0 -0
  186. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/templates/confirmation.html +0 -0
  187. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/templates/reset_password.html +0 -0
  188. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/templates/reset_password_base.html +0 -0
  189. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/templates/reset_password_error.html +0 -0
  190. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/translogger.py +0 -0
  191. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/types.py +0 -0
  192. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/undodb.py +0 -0
  193. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/util/__init__.py +0 -0
  194. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/util/celery.py +0 -0
  195. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/verify_lib.py +0 -0
  196. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi/wsgi.py +0 -0
  197. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi.egg-info/dependency_links.txt +0 -0
  198. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/gramps_webapi.egg-info/top_level.txt +0 -0
  199. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/mypy.ini +0 -0
  200. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/scripts/s3_rename.py +0 -0
  201. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/scripts/start_dev_container.sh +0 -0
  202. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/semantic_index.txt +0 -0
  203. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/setup.cfg +0 -0
  204. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/setup.py +0 -0
  205. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/__init__.py +0 -0
  206. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_check.py +0 -0
  207. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_cli.py +0 -0
  208. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_db.py +0 -0
  209. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_delete.py +0 -0
  210. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_dna_match_parser.py +0 -0
  211. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_embeddings.py +0 -0
  212. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/__init__.py +0 -0
  213. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/checks.py +0 -0
  214. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_access_tokens.py +0 -0
  215. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_anniversaries_ics.py +0 -0
  216. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_bookmarks.py +0 -0
  217. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_chat.py +0 -0
  218. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_citations.py +0 -0
  219. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_config.py +0 -0
  220. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_delete.py +0 -0
  221. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_dna.py +0 -0
  222. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_events.py +0 -0
  223. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_exporters.py +0 -0
  224. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_facts.py +0 -0
  225. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_families.py +0 -0
  226. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_file.py +0 -0
  227. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_history.py +0 -0
  228. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_holidays.py +0 -0
  229. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_living.py +0 -0
  230. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_media.py +0 -0
  231. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_media_archive.py +0 -0
  232. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_merge.py +0 -0
  233. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_name_formats.py +0 -0
  234. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_name_groups.py +0 -0
  235. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_notes.py +0 -0
  236. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_object_query.py +0 -0
  237. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_object_query_where_tree.py +0 -0
  238. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_people.py +0 -0
  239. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_people_query.py +0 -0
  240. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_places.py +0 -0
  241. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_post.py +0 -0
  242. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_put.py +0 -0
  243. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_ready.py +0 -0
  244. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_relations.py +0 -0
  245. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_repair.py +0 -0
  246. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_repositories.py +0 -0
  247. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_restore.py +0 -0
  248. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_search.py +0 -0
  249. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_sources.py +0 -0
  250. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_tags.py +0 -0
  251. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_tasks.py +0 -0
  252. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_timelines.py +0 -0
  253. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_translations.py +0 -0
  254. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_treeless_admin.py +0 -0
  255. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_trees.py +0 -0
  256. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_types.py +0 -0
  257. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_upload.py +0 -0
  258. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_user.py +0 -0
  259. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_verify.py +0 -0
  260. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/test_ydna.py +0 -0
  261. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_endpoints/util.py +0 -0
  262. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_file.py +0 -0
  263. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_import_util.py +0 -0
  264. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_jwt.py +0 -0
  265. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_limiter.py +0 -0
  266. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_llm_errors.py +0 -0
  267. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_llm_tools.py +0 -0
  268. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_logging_utils.py +0 -0
  269. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_map_tile.py +0 -0
  270. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_media_importer.py +0 -0
  271. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_media_prefix_tree.py +0 -0
  272. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_migrate_grampsdb.py +0 -0
  273. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_object_query_parsing.py +0 -0
  274. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_oidc_auth.py +0 -0
  275. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_password.py +0 -0
  276. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_private_proxy.py +0 -0
  277. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_search_metadata.py +0 -0
  278. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_sentry.py +0 -0
  279. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_sqlauth.py +0 -0
  280. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_task_error.py +0 -0
  281. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_telemetry.py +0 -0
  282. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_text.py +0 -0
  283. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_undodb.py +0 -0
  284. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_util.py +0 -0
  285. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_verify_lib.py +0 -0
  286. {gramps_webapi-3.22.0 → gramps_webapi-3.22.2}/tests/test_version.py +0 -0
@@ -8,7 +8,10 @@ WORKDIR /app
8
8
  # install poppler (needed for PDF thumbnails)
9
9
  # ffmpeg (needed for video thumbnails)
10
10
  # postgresql client (needed for PostgreSQL backend)
11
+ # Noto fonts (needed to render non-Latin scripts in generated reports -
12
+ # without them, Pango has no glyphs and PDF output is garbled)
11
13
  RUN apt-get update && apt-get install -y \
14
+ fonts-dejavu-core fonts-noto-core fonts-noto-cjk \
12
15
  appstream pkg-config libcairo2-dev \
13
16
  gir1.2-gtk-3.0 libgirepository1.0-dev libicu-dev \
14
17
  graphviz gir1.2-gexiv2-0.10 gir1.2-osmgpsmap-1.0 \
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gramps-webapi
3
- Version: 3.22.0
3
+ Version: 3.22.2
4
4
  Summary: A RESTful web API for the Gramps genealogical database.
5
5
  Author: Gramps Development Team
6
6
  Author-email: "David M. Straub" <straub@protonmail.com>
@@ -39,7 +39,7 @@ Requires-Dist: alembic
39
39
  Requires-Dist: celery[redis]
40
40
  Requires-Dist: Unidecode
41
41
  Requires-Dist: pytesseract
42
- Requires-Dist: gramps-object-query-language<0.6,>=0.5.0
42
+ Requires-Dist: gramps-object-query-language<0.6,>=0.5.1
43
43
  Requires-Dist: gramps-ql>=0.5.0
44
44
  Requires-Dist: object-ql>=0.1.3
45
45
  Requires-Dist: sifts>=1.1.0
@@ -13,14 +13,14 @@ removed no earlier than the next major.
13
13
 
14
14
  | Version | Target | Gramps | Breaking changes |
15
15
  |---|---|---|---|
16
- | 3.21.x | current | 6.0 | none |
17
- | 3.22.0 | September 2026 | 6.0 | none |
16
+ | 3.22.x | current | 6.0 | none |
17
+ | 3.23.0 | October 2026 | 6.0 | none |
18
18
  | 3.x | as needed | 6.0 | none |
19
19
  | 4.0.0 | TBD | 6.0, 6.1 or 6.2 (open) | multi-tree by default, removals below |
20
20
 
21
- ## Notable in 3.22
21
+ ## Notable in 3.23
22
22
 
23
- E-mail addresses no longer have to be unique across accounts; a user database migration drops the constraint ([#961](https://github.com/gramps-project/gramps-web-api/pull/961)).
23
+ TBD
24
24
 
25
25
  ## Breaking in 4.0
26
26
 
@@ -19,4 +19,4 @@
19
19
 
20
20
  """Version information."""
21
21
 
22
- __version__ = "3.22.0"
22
+ __version__ = "3.22.2"
@@ -176,6 +176,7 @@ def register_endpt(
176
176
  name: str,
177
177
  tags: Optional[List[str]] = None,
178
178
  request_body: Optional[dict] = None,
179
+ object_name: Optional[str] = None,
179
180
  ):
180
181
  """Register an endpoint.
181
182
 
@@ -188,6 +189,11 @@ def register_endpt(
188
189
  `request_body` documents the JSON payload of a POST/PUT endpoint (e.g.
189
190
  via `object_request_body()`); omit it for endpoints that take no body
190
191
  or a non-JSON one (such as Media's raw file upload).
192
+
193
+ `object_name` names the Gramps object type (e.g. "Person") used to
194
+ build CRUD summary text ("Get a Gramps Person record", ...). Pass it
195
+ for endpoints that operate on a single typed object; omit it for
196
+ endpoints (transactions, tokens, queries, ...) that don't.
191
197
  """
192
198
  operation_name = name.replace("-", "_")
193
199
  for verb in ("get", "post", "put", "delete", "patch"):
@@ -195,6 +201,24 @@ def register_endpt(
195
201
  if method is None:
196
202
  continue
197
203
  doc_kwargs: dict = {"operationId": f"{verb}_{operation_name}"}
204
+ if object_name is not None:
205
+ collection_name = name.replace("-", " ").replace("_", " ")
206
+ if verb == "get":
207
+ summary = (
208
+ f"Get a Gramps {object_name} record"
209
+ if "<string:handle>" in url
210
+ else f"List Gramps {collection_name} with filters"
211
+ )
212
+ elif verb == "post":
213
+ summary = f"Create a Gramps {object_name} record"
214
+ elif verb == "put":
215
+ summary = f"Update a Gramps {object_name} record"
216
+ elif verb == "delete":
217
+ summary = f"Delete a Gramps {object_name} record"
218
+ else:
219
+ summary = None
220
+ if summary is not None:
221
+ doc_kwargs["summary"] = summary
198
222
  if request_body is not None and verb in ("post", "put"):
199
223
  doc_kwargs["requestBody"] = request_body
200
224
  # setattr rather than plain assignment: mypy rejects assigning to a
@@ -295,6 +319,7 @@ register_endpt(
295
319
  "person",
296
320
  tags=["People"],
297
321
  request_body=object_request_body("Person"),
322
+ object_name="Person",
298
323
  )
299
324
  register_endpt(
300
325
  PersonDnaMatchesResource,
@@ -311,6 +336,7 @@ register_endpt(
311
336
  "people",
312
337
  tags=["People"],
313
338
  request_body=object_request_body("Person"),
339
+ object_name="Person",
314
340
  )
315
341
  register_endpt(PersonQueryResource, "/people/query/", "people-query", tags=["People"])
316
342
  register_endpt(
@@ -332,6 +358,7 @@ register_endpt(
332
358
  "family",
333
359
  tags=["Families"],
334
360
  request_body=object_request_body("Family"),
361
+ object_name="Family",
335
362
  )
336
363
  register_endpt(
337
364
  FamiliesResource,
@@ -339,6 +366,7 @@ register_endpt(
339
366
  "families",
340
367
  tags=["Families"],
341
368
  request_body=object_request_body("Family"),
369
+ object_name="Family",
342
370
  )
343
371
  register_endpt(
344
372
  FamilyQueryResource, "/families/query/", "families-query", tags=["Families"]
@@ -362,6 +390,7 @@ register_endpt(
362
390
  "event",
363
391
  tags=["Events"],
364
392
  request_body=object_request_body("Event"),
393
+ object_name="Event",
365
394
  )
366
395
  register_endpt(
367
396
  EventsResource,
@@ -369,6 +398,7 @@ register_endpt(
369
398
  "events",
370
399
  tags=["Events"],
371
400
  request_body=object_request_body("Event"),
401
+ object_name="Event",
372
402
  )
373
403
  register_endpt(EventQueryResource, "/events/query/", "events-query", tags=["Events"])
374
404
  register_endpt(
@@ -394,6 +424,7 @@ register_endpt(
394
424
  "place",
395
425
  tags=["Places"],
396
426
  request_body=object_request_body("Place"),
427
+ object_name="Place",
397
428
  )
398
429
  register_endpt(
399
430
  PlacesResource,
@@ -401,6 +432,7 @@ register_endpt(
401
432
  "places",
402
433
  tags=["Places"],
403
434
  request_body=object_request_body("Place"),
435
+ object_name="Place",
404
436
  )
405
437
  register_endpt(PlaceQueryResource, "/places/query/", "places-query", tags=["Places"])
406
438
  register_endpt(
@@ -416,6 +448,7 @@ register_endpt(
416
448
  "citation",
417
449
  tags=["Citations"],
418
450
  request_body=object_request_body("Citation"),
451
+ object_name="Citation",
419
452
  )
420
453
  register_endpt(
421
454
  CitationsResource,
@@ -423,6 +456,7 @@ register_endpt(
423
456
  "citations",
424
457
  tags=["Citations"],
425
458
  request_body=object_request_body("Citation"),
459
+ object_name="Citation",
426
460
  )
427
461
  register_endpt(
428
462
  CitationQueryResource, "/citations/query/", "citations-query", tags=["Citations"]
@@ -440,6 +474,7 @@ register_endpt(
440
474
  "source",
441
475
  tags=["Sources"],
442
476
  request_body=object_request_body("Source"),
477
+ object_name="Source",
443
478
  )
444
479
  register_endpt(
445
480
  SourcesResource,
@@ -447,6 +482,7 @@ register_endpt(
447
482
  "sources",
448
483
  tags=["Sources"],
449
484
  request_body=object_request_body("Source"),
485
+ object_name="Source",
450
486
  )
451
487
  register_endpt(
452
488
  SourceQueryResource, "/sources/query/", "sources-query", tags=["Sources"]
@@ -464,6 +500,7 @@ register_endpt(
464
500
  "repository",
465
501
  tags=["Repositories"],
466
502
  request_body=object_request_body("Repository"),
503
+ object_name="Repository",
467
504
  )
468
505
  register_endpt(
469
506
  RepositoriesResource,
@@ -471,6 +508,7 @@ register_endpt(
471
508
  "repositories",
472
509
  tags=["Repositories"],
473
510
  request_body=object_request_body("Repository"),
511
+ object_name="Repository",
474
512
  )
475
513
  register_endpt(
476
514
  RepositoryQueryResource,
@@ -491,10 +529,17 @@ register_endpt(
491
529
  "media_object",
492
530
  tags=["Media"],
493
531
  request_body=object_request_body("Media"),
532
+ object_name="Media",
494
533
  )
495
534
  # MediaObjectsResource's POST takes a raw file upload (multipart), not JSON,
496
535
  # so it gets no request_body doc here.
497
- register_endpt(MediaObjectsResource, "/media/", "media_objects", tags=["Media"])
536
+ register_endpt(
537
+ MediaObjectsResource,
538
+ "/media/",
539
+ "media_objects",
540
+ tags=["Media"],
541
+ object_name="Media",
542
+ )
498
543
  register_endpt(MediaQueryResource, "/media/query/", "media-query", tags=["Media"])
499
544
  register_endpt(
500
545
  MergeMediaResource,
@@ -509,6 +554,7 @@ register_endpt(
509
554
  "note",
510
555
  tags=["Notes"],
511
556
  request_body=object_request_body("Note"),
557
+ object_name="Note",
512
558
  )
513
559
  register_endpt(
514
560
  NotesResource,
@@ -516,6 +562,7 @@ register_endpt(
516
562
  "notes",
517
563
  tags=["Notes"],
518
564
  request_body=object_request_body("Note"),
565
+ object_name="Note",
519
566
  )
520
567
  register_endpt(NoteQueryResource, "/notes/query/", "notes-query", tags=["Notes"])
521
568
  register_endpt(
@@ -531,6 +578,7 @@ register_endpt(
531
578
  "tag",
532
579
  tags=["Tags"],
533
580
  request_body=object_request_body("Tag"),
581
+ object_name="Tag",
534
582
  )
535
583
  register_endpt(
536
584
  TagsResource,
@@ -538,6 +586,7 @@ register_endpt(
538
586
  "tags",
539
587
  tags=["Tags"],
540
588
  request_body=object_request_body("Tag"),
589
+ object_name="Tag",
541
590
  )
542
591
  register_endpt(TagQueryResource, "/tags/query/", "tags-query", tags=["Tags"])
543
592
  # Trees
@@ -96,9 +96,12 @@ def check_deprecations(
96
96
  _deprecation(
97
97
  "SEARCH_INDEX_DIR",
98
98
  "SEARCH_INDEX_DB_URI",
99
+ # the configured directory is deliberately not interpolated here:
100
+ # the message is exposed via the API, so it must not disclose a
101
+ # server filesystem path
99
102
  "The `SEARCH_INDEX_DIR` config option is deprecated. Please use"
100
103
  " `SEARCH_INDEX_DB_URI` instead, e.g. setting it to"
101
- f" `sqlite:///{config['SEARCH_INDEX_DIR']}/search_index.db`.",
104
+ " `sqlite:///<search index directory>/search_index.db`.",
102
105
  )
103
106
  )
104
107
  if (
@@ -29,7 +29,7 @@ from importlib.resources import as_file, files
29
29
  from pathlib import Path
30
30
  from typing import BinaryIO, Callable, Iterator, Union
31
31
 
32
- from PIL import Image, ImageOps, UnidentifiedImageError
32
+ from PIL import Image, ImageCms, ImageOps, UnidentifiedImageError
33
33
  from PIL.Image import DecompressionBombError
34
34
  from PIL.Image import Image as ImageType
35
35
 
@@ -123,7 +123,36 @@ def save_image_buffer(image: ImageType, fmt="AVIF") -> BinaryIO:
123
123
  if image.mode == "RGBA" and not supports_alpha:
124
124
  image = image.convert("RGB")
125
125
  elif image.mode not in ("RGB", "RGBA"):
126
- image = image.convert("RGB")
126
+ if image.mode == "L" and image.info.get("icc_profile"):
127
+ # `convert("RGB")` changes the pixel mode but keeps the source
128
+ # ICC profile as-is, which is invalid for RGB output and gets
129
+ # embedded verbatim on save, producing files some decoders
130
+ # (e.g. Chrome for AVIF) refuse to open.
131
+ try:
132
+ source_profile = ImageCms.ImageCmsProfile(
133
+ io.BytesIO(image.info["icc_profile"])
134
+ )
135
+ transformed_image = ImageCms.profileToProfile(
136
+ image,
137
+ source_profile,
138
+ ImageCms.createProfile("sRGB"),
139
+ outputMode="RGB",
140
+ )
141
+ # `profileToProfile()` only returns `None` when called with
142
+ # `inPlace=True`, which we don't do here.
143
+ assert transformed_image is not None
144
+ image = transformed_image
145
+ except ImageCms.PyCMSError:
146
+ # The embedded profile is untrusted (uploaded) data: it can be
147
+ # malformed enough to open but still fail to build a color
148
+ # transform. Fall back to an uncolor-managed conversion rather
149
+ # than crashing.
150
+ image = image.convert("RGB")
151
+ image.info.pop("icc_profile", None)
152
+ else:
153
+ image = image.convert("RGB")
154
+ # the profile (if any) belonged to the previous color space
155
+ image.info.pop("icc_profile", None)
127
156
  image.save(buffer, format=fmt)
128
157
  buffer.seek(0)
129
158
  return buffer
@@ -390,6 +390,7 @@ def check_quota_media(
390
390
  usage_dict = get_tree_usage(tree)
391
391
  assert usage_dict is not None, "Unexpected error while looking up usage data."
392
392
  usage = usage_dict["usage_media"]
393
+ assert usage is not None, "Unexpected error while looking up usage data."
393
394
  quota = usage_dict.get("quota_media")
394
395
  if quota is None:
395
396
  return
@@ -59,7 +59,6 @@ from gramps.gen.plug.report import (
59
59
  CATEGORY_TEXT,
60
60
  CATEGORY_TREE,
61
61
  )
62
- from gramps.gen.utils.grampslocale import GrampsLocale
63
62
  from gramps.gen.utils.resourcepath import ResourcePath
64
63
 
65
64
  from ..const import MIME_TYPES, REPORT_DEFAULTS, REPORT_FILTERS
@@ -328,7 +327,11 @@ def cl_report_new(
328
327
  ):
329
328
  clr.option_class.handler.doc.set_css_filename(clr.css_filename)
330
329
  my_report = report_class(database, clr.option_class, User())
331
- my_report.set_locale(language or GrampsLocale.DEFAULT_TRANSLATION_STR)
330
+ if language:
331
+ # Only reached for reports that have no "trans" option. Reports that do
332
+ # have one have already applied the language in their constructor, and
333
+ # setting it here would discard the option value chosen by the user.
334
+ my_report.set_locale(language)
332
335
  my_report.doc.init()
333
336
  my_report.begin_report()
334
337
  my_report.write_report()
@@ -368,6 +371,18 @@ def run_report(
368
371
  report_options["of"] = os.path.join(report_path, file_name)
369
372
  report_profile = get_report_profile(db_handle, plugin_manager, report_data)
370
373
  validate_options(report_profile, report_options, allow_file=allow_file)
374
+ if (
375
+ language
376
+ and "trans" in report_profile["options_dict"]
377
+ and language in glocale.get_language_dict().values()
378
+ ):
379
+ # Apply the language via the report's own localization option,
380
+ # which is evaluated in the report's constructor. Reports build
381
+ # locale-dependent objects there, so setting the locale
382
+ # afterwards is not enough. Languages the option does not accept
383
+ # are left to set_locale, which handles e.g. regional variants.
384
+ report_options["trans"] = language
385
+ language = None
371
386
  module = plugin_manager.load_plugin(report_data)
372
387
  option_class = getattr(module, report_data.optionclass)
373
388
  report_class = getattr(module, report_data.reportclass)
@@ -56,7 +56,7 @@ from .delete import delete_object, remove_deleted_from_search_indices
56
56
  from .emit import GrampsJSONEncoder
57
57
  from .filters import apply_filter
58
58
  from .match import match_dates
59
- from .sort import sort_objects
59
+ from .sort import sort_handles_by_raw_data, sort_objects
60
60
  from .util import (
61
61
  abort_with_message,
62
62
  add_object,
@@ -614,21 +614,66 @@ class GrampsObjectsResource(GrampsObjectResourceHelper, Resource):
614
614
  pass
615
615
  return self.response(200, objects, args, total_items=len(objects))
616
616
 
617
+ query_method = self.db_handle.method("get_%s_handles", self.gramps_class_name)
618
+ assert query_method is not None # type checker
619
+ # for all objects except events, repos, and notes, Gramps supports
620
+ # a database-backed default sort order. Use that if no sort order
621
+ # requested.
622
+ sort_by_default = self.gramps_class_name not in ["Event", "Repository", "Note"]
623
+
624
+ # if a single page is requested and no filtering is needed, try to sort
625
+ # the handles without loading all objects to memory
626
+ if args["page"] > 0 and not (
627
+ "filter" in args
628
+ or "rules" in args
629
+ or "gql" in args
630
+ or "oql" in args
631
+ or args["dates"]
632
+ or (self.gramps_class_name == "Media" and args.get("filemissing"))
633
+ ):
634
+ if "sort" in args:
635
+ # the database-backed default sort order is expensive and
636
+ # not needed here since the requested sort order replaces it
637
+ sorted_handles = sort_handles_by_raw_data(
638
+ self.db_handle,
639
+ self.gramps_class_name,
640
+ query_method(),
641
+ args["sort"],
642
+ locale=locale,
643
+ )
644
+ elif sort_by_default:
645
+ sorted_handles = query_method(sort_handles=True, locale=locale)
646
+ else:
647
+ sorted_handles = query_method()
648
+ if sorted_handles is not None:
649
+ offset = (args["page"] - 1) * args["pagesize"]
650
+ page_handles = sorted_handles[offset : offset + args["pagesize"]]
651
+ objects = []
652
+ for handle in page_handles:
653
+ try:
654
+ obj = self.get_object_from_handle(handle)
655
+ except HandleError:
656
+ # object was deleted after the handles were fetched
657
+ continue
658
+ if obj is None:
659
+ # object is no longer included by a proxy database
660
+ continue
661
+ objects.append(self.full_object(obj, args, locale=locale))
662
+ return self.response(
663
+ 200, objects, args, total_items=len(sorted_handles)
664
+ )
665
+
666
+ if sort_by_default:
667
+ handles = query_method(sort_handles=True, locale=locale)
668
+ else:
669
+ handles = query_method()
670
+
617
671
  # load all objects to memory
618
672
  objects_name = GRAMPS_OBJECT_PLURAL[self.gramps_class_name]
619
673
  iter_objects_method = self.db_handle.method("iter_%s", objects_name)
620
674
  assert iter_objects_method is not None # type checker
621
675
  objects = list(iter_objects_method())
622
676
 
623
- # for all objects except events, repos, and notes, Gramps supports
624
- # a database-backed default sort order. Use that if no sort order
625
- # requested.
626
- query_method = self.db_handle.method("get_%s_handles", self.gramps_class_name)
627
- assert query_method is not None # type checker
628
- if self.gramps_class_name in ["Event", "Repository", "Note"]:
629
- handles = query_method()
630
- else:
631
- handles = query_method(sort_handles=True, locale=locale)
632
677
  handle_index = {handle: index for index, handle in enumerate(handles)}
633
678
  # sort objects by the sorted handle order
634
679
  objects = sorted(
@@ -22,6 +22,7 @@
22
22
 
23
23
  import inspect
24
24
  import json
25
+ import re
25
26
  from collections import defaultdict
26
27
  from collections.abc import Callable
27
28
  from typing import Any
@@ -29,6 +30,7 @@ from typing import Any
29
30
  import gramps.gen.filters as filters
30
31
  from flask import Response, abort, current_app
31
32
  from gramps.gen.db.base import DbReadBase
33
+ from gramps.gen.errors import FilterError
32
34
  from gramps.gen.filters import GenericFilter
33
35
  from gramps.gen.filters.rules import Rule
34
36
  from gramps.gen.lib import Media, Person
@@ -99,6 +101,23 @@ _ADDITIONAL_RULES: dict[str, list[type[Rule]]] = {
99
101
  "Media": [IsReferencedByObjectType],
100
102
  }
101
103
 
104
+ # Rules shipped by Gramps that fail on every object of their namespace
105
+ _EXCLUDED_RULES: dict[str, set[str]] = {
106
+ "Repository": {"HasAttribute"}, # Repository has no attribute_list
107
+ }
108
+
109
+ # Errors raised by Gramps rules when evaluated with invalid values
110
+ # (e.g. unparsable dates or integers, invalid regular expressions)
111
+ _RULE_VALUE_ERRORS = (
112
+ FilterError,
113
+ ValueError,
114
+ TypeError,
115
+ AttributeError,
116
+ IndexError,
117
+ KeyError,
118
+ re.error,
119
+ )
120
+
102
121
  _RULE_CLASS_CACHE: dict[str, dict[str, type[Rule]]] = {}
103
122
 
104
123
  # Maps (parent_namespace, sub_namespace) to a function (db, obj) -> [sub_handle, ...]
@@ -130,7 +149,7 @@ def get_rule_map(namespace: str) -> dict[str, type[Rule]]:
130
149
  if mod is None:
131
150
  _RULE_CLASS_CACHE[namespace] = {}
132
151
  else:
133
- seen: set[str] = set()
152
+ seen: set[str] = set(_EXCLUDED_RULES.get(namespace, set()))
134
153
  result: dict[str, type[Rule]] = {}
135
154
  for _, cls in inspect.getmembers(mod, inspect.isclass):
136
155
  if (
@@ -211,16 +230,61 @@ def get_custom_filters(args: dict[str, Any], namespace: str) -> list[dict[str, A
211
230
  return filter_list
212
231
 
213
232
 
214
- def _build_rule_instance(rule_parms: dict[str, Any], namespace: str) -> Rule:
215
- """Instantiate a single rule from its parameter dict."""
216
- rule_class = get_rule_map(namespace).get(rule_parms["name"])
233
+ def _validate_rule_parms(
234
+ rule_parms: dict[str, Any], namespace: str
235
+ ) -> tuple[type[Rule], list[str]]:
236
+ """Return the rule class and its values as strings, aborting on invalid input.
237
+
238
+ Gramps rules expect string values, one per label. The number of values is not
239
+ enforced, as existing clients rely on both directions. Like Gramps when loading
240
+ saved filters, extra values are ignored. Missing values are passed through:
241
+ some rules supply defaults (e.g. HasNote), the others fail during evaluation,
242
+ which returns 422.
243
+ """
244
+ name = rule_parms["name"]
245
+ rule_class = get_rule_map(namespace).get(name)
217
246
  if rule_class is None:
218
247
  abort(404)
219
248
  assert rule_class is not None
220
- return rule_class(
221
- rule_parms.get("values", []),
222
- use_regex=rule_parms.get("regex", False),
223
- )
249
+ values = rule_parms.get("values", [])
250
+ str_values = []
251
+ for value in values:
252
+ if isinstance(value, bool):
253
+ str_values.append("1" if value else "0")
254
+ elif isinstance(value, (str, int, float)):
255
+ str_values.append(str(value))
256
+ else:
257
+ abort_with_message(
258
+ 422, f"Rule {name} values must be strings, numbers or booleans"
259
+ )
260
+ return rule_class, str_values[: len(rule_class.labels)]
261
+
262
+
263
+ def _validate_filter_parms(
264
+ filter_parms: dict[str, Any], namespace: str, depth: int = 0
265
+ ) -> None:
266
+ """Validate a (possibly nested) filter spec before evaluating it."""
267
+ if depth >= MAX_FILTER_DEPTH:
268
+ abort_with_message(400, "Filter nesting depth exceeded")
269
+ for item in filter_parms["rules"]:
270
+ if "name" in item:
271
+ _validate_rule_parms(item, namespace)
272
+ continue
273
+ sub_namespace = item.get("namespace", namespace)
274
+ if (
275
+ sub_namespace != namespace
276
+ and (namespace, sub_namespace) not in _NAMESPACE_BRIDGES
277
+ ):
278
+ abort_with_message(
279
+ 400, f"Unsupported namespace bridge: {namespace} → {sub_namespace}"
280
+ )
281
+ _validate_filter_parms(item, sub_namespace, depth + 1)
282
+
283
+
284
+ def _build_rule_instance(rule_parms: dict[str, Any], namespace: str) -> Rule:
285
+ """Instantiate a single rule from its parameter dict."""
286
+ rule_class, values = _validate_rule_parms(rule_parms, namespace)
287
+ return rule_class(values, use_regex=rule_parms.get("regex", False))
224
288
 
225
289
 
226
290
  def _apply_filter_parms(
@@ -243,7 +307,12 @@ def _apply_filter_parms(
243
307
  if "name" in item:
244
308
  single = filters.GenericFilterFactory(namespace)()
245
309
  single.add_rule(_build_rule_instance(item, namespace))
246
- result_sets.append(set(single.apply(db_handle, id_list=handles)))
310
+ try:
311
+ result_sets.append(set(single.apply(db_handle, id_list=handles)))
312
+ except _RULE_VALUE_ERRORS as exc:
313
+ abort_with_message(
314
+ 422, f"Rule {item['name']} could not be evaluated: {exc}"
315
+ )
247
316
  elif item.get("namespace", namespace) != namespace:
248
317
  sub_namespace = item["namespace"]
249
318
  bridge = _NAMESPACE_BRIDGES.get((namespace, sub_namespace))
@@ -316,7 +385,13 @@ def apply_filter(
316
385
  if args.get("filter"):
317
386
  for filter_class in filters.CustomFilters.get_filters(namespace):
318
387
  if args["filter"] == filter_class.get_name():
319
- return filter_class.apply(db_handle, id_list=handles)
388
+ try:
389
+ return filter_class.apply(db_handle, id_list=handles)
390
+ except _RULE_VALUE_ERRORS as exc:
391
+ abort_with_message(
392
+ 422,
393
+ f"Filter {args['filter']} could not be evaluated: {exc}",
394
+ )
320
395
  abort(404)
321
396
 
322
397
  try:
@@ -325,6 +400,7 @@ def apply_filter(
325
400
  abort_with_message(400, "Error decoding JSON")
326
401
  except ValidationError:
327
402
  abort_with_message(422, "Filter does not adhere to schema")
403
+ _validate_filter_parms(filter_parms, namespace)
328
404
 
329
405
  if handles is None:
330
406
  query_method = db_handle.method("get_%s_handles", namespace)
@@ -54,7 +54,11 @@ def get_max_upload_bytes(free_bytes: int) -> int:
54
54
 
55
55
 
56
56
  def write_upload_to_file(file_path: str, max_bytes: int) -> None:
57
- """Stream the request body to a file, aborting if it exceeds `max_bytes`."""
57
+ """Stream the request body to a file, aborting if it exceeds `max_bytes`.
58
+
59
+ Also aborts if the body size does not match the declared Content-Length
60
+ (e.g. a truncated upload). The caller is responsible for removing the file.
61
+ """
58
62
  request_stream = request.stream
59
63
  size = 0
60
64
  with open(file_path, "w+b") as ftmp:
@@ -67,6 +71,11 @@ def write_upload_to_file(file_path: str, max_bytes: int) -> None:
67
71
  if size > max_bytes:
68
72
  abort_with_message(413, "Uploaded archive is too large")
69
73
  ftmp.write(chunk)
74
+ expected = request.content_length
75
+ if expected is not None and size and size != expected:
76
+ abort_with_message(
77
+ 400, f"Upload incomplete: received {size} of {expected} bytes"
78
+ )
70
79
 
71
80
 
72
81
  class MediaUploadZipResource(ProtectedResource):