Skip to content

Commit

Permalink
_make_request error output
Browse files Browse the repository at this point in the history
  • Loading branch information
jahwag committed Aug 2, 2024
1 parent 1b5dedc commit 2eeec68
Show file tree
Hide file tree
Showing 7 changed files with 174 additions and 75 deletions.
42 changes: 24 additions & 18 deletions .github/ISSUE_TEMPLATE/feature_request.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,27 @@ assignees: ''

---

Is your feature request related to a problem? Please describe.
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
Describe the solution you'd like
A clear and concise description of what you want to happen.
Describe alternatives you've considered
A clear and concise description of any alternative solutions or features you've considered.
Use case
Describe how you would use this feature and how it would benefit your workflow with ClaudeSync.
Additional context
Add any other context, screenshots, or examples about the feature request here.
Environment (if relevant):

OS: [e.g. Windows 10, macOS 11.4, Ubuntu 20.04]
Python version: [e.g. 3.8.10]
ClaudeSync version: [e.g. 0.3.7]

Are you willing to contribute to this feature?
Let us know if you're interested in helping implement this feature or if you're just suggesting an idea.
# Feature Request Template

## 1. Problem Description
Provide a clear and concise description of the problem you're experiencing. For example: "I'm always frustrated when [...]"

## 2. Desired Solution
Outline the solution you would like to see implemented. Be clear and specific about what you want to happen.

## 3. Considered Alternatives
Describe any alternative solutions or features you have considered. Include why these alternatives may or may not work for your needs.

## 4. Use Case
Explain how you would use this feature and how it would benefit your workflow with ClaudeSync.

## 5. Additional Context
Include any other relevant context, screenshots, or examples that can help in understanding the feature request.

## 6. Environment (if relevant)
- **OS:** [e.g. Windows 10, macOS 11.4, Ubuntu 20.04]
- **Python version:** [e.g. 3.8.10]
- **ClaudeSync version:** [e.g. 0.3.7]

## 7. Willingness to Contribute
Let us know if you're interested in helping to implement this feature or if you're just suggesting an idea.
2 changes: 1 addition & 1 deletion .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ on:

jobs:
build:

runs-on: ubuntu-latest
timeout-minutes: 1
strategy:
fail-fast: false
matrix:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "claudesync"
version = "0.4.0"
version = "0.4.1"
authors = [
{name = "Jahziah Wagner", email = "[email protected]"},
]
Expand Down
30 changes: 29 additions & 1 deletion src/claudesync/providers/base_claude_ai.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
import logging
import click
from .base_provider import BaseProvider
from ..config_manager import ConfigManager
from ..exceptions import ProviderError


class BaseClaudeAIProvider(BaseProvider):
BASE_URL = "https://claude.ai/api"

def __init__(self, session_key=None):
self.config = ConfigManager()
self.session_key = session_key
self.logger = logging.getLogger(__name__)
self._configure_logging()

def _configure_logging(self):
log_level = self.config.get("log_level", "INFO")
logging.basicConfig(level=getattr(logging, log_level))
self.logger.setLevel(getattr(logging, log_level))

def login(self):
click.echo("To obtain your session key, please follow these steps:")
Expand All @@ -26,7 +36,25 @@ def login(self):
"5. In the left sidebar, expand 'Cookies' and select 'https://claude.ai'"
)
click.echo("6. Find the cookie named 'sessionKey' and copy its value")
self.session_key = click.prompt("Please enter your sessionKey", type=str)

while True:
session_key = click.prompt("Please enter your sessionKey", type=str)
if not session_key.startswith("sk-ant"):
click.echo(
"Invalid sessionKey format. Please make sure it starts with 'sk-ant'."
)
continue

self.session_key = session_key
try:
organizations = self.get_organizations()
if organizations:
break # Exit the loop if get_organizations is successful
except ProviderError:
click.echo(
"Failed to retrieve organizations. Please enter a valid sessionKey."
)

return self.session_key

def get_organizations(self):
Expand Down
39 changes: 14 additions & 25 deletions src/claudesync/providers/claude_ai.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,12 @@
import json
import logging
import requests
from .base_claude_ai import BaseClaudeAIProvider
from ..config_manager import ConfigManager
from ..exceptions import ProviderError

logger = logging.getLogger(__name__)


