deepsights-api 1.3.2__tar.gz → 1.3.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 (101) hide show
  1. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/CHANGELOG.md +10 -0
  2. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/PKG-INFO +77 -10
  3. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/README.md +76 -9
  4. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/deepsights/__init__.py +11 -1
  5. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/deepsights/api/api.py +66 -21
  6. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/deepsights/documentstore/resources/documents/_search.py +4 -2
  7. deepsights_api-1.3.4/deepsights/exceptions.py +44 -0
  8. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/deepsights/userclient/resources/answersV2/answerV2.py +26 -5
  9. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/deepsights/userclient/resources/documents/documents.py +7 -3
  10. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/deepsights/userclient/resources/reports/report.py +19 -4
  11. deepsights_api-1.3.4/deepsights/userclient/userclient.py +260 -0
  12. deepsights_api-1.3.4/main.py +298 -0
  13. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/pyproject.toml +1 -1
  14. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/tests/deepsights/quota/test_quota.py +13 -13
  15. deepsights_api-1.3.4/tests/userclient/test_rate_limiting.py +266 -0
  16. deepsights_api-1.3.4/tests/userclient/test_userclient_refresh.py +272 -0
  17. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/uv.lock +1 -1
  18. deepsights_api-1.3.2/deepsights/userclient/userclient.py +0 -56
  19. deepsights_api-1.3.2/main.py +0 -163
  20. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/.claude/settings.local.json +0 -0
  21. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/.github/workflows/build_docs.yml +0 -0
  22. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/.github/workflows/run_pylint.yml +0 -0
  23. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/.github/workflows/run_tests.yml +0 -0
  24. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/.gitignore +0 -0
  25. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/.pylintrc +0 -0
  26. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/.python-version +0 -0
  27. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/.vscode/settings.json +0 -0
  28. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/CLAUDE.md +0 -0
  29. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/LICENSE +0 -0
  30. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/deepsights/api/__init__.py +0 -0
  31. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/deepsights/api/resource.py +0 -0
  32. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/deepsights/contentstore/__init__.py +0 -0
  33. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/deepsights/contentstore/contentstore.py +0 -0
  34. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/deepsights/contentstore/resources/__init__.py +0 -0
  35. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/deepsights/contentstore/resources/_download.py +0 -0
  36. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/deepsights/contentstore/resources/_model.py +0 -0
  37. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/deepsights/contentstore/resources/_search.py +0 -0
  38. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/deepsights/contentstore/resources/news.py +0 -0
  39. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/deepsights/contentstore/resources/secondary.py +0 -0
  40. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/deepsights/deepsights/__init__.py +0 -0
  41. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/deepsights/deepsights/_mip_identity.py +0 -0
  42. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/deepsights/deepsights/deepsights.py +0 -0
  43. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/deepsights/deepsights/resources/__init__.py +0 -0
  44. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/deepsights/deepsights/resources/quota/__init__.py +0 -0
  45. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/deepsights/deepsights/resources/quota/_model.py +0 -0
  46. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/deepsights/deepsights/resources/quota/quota.py +0 -0
  47. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/deepsights/documentstore/__init__.py +0 -0
  48. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/deepsights/documentstore/documentstore.py +0 -0
  49. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/deepsights/documentstore/resources/__init__.py +0 -0
  50. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/deepsights/documentstore/resources/documents/__init__.py +0 -0
  51. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/deepsights/documentstore/resources/documents/_cache.py +0 -0
  52. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/deepsights/documentstore/resources/documents/_delete.py +0 -0
  53. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/deepsights/documentstore/resources/documents/_download.py +0 -0
  54. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/deepsights/documentstore/resources/documents/_list.py +0 -0
  55. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/deepsights/documentstore/resources/documents/_load.py +0 -0
  56. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/deepsights/documentstore/resources/documents/_model.py +0 -0
  57. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/deepsights/documentstore/resources/documents/_segmenter.py +0 -0
  58. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/deepsights/documentstore/resources/documents/_upload.py +0 -0
  59. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/deepsights/documentstore/resources/documents/documents.py +0 -0
  60. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/deepsights/userclient/__init__.py +0 -0
  61. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/deepsights/userclient/resources/__init__.py +0 -0
  62. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/deepsights/userclient/resources/_model.py +0 -0
  63. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/deepsights/userclient/resources/answersV2/__init__.py +0 -0
  64. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/deepsights/userclient/resources/answersV2/_model.py +0 -0
  65. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/deepsights/userclient/resources/documents/__init__.py +0 -0
  66. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/deepsights/userclient/resources/reports/__init__.py +0 -0
  67. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/deepsights/userclient/resources/reports/_model.py +0 -0
  68. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/deepsights/userclient/resources/search/__init__.py +0 -0
  69. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/deepsights/userclient/resources/search/_model.py +0 -0
  70. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/deepsights/userclient/resources/search/search.py +0 -0
  71. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/deepsights/utils/__init__.py +0 -0
  72. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/deepsights/utils/_cache.py +0 -0
  73. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/deepsights/utils/_ranking.py +0 -0
  74. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/deepsights/utils/_utils.py +0 -0
  75. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/deepsights/utils/model.py +0 -0
  76. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/docs/.gitkeep +0 -0
  77. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/requirements.txt +0 -0
  78. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/setup.py +0 -0
  79. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/tests/conftest.py +0 -0
  80. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/tests/contentstore/test_news_search.py +0 -0
  81. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/tests/contentstore/test_secondary_search.py +0 -0
  82. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/tests/data/test_data.json +0 -0
  83. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/tests/data/test_presentation.pdf +0 -0
  84. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/tests/data/test_text.txt +0 -0
  85. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/tests/deepsights/identity/test_mip_identity.py +0 -0
  86. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/tests/deepsights/userclient/test_userclient.py +0 -0
  87. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/tests/documentstore/test_download.py +0 -0
  88. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/tests/documentstore/test_lifecycle.py +0 -0
  89. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/tests/documentstore/test_list.py +0 -0
  90. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/tests/documentstore/test_load.py +0 -0
  91. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/tests/documentstore/test_search.py +0 -0
  92. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/tests/helpers/__init__.py +0 -0
  93. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/tests/helpers/common.py +0 -0
  94. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/tests/helpers/validation.py +0 -0
  95. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/tests/userclient/answersV2/test_answersV2.py +0 -0
  96. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/tests/userclient/documents/test_document_pages_load.py +0 -0
  97. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/tests/userclient/documents/test_documents_list.py +0 -0
  98. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/tests/userclient/documents/test_documents_load.py +0 -0
  99. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/tests/userclient/documents/test_documents_search.py +0 -0
  100. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/tests/userclient/reports/test_reports.py +0 -0
  101. {deepsights_api-1.3.2 → deepsights_api-1.3.4}/tests/userclient/search/test_topicsearch.py +0 -0
@@ -2,6 +2,16 @@
2
2
 
3
3
  <!--next-version-placeholder-->
4
4
 
5
+ ## v1.3.4 (11-Jul-2025)
6
+
7
+ - Extended hybrid search query length limitation to 512 chars
8
+
9
+ ## v1.3.3 (26-Jun-2025)
10
+
11
+ - Auto-refresh token for userclient
12
+ - Streamlined rate limiting and error handling
13
+
14
+
5
15
  ## v1.3.2 (18-Jun-2025)
6
16
 
7
17
  - Improved error logging for answer polling
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deepsights-api
3
- Version: 1.3.2
3
+ Version: 1.3.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
@@ -202,14 +202,48 @@ for item in content.results:
202
202
  print(f"{item.title} - {item.source}")
203
203
  ```
204
204
 
205
- #### Error Handling
205
+ #### Error Handling & Rate Limiting
206
206
  ```python
207
+ import time
208
+ import deepsights
209
+
210
+ ds = deepsights.DeepSights()
211
+ uc = ds.get_userclient("analyst@company.com")
212
+
213
+ try:
214
+ # Ask multiple questions - will demonstrate rate limiting
215
+ for i in range(15): # Exceeds 10/minute limit for answers
216
+ response = uc.answersV2.create(f"Question {i}: Market trends?")
217
+ print(f"✅ Question {i+1} processed")
218
+
219
+ except deepsights.RateLimitError as e:
220
+ print(f"🚫 Rate limit exceeded: {e}")
221
+
222
+ if e.retry_after:
223
+ print(f"⏱️ Client-side limit - wait {e.retry_after} seconds")
224
+ time.sleep(e.retry_after)
225
+ else:
226
+ print("⏱️ Server busy - try again later")
227
+
228
+ except deepsights.AuthenticationError as e:
229
+ print(f"🔐 Authentication failed: {e}")
230
+
231
+ except deepsights.DeepSightsError as e:
232
+ print(f"⚠️ API error: {e}")
233
+
234
+ # Alternative: Handle all rate limiting uniformly
207
235
  try:
208
236
  response = uc.answersV2.create_and_wait("Your question here")
209
- except deepsights.exceptions.AuthenticationError:
210
- print("Invalid API key or permissions")
211
- except deepsights.exceptions.RateLimitError:
212
- print("Rate limit exceeded - please wait")
237
+
238
+ except deepsights.RateLimitError as e:
239
+ # Handles both client-side (10/min) and server-side (persistent 429) limits
240
+ print(f"Rate limit hit: {e}")
241
+
242
+ # Implement your retry strategy
243
+ if e.retry_after:
244
+ time.sleep(e.retry_after) # Known wait time
245
+ else:
246
+ time.sleep(60) # Conservative fallback for server limits
213
247
  ```
214
248
 
215
249
  All return values are [Pydantic objects](https://docs.pydantic.dev/latest/) with `.schema_human()` for exploring available properties. See [main.py](https://github.com/marketlogicsoftware/deepsights-api/blob/main/main.py) for more examples.
@@ -217,10 +251,43 @@ All return values are [Pydantic objects](https://docs.pydantic.dev/latest/) with
217
251
 
218
252
  ## Developer Information
219
253
 
220
- ### Rate Limits
221
- - **GET requests**: 1,000 per 60 seconds
222
- - **POST requests**: 100 per 60 seconds
223
- - Automatic exponential backoff retry logic included
254
+ ### Rate Limits & Error Handling
255
+
256
+ The DeepSights API implements **comprehensive rate limiting** to ensure fair usage and optimal performance:
257
+
258
+ #### Client-Side Rate Limits
259
+ - **AI Answers**: 10 requests per 60 seconds (`uc.answersV2.create()`)
260
+ - **AI Reports**: 3 requests per 60 seconds (`uc.reports.create()`)
261
+ - **General GET requests**: 1,000 per 60 seconds
262
+ - **General POST requests**: 100 per 60 seconds
263
+
264
+ #### Behavior
265
+ - **Client-side limits**: Immediate `RateLimitError` with `retry_after` information
266
+ - **Server-side limits**: Automatic retry with exponential backoff (up to 3 attempts)
267
+ - **Persistent server limits**: Convert to `RateLimitError` after retries for consistent handling
268
+
269
+ #### Exception Hierarchy
270
+ ```python
271
+ deepsights.DeepSightsError # Base exception
272
+ ├── deepsights.RateLimitError # Rate limiting (client + server)
273
+ └── deepsights.AuthenticationError # Invalid API keys/permissions
274
+ ```
275
+
276
+ #### Best Practices
277
+ ```python
278
+ # Recommended error handling pattern
279
+ try:
280
+ response = uc.answersV2.create_and_wait("Your question")
281
+
282
+ except deepsights.RateLimitError as e:
283
+ # Single handler for all rate limiting scenarios
284
+ wait_time = e.retry_after or 60 # Use provided time or conservative fallback
285
+ print(f"Rate limited, waiting {wait_time} seconds...")
286
+ time.sleep(wait_time)
287
+
288
+ except deepsights.AuthenticationError:
289
+ print("Check your API keys and permissions")
290
+ ```
224
291
 
225
292
  ### Caching
226
293
  - User client responses cached for 240 seconds (TTL)
@@ -172,14 +172,48 @@ for item in content.results:
172
172
  print(f"{item.title} - {item.source}")
173
173
  ```
174
174
 
175
- #### Error Handling
175
+ #### Error Handling & Rate Limiting
176
176
  ```python
177
+ import time
178
+ import deepsights
179
+
180
+ ds = deepsights.DeepSights()
181
+ uc = ds.get_userclient("analyst@company.com")
182
+
183
+ try:
184
+ # Ask multiple questions - will demonstrate rate limiting
185
+ for i in range(15): # Exceeds 10/minute limit for answers
186
+ response = uc.answersV2.create(f"Question {i}: Market trends?")
187
+ print(f"✅ Question {i+1} processed")
188
+
189
+ except deepsights.RateLimitError as e:
190
+ print(f"🚫 Rate limit exceeded: {e}")
191
+
192
+ if e.retry_after:
193
+ print(f"⏱️ Client-side limit - wait {e.retry_after} seconds")
194
+ time.sleep(e.retry_after)
195
+ else:
196
+ print("⏱️ Server busy - try again later")
197
+
198
+ except deepsights.AuthenticationError as e:
199
+ print(f"🔐 Authentication failed: {e}")
200
+
201
+ except deepsights.DeepSightsError as e:
202
+ print(f"⚠️ API error: {e}")
203
+
204
+ # Alternative: Handle all rate limiting uniformly
177
205
  try:
178
206
  response = uc.answersV2.create_and_wait("Your question here")
179
- except deepsights.exceptions.AuthenticationError:
180
- print("Invalid API key or permissions")
181
- except deepsights.exceptions.RateLimitError:
182
- print("Rate limit exceeded - please wait")
207
+
208
+ except deepsights.RateLimitError as e:
209
+ # Handles both client-side (10/min) and server-side (persistent 429) limits
210
+ print(f"Rate limit hit: {e}")
211
+
212
+ # Implement your retry strategy
213
+ if e.retry_after:
214
+ time.sleep(e.retry_after) # Known wait time
215
+ else:
216
+ time.sleep(60) # Conservative fallback for server limits
183
217
  ```
184
218
 
185
219
  All return values are [Pydantic objects](https://docs.pydantic.dev/latest/) with `.schema_human()` for exploring available properties. See [main.py](https://github.com/marketlogicsoftware/deepsights-api/blob/main/main.py) for more examples.
@@ -187,10 +221,43 @@ All return values are [Pydantic objects](https://docs.pydantic.dev/latest/) with
187
221
 
188
222
  ## Developer Information
189
223
 
190
- ### Rate Limits
191
- - **GET requests**: 1,000 per 60 seconds
192
- - **POST requests**: 100 per 60 seconds
193
- - Automatic exponential backoff retry logic included
224
+ ### Rate Limits & Error Handling
225
+
226
+ The DeepSights API implements **comprehensive rate limiting** to ensure fair usage and optimal performance:
227
+
228
+ #### Client-Side Rate Limits
229
+ - **AI Answers**: 10 requests per 60 seconds (`uc.answersV2.create()`)
230
+ - **AI Reports**: 3 requests per 60 seconds (`uc.reports.create()`)
231
+ - **General GET requests**: 1,000 per 60 seconds
232
+ - **General POST requests**: 100 per 60 seconds
233
+
234
+ #### Behavior
235
+ - **Client-side limits**: Immediate `RateLimitError` with `retry_after` information
236
+ - **Server-side limits**: Automatic retry with exponential backoff (up to 3 attempts)
237
+ - **Persistent server limits**: Convert to `RateLimitError` after retries for consistent handling
238
+
239
+ #### Exception Hierarchy
240
+ ```python
241
+ deepsights.DeepSightsError # Base exception
242
+ ├── deepsights.RateLimitError # Rate limiting (client + server)
243
+ └── deepsights.AuthenticationError # Invalid API keys/permissions
244
+ ```
245
+
246
+ #### Best Practices
247
+ ```python
248
+ # Recommended error handling pattern
249
+ try:
250
+ response = uc.answersV2.create_and_wait("Your question")
251
+
252
+ except deepsights.RateLimitError as e:
253
+ # Single handler for all rate limiting scenarios
254
+ wait_time = e.retry_after or 60 # Use provided time or conservative fallback
255
+ print(f"Rate limited, waiting {wait_time} seconds...")
256
+ time.sleep(wait_time)
257
+
258
+ except deepsights.AuthenticationError:
259
+ print("Check your API keys and permissions")
260
+ ```
194
261
 
195
262
  ### Caching
196
263
  - User client responses cached for 240 seconds (TTL)
@@ -16,7 +16,17 @@
16
16
  This module contains the client library to interact with the Market Logic DeepSights and ContentStore APIs.
17
17
  """
18
18
 
19
+ from deepsights import exceptions
19
20
  from deepsights.deepsights import DeepSights
20
21
  from deepsights.documentstore.resources import SortingField, SortingOrder
22
+ from deepsights.exceptions import AuthenticationError, DeepSightsError, RateLimitError
21
23
 
22
- __all__ = ["SortingField", "SortingOrder", "DeepSights"]
24
+ __all__ = [
25
+ "SortingField",
26
+ "SortingOrder",
27
+ "DeepSights",
28
+ "exceptions",
29
+ "AuthenticationError",
30
+ "DeepSightsError",
31
+ "RateLimitError",
32
+ ]
@@ -29,6 +29,8 @@ from tenacity import (
29
29
  wait_random_exponential,
30
30
  )
31
31
 
32
+ from deepsights.exceptions import AuthenticationError, RateLimitError
33
+
32
34
 
33
35
  def _should_retry_http_error(exception: Exception) -> bool:
34
36
  """
@@ -53,6 +55,50 @@ def _should_retry_http_error(exception: Exception) -> bool:
53
55
  return isinstance(exception, (ConnectionError, Timeout))
54
56
 
55
57
 
58
+ def _handle_http_error(response):
59
+ """
60
+ Handles HTTP errors and raises appropriate custom exceptions.
61
+
62
+ Args:
63
+ response: The HTTP response object
64
+
65
+ Raises:
66
+ AuthenticationError: If status code is 401 (Unauthorized)
67
+ HTTPError: For other HTTP errors
68
+ """
69
+ if response.status_code == 401:
70
+ raise AuthenticationError("Invalid API key or insufficient permissions")
71
+ elif response.status_code in [429, 502, 503]:
72
+ raise HTTPError(f"Retriable error {response.status_code}", response=response)
73
+ else:
74
+ response.raise_for_status()
75
+
76
+
77
+ def _handle_persistent_rate_limit(func):
78
+ """
79
+ Decorator to catch persistent 429 errors after retries and convert them to RateLimitError.
80
+
81
+ This ensures consistent exception handling for both client-side and server-side rate limiting.
82
+ """
83
+
84
+ def wrapper(*args, **kwargs):
85
+ try:
86
+ return func(*args, **kwargs)
87
+ except HTTPError as e:
88
+ if (
89
+ hasattr(e, "response")
90
+ and e.response is not None
91
+ and e.response.status_code == 429
92
+ ):
93
+ raise RateLimitError(
94
+ "Server rate limit exceeded after retries. Please wait before making another request.",
95
+ retry_after=None, # Server didn't provide retry-after info
96
+ ) from e
97
+ raise
98
+
99
+ return wrapper
100
+
101
+
56
102
  #################################################
57
103
  class API:
58
104
  """
@@ -98,7 +144,7 @@ class API:
98
144
 
99
145
  # set keep-alive headers
100
146
  self._session.headers.update(
101
- {"Connection": "keep-alive", "User-Agent": "deepsights-api/1.3.2"}
147
+ {"Connection": "keep-alive", "User-Agent": "deepsights-api/1.3.4"}
102
148
  )
103
149
 
104
150
  # store default timeout
@@ -120,6 +166,7 @@ class API:
120
166
  return self._endpoint_base + path.strip("/")
121
167
 
122
168
  #######################################
169
+ @_handle_persistent_rate_limit
123
170
  @retry(
124
171
  stop=stop_after_attempt(3),
125
172
  wait=wait_random_exponential(max=5),
@@ -143,6 +190,8 @@ class API:
143
190
  The JSON body of the server's response to the request.
144
191
 
145
192
  Raises:
193
+ AuthenticationError: If the request fails with a 401 status code.
194
+ RateLimitError: If the request fails with persistent 429 status code after retries.
146
195
  HTTPError: If the GET request fails with a non-200 status code and not in the expected_statuscodes list.
147
196
  """
148
197
  timeout = timeout or self._default_timeout
@@ -154,15 +203,12 @@ class API:
154
203
  response.status_code not in [200, 201, 202]
155
204
  and response.status_code not in expected_statuscodes
156
205
  ):
157
- if response.status_code in [429, 502, 503]:
158
- raise HTTPError(
159
- f"Retriable error {response.status_code}", response=response
160
- )
161
- response.raise_for_status()
206
+ _handle_http_error(response)
162
207
 
163
208
  return response.json()
164
209
 
165
210
  #######################################
211
+ @_handle_persistent_rate_limit
166
212
  @retry(
167
213
  stop=stop_after_attempt(3),
168
214
  wait=wait_random_exponential(max=5),
@@ -189,6 +235,8 @@ class API:
189
235
  bytes: The raw content of the server's response.
190
236
 
191
237
  Raises:
238
+ AuthenticationError: If the request fails with a 401 status code.
239
+ RateLimitError: If the request fails with persistent 429 status code after retries.
192
240
  HTTPError: If the GET request fails with a non-200 status code and not in the expected_statuscodes list.
193
241
  """
194
242
  timeout = timeout or self._default_timeout
@@ -200,15 +248,12 @@ class API:
200
248
  response.status_code not in [200, 201, 202]
201
249
  and response.status_code not in expected_statuscodes
202
250
  ):
203
- if response.status_code in [429, 502, 503]:
204
- raise HTTPError(
205
- f"Retriable error {response.status_code}", response=response
206
- )
207
- response.raise_for_status()
251
+ _handle_http_error(response)
208
252
 
209
253
  return response.content
210
254
 
211
255
  #######################################
256
+ @_handle_persistent_rate_limit
212
257
  @retry(
213
258
  stop=stop_after_attempt(3),
214
259
  wait=wait_random_exponential(max=5),
@@ -236,6 +281,11 @@ class API:
236
281
 
237
282
  Returns:
238
283
  Dict: The JSON body of the server's response to the request.
284
+
285
+ Raises:
286
+ AuthenticationError: If the request fails with a 401 status code.
287
+ RateLimitError: If the request fails with persistent 429 status code after retries.
288
+ HTTPError: For other HTTP errors.
239
289
  """
240
290
  timeout = timeout or self._default_timeout
241
291
  response = self._session.post(
@@ -246,15 +296,12 @@ class API:
246
296
  response.status_code not in [200, 201, 202]
247
297
  and response.status_code not in expected_statuscodes
248
298
  ):
249
- if response.status_code in [429, 502, 503]:
250
- raise HTTPError(
251
- f"Retriable error {response.status_code}", response=response
252
- )
253
- response.raise_for_status()
299
+ _handle_http_error(response)
254
300
 
255
301
  return response.json()
256
302
 
257
303
  #######################################
304
+ @_handle_persistent_rate_limit
258
305
  @retry(
259
306
  stop=stop_after_attempt(3),
260
307
  wait=wait_random_exponential(max=5),
@@ -273,6 +320,8 @@ class API:
273
320
 
274
321
  Raises:
275
322
 
323
+ AuthenticationError: If the request fails with a 401 status code.
324
+ RateLimitError: If the request fails with persistent 429 status code after retries.
276
325
  HTTPError: If the DELETE request fails with a non-200 status code.
277
326
  """
278
327
  timeout = timeout or self._default_timeout
@@ -282,11 +331,7 @@ class API:
282
331
  response.status_code not in [200, 204]
283
332
  and response.status_code not in expected_statuscodes
284
333
  ):
285
- if response.status_code in [429, 502, 503]:
286
- raise HTTPError(
287
- f"Retriable error {response.status_code}", response=response
288
- )
289
- response.raise_for_status()
334
+ _handle_http_error(response)
290
335
 
291
336
 
292
337
  #################################################
@@ -219,6 +219,8 @@ def hybrid_search(
219
219
  Returns:
220
220
  List[HybridSearchResult]: The list of hybrid search results.
221
221
  """
222
+ MAX_QUERY_LENGTH = 512
223
+
222
224
  # Input validation
223
225
  if query is None:
224
226
  raise ValueError("The 'query' argument is required.")
@@ -227,8 +229,8 @@ def hybrid_search(
227
229
  query = query.strip()
228
230
  if len(query) == 0:
229
231
  raise ValueError("The 'query' cannot be empty.")
230
- if len(query) > 100:
231
- raise ValueError("The 'query' must be 100 characters or less.")
232
+ if len(query) > MAX_QUERY_LENGTH:
233
+ raise ValueError(f"The 'query' must be {MAX_QUERY_LENGTH} characters or less.")
232
234
 
233
235
  body = {
234
236
  "query": query,
@@ -0,0 +1,44 @@
1
+ # Copyright 2024-2025 Market Logic Software AG. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """
16
+ This module contains custom exception classes for the DeepSights API.
17
+ """
18
+
19
+
20
+ class DeepSightsError(Exception):
21
+ """Base exception class for all DeepSights API errors."""
22
+
23
+ pass
24
+
25
+
26
+ class AuthenticationError(DeepSightsError):
27
+ """Raised when authentication fails due to invalid credentials or permissions."""
28
+
29
+ pass
30
+
31
+
32
+ class RateLimitError(DeepSightsError):
33
+ """Raised when API rate limits are exceeded."""
34
+
35
+ def __init__(self, message: str = "Rate limit exceeded", retry_after: float = None):
36
+ """
37
+ Initialize RateLimitError.
38
+
39
+ Args:
40
+ message: Error message describing the rate limit violation
41
+ retry_after: Optional seconds to wait before retrying
42
+ """
43
+ super().__init__(message)
44
+ self.retry_after = retry_after
@@ -16,7 +16,10 @@
16
16
  This module contains the functions to retrieve reports from the DeepSights self.
17
17
  """
18
18
 
19
+ from ratelimit import RateLimitException, limits
20
+
19
21
  from deepsights.api import APIResource
22
+ from deepsights.exceptions import RateLimitError
20
23
  from deepsights.userclient.resources.answersV2._model import AnswerV2
21
24
  from deepsights.utils import (
22
25
  PollingFailedError,
@@ -32,6 +35,7 @@ class AnswerV2Resource(APIResource):
32
35
  """
33
36
 
34
37
  #################################################
38
+ @limits(calls=10, period=60)
35
39
  def create(self, question: str) -> str:
36
40
  """
37
41
  Creates a new answer V2 by submitting a question to the DeepSights self.
@@ -43,12 +47,21 @@ class AnswerV2Resource(APIResource):
43
47
  Returns:
44
48
 
45
49
  str: The ID of the created answer's minion job.
46
- """
47
50
 
48
- body = {"input": question}
49
- response = self.api.post("/end-user-gateway-service/answers-v2", body=body)
51
+ Raises:
50
52
 
51
- return response["answer_v2"]["minion_job"]["id"]
53
+ RateLimitError: If the rate limit of 10 calls per 60 seconds is exceeded.
54
+ """
55
+ try:
56
+ body = {"input": question}
57
+ response = self.api.post("/end-user-gateway-service/answers-v2", body=body)
58
+
59
+ return response["answer_v2"]["minion_job"]["id"]
60
+ except RateLimitException as e:
61
+ raise RateLimitError(
62
+ "Answer creation rate limit exceeded (10 calls per 60 seconds). Please wait before making another request.",
63
+ retry_after=60,
64
+ ) from e
52
65
 
53
66
  #################################################
54
67
  def wait_for_answer(self, answer_id: str, timeout=90) -> AnswerV2:
@@ -148,6 +161,7 @@ class AnswerV2Resource(APIResource):
148
161
  )
149
162
 
150
163
  #################################################
164
+ @limits(calls=10, period=60)
151
165
  def create_and_wait(self, question: str, timeout=90) -> AnswerV2:
152
166
  """
153
167
  Submits a question to the DeepSights API and waits for the answer to complete.
@@ -163,7 +177,14 @@ class AnswerV2Resource(APIResource):
163
177
 
164
178
  Raises:
165
179
 
180
+ RateLimitError: If the rate limit of 10 calls per 60 seconds is exceeded.
166
181
  PollingTimeoutError: If the answer fails to complete within timeout.
167
182
  PollingFailedError: If the answer fails to complete.
168
183
  """
169
- return self.wait_for_answer(self.create(question), timeout=timeout)
184
+ try:
185
+ return self.wait_for_answer(self.create(question), timeout=timeout)
186
+ except RateLimitException as e:
187
+ raise RateLimitError(
188
+ "Answer creation rate limit exceeded (10 calls per 60 seconds). Please wait before making another request.",
189
+ retry_after=60,
190
+ ) from e
@@ -48,6 +48,8 @@ class DocumentResource(APIResource):
48
48
  Represents a resource for performing hybrid searches via the DeepSights API.
49
49
  """
50
50
 
51
+ MAX_QUERY_LENGTH = 512
52
+
51
53
  #################################################
52
54
  def search(
53
55
  self, query: str, extended_search: bool = False
@@ -68,8 +70,10 @@ class DocumentResource(APIResource):
68
70
  query = query.strip()
69
71
  if len(query) == 0:
70
72
  raise ValueError("The 'query' cannot be empty.")
71
- if len(query) > 100:
72
- raise ValueError("The 'query' must be 100 characters or less.")
73
+ if len(query) > self.MAX_QUERY_LENGTH:
74
+ raise ValueError(
75
+ f"The 'query' must be {self.MAX_QUERY_LENGTH} characters or less."
76
+ )
73
77
 
74
78
  body = {
75
79
  "query": query,
@@ -171,7 +175,7 @@ class DocumentResource(APIResource):
171
175
  # load uncached document pages
172
176
  def _load_document_page(page_id: str) -> DocumentPage:
173
177
  result = self.api.get(
174
- f"/end-user-gateway-service/pages/{page_id}", timeout=5
178
+ f"/end-user-gateway-service/artifacts/pages/{page_id}", timeout=5
175
179
  )
176
180
 
177
181
  # map the document page
@@ -16,7 +16,10 @@
16
16
  This module contains the functions to retrieve reports from the DeepSights self.
17
17
  """
18
18
 
19
+ from ratelimit import RateLimitException, limits
20
+
19
21
  from deepsights.api import APIResource
22
+ from deepsights.exceptions import RateLimitError
20
23
  from deepsights.userclient.resources.reports._model import Report
21
24
  from deepsights.utils import (
22
25
  PollingFailedError,
@@ -32,6 +35,7 @@ class ReportResource(APIResource):
32
35
  """
33
36
 
34
37
  #################################################
38
+ @limits(calls=3, period=60)
35
39
  def create(self, question: str) -> str:
36
40
  """
37
41
  Creates a new report by submitting a question to the DeepSights self.
@@ -43,12 +47,23 @@ class ReportResource(APIResource):
43
47
  Returns:
44
48
 
45
49
  str: The ID of the created report's minion job.
46
- """
47
50
 
48
- body = {"input": question}
49
- response = self.api.post("/end-user-gateway-service/desk-researches", body=body)
51
+ Raises:
52
+
53
+ RateLimitError: If the rate limit of 3 calls per 60 seconds is exceeded.
54
+ """
55
+ try:
56
+ body = {"input": question}
57
+ response = self.api.post(
58
+ "/end-user-gateway-service/desk-researches", body=body
59
+ )
50
60
 
51
- return response["desk_research"]["minion_job"]["id"]
61
+ return response["desk_research"]["minion_job"]["id"]
62
+ except RateLimitException as e:
63
+ raise RateLimitError(
64
+ "Report creation rate limit exceeded (3 calls per 60 seconds). Please wait before making another request.",
65
+ retry_after=60,
66
+ ) from e
52
67
 
53
68
  #################################################
54
69
  def wait_for_report(self, report_id: str, timeout=600) -> Report: