Skip to content

Commit

Permalink
Fix ClaudeSync 403 Error on Python 3.12.5 with Claude.ai (#49)
Browse files Browse the repository at this point in the history
This update allows ClaudeSync to run with the provider Claude.ai on
Python 3.12.5 by switching the HTTP client from requests to urllib.
  • Loading branch information
jahwag authored Aug 19, 2024
1 parent 7193080 commit 5dee612
Show file tree
Hide file tree
Showing 8 changed files with 292 additions and 239 deletions.
82 changes: 41 additions & 41 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
@@ -1,41 +1,41 @@
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python

name: Python package

on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]

jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 1
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.10", "3.11"]

steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
pip install -e .
python -m pip install --upgrade pip
python -m pip install flake8 pytest
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest
run: |
pytest
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python

name: Python package

on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]

jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 1
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12"]

steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
pip install -e .
python -m pip install --upgrade pip
python -m pip install flake8 pytest
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest
run: |
pytest
3 changes: 1 addition & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "claudesync"
version = "0.5.0"
version = "0.5.1"
authors = [
{name = "Jahziah Wagner", email = "[email protected]"},
]
Expand All @@ -15,7 +15,6 @@ classifiers = [
]
dependencies = [
"Click>=8.1.7",
"requests>=2.32.3",
"pathspec>=0.12.1",
"crontab>=1.0.1",
"click_completion>=0.5.2",
Expand Down
1 change: 0 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
Click>=8.1.7
requests>=2.32.3
pathspec>=0.12.1
crontab>=1.0.1
click_completion>=0.5.2
Expand Down
12 changes: 8 additions & 4 deletions src/claudesync/providers/base_claude_ai.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def _get_session_key_expiry():
) + datetime.timedelta(days=30)
formatted_expires = default_expires.strftime(date_format).strip()
expires = click.prompt(
"Please enter the expires time for the sessionKey",
"Please enter the expires time for the sessionKey (optional)",
default=formatted_expires,
type=str,
).strip()
Expand Down Expand Up @@ -71,7 +71,9 @@ def login(self):
)

while True:
session_key = click.prompt("Please enter your sessionKey", type=str)
session_key = click.prompt(
"Please enter your sessionKey", type=str, hide_input=True
)
if not session_key.startswith("sk-ant"):
click.echo(
"Invalid sessionKey format. Please make sure it starts with 'sk-ant'."
Expand Down Expand Up @@ -105,8 +107,10 @@ def get_organizations(self):
return [
{"id": org["uuid"], "name": org["name"]}
for org in response
if ({"chat", "claude_pro"}.issubset(set(org.get("capabilities", []))) or
{"chat", "raven"}.issubset(set(org.get("capabilities", []))))
if (
{"chat", "claude_pro"}.issubset(set(org.get("capabilities", [])))
or {"chat", "raven"}.issubset(set(org.get("capabilities", [])))
)
]

def get_projects(self, organization_id, include_archived=False):
Expand Down
99 changes: 57 additions & 42 deletions src/claudesync/providers/claude_ai.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import urllib.request
import urllib.error
import urllib.parse
import json
import requests
import gzip
from .base_claude_ai import BaseClaudeAIProvider
from ..exceptions import ProviderError

Expand All @@ -8,24 +11,13 @@ class ClaudeAIProvider(BaseClaudeAIProvider):
def _make_request(self, method, endpoint, data=None):
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",
"Origin": "https://claude.ai",
"Referer": "https://claude.ai/projects",
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate, zstd",
"Accept-Language": "en-US,en;q=0.5",
"anthropic-client-sha": "unknown",
"anthropic-client-version": "unknown",
"Connection": "keep-alive",
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "same-origin",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:129.0) Gecko/20100101 Firefox/129.0",
"Content-Type": "application/json",
"Accept-Encoding": "gzip",
}

cookies = {
"sessionKey": self.session_key,
"CH-prefers-color-scheme": "dark",
"anthropic-consent-preferences": '{"analytics":true,"marketing":true}',
}

try:
Expand All @@ -35,40 +27,63 @@ def _make_request(self, method, endpoint, data=None):
if data:
self.logger.debug(f"Request data: {data}")

response = requests.request(
method, url, headers=headers, cookies=cookies, json=data
)
# Prepare the request
req = urllib.request.Request(url, method=method)
for key, value in headers.items():
req.add_header(key, value)

# Add cookies
cookie_string = "; ".join([f"{k}={v}" for k, v in cookies.items()])
req.add_header("Cookie", cookie_string)

# Add data if present
if data:
json_data = json.dumps(data).encode("utf-8")
req.data = json_data

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]}...")
# Make the request
with urllib.request.urlopen(req) as response:
self.logger.debug(f"Response status code: {response.status}")
self.logger.debug(f"Response headers: {response.headers}")

if response.status_code == 403:
error_msg = (
"Received a 403 Forbidden error. Your session key might be invalid. "
"Please try logging out and logging in again. If the issue persists, "
"you can try using the claude.ai-curl provider as a workaround:\n"
"claudesync api logout\n"
"claudesync api login claude.ai-curl"
)
self.logger.error(error_msg)
raise ProviderError(error_msg)
# Handle gzip encoding
if response.headers.get("Content-Encoding") == "gzip":
content = gzip.decompress(response.read())
else:
content = response.read()

response.raise_for_status()
content_str = content.decode("utf-8")
self.logger.debug(f"Response content: {content_str[:1000]}...")

if not response.content:
return None
if not content:
return None

return response.json()
return json.loads(content_str)

except requests.RequestException as e:
self.logger.error(f"Request failed: {str(e)}")
if hasattr(e, "response") and e.response is not None:
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}")
except urllib.error.HTTPError as e:
self.handle_http_error(e)
except urllib.error.URLError as e:
self.logger.error(f"URL Error: {str(e)}")
raise ProviderError(f"API request failed: {str(e)}")
except json.JSONDecodeError as json_err:
self.logger.error(f"Failed to parse JSON response: {str(json_err)}")
self.logger.error(f"Response content: {response.text}")
self.logger.error(f"Response content: {content_str}")
raise ProviderError(f"Invalid JSON response from API: {str(json_err)}")

def handle_http_error(self, e):
self.logger.error(f"Request failed: {str(e)}")
self.logger.error(f"Response status code: {e.code}")
self.logger.error(f"Response headers: {e.headers}")
content = e.read().decode("utf-8")
self.logger.error(f"Response content: {content}")
if e.code == 403:
error_msg = (
"Received a 403 Forbidden error. Your session key might be invalid. "
"Please try logging out and logging in again. If the issue persists, "
"you can try using the claude.ai-curl provider as a workaround:\n"
"claudesync api logout\n"
"claudesync api login claude.ai-curl"
)
self.logger.error(error_msg)
raise ProviderError(error_msg)
raise ProviderError(f"API request failed: {str(e)}")
7 changes: 5 additions & 2 deletions src/claudesync/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,10 +194,13 @@ def get_local_files(local_path):

# Filter out directories before traversing
dirs[:] = [
d for d in dirs
d
for d in dirs
if d not in exclude_dirs
and not (gitignore and gitignore.match_file(os.path.join(rel_root, d)))
and not (claudeignore and claudeignore.match_file(os.path.join(rel_root, d)))
and not (
claudeignore and claudeignore.match_file(os.path.join(rel_root, d))
)
]

for filename in filenames:
Expand Down
Loading

0 comments on commit 5dee612

Please sign in to comment.