port-ocean 0.28.15__py3-none-any.whl → 0.28.17__py3-none-any.whl
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.
Potentially problematic release.
This version of port-ocean might be problematic. Click here for more details.
- port_ocean/clients/port/mixins/entities.py +79 -44
- port_ocean/helpers/retry.py +8 -2
- port_ocean/tests/clients/port/mixins/test_entities.py +71 -5
- port_ocean/tests/helpers/test_retry.py +623 -1
- {port_ocean-0.28.15.dist-info → port_ocean-0.28.17.dist-info}/METADATA +1 -1
- {port_ocean-0.28.15.dist-info → port_ocean-0.28.17.dist-info}/RECORD +9 -9
- {port_ocean-0.28.15.dist-info → port_ocean-0.28.17.dist-info}/LICENSE.md +0 -0
- {port_ocean-0.28.15.dist-info → port_ocean-0.28.17.dist-info}/WHEEL +0 -0
- {port_ocean-0.28.15.dist-info → port_ocean-0.28.17.dist-info}/entry_points.txt +0 -0
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import json
|
|
2
3
|
from typing import Any, Literal
|
|
3
4
|
from urllib.parse import quote_plus
|
|
4
|
-
import json
|
|
5
5
|
|
|
6
6
|
import httpx
|
|
7
7
|
from loguru import logger
|
|
8
|
-
from
|
|
8
|
+
from starlette import status
|
|
9
|
+
|
|
9
10
|
from port_ocean.clients.port.authentication import PortAuthentication
|
|
10
11
|
from port_ocean.clients.port.types import RequestOptions, UserAgentType
|
|
11
12
|
from port_ocean.clients.port.utils import (
|
|
12
|
-
handle_port_status_code,
|
|
13
13
|
PORT_HTTP_MAX_CONNECTIONS_LIMIT,
|
|
14
|
+
handle_port_status_code,
|
|
14
15
|
)
|
|
16
|
+
from port_ocean.context.ocean import ocean
|
|
15
17
|
from port_ocean.core.models import (
|
|
16
18
|
BulkUpsertResponse,
|
|
17
19
|
Entity,
|
|
18
20
|
PortAPIErrorMessage,
|
|
19
21
|
)
|
|
20
|
-
from starlette import status
|
|
21
|
-
|
|
22
22
|
from port_ocean.helpers.metric.metric import MetricPhase, MetricType
|
|
23
23
|
|
|
24
24
|
ENTITIES_BULK_SAMPLES_SIZE = 10
|
|
@@ -484,54 +484,89 @@ class EntityClientMixin:
|
|
|
484
484
|
parameters_to_include: list[str] | None = None,
|
|
485
485
|
) -> list[Entity]:
|
|
486
486
|
if query is None:
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
487
|
+
return await self._search_entities_by_datasource_paginated(user_agent_type)
|
|
488
|
+
|
|
489
|
+
return await self._search_entities_by_query(
|
|
490
|
+
user_agent_type=user_agent_type,
|
|
491
|
+
query=query,
|
|
492
|
+
parameters_to_include=parameters_to_include,
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
async def _search_entities_by_datasource_paginated(
|
|
496
|
+
self, user_agent_type: UserAgentType
|
|
497
|
+
) -> list[Entity]:
|
|
498
|
+
datasource_prefix = f"port-ocean/{self.auth.integration_type}/"
|
|
499
|
+
datasource_suffix = (
|
|
500
|
+
f"/{self.auth.integration_identifier}/{user_agent_type.value}"
|
|
501
|
+
)
|
|
502
|
+
logger.info(
|
|
503
|
+
f"Searching entities with datasource prefix: {datasource_prefix} and suffix: {datasource_suffix}"
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
next_from: str | None = None
|
|
507
|
+
aggregated_entities: list[Entity] = []
|
|
508
|
+
while True:
|
|
509
|
+
request_body: dict[str, Any] = {
|
|
510
|
+
"datasource_prefix": datasource_prefix,
|
|
511
|
+
"datasource_suffix": datasource_suffix,
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if next_from:
|
|
515
|
+
request_body["from"] = next_from
|
|
494
516
|
|
|
495
517
|
response = await self.client.post(
|
|
496
518
|
f"{self.auth.api_url}/blueprints/entities/datasource-entities",
|
|
497
|
-
json=
|
|
498
|
-
"datasource_prefix": datasource_prefix,
|
|
499
|
-
"datasource_suffix": datasource_suffix,
|
|
500
|
-
},
|
|
519
|
+
json=request_body,
|
|
501
520
|
headers=await self.auth.headers(user_agent_type),
|
|
502
521
|
extensions={"retryable": True},
|
|
503
522
|
)
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
"
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
},
|
|
513
|
-
{
|
|
514
|
-
"property": "$datasource",
|
|
515
|
-
"operator": "contains",
|
|
516
|
-
"value": f"/{self.auth.integration_identifier}/{user_agent_type.value}",
|
|
517
|
-
},
|
|
518
|
-
],
|
|
519
|
-
}
|
|
523
|
+
handle_port_status_code(response)
|
|
524
|
+
response_json = response.json()
|
|
525
|
+
aggregated_entities.extend(
|
|
526
|
+
Entity.parse_obj(result) for result in response_json.get("entities", [])
|
|
527
|
+
)
|
|
528
|
+
next_from = response_json.get("next")
|
|
529
|
+
if not next_from:
|
|
530
|
+
break
|
|
520
531
|
|
|
521
|
-
|
|
522
|
-
query["rules"].extend(default_query["rules"])
|
|
532
|
+
return aggregated_entities
|
|
523
533
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
534
|
+
async def _search_entities_by_query(
|
|
535
|
+
self,
|
|
536
|
+
user_agent_type: UserAgentType,
|
|
537
|
+
query: dict[Any, Any],
|
|
538
|
+
parameters_to_include: list[str] | None,
|
|
539
|
+
) -> list[Entity]:
|
|
540
|
+
default_query = {
|
|
541
|
+
"combinator": "and",
|
|
542
|
+
"rules": [
|
|
543
|
+
{
|
|
544
|
+
"property": "$datasource",
|
|
545
|
+
"operator": "contains",
|
|
546
|
+
"value": f"port-ocean/{self.auth.integration_type}/",
|
|
532
547
|
},
|
|
533
|
-
|
|
534
|
-
|
|
548
|
+
{
|
|
549
|
+
"property": "$datasource",
|
|
550
|
+
"operator": "contains",
|
|
551
|
+
"value": f"/{self.auth.integration_identifier}/{user_agent_type.value}",
|
|
552
|
+
},
|
|
553
|
+
],
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if query.get("rules"):
|
|
557
|
+
query["rules"].extend(default_query["rules"])
|
|
558
|
+
|
|
559
|
+
logger.info(f"Searching entities with custom query: {query}")
|
|
560
|
+
response = await self.client.post(
|
|
561
|
+
f"{self.auth.api_url}/entities/search",
|
|
562
|
+
json=query,
|
|
563
|
+
headers=await self.auth.headers(user_agent_type),
|
|
564
|
+
params={
|
|
565
|
+
"exclude_calculated_properties": "true",
|
|
566
|
+
"include": parameters_to_include or ["blueprint", "identifier"],
|
|
567
|
+
},
|
|
568
|
+
extensions={"retryable": True},
|
|
569
|
+
)
|
|
535
570
|
|
|
536
571
|
handle_port_status_code(response)
|
|
537
572
|
return [Entity.parse_obj(result) for result in response.json()["entities"]]
|
port_ocean/helpers/retry.py
CHANGED
|
@@ -359,8 +359,11 @@ class RetryTransport(httpx.AsyncBaseTransport, httpx.BaseTransport):
|
|
|
359
359
|
else:
|
|
360
360
|
# If no Content-Length header, try to get actual content size
|
|
361
361
|
try:
|
|
362
|
-
|
|
362
|
+
content = await response.aread()
|
|
363
|
+
actual_size = len(content)
|
|
363
364
|
size_info = actual_size
|
|
365
|
+
# Restore the cached body so downstream code can still use .json()/.text/.content
|
|
366
|
+
response._content = content # httpx convention
|
|
364
367
|
except Exception as e:
|
|
365
368
|
cast(logging.Logger, self._logger).error(
|
|
366
369
|
f"Error getting response size: {e}"
|
|
@@ -383,8 +386,11 @@ class RetryTransport(httpx.AsyncBaseTransport, httpx.BaseTransport):
|
|
|
383
386
|
else:
|
|
384
387
|
# If no Content-Length header, try to get actual content size
|
|
385
388
|
try:
|
|
386
|
-
|
|
389
|
+
content = response.read()
|
|
390
|
+
actual_size = len(content)
|
|
387
391
|
size_info = actual_size
|
|
392
|
+
# Restore the cached body so downstream code can still use .json()/.text/.content
|
|
393
|
+
response._content = content # httpx convention
|
|
388
394
|
except Exception as e:
|
|
389
395
|
cast(logging.Logger, self._logger).error(
|
|
390
396
|
f"Error getting response size: {e}"
|
|
@@ -179,7 +179,7 @@ async def test_search_entities_uses_datasource_route_when_query_is_none(
|
|
|
179
179
|
) -> None:
|
|
180
180
|
"""Test that search_entities uses datasource route when query is None"""
|
|
181
181
|
mock_response = MagicMock()
|
|
182
|
-
mock_response.json.return_value = {"entities": []}
|
|
182
|
+
mock_response.json.return_value = {"entities": [], "next": None}
|
|
183
183
|
mock_response.is_error = False
|
|
184
184
|
mock_response.status_code = 200
|
|
185
185
|
mock_response.headers = {}
|
|
@@ -208,8 +208,74 @@ async def test_search_entities_uses_datasource_route_when_query_is_none(
|
|
|
208
208
|
== "https://api.getport.io/v1/blueprints/entities/datasource-entities"
|
|
209
209
|
)
|
|
210
210
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
211
|
+
sent_json = call_args[1]["json"]
|
|
212
|
+
assert sent_json["datasource_prefix"] == "port-ocean/test-integration/"
|
|
213
|
+
assert sent_json["datasource_suffix"] == "/test-identifier/sync"
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
async def test_search_entities_uses_datasource_route_when_query_is_none_two_pages(
|
|
217
|
+
entity_client: EntityClientMixin,
|
|
218
|
+
) -> None:
|
|
219
|
+
"""Test that search_entities uses datasource route when query is None"""
|
|
220
|
+
# First response with pagination token
|
|
221
|
+
mock_response_first = MagicMock()
|
|
222
|
+
mock_response_first.json.return_value = {
|
|
223
|
+
"entities": [
|
|
224
|
+
Entity(identifier="entity_1", blueprint="entity_1"),
|
|
225
|
+
Entity(identifier="entity_2", blueprint="entity_2"),
|
|
226
|
+
],
|
|
227
|
+
"next": "next_page_token",
|
|
228
|
+
}
|
|
229
|
+
mock_response_first.is_error = False
|
|
230
|
+
mock_response_first.status_code = 200
|
|
231
|
+
mock_response_first.headers = {}
|
|
232
|
+
|
|
233
|
+
# Second response without pagination token (end of pagination)
|
|
234
|
+
mock_response_second = MagicMock()
|
|
235
|
+
mock_response_second.json.return_value = {
|
|
236
|
+
"entities": [Entity(identifier="entity_3", blueprint="entity_3")],
|
|
237
|
+
"next": None,
|
|
214
238
|
}
|
|
215
|
-
|
|
239
|
+
mock_response_second.is_error = False
|
|
240
|
+
mock_response_second.status_code = 200
|
|
241
|
+
mock_response_second.headers = {}
|
|
242
|
+
|
|
243
|
+
# Mock the client to return different responses for each call
|
|
244
|
+
entity_client.client.post = AsyncMock(side_effect=[mock_response_first, mock_response_second]) # type: ignore
|
|
245
|
+
entity_client.auth.headers = AsyncMock(return_value={"Authorization": "Bearer test"}) # type: ignore
|
|
246
|
+
|
|
247
|
+
entity_client.auth.integration_type = "test-integration"
|
|
248
|
+
entity_client.auth.integration_identifier = "test-identifier"
|
|
249
|
+
entity_client.auth.api_url = "https://api.getport.io/v1"
|
|
250
|
+
|
|
251
|
+
mock_user_agent_type = MagicMock()
|
|
252
|
+
mock_user_agent_type.value = "sync"
|
|
253
|
+
|
|
254
|
+
entities = await entity_client.search_entities(
|
|
255
|
+
user_agent_type=mock_user_agent_type,
|
|
256
|
+
query=None,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
# Should call the datasource-entities endpoint exactly twice for pagination
|
|
260
|
+
assert entity_client.client.post.await_count == 2
|
|
261
|
+
assert len(entities) == 3
|
|
262
|
+
|
|
263
|
+
# Check first call
|
|
264
|
+
first_call_args = entity_client.client.post.call_args_list[0]
|
|
265
|
+
assert (
|
|
266
|
+
first_call_args[0][0]
|
|
267
|
+
== "https://api.getport.io/v1/blueprints/entities/datasource-entities"
|
|
268
|
+
)
|
|
269
|
+
first_sent_json = first_call_args[1]["json"]
|
|
270
|
+
assert first_sent_json["datasource_prefix"] == "port-ocean/test-integration/"
|
|
271
|
+
assert first_sent_json["datasource_suffix"] == "/test-identifier/sync"
|
|
272
|
+
|
|
273
|
+
# Check second call
|
|
274
|
+
second_call_args = entity_client.client.post.call_args_list[1]
|
|
275
|
+
assert (
|
|
276
|
+
second_call_args[0][0]
|
|
277
|
+
== "https://api.getport.io/v1/blueprints/entities/datasource-entities"
|
|
278
|
+
)
|
|
279
|
+
second_sent_json = second_call_args[1]["json"]
|
|
280
|
+
assert second_sent_json["datasource_prefix"] == "port-ocean/test-integration/"
|
|
281
|
+
assert second_sent_json["datasource_suffix"] == "/test-identifier/sync"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import pytest
|
|
2
|
-
from unittest.mock import Mock
|
|
2
|
+
from unittest.mock import Mock, AsyncMock, patch
|
|
3
3
|
from http import HTTPStatus
|
|
4
4
|
import httpx
|
|
5
5
|
|
|
@@ -307,3 +307,625 @@ class TestRetryConfigIntegration:
|
|
|
307
307
|
"Retry-After",
|
|
308
308
|
]
|
|
309
309
|
assert HTTPStatus.FORBIDDEN in transport._retry_config.retry_status_codes
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
class TestResponseSizeLogging:
|
|
313
|
+
"""Tests for response size logging functionality."""
|
|
314
|
+
|
|
315
|
+
def setup_method(self) -> None:
|
|
316
|
+
"""Reset global callback state before each test."""
|
|
317
|
+
retry_module._RETRY_CONFIG_CALLBACK = None
|
|
318
|
+
retry_module._ON_RETRY_CALLBACK = None
|
|
319
|
+
|
|
320
|
+
def test_should_log_response_size_with_logger(self) -> None:
|
|
321
|
+
"""Test _should_log_response_size returns True when logger is present and not getport.io."""
|
|
322
|
+
mock_transport = Mock()
|
|
323
|
+
mock_logger = Mock()
|
|
324
|
+
transport = RetryTransport(wrapped_transport=mock_transport, logger=mock_logger)
|
|
325
|
+
|
|
326
|
+
mock_request = Mock()
|
|
327
|
+
mock_request.url.host = "api.example.com"
|
|
328
|
+
|
|
329
|
+
assert transport._should_log_response_size(mock_request) is True
|
|
330
|
+
|
|
331
|
+
def test_should_log_response_size_without_logger(self) -> None:
|
|
332
|
+
"""Test _should_log_response_size returns False when no logger."""
|
|
333
|
+
mock_transport = Mock()
|
|
334
|
+
transport = RetryTransport(wrapped_transport=mock_transport)
|
|
335
|
+
|
|
336
|
+
mock_request = Mock()
|
|
337
|
+
mock_request.url.host = "api.example.com"
|
|
338
|
+
|
|
339
|
+
assert transport._should_log_response_size(mock_request) is False
|
|
340
|
+
|
|
341
|
+
def test_should_log_response_size_getport_io(self) -> None:
|
|
342
|
+
"""Test _should_log_response_size returns False for getport.io domains."""
|
|
343
|
+
mock_transport = Mock()
|
|
344
|
+
mock_logger = Mock()
|
|
345
|
+
transport = RetryTransport(wrapped_transport=mock_transport, logger=mock_logger)
|
|
346
|
+
|
|
347
|
+
mock_request = Mock()
|
|
348
|
+
mock_request.url.host = "api.getport.io"
|
|
349
|
+
|
|
350
|
+
assert transport._should_log_response_size(mock_request) is False
|
|
351
|
+
|
|
352
|
+
def test_get_content_length_from_headers(self) -> None:
|
|
353
|
+
"""Test _get_content_length extracts content length from headers."""
|
|
354
|
+
mock_transport = Mock()
|
|
355
|
+
transport = RetryTransport(wrapped_transport=mock_transport)
|
|
356
|
+
|
|
357
|
+
mock_response = Mock()
|
|
358
|
+
mock_response.headers = {"Content-Length": "1024"}
|
|
359
|
+
|
|
360
|
+
assert transport._get_content_length(mock_response) == 1024
|
|
361
|
+
|
|
362
|
+
def test_get_content_length_case_insensitive(self) -> None:
|
|
363
|
+
"""Test _get_content_length works with case insensitive headers."""
|
|
364
|
+
mock_transport = Mock()
|
|
365
|
+
transport = RetryTransport(wrapped_transport=mock_transport)
|
|
366
|
+
|
|
367
|
+
mock_response = Mock()
|
|
368
|
+
mock_response.headers = {"content-length": "2048"}
|
|
369
|
+
|
|
370
|
+
assert transport._get_content_length(mock_response) == 2048
|
|
371
|
+
|
|
372
|
+
def test_get_content_length_no_header(self) -> None:
|
|
373
|
+
"""Test _get_content_length returns None when no content-length header."""
|
|
374
|
+
mock_transport = Mock()
|
|
375
|
+
transport = RetryTransport(wrapped_transport=mock_transport)
|
|
376
|
+
|
|
377
|
+
mock_response = Mock()
|
|
378
|
+
mock_response.headers = {}
|
|
379
|
+
|
|
380
|
+
assert transport._get_content_length(mock_response) is None
|
|
381
|
+
|
|
382
|
+
def test_get_content_length_invalid_value(self) -> None:
|
|
383
|
+
"""Test _get_content_length handles invalid header values."""
|
|
384
|
+
mock_transport = Mock()
|
|
385
|
+
transport = RetryTransport(wrapped_transport=mock_transport)
|
|
386
|
+
|
|
387
|
+
mock_response = Mock()
|
|
388
|
+
mock_response.headers = {"Content-Length": "invalid"}
|
|
389
|
+
|
|
390
|
+
# Should raise ValueError when converting to int
|
|
391
|
+
with pytest.raises(ValueError):
|
|
392
|
+
transport._get_content_length(mock_response)
|
|
393
|
+
|
|
394
|
+
@patch("port_ocean.helpers.retry.cast")
|
|
395
|
+
def test_log_response_size_with_content_length(self, mock_cast: Mock) -> None:
|
|
396
|
+
"""Test _log_response_size logs when Content-Length header is present."""
|
|
397
|
+
mock_transport = Mock()
|
|
398
|
+
mock_logger = Mock()
|
|
399
|
+
mock_cast.return_value = mock_logger
|
|
400
|
+
transport = RetryTransport(wrapped_transport=mock_transport, logger=mock_logger)
|
|
401
|
+
|
|
402
|
+
mock_request = Mock()
|
|
403
|
+
mock_request.method = "GET"
|
|
404
|
+
mock_url = Mock()
|
|
405
|
+
mock_url.host = "api.example.com"
|
|
406
|
+
mock_url.configure_mock(__str__=lambda self: "https://api.example.com/data")
|
|
407
|
+
mock_request.url = mock_url
|
|
408
|
+
|
|
409
|
+
mock_response = Mock()
|
|
410
|
+
mock_response.headers = {"Content-Length": "1024"}
|
|
411
|
+
|
|
412
|
+
transport._log_response_size(mock_request, mock_response)
|
|
413
|
+
|
|
414
|
+
mock_logger.info.assert_called_once_with(
|
|
415
|
+
"Response for GET https://api.example.com/data - Size: 1024 bytes"
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
@patch("port_ocean.helpers.retry.cast")
|
|
419
|
+
def test_log_response_size_without_content_length(self, mock_cast: Mock) -> None:
|
|
420
|
+
"""Test _log_response_size reads content when no Content-Length header."""
|
|
421
|
+
mock_transport = Mock()
|
|
422
|
+
mock_logger = Mock()
|
|
423
|
+
mock_cast.return_value = mock_logger
|
|
424
|
+
transport = RetryTransport(wrapped_transport=mock_transport, logger=mock_logger)
|
|
425
|
+
|
|
426
|
+
mock_request = Mock()
|
|
427
|
+
mock_request.method = "POST"
|
|
428
|
+
mock_url = Mock()
|
|
429
|
+
mock_url.host = "api.example.com"
|
|
430
|
+
mock_url.configure_mock(__str__=lambda self: "https://api.example.com/create")
|
|
431
|
+
mock_request.url = mock_url
|
|
432
|
+
|
|
433
|
+
mock_response = Mock()
|
|
434
|
+
mock_response.headers = {}
|
|
435
|
+
mock_response.read.return_value = b"test content"
|
|
436
|
+
|
|
437
|
+
transport._log_response_size(mock_request, mock_response)
|
|
438
|
+
|
|
439
|
+
mock_response.read.assert_called_once()
|
|
440
|
+
mock_logger.info.assert_called_once_with(
|
|
441
|
+
"Response for POST https://api.example.com/create - Size: 12 bytes"
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
@patch("port_ocean.helpers.retry.cast")
|
|
445
|
+
def test_log_response_size_read_error(self, mock_cast: Mock) -> None:
|
|
446
|
+
"""Test _log_response_size handles read errors gracefully."""
|
|
447
|
+
mock_transport = Mock()
|
|
448
|
+
mock_logger = Mock()
|
|
449
|
+
mock_cast.return_value = mock_logger
|
|
450
|
+
transport = RetryTransport(wrapped_transport=mock_transport, logger=mock_logger)
|
|
451
|
+
|
|
452
|
+
mock_request = Mock()
|
|
453
|
+
mock_request.method = "GET"
|
|
454
|
+
mock_request.url.host = "api.example.com"
|
|
455
|
+
|
|
456
|
+
mock_response = Mock()
|
|
457
|
+
mock_response.headers = {}
|
|
458
|
+
mock_response.read.side_effect = Exception("Read error")
|
|
459
|
+
|
|
460
|
+
transport._log_response_size(mock_request, mock_response)
|
|
461
|
+
|
|
462
|
+
mock_logger.error.assert_called_once_with(
|
|
463
|
+
"Error getting response size: Read error"
|
|
464
|
+
)
|
|
465
|
+
mock_logger.info.assert_not_called()
|
|
466
|
+
|
|
467
|
+
@patch("port_ocean.helpers.retry.cast")
|
|
468
|
+
def test_log_response_size_skips_when_should_not_log(self, mock_cast: Mock) -> None:
|
|
469
|
+
"""Test _log_response_size skips logging when _should_log_response_size returns False."""
|
|
470
|
+
mock_transport = Mock()
|
|
471
|
+
mock_logger = Mock()
|
|
472
|
+
mock_cast.return_value = mock_logger
|
|
473
|
+
transport = RetryTransport(wrapped_transport=mock_transport, logger=mock_logger)
|
|
474
|
+
|
|
475
|
+
mock_request = Mock()
|
|
476
|
+
mock_request.url.host = "api.getport.io" # This should skip logging
|
|
477
|
+
|
|
478
|
+
mock_response = Mock()
|
|
479
|
+
mock_response.headers = {"Content-Length": "1024"}
|
|
480
|
+
|
|
481
|
+
transport._log_response_size(mock_request, mock_response)
|
|
482
|
+
|
|
483
|
+
mock_logger.info.assert_not_called()
|
|
484
|
+
|
|
485
|
+
@pytest.mark.asyncio
|
|
486
|
+
@patch("port_ocean.helpers.retry.cast")
|
|
487
|
+
async def test_log_response_size_async_with_content_length(
|
|
488
|
+
self, mock_cast: Mock
|
|
489
|
+
) -> None:
|
|
490
|
+
"""Test _log_response_size_async logs when Content-Length header is present."""
|
|
491
|
+
mock_transport = Mock()
|
|
492
|
+
mock_logger = Mock()
|
|
493
|
+
mock_cast.return_value = mock_logger
|
|
494
|
+
transport = RetryTransport(wrapped_transport=mock_transport, logger=mock_logger)
|
|
495
|
+
|
|
496
|
+
mock_request = Mock()
|
|
497
|
+
mock_request.method = "GET"
|
|
498
|
+
mock_url = Mock()
|
|
499
|
+
mock_url.host = "api.example.com"
|
|
500
|
+
mock_url.configure_mock(__str__=lambda self: "https://api.example.com/data")
|
|
501
|
+
mock_request.url = mock_url
|
|
502
|
+
|
|
503
|
+
mock_response = Mock()
|
|
504
|
+
mock_response.headers = {"Content-Length": "1024"}
|
|
505
|
+
|
|
506
|
+
await transport._log_response_size_async(mock_request, mock_response)
|
|
507
|
+
|
|
508
|
+
mock_logger.info.assert_called_once_with(
|
|
509
|
+
"Response for GET https://api.example.com/data - Size: 1024 bytes"
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
@pytest.mark.asyncio
|
|
513
|
+
@patch("port_ocean.helpers.retry.cast")
|
|
514
|
+
async def test_log_response_size_async_without_content_length(
|
|
515
|
+
self, mock_cast: Mock
|
|
516
|
+
) -> None:
|
|
517
|
+
"""Test _log_response_size_async reads content when no Content-Length header."""
|
|
518
|
+
mock_transport = Mock()
|
|
519
|
+
mock_logger = Mock()
|
|
520
|
+
mock_cast.return_value = mock_logger
|
|
521
|
+
transport = RetryTransport(wrapped_transport=mock_transport, logger=mock_logger)
|
|
522
|
+
|
|
523
|
+
mock_request = Mock()
|
|
524
|
+
mock_request.method = "POST"
|
|
525
|
+
mock_url = Mock()
|
|
526
|
+
mock_url.host = "api.example.com"
|
|
527
|
+
mock_url.configure_mock(__str__=lambda self: "https://api.example.com/create")
|
|
528
|
+
mock_request.url = mock_url
|
|
529
|
+
|
|
530
|
+
mock_response = Mock()
|
|
531
|
+
mock_response.headers = {}
|
|
532
|
+
mock_response.aread = AsyncMock(return_value=b"test content")
|
|
533
|
+
|
|
534
|
+
await transport._log_response_size_async(mock_request, mock_response)
|
|
535
|
+
|
|
536
|
+
mock_response.aread.assert_called_once()
|
|
537
|
+
mock_logger.info.assert_called_once_with(
|
|
538
|
+
"Response for POST https://api.example.com/create - Size: 12 bytes"
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
@pytest.mark.asyncio
|
|
542
|
+
@patch("port_ocean.helpers.retry.cast")
|
|
543
|
+
async def test_log_response_size_async_read_error(self, mock_cast: Mock) -> None:
|
|
544
|
+
"""Test _log_response_size_async handles read errors gracefully."""
|
|
545
|
+
mock_transport = Mock()
|
|
546
|
+
mock_logger = Mock()
|
|
547
|
+
mock_cast.return_value = mock_logger
|
|
548
|
+
transport = RetryTransport(wrapped_transport=mock_transport, logger=mock_logger)
|
|
549
|
+
|
|
550
|
+
mock_request = Mock()
|
|
551
|
+
mock_request.method = "GET"
|
|
552
|
+
mock_request.url.host = "api.example.com"
|
|
553
|
+
|
|
554
|
+
mock_response = Mock()
|
|
555
|
+
mock_response.headers = {}
|
|
556
|
+
mock_response.aread = AsyncMock(side_effect=Exception("Async read error"))
|
|
557
|
+
|
|
558
|
+
await transport._log_response_size_async(mock_request, mock_response)
|
|
559
|
+
|
|
560
|
+
mock_logger.error.assert_called_once_with(
|
|
561
|
+
"Error getting response size: Async read error"
|
|
562
|
+
)
|
|
563
|
+
mock_logger.info.assert_not_called()
|
|
564
|
+
|
|
565
|
+
@pytest.mark.asyncio
|
|
566
|
+
@patch("port_ocean.helpers.retry.cast")
|
|
567
|
+
async def test_log_response_size_async_skips_when_should_not_log(
|
|
568
|
+
self, mock_cast: Mock
|
|
569
|
+
) -> None:
|
|
570
|
+
"""Test _log_response_size_async skips logging when _should_log_response_size returns False."""
|
|
571
|
+
mock_transport = Mock()
|
|
572
|
+
mock_logger = Mock()
|
|
573
|
+
mock_cast.return_value = mock_logger
|
|
574
|
+
transport = RetryTransport(wrapped_transport=mock_transport, logger=mock_logger)
|
|
575
|
+
|
|
576
|
+
mock_request = Mock()
|
|
577
|
+
mock_request.url.host = "api.getport.io" # This should skip logging
|
|
578
|
+
|
|
579
|
+
mock_response = Mock()
|
|
580
|
+
mock_response.headers = {"Content-Length": "1024"}
|
|
581
|
+
|
|
582
|
+
await transport._log_response_size_async(mock_request, mock_response)
|
|
583
|
+
|
|
584
|
+
mock_logger.info.assert_not_called()
|
|
585
|
+
|
|
586
|
+
@pytest.mark.asyncio
|
|
587
|
+
@patch("port_ocean.helpers.retry.cast")
|
|
588
|
+
async def test_log_response_size_async_restores_content(
|
|
589
|
+
self, mock_cast: Mock
|
|
590
|
+
) -> None:
|
|
591
|
+
"""Test _log_response_size_async restores response content after reading."""
|
|
592
|
+
mock_transport = Mock()
|
|
593
|
+
mock_logger = Mock()
|
|
594
|
+
mock_cast.return_value = mock_logger
|
|
595
|
+
transport = RetryTransport(wrapped_transport=mock_transport, logger=mock_logger)
|
|
596
|
+
|
|
597
|
+
mock_request = Mock()
|
|
598
|
+
mock_request.method = "GET"
|
|
599
|
+
mock_url = Mock()
|
|
600
|
+
mock_url.host = "api.example.com"
|
|
601
|
+
mock_url.configure_mock(__str__=lambda self: "https://api.example.com/data")
|
|
602
|
+
mock_request.url = mock_url
|
|
603
|
+
|
|
604
|
+
test_content = b"test response content"
|
|
605
|
+
mock_response = Mock()
|
|
606
|
+
mock_response.headers = {}
|
|
607
|
+
mock_response.aread = AsyncMock(return_value=test_content)
|
|
608
|
+
|
|
609
|
+
await transport._log_response_size_async(mock_request, mock_response)
|
|
610
|
+
|
|
611
|
+
# Verify that the content was restored to the response
|
|
612
|
+
assert mock_response._content == test_content
|
|
613
|
+
mock_logger.info.assert_called_once_with(
|
|
614
|
+
"Response for GET https://api.example.com/data - Size: 21 bytes"
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
@patch("port_ocean.helpers.retry.cast")
|
|
618
|
+
def test_log_response_size_restores_content(self, mock_cast: Mock) -> None:
|
|
619
|
+
"""Test _log_response_size restores response content after reading."""
|
|
620
|
+
mock_transport = Mock()
|
|
621
|
+
mock_logger = Mock()
|
|
622
|
+
mock_cast.return_value = mock_logger
|
|
623
|
+
transport = RetryTransport(wrapped_transport=mock_transport, logger=mock_logger)
|
|
624
|
+
|
|
625
|
+
mock_request = Mock()
|
|
626
|
+
mock_request.method = "GET"
|
|
627
|
+
mock_url = Mock()
|
|
628
|
+
mock_url.host = "api.example.com"
|
|
629
|
+
mock_url.configure_mock(__str__=lambda self: "https://api.example.com/data")
|
|
630
|
+
mock_request.url = mock_url
|
|
631
|
+
|
|
632
|
+
test_content = b"test response content"
|
|
633
|
+
mock_response = Mock()
|
|
634
|
+
mock_response.headers = {}
|
|
635
|
+
mock_response.read.return_value = test_content
|
|
636
|
+
|
|
637
|
+
transport._log_response_size(mock_request, mock_response)
|
|
638
|
+
|
|
639
|
+
# Verify that the content was restored to the response
|
|
640
|
+
assert mock_response._content == test_content
|
|
641
|
+
mock_logger.info.assert_called_once_with(
|
|
642
|
+
"Response for GET https://api.example.com/data - Size: 21 bytes"
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
class TestResponseSizeLoggingIntegration:
|
|
647
|
+
"""Integration tests to verify response consumption works after size logging."""
|
|
648
|
+
|
|
649
|
+
def setup_method(self) -> None:
|
|
650
|
+
"""Reset global callback state before each test."""
|
|
651
|
+
retry_module._RETRY_CONFIG_CALLBACK = None
|
|
652
|
+
retry_module._ON_RETRY_CALLBACK = None
|
|
653
|
+
|
|
654
|
+
@patch("port_ocean.helpers.retry.cast")
|
|
655
|
+
def test_log_response_size_preserves_json_consumption(
|
|
656
|
+
self, mock_cast: Mock
|
|
657
|
+
) -> None:
|
|
658
|
+
"""Test that _log_response_size preserves response for .json() consumption."""
|
|
659
|
+
mock_transport = Mock()
|
|
660
|
+
mock_logger = Mock()
|
|
661
|
+
mock_cast.return_value = mock_logger
|
|
662
|
+
transport = RetryTransport(wrapped_transport=mock_transport, logger=mock_logger)
|
|
663
|
+
|
|
664
|
+
mock_request = Mock()
|
|
665
|
+
mock_request.method = "GET"
|
|
666
|
+
mock_request.url.host = "api.example.com"
|
|
667
|
+
|
|
668
|
+
# Create a mock response with JSON content
|
|
669
|
+
json_content = b'{"message": "test", "data": [1, 2, 3]}'
|
|
670
|
+
mock_response = Mock()
|
|
671
|
+
mock_response.headers = {} # No Content-Length header to force content reading
|
|
672
|
+
mock_response.read.return_value = json_content
|
|
673
|
+
mock_response.json.return_value = {"message": "test", "data": [1, 2, 3]}
|
|
674
|
+
|
|
675
|
+
# Call the logging function
|
|
676
|
+
transport._log_response_size(mock_request, mock_response)
|
|
677
|
+
|
|
678
|
+
# Verify logging occurred
|
|
679
|
+
mock_logger.info.assert_called_once()
|
|
680
|
+
|
|
681
|
+
# Verify that response.json() can still be called without StreamConsumed error
|
|
682
|
+
result = mock_response.json()
|
|
683
|
+
assert result == {"message": "test", "data": [1, 2, 3]}
|
|
684
|
+
|
|
685
|
+
# Verify that read was called and content was restored
|
|
686
|
+
mock_response.read.assert_called_once()
|
|
687
|
+
assert mock_response._content == json_content
|
|
688
|
+
|
|
689
|
+
@patch("port_ocean.helpers.retry.cast")
|
|
690
|
+
def test_log_response_size_with_content_length_preserves_json(
|
|
691
|
+
self, mock_cast: Mock
|
|
692
|
+
) -> None:
|
|
693
|
+
"""Test that _log_response_size with Content-Length header preserves JSON consumption."""
|
|
694
|
+
mock_transport = Mock()
|
|
695
|
+
mock_logger = Mock()
|
|
696
|
+
mock_cast.return_value = mock_logger
|
|
697
|
+
transport = RetryTransport(wrapped_transport=mock_transport, logger=mock_logger)
|
|
698
|
+
|
|
699
|
+
mock_request = Mock()
|
|
700
|
+
mock_request.method = "POST"
|
|
701
|
+
mock_request.url.host = "api.example.com"
|
|
702
|
+
|
|
703
|
+
# Create a mock response with Content-Length header
|
|
704
|
+
mock_response = Mock()
|
|
705
|
+
mock_response.headers = {"Content-Length": "1024"}
|
|
706
|
+
mock_response.json.return_value = {"status": "success", "id": 123}
|
|
707
|
+
|
|
708
|
+
# Call the logging function
|
|
709
|
+
transport._log_response_size(mock_request, mock_response)
|
|
710
|
+
|
|
711
|
+
# Verify logging occurred
|
|
712
|
+
mock_logger.info.assert_called_once()
|
|
713
|
+
|
|
714
|
+
# Verify that response.json() can still be called
|
|
715
|
+
result = mock_response.json()
|
|
716
|
+
assert result == {"status": "success", "id": 123}
|
|
717
|
+
|
|
718
|
+
# Verify that read was NOT called since we had Content-Length
|
|
719
|
+
mock_response.read.assert_not_called()
|
|
720
|
+
|
|
721
|
+
@pytest.mark.asyncio
|
|
722
|
+
@patch("port_ocean.helpers.retry.cast")
|
|
723
|
+
async def test_log_response_size_async_preserves_json_consumption(
|
|
724
|
+
self, mock_cast: Mock
|
|
725
|
+
) -> None:
|
|
726
|
+
"""Test that _log_response_size_async preserves response for .json() consumption."""
|
|
727
|
+
mock_transport = Mock()
|
|
728
|
+
mock_logger = Mock()
|
|
729
|
+
mock_cast.return_value = mock_logger
|
|
730
|
+
transport = RetryTransport(wrapped_transport=mock_transport, logger=mock_logger)
|
|
731
|
+
|
|
732
|
+
mock_request = Mock()
|
|
733
|
+
mock_request.method = "GET"
|
|
734
|
+
mock_request.url.host = "api.example.com"
|
|
735
|
+
|
|
736
|
+
# Create a mock response with JSON content
|
|
737
|
+
json_content = b'{"users": [{"name": "John", "age": 30}]}'
|
|
738
|
+
mock_response = Mock()
|
|
739
|
+
mock_response.headers = {} # No Content-Length header to force content reading
|
|
740
|
+
mock_response.aread = AsyncMock(return_value=json_content)
|
|
741
|
+
mock_response.json.return_value = {"users": [{"name": "John", "age": 30}]}
|
|
742
|
+
|
|
743
|
+
# Call the async logging function
|
|
744
|
+
await transport._log_response_size_async(mock_request, mock_response)
|
|
745
|
+
|
|
746
|
+
# Verify logging occurred
|
|
747
|
+
mock_logger.info.assert_called_once()
|
|
748
|
+
|
|
749
|
+
# Verify that response.json() can still be called without StreamConsumed error
|
|
750
|
+
result = mock_response.json()
|
|
751
|
+
assert result == {"users": [{"name": "John", "age": 30}]}
|
|
752
|
+
|
|
753
|
+
# Verify that aread was called and content was restored
|
|
754
|
+
mock_response.aread.assert_called_once()
|
|
755
|
+
assert mock_response._content == json_content
|
|
756
|
+
|
|
757
|
+
@pytest.mark.asyncio
|
|
758
|
+
@patch("port_ocean.helpers.retry.cast")
|
|
759
|
+
async def test_log_response_size_async_with_content_length_preserves_json(
|
|
760
|
+
self, mock_cast: Mock
|
|
761
|
+
) -> None:
|
|
762
|
+
"""Test that _log_response_size_async with Content-Length header preserves JSON consumption."""
|
|
763
|
+
mock_transport = Mock()
|
|
764
|
+
mock_logger = Mock()
|
|
765
|
+
mock_cast.return_value = mock_logger
|
|
766
|
+
transport = RetryTransport(wrapped_transport=mock_transport, logger=mock_logger)
|
|
767
|
+
|
|
768
|
+
mock_request = Mock()
|
|
769
|
+
mock_request.method = "PUT"
|
|
770
|
+
mock_request.url.host = "api.example.com"
|
|
771
|
+
|
|
772
|
+
# Create a mock response with Content-Length header
|
|
773
|
+
mock_response = Mock()
|
|
774
|
+
mock_response.headers = {"Content-Length": "2048"}
|
|
775
|
+
mock_response.json.return_value = {
|
|
776
|
+
"updated": True,
|
|
777
|
+
"timestamp": "2023-12-01T12:00:00Z",
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
# Call the async logging function
|
|
781
|
+
await transport._log_response_size_async(mock_request, mock_response)
|
|
782
|
+
|
|
783
|
+
# Verify logging occurred
|
|
784
|
+
mock_logger.info.assert_called_once()
|
|
785
|
+
|
|
786
|
+
# Verify that response.json() can still be called
|
|
787
|
+
result = mock_response.json()
|
|
788
|
+
assert result == {"updated": True, "timestamp": "2023-12-01T12:00:00Z"}
|
|
789
|
+
|
|
790
|
+
# Verify that aread was NOT called since we had Content-Length
|
|
791
|
+
mock_response.aread.assert_not_called()
|
|
792
|
+
|
|
793
|
+
@patch("port_ocean.helpers.retry.cast")
|
|
794
|
+
def test_log_response_size_preserves_text_consumption(
|
|
795
|
+
self, mock_cast: Mock
|
|
796
|
+
) -> None:
|
|
797
|
+
"""Test that _log_response_size preserves response for .text consumption."""
|
|
798
|
+
mock_transport = Mock()
|
|
799
|
+
mock_logger = Mock()
|
|
800
|
+
mock_cast.return_value = mock_logger
|
|
801
|
+
transport = RetryTransport(wrapped_transport=mock_transport, logger=mock_logger)
|
|
802
|
+
|
|
803
|
+
mock_request = Mock()
|
|
804
|
+
mock_request.method = "GET"
|
|
805
|
+
mock_request.url.host = "api.example.com"
|
|
806
|
+
|
|
807
|
+
# Create a mock response with text content
|
|
808
|
+
text_content = b"Hello, World! This is a test response."
|
|
809
|
+
mock_response = Mock()
|
|
810
|
+
mock_response.headers = {} # No Content-Length header to force content reading
|
|
811
|
+
mock_response.read.return_value = text_content
|
|
812
|
+
mock_response.text = "Hello, World! This is a test response."
|
|
813
|
+
|
|
814
|
+
# Call the logging function
|
|
815
|
+
transport._log_response_size(mock_request, mock_response)
|
|
816
|
+
|
|
817
|
+
# Verify logging occurred
|
|
818
|
+
mock_logger.info.assert_called_once()
|
|
819
|
+
|
|
820
|
+
# Verify that response.text can still be accessed
|
|
821
|
+
assert mock_response.text == "Hello, World! This is a test response."
|
|
822
|
+
|
|
823
|
+
# Verify that read was called and content was restored
|
|
824
|
+
mock_response.read.assert_called_once()
|
|
825
|
+
assert mock_response._content == text_content
|
|
826
|
+
|
|
827
|
+
@pytest.mark.asyncio
|
|
828
|
+
@patch("port_ocean.helpers.retry.cast")
|
|
829
|
+
async def test_log_response_size_async_preserves_content_consumption(
|
|
830
|
+
self, mock_cast: Mock
|
|
831
|
+
) -> None:
|
|
832
|
+
"""Test that _log_response_size_async preserves response for .content consumption."""
|
|
833
|
+
mock_transport = Mock()
|
|
834
|
+
mock_logger = Mock()
|
|
835
|
+
mock_cast.return_value = mock_logger
|
|
836
|
+
transport = RetryTransport(wrapped_transport=mock_transport, logger=mock_logger)
|
|
837
|
+
|
|
838
|
+
mock_request = Mock()
|
|
839
|
+
mock_request.method = "GET"
|
|
840
|
+
mock_request.url.host = "api.example.com"
|
|
841
|
+
|
|
842
|
+
# Create a mock response with binary content
|
|
843
|
+
binary_content = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01"
|
|
844
|
+
mock_response = Mock()
|
|
845
|
+
mock_response.headers = {} # No Content-Length header to force content reading
|
|
846
|
+
mock_response.aread = AsyncMock(return_value=binary_content)
|
|
847
|
+
mock_response.content = binary_content
|
|
848
|
+
|
|
849
|
+
# Call the async logging function
|
|
850
|
+
await transport._log_response_size_async(mock_request, mock_response)
|
|
851
|
+
|
|
852
|
+
# Verify logging occurred
|
|
853
|
+
mock_logger.info.assert_called_once()
|
|
854
|
+
|
|
855
|
+
# Verify that response.content can still be accessed
|
|
856
|
+
assert mock_response.content == binary_content
|
|
857
|
+
|
|
858
|
+
# Verify that aread was called and content was restored
|
|
859
|
+
mock_response.aread.assert_called_once()
|
|
860
|
+
assert mock_response._content == binary_content
|
|
861
|
+
|
|
862
|
+
@patch("port_ocean.helpers.retry.cast")
|
|
863
|
+
def test_log_response_size_error_handling_preserves_response(
|
|
864
|
+
self, mock_cast: Mock
|
|
865
|
+
) -> None:
|
|
866
|
+
"""Test that _log_response_size error handling doesn't break response consumption."""
|
|
867
|
+
mock_transport = Mock()
|
|
868
|
+
mock_logger = Mock()
|
|
869
|
+
mock_cast.return_value = mock_logger
|
|
870
|
+
transport = RetryTransport(wrapped_transport=mock_transport, logger=mock_logger)
|
|
871
|
+
|
|
872
|
+
mock_request = Mock()
|
|
873
|
+
mock_request.method = "GET"
|
|
874
|
+
mock_request.url.host = "api.example.com"
|
|
875
|
+
|
|
876
|
+
# Create a mock response that will fail on read
|
|
877
|
+
mock_response = Mock()
|
|
878
|
+
mock_response.headers = {} # No Content-Length header to force content reading
|
|
879
|
+
mock_response.read.side_effect = Exception("Network error")
|
|
880
|
+
mock_response.json.return_value = {"error": "handled gracefully"}
|
|
881
|
+
|
|
882
|
+
# Call the logging function
|
|
883
|
+
transport._log_response_size(mock_request, mock_response)
|
|
884
|
+
|
|
885
|
+
# Verify error was logged
|
|
886
|
+
mock_logger.error.assert_called_once_with(
|
|
887
|
+
"Error getting response size: Network error"
|
|
888
|
+
)
|
|
889
|
+
|
|
890
|
+
# Verify that response.json() can still be called despite the error
|
|
891
|
+
result = mock_response.json()
|
|
892
|
+
assert result == {"error": "handled gracefully"}
|
|
893
|
+
|
|
894
|
+
# Verify that read was attempted
|
|
895
|
+
mock_response.read.assert_called_once()
|
|
896
|
+
|
|
897
|
+
@pytest.mark.asyncio
|
|
898
|
+
@patch("port_ocean.helpers.retry.cast")
|
|
899
|
+
async def test_log_response_size_async_error_handling_preserves_response(
|
|
900
|
+
self, mock_cast: Mock
|
|
901
|
+
) -> None:
|
|
902
|
+
"""Test that _log_response_size_async error handling doesn't break response consumption."""
|
|
903
|
+
mock_transport = Mock()
|
|
904
|
+
mock_logger = Mock()
|
|
905
|
+
mock_cast.return_value = mock_logger
|
|
906
|
+
transport = RetryTransport(wrapped_transport=mock_transport, logger=mock_logger)
|
|
907
|
+
|
|
908
|
+
mock_request = Mock()
|
|
909
|
+
mock_request.method = "GET"
|
|
910
|
+
mock_request.url.host = "api.example.com"
|
|
911
|
+
|
|
912
|
+
# Create a mock response that will fail on aread
|
|
913
|
+
mock_response = Mock()
|
|
914
|
+
mock_response.headers = {} # No Content-Length header to force content reading
|
|
915
|
+
mock_response.aread = AsyncMock(side_effect=Exception("Async network error"))
|
|
916
|
+
mock_response.json.return_value = {"error": "handled gracefully"}
|
|
917
|
+
|
|
918
|
+
# Call the async logging function
|
|
919
|
+
await transport._log_response_size_async(mock_request, mock_response)
|
|
920
|
+
|
|
921
|
+
# Verify error was logged
|
|
922
|
+
mock_logger.error.assert_called_once_with(
|
|
923
|
+
"Error getting response size: Async network error"
|
|
924
|
+
)
|
|
925
|
+
|
|
926
|
+
# Verify that response.json() can still be called despite the error
|
|
927
|
+
result = mock_response.json()
|
|
928
|
+
assert result == {"error": "handled gracefully"}
|
|
929
|
+
|
|
930
|
+
# Verify that aread was attempted
|
|
931
|
+
mock_response.aread.assert_called_once()
|
|
@@ -60,7 +60,7 @@ port_ocean/clients/port/authentication.py,sha256=ZO1Vw1nm-NlVUPPtPS5O4GGDvRmyS3v
|
|
|
60
60
|
port_ocean/clients/port/client.py,sha256=hBXgU0CDseN2F-vn20JqowfVkcd6oSVmYrjn6t4TI6c,3616
|
|
61
61
|
port_ocean/clients/port/mixins/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
62
62
|
port_ocean/clients/port/mixins/blueprints.py,sha256=aMCG4zePsMSMjMLiGrU37h5z5_ElfMzTcTvqvOI5wXY,4683
|
|
63
|
-
port_ocean/clients/port/mixins/entities.py,sha256=
|
|
63
|
+
port_ocean/clients/port/mixins/entities.py,sha256=UiVssYYqJeHhrLahx1mW24B7oGVMZV2WVvUze_htuBk,25279
|
|
64
64
|
port_ocean/clients/port/mixins/integrations.py,sha256=8EhGms1_iOaAPEexmHGF4PJaSSL4O09GtXr_Q8UyaJI,12049
|
|
65
65
|
port_ocean/clients/port/mixins/migrations.py,sha256=vdL_A_NNUogvzujyaRLIoZEu5vmKDY2BxTjoGP94YzI,1467
|
|
66
66
|
port_ocean/clients/port/mixins/organization.py,sha256=A2cP5V49KnjoAXxjmnm_XGth4ftPSU0qURNfnyUyS_Y,1041
|
|
@@ -144,7 +144,7 @@ port_ocean/helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuF
|
|
|
144
144
|
port_ocean/helpers/async_client.py,sha256=M8gKUjX8ZwRbmJ-U6KNq-p-nfGr0CwHdS0eN_pbZAJ0,2103
|
|
145
145
|
port_ocean/helpers/metric/metric.py,sha256=6SMxov1WcZAV0NehMGMqWiLoOIpw-2fOpVbtPWhmW1c,14544
|
|
146
146
|
port_ocean/helpers/metric/utils.py,sha256=1lAgrxnZLuR_wUNDyPOPzLrm32b8cDdioob2lvnPQ1A,1619
|
|
147
|
-
port_ocean/helpers/retry.py,sha256=
|
|
147
|
+
port_ocean/helpers/retry.py,sha256=fkKL_dSNKbGLpa9qi3Ceu2yCqcnzC0OV3dcEWo_kPHA,22067
|
|
148
148
|
port_ocean/helpers/stream.py,sha256=_UwsThzXynxWzL8OlBT1pmb2evZBi9HaaqeAGNuTuOI,2338
|
|
149
149
|
port_ocean/log/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
150
150
|
port_ocean/log/handlers.py,sha256=LJ1WAfq7wYCrBpeLPihMKmWjdSahKKXNHFMRYkbk0Co,3630
|
|
@@ -162,7 +162,7 @@ port_ocean/tests/cache/test_memory_cache.py,sha256=xlwIOBU0RVLYYJU83l_aoZDzZ6QID
|
|
|
162
162
|
port_ocean/tests/clients/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
163
163
|
port_ocean/tests/clients/oauth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
164
164
|
port_ocean/tests/clients/oauth/test_oauth_client.py,sha256=2XVMQUalDpiD539Z7_dk5BK_ngXQzsTmb2lNBsfEm9c,3266
|
|
165
|
-
port_ocean/tests/clients/port/mixins/test_entities.py,sha256=
|
|
165
|
+
port_ocean/tests/clients/port/mixins/test_entities.py,sha256=jOMJ3ICUlhjjPTo4q6qUrEjTKvXRLUE6KjqjdFiDRBY,10766
|
|
166
166
|
port_ocean/tests/clients/port/mixins/test_integrations.py,sha256=vRt_EMsLozQC1LJNXxlvnHj3-FlOBGgAYxg5T0IAqtA,7621
|
|
167
167
|
port_ocean/tests/clients/port/mixins/test_organization_mixin.py,sha256=zzKYz3h8dl4Z5A2QG_924m0y9U6XTth1XYOfwNrd_24,914
|
|
168
168
|
port_ocean/tests/config/test_config.py,sha256=Rk4N-ldVSOfn1p23NzdVdfqUpPrqG2cMut4Sv-sAOrw,1023
|
|
@@ -193,7 +193,7 @@ port_ocean/tests/helpers/integration.py,sha256=_RxS-RHpu11lrbhUXYPZp862HLWx8AoD7
|
|
|
193
193
|
port_ocean/tests/helpers/ocean_app.py,sha256=N06vcNI1klqdcNFq-PXL5vm77u-hODsOSXnj9p8d1AI,2249
|
|
194
194
|
port_ocean/tests/helpers/port_client.py,sha256=S0CXvZWUoHFWWQUOEgdkDammK9Fs3R06wx0flaMrTsg,647
|
|
195
195
|
port_ocean/tests/helpers/smoke_test.py,sha256=_9aJJFRfuGJEg2D2YQJVJRmpreS6gEPHHQq8Q01x4aQ,2697
|
|
196
|
-
port_ocean/tests/helpers/test_retry.py,sha256=
|
|
196
|
+
port_ocean/tests/helpers/test_retry.py,sha256=4wuacabVZTGOICCd0dHlVklhLytsCGu2zjo8hnqEeEs,35912
|
|
197
197
|
port_ocean/tests/log/test_handlers.py,sha256=x2P2Hd6Cb3sQafIE3TRGltbbHeiFHaiEjwRn9py_03g,2165
|
|
198
198
|
port_ocean/tests/test_metric.py,sha256=gDdeJcqJDQ_o3VrYrW23iZyw2NuUsyATdrygSXhcDuQ,8096
|
|
199
199
|
port_ocean/tests/test_ocean.py,sha256=bsXKGTVEjwLSbR7-qSmI4GZ-EzDo0eBE3TNSMsWzYxM,1502
|
|
@@ -211,8 +211,8 @@ port_ocean/utils/repeat.py,sha256=U2OeCkHPWXmRTVoPV-VcJRlQhcYqPWI5NfmPlb1JIbc,32
|
|
|
211
211
|
port_ocean/utils/signal.py,sha256=J1sI-e_32VHP_VUa5bskLMFoJjJOAk5isrnewKDikUI,2125
|
|
212
212
|
port_ocean/utils/time.py,sha256=pufAOH5ZQI7gXvOvJoQXZXZJV-Dqktoj9Qp9eiRwmJ4,1939
|
|
213
213
|
port_ocean/version.py,sha256=UsuJdvdQlazzKGD3Hd5-U7N69STh8Dq9ggJzQFnu9fU,177
|
|
214
|
-
port_ocean-0.28.
|
|
215
|
-
port_ocean-0.28.
|
|
216
|
-
port_ocean-0.28.
|
|
217
|
-
port_ocean-0.28.
|
|
218
|
-
port_ocean-0.28.
|
|
214
|
+
port_ocean-0.28.17.dist-info/LICENSE.md,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
|
|
215
|
+
port_ocean-0.28.17.dist-info/METADATA,sha256=rHk9ILkvieFzWeeX1dljjZ0wfjN9EPX9NDl5tLwOFDc,7016
|
|
216
|
+
port_ocean-0.28.17.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
|
217
|
+
port_ocean-0.28.17.dist-info/entry_points.txt,sha256=F_DNUmGZU2Kme-8NsWM5LLE8piGMafYZygRYhOVtcjA,54
|
|
218
|
+
port_ocean-0.28.17.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|