diff --git a/.github/workflows/changeset-check.yml b/.github/workflows/changeset-check.yml
deleted file mode 100644
index 3ce60c1e3..000000000
--- a/.github/workflows/changeset-check.yml
+++ /dev/null
@@ -1,47 +0,0 @@
-name: Changeset Check
-
-on:
- pull_request:
- types: [opened, synchronize, reopened, labeled, unlabeled]
-
-jobs:
- check:
- name: Check for Changeset
- runs-on: ubuntu-latest
- steps:
- - name: Checkout Repo
- uses: actions/checkout@v6
- with:
- fetch-depth: 0
-
- - name: Check for changeset
- uses: actions/github-script@v7
- with:
- script: |
- const { execSync } = require('child_process');
-
- const labels = context.payload.pull_request.labels.map(l => l.name);
- if (labels.includes('skip-changeset')) {
- console.log('Skipping changeset check for PR with skip-changeset label');
- return;
- }
-
- const baseSha = context.payload.pull_request.base.sha;
- const headSha = context.payload.pull_request.head.sha;
-
- const diff = execSync(
- `git diff --name-only ${baseSha}...${headSha}`,
- { encoding: 'utf-8' }
- );
-
- const hasChangeset = diff
- .split('\n')
- .some(file => file.startsWith('.changeset/') && file.endsWith('.md') && !file.includes('README'));
-
- if (!hasChangeset) {
- core.setFailed(
- 'No changeset found. Please add a changeset using `pnpm changeset` or add the "skip-changeset" label if this PR doesn\'t need one (e.g., docs, CI changes).'
- );
- } else {
- console.log('Changeset found!');
- }
diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml
new file mode 100644
index 000000000..4b208dbd0
--- /dev/null
+++ b/.github/workflows/python-ci.yml
@@ -0,0 +1,96 @@
+name: Python CI
+
+on:
+ push:
+ branches: [main, master]
+ paths:
+ - "packages/python/**"
+ - ".github/workflows/python-ci.yml"
+ pull_request:
+ branches: [main, master]
+ paths:
+ - "packages/python/**"
+ - ".github/workflows/python-ci.yml"
+
+defaults:
+ run:
+ working-directory: packages/python
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Install uv
+ uses: astral-sh/setup-uv@v4
+ with:
+ version: "latest"
+
+ - name: Set up Python ${{ matrix.python-version }}
+ run: uv python install ${{ matrix.python-version }}
+
+ - name: Install dependencies
+ run: uv sync --all-packages
+
+ - name: Run linting
+ run: uv run ruff check .
+
+ - name: Run formatting check
+ run: uv run ruff format --check .
+
+ - name: Run type checking
+ run: uv run mypy sdk/src/context7
+
+ - name: Run tests
+ env:
+ CONTEXT7_API_KEY: ${{ secrets.CONTEXT7_API_KEY }}
+ run: uv run pytest sdk/tests -v --cov=context7 --cov-report=xml
+
+ build:
+ runs-on: ubuntu-latest
+ needs: test
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Install uv
+ uses: astral-sh/setup-uv@v4
+
+ - name: Build package
+ working-directory: packages/python/sdk
+ run: uv build
+
+ - name: Upload build artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: python-sdk-dist
+ path: packages/python/sdk/dist/
+
+ publish:
+ runs-on: ubuntu-latest
+ needs: [test, build]
+ if: github.event_name == 'push' && github.ref == 'refs/heads/master'
+ environment: pypi
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Install uv
+ uses: astral-sh/setup-uv@v4
+
+ - name: Download build artifacts
+ uses: actions/download-artifact@v4
+ with:
+ name: python-sdk-dist
+ path: packages/python/sdk/dist/
+
+ - name: Publish to PyPI
+ working-directory: packages/python/sdk
+ env:
+ UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }}
+ run: uv publish
diff --git a/.gitignore b/.gitignore
index 8ed737943..bf6f3bed8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -182,3 +182,20 @@ prompt.txt
reports
reports-old
src/test/questions*
+
+# Python
+__pycache__/
+*.py[cod]
+*$py.class
+*.so
+.Python
+.venv/
+venv/
+ENV/
+.mypy_cache/
+.ruff_cache/
+.pytest_cache/
+*.egg-info/
+dist/
+build/
+uv.lock
diff --git a/docs/docs.json b/docs/docs.json
index dd3b9eb95..9fe142756 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -53,6 +53,16 @@
"pages": ["sdks/ts/commands/search-library", "sdks/ts/commands/get-context"]
}
]
+ },
+ {
+ "group": "Python",
+ "pages": [
+ "sdks/python/getting-started",
+ {
+ "group": "Commands",
+ "pages": ["sdks/python/commands/search-library", "sdks/python/commands/get-context"]
+ }
+ ]
}
]
},
diff --git a/docs/sdks/python/commands/get-context.mdx b/docs/sdks/python/commands/get-context.mdx
new file mode 100644
index 000000000..7c1e3262a
--- /dev/null
+++ b/docs/sdks/python/commands/get-context.mdx
@@ -0,0 +1,150 @@
+---
+title: "Get Context"
+description: "Retrieve documentation context for a library"
+---
+
+# Get Context
+
+Retrieve documentation context for a specific library. Returns documentation as a list of documentation objects (default) or as plain text.
+
+## Arguments
+
+
+ The user's question or task (used for relevance ranking)
+
+
+
+ The library identifier (e.g., `/facebook/react`)
+
+
+
+ Format of the response
+ - `json`: List of Documentation objects (default)
+ - `txt`: Plain text format
+
+ Default: `"json"`
+
+
+## Response
+
+The response type depends on the `type` parameter.
+
+### JSON Format (`type="json"` or default)
+
+Returns `list[Documentation]` - a list of documentation objects.
+
+### Text Format (`type="txt"`)
+
+Returns a `str` containing the documentation context ready to use in LLM prompts.
+
+
+
+
+ Title of the documentation snippet
+
+
+ The documentation content (includes code blocks in markdown format)
+
+
+ Source identifier for the snippet
+
+
+
+
+## Examples
+
+
+```python JSON Format (Default)
+from context7 import Context7
+
+with Context7() as client:
+ docs = client.get_context(
+ "How do I use hooks?",
+ "/facebook/react"
+ )
+
+ for doc in docs:
+ print(doc.title)
+ print(doc.content)
+ print(doc.source)
+```
+
+```python Text Format
+from context7 import Context7
+
+with Context7() as client:
+ context = client.get_context(
+ "How do I use hooks?",
+ "/facebook/react",
+ type="txt"
+ )
+ print(context)
+```
+
+```python Async Usage
+import asyncio
+from context7 import Context7
+
+async def main():
+ async with Context7() as client:
+ context = await client.get_context_async(
+ "How do I use hooks?",
+ "/facebook/react"
+ )
+ print(context)
+
+asyncio.run(main())
+```
+
+```python Error Handling
+from context7 import Context7, Context7Error
+
+with Context7() as client:
+ try:
+ context = client.get_context(
+ "How to get started?",
+ "/invalid/library"
+ )
+ except Context7Error as e:
+ print(f"API Error: {e}")
+```
+
+
+## Use Cases
+
+### Providing Context to LLMs
+
+```python
+from context7 import Context7
+
+def get_docs_for_prompt(library: str, question: str) -> str:
+ with Context7() as client:
+ context = client.get_context(question, library, type="txt")
+
+ return f"""
+Here is the relevant documentation:
+
+{context}
+
+User question: {question}
+"""
+
+prompt = get_docs_for_prompt("/facebook/react", "How do I use useEffect?")
+```
+
+### Processing Individual Snippets
+
+```python
+from context7 import Context7
+
+with Context7() as client:
+ docs = client.get_context(
+ "How to create components?",
+ "/facebook/react"
+ )
+
+ for doc in docs:
+ print(f"Title: {doc.title}")
+ print(f"Source: {doc.source}")
+ print(f"Content length: {len(doc.content)} chars")
+```
diff --git a/docs/sdks/python/commands/search-library.mdx b/docs/sdks/python/commands/search-library.mdx
new file mode 100644
index 000000000..739b548f9
--- /dev/null
+++ b/docs/sdks/python/commands/search-library.mdx
@@ -0,0 +1,164 @@
+---
+title: "Search Library"
+description: "Search available libraries"
+---
+
+# Search Library
+
+Search across available libraries. Returns a list of matching libraries with metadata useful for selection.
+
+## Arguments
+
+
+ The user's question or task (used for relevance ranking)
+
+
+
+ The library name to search for
+
+
+## Response
+
+Returns `list[Library]` - a list of library objects.
+
+
+
+
+ Unique identifier for the library (e.g., `/facebook/react`)
+
+
+ Display name of the library
+
+
+ Brief description of the library
+
+
+ Total number of documentation snippets available
+
+
+ Trust score of the library
+
+
+ Benchmark score indicating documentation quality
+
+
+ Available versions
+
+
+
+
+## Examples
+
+
+```python Basic Search
+from context7 import Context7
+
+with Context7() as client:
+ libraries = client.search_library(
+ "I need to build a UI",
+ "react"
+ )
+
+ print(f"Found {len(libraries)} libraries")
+ for library in libraries:
+ print(f"{library.name}: {library.description}")
+```
+
+```python Async Usage
+import asyncio
+from context7 import Context7
+
+async def main():
+ async with Context7() as client:
+ libraries = await client.search_library_async(
+ "I need to build a UI",
+ "react"
+ )
+
+ print(f"Found {len(libraries)} libraries")
+ for library in libraries:
+ print(f"{library.name}: {library.description}")
+
+asyncio.run(main())
+```
+
+```python Selecting Best Library
+from context7 import Context7
+
+with Context7() as client:
+ libraries = client.search_library(
+ "I want to build a web app",
+ "next"
+ )
+
+ sorted_libs = sorted(libraries, key=lambda x: x.benchmark_score, reverse=True)
+ best = sorted_libs[0]
+
+ print(f"Best match: {best.name} (score: {best.benchmark_score})")
+ print(f"Available snippets: {best.total_snippets}")
+```
+
+```python Error Handling
+from context7 import Context7, Context7Error
+
+with Context7() as client:
+ try:
+ libraries = client.search_library("query", "express")
+
+ if not libraries:
+ print("No libraries found")
+ else:
+ print(f"Found {len(libraries)} libraries")
+ except Context7Error as e:
+ print(f"API Error: {e}")
+```
+
+
+## Use Cases
+
+### Finding Libraries by Score
+
+```python
+from context7 import Context7
+
+with Context7() as client:
+ libraries = client.search_library("state management", "redux")
+
+ trusted = sorted(libraries, key=lambda x: x.trust_score, reverse=True)
+ print(f"Most trusted: {trusted[0].name}")
+```
+
+### Checking Available Versions
+
+```python
+from context7 import Context7
+
+with Context7() as client:
+ libraries = client.search_library("I want to use lodash", "lodash")
+
+ library = libraries[0]
+ if library.versions:
+ print(f"Available versions: {', '.join(library.versions)}")
+```
+
+### Full Workflow
+
+```python
+from context7 import Context7
+
+with Context7() as client:
+ libraries = client.search_library(
+ "I want to build a React app",
+ "react"
+ )
+
+ library = libraries[0]
+ print(f"Using: {library.name} ({library.id})")
+
+ context = client.get_context(
+ "How do I create a component?",
+ library.id
+ )
+
+ print(context)
+```
diff --git a/docs/sdks/python/getting-started.mdx b/docs/sdks/python/getting-started.mdx
new file mode 100644
index 000000000..0d5a653d8
--- /dev/null
+++ b/docs/sdks/python/getting-started.mdx
@@ -0,0 +1,159 @@
+---
+title: "Getting Started"
+description: "Get started with the Context7 Python SDK"
+---
+
+
+ **Work in Progress**: This SDK is currently under active development. The API is subject to change and may introduce breaking changes in future releases.
+
+
+# Getting Started
+
+`context7` is a Python SDK for Context7, enabling easier access to library documentation with full type hints.
+
+Using `context7` you can:
+
+- Search across available libraries
+- Get documentation context for any library
+- Access library metadata including trust scores and versions
+
+You can find the Github Repository [here](https://github.com/upstash/context7/tree/master/packages/python/sdk).
+
+## Install
+
+
+ ```shell pip
+ pip install context7
+ ```
+
+```shell uv
+uv add context7
+```
+
+```shell poetry
+poetry add context7
+```
+
+
+
+## Usage
+
+### Initializing the Client
+
+To use the Context7 SDK, you need an API key. You can get your API key from the [Context7 Dashboard](https://context7.com/dashboard).
+
+#### Using environment variables
+
+The SDK automatically reads from environment variables if no API key is provided:
+
+```bash
+CONTEXT7_API_KEY="your_api_key_here"
+```
+
+When an environment variable is set, you can initialize the client without any parameters:
+
+```python
+from context7 import Context7
+
+with Context7() as client:
+ # Use the client
+ pass
+```
+
+#### Using a configuration parameter
+
+If you prefer to pass the API key in code:
+
+```python
+from context7 import Context7
+
+with Context7(api_key="ctx7sk_...") as client:
+ # Use the client
+ pass
+```
+
+
+ The SDK checks for API keys in this order: 1. `api_key` parameter (if provided) 2.
+ `CONTEXT7_API_KEY` environment variable
+
+
+## Quick Start Example
+
+### Synchronous Usage
+
+```python
+from context7 import Context7
+
+with Context7() as client:
+ # Search for libraries
+ libraries = client.search_library(
+ "I need to build a UI with components",
+ "react"
+ )
+ print(f"Found {len(libraries)} libraries")
+ print(libraries[0].id) # "/facebook/react"
+
+ # Get documentation as JSON array (default)
+ docs = client.get_context(
+ "How do I use hooks?",
+ "/facebook/react"
+ )
+ print(docs[0].title, docs[0].content)
+
+ # Get documentation as plain text
+ context = client.get_context(
+ "How do I use hooks?",
+ "/facebook/react",
+ type="txt"
+ )
+ print(context)
+```
+
+### Asynchronous Usage
+
+```python
+import asyncio
+from context7 import Context7
+
+async def main():
+ async with Context7() as client:
+ # Search for libraries
+ libraries = await client.search_library_async(
+ "I need to build a UI with components",
+ "react"
+ )
+ print(f"Found {len(libraries)} libraries")
+ print(libraries[0].id) # "/facebook/react"
+
+ # Get documentation context (JSON array by default)
+ docs = await client.get_context_async(
+ "How do I use hooks?",
+ "/facebook/react"
+ )
+ print(docs[0].title, docs[0].content)
+
+asyncio.run(main())
+```
+
+## Error Handling
+
+The SDK raises `Context7Error` for API errors:
+
+```python
+from context7 import Context7, Context7Error
+
+with Context7() as client:
+ try:
+ context = client.get_context("query", "/invalid/library")
+ except Context7Error as e:
+ print(f"Context7 API Error: {e}")
+ except Exception as e:
+ print(f"Unexpected error: {e}")
+```
+
+## Next Steps
+
+Explore the SDK commands:
+
+- [Search Library](/sdks/python/commands/search-library) - Search for libraries
+- [Get Context](/sdks/python/commands/get-context) - Retrieve library documentation context
diff --git a/package.json b/package.json
index 8e2f2accc..bc515508a 100644
--- a/package.json
+++ b/package.json
@@ -4,7 +4,8 @@
"version": "1.0.0",
"description": "Context7 monorepo - Documentation tools and SDKs",
"workspaces": [
- "packages/*"
+ "packages/*",
+ "!packages/python"
],
"scripts": {
"build": "pnpm -r run build",
@@ -21,7 +22,14 @@
"format": "pnpm -r run format",
"format:check": "pnpm -r run format:check",
"release": "pnpm build && changeset publish",
- "release:snapshot": "changeset version --snapshot canary && pnpm build && changeset publish --tag canary --no-git-tag"
+ "release:snapshot": "changeset version --snapshot canary && pnpm build && changeset publish --tag canary --no-git-tag",
+ "py:install": "cd packages/python && uv sync --all-packages",
+ "py:test": "cd packages/python && uv run pytest sdk/tests -v",
+ "py:lint": "cd packages/python && uv run ruff check .",
+ "py:format": "cd packages/python && uv run ruff format .",
+ "py:typecheck": "cd packages/python && uv run mypy sdk/src/context7",
+ "py:build": "cd packages/python/sdk && uv build",
+ "py:clean": "rm -rf packages/python/.venv packages/python/uv.lock packages/python/sdk/dist"
},
"repository": {
"type": "git",
diff --git a/packages/python/.python-version b/packages/python/.python-version
new file mode 100644
index 000000000..2c0733315
--- /dev/null
+++ b/packages/python/.python-version
@@ -0,0 +1 @@
+3.11
diff --git a/packages/python/pyproject.toml b/packages/python/pyproject.toml
new file mode 100644
index 000000000..303a7d491
--- /dev/null
+++ b/packages/python/pyproject.toml
@@ -0,0 +1,48 @@
+[project]
+name = "context7-workspace"
+version = "0.0.0"
+description = "Context7 Python packages workspace"
+requires-python = ">=3.9"
+license = "MIT"
+
+[tool.uv]
+package = false
+
+[tool.uv.workspace]
+members = ["sdk"]
+
+[tool.ruff]
+line-length = 100
+target-version = "py39"
+
+[tool.ruff.lint]
+select = ["E", "F", "I", "UP", "B", "SIM", "TCH"]
+ignore = ["E501"]
+
+[tool.ruff.format]
+quote-style = "double"
+
+[tool.pytest.ini_options]
+testpaths = ["sdk/tests"]
+asyncio_mode = "auto"
+asyncio_default_fixture_loop_scope = "function"
+
+[tool.mypy]
+python_version = "3.9"
+strict = true
+warn_return_any = true
+warn_unused_ignores = true
+
+[tool.pyright]
+pythonVersion = "3.9"
+typeCheckingMode = "strict"
+
+[dependency-groups]
+dev = [
+ "mypy>=1.19.0",
+ "pytest>=8.4.2",
+ "pytest-asyncio>=1.2.0",
+ "pytest-cov>=7.0.0",
+ "python-dotenv>=1.2.1",
+ "ruff>=0.14.8",
+]
diff --git a/packages/python/sdk/README.md b/packages/python/sdk/README.md
new file mode 100644
index 000000000..1826fcc51
--- /dev/null
+++ b/packages/python/sdk/README.md
@@ -0,0 +1,69 @@
+# Context7 Python SDK
+
+Python SDK for Context7 - Documentation retrieval for AI agents.
+
+## Installation
+
+```bash
+pip install context7
+```
+
+Or with uv:
+
+```bash
+uv add context7
+```
+
+## Quick Start
+
+```python
+import asyncio
+from context7 import Context7
+
+async def main():
+ async with Context7(api_key="ctx7sk_...") as client:
+ # Search for libraries
+ libraries = await client.search_library_async("I need a UI library", "react")
+ for lib in libraries:
+ print(f"{lib.id}: {lib.name}")
+
+ # Get documentation context
+ context = await client.get_context_async("How to use hooks", "/facebook/react")
+ print(context)
+
+asyncio.run(main())
+```
+
+## API Reference
+
+### `Context7(api_key=None, base_url=None)`
+
+Initialize the Context7 client.
+
+- `api_key`: API key for authentication. Falls back to `CONTEXT7_API_KEY` environment variable.
+- `base_url`: Optional custom base URL for the API.
+
+### `client.search_library(query: str, library_name: str) -> list[Library]`
+
+Search for libraries matching the given query.
+
+- `query`: The user's question or task (used for relevance ranking)
+- `library_name`: The library name to search for
+
+Async version: `await client.search_library_async(query, library_name)`
+
+### `client.get_context(query: str, library_id: str, *, type="txt") -> str | list[Documentation]`
+
+Get documentation context for a library.
+
+**Parameters:**
+
+- `query`: The user's question or task
+- `library_id`: Context7 library ID (e.g., `/facebook/react`)
+- `type`: Response format - `"txt"` (default) returns plain text, `"json"` returns list of Documentation objects
+
+Async version: `await client.get_context_async(query, library_id, type=...)`
+
+## License
+
+MIT
diff --git a/packages/python/sdk/pyproject.toml b/packages/python/sdk/pyproject.toml
new file mode 100644
index 000000000..1aa886153
--- /dev/null
+++ b/packages/python/sdk/pyproject.toml
@@ -0,0 +1,56 @@
+[project]
+name = "context7"
+version = "0.1.0"
+description = "Python SDK for Context7 - Documentation retrieval for AI agents"
+readme = "README.md"
+requires-python = ">=3.9"
+license = "MIT"
+authors = [
+ { name = "Upstash", email = "support@upstash.com" }
+]
+keywords = ["context7", "sdk", "documentation", "ai", "llm", "mcp", "upstash"]
+classifiers = [
+ "Development Status :: 4 - Beta",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: MIT License",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
+ "Typing :: Typed",
+]
+
+dependencies = [
+ "httpx>=0.27.0",
+ "pydantic>=2.0.0",
+]
+
+[project.optional-dependencies]
+dev = [
+ "pytest>=8.0.0",
+ "pytest-asyncio>=0.24.0",
+ "pytest-cov>=5.0.0",
+ "mypy>=1.11.0",
+ "ruff>=0.6.0",
+]
+
+[project.urls]
+Homepage = "https://context7.com"
+Documentation = "https://context7.com/docs"
+Repository = "https://github.com/upstash/context7"
+Issues = "https://github.com/upstash/context7/issues"
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[tool.hatch.build.targets.wheel]
+packages = ["src/context7"]
+
+[tool.hatch.build.targets.sdist]
+include = [
+ "/src",
+ "/tests",
+]
diff --git a/packages/python/sdk/src/context7/__init__.py b/packages/python/sdk/src/context7/__init__.py
new file mode 100644
index 000000000..d6d581151
--- /dev/null
+++ b/packages/python/sdk/src/context7/__init__.py
@@ -0,0 +1,41 @@
+"""
+Context7 Python SDK - Documentation retrieval for AI agents.
+
+This SDK provides a simple interface to search for libraries and retrieve
+documentation from Context7, optimized for AI agents and LLMs.
+
+Example:
+ ```python
+ import asyncio
+ from context7 import Context7
+
+ async def main():
+ async with Context7(api_key="ctx7sk_...") as client:
+ # Search for libraries
+ libraries = await client.search_library_async(
+ "I need a UI library", "react"
+ )
+
+ # Get documentation context
+ context = await client.get_context_async(
+ "How to use hooks", "/facebook/react"
+ )
+ print(context)
+
+ asyncio.run(main())
+ ```
+"""
+
+from context7.client import Context7
+from context7.errors import Context7APIError, Context7Error
+from context7.models import Documentation, Library
+
+__all__ = [
+ "Context7",
+ "Context7Error",
+ "Context7APIError",
+ "Library",
+ "Documentation",
+]
+
+__version__ = "0.1.0"
diff --git a/packages/python/sdk/src/context7/client.py b/packages/python/sdk/src/context7/client.py
new file mode 100644
index 000000000..985c57f81
--- /dev/null
+++ b/packages/python/sdk/src/context7/client.py
@@ -0,0 +1,389 @@
+"""Main Context7 client for interacting with the Context7 API."""
+
+from __future__ import annotations
+
+import os
+import warnings
+from typing import Any, Literal, overload
+
+from context7.errors import Context7Error
+from context7.http import HttpClient
+from context7.models import (
+ ApiCodeSnippet,
+ ApiContextJsonResponse,
+ ApiInfoSnippet,
+ ApiSearchResult,
+ Documentation,
+ Library,
+)
+
+DEFAULT_BASE_URL = "https://context7.com/api"
+API_KEY_PREFIX = "ctx7sk"
+
+
+def _validate_api_key(api_key: str | None) -> str:
+ """Validate and resolve the API key."""
+ resolved_api_key = api_key or os.environ.get("CONTEXT7_API_KEY")
+
+ if not resolved_api_key:
+ raise Context7Error(
+ "API key is required. Pass it in the constructor or set "
+ "CONTEXT7_API_KEY environment variable."
+ )
+
+ if not resolved_api_key.startswith(API_KEY_PREFIX):
+ warnings.warn(
+ f"API key should start with '{API_KEY_PREFIX}'",
+ UserWarning,
+ stacklevel=3,
+ )
+
+ return resolved_api_key
+
+
+def _format_code_snippet(snippet: ApiCodeSnippet) -> Documentation:
+ """Format a code snippet into a Documentation object."""
+ code_blocks = "\n\n".join(
+ f"```{item.get('language', '')}\n{item.get('code', '')}\n```" for item in snippet.code_list
+ )
+ content = f"{snippet.code_description}\n\n{code_blocks}"
+
+ return Documentation(
+ title=snippet.code_title,
+ content=content,
+ source=snippet.page_title or snippet.code_id,
+ )
+
+
+def _format_info_snippet(snippet: ApiInfoSnippet) -> Documentation:
+ """Format an info snippet into a Documentation object."""
+ return Documentation(
+ title=snippet.breadcrumb or "Documentation",
+ content=snippet.content,
+ source=snippet.page_id,
+ )
+
+
+def _format_library(result: ApiSearchResult) -> Library:
+ """Format an API search result into a Library object."""
+ return Library(
+ id=result.id,
+ name=result.title,
+ description=result.description,
+ total_snippets=result.total_snippets or 0,
+ trust_score=result.trust_score or 0.0,
+ benchmark_score=result.benchmark_score or 0.0,
+ versions=result.versions,
+ )
+
+
+def _process_context_response(
+ result: str | dict[str, Any],
+ response_type: Literal["json", "txt"],
+) -> str | list[Documentation]:
+ """Process the context API response based on type."""
+ if response_type == "txt":
+ if isinstance(result, str):
+ return result
+ return ""
+
+ # Parse JSON response
+ api_response = ApiContextJsonResponse.model_validate(result)
+
+ docs: list[Documentation] = []
+ for code_snippet in api_response.code_snippets:
+ docs.append(_format_code_snippet(code_snippet))
+ for info_snippet in api_response.info_snippets:
+ docs.append(_format_info_snippet(info_snippet))
+
+ return docs
+
+
+class Context7:
+ """
+ Context7 Python SDK client.
+
+ The Context7 client provides methods to search for libraries and retrieve
+ documentation optimized for AI agents and LLMs.
+
+ Synchronous Usage:
+ ```python
+ from context7 import Context7
+
+ # Initialize with API key (or set CONTEXT7_API_KEY env var)
+ with Context7(api_key="ctx7sk_...") as client:
+ # Search for libraries
+ libraries = client.search_library("I need a UI library", "react")
+ for lib in libraries:
+ print(f"{lib.id}: {lib.name}")
+
+ # Get documentation context
+ docs = client.get_context("How to use hooks", "/facebook/react")
+ for doc in docs:
+ print(f"{doc.title}: {doc.content[:100]}...")
+ ```
+
+ Asynchronous Usage:
+ ```python
+ import asyncio
+ from context7 import Context7
+
+ async def main():
+ async with Context7(api_key="ctx7sk_...") as client:
+ # Search for libraries
+ libraries = await client.search_library_async(
+ "I need a UI library", "react"
+ )
+ for lib in libraries:
+ print(f"{lib.id}: {lib.name}")
+
+ # Get documentation context
+ docs = await client.get_context_async(
+ "How to use hooks", "/facebook/react"
+ )
+ for doc in docs:
+ print(f"{doc.title}: {doc.content[:100]}...")
+
+ asyncio.run(main())
+ ```
+ """
+
+ def __init__(
+ self,
+ api_key: str | None = None,
+ base_url: str | None = None,
+ ) -> None:
+ """
+ Initialize the Context7 client.
+
+ Args:
+ api_key: API key for authentication. Falls back to CONTEXT7_API_KEY
+ environment variable if not provided.
+ base_url: Optional custom base URL for the API. Defaults to
+ https://context7.com/api.
+
+ Raises:
+ Context7Error: If no API key is provided or found in environment.
+ """
+ resolved_api_key = _validate_api_key(api_key)
+ http_headers = {"Authorization": f"Bearer {resolved_api_key}"}
+ resolved_base_url = base_url or DEFAULT_BASE_URL
+
+ self._http = HttpClient(
+ base_url=resolved_base_url,
+ headers=http_headers,
+ )
+
+ # Sync context manager
+ def __enter__(self) -> Context7:
+ """Enter sync context manager."""
+ return self
+
+ def __exit__(self, *args: object) -> None:
+ """Exit sync context manager and close connections."""
+ self.close()
+
+ # Async context manager
+ async def __aenter__(self) -> Context7:
+ """Enter async context manager."""
+ return self
+
+ async def __aexit__(self, *args: object) -> None:
+ """Exit async context manager and close connections."""
+ await self.close_async()
+
+ def close(self) -> None:
+ """Close the sync HTTP client connection."""
+ self._http.close()
+
+ async def close_async(self) -> None:
+ """Close the async HTTP client connection."""
+ await self._http.close_async()
+
+ # Synchronous methods
+ def search_library(self, query: str, library_name: str) -> list[Library]:
+ """
+ Search for libraries matching the given query.
+
+ Args:
+ query: The user's question or task (used for relevance ranking).
+ library_name: The library name to search for.
+
+ Returns:
+ List of matching libraries.
+
+ Example:
+ ```python
+ libraries = client.search_library("I need a UI library", "react")
+ for lib in libraries:
+ print(f"{lib.id}: {lib.name} ({lib.total_snippets} snippets)")
+ ```
+ """
+ result = self._http.request(
+ method="GET",
+ path=["v2", "libs", "search"],
+ query={"query": query, "libraryName": library_name},
+ )
+ api_results = [ApiSearchResult.model_validate(item) for item in result["results"]]
+ return [_format_library(r) for r in api_results]
+
+ @overload
+ def get_context(
+ self,
+ query: str,
+ library_id: str,
+ *,
+ type: Literal["json"] = "json", # pylint: disable=redefined-builtin
+ ) -> list[Documentation]: ...
+
+ @overload
+ def get_context(
+ self,
+ query: str,
+ library_id: str,
+ *,
+ type: Literal["txt"], # pylint: disable=redefined-builtin
+ ) -> str: ...
+
+ def get_context(
+ self,
+ query: str,
+ library_id: str,
+ *,
+ type: Literal["json", "txt"] = "json", # pylint: disable=redefined-builtin
+ ) -> list[Documentation] | str:
+ """
+ Get documentation context for a library.
+
+ Args:
+ query: The user's question or task.
+ library_id: Context7 library ID (e.g., "/facebook/react").
+ type: Response format:
+ - "json": List of Documentation objects (default)
+ - "txt": Plain text documentation
+
+ Returns:
+ Documentation as list of Documentation objects (json) or string (txt).
+
+ Example:
+ ```python
+ # Get context as structured data (default)
+ docs = client.get_context("How to use hooks", "/facebook/react")
+ for doc in docs:
+ print(f"{doc.title}: {doc.content[:100]}...")
+
+ # Get context as plain text
+ context = client.get_context(
+ "How to use hooks",
+ "/facebook/react",
+ type="txt"
+ )
+ print(context)
+ ```
+ """
+ result = self._http.request(
+ method="GET",
+ path=["v2", "context"],
+ query={
+ "query": query,
+ "libraryId": library_id,
+ "type": type,
+ },
+ )
+ return _process_context_response(result, type)
+
+ # Asynchronous methods
+ async def search_library_async(self, query: str, library_name: str) -> list[Library]:
+ """
+ Search for libraries matching the given query (async version).
+
+ Args:
+ query: The user's question or task (used for relevance ranking).
+ library_name: The library name to search for.
+
+ Returns:
+ List of matching libraries.
+
+ Example:
+ ```python
+ libraries = await client.search_library_async(
+ "I need a UI library", "react"
+ )
+ for lib in libraries:
+ print(f"{lib.id}: {lib.name} ({lib.total_snippets} snippets)")
+ ```
+ """
+ result = await self._http.request_async(
+ method="GET",
+ path=["v2", "libs", "search"],
+ query={"query": query, "libraryName": library_name},
+ )
+ api_results = [ApiSearchResult.model_validate(item) for item in result["results"]]
+ return [_format_library(r) for r in api_results]
+
+ @overload
+ async def get_context_async(
+ self,
+ query: str,
+ library_id: str,
+ *,
+ type: Literal["json"] = "json", # pylint: disable=redefined-builtin
+ ) -> list[Documentation]: ...
+
+ @overload
+ async def get_context_async(
+ self,
+ query: str,
+ library_id: str,
+ *,
+ type: Literal["txt"], # pylint: disable=redefined-builtin
+ ) -> str: ...
+
+ async def get_context_async(
+ self,
+ query: str,
+ library_id: str,
+ *,
+ type: Literal["json", "txt"] = "json", # pylint: disable=redefined-builtin
+ ) -> list[Documentation] | str:
+ """
+ Get documentation context for a library (async version).
+
+ Args:
+ query: The user's question or task.
+ library_id: Context7 library ID (e.g., "/facebook/react").
+ type: Response format:
+ - "json": List of Documentation objects (default)
+ - "txt": Plain text documentation
+
+ Returns:
+ Documentation as list of Documentation objects (json) or string (txt).
+
+ Example:
+ ```python
+ # Get context as structured data (default)
+ docs = await client.get_context_async(
+ "How to use hooks", "/facebook/react"
+ )
+ for doc in docs:
+ print(f"{doc.title}: {doc.content[:100]}...")
+
+ # Get context as plain text
+ context = await client.get_context_async(
+ "How to use hooks",
+ "/facebook/react",
+ type="txt"
+ )
+ print(context)
+ ```
+ """
+ result = await self._http.request_async(
+ method="GET",
+ path=["v2", "context"],
+ query={
+ "query": query,
+ "libraryId": library_id,
+ "type": type,
+ },
+ )
+ return _process_context_response(result, type)
diff --git a/packages/python/sdk/src/context7/errors.py b/packages/python/sdk/src/context7/errors.py
new file mode 100644
index 000000000..f51c4f24f
--- /dev/null
+++ b/packages/python/sdk/src/context7/errors.py
@@ -0,0 +1,19 @@
+"""Custom exceptions for the Context7 SDK."""
+
+from __future__ import annotations
+
+
+class Context7Error(Exception):
+ """Base exception for Context7 SDK errors."""
+
+ def __init__(self, message: str) -> None:
+ self.message = message
+ super().__init__(message)
+
+
+class Context7APIError(Context7Error):
+ """Raised when the API returns an error response."""
+
+ def __init__(self, message: str, status_code: int | None = None) -> None:
+ self.status_code = status_code
+ super().__init__(message)
diff --git a/packages/python/sdk/src/context7/http.py b/packages/python/sdk/src/context7/http.py
new file mode 100644
index 000000000..7fa1a90bc
--- /dev/null
+++ b/packages/python/sdk/src/context7/http.py
@@ -0,0 +1,154 @@
+"""HTTP client with retry logic for Context7 API."""
+
+from __future__ import annotations
+
+import asyncio
+import math
+import time
+from typing import Any
+
+import httpx
+
+from context7.errors import Context7APIError
+
+
+class HttpClient:
+ """HTTP client with retry logic for Context7 API (sync and async)."""
+
+ def __init__(
+ self,
+ base_url: str,
+ headers: dict[str, str] | None = None,
+ retries: int = 5,
+ timeout: float = 30.0,
+ ) -> None:
+ """
+ Initialize the HTTP client.
+
+ Args:
+ base_url: Base URL for the API.
+ headers: Optional headers to include in all requests.
+ retries: Number of retry attempts for failed requests.
+ timeout: Request timeout in seconds.
+ """
+ self.base_url = base_url.rstrip("/")
+ self.headers = {"Content-Type": "application/json", **(headers or {})}
+ self.retries = retries
+ self.timeout = timeout
+ self._sync_client: httpx.Client | None = None
+ self._async_client: httpx.AsyncClient | None = None
+
+ def _get_sync_client(self) -> httpx.Client:
+ """Get or create the sync HTTP client."""
+ if self._sync_client is None or self._sync_client.is_closed:
+ self._sync_client = httpx.Client(
+ base_url=self.base_url,
+ headers=self.headers,
+ timeout=self.timeout,
+ )
+ return self._sync_client
+
+ async def _get_async_client(self) -> httpx.AsyncClient:
+ """Get or create the async HTTP client."""
+ if self._async_client is None or self._async_client.is_closed:
+ self._async_client = httpx.AsyncClient(
+ base_url=self.base_url,
+ headers=self.headers,
+ timeout=self.timeout,
+ )
+ return self._async_client
+
+ def close(self) -> None:
+ """Close the sync HTTP client connection."""
+ if self._sync_client is not None:
+ self._sync_client.close()
+ self._sync_client = None
+
+ async def close_async(self) -> None:
+ """Close the async HTTP client connection."""
+ if self._async_client is not None:
+ await self._async_client.aclose()
+ self._async_client = None
+
+ def _backoff(self, retry_count: int) -> float:
+ """Calculate exponential backoff delay (matches TS SDK)."""
+ return math.exp(retry_count) * 0.05
+
+ def request(
+ self,
+ method: str,
+ path: list[str] | None = None,
+ query: dict[str, Any] | None = None,
+ body: dict[str, Any] | None = None,
+ ) -> Any:
+ """Make a synchronous HTTP request with retry logic."""
+ url = "/".join([self.base_url, *(path or [])])
+ params = {k: v for k, v in (query or {}).items() if v is not None}
+ client = self._get_sync_client()
+ last_error: Exception | None = None
+
+ for attempt in range(self.retries + 1):
+ try:
+ response = client.request(
+ method=method,
+ url=url,
+ params=params if method == "GET" else None,
+ json=body if method == "POST" else None,
+ )
+ return self._handle_response(response)
+ except httpx.RequestError as e:
+ last_error = e
+ if attempt < self.retries:
+ time.sleep(self._backoff(attempt))
+ continue
+
+ raise last_error or Context7APIError("Exhausted all retries")
+
+ async def request_async(
+ self,
+ method: str,
+ path: list[str] | None = None,
+ query: dict[str, Any] | None = None,
+ body: dict[str, Any] | None = None,
+ ) -> Any:
+ """Make an asynchronous HTTP request with retry logic."""
+ url = "/".join([self.base_url, *(path or [])])
+ params = {k: v for k, v in (query or {}).items() if v is not None}
+ client = await self._get_async_client()
+ last_error: Exception | None = None
+
+ for attempt in range(self.retries + 1):
+ try:
+ response = await client.request(
+ method=method,
+ url=url,
+ params=params if method == "GET" else None,
+ json=body if method == "POST" else None,
+ )
+ return self._handle_response(response)
+ except httpx.RequestError as e:
+ last_error = e
+ if attempt < self.retries:
+ await asyncio.sleep(self._backoff(attempt))
+ continue
+
+ raise last_error or Context7APIError("Exhausted all retries")
+
+ def _handle_response(self, response: httpx.Response) -> Any:
+ """Handle HTTP response (shared between sync and async)."""
+ if not response.is_success:
+ try:
+ error_body = response.json()
+ message = (
+ error_body.get("error") or error_body.get("message") or response.reason_phrase
+ )
+ except (ValueError, KeyError, TypeError):
+ message = response.reason_phrase or "Unknown error"
+ raise Context7APIError(message, status_code=response.status_code)
+
+ content_type = response.headers.get("content-type", "")
+
+ if "application/json" in content_type:
+ return response.json()
+ else:
+ return response.text
diff --git a/packages/python/sdk/src/context7/models.py b/packages/python/sdk/src/context7/models.py
new file mode 100644
index 000000000..ff11706ae
--- /dev/null
+++ b/packages/python/sdk/src/context7/models.py
@@ -0,0 +1,91 @@
+"""Pydantic models for Context7 API request/response types."""
+
+from __future__ import annotations
+
+from pydantic import BaseModel, ConfigDict, Field
+
+
+class Library(BaseModel):
+ """A library available in Context7."""
+
+ id: str
+ """Context7 library ID (e.g., "/facebook/react")"""
+
+ name: str
+ """Library display name"""
+
+ description: str
+ """Library description"""
+
+ total_snippets: int = Field(default=0)
+ """Number of documentation snippets available"""
+
+ trust_score: float = Field(default=0.0)
+ """Source reputation score (0-10)"""
+
+ benchmark_score: float = Field(default=0.0)
+ """Quality indicator score (0-100)"""
+
+ versions: list[str] | None = None
+ """Available versions/tags"""
+
+
+class Documentation(BaseModel):
+ """A piece of documentation content."""
+
+ title: str
+ """Title of the documentation snippet"""
+
+ content: str
+ """The documentation content (may include code blocks in markdown format)"""
+
+ source: str
+ """Source URL or identifier for the snippet"""
+
+
+class ApiCodeSnippet(BaseModel):
+ """Internal: A code snippet from the API response."""
+
+ code_title: str = Field(alias="codeTitle")
+ code_description: str = Field(alias="codeDescription")
+ code_language: str = Field(alias="codeLanguage")
+ code_list: list[dict[str, str]] = Field(alias="codeList")
+ code_id: str = Field(alias="codeId")
+ code_tokens: int | None = Field(default=None, alias="codeTokens")
+ page_title: str | None = Field(default=None, alias="pageTitle")
+
+ model_config = ConfigDict(populate_by_name=True)
+
+
+class ApiInfoSnippet(BaseModel):
+ """Internal: An info snippet from the API response."""
+
+ content: str
+ breadcrumb: str | None = None
+ page_id: str = Field(alias="pageId")
+ content_tokens: int | None = Field(default=None, alias="contentTokens")
+
+ model_config = ConfigDict(populate_by_name=True)
+
+
+class ApiContextJsonResponse(BaseModel):
+ """Internal: JSON response from the context API."""
+
+ code_snippets: list[ApiCodeSnippet] = Field(alias="codeSnippets")
+ info_snippets: list[ApiInfoSnippet] = Field(alias="infoSnippets")
+
+ model_config = ConfigDict(populate_by_name=True)
+
+
+class ApiSearchResult(BaseModel):
+ """Internal: A search result from the API response."""
+
+ id: str
+ title: str
+ description: str
+ versions: list[str] | None = None
+ total_snippets: int | None = Field(default=None, alias="totalSnippets")
+ trust_score: float | None = Field(default=None, alias="trustScore")
+ benchmark_score: float | None = Field(default=None, alias="benchmarkScore")
+
+ model_config = ConfigDict(populate_by_name=True)
diff --git a/packages/python/sdk/src/context7/py.typed b/packages/python/sdk/src/context7/py.typed
new file mode 100644
index 000000000..e69de29bb
diff --git a/packages/python/sdk/tests/__init__.py b/packages/python/sdk/tests/__init__.py
new file mode 100644
index 000000000..8b6e6c7ec
--- /dev/null
+++ b/packages/python/sdk/tests/__init__.py
@@ -0,0 +1 @@
+"""Tests for Context7 Python SDK."""
diff --git a/packages/python/sdk/tests/conftest.py b/packages/python/sdk/tests/conftest.py
new file mode 100644
index 000000000..d4a8db62b
--- /dev/null
+++ b/packages/python/sdk/tests/conftest.py
@@ -0,0 +1,16 @@
+"""Pytest configuration and fixtures for Context7 SDK tests."""
+
+from pathlib import Path
+
+import pytest
+from dotenv import load_dotenv
+
+# Load .env from the python workspace root (packages/python/.env)
+# Path: sdk/tests/conftest.py -> sdk/tests -> sdk -> packages/python
+env_path = Path(__file__).parent.parent.parent / ".env"
+load_dotenv(env_path)
+
+
+def pytest_configure(config: pytest.Config) -> None:
+ """Configure pytest markers."""
+ config.addinivalue_line("markers", "asyncio: mark test as an async test")
diff --git a/packages/python/sdk/tests/test_client.py b/packages/python/sdk/tests/test_client.py
new file mode 100644
index 000000000..1742dc35b
--- /dev/null
+++ b/packages/python/sdk/tests/test_client.py
@@ -0,0 +1,264 @@
+"""Tests for the Context7 client."""
+
+import os
+
+import pytest
+from context7 import Context7, Context7Error, Documentation, Library
+
+
+@pytest.fixture(name="api_key")
+def api_key_fixture() -> str:
+ """Get API key from environment or skip test."""
+ key = os.environ.get("CONTEXT7_API_KEY")
+ if not key:
+ pytest.skip("CONTEXT7_API_KEY not set")
+ return key
+
+
+class TestContext7Init:
+ """Tests for Context7 client initialization."""
+
+ def test_init_with_api_key(self, api_key: str) -> None:
+ """Test client initialization with explicit API key."""
+ client = Context7(api_key=api_key)
+ assert client is not None
+ assert client._http is not None
+
+ def test_init_without_api_key_raises(self, monkeypatch: pytest.MonkeyPatch) -> None:
+ """Test that missing API key raises an error."""
+ monkeypatch.delenv("CONTEXT7_API_KEY", raising=False)
+ with pytest.raises(Context7Error, match="API key is required"):
+ Context7()
+
+ def test_init_from_env(self, api_key: str, monkeypatch: pytest.MonkeyPatch) -> None:
+ """Test client initialization from environment variable."""
+ monkeypatch.setenv("CONTEXT7_API_KEY", api_key)
+ client = Context7()
+ assert client is not None
+ assert client._http is not None
+
+ def test_init_with_invalid_prefix_warns(self, monkeypatch: pytest.MonkeyPatch) -> None:
+ """Test that invalid API key prefix produces a warning."""
+ monkeypatch.delenv("CONTEXT7_API_KEY", raising=False)
+ with pytest.warns(UserWarning, match="API key should start with"):
+ Context7(api_key="invalid_key")
+
+ def test_init_with_custom_base_url(self, api_key: str) -> None:
+ """Test client initialization with custom base URL."""
+ custom_url = "https://custom.context7.com/api"
+ client = Context7(api_key=api_key, base_url=custom_url)
+ assert client is not None
+ assert client._http.base_url == custom_url
+
+
+class TestSearchLibrarySync:
+ """Tests for the synchronous search_library method."""
+
+ def test_search_library(self, api_key: str) -> None:
+ """Test basic library search (sync)."""
+ with Context7(api_key=api_key) as client:
+ libraries = client.search_library("I need a UI library", "react")
+ assert libraries is not None
+ assert len(libraries) > 0
+ assert isinstance(libraries, list)
+
+ def test_search_library_result_fields(self, api_key: str) -> None:
+ """Test that search results have expected fields (sync)."""
+ with Context7(api_key=api_key) as client:
+ libraries = client.search_library("I need a UI library", "react")
+ assert len(libraries) > 0
+ lib = libraries[0]
+ assert isinstance(lib, Library)
+ assert lib.id is not None
+ assert isinstance(lib.id, str)
+ assert lib.name is not None
+ assert isinstance(lib.name, str)
+ assert lib.description is not None
+ assert lib.total_snippets >= 0
+ assert lib.trust_score >= 0
+ assert lib.benchmark_score >= 0
+
+ def test_search_library_different_queries(self, api_key: str) -> None:
+ """Test search with different queries returns different results."""
+ with Context7(api_key=api_key) as client:
+ react_results = client.search_library("I need a UI library", "react")
+ vue_results = client.search_library("I need a UI library", "vue")
+ assert react_results[0].id != vue_results[0].id
+
+ def test_search_library_specific_query(self, api_key: str) -> None:
+ """Test search with specific query returns relevant results."""
+ with Context7(api_key=api_key) as client:
+ libraries = client.search_library("I need a server framework", "nextjs")
+ assert libraries is not None
+ assert len(libraries) > 0
+ # Results should contain nextjs-related libraries
+ names = [lib.name.lower() for lib in libraries]
+ assert any("next" in n for n in names)
+
+
+class TestSearchLibraryAsync:
+ """Tests for the asynchronous search_library_async method."""
+
+ @pytest.mark.asyncio
+ async def test_search_library_async(self, api_key: str) -> None:
+ """Test basic library search (async)."""
+ async with Context7(api_key=api_key) as client:
+ libraries = await client.search_library_async("I need a UI library", "react")
+ assert libraries is not None
+ assert len(libraries) > 0
+ assert isinstance(libraries, list)
+
+ @pytest.mark.asyncio
+ async def test_search_library_async_result_fields(self, api_key: str) -> None:
+ """Test that search results have expected fields (async)."""
+ async with Context7(api_key=api_key) as client:
+ libraries = await client.search_library_async("I need a UI library", "react")
+ assert len(libraries) > 0
+ lib = libraries[0]
+ assert isinstance(lib, Library)
+ assert lib.id is not None
+ assert isinstance(lib.id, str)
+ assert lib.name is not None
+ assert isinstance(lib.name, str)
+ assert lib.description is not None
+ assert lib.total_snippets >= 0
+ assert lib.trust_score >= 0
+ assert lib.benchmark_score >= 0
+
+
+class TestGetContextSync:
+ """Tests for the synchronous get_context method."""
+
+ def test_get_context_json_default(self, api_key: str) -> None:
+ """Test getting context as JSON (default, sync)."""
+ with Context7(api_key=api_key) as client:
+ docs = client.get_context("How to use hooks", "/facebook/react")
+ assert isinstance(docs, list)
+ assert len(docs) > 0
+
+ def test_get_context_text(self, api_key: str) -> None:
+ """Test getting context as text (sync)."""
+ with Context7(api_key=api_key) as client:
+ context = client.get_context("How to use hooks", "/facebook/react", type="txt")
+ assert isinstance(context, str)
+ assert len(context) > 0
+
+ def test_get_context_json_fields(self, api_key: str) -> None:
+ """Test that JSON documentation has all expected fields."""
+ with Context7(api_key=api_key) as client:
+ docs = client.get_context("How to use hooks", "/facebook/react")
+ assert len(docs) > 0
+ doc = docs[0]
+ assert isinstance(doc, Documentation)
+ assert doc.title is not None
+ assert isinstance(doc.title, str)
+ assert doc.content is not None
+ assert isinstance(doc.content, str)
+ assert doc.source is not None
+ assert isinstance(doc.source, str)
+
+
+class TestGetContextAsync:
+ """Tests for the asynchronous get_context_async method."""
+
+ @pytest.mark.asyncio
+ async def test_get_context_async_json_default(self, api_key: str) -> None:
+ """Test getting context as JSON (default, async)."""
+ async with Context7(api_key=api_key) as client:
+ docs = await client.get_context_async("How to use hooks", "/facebook/react")
+ assert isinstance(docs, list)
+ assert len(docs) > 0
+
+ @pytest.mark.asyncio
+ async def test_get_context_async_text(self, api_key: str) -> None:
+ """Test getting context as text (async)."""
+ async with Context7(api_key=api_key) as client:
+ context = await client.get_context_async(
+ "How to use hooks", "/facebook/react", type="txt"
+ )
+ assert isinstance(context, str)
+ assert len(context) > 0
+
+ @pytest.mark.asyncio
+ async def test_get_context_async_json_fields(self, api_key: str) -> None:
+ """Test that JSON documentation has all expected fields (async)."""
+ async with Context7(api_key=api_key) as client:
+ docs = await client.get_context_async("How to use hooks", "/facebook/react")
+ assert len(docs) > 0
+ doc = docs[0]
+ assert isinstance(doc, Documentation)
+ assert doc.title is not None
+ assert isinstance(doc.title, str)
+ assert doc.content is not None
+ assert isinstance(doc.content, str)
+ assert doc.source is not None
+ assert isinstance(doc.source, str)
+
+
+class TestSyncContextManager:
+ """Tests for sync context manager behavior."""
+
+ def test_context_manager(self, api_key: str) -> None:
+ """Test that sync context manager properly opens and closes."""
+ with Context7(api_key=api_key) as client:
+ assert client is not None
+ libraries = client.search_library("I need a UI library", "react")
+ assert libraries is not None
+ assert len(libraries) > 0
+
+ def test_manual_close(self, api_key: str) -> None:
+ """Test manual close method (sync)."""
+ client = Context7(api_key=api_key)
+ libraries = client.search_library("I need a UI library", "react")
+ assert libraries is not None
+ assert len(libraries) > 0
+ client.close()
+ # Client should still exist but HTTP client should be closed
+ assert client._http._sync_client is None
+
+ def test_multiple_requests_same_client(self, api_key: str) -> None:
+ """Test making multiple requests with same client."""
+ with Context7(api_key=api_key) as client:
+ libraries1 = client.search_library("I need a UI library", "react")
+ libraries2 = client.search_library("I need a UI library", "vue")
+ docs = client.get_context("How to use hooks", "/facebook/react")
+ assert libraries1 is not None
+ assert libraries2 is not None
+ assert docs is not None
+ assert isinstance(docs, list)
+
+
+class TestAsyncContextManager:
+ """Tests for async context manager behavior."""
+
+ @pytest.mark.asyncio
+ async def test_context_manager_async(self, api_key: str) -> None:
+ """Test that async context manager properly opens and closes."""
+ async with Context7(api_key=api_key) as client:
+ assert client is not None
+ libraries = await client.search_library_async("I need a UI library", "react")
+ assert libraries is not None
+ assert len(libraries) > 0
+
+ @pytest.mark.asyncio
+ async def test_manual_close_async(self, api_key: str) -> None:
+ """Test manual close method (async)."""
+ client = Context7(api_key=api_key)
+ libraries = await client.search_library_async("I need a UI library", "react")
+ assert libraries is not None
+ assert len(libraries) > 0
+ await client.close_async()
+ # Client should still exist but HTTP client should be closed
+ assert client._http._async_client is None
+
+ @pytest.mark.asyncio
+ async def test_multiple_requests_same_client_async(self, api_key: str) -> None:
+ """Test making multiple async requests with same client."""
+ async with Context7(api_key=api_key) as client:
+ libraries1 = await client.search_library_async("I need a UI library", "react")
+ libraries2 = await client.search_library_async("I need a UI library", "vue")
+ docs = await client.get_context_async("How to use hooks", "/facebook/react")
+ assert libraries1 is not None
+ assert libraries2 is not None
+ assert docs is not None
+ assert isinstance(docs, list)
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index dee51e928..4fb638d6d 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -1,2 +1,3 @@
packages:
- "packages/*"
+ - "!packages/python"