From 8b053edf25ba870f5be757141348cf9286fef15f Mon Sep 17 00:00:00 2001 From: Jahziah Wagner Date: Mon, 22 Jul 2024 11:13:13 +0200 Subject: [PATCH] Configurable headers --- pyproject.toml | 2 +- src/claudesync/config_manager.py | 33 ++++++ src/claudesync/providers/claude_ai.py | 141 +++++++++++++------------- 3 files changed, 102 insertions(+), 74 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 98b6f45..6ad3c6c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 = "jahziah.wagner+pypi@gmail.com"}, ] diff --git a/src/claudesync/config_manager.py b/src/claudesync/config_manager.py index cbed078..99faaee 100644 --- a/src/claudesync/config_manager.py +++ b/src/claudesync/config_manager.py @@ -1,3 +1,5 @@ +# src/claudesync/config_manager.py + import json from pathlib import Path @@ -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) @@ -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): @@ -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", {}) diff --git a/src/claudesync/providers/claude_ai.py b/src/claudesync/providers/claude_ai.py index 7581173..d7b5678 100644 --- a/src/claudesync/providers/claude_ai.py +++ b/src/claudesync/providers/claude_ai.py @@ -1,3 +1,5 @@ +# src/claudesync/providers/claude_ai.py + import json import logging @@ -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. @@ -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.