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

Fix ClaudeSync 403 Error on Python 3.12.5 with Claude.ai #49

Merged
merged 5 commits into from
Aug 19, 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
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
Loading