Skip to content

feat(sql_execution): Implement retry logic for userpod API#89

Draft
tkislan wants to merge 3 commits intomainfrom
tk/userpod-api-retry-5xx
Draft

feat(sql_execution): Implement retry logic for userpod API#89
tkislan wants to merge 3 commits intomainfrom
tk/userpod-api-retry-5xx

Conversation

@tkislan
Copy link
Copy Markdown
Contributor

@tkislan tkislan commented Apr 2, 2026

  • Added a new function _create_retry_session to create a requests session with retry capabilities for handling 5xx errors on POST requests.
  • Updated _generate_temporary_credentials and _get_federated_auth_credentials to use the new retry session for making requests.
  • Introduced unit tests to verify the retry session configuration and its usage in the credential generation functions.

Summary by CodeRabbit

  • New Features

    • Added automatic retry support for credential- and federated-auth POSTs, retrying transient server errors (500, 502, 503, 504) with backoff to improve reliability.
  • Tests

    • Added tests validating retry configuration and that credential and federated-auth requests use the retry-enabled session with expected timeouts and headers.

- Added a new function `_create_retry_session` to create a requests session with retry capabilities for handling 5xx errors on POST requests.
- Updated `_generate_temporary_credentials` and `_get_federated_auth_credentials` to use the new retry session for making requests.
- Introduced unit tests to verify the retry session configuration and its usage in the credential generation functions.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 2, 2026

📝 Walkthrough

Walkthrough

Added _create_retry_session() returning a requests.Session with a Retry/HTTPAdapter configured to retry POSTs on HTTP 500, 502, 503, 504 with total=3 and backoff_factor=0.5. _generate_temporary_credentials() (now annotated -> tuple[str, str]) and _get_federated_auth_credentials() were updated to call _create_retry_session() and use session.post(...) (same timeout=10, headers, response.raise_for_status(), and JSON parsing). Tests were updated/added to validate session creation, adapter configuration, and that both POSTs use the session.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant Caller as Caller
participant Creator as _create_retry_session()
participant Session as RetrySession
participant Auth as Auth Server

Caller->>Creator: request retry-enabled session
Creator-->>Session: returns configured requests.Session
Caller->>Session: session.post(...) (temporary creds / federated auth)
Session-->>Auth: HTTP POST (may retry on 500/502/503/504)
Auth-->>Session: HTTP response
Session-->>Caller: response returned
Caller->>Caller: response.raise_for_status() and parse JSON

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Updates Docs ⚠️ Warning PR adds retry logic for userpod API but omits documentation updates for README, docs/, and upstream repos. Update documentation files to describe the retry logic, including 3 retries with 0.5s backoff on 5xx errors.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Title clearly describes the main change: adding retry logic for API calls in sql_execution module.
Docstring Coverage ✅ Passed Docstring coverage is 88.89% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 2, 2026

📦 Python package built successfully!

  • Version: 2.2.0.dev6+09b3c4b
  • Wheel: deepnote_toolkit-2.2.0.dev6+09b3c4b-py3-none-any.whl
  • Install:
    pip install "deepnote-toolkit @ https://deepnote-staging-runtime-artifactory.s3.amazonaws.com/deepnote-toolkit-packages/2.2.0.dev6%2B09b3c4b/deepnote_toolkit-2.2.0.dev6%2B09b3c4b-py3-none-any.whl"

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@deepnote_toolkit/sql/sql_execution.py`:
- Around line 267-278: The cache lookup in _request_cache_info_from_webapp is
using requests.get directly and thus misses the retry behavior defined in
_create_retry_session; modify _create_retry_session (or its Retry config) to
include GET in allowed_methods (or use allowed_methods=None to cover all
idempotent methods) and then replace the direct requests.get call inside
_request_cache_info_from_webapp with a call through a session returned by
_create_retry_session (e.g., session.get(...)) so cache lookups get the same 5xx
retry protection as POSTs.
- Line 281: Update the signature of
_generate_temporary_credentials(integration_id) to include an explicit return
type annotation (e.g., -> Dict[str, Any]) and add the corresponding typing
imports (from typing import Dict, Any) at the top of the module; ensure the
annotated type matches the actual returned structure from
_generate_temporary_credentials so the function signature and implementation
remain consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: bf6b45a7-61ec-4a87-a583-c7d418b8fe72

📥 Commits

Reviewing files that changed from the base of the PR and between 46807ba and 39baf42.

📒 Files selected for processing (2)
  • deepnote_toolkit/sql/sql_execution.py
  • tests/unit/test_sql_execution.py

Comment on lines +267 to +278
def _create_retry_session() -> requests.Session:
"""Create a requests session with retry on 5xx for POST requests."""
session = requests.Session()
retries = Retry(
total=3,
backoff_factor=0.5,
status_forcelist=[500, 502, 503, 504],
allowed_methods=["POST"],
)
session.mount("http://", HTTPAdapter(max_retries=retries))
session.mount("https://", HTTPAdapter(max_retries=retries))
return session
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider extending retry coverage to GET requests in sql_caching.py.

_request_cache_info_from_webapp (line 156 in sql_caching.py) uses requests.get() directly without retry protection. Cache lookups remain vulnerable to transient 5xx errors that POST requests now handle gracefully.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@deepnote_toolkit/sql/sql_execution.py` around lines 267 - 278, The cache
lookup in _request_cache_info_from_webapp is using requests.get directly and
thus misses the retry behavior defined in _create_retry_session; modify
_create_retry_session (or its Retry config) to include GET in allowed_methods
(or use allowed_methods=None to cover all idempotent methods) and then replace
the direct requests.get call inside _request_cache_info_from_webapp with a call
through a session returned by _create_retry_session (e.g., session.get(...)) so
cache lookups get the same 5xx retry protection as POSTs.

