Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12"]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v6

Expand Down
6 changes: 3 additions & 3 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Runpod Serverless Module Architecture

**Last Updated**: 2025-12-13
**Last Updated**: 2026-06-18
**Module**: `runpod/serverless/`
**Python Support**: 3.8-3.11
**Python Support**: 3.10-3.14

---

Expand Down Expand Up @@ -1467,4 +1467,4 @@ stateDiagram-v2
---

**Document Version**: 1.0
**Last Updated**: 2025-12-13
**Last Updated**: 2026-06-18
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ cd runpod-python
pip install -e .
```

*Python 3.8 or higher is required to use the latest version of this package.*
*Python 3.10 or higher is required to use the latest version of this package.*

## ⚡ | Serverless Worker (SDK)

Expand Down
8 changes: 4 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name = "runpod"
dynamic = ["version", "dependencies"]
description = "🐍 | Python library for Runpod API and serverless worker SDK."
readme = { file = "README.md", content-type = "text/markdown" }
requires-python = ">=3.8"
requires-python = ">=3.10"
license = { text = "MIT License" }
authors = [
{ name = "Runpod", email = "engineer@runpod.io" },
Expand All @@ -25,10 +25,11 @@ classifiers = [
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Topic :: Internet :: WWW/HTTP",
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
]
Expand Down Expand Up @@ -69,7 +70,6 @@ dev = [
]
test = [
"asynctest",
"nest_asyncio",
"faker",
"pytest-asyncio",
"pytest-cov",
Expand Down
9 changes: 8 additions & 1 deletion pytest.ini
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
[pytest]
addopts = --durations=10 --cov-config=.coveragerc --timeout=120 --timeout_method=thread --cov=runpod --cov-report=xml --cov-report=term-missing --cov-fail-under=90 -W error -p no:cacheprovider -p no:unraisableexception
addopts = --durations=10 --cov-config=.coveragerc --timeout=120 --timeout_method=thread --cov=runpod --cov-report=xml --cov-report=term-missing --cov-fail-under=90 -p no:cacheprovider -p no:unraisableexception
filterwarnings =
error
# Third-party deps (e.g. backoff) still call asyncio APIs that 3.14 deprecates
# and slates for removal in 3.16. The SDK's own code does not; at runtime these
# only warn. Don't fail the suite on them until the deps update.
ignore:'asyncio\.get_event_loop_policy' is deprecated:DeprecationWarning
ignore:'asyncio\.iscoroutinefunction' is deprecated:DeprecationWarning
python_files = tests.py test_*.py *_test.py
norecursedirs = venv *.egg-info .git build tests/e2e
asyncio_mode = auto
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
install_requires=install_requires,
extras_require=extras_require,
packages=find_packages(),
python_requires=">=3.8",
python_requires=">=3.10",
description="🐍 | Python library for Runpod API and serverless worker SDK.",
long_description=long_description,
long_description_content_type="text/markdown",
Expand Down
12 changes: 8 additions & 4 deletions tests/test_serverless/test_utils/test_download.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,14 @@ def test_download_files_from_urls(self, mock_open_file, mock_get, mock_makedirs)

self.assertEqual(len(downloaded_files), len(urls))

for index, url in enumerate(urls):
# Check that the url was called with SyncClientSession.get
self.assertIn(url, mock_get.call_args_list[index][0])

# Downloads run in parallel threads, so the order of get() calls is
# non-deterministic; assert the set of requested URLs instead of order.
requested_urls = {call.args[0] for call in mock_get.call_args_list}
self.assertEqual(requested_urls, set(urls))

# executor.map preserves input order in results, so downloaded_files
# still aligns positionally with urls.
for index in range(len(urls)):
# Check that the file has the correct extension
self.assertTrue(downloaded_files[index].endswith(".jpg"))

Expand Down
46 changes: 26 additions & 20 deletions tests/test_serverless/test_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,19 @@
import os
import sys
from unittest import mock
from unittest.mock import patch, mock_open, Mock, MagicMock

from unittest import IsolatedAsyncioTestCase
import nest_asyncio
from unittest import TestCase
from unittest.mock import patch, mock_open, Mock, MagicMock, AsyncMock

import runpod
from runpod.serverless.modules.rp_logger import RunPodLogger
from runpod.serverless.modules.rp_scale import _handle_uncaught_exception
from runpod.serverless import _signal_handler

nest_asyncio.apply()


class TestWorker(IsolatedAsyncioTestCase):
class TestWorker(TestCase):
"""Tests for Runpod serverless worker."""

async def asyncSetUp(self):
def setUp(self):
self.mock_handler = mock.Mock(return_value="test")
self.mock_config = {
"handler": self.mock_handler,
Expand Down Expand Up @@ -105,10 +101,10 @@ def test_signal_handler(self, mock_exit, mock_logger):
assert mock_logger.info.called


class TestWorkerTestInput(IsolatedAsyncioTestCase):
class TestWorkerTestInput(TestCase):
"""Tests for runpod | serverless| worker"""

async def asyncSetUp(self):
def setUp(self):
self.mock_handler = Mock()
self.mock_handler.return_value = {}

Expand Down Expand Up @@ -176,28 +172,38 @@ def test_generator_handler_exception():
assert True, "Exception was caught as expected"


class TestRunWorker(IsolatedAsyncioTestCase):
class TestRunWorker(TestCase):
"""Tests for runpod | serverless| worker"""

async def asyncSetUp(self):
def setUp(self):
os.environ["RUNPOD_WEBHOOK_GET_JOB"] = "https://test.com"

# run_worker() runs real fitness checks (GPU/memory probes) that exit the
# process when unmet; they have dedicated coverage in
# test_modules/test_fitness/. Stub them so these tests are deterministic
# regardless of host state.
fitness_patcher = patch(
"runpod.serverless.worker.run_fitness_checks", new=AsyncMock()
)
fitness_patcher.start()
self.addCleanup(fitness_patcher.stop)

# Set up the config
self.config = {
"handler": MagicMock(),
"refresh_worker": True,
"rp_args": {"rp_debugger": True, "rp_log_level": "DEBUG"},
}

async def asyncTearDown(self):
def tearDown(self):
sys.excepthook = sys.__excepthook__

@patch("runpod.serverless.modules.rp_scale.AsyncClientSession")
@patch("runpod.serverless.modules.rp_scale.get_job")
@patch("runpod.serverless.modules.rp_job.run_job")
@patch("runpod.serverless.modules.rp_job.stream_result")
@patch("runpod.serverless.modules.rp_job.send_result")
async def test_run_worker(
def test_run_worker(
self,
mock_send_result,
mock_stream_result,
Expand Down Expand Up @@ -228,7 +234,7 @@ async def test_run_worker(
@patch("runpod.serverless.modules.rp_job.run_job")
@patch("runpod.serverless.modules.rp_job.stream_result")
@patch("runpod.serverless.modules.rp_job.send_result")
async def test_run_worker_generator_handler(
def test_run_worker_generator_handler(
self, mock_send_result, mock_stream_result, mock_run_job, mock_get_job
):
"""
Expand Down Expand Up @@ -258,7 +264,7 @@ async def test_run_worker_generator_handler(
@patch("runpod.serverless.modules.rp_job.run_job")
@patch("runpod.serverless.modules.rp_job.stream_result")
@patch("runpod.serverless.modules.rp_job.send_result")
async def test_run_worker_generator_handler_exception(
def test_run_worker_generator_handler_exception(
self, mock_send_result, mock_stream_result, mock_run_job, mock_get_job
):
"""
Expand Down Expand Up @@ -303,7 +309,7 @@ async def test_run_worker_generator_handler_exception(
@patch("runpod.serverless.modules.rp_job.run_job")
@patch("runpod.serverless.modules.rp_job.stream_result")
@patch("runpod.serverless.modules.rp_job.send_result")
async def test_run_worker_generator_aggregate_handler(
def test_run_worker_generator_aggregate_handler(
self, mock_send_result, mock_stream_result, mock_run_job, mock_get_job
):
"""
Expand Down Expand Up @@ -343,7 +349,7 @@ async def test_run_worker_generator_aggregate_handler(
@patch("runpod.serverless.modules.rp_job.run_job")
@patch("runpod.serverless.modules.rp_job.stream_result")
@patch("runpod.serverless.modules.rp_job.send_result")
async def test_run_worker_concurrency(
def test_run_worker_concurrency(
self,
mock_send_result,
mock_stream_result,
Expand Down Expand Up @@ -420,7 +426,7 @@ def concurrency_modifier(current_concurrency):
@patch("runpod.serverless.modules.rp_job.run_job")
@patch("runpod.serverless.modules.rp_job.stream_result")
@patch("runpod.serverless.modules.rp_job.send_result")
async def test_run_worker_multi_processing(
def test_run_worker_multi_processing(
self,
mock_send_result,
mock_stream_result,
Expand Down Expand Up @@ -480,7 +486,7 @@ async def test_run_worker_multi_processing(

@patch("runpod.serverless.modules.rp_scale.get_job")
@patch("runpod.serverless.modules.rp_job.run_job")
async def test_run_worker_multi_processing_scaling_up(
def test_run_worker_multi_processing_scaling_up(
self, mock_run_job, mock_get_job
):
"""
Expand Down