Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

_make_request error output #28

Merged
merged 2 commits into from
Aug 2, 2024
Merged
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
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)}")
94 changes: 69 additions & 25 deletions src/claudesync/providers/claude_ai_curl.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,33 @@
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",
f"Cookie: sessionKey={self.session_key};",
f"Cookie: sessionKey={self.session_key}",
"-H",
"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,61 @@ 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:
http_status_code = result.stdout[-3:]
response_body = result.stdout[:-3].strip()
self.logger.debug(f"Got HTTP {http_status_code}")

if http_status_code.startswith("2"):
if http_status_code == "204":
# HTTP 204 No Content
return None
if not response_body:
self.logger.warn(f"Got HTTP {http_status_code} but empty response")
return None

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: {response_body}. Reason: {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)
22 changes: 15 additions & 7 deletions src/claudesync/syncmanager.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import os
import time
import logging
from datetime import datetime, timezone

import click
from tqdm import tqdm

from claudesync.utils import compute_md5_hash

logger = logging.getLogger(__name__)


class SyncManager:
def __init__(self, provider, config):
Expand Down Expand Up @@ -106,7 +108,7 @@ def update_existing_file(
"""
remote_checksum = compute_md5_hash(remote_file["content"])
if local_checksum != remote_checksum:
click.echo(f"Updating {local_file} on remote...")
logger.info(f"Updating {local_file} on remote...")
with tqdm(total=2, desc=f"Updating {local_file}", leave=False) as pbar:
self.provider.delete_file(
self.active_organization_id,
Expand Down Expand Up @@ -139,7 +141,7 @@ def upload_new_file(self, local_file, synced_files):
local_file (str): Name of the local file to be uploaded.
synced_files (set): Set of file names that have been synchronized.
"""
click.echo(f"Uploading new file {local_file} to remote...")
logger.info(f"Uploading new file {local_file} to remote...")
with open(
os.path.join(self.local_path, local_file), "r", encoding="utf-8"
) as file:
Expand Down Expand Up @@ -174,7 +176,9 @@ def update_local_timestamps(self, remote_files, synced_files):
remote_file["created_at"].replace("Z", "+00:00")
).timestamp()
os.utime(local_file_path, (remote_timestamp, remote_timestamp))
click.echo(f"Updated timestamp on local file {local_file_path}")
logger.info(
f"Updated timestamp on local file {local_file_path}"
)
pbar.update(1)

def sync_remote_to_local(self, remote_file, remote_files_to_delete, synced_files):
Expand Down Expand Up @@ -221,7 +225,9 @@ def update_existing_local_file(
remote_file["created_at"].replace("Z", "+00:00")
)
if remote_mtime > local_mtime:
click.echo(f"Updating local file {remote_file['file_name']} from remote...")
logger.info(
f"Updating local file {remote_file['file_name']} from remote..."
)
with tqdm(
total=1, desc=f"Updating {remote_file['file_name']}", leave=False
) as pbar:
Expand All @@ -246,7 +252,9 @@ def create_new_local_file(
remote_files_to_delete (set): Set of remote file names to be considered for deletion.
synced_files (set): Set of file names that have been synchronized.
"""
click.echo(f"Creating new local file {remote_file['file_name']} from remote...")
logger.info(
f"Creating new local file {remote_file['file_name']} from remote..."
)
with tqdm(
total=1, desc=f"Creating {remote_file['file_name']}", leave=False
) as pbar:
Expand All @@ -267,7 +275,7 @@ def delete_remote_files(self, file_to_delete, remote_files):
file_to_delete (str): Name of the remote file to be deleted.
remote_files (list): List of dictionaries representing remote files.
"""
click.echo(f"Deleting {file_to_delete} from remote...")
logger.info(f"Deleting {file_to_delete} from remote...")
remote_file = next(
rf for rf in remote_files if rf["file_name"] == file_to_delete
)
Expand Down
Loading
Loading