deepsights-api 1.7.2__tar.gz → 1.7.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 (147) hide show
  1. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/CHANGELOG.md +8 -0
  2. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/PKG-INFO +2 -2
  3. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/_version.py +1 -1
  4. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/contentstore/resources/_model.py +4 -4
  5. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/contentstore/resources/_search.py +6 -3
  6. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/documentstore/resources/documents/_search.py +7 -5
  7. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/userclient/resources/documents/documents.py +7 -5
  8. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/pyproject.toml +3 -0
  9. deepsights_api-1.7.4/tests/deepsights/contentstore/test_contentstore_search_null_tolerance_unit.py +84 -0
  10. deepsights_api-1.7.4/tests/deepsights/documentstore/test_documentstore_search_null_tolerance_unit.py +98 -0
  11. deepsights_api-1.7.4/tests/deepsights/userclient/documents/test_userclient_search_null_tolerance_unit.py +87 -0
  12. deepsights_api-1.7.2/ds-b2b-client-product-prod.yaml +0 -8363
  13. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/.claude/settings.local.json +0 -0
  14. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/.github/workflows/build_docs.yml +0 -0
  15. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/.github/workflows/run_pylint.yml +0 -0
  16. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/.github/workflows/run_tests.yml +0 -0
  17. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/.gitignore +0 -0
  18. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/.pre-commit-config.yaml +0 -0
  19. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/.python-version +0 -0
  20. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/.vscode/settings.json +0 -0
  21. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/AGENTS.md +0 -0
  22. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/CLAUDE.md +0 -0
  23. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/LICENSE +0 -0
  24. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/README.md +0 -0
  25. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/__init__.py +0 -0
  26. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/api/__init__.py +0 -0
  27. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/api/api.py +0 -0
  28. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/api/resource.py +0 -0
  29. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/api/unified_token.py +0 -0
  30. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/contentstore/__init__.py +0 -0
  31. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/contentstore/contentstore.py +0 -0
  32. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/contentstore/resources/__init__.py +0 -0
  33. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/contentstore/resources/_download.py +0 -0
  34. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/contentstore/resources/_url.py +0 -0
  35. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/contentstore/resources/news.py +0 -0
  36. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/contentstore/resources/secondary.py +0 -0
  37. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/deepsights/__init__.py +0 -0
  38. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/deepsights/_mip_identity.py +0 -0
  39. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/deepsights/deepsights.py +0 -0
  40. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/deepsights/resources/__init__.py +0 -0
  41. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/deepsights/resources/quota/__init__.py +0 -0
  42. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/deepsights/resources/quota/_model.py +0 -0
  43. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/deepsights/resources/quota/quota.py +0 -0
  44. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/documentstore/__init__.py +0 -0
  45. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/documentstore/documentstore.py +0 -0
  46. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/documentstore/resources/__init__.py +0 -0
  47. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/documentstore/resources/documents/__init__.py +0 -0
  48. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/documentstore/resources/documents/_cache.py +0 -0
  49. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/documentstore/resources/documents/_delete.py +0 -0
  50. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/documentstore/resources/documents/_download.py +0 -0
  51. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/documentstore/resources/documents/_list.py +0 -0
  52. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/documentstore/resources/documents/_load.py +0 -0
  53. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/documentstore/resources/documents/_model.py +0 -0
  54. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/documentstore/resources/documents/_segmenter.py +0 -0
  55. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/documentstore/resources/documents/_taxonomy.py +0 -0
  56. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/documentstore/resources/documents/_upload.py +0 -0
  57. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/documentstore/resources/documents/documents.py +0 -0
  58. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/documentstore/resources/taxonomies/__init__.py +0 -0
  59. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/documentstore/resources/taxonomies/_model.py +0 -0
  60. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/documentstore/resources/taxonomies/_taxon_crud.py +0 -0
  61. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/documentstore/resources/taxonomies/_taxon_type_crud.py +0 -0
  62. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/documentstore/resources/taxonomies/_taxonomy_crud.py +0 -0
  63. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/documentstore/resources/taxonomies/taxonomies.py +0 -0
  64. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/exceptions.py +0 -0
  65. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/userclient/__init__.py +0 -0
  66. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/userclient/resources/__init__.py +0 -0
  67. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/userclient/resources/_model.py +0 -0
  68. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/userclient/resources/answersV2/__init__.py +0 -0
  69. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/userclient/resources/answersV2/_model.py +0 -0
  70. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/userclient/resources/answersV2/answerV2.py +0 -0
  71. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/userclient/resources/documents/__init__.py +0 -0
  72. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/userclient/resources/documents/_download.py +0 -0
  73. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/userclient/resources/reports/__init__.py +0 -0
  74. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/userclient/resources/reports/_model.py +0 -0
  75. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/userclient/resources/reports/report.py +0 -0
  76. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/userclient/userclient.py +0 -0
  77. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/utils/__init__.py +0 -0
  78. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/utils/_cache.py +0 -0
  79. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/utils/_ranking.py +0 -0
  80. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/utils/_utils.py +0 -0
  81. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/deepsights/utils/model.py +0 -0
  82. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/docs/.gitkeep +0 -0
  83. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/main.py +0 -0
  84. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/mypy.ini +0 -0
  85. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/pytest.ini +0 -0
  86. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/requirements.txt +0 -0
  87. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/scripts/precommit.sh +0 -0
  88. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/setup.py +0 -0
  89. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/tests/conftest.py +0 -0
  90. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/tests/contentstore/test_contentstore_unified.py +0 -0
  91. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/tests/contentstore/test_news_search.py +0 -0
  92. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/tests/contentstore/test_secondary_search.py +0 -0
  93. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/tests/data/test_data.json +0 -0
  94. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/tests/data/test_presentation.pdf +0 -0
  95. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/tests/data/test_text.txt +0 -0
  96. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/tests/deepsights/api/test_api_headers_timeout.py +0 -0
  97. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/tests/deepsights/api/test_error_mapping.py +0 -0
  98. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/tests/deepsights/api/test_get_content.py +0 -0
  99. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/tests/deepsights/api/test_retry_logic.py +0 -0
  100. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/tests/deepsights/api/test_unified_token.py +0 -0
  101. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/tests/deepsights/contentstore/test_validation.py +0 -0
  102. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/tests/deepsights/documentstore/test_deprecation.py +0 -0
  103. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/tests/deepsights/documentstore/test_documentstore_topic_search_unit.py +0 -0
  104. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/tests/deepsights/identity/test_mip_identity.py +0 -0
  105. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/tests/deepsights/models/test_models_defaults.py +0 -0
  106. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/tests/deepsights/quota/test_quota.py +0 -0
  107. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/tests/deepsights/test_deepsights_init.py +0 -0
  108. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/tests/deepsights/userclient/documents/test_pages_load_unit.py +0 -0
  109. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/tests/deepsights/userclient/documents/test_userclient_topic_search_unit.py +0 -0
  110. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/tests/deepsights/userclient/test_answers_reports_unit.py +0 -0
  111. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/tests/deepsights/userclient/test_userclient.py +0 -0
  112. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/tests/deepsights/userclient/test_userclient_cache_jitter.py +0 -0
  113. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/tests/deepsights/utils/test_parallel.py +0 -0
  114. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/tests/deepsights/utils/test_polling.py +0 -0
  115. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/tests/documentstore/__init__.py +0 -0
  116. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/tests/documentstore/test_document_taxonomy.py +0 -0
  117. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/tests/documentstore/test_download.py +0 -0
  118. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/tests/documentstore/test_lifecycle.py +0 -0
  119. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/tests/documentstore/test_list.py +0 -0
  120. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/tests/documentstore/test_load.py +0 -0
  121. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/tests/documentstore/test_search.py +0 -0
  122. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/tests/documentstore/test_search_taxonomy_filters.py +0 -0
  123. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/tests/documentstore/test_taxonomy.py +0 -0
  124. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/tests/helpers/__init__.py +0 -0
  125. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/tests/helpers/common.py +0 -0
  126. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/tests/helpers/validation.py +0 -0
  127. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/tests/userclient/__init__.py +0 -0
  128. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/tests/userclient/answersV2/test_answersV2.py +0 -0
  129. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/tests/userclient/documents/__init__.py +0 -0
  130. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/tests/userclient/documents/test_document_pages_load.py +0 -0
  131. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/tests/userclient/documents/test_documents_list.py +0 -0
  132. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/tests/userclient/documents/test_documents_load.py +0 -0
  133. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/tests/userclient/documents/test_documents_search.py +0 -0
  134. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/tests/userclient/documents/test_download.py +0 -0
  135. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/tests/userclient/documents/test_topic_search.py +0 -0
  136. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/tests/userclient/reports/test_reports.py +0 -0
  137. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/tests/userclient/test_rate_limiting.py +0 -0
  138. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/tests/userclient/test_userclient_refresh.py +0 -0
  139. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/tests/userclient/test_userclient_unified.py +0 -0
  140. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/typings/cachetools/__init__.pyi +0 -0
  141. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/typings/pydantic/__init__.pyi +0 -0
  142. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/typings/ratelimit/__init__.pyi +0 -0
  143. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/typings/requests/__init__.pyi +0 -0
  144. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/typings/requests/adapters.pyi +0 -0
  145. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/typings/requests/exceptions.pyi +0 -0
  146. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/typings/tenacity/__init__.pyi +0 -0
  147. {deepsights_api-1.7.2 → deepsights_api-1.7.4}/uv.lock +0 -0
