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
10 changes: 6 additions & 4 deletions pycaprio/core/adapters/http_adapter.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
from typing import IO, Union
from typing import List
from typing import Optional
from typing import IO, Union, List, Optional

from pycaprio.core.clients.retryable_client import RetryableInceptionClient
from pycaprio.core.interfaces.adapter import BaseInceptionAdapter
Expand Down Expand Up @@ -30,8 +28,12 @@ def __init__(
inception_host: str,
authentication: authentication_type,
inception_client: Optional[BaseInceptionClient] = None,
ca_bundle: Optional[str] = None,
verify: Union[bool, str] = True,
):
self.client = inception_client or RetryableInceptionClient(inception_host, authentication)
self.client = inception_client or RetryableInceptionClient(
inception_host, authentication, ca_bundle=ca_bundle, verify=verify
)
self.default_username, _ = authentication

def projects(self) -> List[Project]:
Expand Down
48 changes: 46 additions & 2 deletions pycaprio/core/clients/retryable_client.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,39 @@
import io
import os
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

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

Import of 'os' is not used.

Suggested change
import os

Copilot uses AI. Check for mistakes.
import time
from typing import Optional
from typing import Optional, Union
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

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

Import of 'Union' is not used.

Suggested change
from typing import Optional, Union
from typing import Optional

Copilot uses AI. Check for mistakes.

import requests
from requests.adapters import HTTPAdapter
from urllib3.util.ssl_ import create_urllib3_context
from requests_toolbelt import MultipartEncoder

from pycaprio.core.exceptions import InceptionBadResponse
from pycaprio.core.interfaces.client import BaseInceptionClient
from pycaprio.core.interfaces.types import authentication_type


class SSLAdapter(HTTPAdapter):
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

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

The class name SSLAdapter might be confusing since it's actually an HTTPAdapter that handles SSL configuration. Consider renaming to CustomCAAdapter or CABundleAdapter to better reflect its specific purpose of handling custom CA certificates.

Copilot uses AI. Check for mistakes.
"""
Custom HTTPAdapter that allows merging custom CA certificates with the system default certificates.
This enables trusting both system CAs and custom/self-signed certificates.
"""
Comment on lines +17 to +20
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

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

The class docstring says "allows merging custom CA certificates with the system default certificates" but the implementation in init_poolmanager may not actually merge them properly. The comment on line 31 says "Load both system CAs and custom CAs" but only calls load_verify_locations with the custom CA file, which might not preserve the system CAs depending on the SSL context state.

Copilot uses AI. Check for mistakes.

def __init__(self, ca_bundle: Optional[str] = None, **kwargs):
self.ca_bundle = ca_bundle
super().__init__(**kwargs)

def init_poolmanager(self, *args, **kwargs):
"""Initialize pool manager with custom SSL context that includes custom CA certificates."""
if self.ca_bundle:
# Create a default SSL context
ctx = create_urllib3_context()
# Load both system CAs and custom CAs
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

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

The SSL context created by create_urllib3_context() already loads system default CA certificates. However, calling ctx.load_verify_locations(cafile=self.ca_bundle) may replace the default certificates rather than adding to them, depending on the SSL implementation. To ensure both system CAs and custom CAs are trusted, you should also explicitly load the default CA bundle using ctx.load_default_certs() or load both the system CA bundle and the custom CA bundle.

Suggested change
# Load both system CAs and custom CAs
# Ensure system default CAs are loaded, then add custom CAs
ctx.load_default_certs()

Copilot uses AI. Check for mistakes.
ctx.load_verify_locations(cafile=self.ca_bundle)
kwargs["ssl_context"] = ctx
return super().init_poolmanager(*args, **kwargs)


class RetryableInceptionClient(BaseInceptionClient):
"""
HTTP client which implements retrying with exponential backoff.
Expand All @@ -18,13 +42,33 @@ class RetryableInceptionClient(BaseInceptionClient):

RETRY_STATUSES = (408, 502, 503, 504)

def __init__(self, inception_host: str, authentication: authentication_type, max_retries=3):
def __init__(
self,
inception_host: str,
authentication: authentication_type,
max_retries: int = 3,
ca_bundle: Optional[str] = None,
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

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

Missing input validation for the ca_bundle parameter. If a non-existent file path is provided, the error will only surface when making HTTPS requests, making debugging harder. Consider validating that the file exists and is readable during initialization.

Copilot uses AI. Check for mistakes.
verify: bool = True,
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

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

Type hint mismatch: verify is declared as Union[bool, str] in pycaprio.py and http_adapter.py, but as bool in RetryableInceptionClient. The implementation only handles boolean values. If string values (typically CA bundle paths) are intended to be supported, the implementation needs to handle them. Otherwise, the type hints should be consistently bool across all three files.

Copilot uses AI. Check for mistakes.
):
super().__init__(inception_host, authentication)
self.session = requests.Session()
self.session.auth = authentication
assert 0 < max_retries, "max_retries must be greater than 0"
self.max_retries = max_retries

if verify is True:
# If verify is True, use the SSLAdapter
self.session.verify = True
else:
# If verify is False, disable verification
self.session.verify = False
Comment on lines +59 to +64
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

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

The logic for setting self.session.verify is redundant. Lines 59-64 explicitly set self.session.verify based on the verify parameter, but this is the default behavior for requests.Session(). The code can be simplified by just setting self.session.verify = verify unless there's a specific reason for the if/else structure.

Suggested change
if verify is True:
# If verify is True, use the SSLAdapter
self.session.verify = True
else:
# If verify is False, disable verification
self.session.verify = False
self.session.verify = verify

Copilot uses AI. Check for mistakes.
Comment on lines +62 to +64
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

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

When verify=False is used to disable SSL verification, urllib3 will emit warnings. Consider adding a warning suppression or at least documenting this behavior. Additionally, disabling SSL verification without proper warnings could lead to security issues in production environments.

Copilot uses AI. Check for mistakes.

# Mount custom SSL adapter if we have a custom CA bundle
if ca_bundle and verify is not False:
Comment on lines +66 to +67
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

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

The condition verify is not False uses identity comparison instead of equality comparison. This will fail to detect False-like values (e.g., 0, empty strings). Since verify can be Union[bool, str], use verify != False or better yet, verify is True since you only want to mount the adapter when verification is explicitly enabled.

Suggested change
# Mount custom SSL adapter if we have a custom CA bundle
if ca_bundle and verify is not False:
# Mount custom SSL adapter if we have a custom CA bundle and verification is explicitly enabled
if ca_bundle and verify is True:

Copilot uses AI. Check for mistakes.
adapter = SSLAdapter(ca_bundle=ca_bundle)
self.session.mount("https://", adapter)
self.session.mount("http://", adapter)

Comment on lines +70 to +71
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

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

The SSLAdapter is mounted on both "https://" and "http://" schemes. However, SSL/TLS is only used for HTTPS connections. Mounting the SSL adapter on HTTP is unnecessary and could be misleading since HTTP connections don't use SSL/TLS at all.

Suggested change
self.session.mount("http://", adapter)

Copilot uses AI. Check for mistakes.
def get(self, url: str, params: Optional[dict] = None) -> requests.Response:
return self.request("get", url, params=params)

Expand Down
13 changes: 10 additions & 3 deletions pycaprio/core/pycaprio.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import os
from typing import Optional
from typing import Optional, Union

from pycaprio.core.adapters.http_adapter import HttpInceptionAdapter
from pycaprio.core.exceptions import ConfigurationNotProvided
Expand All @@ -17,13 +17,20 @@ def __init__(
inception_host: Optional[str] = None,
authentication: Optional[authentication_type] = None,
local_projects_dir: Optional[str] = None,
ca_bundle: Optional[str] = None,
verify: Union[bool, str] = True,
):
"""
Initializes Pycaprio in either remote or local mode.

:param inception_host: Hostname of the INCEpTION instance.
:param authentication: Tuple of username and password for INCEpTION instance.
:param local_projects_dir: Directory containing exported INCEpTION projects in ZIP format.
:param ca_bundle: Path to a custom CA certificate file to trust.
This is the recommended way to support self-signed certificates.
:param verify: Controls SSL verification behavior:
- True (default): Verify SSL certificates using system CAs + ca_bundle if provided
- False: Disable SSL verification (not recommended for production)
Comment on lines +31 to +33
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

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

The docstring describes verify as accepting string values ("True (default): Verify SSL certificates..."), but the implementation in RetryableInceptionClient only handles boolean values. The documentation should clarify whether string values are supported, and if so, what they represent (typically a path to a CA bundle file).

Copilot uses AI. Check for mistakes.
"""
inception_host = inception_host or os.getenv("INCEPTION_HOST")
if inception_host:
Expand All @@ -33,8 +40,8 @@ def __init__(
"Authentication was not provided. "
"You can set it via environment variables as 'INCEPTION_USERNAME' and 'INCEPTION_PASSWORD'"
)
self.api = HttpInceptionAdapter(inception_host, authentication)

self.api = HttpInceptionAdapter(inception_host, authentication, ca_bundle=ca_bundle, verify=verify)
elif local_projects_dir:
self.api = LocalInceptionAdapter(local_projects_dir)
else:
Expand Down