├── .github ├── FUNDING.yml └── workflows │ └── python-publish.yml ├── .gitignore ├── LICENSE ├── README.md ├── claude2_api └── __init__.py ├── claude_api ├── __init__.py ├── client.py ├── errors.py └── session.py └── setup.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | custom: ['https://www.buymeacoffee.com/st1vms'] 3 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | release: 13 | types: [published] 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | deploy: 20 | 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - uses: actions/checkout@v3 25 | - name: Set up Python 26 | uses: actions/setup-python@v3 27 | with: 28 | python-version: '3.x' 29 | - name: Install dependencies 30 | run: | 31 | python -m pip install --upgrade pip 32 | pip install build 33 | - name: Build package 34 | run: python -m build 35 | - name: Publish package 36 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 37 | with: 38 | user: __token__ 39 | password: ${{ secrets.PYPI_API_TOKEN }} 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # App specific ignores 2 | prompt.txt 3 | test*.* 4 | 5 | vscode 6 | 7 | ### OSX ### 8 | .DS_Store 9 | .AppleDouble 10 | .LSOverride 11 | 12 | # Icon must end with two \r 13 | Icon 14 | 15 | 16 | # Thumbnails 17 | ._* 18 | 19 | # Files that might appear on external disk 20 | .Spotlight-V100 21 | .Trashes 22 | 23 | # Directories potentially created on remote AFP share 24 | .AppleDB 25 | .AppleDesktop 26 | Network Trash Folder 27 | Temporary Items 28 | .apdisk 29 | 30 | 31 | ### Python ### 32 | # Byte-compiled / optimized / DLL files 33 | __pycache__/ 34 | *.py[cod] 35 | 36 | # C extensions 37 | *.so 38 | 39 | # Distribution / packaging 40 | .Python 41 | env/ 42 | build/ 43 | develop-eggs/ 44 | dist/ 45 | downloads/ 46 | eggs/ 47 | lib/ 48 | lib64/ 49 | parts/ 50 | sdist/ 51 | var/ 52 | *.egg-info/ 53 | .installed.cfg 54 | *.egg 55 | 56 | # PyInstaller 57 | # Usually these files are written by a python script from a template 58 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 59 | *.manifest 60 | *.spec 61 | 62 | # Installer logs 63 | pip-log.txt 64 | pip-delete-this-directory.txt 65 | 66 | # Unit test / coverage reports 67 | htmlcov/ 68 | .tox/ 69 | .coverage 70 | .cache 71 | nosetests.xml 72 | coverage.xml 73 | 74 | # Translations 75 | *.mo 76 | *.pot 77 | 78 | # Sphinx documentation 79 | docs/_build/ 80 | 81 | # PyBuilder 82 | target/ 83 | 84 | 85 | ### Django ### 86 | *.log 87 | *.pot 88 | *.pyc 89 | __pycache__/ 90 | local_settings.py 91 | 92 | .env 93 | db.sqlite3 94 | 95 | # Byte-compiled / optimized / DLL files 96 | __pycache__/ 97 | *.py[cod] 98 | *$py.class 99 | 100 | # VS Code 101 | 102 | .vscode 103 | 104 | # C extensions 105 | *.so 106 | 107 | # Distribution / packaging 108 | .Python 109 | build/ 110 | develop-eggs/ 111 | dist/ 112 | downloads/ 113 | eggs/ 114 | .eggs/ 115 | lib/ 116 | lib64/ 117 | parts/ 118 | sdist/ 119 | var/ 120 | wheels/ 121 | share/python-wheels/ 122 | *.egg-info/ 123 | .installed.cfg 124 | *.egg 125 | MANIFEST 126 | 127 | # PyInstaller 128 | # Usually these files are written by a python script from a template 129 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 130 | *.manifest 131 | *.spec 132 | 133 | # Installer logs 134 | pip-log.txt 135 | pip-delete-this-directory.txt 136 | 137 | # Unit test / coverage reports 138 | htmlcov/ 139 | .tox/ 140 | .nox/ 141 | .coverage 142 | .coverage.* 143 | .cache 144 | nosetests.xml 145 | coverage.xml 146 | *.cover 147 | *.py,cover 148 | .hypothesis/ 149 | .pytest_cache/ 150 | cover/ 151 | 152 | # Translations 153 | *.mo 154 | *.pot 155 | 156 | # Django stuff: 157 | *.log 158 | local_settings.py 159 | db.sqlite3 160 | db.sqlite3-journal 161 | 162 | # Flask stuff: 163 | instance/ 164 | .webassets-cache 165 | 166 | # Scrapy stuff: 167 | .scrapy 168 | 169 | # Sphinx documentation 170 | docs/_build/ 171 | 172 | # PyBuilder 173 | .pybuilder/ 174 | target/ 175 | 176 | # Jupyter Notebook 177 | .ipynb_checkpoints 178 | 179 | # IPython 180 | profile_default/ 181 | ipython_config.py 182 | 183 | # pyenv 184 | # For a library or package, you might want to ignore these files since the code is 185 | # intended to run in multiple environments; otherwise, check them in: 186 | # .python-version 187 | 188 | # pipenv 189 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 190 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 191 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 192 | # install all needed dependencies. 193 | #Pipfile.lock 194 | 195 | # poetry 196 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 197 | # This is especially recommended for binary packages to ensure reproducibility, and is more 198 | # commonly ignored for libraries. 199 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 200 | #poetry.lock 201 | 202 | # pdm 203 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 204 | #pdm.lock 205 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 206 | # in version control. 207 | # https://pdm.fming.dev/#use-with-ide 208 | .pdm.toml 209 | 210 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 211 | __pypackages__/ 212 | 213 | # Celery stuff 214 | celerybeat-schedule 215 | celerybeat.pid 216 | 217 | # SageMath parsed files 218 | *.sage.py 219 | 220 | # Environments 221 | .env 222 | .venv 223 | env/ 224 | venv/ 225 | ENV/ 226 | env.bak/ 227 | venv.bak/ 228 | 229 | # Spyder project settings 230 | .spyderproject 231 | .spyproject 232 | 233 | # Rope project settings 234 | .ropeproject 235 | 236 | # mkdocs documentation 237 | /site 238 | 239 | # mypy 240 | .mypy_cache/ 241 | .dmypy.json 242 | dmypy.json 243 | 244 | # Pyre type checker 245 | .pyre/ 246 | 247 | # pytype static type analyzer 248 | .pytype/ 249 | 250 | # Cython debug symbols 251 | cython_debug/ 252 | 253 | # PyCharm 254 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 255 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 256 | # and can be added to the global gitignore or merged into this file. For a more nuclear 257 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 258 | #.idea/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Stefano Raneri 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # unofficial-claude-api 3 | 4 | ## Table of Contents 5 | 6 | - [Installation](#how-to-install) 7 | - [Requirements](#requirements) 8 | - [Example Usage](#example-usage) 9 | - [Tips](#tips) 10 | - [Retrieving Chat History](#retrieving-chat-history) 11 | - [Faster Loading](#faster-loading-avoiding-selenium) 12 | - [Proxies](#proxies) 13 | - [Changing model version](#changing-claude-model) 14 | - [Changing Organization](#changing-organization) 15 | - [Troubleshooting](#troubleshooting) 16 | - [Donating](#donating) 17 | 18 | ## What is this? 19 | 20 | This unofficial Python API provides access to the conversational capabilities of Anthropic's Claude AI through a simple chat messaging interface. 21 | 22 | While not officially supported by Anthropic, this library can enable interesting conversational applications. 23 | 24 | It allows for: 25 | 26 | - Creating chat sessions with Claude and getting chat IDs. 27 | - Sending messages to Claude containing up to 5 attachment files (txt, pdf, csv, png, jpeg, etc...) 10 MB each, images are also supported! 28 | - Retrieving chat message history, accessing specific chat conversations. 29 | - Deleting old chats when they are no longer needed. 30 | - Sending requests through proxies. 31 | 32 | ### Some of the key things you can do with Claude through this API 33 | 34 | - Ask questions about a wide variety of topics. Claude can chat about current events, pop culture, sports, 35 | and more. 36 | 37 | - Get helpful explanations on complex topics. Ask Claude to explain concepts and ideas in simple terms. 38 | 39 | - Generate summaries from long text or documents. Just give the filepath as an attachment to Claude and get back a concise summary. 40 | 41 | - Receive thoughtful responses to open-ended prompts and ideas. Claude can brainstorm ideas, expand on concepts, and have philosophical discussions. 42 | 43 | - Send images and let Claude analyze them for you. 44 | 45 | ## How to install 46 | 47 | ```shell 48 | pip install unofficial-claude-api 49 | ``` 50 | 51 | ## Uninstallation 52 | 53 | ```shell 54 | pip uninstall unofficial-claude-api 55 | ``` 56 | 57 | ## Requirements 58 | 59 | ### These requirements are needed to auto retrieve a SessionData object using selenium 60 | 61 | - Python >= 3.10 62 | - Firefox installed, and with at least one profile logged into [Claude](https://claude.ai/chats). 63 | - [geckodriver](https://github.com/mozilla/geckodriver/releases) installed inside a folder registered in PATH environment variable. 64 | 65 | ***( scrolling through this README you'll find also a manual alternative )*** 66 | 67 | ## Example Usage 68 | 69 | ```python 70 | from sys import exit as sys_exit 71 | from claude_api.client import ( 72 | ClaudeAPIClient, 73 | SendMessageResponse, 74 | ) 75 | from claude_api.session import SessionData, get_session_data 76 | from claude_api.errors import ClaudeAPIError, MessageRateLimitError, OverloadError 77 | 78 | # Wildcard import will also work the same as above 79 | # from claude_api import * 80 | 81 | # List of attachments filepaths, up to 5, max 10 MB each 82 | FILEPATH_LIST = ["test.txt"] 83 | 84 | # This function will automatically retrieve a SessionData instance using selenium 85 | # It will auto gather cookie session, user agent and organization ID. 86 | # Omitting profile argument will use default Firefox profile 87 | session: SessionData = get_session_data() 88 | 89 | # Initialize a client instance using a session 90 | # Optionally change the requests timeout parameter to best fit your needs...default to 240 seconds. 91 | client = ClaudeAPIClient(session, timeout=240) 92 | 93 | # Create a new chat and cache the chat_id 94 | chat_id = client.create_chat() 95 | if not chat_id: 96 | # This will not throw MessageRateLimitError 97 | # But it still means that account has no more messages left. 98 | print("\nMessage limit hit, cannot create chat...") 99 | sys_exit(1) 100 | 101 | try: 102 | # Used for sending message with or without attachments 103 | # Returns a SendMessageResponse instance 104 | res: SendMessageResponse = client.send_message( 105 | chat_id, "Hello!", attachment_paths=FILEPATH_LIST 106 | ) 107 | # Inspect answer 108 | if res.answer: 109 | print(res.answer) 110 | else: 111 | # Inspect response status code and raw answer bytes 112 | print(f"\nError code {res.status_code}, raw_answer: {res.raw_answer}") 113 | except ClaudeAPIError as e: 114 | # Identify the error 115 | if isinstance(e, MessageRateLimitError): 116 | # The exception will hold these informations about the rate limit: 117 | print(f"\nMessage limit hit, resets at {e.reset_date}") 118 | print(f"\n{e.sleep_sec} seconds left until -> {e.reset_timestamp}") 119 | elif isinstance(e, OverloadError): 120 | print(f"\nOverloaded error: {e}") 121 | else: 122 | print(f"\nGot unknown Claude error: {e}") 123 | finally: 124 | # Perform chat deletion for cleanup 125 | client.delete_chat(chat_id) 126 | 127 | # Get a list of all chats ids 128 | all_chat_ids = client.get_all_chat_ids() 129 | # Delete all chats 130 | for chat in all_chat_ids: 131 | client.delete_chat(chat) 132 | 133 | # Or by using a shortcut utility 134 | client.delete_all_chats() 135 | sys_exit(0) 136 | ``` 137 | 138 | ## Tips 139 | 140 | ### Retrieving chat history 141 | 142 | ```python 143 | # A convenience method to access a specific chat conversation is 144 | chat_data = client.get_chat_data(chat_id) 145 | ``` 146 | 147 | `chat_data` will be the same json dictionary returned by calling 148 | `/api/organizations/{organization_id}/chat_conversations/{chat_id}` 149 | 150 | Here's an example of this json: 151 | 152 | ```json 153 | { 154 | "uuid": "", 155 | "name": "", 156 | "summary": "", 157 | "model": null, 158 | "created_at": "1997-12-25T13:33:33.959409+00:00", 159 | "updated_at": "1997-12-25T13:33:39.487561+00:00", 160 | "chat_messages": [ 161 | { 162 | "uuid": "", 163 | "text": "Who is Bugs Bunny?", 164 | "sender": "human", 165 | "index": 0, 166 | "created_at": "1997-12-25T13:33:39.487561+00:00", 167 | "updated_at": "1997-12-25T13:33:40.959409+00:00", 168 | "edited_at": null, 169 | "chat_feedback": null, 170 | "attachments": [] 171 | }, 172 | { 173 | "uuid": "", 174 | "text": "", 175 | "sender": "assistant", 176 | "index": 1, 177 | "created_at": "1997-12-25T13:33:40.959409+00:00", 178 | "updated_at": "1997-12-25T13:33:42.487561+00:00", 179 | "edited_at": null, 180 | "chat_feedback": null, 181 | "attachments": [] 182 | } 183 | ] 184 | } 185 | ``` 186 | 187 | __________ 188 | 189 | ### Faster loading, avoiding selenium 190 | 191 | If for whatever reason you'd like to avoid auto session gathering using selenium, 192 | you just need to manually create a `SessionData` class for `ClaudeAPIClient` constructor, like so... 193 | 194 | ```python 195 | from claude_api.session import SessionData 196 | 197 | cookie_header_value = "The entire Cookie header value string when you visit https://claude.ai/chats" 198 | user_agent = "User agent to use, required" 199 | 200 | # You can retrieve this string from /api/organizations endpoint 201 | # If omitted or None it will be auto retrieved when instantiating ClaudeAPIClient 202 | organization_id = "" 203 | 204 | session = SessionData(cookie_header_value, user_agent, organization_id) 205 | ``` 206 | 207 | __________ 208 | 209 | ### Proxies 210 | 211 | #### How to set HTTP/S proxies 212 | 213 | If you'd like to set an HTTP proxy for all requests, follow this example: 214 | 215 | ```py 216 | from claude_api.client import HTTPProxy, ClaudeAPIClient 217 | from claude_api.session import SessionData 218 | 219 | # Create HTTPProxy instance 220 | http_proxy = HTTPProxy( 221 | "the.proxy.ip.addr", # Proxy IP 222 | 8080, # Proxy port 223 | "username", # Proxy Username (optional) 224 | "password", # Proxy Password (optional) 225 | use_ssl=False # Set to True if proxy uses HTTPS schema 226 | ) 227 | 228 | session = SessionData(...) 229 | 230 | # Give the proxy instance to ClaudeAPIClient constructor, along with session data. 231 | client = ClaudeAPIClient(session, proxy=http_proxy) 232 | ``` 233 | 234 | #### How to set SOCKS proxies 235 | 236 | If you want to opt for SOCKS proxies instead, the procedure is the same, but you need to import the `SOCKSProxy` class instead, configuring it with the version number. 237 | 238 | ```py 239 | from claude_api.client import SOCKSProxy, ClaudeAPIClient 240 | from claude_api.session import SessionData 241 | 242 | # Create SOCKSProxy instance 243 | socks_proxy = SOCKSProxy( 244 | "the.proxy.ip.addr", # Proxy IP 245 | 8080, # Proxy port 246 | "username", # Proxy Username (optional) 247 | "password", # Proxy Password (optional) 248 | version_num=5 # Either 4 or 5, defaults to 4 249 | ) 250 | 251 | session = SessionData(...) 252 | 253 | # Give the proxy instance to ClaudeAPIClient constructor as usual 254 | client = ClaudeAPIClient(session, proxy=socks_proxy) 255 | ``` 256 | 257 | __________ 258 | 259 | ### Changing Claude model 260 | 261 | In case you'd like to change the model used, or you do have accounts that are unable to migrate to latest model, you can override the `model_name` string parameter of `ClaudeAPIClient` constructor like so: 262 | 263 | ```py 264 | from claude_api.client import ClaudeAPIClient 265 | from claude_api.session import SessionData 266 | 267 | session = SessionData(...) 268 | 269 | # Defaults to None (latest Claude model) 270 | client = ClaudeAPIClient(session, model_name="claude-2.0") 271 | ``` 272 | 273 | You can retrieve the `model_name` strings from the [official API docs](https://docs.anthropic.com/claude/docs/models-overview#model-comparison) 274 | 275 | __________ 276 | 277 | ### Changing Organization 278 | 279 | As reported in issue [#23](https://github.com/st1vms/unofficial-claude-api/issues/23) 280 | if you're encountering 403 errors when using Selenium to auto retrieve a `SessionData` class and your account has multiple organizations, 281 | you may want to override the default organization retrieved. 282 | 283 | By default `get_session_data` retrieves the last organization from the result array found [here](https://claude.ai/api/organizations). 284 | You can override the index to fetch by using parameter `organization_index`: 285 | 286 | ```py 287 | from claude_api.session import get_session_data 288 | 289 | # Defaults to -1 (last entry) 290 | session = get_session_data(organization_index=-1) 291 | ``` 292 | 293 | ## TROUBLESHOOTING 294 | 295 | Some common errors that may arise during the usage of this API: 296 | 297 | - *Error [400]* (Unable to prepare file attachment): 298 | 299 | To fix this error, change the extension of the attachment file to something like .txt, since by default this api will fallback to octet-stream for unknown file extensions, Claude may reject the file data. 300 | 301 | - *Error [403]*: 302 | 303 | \****This bug should be already fixed after version 0.2.2***\* 304 | 305 | This api will sometime return a 403 status_code when calling `send_message`, when this happens it is recommeded to look for these things: 306 | 307 | Check if your IP location is allowed, should be in US/UK, other locations may work sporadically. 308 | 309 | Don't try to send the same prompt/file over and over again, instead wait for some time, and change input. 310 | 311 | ## DISCLAIMER 312 | 313 | This repository provides an unofficial API for automating free accounts on [claude.ai](https://claude.ai/chats). 314 | Please note that this API is not endorsed, supported, or maintained by Anthropic. Use it at your own discretion and risk. Anthropic may make changes to their official product or APIs at any time, which could affect the functionality of this unofficial API. We do not guarantee the accuracy, reliability, or security of the information and data retrieved using this API. By using this repository, you agree that the maintainers are not responsible for any damages, issues, or consequences that may arise from its usage. Always refer to Anthropic's official documentation and terms of use. This project is maintained independently by contributors who are not affiliated with Anthropic. 315 | 316 | ## DONATING 317 | 318 | A huge thank you in advance to anyone who wants to donate :) 319 | 320 | [![Buy Me a Pizza](https://img.buymeacoffee.com/button-api/?text=1%20Pizza%20Margherita&emoji=🍕&slug=st1vms&button_colour=0fa913&font_colour=ffffff&font_family=Bree&outline_colour=ffffff&coffee_colour=FFDD00)](https://www.buymeacoffee.com/st1vms) 321 | -------------------------------------------------------------------------------- /claude2_api/__init__.py: -------------------------------------------------------------------------------- 1 | """Backward compatibility import (Before dev-0.3.1)""" 2 | from claude_api.client import ( 3 | ClaudeAPIClient, 4 | SendMessageResponse, 5 | HTTPProxy, 6 | ) 7 | from claude_api.session import SessionData, get_session_data 8 | from claude_api.errors import ClaudeAPIError, MessageRateLimitError, OverloadError 9 | 10 | 11 | __all__ = [ 12 | "ClaudeAPIClient", 13 | "SendMessageResponse", 14 | "HTTPProxy", 15 | "SessionData", 16 | "get_session_data", 17 | "MessageRateLimitError", 18 | "ClaudeAPIError", 19 | "OverloadError", 20 | ] 21 | -------------------------------------------------------------------------------- /claude_api/__init__.py: -------------------------------------------------------------------------------- 1 | """unofficial-claude-api""" 2 | from .client import ( 3 | ClaudeAPIClient, 4 | SendMessageResponse, 5 | HTTPProxy, 6 | ) 7 | from .session import SessionData, get_session_data 8 | from .errors import ClaudeAPIError, MessageRateLimitError, OverloadError 9 | 10 | 11 | __all__ = [ 12 | "ClaudeAPIClient", 13 | "SendMessageResponse", 14 | "HTTPProxy", 15 | "SessionData", 16 | "get_session_data", 17 | "MessageRateLimitError", 18 | "ClaudeAPIError", 19 | "OverloadError", 20 | ] 21 | -------------------------------------------------------------------------------- /claude_api/client.py: -------------------------------------------------------------------------------- 1 | """Client module""" 2 | 3 | from os import path as ospath 4 | from re import sub, search 5 | from typing import TypeVar, Annotated, Optional 6 | from dataclasses import dataclass 7 | from ipaddress import IPv4Address 8 | from json import dumps, loads 9 | from uuid import uuid4 10 | from mimetypes import guess_type 11 | from zlib import decompress as zlib_decompress 12 | from zlib import MAX_WBITS 13 | 14 | # from brotli import decompress as br_decompress 15 | from tzlocal import get_localzone 16 | from requests import post as requests_post 17 | from curl_cffi.requests import get as http_get 18 | from curl_cffi.requests import post as http_post 19 | from curl_cffi.requests import delete as http_delete 20 | from .session import SessionData 21 | from .errors import ClaudeAPIError, MessageRateLimitError, OverloadError 22 | 23 | 24 | @dataclass(frozen=True) 25 | class SendMessageResponse: 26 | """ 27 | Response class returned from `send_message` 28 | """ 29 | 30 | answer: str 31 | """ 32 | The response string, if None check the `status_code` and `raw_answer` fields for errors 33 | """ 34 | status_code: int 35 | """ 36 | Response status code integer 37 | """ 38 | raw_answer: bytes 39 | """ 40 | Raw response bytes returned from send_message POST request, useful for error inspections 41 | """ 42 | 43 | 44 | @dataclass 45 | class ClaudeProxy: 46 | """Base class for Claude proxies""" 47 | 48 | proxy_ip: str = None 49 | proxy_port: int = None 50 | proxy_username: Optional[str] = None 51 | proxy_password: Optional[str] = None 52 | 53 | def __post_init__(self): 54 | if self.proxy_ip is None or self.proxy_port is None: 55 | raise ValueError("Both proxy_ip and proxy_port must be set") 56 | 57 | try: 58 | port = int(self.proxy_port) 59 | except ValueError as e: 60 | raise ValueError("proxy_port must be an integer") from e 61 | 62 | if not 0 <= port <= 65535: 63 | raise ValueError(f"Invalid proxy port number: {port}") 64 | 65 | self.proxy_port = port 66 | 67 | IPv4Address(self.proxy_ip) 68 | 69 | 70 | ClaudeProxyT = TypeVar("ClaudeProxyT", bound=Annotated[dataclass, ClaudeProxy]) 71 | 72 | 73 | @dataclass 74 | class HTTPProxy(ClaudeProxy): 75 | """ 76 | Dataclass holding http/s proxy informations: 77 | 78 | `ip` -> Proxy IP 79 | 80 | `port` -> Proxy port 81 | 82 | `use_ssl` -> Boolean flag to indicate if this proxy uses https schema 83 | """ 84 | 85 | use_ssl: bool = False 86 | 87 | 88 | @dataclass 89 | class SOCKSProxy(ClaudeProxy): 90 | """ 91 | Dataclass holding SOCKS proxy informations: 92 | 93 | `ip` -> Proxy IP 94 | 95 | `port` -> Proxy port 96 | 97 | `version_num` -> integer flag indicating which SOCKS proxy version to use, 98 | defaults to 4. 99 | """ 100 | 101 | version_num: int = 4 102 | 103 | def __post_init__(self): 104 | super().__post_init__() 105 | if self.version_num not in {4, 5}: 106 | raise ValueError(f"Invalid SOCKS version number: {self.version_num}") 107 | 108 | 109 | class ClaudeAPIClient: 110 | """Base client class to interact with claude 111 | 112 | Requires: 113 | 114 | - `session`: SessionData class holding session cookie and UserAgent. 115 | - `proxy` (Optional): HTTPProxy class holding the proxy IP:port configuration. 116 | - `timeout`(Optional): Timeout in seconds to wait for each request to complete. 117 | Defaults to 240 seconds. 118 | """ 119 | 120 | __BASE_URL = "https://claude.ai" 121 | 122 | def __init__( 123 | self, 124 | session: SessionData, 125 | model_name: str = None, 126 | proxy: ClaudeProxyT = None, 127 | timeout: float = 240, 128 | ) -> None: 129 | """ 130 | Constructs a `ClaudeAPIClient` instance using provided `SessionData`, 131 | automatically retrieving organization_id and local timezone. 132 | 133 | `proxy` argument is an optional `HTTPProxy` instance, 134 | holding proxy informations ( ip, port ) 135 | 136 | Raises `ValueError` in case of failure 137 | 138 | """ 139 | 140 | self.model_name: str = model_name 141 | self.timeout: float = timeout 142 | self.proxy: ClaudeProxyT = proxy 143 | self.__session: SessionData = session 144 | if ( 145 | not self.__session 146 | or not self.__session.cookie 147 | or not self.__session.user_agent 148 | ): 149 | raise ValueError("Invalid SessionData argument!") 150 | 151 | if self.__session.organization_id is None: 152 | print("\nRetrieving organization ID...") 153 | self.__session.organization_id = self.__get_organization_id() 154 | 155 | # Retrieve timezone string 156 | self.timezone: str = get_localzone().key 157 | 158 | def __get_proxy(self) -> dict[str, str] | None: 159 | if self.proxy is None or not issubclass(self.proxy.__class__, ClaudeProxy): 160 | return None 161 | 162 | ip, port = self.proxy.proxy_ip, self.proxy.proxy_port 163 | auth = "" 164 | if self.proxy.proxy_username and self.proxy.proxy_password: 165 | auth = f"{self.proxy.proxy_username}:{self.proxy.proxy_password}@" 166 | 167 | if isinstance(self.proxy, HTTPProxy): 168 | scheme = "https" if self.proxy.use_ssl else "http" 169 | proxy_url = f"{scheme}://{auth}{ip}:{port}" 170 | return { 171 | "http": proxy_url, 172 | "https": proxy_url, 173 | } 174 | if isinstance(self.proxy, SOCKSProxy): 175 | proxy_url = f"socks{self.proxy.version_num}://{auth}{ip}:{port}" 176 | return { 177 | "http": proxy_url, 178 | "https": proxy_url, 179 | } 180 | 181 | return None 182 | 183 | def __get_organization_id(self) -> str: 184 | url = f"{self.__BASE_URL}/api/organizations" 185 | 186 | headers = { 187 | "Accept-Encoding": "gzip, deflate, br", 188 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", 189 | "Accept-Language": "en-US,en;q=0.5", 190 | "Connection": "keep-alive", 191 | "Cookie": self.__session.cookie, 192 | "Host": "claude.ai", 193 | "DNT": "1", 194 | "Sec-Fetch-Dest": "document", 195 | "Sec-Fetch-Mode": "navigate", 196 | "Sec-Fetch-Site": "none", 197 | "Sec-Fetch-User": "?1", 198 | "Upgrade-Insecure-Requests": "1", 199 | "User-Agent": self.__session.user_agent, 200 | } 201 | 202 | response = http_get( 203 | url, 204 | headers=headers, 205 | proxies=self.__get_proxy(), 206 | timeout=self.timeout, 207 | impersonate="chrome110", 208 | ) 209 | if response.status_code == 200 and response.content: 210 | res = response.json() 211 | if res and "uuid" in res[0]: 212 | return res[0]["uuid"] 213 | raise RuntimeError(f"Cannot retrieve Organization ID!\n{response.text}") 214 | 215 | def __prepare_text_file_attachment(self, file_path: str) -> dict: 216 | file_name = ospath.basename(file_path) 217 | file_size = ospath.getsize(file_path) 218 | 219 | with open(file_path, "r", encoding="utf-8", errors="ignore") as file: 220 | file_content = file.read() 221 | 222 | return { 223 | "extracted_content": file_content, 224 | "file_name": file_name, 225 | "file_size": f"{file_size}", 226 | "file_type": "text/plain", 227 | } 228 | 229 | def __get_content_type(self, fpath: str): 230 | extension = ospath.splitext(fpath)[1].lower() 231 | mime_type, _ = guess_type(f"file.{extension}") 232 | return mime_type or "application/octet-stream" 233 | 234 | def __prepare_file_attachment(self, fpath: str, chat_id: str) -> dict | None: 235 | content_type = self.__get_content_type(fpath) 236 | if content_type == "text/plain": 237 | return self.__prepare_text_file_attachment(fpath) 238 | 239 | url = f"{self.__BASE_URL}/api/{self.__session.organization_id}/upload" 240 | 241 | headers = { 242 | "Host": "claude.ai", 243 | "User-Agent": self.__session.user_agent, 244 | "Accept": "*/*", 245 | "Accept-Language": "en-US,en;q=0.5", 246 | "Accept-Encoding": "gzip, deflate, br", 247 | "Referer": f"{self.__BASE_URL}/chat/{chat_id}", 248 | "Origin": self.__BASE_URL, 249 | "DNT": "1", 250 | "Sec-Fetch-Dest": "empty", 251 | "Sec-Fetch-Mode": "cors", 252 | "Sec-Fetch-Site": "same-origin", 253 | "Connection": "keep-alive", 254 | "Cookie": self.__session.cookie, 255 | "TE": "trailers", 256 | } 257 | 258 | with open(fpath, "rb") as fp: 259 | files = { 260 | "file": (ospath.basename(fpath), fp, content_type), 261 | "orgUuid": (None, self.__session.organization_id), 262 | } 263 | 264 | response = requests_post( 265 | url, 266 | headers=headers, 267 | files=files, 268 | timeout=self.timeout, 269 | proxies=self.__get_proxy(), 270 | ) 271 | if response.status_code == 200: 272 | res = response.json() 273 | if "file_uuid" in res: 274 | return res["file_uuid"] 275 | print( 276 | f"\n[{response.status_code}] Unable to prepare file attachment -> {fpath}\n" 277 | f"\nReason: {response.text}\n\n" 278 | ) 279 | return None 280 | 281 | def __check_file_attachments_paths(self, path_list: list[str]): 282 | __filesize_limit = 10485760 # 10 MB 283 | if not path_list: 284 | return 285 | 286 | if len(path_list) > 5: # No more than 5 attachments 287 | raise ValueError("Cannot attach more than 5 files!") 288 | 289 | for path in path_list: 290 | # Check if file exists 291 | if not ospath.exists(path) or not ospath.isfile(path): 292 | raise ValueError(f"Attachment file does not exists -> {path}") 293 | 294 | # Check file size 295 | _size = ospath.getsize(path) 296 | if _size > __filesize_limit: 297 | raise ValueError( 298 | f"Attachment file exceed file size limit by {_size-__filesize_limit}" 299 | "out of 10MB -> {path}" 300 | ) 301 | 302 | def create_chat(self) -> str | None: 303 | """ 304 | Create new chat and return chat UUID string if successfull 305 | """ 306 | url = ( 307 | f"{self.__BASE_URL}/api/organizations/" 308 | f"{self.__session.organization_id}/chat_conversations" 309 | ) 310 | 311 | new_uuid = str(uuid4()) 312 | 313 | payload = dumps( 314 | {"name": "", "uuid": new_uuid}, indent=None, separators=(",", ":") 315 | ) 316 | headers = { 317 | "Host": "claude.ai", 318 | "User-Agent": self.__session.user_agent, 319 | "Accept": "*/*", 320 | "Accept-Language": "en-US,en;q=0.5", 321 | "Accept-Encoding": "gzip, deflate, br", 322 | "Content-Type": "application/json", 323 | "Content-Length": f"{len(payload)}", 324 | "Referer": f"{self.__BASE_URL}/chats", 325 | "Origin": self.__BASE_URL, 326 | "DNT": "1", 327 | "Sec-Fetch-Dest": "empty", 328 | "Sec-Fetch-Mode": "cors", 329 | "Sec-Fetch-Site": "same-origin", 330 | "Connection": "keep-alive", 331 | "Cookie": self.__session.cookie, 332 | "TE": "trailers", 333 | } 334 | 335 | response = http_post( 336 | url, 337 | headers=headers, 338 | data=payload, 339 | proxies=self.__get_proxy(), 340 | timeout=self.timeout, 341 | impersonate="chrome110", 342 | ) 343 | if response and response.status_code == 201: 344 | j = response.json() 345 | if j and "uuid" in j: 346 | return j["uuid"] 347 | return None 348 | 349 | def delete_chat(self, chat_id: str) -> bool: 350 | """ 351 | Delete chat by its UUID string, returns True if successfull, False otherwise 352 | """ 353 | url = ( 354 | f"https://claude.ai/api/organizations/" 355 | f"{self.__session.organization_id}/chat_conversations/{chat_id}" 356 | ) 357 | 358 | payload = f'"{chat_id}"' 359 | headers = { 360 | "Host": "claude.ai", 361 | "User-Agent": self.__session.user_agent, 362 | "Accept": "*/*", 363 | "Accept-Language": "en-US,en;q=0.5", 364 | "Accept-Encoding": "gzip, deflate, br", 365 | "Content-Type": "application/json", 366 | "Content-Length": f"{len(payload)}", 367 | "Referer": f"{self.__BASE_URL}/chat/{chat_id}", 368 | "Origin": self.__BASE_URL, 369 | "DNT": "1", 370 | "Sec-Fetch-Dest": "empty", 371 | "Sec-Fetch-Mode": "cors", 372 | "Sec-Fetch-Site": "same-origin", 373 | "Connection": "keep-alive", 374 | "Cookie": self.__session.cookie, 375 | "TE": "trailers", 376 | } 377 | 378 | response = http_delete( 379 | url, 380 | headers=headers, 381 | data=payload, 382 | proxies=self.__get_proxy(), 383 | timeout=self.timeout, 384 | impersonate="chrome110", 385 | ) 386 | return response.status_code == 204 387 | 388 | def get_all_chat_ids(self) -> list[str]: 389 | """ 390 | Retrieve a list with all created chat UUID strings, empty list if no chat is found. 391 | """ 392 | url = ( 393 | f"{self.__BASE_URL}/api/organizations/" 394 | f"{self.__session.organization_id}/chat_conversations" 395 | ) 396 | 397 | headers = { 398 | "Host": "claude.ai", 399 | "User-Agent": self.__session.user_agent, 400 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", 401 | "Accept-Language": "en-US,en;q=0.5", 402 | "Accept-Encoding": "gzip, deflate, br", 403 | "DNT": "1", 404 | "Upgrade-Insecure-Requests": "1", 405 | "Sec-Fetch-Dest": "document", 406 | "Sec-Fetch-Mode": "navigate", 407 | "Sec-Fetch-Site": "none", 408 | "Sec-Fetch-User": "?1", 409 | "Connection": "keep-alive", 410 | "Cookie": self.__session.cookie, 411 | } 412 | 413 | response = http_get( 414 | url, 415 | headers=headers, 416 | proxies=self.__get_proxy(), 417 | timeout=self.timeout, 418 | impersonate="chrome110", 419 | ) 420 | if response.status_code == 200: 421 | j = response.json() 422 | return [chat["uuid"] for chat in j if "uuid" in chat] 423 | 424 | return [] 425 | 426 | def get_chat_data(self, chat_id: str) -> dict: 427 | """ 428 | Print JSON response from calling 429 | `/api/organizations/{organization_id}/chat_conversations/{chat_id}` 430 | """ 431 | 432 | url = ( 433 | f"{self.__BASE_URL}/api/organizations/" 434 | f"{self.__session.organization_id}/chat_conversations/{chat_id}" 435 | ) 436 | 437 | headers = { 438 | "Host": "claude.ai", 439 | "User-Agent": self.__session.user_agent, 440 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", 441 | "Accept-Language": "en-US,en;q=0.5", 442 | "Accept-Encoding": "gzip, deflate, br", 443 | "DNT": "1", 444 | "Upgrade-Insecure-Requests": "1", 445 | "Sec-Fetch-Dest": "document", 446 | "Sec-Fetch-Mode": "navigate", 447 | "Sec-Fetch-Site": "none", 448 | "Sec-Fetch-User": "?1", 449 | "Connection": "keep-alive", 450 | "Cookie": self.__session.cookie, 451 | } 452 | 453 | return http_get( 454 | url, 455 | headers=headers, 456 | proxies=self.__get_proxy(), 457 | timeout=self.timeout, 458 | impersonate="chrome110", 459 | ).json() 460 | 461 | def delete_all_chats(self) -> bool: 462 | """ 463 | Deleted all chats associated with this session 464 | 465 | Returns True on success, False in case at least one chat was not deleted. 466 | """ 467 | chats = self.get_all_chat_ids() 468 | return all([self.delete_chat(chat_id) for chat_id in chats]) 469 | 470 | def __decode_response(self, buffer: bytes, encoding_header: str) -> bytes: 471 | """Return decoded response bytes""" 472 | 473 | if encoding_header == "gzip": 474 | # Content is gzip-encoded, decode it using zlib 475 | return zlib_decompress(buffer, MAX_WBITS | 16) 476 | if encoding_header == "deflate": 477 | # Content is deflate-encoded, decode it using zlib 478 | return zlib_decompress(buffer, -MAX_WBITS) 479 | 480 | # DROPPING BROTLI DECODING 481 | # if encoding_header == "br": 482 | # Content is Brotli-encoded, decode it using the brotli library 483 | # return br_decompress(buffer) 484 | 485 | # Content is either not encoded or with a non supported encoding. 486 | return buffer 487 | 488 | def __parse_send_message_response(self, data_bytes: bytes) -> str | None: 489 | """Return a tuple consisting of (answer, error_string)""" 490 | 491 | # Parse json string lines from raw response string 492 | res = data_bytes.decode("utf-8").strip() 493 | 494 | # Removes extre newline separators 495 | res = sub("\n+", "\n", res).strip() 496 | 497 | # Get json data lines 498 | data_lines = [] 499 | for r in res.splitlines(): 500 | s = search(r"\{.*\}", r) 501 | if s is not None: 502 | data_lines.append(s.group(0)) 503 | 504 | if not data_lines: 505 | # Unable to parse data 506 | return None 507 | 508 | completions = [] 509 | 510 | for line in data_lines: 511 | data_dict: dict = loads(line) 512 | 513 | if "error" in data_dict: 514 | if "resets_at" in data_dict["error"]: 515 | # Wrap the rate limit into exception 516 | raise MessageRateLimitError(int(data_dict["error"]["resets_at"])) 517 | 518 | # Get the error type and message 519 | error_d = data_dict.get("error", {}) 520 | error_type = error_d.get("type", "") 521 | error_msg = error_d.get("message", "") 522 | 523 | if "overloaded" in error_type: 524 | # Wrap Overload error 525 | raise OverloadError( 526 | f"Claude returned error ({error_msg}): " 527 | "Wait some time for before subsequent requests!" 528 | ) 529 | # Raise generic error 530 | raise ClaudeAPIError(f"Unkown Claude error ({error_type}): {error_msg}") 531 | 532 | # Add the completion string to the answer 533 | if "completion" in data_dict: 534 | completions.append(data_dict["completion"]) 535 | 536 | # Return all of the completion strings joined togheter 537 | return "".join(completions).strip() 538 | 539 | def send_message( 540 | self, 541 | chat_id: str, 542 | prompt: str, 543 | attachment_paths: list[str] = None, 544 | ) -> SendMessageResponse: 545 | """ 546 | Send message to `chat_id` using specified `prompt` string. 547 | 548 | You can omitt or provide an attachments path list using `attachment_paths` 549 | 550 | Returns a `SendMessageResponse` instance, having: 551 | - `answer` string field, 552 | - `status_code` integer field, 553 | - `error_response` string field, which will be None in case of no errors. 554 | """ 555 | 556 | self.__check_file_attachments_paths(attachment_paths) 557 | 558 | attachments = [] 559 | if attachment_paths: 560 | for path in attachment_paths: 561 | attachments.append(self.__prepare_file_attachment(path, chat_id)) 562 | 563 | url = ( 564 | f"{self.__BASE_URL}/api/organizations/" 565 | + f"{self.__session.organization_id}/chat_conversations/" 566 | + f"{chat_id}/completion" 567 | ) 568 | 569 | payload = { 570 | "attachments": [], 571 | "files": [], 572 | "prompt": prompt, 573 | "timezone": self.timezone, 574 | } 575 | 576 | for a in attachments: 577 | if isinstance(a, dict): 578 | # Text file attachment 579 | payload["attachments"].append(a) 580 | elif isinstance(a, str): 581 | # Other files uploaded 582 | payload["files"].append(a) 583 | 584 | if self.model_name is not None: 585 | payload["model"] = self.model_name 586 | 587 | payload = dumps( 588 | payload, 589 | indent=None, 590 | separators=(",", ":"), 591 | ) 592 | 593 | headers = { 594 | "Host": "claude.ai", 595 | "User-Agent": self.__session.user_agent, 596 | "Accept": "text/event-stream, text/event-stream", 597 | "Accept-Language": "en-US,en;q=0.5", 598 | "Accept-Encoding": "gzip, deflate, br", 599 | "Content-Type": "application/json", 600 | "Content-Length": f"{len(payload)}", 601 | "Referer": f"{self.__BASE_URL}/chat/{chat_id}", 602 | "Origin": self.__BASE_URL, 603 | "DNT": "1", 604 | "Sec-Fetch-Dest": "empty", 605 | "Sec-Fetch-Mode": "cors", 606 | "Sec-Fetch-Site": "same-origin", 607 | "Connection": "keep-alive", 608 | "Cookie": self.__session.cookie, 609 | "TE": "trailers", 610 | } 611 | 612 | response = http_post( 613 | url, 614 | headers=headers, 615 | data=payload, 616 | timeout=self.timeout, 617 | proxies=self.__get_proxy(), 618 | impersonate="chrome110", 619 | ) 620 | 621 | enc = None 622 | if "Content-Encoding" in response.headers: 623 | enc = response.headers["Content-Encoding"] 624 | 625 | # Decrypt encoded response 626 | try: 627 | dec = self.__decode_response(response.content, enc) 628 | except Exception as e: 629 | # Return raw response for inspection 630 | print(f"Exception decoding from {enc}: {e}") 631 | return SendMessageResponse( 632 | None, 633 | response.status_code, 634 | response.content, 635 | ) 636 | 637 | return SendMessageResponse( 638 | self.__parse_send_message_response(dec), 639 | response.status_code, 640 | response.content, 641 | ) 642 | -------------------------------------------------------------------------------- /claude_api/errors.py: -------------------------------------------------------------------------------- 1 | """custom errors module""" 2 | from time import time 3 | from datetime import datetime 4 | 5 | 6 | class ClaudeAPIError(Exception): 7 | """Base class for ClaudeAPIClient exceptions""" 8 | 9 | 10 | class MessageRateLimitError(ClaudeAPIError): 11 | """Exception for MessageRateLimit 12 | Will hold three variables: 13 | 14 | - `reset_timestamp`: Timestamp in seconds at which the rate limit expires. 15 | 16 | - `reset_date`: Formatted datetime string (%Y-%m-%d %H:%M:%S) at which the rate limit expires. 17 | 18 | - `sleep_sec`: Amount of seconds to wait before reaching the expiration timestamp 19 | (Auto calculated based on reset_timestamp)""" 20 | 21 | def __init__(self, reset_timestamp: int, *args: object) -> None: 22 | super().__init__(*args) 23 | self.reset_timestamp: int = reset_timestamp 24 | """ 25 | The timestamp in seconds when the message rate limit will be reset 26 | """ 27 | self.reset_date: str = datetime.fromtimestamp(reset_timestamp).strftime( 28 | "%Y-%m-%d %H:%M:%S" 29 | ) 30 | """ 31 | Formatted datetime string of expiration timestamp in the format %Y-%m-%d %H:%M:%S 32 | """ 33 | 34 | @property 35 | def sleep_sec(self) -> int: 36 | """ 37 | The amount of seconds to wait before reaching the reset_timestamp 38 | """ 39 | return int(abs(time() - self.reset_timestamp)) + 1 40 | 41 | 42 | class OverloadError(ClaudeAPIError): 43 | """Exception wrapping the Claude's overload_error""" 44 | 45 | def __init__(self, *args: object) -> None: 46 | super().__init__(*args) 47 | -------------------------------------------------------------------------------- /claude_api/session.py: -------------------------------------------------------------------------------- 1 | """Session module""" 2 | from typing import Optional 3 | from dataclasses import dataclass 4 | from json import loads as json_loads 5 | from selgym.gym import ( 6 | cleanup_resources, 7 | get_firefox_options, 8 | get_firefox_webdriver, 9 | get_default_firefox_profile, 10 | wait_element_by, 11 | click_element, 12 | By, 13 | ) 14 | 15 | 16 | @dataclass 17 | class SessionData: 18 | """ 19 | This session class is made for `ClaudeAPIClient` constructor. 20 | 21 | It can be auto generated by having a working login in Firefox 22 | and geckodriver installed, calling `get_session_data()` 23 | with the Firefox profile path, or the default one if omitted. 24 | """ 25 | 26 | cookie: str 27 | """ 28 | The entire Cookie header string value 29 | """ 30 | user_agent: str 31 | """ 32 | Browser User agent 33 | """ 34 | 35 | organization_id: Optional[str] = None 36 | """ 37 | Claude's account organization ID, will be auto retrieved if None 38 | """ 39 | 40 | 41 | def get_session_data(profile: str = "", quiet: bool = False, organization_index:int=-1) -> SessionData | None: 42 | """ 43 | Retrieves Claude session data 44 | 45 | This function requires a profile with Claude login and geckodriver installed! 46 | 47 | The default Firefox profile will be used, if the profile argument was not overwrited. 48 | 49 | Parameter `organization_index` is by default -1 50 | (last entry from https://claude.ai/api/organizations) 51 | """ 52 | 53 | json_tab_id = 'a[id="rawdata-tab"]' 54 | json_text_csss = 'pre[class="data"]' 55 | 56 | base_url = "https://claude.ai/api/organizations" 57 | if not profile: 58 | profile = get_default_firefox_profile() 59 | 60 | if not quiet: 61 | print(f"\nRetrieving Claude session cookie from {profile}") 62 | 63 | opts = get_firefox_options(firefox_profile=profile, headless=True) 64 | driver = get_firefox_webdriver(options=opts) 65 | try: 66 | driver.get(base_url) 67 | 68 | driver.implicitly_wait(10) 69 | user_agent = driver.execute_script("return navigator.userAgent") 70 | if not user_agent: 71 | raise RuntimeError("Cannot retrieve UserAgent...") 72 | 73 | cookies = driver.get_cookies() 74 | 75 | cookie_string = "; ".join( 76 | [f"{cookie['name']}={cookie['value']}" for cookie in cookies] 77 | ) 78 | 79 | btn = wait_element_by(driver, By.CSS_SELECTOR, json_tab_id) 80 | click_element(driver, btn) 81 | 82 | org_id = None 83 | pre = wait_element_by(driver, By.CSS_SELECTOR, json_text_csss) 84 | if pre.text: 85 | j = json_loads(pre.text) 86 | try: 87 | if j and len(j) > organization_index and "uuid" in j[organization_index]: 88 | org_id = j[organization_index]["uuid"] 89 | except KeyError: 90 | print( 91 | f"\nUnable to retrieve organization_id from profile: {profile}\n" 92 | "Check if this profile is logged into Claude!" 93 | ) 94 | return None 95 | return SessionData(cookie_string, user_agent, org_id) 96 | finally: 97 | driver.quit() 98 | cleanup_resources() 99 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """setup.py""" 2 | from os.path import dirname, join, abspath 3 | from setuptools import setup, find_packages 4 | 5 | __DESCRIPTION = """This unofficial Python API provides access to \ 6 | the conversational capabilities of Anthropic's Claude AI \ 7 | through a simple chat messaging interface.""" 8 | 9 | with open( 10 | join(abspath(dirname(__file__)), "README.md"), 11 | "r", 12 | encoding="utf-8", 13 | errors="ignore", 14 | ) as fp: 15 | __LONG_DESCRIPTION = fp.read().lstrip().rstrip() 16 | 17 | setup( 18 | name="unofficial-claude-api", 19 | version="0.3.3", 20 | author="st1vms", 21 | author_email="stefano.maria.salvatore@gmail.com", 22 | description=__DESCRIPTION, 23 | long_description=__LONG_DESCRIPTION, 24 | long_description_content_type="text/markdown", 25 | url="https://github.com/st1vms/unofficial-claude-api", 26 | packages=find_packages(), 27 | classifiers=[ 28 | "Programming Language :: Python :: 3.10", 29 | "License :: OSI Approved :: MIT License", 30 | "Operating System :: OS Independent", 31 | ], 32 | python_requires=">=3.10", 33 | install_requires=[ 34 | "requests", 35 | "selgym", 36 | "curl_cffi", 37 | "tzlocal", 38 | #"brotli", 39 | ], 40 | ) 41 | --------------------------------------------------------------------------------