@@ -2,6 +2,14 @@
2
2
 
3
3
  <!--next-version-placeholder-->
4
4
 
5
+ ## v1.7.4 (09-Sep-2026)
6
+
7
+ - Treat null `context`, `search_results`, and `items` in search responses as empty result sets instead of crashing when the backend is degraded (documentstore, userclient, and contentstore search functions).
8
+
9
+ ## v1.7.3 (02-Sep-2026)
10
+
11
+ - Exclude the production OpenAPI specification from source distributions.
12
+
5
13
  ## v1.7.2 (12-May-2026)
6
14
 
7
15
  - Add optional content type filtering to topic search for userclient and documentstore clients.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: deepsights-api
3
- Version: 1.7.2
3
+ Version: 1.7.4
4
4
  Summary: Python library for the DeepSights APIs
5
5
  Project-URL: Documentation, https://marketlogicsoftware.github.io/deepsights-api/
6
6
  Project-URL: Repository, https://github.com/marketlogicsoftware/deepsights-api
@@ -2,4 +2,4 @@ __all__ = ["__version__"]
2
2
 
3
3
  # Single-source version for the package.
4
4
  # Update this value when releasing.
5
- __version__ = "1.7.2"
5
+ __version__ = "1.7.4"
@@ -49,9 +49,9 @@ class ContentStoreSearchResult(DeepSightsIdTitleModel):
49
49
 
