-
Notifications
You must be signed in to change notification settings - Fork 14
Make timeouts part of RequestsSettings struct #450
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,29 +1,14 @@ | ||
from encord import Dataset, EncordUserClient | ||
from encord.client import EncordClientDataset | ||
|
||
user_client: EncordUserClient = EncordUserClient.create_with_ssh_private_key( | ||
"<your_private_key>", | ||
) | ||
from encord import EncordUserClient | ||
from encord.http.constants import RequestsSettings | ||
|
||
NEW_TIMEOUT = 300 # seconds | ||
|
||
# For user_client requests specifically. These will NOT automatically | ||
# propagate to the Dataset or Project objects | ||
user_client.user_config.read_timeout = NEW_TIMEOUT | ||
user_client.user_config.write_timeout = NEW_TIMEOUT | ||
user_client.user_config.connect_timeout = NEW_TIMEOUT | ||
|
||
# The same procedure works for the Project class that is returned from | ||
# `user_client.get_project("<project_hash>") | ||
dataset: Dataset = user_client.get_dataset("<dataset_hash>") | ||
dataset._client._config.read_timeout = NEW_TIMEOUT | ||
dataset._client._config.write_timeout = NEW_TIMEOUT | ||
dataset._client._config.connect_timeout = NEW_TIMEOUT | ||
requests_settings = RequestsSettings( | ||
connect_timeout=NEW_TIMEOUT, | ||
read_timeout=NEW_TIMEOUT, | ||
write_timeout=NEW_TIMEOUT, | ||
) | ||
|
||
# If you are using the deprecated EncordClientDataset instead | ||
dataset_client: EncordClientDataset = user_client.get_dataset_client( | ||
"<dataset_hash>" | ||
user_client: EncordUserClient = EncordUserClient.create_with_ssh_private_key( | ||
"<your_private_key>", requests_settings=requests_settings | ||
) | ||
dataset_client._config.read_timeout = NEW_TIMEOUT | ||
dataset_client._config.write_timeout = NEW_TIMEOUT | ||
dataset_client._config.connect_timeout = NEW_TIMEOUT |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -156,7 +156,7 @@ def _exception_context(request: requests.PreparedRequest) -> RequestContext: | |
except Exception: | ||
return RequestContext() | ||
|
||
def _request(self, method: QueryMethods, db_object_type: Type[T], uid, timeout, payload=None): | ||
def _request(self, method: QueryMethods, db_object_type: Type[T], uid, timeout: int, payload=None) -> Request: | ||
request = Request(method, db_object_type, uid, timeout, self._config.connect_timeout, payload) | ||
|
||
request.headers = self._config.define_headers(request.data) | ||
Comment on lines
156
to
162
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
|
@@ -205,7 +205,7 @@ def _execute(self, request: Request, retryable=False, enable_logging=True) -> Tu | |
|
||
@contextmanager | ||
def create_new_session( | ||
max_retries: Optional[int] = None, backoff_factor: float = 0, connect_retries=3 | ||
max_retries: Optional[int], backoff_factor: float, connect_retries | ||
) -> Generator[Session, None, None]: | ||
retry_policy = Retry( | ||
connect=connect_retries, | ||
Comment on lines
205
to
211
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The signature of the |
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -148,7 +148,9 @@ def _upload_single_file( | |
backoff_factor: float, | ||
cache_max_age: int = CACHE_DURATION_IN_SECONDS, | ||
) -> None: | ||
with create_new_session(max_retries=max_retries, backoff_factor=backoff_factor) as session: | ||
with create_new_session( | ||
max_retries=max_retries, backoff_factor=backoff_factor, connect_retries=max_retries | ||
) as session: | ||
Comment on lines
148
to
+153
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
url = signed_url["signed_url"] | ||
|
||
with open(file_path, "rb") as f: | ||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,124 @@ | ||||||||||||||||||||||||||||||
import json | ||||||||||||||||||||||||||||||
from datetime import datetime | ||||||||||||||||||||||||||||||
from unittest.mock import MagicMock, patch | ||||||||||||||||||||||||||||||
from uuid import uuid4 | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
import pytest | ||||||||||||||||||||||||||||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey | ||||||||||||||||||||||||||||||
from requests import Session | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
from encord.configs import UserConfig | ||||||||||||||||||||||||||||||
from encord.http.constants import RequestsSettings | ||||||||||||||||||||||||||||||
from encord.http.v2.api_client import ApiClient | ||||||||||||||||||||||||||||||
from encord.user_client import EncordUserClient | ||||||||||||||||||||||||||||||
from tests.fixtures import DUMMY_PRIVATE_KEY | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
PRIVATE_KEY = Ed25519PrivateKey.generate() | ||||||||||||||||||||||||||||||
PROJECT_HASH = uuid4().hex | ||||||||||||||||||||||||||||||
ONTOLOGY_HASH = uuid4().hex | ||||||||||||||||||||||||||||||
DATASET_HASH = uuid4().hex | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
@pytest.fixture | ||||||||||||||||||||||||||||||
def api_client(): | ||||||||||||||||||||||||||||||
return ApiClient(config=UserConfig(PRIVATE_KEY)) | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
def stub_responses(*args, **kwargs) -> MagicMock: | ||||||||||||||||||||||||||||||
body_json = json.loads(args[0].body) | ||||||||||||||||||||||||||||||
request_type = body_json["query_type"] | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
if request_type == "project": | ||||||||||||||||||||||||||||||
response = { | ||||||||||||||||||||||||||||||
"project_hash": PROJECT_HASH, | ||||||||||||||||||||||||||||||
"title": "Test project", | ||||||||||||||||||||||||||||||
"description": "", | ||||||||||||||||||||||||||||||
"created_at": datetime.utcnow(), | ||||||||||||||||||||||||||||||
"last_edited_at": datetime.utcnow(), | ||||||||||||||||||||||||||||||
"editor_ontology": {}, | ||||||||||||||||||||||||||||||
"datasets": [uuid4().hex], | ||||||||||||||||||||||||||||||
"label_rows": [], | ||||||||||||||||||||||||||||||
"ontology_hash": ONTOLOGY_HASH, | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
elif request_type == "ontology": | ||||||||||||||||||||||||||||||
response = { | ||||||||||||||||||||||||||||||
"title": "Test ontology", | ||||||||||||||||||||||||||||||
"description": "", | ||||||||||||||||||||||||||||||
"ontology_hash": ONTOLOGY_HASH, | ||||||||||||||||||||||||||||||
"created_at": datetime.utcnow(), | ||||||||||||||||||||||||||||||
"last_edited_at": datetime.utcnow(), | ||||||||||||||||||||||||||||||
"editor": { | ||||||||||||||||||||||||||||||
"objects": [], | ||||||||||||||||||||||||||||||
"classifications": [], | ||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
elif request_type == "dataset": | ||||||||||||||||||||||||||||||
response = { | ||||||||||||||||||||||||||||||
"dataset_hash": DATASET_HASH, | ||||||||||||||||||||||||||||||
"title": "Test dataset", | ||||||||||||||||||||||||||||||
"description": "", | ||||||||||||||||||||||||||||||
"dataset_type": "cord", | ||||||||||||||||||||||||||||||
"data_rows": [], | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
elif request_type in ["datarows", "labelrowmetadata"]: | ||||||||||||||||||||||||||||||
response = {} # These objects are not used in tests, so it's ok to return empty descriptor for now | ||||||||||||||||||||||||||||||
else: | ||||||||||||||||||||||||||||||
assert False, f"Unsupported request: {request_type}" | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
mock_response = MagicMock() | ||||||||||||||||||||||||||||||
mock_response.status_code = 200 | ||||||||||||||||||||||||||||||
mock_response.json.return_value = {"status": 200, "response": response} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
return mock_response | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
def verify_timeout_and_reset(mock: MagicMock, requests_settings: RequestsSettings, expected_call_count=1) -> None: | ||||||||||||||||||||||||||||||
assert mock.call_count == expected_call_count | ||||||||||||||||||||||||||||||
assert all( | ||||||||||||||||||||||||||||||
call[1]["timeout"] == (requests_settings.connect_timeout, requests_settings.read_timeout) | ||||||||||||||||||||||||||||||
for call in mock.call_args_list | ||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
Comment on lines
+75
to
+81
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The function
Suggested change
|
||||||||||||||||||||||||||||||
mock.reset_mock() | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
@patch.object(Session, "send") | ||||||||||||||||||||||||||||||
def test_request_timeout_settings_correctly_propagated(send: MagicMock, api_client: ApiClient): | ||||||||||||||||||||||||||||||
requests_settings = RequestsSettings( | ||||||||||||||||||||||||||||||
connect_timeout=1001, | ||||||||||||||||||||||||||||||
read_timeout=1002, | ||||||||||||||||||||||||||||||
write_timeout=1003, | ||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
send.side_effect = stub_responses | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
user_client = EncordUserClient.create_with_ssh_private_key( | ||||||||||||||||||||||||||||||
ssh_private_key=DUMMY_PRIVATE_KEY, requests_settings=requests_settings | ||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||
project = user_client.get_project(PROJECT_HASH) | ||||||||||||||||||||||||||||||
verify_timeout_and_reset(send, requests_settings, 2) | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
project.list_label_rows_v2() | ||||||||||||||||||||||||||||||
verify_timeout_and_reset(send, requests_settings) | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
dataset = user_client.get_dataset(DATASET_HASH) | ||||||||||||||||||||||||||||||
verify_timeout_and_reset(send, requests_settings) | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
dataset.list_data_rows() | ||||||||||||||||||||||||||||||
verify_timeout_and_reset(send, requests_settings) | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
ontology = user_client.get_ontology(ONTOLOGY_HASH) | ||||||||||||||||||||||||||||||
verify_timeout_and_reset(send, requests_settings) | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
ontology.refetch_data() | ||||||||||||||||||||||||||||||
verify_timeout_and_reset(send, requests_settings) | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
# Re-mocking for V2 API | ||||||||||||||||||||||||||||||
mock_response = MagicMock() | ||||||||||||||||||||||||||||||
mock_response.status_code = 200 | ||||||||||||||||||||||||||||||
mock_response.json.return_value = {"results": []} | ||||||||||||||||||||||||||||||
send.side_effect = None | ||||||||||||||||||||||||||||||
send.return_value = mock_response | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
_ = list(project.list_collaborator_timers(datetime.now())) | ||||||||||||||||||||||||||||||
verify_timeout_and_reset(send, requests_settings) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The changes look good. The timeouts are now part of the
RequestsSettings
struct and are no longer hard-coded, which improves flexibility and maintainability. TheBaseConfig
class now uses these settings from therequests_settings
object. This is a good practice as it allows for easier configuration changes in the future.