return session


def _generate_temporary_credentials(integration_id):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Add return type annotation.

Missing type hint per coding guidelines.

Proposed fix
-def _generate_temporary_credentials(integration_id):
+def _generate_temporary_credentials(integration_id: str) -> tuple[str, str]:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _generate_temporary_credentials(integration_id):
def _generate_temporary_credentials(integration_id: str) -> tuple[str, str]:
🧰 Tools
🪛 Ruff (0.15.7)

[warning] 281-281: Missing return type annotation for private function _generate_temporary_credentials

(ANN202)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@deepnote_toolkit/sql/sql_execution.py` at line 281, Update the signature of
_generate_temporary_credentials(integration_id) to include an explicit return
type annotation (e.g., -> Dict[str, Any]) and add the corresponding typing
imports (from typing import Dict, Any) at the top of the module; ensure the
annotated type matches the actual returned structure from
_generate_temporary_credentials so the function signature and implementation
remain consistent.

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 2, 2026

❌ 1 Tests Failed:

Tests completed Failed Passed Skipped
941 1 940 4
View the top 1 failed test(s) by shortest run time
tests/unit/test_sql_execution.py::TestCreateRetrySession::test_retry_session_has_correct_config
Stack Traces | 0.001s run time
tests/unit/test_sql_execution.py:1040: in test_retry_session_has_correct_config
    self.assertIn("POST", retries.allowed_methods)
E   AssertionError: 'POST' not found in frozenset({'GET', 'PUT', 'HEAD', 'TRACE', 'DELETE', 'OPTIONS'})

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

@deepnote-bot
Copy link
Copy Markdown

deepnote-bot commented Apr 2, 2026

🚀 Review App Deployment Started

📝 Description 🌐 Link / Info
🌍 Review application ra-89
🔑 Sign-in URL Click to sign-in
📊 Application logs View logs
🔄 Actions Click to redeploy
🚀 ArgoCD deployment View deployment
Last deployed 2026-04-03 08:03:59 (UTC)
📜 Deployed commit 29b16f0b9ca4b1b9e2aadbf11b0d8eb3a59a2da8
🛠️ Toolkit version 09b3c4b

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/unit/test_sql_execution.py`:
- Around line 1055-1065: Convert the context-manager style mocking of
deepnote_toolkit.sql.sql_execution._create_retry_session (currently using with
mock.patch(...) as mock_create_session in the test block) to the decorator style
`@mock.patch` on the test function signature (e.g., add
`@mock.patch`("deepnote_toolkit.sql.sql_execution._create_retry_session") and a
mock_create_session parameter) and remove the inner with-block; apply the same
change to the similar block around lines 1089-1099 so both tests use
decorator-style mocking for consistency with
test_get_federated_auth_credentials_returns_validated_response.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 469a19f4-668a-4b55-8dfc-c8551fc61827

📥 Commits

Reviewing files that changed from the base of the PR and between 39baf42 and aa99cc3.

📒 Files selected for processing (1)
  • tests/unit/test_sql_execution.py

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
deepnote_toolkit/sql/sql_execution.py (1)

280-280: ⚠️ Potential issue | 🟡 Minor

Add parameter type hint.

Return type added, but integration_id lacks type annotation per coding guidelines.

🔧 Fix
-def _generate_temporary_credentials(integration_id) -> tuple[str, str]:
+def _generate_temporary_credentials(integration_id: str) -> tuple[str, str]:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@deepnote_toolkit/sql/sql_execution.py` at line 280, The function
_generate_temporary_credentials has a return type but the parameter
integration_id is missing a type annotation; update the function signature for
_generate_temporary_credentials to add the appropriate type (e.g.,
integration_id: str) to comply with the coding guidelines and any static type
checks, keeping the return annotation tuple[str, str] unchanged and adjusting
any callers only if they rely on a different type.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@deepnote_toolkit/sql/sql_execution.py`:
- Around line 270-274: The Retry instance named "retries" is missing
allowed_methods so POST requests won't be retried; update the Retry(...) call
(the retries variable in sql_execution.py) to include an allowed_methods
parameter that includes "POST" (for example
allowed_methods=frozenset({"GET","POST","PUT","DELETE","HEAD","OPTIONS","TRACE"})
or at minimum allowed_methods=frozenset({"POST"}) depending on desired scope) so
POST requests are retried per the backoff/status_forcelist settings.

In `@tests/unit/test_sql_execution.py`:
- Around line 1026-1040: The Retry configuration in _create_retry_session is
missing the allowed_methods parameter so POST retries aren't enabled; update the
Retry(...) construction inside the _create_retry_session function to include
allowed_methods=["DELETE","GET","HEAD","OPTIONS","POST","PUT","TRACE"] (or an
equivalent set that includes "POST"), then ensure that same Retry instance is
used when mounting the HTTP/HTTPS adapters so
adapter.max_retries.allowed_methods contains "POST".

---

Duplicate comments:
In `@deepnote_toolkit/sql/sql_execution.py`:
- Line 280: The function _generate_temporary_credentials has a return type but
the parameter integration_id is missing a type annotation; update the function
signature for _generate_temporary_credentials to add the appropriate type (e.g.,
integration_id: str) to comply with the coding guidelines and any static type
checks, keeping the return annotation tuple[str, str] unchanged and adjusting
any callers only if they rely on a different type.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: db1a3ece-0680-4ebe-a62e-d27e219521b2

📥 Commits

Reviewing files that changed from the base of the PR and between aa99cc3 and 01be651.

📒 Files selected for processing (2)
  • deepnote_toolkit/sql/sql_execution.py
  • tests/unit/test_sql_execution.py

Comment on lines +270 to +274
retries = Retry(
total=3,
backoff_factor=0.5,
status_forcelist=[500, 502, 503, 504],
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

What are the default allowed_methods in urllib3 Retry class and does it include POST?

💡 Result:

The default allowed_methods in the urllib3 Retry class are the idempotent HTTP methods: DELETE, GET, HEAD, OPTIONS, PUT, TRACE. It does not include POST, as POST is not considered idempotent by default.

Citations:


🏁 Script executed:

# Check the actual code at the mentioned lines
cat -n deepnote_toolkit/sql/sql_execution.py | sed -n '260,280p'

Repository: deepnote/deepnote-toolkit

Length of output: 878


🏁 Script executed:

# Check the test file for the assertion mentioned
cat -n deepnote_toolkit/tests/test_sql_execution.py | sed -n '1035,1045p'

Repository: deepnote/deepnote-toolkit

Length of output: 145


🏁 Script executed:

# Find test files related to sql_execution
find . -name "*test*sql*" -o -name "*sql*test*" | head -20

Repository: deepnote/deepnote-toolkit

Length of output: 422


🏁 Script executed:

# Also check the structure of test directories
find . -type d -name "test*" -o -type d -name "*test" | head -10

Repository: deepnote/deepnote-toolkit

Length of output: 76


🏁 Script executed:

# Check the test file around line 1040
wc -l tests/unit/test_sql_execution.py

Repository: deepnote/deepnote-toolkit

Length of output: 106


🏁 Script executed:

# Search for allowed_methods in test file
rg -n "allowed_methods" tests/unit/test_sql_execution.py

Repository: deepnote/deepnote-toolkit

Length of output: 132


🏁 Script executed:

# Get context around line 1040 in the test file
sed -n '1030,1050p' tests/unit/test_sql_execution.py

Repository: deepnote/deepnote-toolkit

Length of output: 1120


POST requests won't be retried without explicit allowed_methods.

urllib3's Retry defaults exclude POST from allowed_methods (only includes DELETE, GET, HEAD, OPTIONS, PUT, TRACE). The current config will only retry those methods, not POST despite the docstring stating intent to retry POST.

🐛 Fix: Add allowed_methods
     retries = Retry(
         total=3,
         backoff_factor=0.5,
         status_forcelist=[500, 502, 503, 504],
+        allowed_methods=["POST"],
     )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
retries = Retry(
total=3,
backoff_factor=0.5,
status_forcelist=[500, 502, 503, 504],
)
retries = Retry(
total=3,
backoff_factor=0.5,
status_forcelist=[500, 502, 503, 504],
allowed_methods=["POST"],
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@deepnote_toolkit/sql/sql_execution.py` around lines 270 - 274, The Retry
instance named "retries" is missing allowed_methods so POST requests won't be
retried; update the Retry(...) call (the retries variable in sql_execution.py)
to include an allowed_methods parameter that includes "POST" (for example
allowed_methods=frozenset({"GET","POST","PUT","DELETE","HEAD","OPTIONS","TRACE"})
or at minimum allowed_methods=frozenset({"POST"}) depending on desired scope) so
POST requests are retried per the backoff/status_forcelist settings.

Comment on lines +1026 to +1040
class TestCreateRetrySession(unittest.TestCase):
def test_retry_session_has_correct_config(self):
"""Test that _create_retry_session configures retries correctly."""
from deepnote_toolkit.sql.sql_execution import _create_retry_session

session = _create_retry_session()

# Check that both http and https adapters are mounted with retry config
for prefix in ("http://", "https://"):
adapter = session.get_adapter(prefix)
retries = adapter.max_retries
self.assertEqual(retries.total, 3)
self.assertEqual(retries.backoff_factor, 0.5)
self.assertEqual(list(retries.status_forcelist), [500, 502, 503, 504])
self.assertIn("POST", retries.allowed_methods)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Run the specific test to confirm failure
cd "$(git rev-parse --show-toplevel)"
python -m pytest tests/unit/test_sql_execution.py::TestCreateRetrySession::test_retry_session_has_correct_config -v 2>&1 | head -50

Repository: deepnote/deepnote-toolkit

Length of output: 1964


🏁 Script executed:

#!/bin/bash
# Check if the test file exists and read the relevant section
cd "$(git rev-parse --show-toplevel)"
if [ -f tests/unit/test_sql_execution.py ]; then
  echo "=== Test file content around lines 1026-1040 ==="
  sed -n '1026,1040p' tests/unit/test_sql_execution.py
else
  echo "Test file not found"
fi

Repository: deepnote/deepnote-toolkit

Length of output: 882


🏁 Script executed:

#!/bin/bash
# Find the _create_retry_session function
cd "$(git rev-parse --show-toplevel)"
fd -e py -x grep -l "_create_retry_session" {} \;

Repository: deepnote/deepnote-toolkit

Length of output: 143


🏁 Script executed:

#!/bin/bash
# Read the _create_retry_session implementation
cd "$(git rev-parse --show-toplevel)"
rg -A 30 "def _create_retry_session" deepnote_toolkit/sql/sql_execution.py

Repository: deepnote/deepnote-toolkit

Length of output: 1056


Add allowed_methods parameter to the Retry configuration.

Line 1040's assertion expects allowed_methods to contain "POST", but the current _create_retry_session() implementation doesn't set this parameter. The Retry object should include allowed_methods=["DELETE", "GET", "HEAD", "OPTIONS", "POST", "PUT", "TRACE"] to enable retries on POST requests as the docstring claims.

🧰 Tools
🪛 Ruff (0.15.7)

[warning] 1037-1037: Use a regular assert instead of unittest-style assertEqual

Replace assertEqual(...) with assert ...

(PT009)


[warning] 1038-1038: Use a regular assert instead of unittest-style assertEqual

Replace assertEqual(...) with assert ...

(PT009)


[warning] 1039-1039: Use a regular assert instead of unittest-style assertEqual

Replace assertEqual(...) with assert ...

(PT009)


[warning] 1040-1040: Use a regular assert instead of unittest-style assertIn

Replace assertIn(...) with assert ...

(PT009)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/test_sql_execution.py` around lines 1026 - 1040, The Retry
configuration in _create_retry_session is missing the allowed_methods parameter
so POST retries aren't enabled; update the Retry(...) construction inside the
_create_retry_session function to include
allowed_methods=["DELETE","GET","HEAD","OPTIONS","POST","PUT","TRACE"] (or an
equivalent set that includes "POST"), then ensure that same Retry instance is
used when mounting the HTTP/HTTPS adapters so
adapter.max_retries.allowed_methods contains "POST".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants