fairagro-middleware-api-client 10.0.3.dev25__tar.gz → 10.0.3.dev26__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 (22) hide show
  1. {fairagro_middleware_api_client-10.0.3.dev25 → fairagro_middleware_api_client-10.0.3.dev26}/PKG-INFO +1 -1
  2. {fairagro_middleware_api_client-10.0.3.dev25 → fairagro_middleware_api_client-10.0.3.dev26}/src/middleware/api_client/api_client.py +21 -3
  3. {fairagro_middleware_api_client-10.0.3.dev25 → fairagro_middleware_api_client-10.0.3.dev26}/tests/unit/test_client.py +20 -0
  4. {fairagro_middleware_api_client-10.0.3.dev25 → fairagro_middleware_api_client-10.0.3.dev26}/.gitignore +0 -0
  5. {fairagro_middleware_api_client-10.0.3.dev25 → fairagro_middleware_api_client-10.0.3.dev26}/README.md +0 -0
  6. {fairagro_middleware_api_client-10.0.3.dev25 → fairagro_middleware_api_client-10.0.3.dev26}/example_client_config.yaml +0 -0
  7. {fairagro_middleware_api_client-10.0.3.dev25 → fairagro_middleware_api_client-10.0.3.dev26}/pyproject.toml +0 -0
  8. {fairagro_middleware_api_client-10.0.3.dev25 → fairagro_middleware_api_client-10.0.3.dev26}/spec/harvest-client/design.md +0 -0
  9. {fairagro_middleware_api_client-10.0.3.dev25 → fairagro_middleware_api_client-10.0.3.dev26}/spec/harvest-client/spec.md +0 -0
  10. {fairagro_middleware_api_client-10.0.3.dev25 → fairagro_middleware_api_client-10.0.3.dev26}/src/middleware/api_client/__init__.py +0 -0
  11. {fairagro_middleware_api_client-10.0.3.dev25 → fairagro_middleware_api_client-10.0.3.dev26}/src/middleware/api_client/config.py +0 -0
  12. {fairagro_middleware_api_client-10.0.3.dev25 → fairagro_middleware_api_client-10.0.3.dev26}/src/middleware/api_client/models.py +0 -0
  13. {fairagro_middleware_api_client-10.0.3.dev25 → fairagro_middleware_api_client-10.0.3.dev26}/src/middleware/api_client/py.typed +0 -0
  14. {fairagro_middleware_api_client-10.0.3.dev25 → fairagro_middleware_api_client-10.0.3.dev26}/tests/client_test_support.py +0 -0
  15. {fairagro_middleware_api_client-10.0.3.dev25 → fairagro_middleware_api_client-10.0.3.dev26}/tests/conftest.py +0 -0
  16. {fairagro_middleware_api_client-10.0.3.dev25 → fairagro_middleware_api_client-10.0.3.dev26}/tests/integration/conftest.py +0 -0
  17. {fairagro_middleware_api_client-10.0.3.dev25 → fairagro_middleware_api_client-10.0.3.dev26}/tests/integration/test_create_arcs.py +0 -0
  18. {fairagro_middleware_api_client-10.0.3.dev25 → fairagro_middleware_api_client-10.0.3.dev26}/tests/unit/conftest.py +0 -0
  19. {fairagro_middleware_api_client-10.0.3.dev25 → fairagro_middleware_api_client-10.0.3.dev26}/tests/unit/test_api_client_config.py +0 -0
  20. {fairagro_middleware_api_client-10.0.3.dev25 → fairagro_middleware_api_client-10.0.3.dev26}/tests/unit/test_client_config.py +0 -0
  21. {fairagro_middleware_api_client-10.0.3.dev25 → fairagro_middleware_api_client-10.0.3.dev26}/tests/unit/test_harvest_arcs.py +0 -0
  22. {fairagro_middleware_api_client-10.0.3.dev25 → fairagro_middleware_api_client-10.0.3.dev26}/tests/unit/test_retry_logic.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fairagro-middleware-api-client