class ClaudeAIProvider(BaseClaudeAIProvider):
def __init__(self, session_key=None):
super().__init__(session_key)
self.config = ConfigManager()
self._configure_logging()

def _configure_logging(self):
log_level = self.config.get("log_level", "INFO")
logging.basicConfig(level=getattr(logging, log_level))
logger.setLevel(getattr(logging, log_level))

def _make_request(self, method, endpoint, data=None):
url = f"{self.BASE_URL}{endpoint}"
Expand All @@ -43,19 +32,19 @@ def _make_request(self, method, endpoint, data=None):
}

try:
logger.debug(f"Making {method} request to {url}")
logger.debug(f"Headers: {headers}")
logger.debug(f"Cookies: {cookies}")
self.logger.debug(f"Making {method} request to {url}")
self.logger.debug(f"Headers: {headers}")
self.logger.debug(f"Cookies: {cookies}")
if data:
logger.debug(f"Request data: {data}")
self.logger.debug(f"Request data: {data}")

response = requests.request(
method, url, headers=headers, cookies=cookies, json=data
)

logger.debug(f"Response status code: {response.status_code}")
logger.debug(f"Response headers: {response.headers}")
logger.debug(f"Response content: {response.text[:1000]}...")
self.logger.debug(f"Response status code: {response.status_code}")
self.logger.debug(f"Response headers: {response.headers}")
self.logger.debug(f"Response content: {response.text[:1000]}...")

if response.status_code == 403:
error_msg = (
Expand All @@ -65,7 +54,7 @@ def _make_request(self, method, endpoint, data=None):
"claudesync api logout\n"
"claudesync api login claude.ai-curl"
)
logger.error(error_msg)
self.logger.error(error_msg)
raise ProviderError(error_msg)

response.raise_for_status()
Expand All @@ -76,13 +65,13 @@ def _make_request(self, method, endpoint, data=None):
return response.json()

except requests.RequestException as e:
logger.error(f"Request failed: {str(e)}")
self.logger.error(f"Request failed: {str(e)}")
if hasattr(e, "response") and e.response is not None:
logger.error(f"Response status code: {e.response.status_code}")
logger.error(f"Response headers: {e.response.headers}")
logger.error(f"Response content: {e.response.text}")
self.logger.error(f"Response status code: {e.response.status_code}")
self.logger.error(f"Response headers: {e.response.headers}")
self.logger.error(f"Response content: {e.response.text}")
raise ProviderError(f"API request failed: {str(e)}")
except json.JSONDecodeError as json_err:
logger.error(f"Failed to parse JSON response: {str(json_err)}")
logger.error(f"Response content: {response.text}")
self.logger.error(f"Failed to parse JSON response: {str(json_err)}")
self.logger.error(f"Response content: {response.text}")
raise ProviderError(f"Invalid JSON response from API: {str(json_err)}")
86 changes: 61 additions & 25 deletions src/claudesync/providers/claude_ai_curl.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,23 @@
class ClaudeAICurlProvider(BaseClaudeAIProvider):
def _make_request(self, method, endpoint, data=None):
url = f"{self.BASE_URL}{endpoint}"
headers = [
headers = self._prepare_headers()

command = self._build_curl_command(method, url, headers, data)

try:
result = subprocess.run(
command, capture_output=True, text=True, check=True, encoding="utf-8"
)

return self._process_result(result, headers)
except subprocess.CalledProcessError as e:
self._handle_called_process_error(e, headers)
except UnicodeDecodeError as e:
self._handle_unicode_decode_error(e, headers)

def _prepare_headers(self):
return [
"-H",
"User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0",
"-H",
Expand All @@ -16,13 +32,8 @@ def _make_request(self, method, endpoint, data=None):
"Content-Type: application/json",
]

command = [
"curl",
url,
"--compressed",
"-s",
"-S",
]
def _build_curl_command(self, method, url, headers, data):
command = ["curl", url, "--compressed", "-s", "-S", "-w", "%{http_code}"]
command.extend(headers)

if method != "GET":
Expand All @@ -32,28 +43,53 @@ def _make_request(self, method, endpoint, data=None):
json_data = json.dumps(data)
command.extend(["-d", json_data])

try:
result = subprocess.run(
command, capture_output=True, text=True, check=True, encoding="utf-8"
return command

def _process_result(self, result, headers):
if not result.stdout:
raise ProviderError(
f"Empty response from the server. Request headers: {headers}"
)

if not result.stdout:
return None
http_status_code = result.stdout[-3:]
response_body = result.stdout[:-3].strip()

if http_status_code.startswith("2"):
try:
return json.loads(result.stdout)
return json.loads(response_body)
except json.JSONDecodeError as e:
raise ProviderError(
f"Failed to parse JSON response: {e}. Response content: {result.stdout}"
error_message = (
f"Failed to parse JSON response: {e}. Response content: {response_body}. Request "
f"headers: {headers}"
)

except subprocess.CalledProcessError as e:
error_message = f"cURL command failed with return code {e.returncode}. "
error_message += f"stdout: {e.stdout}, stderr: {e.stderr}"
raise ProviderError(error_message)
except UnicodeDecodeError as e:
error_message = f"Failed to decode cURL output: {e}. "
error_message += (
"This might be due to non-UTF-8 characters in the response."
self.logger.error(error_message)
raise ProviderError(error_message)
else:
error_message = (
f"HTTP request failed with status code {http_status_code}. "
f"Response content: {response_body}. Request headers: {headers}"
)
self.logger.error(error_message)
raise ProviderError(error_message)

def _handle_called_process_error(self, e, headers):
if e.returncode == 1:
error_message = (
f"cURL command failed due to an unsupported protocol or a failed initialization. "
f"Request headers: {headers}"
)
else:
error_message = (
f"cURL command failed with return code {e.returncode}. stdout: {e.stdout}, "
f"stderr: {e.stderr}. Request headers: {headers}"
)
self.logger.error(error_message)
raise ProviderError(error_message)

def _handle_unicode_decode_error(self, e, headers):
error_message = (
f"Failed to decode cURL output: {e}. This might be due to non-UTF-8 characters in the "
f"response. Request headers: {headers}"
)
self.logger.error(error_message)
raise ProviderError(error_message)
48 changes: 44 additions & 4 deletions tests/providers/test_base_claude_ai.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,51 @@ def setUp(self):
self.provider = BaseClaudeAIProvider("test_session_key")

@patch("click.prompt")
def test_login(self, mock_prompt):
mock_prompt.return_value = "new_session_key"
@patch.object(BaseClaudeAIProvider, "get_organizations")
def test_login(self, mock_get_organizations, mock_prompt):
# Test successful login on first attempt
mock_prompt.return_value = "sk-ant-valid_session_key"
mock_get_organizations.return_value = [{"id": "org1", "name": "Org 1"}]

result = self.provider.login()

self.assertEqual(result, "sk-ant-valid_session_key")
self.assertEqual(self.provider.session_key, "sk-ant-valid_session_key")
mock_prompt.assert_called_once()
mock_get_organizations.assert_called_once()

# Reset mocks for next test
mock_prompt.reset_mock()
mock_get_organizations.reset_mock()

# Test invalid session key followed by valid session key
mock_prompt.side_effect = ["invalid_key", "sk-ant-valid_session_key"]
mock_get_organizations.side_effect = [
ProviderError("Invalid session key"),
[{"id": "org1", "name": "Org 1"}],
]

result = self.provider.login()
self.assertEqual(result, "new_session_key")
self.assertEqual(self.provider.session_key, "new_session_key")

self.assertEqual(result, "sk-ant-valid_session_key")
self.assertEqual(self.provider.session_key, "sk-ant-valid_session_key")
self.assertEqual(mock_prompt.call_count, 2)
self.assertEqual(mock_get_organizations.call_count, 2)

# Reset mocks for next test
mock_prompt.reset_mock()
mock_get_organizations.reset_mock()

# Test when get_organizations returns an empty list
mock_prompt.return_value = "sk-ant-valid_session_key"
mock_get_organizations.return_value = []

result = self.provider.login()

self.assertEqual(result, "sk-ant-valid_session_key")
self.assertEqual(self.provider.session_key, "sk-ant-valid_session_key")
mock_prompt.assert_called_once()
mock_get_organizations.assert_called_once()

@patch.object(BaseClaudeAIProvider, "_make_request")
def test_get_organizations(self, mock_make_request):
Expand Down

0 comments on commit 2eeec68

Please sign in to comment.