diff --git a/examples/21_port_security_policy.py b/examples/21_port_security_policy.py
new file mode 100644
index 00000000..b4c6e7e6
--- /dev/null
+++ b/examples/21_port_security_policy.py
@@ -0,0 +1,134 @@
+#!/usr/bin/env python3
+"""Verify exposed-port security policies: no auth, API key, and Basic Auth.
+
+Follows the same pattern as example 14 (expose_port) and adds a security
+policy on the user-facing route so that unauthenticated requests are rejected.
+"""
+
+import os
+import sys
+import time
+import random
+import string
+
+import httpx
+
+from koyeb import Sandbox
+from koyeb.sandbox import ApiKey, BasicAuth
+
+API_KEY = "my-secret-api-key"
+BA_USER = "admin"
+BA_PASS = "s3cr3t"
+
+
+def _suffix() -> str:
+ return "".join(random.choices(string.ascii_lowercase + string.digits, k=8))
+
+
+def _setup_server(sandbox: Sandbox) -> str:
+ """Write a test file, start an HTTP server on 8080, expose it, return the base URL."""
+ sandbox.filesystem.write_file("/tmp/index.html", "
ok
")
+ sandbox.launch_process("python3 -m http.server 8080", cwd="/tmp")
+ time.sleep(3)
+
+ exposed = sandbox.expose_port(8080)
+ print(f" Exposed at: {exposed.exposed_at}")
+ time.sleep(2)
+ return exposed.exposed_at
+
+
+def demo_no_auth(api_token: str) -> None:
+ print("\n=== No security policy (public) ===")
+ sandbox = None
+ try:
+ sandbox = Sandbox.create(
+ image="koyeb/sandbox:slim",
+ name=f"sec-noauth-{_suffix()}",
+ api_token=api_token,
+ )
+ base = _setup_server(sandbox)
+
+ resp = httpx.get(f"{base}/index.html", timeout=15)
+ print(f" GET /index.html → {resp.status_code}")
+ assert resp.status_code == 200, f"Expected 200, got {resp.status_code}"
+ print(" ✓ Publicly accessible as expected")
+ finally:
+ if sandbox:
+ sandbox.delete()
+
+
+def demo_api_key(api_token: str) -> None:
+ print("\n=== API key policy ===")
+ sandbox = None
+ try:
+ sandbox = Sandbox.create(
+ image="koyeb/sandbox:slim",
+ name=f"sec-apikey-{_suffix()}",
+ api_token=api_token,
+ exposed_port_security_policy=ApiKey(API_KEY),
+ )
+ base = _setup_server(sandbox)
+
+ # Without key → rejected
+ resp = httpx.get(f"{base}/index.html", timeout=15)
+ print(f" GET (no key) → {resp.status_code}")
+ assert resp.status_code in (401, 403), f"Expected 401/403, got {resp.status_code}"
+ print(" ✓ Rejected without key")
+
+ # With correct key → accepted
+ resp = httpx.get(
+ f"{base}/index.html",
+ headers={"x-api-key": f"{API_KEY}"},
+ timeout=15,
+ )
+ print(f" GET (Bearer {API_KEY}) → {resp.status_code}")
+ assert resp.status_code == 200, f"Expected 200, got {resp.status_code}"
+ print(" ✓ Accepted with correct API key")
+ finally:
+ if sandbox:
+ sandbox.delete()
+
+
+def demo_basic_auth(api_token: str) -> None:
+ print("\n=== Basic Auth policy ===")
+ sandbox = None
+ try:
+ sandbox = Sandbox.create(
+ image="koyeb/sandbox:slim",
+ name=f"sec-basicauth-{_suffix()}",
+ api_token=api_token,
+ exposed_port_security_policy=BasicAuth(username=BA_USER, password=BA_PASS),
+ )
+ base = _setup_server(sandbox)
+
+ # Without credentials → rejected
+ resp = httpx.get(f"{base}/index.html", timeout=15)
+ print(f" GET (no creds) → {resp.status_code}")
+ assert resp.status_code in (401, 403), f"Expected 401/403, got {resp.status_code}"
+ print(" ✓ Rejected without credentials")
+
+ # With correct credentials → accepted
+ resp = httpx.get(f"{base}/index.html", auth=(BA_USER, BA_PASS), timeout=15)
+ print(f" GET ({BA_USER}:{BA_PASS}) → {resp.status_code}")
+ assert resp.status_code == 200, f"Expected 200, got {resp.status_code}"
+ print(" ✓ Accepted with correct Basic Auth credentials")
+ finally:
+ if sandbox:
+ sandbox.delete()
+
+
+def main() -> int:
+ api_token = os.getenv("KOYEB_API_TOKEN")
+ if not api_token:
+ print("Error: KOYEB_API_TOKEN not set")
+ return 1
+
+ demo_no_auth(api_token)
+ demo_api_key(api_token)
+ demo_basic_auth(api_token)
+ print("\nAll assertions passed.")
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/examples/21_port_security_policy_async.py b/examples/21_port_security_policy_async.py
new file mode 100644
index 00000000..15052fee
--- /dev/null
+++ b/examples/21_port_security_policy_async.py
@@ -0,0 +1,141 @@
+#!/usr/bin/env python3
+"""Verify exposed-port security policies: no auth, API key, and Basic Auth (async variant).
+
+Follows the same pattern as example 14 (expose_port) and adds a security
+policy on the user-facing route so that unauthenticated requests are rejected.
+"""
+
+import asyncio
+import os
+import sys
+import random
+import string
+
+import httpx
+
+from koyeb import AsyncSandbox
+from koyeb.sandbox import ApiKey, BasicAuth
+
+API_KEY = "my-secret-api-key"
+BA_USER = "admin"
+BA_PASS = "s3cr3t"
+
+
+def _suffix() -> str:
+ return "".join(random.choices(string.ascii_lowercase + string.digits, k=8))
+
+
+async def _setup_server(sandbox: AsyncSandbox) -> str:
+ """Write a test file, start an HTTP server on 8080, expose it, return the base URL."""
+ await sandbox.filesystem.write_file("/tmp/index.html", "ok
")
+ await sandbox.launch_process("python3 -m http.server 8080", cwd="/tmp")
+ await asyncio.sleep(3)
+
+ exposed = await sandbox.expose_port(8080)
+ print(f" Exposed at: {exposed.exposed_at}")
+ await asyncio.sleep(2)
+ return exposed.exposed_at
+
+
+async def demo_no_auth(api_token: str) -> None:
+ print("\n=== No security policy (public) ===")
+ sandbox = None
+ try:
+ sandbox = await AsyncSandbox.create(
+ image="koyeb/sandbox:slim",
+ name=f"sec-noauth-{_suffix()}",
+ api_token=api_token,
+ )
+ base = await _setup_server(sandbox)
+
+ async with httpx.AsyncClient() as client:
+ resp = await client.get(f"{base}/index.html", timeout=15)
+ print(f" GET /index.html → {resp.status_code}")
+ assert resp.status_code == 200, f"Expected 200, got {resp.status_code}"
+ print(" ✓ Publicly accessible as expected")
+ finally:
+ if sandbox:
+ await sandbox.delete()
+
+
+async def demo_api_key(api_token: str) -> None:
+ print("\n=== API key policy ===")
+ sandbox = None
+ try:
+ sandbox = await AsyncSandbox.create(
+ image="koyeb/sandbox:slim",
+ name=f"sec-apikey-{_suffix()}",
+ api_token=api_token,
+ exposed_port_security_policy=ApiKey(API_KEY),
+ )
+ base = await _setup_server(sandbox)
+
+ async with httpx.AsyncClient() as client:
+ # Without key → rejected
+ resp = await client.get(f"{base}/index.html", timeout=15)
+ print(f" GET (no key) → {resp.status_code}")
+ assert resp.status_code in (401, 403), f"Expected 401/403, got {resp.status_code}"
+ print(" ✓ Rejected without key")
+
+ # With correct key → accepted
+ resp = await client.get(
+ f"{base}/index.html",
+ headers={"x-api-key": API_KEY},
+ timeout=15,
+ )
+ print(f" GET (x-api-key: {API_KEY}) → {resp.status_code}")
+ assert resp.status_code == 200, f"Expected 200, got {resp.status_code}"
+ print(" ✓ Accepted with correct API key")
+ finally:
+ if sandbox:
+ await sandbox.delete()
+
+
+async def demo_basic_auth(api_token: str) -> None:
+ print("\n=== Basic Auth policy ===")
+ sandbox = None
+ try:
+ sandbox = await AsyncSandbox.create(
+ image="koyeb/sandbox:slim",
+ name=f"sec-basicauth-{_suffix()}",
+ api_token=api_token,
+ exposed_port_security_policy=BasicAuth(username=BA_USER, password=BA_PASS),
+ )
+ base = await _setup_server(sandbox)
+
+ async with httpx.AsyncClient() as client:
+ # Without credentials → rejected
+ resp = await client.get(f"{base}/index.html", timeout=15)
+ print(f" GET (no creds) → {resp.status_code}")
+ assert resp.status_code in (401, 403), f"Expected 401/403, got {resp.status_code}"
+ print(" ✓ Rejected without credentials")
+
+ # With correct credentials → accepted
+ resp = await client.get(
+ f"{base}/index.html",
+ auth=(BA_USER, BA_PASS),
+ timeout=15,
+ )
+ print(f" GET ({BA_USER}:{BA_PASS}) → {resp.status_code}")
+ assert resp.status_code == 200, f"Expected 200, got {resp.status_code}"
+ print(" ✓ Accepted with correct Basic Auth credentials")
+ finally:
+ if sandbox:
+ await sandbox.delete()
+
+
+async def main() -> int:
+ api_token = os.getenv("KOYEB_API_TOKEN")
+ if not api_token:
+ print("Error: KOYEB_API_TOKEN not set")
+ return 1
+
+ await demo_no_auth(api_token)
+ await demo_api_key(api_token)
+ await demo_basic_auth(api_token)
+ print("\nAll assertions passed.")
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(asyncio.run(main()))
diff --git a/koyeb/sandbox/__init__.py b/koyeb/sandbox/__init__.py
index 4688c7c6..b3eca959 100644
--- a/koyeb/sandbox/__init__.py
+++ b/koyeb/sandbox/__init__.py
@@ -19,11 +19,20 @@
)
from .filesystem import FileInfo, SandboxFilesystem
from .sandbox import AsyncSandbox, ExposedPort, ProcessInfo, Sandbox
-from .utils import SandboxDeploymentError, SandboxError, SandboxServiceError, SandboxTimeoutError
+from .utils import (
+ ApiKey,
+ BasicAuth,
+ SandboxDeploymentError,
+ SandboxError,
+ SandboxServiceError,
+ SandboxTimeoutError,
+)
__all__ = [
"Sandbox",
"AsyncSandbox",
+ "ApiKey",
+ "BasicAuth",
"ConfigFile",
"Secret",
"SandboxFilesystem",
diff --git a/koyeb/sandbox/sandbox.py b/koyeb/sandbox/sandbox.py
index 7c67bbbd..ac98d5c9 100644
--- a/koyeb/sandbox/sandbox.py
+++ b/koyeb/sandbox/sandbox.py
@@ -11,7 +11,7 @@
import secrets
import time
from dataclasses import dataclass
-from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple
+from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union
from koyeb.api.api.deployments_api import DeploymentsApi
from koyeb.api.exceptions import ApiException, NotFoundException
@@ -22,6 +22,8 @@
from .executor_client import ConnectionInfo
from .utils import (
+ ApiKey,
+ BasicAuth,
DEFAULT_INSTANCE_WAIT_TIMEOUT,
DEFAULT_POLL_INTERVAL,
SandboxDeploymentError,
@@ -31,7 +33,6 @@
build_env_vars,
create_deployment_definition,
create_docker_source,
- create_koyeb_sandbox_routes,
create_sandbox_client,
get_api_clients,
logger,
@@ -117,6 +118,7 @@ def create(
wait_ready: bool = True,
instance_type: str = "micro",
exposed_port_protocol: Optional[str] = None,
+ exposed_port_security_policy: Optional[Union[ApiKey, BasicAuth]] = None,
env: Optional[Dict[str, Any]] = None,
config_files: Optional[Dict[str, Any]] = None,
region: Optional[str] = None,
@@ -149,6 +151,11 @@ def create(
exposed_port_protocol: Protocol to expose ports with ("http" or "http2").
If None, defaults to "http".
If provided, must be one of "http" or "http2".
+ exposed_port_security_policy: Optional access control for the user-facing port (3031).
+ Pass ``ApiKey("my-key")`` to require an API key via
+ ``x-api-key: ``, or
+ ``BasicAuth(username="u", password="p")`` to require HTTP Basic Auth.
+ If None, the port is publicly accessible (default).
env: Environment variables
config_files: Config files to create in the sandbox, as a dictionary mapping
file paths to file contents. Values can be plain strings (default permissions 0644)
@@ -195,6 +202,14 @@ def create(
... image="ghcr.io/myorg/myimage:latest",
... registry_secret="my-ghcr-secret"
... )
+
+ >>> # Protect the exposed port with an API key
+ >>> sandbox = Sandbox.create(exposed_port_security_policy=ApiKey("my-secret-key"))
+
+ >>> # Protect the exposed port with Basic Auth
+ >>> sandbox = Sandbox.create(
+ ... exposed_port_security_policy=BasicAuth(username="admin", password="s3cr3t")
+ ... )
"""
if api_token is None:
api_token = os.getenv("KOYEB_API_TOKEN")
@@ -208,6 +223,7 @@ def create(
image=image,
instance_type=instance_type,
exposed_port_protocol=exposed_port_protocol,
+ exposed_port_security_policy=exposed_port_security_policy,
env=env,
config_files=config_files,
region=region,
@@ -248,6 +264,7 @@ def _create_sync(
image: str = "koyeb/sandbox",
instance_type: str = "micro",
exposed_port_protocol: Optional[str] = None,
+ exposed_port_security_policy: Optional[Union[ApiKey, BasicAuth]] = None,
env: Optional[Dict[str, Any]] = None,
config_files: Optional[Dict[str, Any]] = None,
region: Optional[str] = None,
@@ -278,9 +295,6 @@ def _create_sync(
apps_api = clients.apps
services_api = clients.services
- # Always create routes (ports are always exposed, default to "http")
- routes = create_koyeb_sandbox_routes()
-
# Generate secure sandbox secret
sandbox_secret = secrets.token_urlsafe(32)
@@ -312,8 +326,8 @@ def _create_sync(
env_vars=env_vars,
instance_type=instance_type,
exposed_port_protocol=exposed_port_protocol,
+ exposed_port_security_policy=exposed_port_security_policy,
region=region,
- routes=routes,
idle_timeout=idle_timeout,
enable_tcp_proxy=enable_tcp_proxy,
_experimental_enable_light_sleep=_experimental_enable_light_sleep,
@@ -1245,6 +1259,7 @@ async def create(
wait_ready: bool = True,
instance_type: str = "micro",
exposed_port_protocol: Optional[str] = None,
+ exposed_port_security_policy: Optional[Union[ApiKey, BasicAuth]] = None,
env: Optional[Dict[str, Any]] = None,
config_files: Optional[Dict[str, Any]] = None,
region: Optional[str] = None,
@@ -1277,6 +1292,11 @@ async def create(
exposed_port_protocol: Protocol to expose ports with ("http" or "http2").
If None, defaults to "http".
If provided, must be one of "http" or "http2".
+ exposed_port_security_policy: Optional access control for the user-facing port.
+ Pass ``ApiKey("my-key")`` to require an API key via
+ ``x-api-key: ``, or
+ ``BasicAuth(username="u", password="p")`` to require HTTP Basic Auth.
+ If None, the port is publicly accessible (default).
env: Environment variables
config_files: Config files to create in the sandbox, as a dictionary mapping
file paths to file contents. Values can be plain strings (default permissions 0644)
@@ -1331,9 +1351,6 @@ async def create(
clients = get_async_api_clients(api_token, host)
- # Always create routes
- routes = create_koyeb_sandbox_routes()
-
# Generate secure sandbox secret
sandbox_secret = secrets.token_urlsafe(32)
@@ -1365,8 +1382,8 @@ async def create(
env_vars=env_vars,
instance_type=instance_type,
exposed_port_protocol=exposed_port_protocol,
+ exposed_port_security_policy=exposed_port_security_policy,
region=region,
- routes=routes,
idle_timeout=idle_timeout,
enable_tcp_proxy=enable_tcp_proxy,
_experimental_enable_light_sleep=_experimental_enable_light_sleep,
diff --git a/koyeb/sandbox/utils.py b/koyeb/sandbox/utils.py
index 4482386f..9ea2d579 100644
--- a/koyeb/sandbox/utils.py
+++ b/koyeb/sandbox/utils.py
@@ -8,7 +8,7 @@
import os
import shlex
from dataclasses import dataclass
-from typing import Any, Dict, List, Optional, Tuple
+from typing import Any, Dict, List, Optional, Tuple, Union
from koyeb.api import ApiClient, Configuration
from koyeb.api.api import (
@@ -19,6 +19,7 @@
SecretsApi,
ServicesApi,
)
+from koyeb.api.models.basic_auth_policy import BasicAuthPolicy
from koyeb.api.models.config_file import ConfigFile
from koyeb.api.models.deployment_definition import DeploymentDefinition
from koyeb.api.models.deployment_definition_type import DeploymentDefinitionType
@@ -36,6 +37,7 @@
from koyeb.api.models.deployment_mesh import DeploymentMesh
from koyeb.api.models.docker_source import DockerSource
from koyeb.api.models.proxy_port_protocol import ProxyPortProtocol
+from koyeb.api.models.security_policies import SecurityPolicies
# Setup logging
logger = logging.getLogger(__name__)
@@ -60,6 +62,43 @@
VALID_DEPLOYMENT_PORT_PROTOCOLS = ("http", "http2")
+@dataclass
+class ApiKey:
+ """An API key used to restrict access to the user-facing sandbox port."""
+
+ key: str
+
+
+@dataclass
+class BasicAuth:
+ """HTTP Basic Auth credentials used to restrict access to the user-facing sandbox port."""
+
+ username: str
+ password: str
+
+
+def _build_security_policy(
+ policy,
+) -> Optional[SecurityPolicies]:
+ """
+ Build a SecurityPolicies object from an ApiKey or BasicAuth instance.
+
+ Raises:
+ ValueError: If policy is not an ApiKey or BasicAuth.
+ """
+ if policy is None:
+ return None
+ if isinstance(policy, ApiKey):
+ return SecurityPolicies(api_keys=[policy.key])
+ if isinstance(policy, BasicAuth):
+ return SecurityPolicies(
+ basic_auths=[BasicAuthPolicy(username=policy.username, password=policy.password)]
+ )
+ raise ValueError(
+ "exposed_port_security_policy must be an ApiKey or BasicAuth instance"
+ )
+
+
def _validate_port_protocol(protocol: str) -> str:
"""
Validate port protocol using API model structure.
@@ -376,20 +415,25 @@ def create_koyeb_sandbox_proxy_ports() -> List[DeploymentProxyPort]:
]
-def create_koyeb_sandbox_routes() -> List[DeploymentRoute]:
+def create_koyeb_sandbox_routes(
+ security_policy: Optional[SecurityPolicies] = None,
+) -> List[DeploymentRoute]:
"""
Create route configuration for koyeb/sandbox image to make it publicly accessible.
Creates two routes:
- - Port 3030 accessible at /koyeb-sandbox/
- - Port 3031 accessible at /
+ - Port 3030 accessible at /koyeb-sandbox/ (control plane, no security policy)
+ - Port 3031 accessible at / (user-facing, optional security policy)
+
+ Args:
+ security_policy: Optional security policy to apply to the user-facing port 3031 route.
Returns:
List of DeploymentRoute objects configured for koyeb/sandbox
"""
return [
DeploymentRoute(port=3030, path="/koyeb-sandbox/"),
- DeploymentRoute(port=3031, path="/"),
+ DeploymentRoute(port=3031, path="/", security_policies=security_policy),
]
@@ -399,6 +443,7 @@ def create_deployment_definition(
env_vars: List[DeploymentEnv],
instance_type: str,
exposed_port_protocol: Optional[str] = None,
+ exposed_port_security_policy: Optional[Union[ApiKey, BasicAuth]] = None,
region: Optional[str] = None,
routes: Optional[List[DeploymentRoute]] = None,
idle_timeout: int = 300,
@@ -419,6 +464,8 @@ def create_deployment_definition(
exposed_port_protocol: Protocol to expose ports with ("http" or "http2").
If None, defaults to "http".
If provided, must be one of "http" or "http2".
+ exposed_port_security_policy: Optional security policy for the user-facing port 3031 route.
+ Pass ApiKey("my-key") or BasicAuth(username="u", password="p").
region: Region to deploy to. Defaults to KOYEB_REGION env var, or "na" if not set.
routes: List of routes for public access
idle_timeout: Number of seconds to wait before sleeping the instance if it receives no traffic
@@ -444,6 +491,10 @@ def create_deployment_definition(
protocol = _validate_port_protocol(protocol)
ports = create_koyeb_sandbox_ports(protocol)
+ if routes is None:
+ security_policy = _build_security_policy(exposed_port_security_policy)
+ routes = create_koyeb_sandbox_routes(security_policy=security_policy)
+
# Create TCP proxy ports if enabled
proxy_ports = None
if enable_tcp_proxy:
diff --git a/uv.lock b/uv.lock
index 3f3a51de..01df92b6 100644
--- a/uv.lock
+++ b/uv.lock
@@ -6,10 +6,6 @@ resolution-markers = [
"python_full_version < '3.10'",
]
-[options]
-exclude-newer = "2026-06-20T16:28:02.207712Z"
-exclude-newer-span = "P2D"
-
[[package]]
name = "annotated-types"
version = "0.7.0"
@@ -479,6 +475,12 @@ dependencies = [
{ name = "websockets" },
]
+[package.optional-dependencies]
+socks = [
+ { name = "pysocks" },
+ { name = "socksio" },
+]
+
[package.dev-dependencies]
dev = [
{ name = "autopep8" },
@@ -493,11 +495,14 @@ dev = [
requires-dist = [
{ name = "httpx", specifier = ">=0.28.1" },
{ name = "pydantic", specifier = ">=2" },
+ { name = "pysocks", marker = "extra == 'socks'", specifier = ">=1.7.1" },
{ name = "python-dateutil", specifier = ">=2.8.2" },
+ { name = "socksio", marker = "extra == 'socks'", specifier = ">=1.0.0" },
{ name = "typing-extensions", specifier = ">=4.7.1" },
{ name = "urllib3", specifier = ">=2.1.0,<3.0.0" },
{ name = "websockets", specifier = ">=15.0.1" },
]
+provides-extras = ["socks"]
[package.metadata.requires-dev]
dev = [
@@ -909,6 +914,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
]
+[[package]]
+name = "pysocks"
+version = "1.7.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/bd/11/293dd436aea955d45fc4e8a35b6ae7270f5b8e00b53cf6c024c83b657a11/PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0", size = 284429, upload-time = "2019-09-20T02:07:35.714Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8d/59/b4572118e098ac8e46e399a1dd0f2d85403ce8bbaad9ec79373ed6badaf9/PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5", size = 16725, upload-time = "2019-09-20T02:06:22.938Z" },
+]
+
[[package]]
name = "pytest"
version = "8.4.2"
@@ -1046,6 +1060,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
]
+[[package]]
+name = "socksio"
+version = "1.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f8/5c/48a7d9495be3d1c651198fd99dbb6ce190e2274d0f28b9051307bdec6b85/socksio-1.0.0.tar.gz", hash = "sha256:f88beb3da5b5c38b9890469de67d0cb0f9d494b78b106ca1845f96c10b91c4ac", size = 19055, upload-time = "2020-04-17T15:50:34.664Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/37/c3/6eeb6034408dac0fa653d126c9204ade96b819c936e136c5e8a6897eee9c/socksio-1.0.0-py3-none-any.whl", hash = "sha256:95dc1f15f9b34e8d7b16f06d74b8ccf48f609af32ab33c608d08761c5dcbb1f3", size = 12763, upload-time = "2020-04-17T15:50:31.878Z" },
+]
+
[[package]]
name = "toml"
version = "0.10.2"