3
- Version: 10.0.3.dev25
3
+ Version: 10.0.3.dev26
4
4
  Summary: The FAIRagro advanced middleware API client
5
5
  Requires-Python: >=3.12
6
6
  Requires-Dist: httpx>=0.28.1
@@ -160,6 +160,23 @@ class ApiClient:
160
160
  return cls._should_retry_request_error(method, path, request_error)
161
161
  return False
162
162
 
163
+ @classmethod
164
+ def _format_request_error(cls, error: BaseException) -> str:
165
+ """Return a non-empty description for a transport/request error.
166
+
167
+ ``httpx.ConnectError`` (and similar) often stringify to ``""``, which
168
+ produces useless log lines like ``Request error: ``. Fall back to the
169
+ exception type name, then ``repr``.
170
+ """
171
+ message = str(error).strip()
172
+ if message:
173
+ return message
174
+ type_name = type(error).__name__
175
+ representation = repr(error).strip()
176
+ if representation and representation != type_name:
177
+ return f"{type_name}: {representation}"
178
+ return type_name
179
+
163
180
  @classmethod
164
181
  def _build_failure_error_message(
165
182
  cls,
@@ -175,9 +192,10 @@ class ApiClient:
175
192
  return f"Request failed after {max_retries} retries: HTTP {status_code}", status_code
176
193
  return cls._format_http_error_message(status_code, failure.response.text), status_code
177
194
 
195
+ detail = cls._format_request_error(failure)
178
196
  if retryable:
179
- return f"Request failed after {max_retries} retries: {failure}", None
180
- return f"Request failed: {failure}", None
197
+ return f"Request failed after {max_retries} retries: {detail}", None
198
+ return f"Request failed: {detail}", None
181
199
 
182
200
  @classmethod
183
201
  def _should_retry_or_raise_failure(
@@ -204,7 +222,7 @@ class ApiClient:
204
222
  if isinstance(failure, httpx.HTTPStatusError):
205
223
  logger.warning("Transient HTTP error %d from server, will retry", failure.response.status_code)
206
224
  else:
207
- logger.warning("Request error: %s. Retrying...", failure)
225
+ logger.warning("Request error: %s. Retrying...", cls._format_request_error(failure))
208
226
  return True
209
227
 
210
228
  msg, normalized_status_code = cls._build_failure_error_message(
@@ -371,6 +371,26 @@ async def test_get_timeout_not_retried(client_config: Config) -> None:
371
371
  assert route.call_count == 1
372
372
 
373
373
 
374
+ def test_format_request_error_falls_back_when_message_empty() -> None:
375
+ """Empty ConnectError messages fall back to the exception type name."""
376
+ empty = httpx.ConnectError("")
377
+ assert str(empty) == ""
378
+ formatted = ApiClient._format_request_error(empty) # noqa: SLF001
379
+ assert "ConnectError" in formatted
380
+ assert formatted.strip() != ""
381
+
382
+
383
+ @pytest.mark.asyncio
384
+ @respx.mock
385
+ async def test_empty_connect_error_message_is_not_blank(client_config: Config) -> None:
386
+ """ApiClientError for empty ConnectError still carries a useful message."""
387
+ respx.post(f"{client_config.api_url}v3/harvests").mock(side_effect=httpx.ConnectError(""))
388
+ async with ApiClient(client_config) as client:
389
+ with pytest.raises(ApiClientError, match="Request failed: ConnectError") as exc_info:
390
+ await client.create_harvest(rdi="test-rdi")
391
+ assert str(exc_info.value).strip() != "Request failed:"
392
+
393
+
374
394
  @pytest.mark.asyncio
375
395
  @respx.mock
376
396
  async def test_global_max_concurrency_limits_parallel_requests(client_config: Config) -> None: