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

Configurable headers #16

Merged
merged 1 commit into from
Jul 22, 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
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.3.4"
version = "0.3.5"
authors = [
{name = "Jahziah Wagner", email = "[email protected]"},
]
Expand Down
33 changes: 33 additions & 0 deletions src/claudesync/config_manager.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# src/claudesync/config_manager.py

import json
from pathlib import Path

Expand Down Expand Up @@ -40,6 +42,10 @@ def _load_config(self):
"log_level": "INFO",
"upload_delay": 0.5,
"max_file_size": 32 * 1024, # Default 32 KB
"headers": {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0",
"Origin": "https://claude.ai",
},
}
with open(self.config_file, "r") as f:
config = json.load(f)
Expand All @@ -49,6 +55,11 @@ def _load_config(self):
config["upload_delay"] = 0.5
if "max_file_size" not in config:
config["max_file_size"] = 32 * 1024 # Default 32 KB
if "headers" not in config:
config["headers"] = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0",
"Origin": "https://claude.ai",
}
return config

def _save_config(self):
Expand Down Expand Up @@ -87,3 +98,25 @@ def set(self, key, value):
"""
self.config[key] = value
self._save_config()

def update_headers(self, new_headers):
"""
Updates the headers configuration with new values.

Args:
new_headers (dict): A dictionary containing the new header key-value pairs to update or add.

This method updates the existing headers with the new values provided, adds any new headers,
and then saves the updated configuration to the file.
"""
self.config.setdefault("headers", {}).update(new_headers)
self._save_config()

def get_headers(self):
"""
Retrieves the current headers configuration.

Returns:
dict: The current headers configuration.
"""
return self.config.get("headers", {})
141 changes: 68 additions & 73 deletions src/claudesync/providers/claude_ai.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# src/claudesync/providers/claude_ai.py

import json
import logging

Expand Down Expand Up @@ -58,6 +60,72 @@ def _configure_logging(self):
getattr(logging, log_level)
) # Set logger instance to the specified log level

def _make_request(self, method, endpoint, **kwargs):
"""
Sends a request to the specified endpoint using the given HTTP method.

This method constructs a request to the Claude AI API, appending the specified endpoint to the base URL.
It sets up common headers and cookies for the request, including a session key for authentication.
Additional headers can be provided through `kwargs`. The method logs the request and response details
and handles common HTTP errors and JSON parsing errors.

Args:
method (str): The HTTP method to use for the request (e.g., 'GET', 'POST').
endpoint (str): The API endpoint to which the request is sent.
**kwargs: Arbitrary keyword arguments. Can include 'headers' to add or override default headers,
and any other request parameters.

Returns:
dict or None: The parsed JSON response from the API if the response contains content; otherwise, None.

Raises:
ProviderError: If the request fails, if the response status code indicates an error,
or if the response cannot be parsed as JSON.
"""
url = f"{self.BASE_URL}{endpoint}"
headers = self.config.get_headers()
cookies = {"sessionKey": self.session_key}

if "headers" in kwargs:
headers.update(kwargs.pop("headers"))

try:
logger.debug(f"Making {method} request to {url}")
logger.debug(f"Headers: {headers}")
logger.debug(f"Cookies: {cookies}")
if "data" in kwargs:
logger.debug(f"Request data: {kwargs['data']}")

response = requests.request(
method, url, headers=headers, cookies=cookies, **kwargs
)

logger.debug(f"Response status code: {response.status_code}")
logger.debug(f"Response headers: {response.headers}")
logger.debug(
f"Response content: {response.text[:1000]}..."
) # Log first 1000 characters of response

response.raise_for_status()

if not response.content:
return None

try:
return response.json()
except json.JSONDecodeError as json_err:
logger.error(f"Failed to parse JSON response: {str(json_err)}")
logger.error(f"Response content: {response.text}")
raise ProviderError(f"Invalid JSON response from API: {str(json_err)}")

except requests.RequestException as e:
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}")
raise ProviderError(f"API request failed: {str(e)}")

def login(self):
"""
Guides the user through obtaining a session key from the Claude AI website.
Expand Down Expand Up @@ -220,79 +288,6 @@ def delete_file(self, organization_id, project_id, file_uuid):
f"/organizations/{organization_id}/projects/{project_id}/docs/{file_uuid}",
)

def _make_request(self, method, endpoint, **kwargs):
"""
Sends a request to the specified endpoint using the given HTTP method.

This method constructs a request to the Claude AI API, appending the specified endpoint to the base URL.
It sets up common headers and cookies for the request, including a session key for authentication.
Additional headers can be provided through `kwargs`. The method logs the request and response details
and handles common HTTP errors and JSON parsing errors.

Args:
method (str): The HTTP method to use for the request (e.g., 'GET', 'POST').
endpoint (str): The API endpoint to which the request is sent.
**kwargs: Arbitrary keyword arguments. Can include 'headers' to add or override default headers,
and any other request parameters.

Returns:
dict or None: The parsed JSON response from the API if the response contains content; otherwise, None.

Raises:
ProviderError: If the request fails, if the response status code indicates an error,
or if the response cannot be parsed as JSON.
"""
url = f"{self.BASE_URL}{endpoint}"
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0",
"Accept": "*/*",
"Accept-Language": "en-US,en;q=0.5",
"Referer": "https://claude.ai/",
"Origin": "https://claude.ai",
"Connection": "keep-alive",
}
cookies = {"sessionKey": self.session_key}

if "headers" in kwargs:
headers.update(kwargs.pop("headers"))

try:
logger.debug(f"Making {method} request to {url}")
logger.debug(f"Headers: {headers}")
logger.debug(f"Cookies: {cookies}")
if "data" in kwargs:
logger.debug(f"Request data: {kwargs['data']}")

response = requests.request(
method, url, headers=headers, cookies=cookies, **kwargs
)

logger.debug(f"Response status code: {response.status_code}")
logger.debug(f"Response headers: {response.headers}")
logger.debug(
f"Response content: {response.text[:1000]}..."
) # Log first 1000 characters of response

response.raise_for_status()

if not response.content:
return None

try:
return response.json()
except json.JSONDecodeError as json_err:
logger.error(f"Failed to parse JSON response: {str(json_err)}")
logger.error(f"Response content: {response.text}")
raise ProviderError(f"Invalid JSON response from API: {str(json_err)}")

except requests.RequestException as e:
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}")
raise ProviderError(f"API request failed: {str(e)}")

def archive_project(self, organization_id, project_id):
"""
Archives a specified project within an organization.
Expand Down
Loading