50
50
  Attributes:
51
51
 
52
- description (str): The description of the item.
52
+ description (Optional[str], optional): The description of the item. Defaults to None.
53
53
  image_url (Optional[str], optional): The URL of the item's image. Defaults to None.
54
- url (str): The URL of the item.
54
+ url (Optional[str], optional): The URL of the item. Defaults to None.
55
55
  language (Optional[str], optional): The language of the item. Defaults to None.
56
56
  publication_date (Optional[datetime], optional): The publication_date of the item's publication. Defaults to None.
57
57
  source (Optional[str], optional): The source of the item. Defaults to None.
@@ -59,9 +59,9 @@ class ContentStoreSearchResult(DeepSightsIdTitleModel):
59
59
  rank (Optional[int], optional): The final rank of the item in the search results. Defaults to None.
60
60
  """
61
61
 
62
- description: str = Field(description="The description of the item.")
62
+ description: Optional[str] = Field(description="The description of the item; may be None.", default=None)
63
63
  image_url: Optional[str] = Field(description="The URL of the item's thumbnail image.", default=None)
64
- url: str = Field(description="The URL of the item.")
64
+ url: Optional[str] = Field(description="The URL of the item; may be None.", default=None)
65
65
  language: Optional[str] = Field(description="The language of the item; may be None.", default=None)
66
66
  publication_date: Optional[datetime] = Field(
67
67
  alias="published_at",
@@ -158,7 +158,8 @@ def contentstore_hybrid_search(
158
158
  response = api.post("item-service/items/_hybrid-search", body=body)
159
159
 
160
160
  # parse
161
- results: List[T] = [search_result(i) for i in response["items"]]
161
+ # a degraded backend may return HTTP 200 with "items": null; treat as empty
162
+ results: List[T] = [search_result(i) for i in response.get("items") or []]
162
163
 
163
164
  # record rank
164
165
  for rank, result in enumerate(results):
@@ -223,7 +224,8 @@ def contentstore_vector_search(
223
224
  response = api.post("item-service/items/_vector-search", body=body)
224
225
 
225
226
  # parse
226
- results: List[T] = [search_result(i) for i in response["items"]]
227
+ # a degraded backend may return HTTP 200 with "items": null; treat as empty
228
+ results: List[T] = [search_result(i) for i in response.get("items") or []]
227
229
 
228
230
  # re-rank
229
231
  return rerank_by_recency(results, recency_weight=recency_weight)
@@ -291,7 +293,8 @@ def contentstore_text_search(
291
293
  response = api.post("item-service/items/_text-search", body=body)
292
294
 
293
295
  # parse
294
- results: List[T] = [search_result(i) for i in response["items"]]
296
+ # a degraded backend may return HTTP 200 with "items": null; treat as empty
297
+ results: List[T] = [search_result(i) for i in response.get("items") or []]
295
298
 
296
299
  # record rank
297
300
  for rank, result in enumerate(results):
@@ -253,8 +253,9 @@ def hybrid_search(
253
253
 
254
254
  response = resource.api.post("supercharged-search-service/hybrid-searches", body=body)
255
255
 
256
- # Extract the search results from the response
257
- search_results = response.get("context", {}).get("search_results", [])
256
+ # Extract the search results from the response; a degraded backend may return
257
+ # HTTP 200 with "context": null or "search_results": null, treat both as empty
258
+ search_results = (response.get("context") or {}).get("search_results") or []
258
259
  return [HybridSearchResult(**result) for result in search_results]
259
260
 
260
261
 
@@ -303,6 +304,7 @@ def topic_search(
303
304
 
304
305
  response = resource.api.post("supercharged-search-service/topic-searches", body=body)
305
306
 
306
- # Extract the search results from the response
307
- search_results = response.get("context", {}).get("search_results", [])
308
- return [TopicSearchResult(**result) for result in search_results if len(result) > 0]
307
+ # Extract the search results from the response; a degraded backend may return
308
+ # HTTP 200 with "context": null or "search_results": null, treat both as empty
309
+ search_results = (response.get("context") or {}).get("search_results") or []
310
+ return [TopicSearchResult(**result) for result in search_results if result]
@@ -91,9 +91,10 @@ def topic_search(
91
91
 
92
92
  response = resource.api.post("end-user-gateway-service/topic-searches", body=body)
93
93
 
94
- # Extract the search results from the response
95
- search_results = response.get("context", {}).get("search_results", [])
96
- return [TopicSearchResult(**result) for result in search_results if len(result) > 0]
94
+ # Extract the search results from the response; a degraded backend may return
95
+ # HTTP 200 with "context": null or "search_results": null, treat both as empty
96
+ search_results = (response.get("context") or {}).get("search_results") or []
97
+ return [TopicSearchResult(**result) for result in search_results if result]
97
98
 
98
99
 
99
100
  #################################################
@@ -125,8 +126,9 @@ def hybrid_search(resource: APIResource, query: str, extended_search: bool = Fal
125
126
  # Temporarily allow 5xx responses to debug the error
126
127
  response = resource.api.post("end-user-gateway-service/hybrid-searches", body=body)
127
128
 
128
- # Extract the search results from the response
129
- search_results = response.get("context", {}).get("search_results", [])
129
+ # Extract the search results from the response; a degraded backend may return
130
+ # HTTP 200 with "context": null or "search_results": null, treat both as empty
131
+ search_results = (response.get("context") or {}).get("search_results") or []
130
132
  return [HybridSearchResult(**result) for result in search_results]
131
133
 
132
134
 
@@ -65,6 +65,9 @@ build-backend = "hatchling.build"
65
65
  [tool.hatch.build.targets.wheel]
66
66
  packages = ["deepsights"]
67
67
 
68
+ [tool.hatch.build.targets.sdist]
69
+ exclude = ["/ds-b2b-client-product-prod.yaml"]
70
+
68
71
  [tool.hatch.metadata]
69
72
  allow-direct-references = true
70
73
 
@@ -0,0 +1,84 @@
1
+ """
2
+ Unit tests for null-tolerant parsing of Content Store search responses (no network).
3
+
4
+ A degraded backend may return HTTP 200 with "items": null (or no "items" key);
5
+ this must be treated as an empty result set.
6
+ """
7
+
8
+ from typing import Any, cast
9
+
10
+ import pytest
11
+
12
+ from deepsights.contentstore.resources._model import ContentStoreSearchResult
13
+ from deepsights.contentstore.resources._search import (
14
+ contentstore_hybrid_search,
15
+ contentstore_text_search,
16
+ contentstore_vector_search,
17
+ )
18
+
19
+
20
+ class _DummyAPI:
21
+ def __init__(self, response):
22
+ self._response = response
23
+
24
+ def post(self, path, params=None, body=None, timeout=None, expected_statuscodes=None):
25
+ return self._response
26
+
27
+
28
+ _ITEM = {
29
+ "id": "item-1",
30
+ "title": "Title",
31
+ "description": "Description",
32
+ "url": "https://example.com",
33
+ "language": "en",
34
+ "publication_date": "2024-01-02T03:04:05Z",
35
+ "source": "Source",
36
+ "source_id": "src-1",
37
+ }
38
+
39
+ _NULL_RESPONSES = [{"items": None}, {}]
40
+ _NULL_IDS = ["items-null", "no-items-key"]
41
+
42
+
43
+ def _search_result(i: Any) -> ContentStoreSearchResult:
44
+ return ContentStoreSearchResult(**i)
45
+
46
+
47
+ @pytest.mark.parametrize("response", _NULL_RESPONSES, ids=_NULL_IDS)
48
+ def test_hybrid_search_null_response_returns_empty(response):
49
+ api = cast(Any, _DummyAPI(response))
50
+ assert contentstore_hybrid_search(api, "query", item_type="NEWS", search_result=_search_result) == []
51
+
52
+
53
+ @pytest.mark.parametrize("response", _NULL_RESPONSES, ids=_NULL_IDS)
54
+ def test_text_search_null_response_returns_empty(response):
55
+ api = cast(Any, _DummyAPI(response))
56
+ assert contentstore_text_search(api, "query", item_type="NEWS", search_result=_search_result) == []
57
+
58
+
59
+ @pytest.mark.parametrize("response", _NULL_RESPONSES, ids=_NULL_IDS)
60
+ def test_vector_search_null_response_returns_empty(response):
61
+ api = cast(Any, _DummyAPI(response))
62
+ assert contentstore_vector_search(api, [0.0] * 1536, item_type="NEWS", search_result=_search_result) == []
63
+
64
+
65
+ def test_hybrid_search_parses_non_empty_payload_and_records_rank():
66
+ api = cast(Any, _DummyAPI({"items": [_ITEM, {**_ITEM, "id": "item-2"}]}))
67
+ results = contentstore_hybrid_search(api, "query", item_type="NEWS", search_result=_search_result)
68
+ assert [r.id for r in results] == ["item-1", "item-2"]
69
+ assert [r.rank for r in results] == [1, 2]
70
+
71
+
72
+ def test_text_search_parses_non_empty_payload_and_records_rank():
73
+ api = cast(Any, _DummyAPI({"items": [_ITEM]}))
74
+ results = contentstore_text_search(api, "query", item_type="NEWS", search_result=_search_result)
75
+ assert len(results) == 1
76
+ assert results[0].id == "item-1"
77
+ assert results[0].rank == 1
78
+
79
+
80
+ def test_vector_search_parses_non_empty_payload():
81
+ api = cast(Any, _DummyAPI({"items": [_ITEM]}))
82
+ results = contentstore_vector_search(api, [0.0] * 1536, item_type="NEWS", search_result=_search_result)
83
+ assert len(results) == 1
84
+ assert results[0].id == "item-1"
@@ -0,0 +1,98 @@
1
+ """
2
+ Unit tests for null-tolerant parsing of documentstore search responses (no network).
3
+
4
+ A degraded backend may return HTTP 200 with "context": null or
5
+ "search_results": null; both must be treated as an empty result set.
6
+ """
7
+
8
+ import pytest
9
+
10
+ from deepsights.api.resource import APIResource
11
+ from deepsights.documentstore.resources.documents._search import hybrid_search, topic_search
12
+
13
+
14
+ class _DummyAPI:
15
+ def __init__(self, response):
16
+ self._response = response
17
+
18
+ def post(self, path, params=None, body=None, timeout=None, expected_statuscodes=None):
19
+ return self._response
20
+
21
+
22
+ _ARTIFACT = {
23
+ "artifact_id": "art-1",
24
+ "artifact_title": "Title",
25
+ "artifact_summary": "Summary",
26
+ "artifact_source": "Source",
27
+ "artifact_content_type": "standalone_document",
28
+ "artifact_publication_date": "2024-01-02T03:04:05Z",
29
+ }
30
+
31
+ _HYBRID_RESULT = {
32
+ **_ARTIFACT,
33
+ "page_references": [{"id": "page-1", "external_id": None, "number": 1, "title": "P1", "text": "Text"}],
34
+ }
35
+
36
+ _TOPIC_RESULT = {
37
+ **_ARTIFACT,
38
+ "page_references": [
39
+ {
40
+ "id": "page-1",
41
+ "external_id": None,
42
+ "number": 1,
43
+ "title": "P1",
44
+ "text": "Text",
45
+ "relevance_class": "HIGH",
46
+ "relevance_assessment": "Good match",
47
+ }
48
+ ],
49
+ "relevance_class": "HIGH",
50
+ "relevance_assessment": "Good match",
51
+ }
52
+
53
+
54
+ @pytest.mark.parametrize(
55
+ "response",
56
+ [
57
+ {"context": None},
58
+ {"context": {"search_results": None}},
59
+ {"context": {}},
60
+ {},
61
+ ],
62
+ ids=["context-null", "search-results-null", "context-empty", "no-context-key"],
63
+ )
64
+ def test_hybrid_search_null_response_returns_empty(response):
65
+ resource = APIResource(api=_DummyAPI(response))
66
+ assert hybrid_search(resource, "query") == []
67
+
68
+
69
+ @pytest.mark.parametrize(
70
+ "response",
71
+ [
72
+ {"context": None},
73
+ {"context": {"search_results": None}},
74
+ {"context": {}},
75
+ {},
76
+ ],
77
+ ids=["context-null", "search-results-null", "context-empty", "no-context-key"],
78
+ )
79
+ def test_topic_search_null_response_returns_empty(response):
80
+ resource = APIResource(api=_DummyAPI(response))
81
+ assert topic_search(resource, "query") == []
82
+
83
+
84
+ def test_hybrid_search_parses_non_empty_payload():
85
+ resource = APIResource(api=_DummyAPI({"context": {"search_results": [_HYBRID_RESULT]}}))
86
+ results = hybrid_search(resource, "query")
87
+ assert len(results) == 1
88
+ assert results[0].artifact_id == "art-1"
89
+ assert results[0].page_references[0].id == "page-1"
90
+
91
+
92
+ def test_topic_search_parses_non_empty_payload_and_skips_empty_entries():
93
+ resource = APIResource(api=_DummyAPI({"context": {"search_results": [_TOPIC_RESULT, {}, None]}}))
94
+ results = topic_search(resource, "query")
95
+ assert len(results) == 1
96
+ assert results[0].artifact_id == "art-1"
97
+ assert results[0].relevance_class == "HIGH"
98
+ assert results[0].page_references[0].relevance_class == "HIGH"
@@ -0,0 +1,87 @@
1
+ """
2
+ Unit tests for null-tolerant parsing of userclient search responses (no network).
3
+
4
+ A degraded backend may return HTTP 200 with "context": null or
5
+ "search_results": null; both must be treated as an empty result set.
6
+ """
7
+
8
+ import pytest
9
+
10
+ from deepsights.api.resource import APIResource
11
+ from deepsights.userclient.resources.documents.documents import hybrid_search, topic_search
12
+
13
+
14
+ class _DummyAPI:
15
+ def __init__(self, response):
16
+ self._response = response
17
+
18
+ def post(self, path, params=None, body=None, timeout=None, expected_statuscodes=None):
19
+ return self._response
20
+
21
+
22
+ _ARTIFACT = {
23
+ "artifact_id": "art-1",
24
+ "artifact_title": "Title",
25
+ "artifact_summary": "Summary",
26
+ "artifact_source": "Source",
27
+ "artifact_content_type": "standalone_document",
28
+ "artifact_publication_date": "2024-01-02T03:04:05Z",
29
+ }
30
+
31
+ _HYBRID_RESULT = {
32
+ **_ARTIFACT,
33
+ "page_references": [{"id": "page-1", "external_id": None, "number": 1, "title": "P1", "text": "Text"}],
34
+ }
35
+
36
+ _TOPIC_RESULT = {
37
+ **_ARTIFACT,
38
+ "page_references": [
39
+ {
40
+ "id": "page-1",
41
+ "external_id": None,
42
+ "number": 1,
43
+ "title": "P1",
44
+ "text": "Text",
45
+ "relevance_class": "HIGH",
46
+ "relevance_assessment": "Good match",
47
+ }
48
+ ],
49
+ "relevance_class": "HIGH",
50
+ "relevance_assessment": "Good match",
51
+ }
52
+
53
+ _NULL_RESPONSES = [
54
+ {"context": None},
55
+ {"context": {"search_results": None}},
56
+ {"context": {}},
57
+ {},
58
+ ]
59
+ _NULL_IDS = ["context-null", "search-results-null", "context-empty", "no-context-key"]
60
+
61
+
62
+ @pytest.mark.parametrize("response", _NULL_RESPONSES, ids=_NULL_IDS)
63
+ def test_hybrid_search_null_response_returns_empty(response):
64
+ resource = APIResource(api=_DummyAPI(response))
65
+ assert hybrid_search(resource, "query") == []
66
+
67
+
68
+ @pytest.mark.parametrize("response", _NULL_RESPONSES, ids=_NULL_IDS)
69
+ def test_topic_search_null_response_returns_empty(response):
70
+ resource = APIResource(api=_DummyAPI(response))
71
+ assert topic_search(resource, "query") == []
72
+
73
+
74
+ def test_hybrid_search_parses_non_empty_payload():
75
+ resource = APIResource(api=_DummyAPI({"context": {"search_results": [_HYBRID_RESULT]}}))
76
+ results = hybrid_search(resource, "query")
77
+ assert len(results) == 1
78
+ assert results[0].artifact_id == "art-1"
79
+ assert results[0].page_references[0].id == "page-1"
80
+
81
+
82
+ def test_topic_search_parses_non_empty_payload_and_skips_empty_entries():
83
+ resource = APIResource(api=_DummyAPI({"context": {"search_results": [_TOPIC_RESULT, {}, None]}}))
84
+ results = topic_search(resource, "query")
85
+ assert len(results) == 1
86
+ assert results[0].artifact_id == "art-1"
87
+ assert results[0].relevance_class == "HIGH"