espocrm-client


Nameespocrm-client JSON
Version 0.2.0 PyPI version JSON
download
home_pageNone
SummaryModern, type-safe and comprehensive EspoCRM API client library for Python
upload_time2025-08-20 16:19:38
maintainerNone
docs_urlNone
authorNone
requires_python>=3.8
licenseMIT
keywords espocrm api client crm rest api-client customer-relationship-management business-automation sales-management lead-management contact-management opportunity-management type-safe pydantic structured-logging
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # EspoCRM Python Client

<div align="center">

[![Python Version](https://img.shields.io/badge/python-3.8%2B-blue)](https://www.python.org)
[![License](https://img.shields.io/badge/license-MIT-green)](LICENSE)
[![EspoCRM](https://img.shields.io/badge/EspoCRM-7.0%2B-orange)](https://www.espocrm.com)

**Modern, Type-Safe, Production-Ready Python Client for EspoCRM API**

[Installation](#installation) โ€ข [Quick Start](#quick-start) โ€ข [Documentation](#documentation) โ€ข [Examples](#examples) โ€ข [API Reference](#api-reference)

</div>

---

## ๐Ÿ“‹ Table of Contents

- [Features](#features)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Authentication](#authentication)
- [CRUD Operations](#crud-operations)
- [Advanced Features](#advanced-features)
- [Error Handling](#error-handling)
- [Configuration](#configuration)
- [Testing](#testing)
- [API Reference](#api-reference)
- [Contributing](#contributing)
- [License](#license)

---

## โœจ Features

### Core Features
- ๐Ÿ” **Multiple Authentication Methods**: API Key, HMAC, Basic Auth
- ๐Ÿ“ **Full CRUD Operations**: Create, Read, Update, Delete, List, Search
- ๐Ÿ”„ **Automatic Retry Logic**: With exponential backoff
- ๐ŸŽฏ **Type Safety**: Full type hints and Pydantic models
- ๐Ÿš€ **Async Support**: Coming soon
- ๐Ÿ“Š **Bulk Operations**: Efficient batch processing
- ๐Ÿ” **Advanced Search**: Complex queries with SearchParams
- โšก **Connection Pooling**: Optimized HTTP connections
- ๐Ÿ›ก๏ธ **Comprehensive Error Handling**: Detailed exceptions
- ๐Ÿ“ˆ **Rate Limiting**: Built-in rate limit management
- ๐Ÿงช **Well Tested**: Extensive test coverage

### Architecture
- **Modular Design**: Separate modules for auth, models, utils
- **Interceptor Pattern**: Request/Response interceptors
- **Factory Pattern**: Entity factories
- **Builder Pattern**: Query builders
- **Strategy Pattern**: Authentication strategies

---

## ๐Ÿ“ฆ Installation

### From PyPI (Recommended)
```bash
pip install espocrm-python-client
```

### From Source
```bash
git clone https://github.com/yourusername/espocrm-client.git
cd espocrm-client
pip install -e .
```

### Development Installation
```bash
git clone https://github.com/yourusername/espocrm-client.git
cd espocrm-client
pip install -e ".[dev]"
```

### Requirements
- Python 3.8+
- requests >= 2.31.0
- pydantic >= 2.5.0
- structlog >= 23.2.0
- typing-extensions >= 4.8.0

---

## ๐Ÿš€ Quick Start

### Basic Usage

```python
from espocrm import EspoCRMClient
from espocrm.auth import APIKeyAuth
from espocrm.config import ClientConfig

# Configure authentication
auth = APIKeyAuth(api_key="your-api-key")

# Configure client
config = ClientConfig(
    base_url="https://your-espocrm.com",
    api_key="your-api-key",
    timeout=30,
    verify_ssl=True
)

# Create client
client = EspoCRMClient(
    base_url="https://your-espocrm.com",
    auth=auth,
    config=config
)

# Create a contact
contact = client.crud.create("Contact", {
    "firstName": "John",
    "lastName": "Doe",
    "emailAddress": "john.doe@example.com"
})

print(f"Created contact with ID: {contact.get_id()}")
```

### Using Context Manager

```python
from espocrm import EspoCRMClient
from espocrm.auth import APIKeyAuth
from espocrm.config import ClientConfig

config = ClientConfig(
    base_url="https://your-espocrm.com",
    api_key="your-api-key"
)

auth = APIKeyAuth(api_key="your-api-key")

with EspoCRMClient(config.base_url, auth, config) as client:
    # Client will be automatically closed after the block
    contacts = client.crud.list("Contact", max_size=10)
    for contact in contacts.list:
        print(f"{contact.firstName} {contact.lastName}")
```

---

## ๐Ÿ” Authentication

### API Key Authentication (Recommended)

```python
from espocrm.auth import APIKeyAuth

# Create API User in EspoCRM Admin Panel
auth = APIKeyAuth(api_key="your-api-key-from-espocrm")
```

### HMAC Authentication (Most Secure)

```python
from espocrm.auth import HMACAuth

auth = HMACAuth(
    api_key="your-api-key",
    secret_key="your-secret-key"
)
```

### Basic Authentication

```python
from espocrm.auth import BasicAuth

# Using password
auth = BasicAuth(
    username="admin",
    password="your-password"
)

# Using token (recommended over password)
auth = BasicAuth(
    username="admin",
    token="your-auth-token"
)
```

### Custom Authentication

```python
from espocrm.auth.base import AuthenticationBase

class CustomAuth(AuthenticationBase):
    def get_headers(self, method: str, uri: str) -> Dict[str, str]:
        return {
            "X-Custom-Auth": "your-custom-token"
        }
```

---

## ๐Ÿ“ CRUD Operations

### Create

```python
# Simple create
contact = client.crud.create("Contact", {
    "firstName": "Jane",
    "lastName": "Smith",
    "emailAddress": "jane.smith@example.com",
    "phoneNumber": "+1 555 123 4567",
    "title": "Sales Manager",
    "description": "VIP Customer"
})

# Using models
from espocrm.models.entities import Contact

contact_model = Contact(
    firstName="Jane",
    lastName="Smith",
    emailAddress="jane.smith@example.com"
)

contact = client.crud.create("Contact", contact_model)
```

### Read

```python
# Get by ID
contact = client.crud.read("Contact", "contact-id-here")

# Get specific fields only
contact = client.crud.read(
    "Contact", 
    "contact-id-here",
    select=["firstName", "lastName", "emailAddress"]
)

# Access data
print(contact.data.firstName)
print(contact.data.emailAddress)
```

### Update

```python
# Update specific fields
updated = client.crud.update(
    "Contact",
    "contact-id-here",
    {
        "title": "Senior Sales Manager",
        "phoneNumber": "+1 555 987 6543"
    }
)

# Full update (PUT)
updated = client.crud.update(
    "Contact",
    "contact-id-here",
    full_contact_data,
    partial=False  # Use PUT instead of PATCH
)
```

### Delete

```python
# Delete by ID
success = client.crud.delete("Contact", "contact-id-here")

if success:
    print("Contact deleted successfully")
```

### List

```python
# Simple list
contacts = client.crud.list("Contact", max_size=50)

for contact in contacts.list:
    print(f"{contact.firstName} {contact.lastName}")

# With pagination
contacts = client.crud.list(
    "Contact",
    offset=0,
    max_size=20,
    order_by="createdAt",
    order="desc"
)

print(f"Total contacts: {contacts.total}")
print(f"Current page: {len(contacts.list)}")
```

### Search

```python
from espocrm.models.search import SearchParams

# Simple search
search_params = SearchParams(
    query="john",
    maxSize=10
)

results = client.crud.search("Contact", search_params)

# Advanced search with filters
search_params = SearchParams()
search_params.add_equals("type", "Customer")
search_params.add_contains("name", "Corp")
search_params.add_greater_than("createdAt", "2024-01-01")
search_params.set_order("createdAt", "desc")
search_params.set_pagination(0, 50)

results = client.crud.search("Account", search_params)

# Using where clauses
from espocrm.models.search import equals, contains, in_array

search_params = SearchParams()
search_params.add_where_clause(equals("status", "Active"))
search_params.add_where_clause(contains("email", "@company.com"))
search_params.add_where_clause(in_array("type", ["Customer", "Partner"]))

results = client.crud.search("Contact", search_params)
```

---

## ๐Ÿ”ง Advanced Features

### Bulk Operations

```python
# Bulk create
contacts_data = [
    {"firstName": "Alice", "lastName": "Johnson"},
    {"firstName": "Bob", "lastName": "Williams"},
    {"firstName": "Charlie", "lastName": "Brown"}
]

result = client.crud.bulk_create("Contact", contacts_data)
print(f"Created: {result.successful}/{result.total}")

# Bulk update
updates = [
    {"id": "id1", "status": "Active"},
    {"id": "id2", "status": "Active"},
    {"id": "id3", "status": "Inactive"}
]

result = client.crud.bulk_update("Contact", updates)

# Bulk delete
ids = ["id1", "id2", "id3"]
result = client.crud.bulk_delete("Contact", ids)

# Check results
for item in result.results:
    if item["success"]:
        print(f"โœ“ Processed: {item['id']}")
    else:
        print(f"โœ— Failed: {item['id']} - {item.get('error')}")
```

### Related Records

```python
# Get account with contacts
account = client.crud.read("Account", "account-id")

# Get related contacts
contacts = client.crud.list(
    "Contact",
    where=[{
        "type": "equals",
        "attribute": "accountId",
        "value": "account-id"
    }]
)

# Link records
client.request(
    "POST",
    f"Account/{account_id}/contacts",
    json={"id": contact_id}
)

# Unlink records
client.request(
    "DELETE",
    f"Account/{account_id}/contacts/{contact_id}"
)
```

### Custom Entities

```python
# Work with custom entities
custom_record = client.crud.create("CustomEntity", {
    "name": "Custom Record",
    "customField1": "Value 1",
    "customField2": 123
})

# List custom entity records
records = client.crud.list("CustomEntity", max_size=100)
```

### Stream (Activity Feed)

```python
# Get stream
stream = client.request("GET", "Stream")

# Post to stream
client.request("POST", "Note", json={
    "post": "This is a note",
    "type": "Post",
    "parentType": "Account",
    "parentId": "account-id"
})
```

### File Attachments

```python
import base64

# Upload attachment
with open("document.pdf", "rb") as f:
    file_content = base64.b64encode(f.read()).decode()

attachment = client.request("POST", "Attachment", json={
    "name": "document.pdf",
    "type": "application/pdf",
    "file": file_content,
    "parentType": "Contact",
    "parentId": "contact-id"
})

# Download attachment
attachment_data = client.request("GET", f"Attachment/{attachment_id}")
file_content = base64.b64decode(attachment_data["file"])
```

### Request Interceptors

```python
# Add custom headers to all requests
def add_custom_header(request):
    request.headers["X-Custom-Header"] = "custom-value"
    return request

client.http_client.add_request_interceptor(add_custom_header)

# Log all responses
def log_response(response):
    print(f"Response: {response.status_code}")
    return response

client.http_client.add_response_interceptor(log_response)
```

### Rate Limiting

```python
# Configure rate limiting
config = ClientConfig(
    base_url="https://your-espocrm.com",
    api_key="your-api-key",
    rate_limit_per_minute=60  # Max 60 requests per minute
)

# Handle rate limit errors
from espocrm.exceptions import EspoCRMRateLimitError

try:
    contacts = client.crud.list("Contact")
except EspoCRMRateLimitError as e:
    print(f"Rate limited. Retry after: {e.retry_after} seconds")
    time.sleep(e.retry_after)
    contacts = client.crud.list("Contact")
```

---

## โš ๏ธ Error Handling

### Exception Hierarchy

```python
from espocrm.exceptions import (
    EspoCRMError,           # Base exception
    EspoCRMAPIError,        # API errors
    EspoCRMAuthenticationError,  # 401 errors
    EspoCRMForbiddenError,  # 403 errors
    EspoCRMNotFoundError,   # 404 errors
    EspoCRMValidationError, # 400 validation errors
    EspoCRMRateLimitError,  # 429 rate limit errors
    EspoCRMServerError,     # 5xx server errors
    EspoCRMConnectionError  # Connection errors
)
```

### Error Handling Examples

```python
from espocrm.exceptions import *

try:
    contact = client.crud.read("Contact", "invalid-id")
except EspoCRMNotFoundError:
    print("Contact not found")
except EspoCRMAuthenticationError:
    print("Authentication failed - check your API key")
except EspoCRMValidationError as e:
    print(f"Validation error: {e.message}")
    print(f"Fields: {e.validation_errors}")
except EspoCRMServerError as e:
    print(f"Server error: {e.status_code} - {e.message}")
except EspoCRMConnectionError:
    print("Connection failed - check network")
except EspoCRMError as e:
    print(f"General error: {e}")
```

### Retry on Errors

```python
from espocrm.utils.retry import retry_on_error

@retry_on_error(max_retries=3, delay=1.0)
def create_contact(client, data):
    return client.crud.create("Contact", data)

# Or use built-in retry
config = ClientConfig(
    base_url="https://your-espocrm.com",
    api_key="your-api-key",
    max_retries=3,
    retry_delay=1.0
)
```

---

## โš™๏ธ Configuration

### Client Configuration

```python
from espocrm.config import ClientConfig

config = ClientConfig(
    # Required
    base_url="https://your-espocrm.com",
    
    # Authentication (one of these)
    api_key="your-api-key",
    # OR
    username="admin",
    password="password",
    
    # Optional settings
    timeout=30,                    # Request timeout in seconds
    verify_ssl=True,               # SSL certificate verification
    max_retries=3,                 # Maximum retry attempts
    retry_delay=1.0,              # Delay between retries
    rate_limit_per_minute=None,   # Rate limiting
    user_agent="MyApp/1.0",       # Custom user agent
    extra_headers={                # Additional headers
        "X-Custom": "value"
    },
    pool_connections=10,           # Connection pool size
    pool_maxsize=10,              # Max pool size
    
    # Logging
    log_level="INFO",             # Logging level
    log_requests=False,           # Log all requests
    log_responses=False,          # Log all responses
)
```

### Environment Variables

```bash
# .env file
ESPO_URL=https://your-espocrm.com
ESPO_API_KEY=your-api-key
# OR
ESPO_USERNAME=admin
ESPO_PASSWORD=your-password

# Optional
ESPO_TIMEOUT=30
ESPO_VERIFY_SSL=true
ESPO_MAX_RETRIES=3
```

```python
import os
from dotenv import load_dotenv

load_dotenv()

config = ClientConfig(
    base_url=os.getenv("ESPO_URL"),
    api_key=os.getenv("ESPO_API_KEY")
)
```

### Logging Configuration

```python
import logging
import structlog

# Standard logging
logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)

# Structured logging with structlog
structlog.configure(
    processors=[
        structlog.stdlib.filter_by_level,
        structlog.stdlib.add_logger_name,
        structlog.stdlib.add_log_level,
        structlog.stdlib.PositionalArgumentsFormatter(),
        structlog.processors.TimeStamper(fmt="iso"),
        structlog.processors.StackInfoRenderer(),
        structlog.processors.format_exc_info,
        structlog.dev.ConsoleRenderer()
    ],
    context_class=dict,
    logger_factory=structlog.stdlib.LoggerFactory(),
    cache_logger_on_first_use=True,
)
```

---

## ๐Ÿงช Testing

### Running Tests

```bash
# Install test dependencies
pip install -r requirements-test.txt

# Run all tests
pytest

# Run with coverage
pytest --cov=espocrm --cov-report=html

# Run specific test
pytest tests/test_crud.py::TestContactCRUD::test_create_contact

# Run with verbose output
pytest -v -s
```

### Writing Tests

```python
import pytest
from unittest.mock import Mock, patch
from espocrm import EspoCRMClient

@pytest.fixture
def mock_client():
    """Create a mock client for testing"""
    with patch('espocrm.client.EspoCRMClient') as mock:
        client = mock.return_value
        client.crud.create.return_value = {"id": "test-id"}
        yield client

def test_create_contact(mock_client):
    """Test contact creation"""
    result = mock_client.crud.create("Contact", {
        "firstName": "Test"
    })
    
    assert result["id"] == "test-id"
    mock_client.crud.create.assert_called_once()
```

### Integration Tests

```python
# tests/integration/test_real_api.py
import os
import pytest
from espocrm import EspoCRMClient
from espocrm.auth import APIKeyAuth
from espocrm.config import ClientConfig

@pytest.mark.integration
def test_real_api_connection():
    """Test real API connection"""
    config = ClientConfig(
        base_url=os.getenv("ESPO_TEST_URL"),
        api_key=os.getenv("ESPO_TEST_API_KEY")
    )
    
    auth = APIKeyAuth(api_key=os.getenv("ESPO_TEST_API_KEY"))
    
    with EspoCRMClient(config.base_url, auth, config) as client:
        # Test connection
        result = client.test_connection()
        assert result is True
        
        # Test CRUD
        contact = client.crud.create("Contact", {
            "firstName": "Test",
            "lastName": "User"
        })
        assert contact.get_id() is not None
        
        # Cleanup
        client.crud.delete("Contact", contact.get_id())
```

---

## ๐Ÿ“š API Reference

### Main Classes

#### EspoCRMClient
Main client class for interacting with EspoCRM API.

```python
class EspoCRMClient:
    def __init__(self, base_url: str, auth: AuthenticationBase, config: ClientConfig = None)
    def test_connection(self) -> bool
    def get_server_info(self) -> Dict[str, Any]
    def request(self, method: str, endpoint: str, **kwargs) -> Dict[str, Any]
    def close(self)
```

#### CrudClient
CRUD operations client.

```python
class CrudClient:
    def create(self, entity_type: str, data: Union[Dict, EntityRecord]) -> EntityResponse
    def read(self, entity_type: str, entity_id: str, select: List[str] = None) -> EntityResponse
    def update(self, entity_type: str, entity_id: str, data: Union[Dict, EntityRecord], partial: bool = True) -> EntityResponse
    def delete(self, entity_type: str, entity_id: str) -> bool
    def list(self, entity_type: str, search_params: SearchParams = None, **kwargs) -> ListResponse
    def search(self, entity_type: str, search_params: SearchParams) -> ListResponse
    def bulk_create(self, entity_type: str, entities: List[Union[Dict, EntityRecord]]) -> BulkOperationResult
    def bulk_update(self, entity_type: str, updates: List[Dict], partial: bool = True) -> BulkOperationResult
    def bulk_delete(self, entity_type: str, entity_ids: List[str]) -> BulkOperationResult
    def count(self, entity_type: str, where: List[Dict] = None) -> int
    def exists(self, entity_type: str, entity_id: str) -> bool
```

#### SearchParams
Search parameters builder.

```python
class SearchParams:
    def __init__(self, query: str = None, maxSize: int = None, offset: int = None)
    def add_where_clause(self, clause: WhereClause)
    def add_equals(self, field: str, value: Any)
    def add_not_equals(self, field: str, value: Any)
    def add_contains(self, field: str, value: str)
    def add_starts_with(self, field: str, value: str)
    def add_ends_with(self, field: str, value: str)
    def add_greater_than(self, field: str, value: Any)
    def add_less_than(self, field: str, value: Any)
    def add_in_array(self, field: str, values: List[Any])
    def add_not_in_array(self, field: str, values: List[Any])
    def add_is_null(self, field: str)
    def add_is_not_null(self, field: str)
    def set_order(self, field: str, direction: str = "asc")
    def set_pagination(self, offset: int, limit: int)
    def to_query_params(self) -> Dict[str, Any]
```

### Models

#### EntityResponse
Response wrapper for single entity operations.

```python
class EntityResponse:
    data: Dict[str, Any]
    entity_type: str
    
    def get_id(self) -> str
    def get_field(self, field: str) -> Any
    def to_dict(self) -> Dict[str, Any]
```

#### ListResponse
Response wrapper for list operations.

```python
class ListResponse:
    list: List[Dict[str, Any]]
    total: int
    entity_type: str
    
    def get_entities(self) -> List[EntityRecord]
    def get_ids(self) -> List[str]
    def is_empty(self) -> bool
```

#### BulkOperationResult
Result of bulk operations.

```python
class BulkOperationResult:
    success: bool
    total: int
    successful: int
    failed: int
    results: List[Dict[str, Any]]
    errors: List[Dict[str, Any]] = None
```

### Authentication Classes

```python
# API Key Authentication
class APIKeyAuth(AuthenticationBase):
    def __init__(self, api_key: str)

# HMAC Authentication
class HMACAuth(AuthenticationBase):
    def __init__(self, api_key: str, secret_key: str)

# Basic Authentication
class BasicAuth(AuthenticationBase):
    def __init__(self, username: str, password: str = None, token: str = None)
```

### Exception Classes

```python
class EspoCRMError(Exception): ...
class EspoCRMAPIError(EspoCRMError): ...
class EspoCRMAuthenticationError(EspoCRMAPIError): ...  # 401
class EspoCRMForbiddenError(EspoCRMAPIError): ...       # 403
class EspoCRMNotFoundError(EspoCRMAPIError): ...        # 404
class EspoCRMValidationError(EspoCRMAPIError): ...      # 400
class EspoCRMRateLimitError(EspoCRMAPIError): ...       # 429
class EspoCRMServerError(EspoCRMAPIError): ...          # 5xx
class EspoCRMConnectionError(EspoCRMError): ...
```

---

## ๐Ÿ“˜ Examples

### Complete CRUD Example

```python
from espocrm import EspoCRMClient
from espocrm.auth import APIKeyAuth
from espocrm.config import ClientConfig
from espocrm.models.search import SearchParams
from espocrm.exceptions import EspoCRMNotFoundError

# Setup
config = ClientConfig(
    base_url="https://your-espocrm.com",
    api_key="your-api-key"
)
auth = APIKeyAuth(api_key="your-api-key")

with EspoCRMClient(config.base_url, auth, config) as client:
    # CREATE
    print("Creating contact...")
    contact = client.crud.create("Contact", {
        "firstName": "John",
        "lastName": "Doe",
        "emailAddress": "john.doe@example.com",
        "phoneNumber": "+1 555 123 4567",
        "title": "CEO",
        "addressStreet": "123 Main St",
        "addressCity": "New York",
        "addressCountry": "USA"
    })
    contact_id = contact.get_id()
    print(f"โœ“ Created contact: {contact_id}")
    
    # READ
    print("\nReading contact...")
    fetched = client.crud.read("Contact", contact_id)
    print(f"โœ“ Contact name: {fetched.data.firstName} {fetched.data.lastName}")
    
    # UPDATE
    print("\nUpdating contact...")
    updated = client.crud.update("Contact", contact_id, {
        "title": "CTO",
        "description": "Updated via API"
    })
    print(f"โœ“ Updated title to: {updated.data.title}")
    
    # SEARCH
    print("\nSearching contacts...")
    search = SearchParams(query="john", maxSize=5)
    results = client.crud.search("Contact", search)
    print(f"โœ“ Found {len(results.list)} contacts")
    
    # LIST
    print("\nListing all contacts...")
    all_contacts = client.crud.list("Contact", max_size=10)
    print(f"โœ“ Total contacts: {all_contacts.total}")
    
    # DELETE
    print("\nDeleting contact...")
    deleted = client.crud.delete("Contact", contact_id)
    print(f"โœ“ Contact deleted: {deleted}")
    
    # VERIFY DELETION
    print("\nVerifying deletion...")
    try:
        client.crud.read("Contact", contact_id)
        print("โœ— Contact still exists!")
    except EspoCRMNotFoundError:
        print("โœ“ Contact successfully deleted")
```

### Account with Contacts Example

```python
# Create account
account = client.crud.create("Account", {
    "name": "Acme Corporation",
    "website": "https://acme.com",
    "type": "Customer",
    "industry": "Technology"
})

# Create contacts for the account
contacts = []
for i in range(3):
    contact = client.crud.create("Contact", {
        "firstName": f"Employee {i+1}",
        "lastName": "Smith",
        "accountId": account.get_id(),
        "emailAddress": f"employee{i+1}@acme.com"
    })
    contacts.append(contact)

# Get account with related contacts
account_data = client.crud.read("Account", account.get_id())
related_contacts = client.crud.list(
    "Contact",
    where=[{
        "type": "equals",
        "attribute": "accountId",
        "value": account.get_id()
    }]
)

print(f"Account: {account_data.data.name}")
print(f"Contacts: {related_contacts.total}")
for contact in related_contacts.list:
    print(f"  - {contact.firstName} {contact.lastName}")
```

### Lead Conversion Example

```python
# Create a lead
lead = client.crud.create("Lead", {
    "firstName": "Jane",
    "lastName": "Prospect",
    "emailAddress": "jane@prospect.com",
    "companyName": "Prospect Inc",
    "title": "CEO",
    "status": "New"
})

# Update lead status
client.crud.update("Lead", lead.get_id(), {
    "status": "Assigned",
    "assignedUserId": "user-id"
})

# Convert lead to contact and account
conversion_result = client.request(
    "POST",
    f"Lead/{lead.get_id()}/convert",
    json={
        "createAccount": True,
        "createContact": True,
        "accountName": "Prospect Inc",
        "opportunityName": "New Opportunity"
    }
)

print(f"Lead converted:")
print(f"  Account ID: {conversion_result.get('accountId')}")
print(f"  Contact ID: {conversion_result.get('contactId')}")
print(f"  Opportunity ID: {conversion_result.get('opportunityId')}")
```

---

## ๐Ÿค Contributing

We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.

### Development Setup

```bash
# Clone repository
git clone https://github.com/yourusername/espocrm-client.git
cd espocrm-client

# Create virtual environment
python -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate

# Install in development mode
pip install -e ".[dev]"

# Run tests
pytest

# Run linting
flake8 espocrm
mypy espocrm

# Format code
black espocrm
isort espocrm
```

### Pull Request Process

1. Fork the repository
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request

---

## ๐Ÿ“„ License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

---

## ๐Ÿ™ Acknowledgments

- EspoCRM Team for the excellent CRM platform
- All contributors who have helped improve this library
- Python community for the amazing ecosystem

---

## ๐Ÿ“ž Support

- **Documentation**: [Full Documentation](https://espocrm-python-client.readthedocs.io)
- **Issues**: [GitHub Issues](https://github.com/yourusername/espocrm-client/issues)
- **Discussions**: [GitHub Discussions](https://github.com/yourusername/espocrm-client/discussions)
- **Email**: support@example.com

---

<div align="center">

**Made with โค๏ธ by the Open Source Community**

[โฌ† Back to Top](#espocrm-python-client)

</div>

            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "espocrm-client",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.8",
    "maintainer_email": "EspoCRM Python Client Team <support@espocrm-python-client.com>",
    "keywords": "espocrm, api, client, crm, rest, api-client, customer-relationship-management, business-automation, sales-management, lead-management, contact-management, opportunity-management, type-safe, pydantic, structured-logging",
    "author": null,
    "author_email": "EspoCRM Python Client Team <support@espocrm-python-client.com>",
    "download_url": "https://files.pythonhosted.org/packages/8d/13/1bfba531398eddc1d92ed8dfec2b21216b178fc996f7ace61c6cf0a10eff/espocrm_client-0.2.0.tar.gz",
    "platform": null,
    "description": "# EspoCRM Python Client\n\n<div align=\"center\">\n\n[![Python Version](https://img.shields.io/badge/python-3.8%2B-blue)](https://www.python.org)\n[![License](https://img.shields.io/badge/license-MIT-green)](LICENSE)\n[![EspoCRM](https://img.shields.io/badge/EspoCRM-7.0%2B-orange)](https://www.espocrm.com)\n\n**Modern, Type-Safe, Production-Ready Python Client for EspoCRM API**\n\n[Installation](#installation) \u2022 [Quick Start](#quick-start) \u2022 [Documentation](#documentation) \u2022 [Examples](#examples) \u2022 [API Reference](#api-reference)\n\n</div>\n\n---\n\n## \ud83d\udccb Table of Contents\n\n- [Features](#features)\n- [Installation](#installation)\n- [Quick Start](#quick-start)\n- [Authentication](#authentication)\n- [CRUD Operations](#crud-operations)\n- [Advanced Features](#advanced-features)\n- [Error Handling](#error-handling)\n- [Configuration](#configuration)\n- [Testing](#testing)\n- [API Reference](#api-reference)\n- [Contributing](#contributing)\n- [License](#license)\n\n---\n\n## \u2728 Features\n\n### Core Features\n- \ud83d\udd10 **Multiple Authentication Methods**: API Key, HMAC, Basic Auth\n- \ud83d\udcdd **Full CRUD Operations**: Create, Read, Update, Delete, List, Search\n- \ud83d\udd04 **Automatic Retry Logic**: With exponential backoff\n- \ud83c\udfaf **Type Safety**: Full type hints and Pydantic models\n- \ud83d\ude80 **Async Support**: Coming soon\n- \ud83d\udcca **Bulk Operations**: Efficient batch processing\n- \ud83d\udd0d **Advanced Search**: Complex queries with SearchParams\n- \u26a1 **Connection Pooling**: Optimized HTTP connections\n- \ud83d\udee1\ufe0f **Comprehensive Error Handling**: Detailed exceptions\n- \ud83d\udcc8 **Rate Limiting**: Built-in rate limit management\n- \ud83e\uddea **Well Tested**: Extensive test coverage\n\n### Architecture\n- **Modular Design**: Separate modules for auth, models, utils\n- **Interceptor Pattern**: Request/Response interceptors\n- **Factory Pattern**: Entity factories\n- **Builder Pattern**: Query builders\n- **Strategy Pattern**: Authentication strategies\n\n---\n\n## \ud83d\udce6 Installation\n\n### From PyPI (Recommended)\n```bash\npip install espocrm-python-client\n```\n\n### From Source\n```bash\ngit clone https://github.com/yourusername/espocrm-client.git\ncd espocrm-client\npip install -e .\n```\n\n### Development Installation\n```bash\ngit clone https://github.com/yourusername/espocrm-client.git\ncd espocrm-client\npip install -e \".[dev]\"\n```\n\n### Requirements\n- Python 3.8+\n- requests >= 2.31.0\n- pydantic >= 2.5.0\n- structlog >= 23.2.0\n- typing-extensions >= 4.8.0\n\n---\n\n## \ud83d\ude80 Quick Start\n\n### Basic Usage\n\n```python\nfrom espocrm import EspoCRMClient\nfrom espocrm.auth import APIKeyAuth\nfrom espocrm.config import ClientConfig\n\n# Configure authentication\nauth = APIKeyAuth(api_key=\"your-api-key\")\n\n# Configure client\nconfig = ClientConfig(\n    base_url=\"https://your-espocrm.com\",\n    api_key=\"your-api-key\",\n    timeout=30,\n    verify_ssl=True\n)\n\n# Create client\nclient = EspoCRMClient(\n    base_url=\"https://your-espocrm.com\",\n    auth=auth,\n    config=config\n)\n\n# Create a contact\ncontact = client.crud.create(\"Contact\", {\n    \"firstName\": \"John\",\n    \"lastName\": \"Doe\",\n    \"emailAddress\": \"john.doe@example.com\"\n})\n\nprint(f\"Created contact with ID: {contact.get_id()}\")\n```\n\n### Using Context Manager\n\n```python\nfrom espocrm import EspoCRMClient\nfrom espocrm.auth import APIKeyAuth\nfrom espocrm.config import ClientConfig\n\nconfig = ClientConfig(\n    base_url=\"https://your-espocrm.com\",\n    api_key=\"your-api-key\"\n)\n\nauth = APIKeyAuth(api_key=\"your-api-key\")\n\nwith EspoCRMClient(config.base_url, auth, config) as client:\n    # Client will be automatically closed after the block\n    contacts = client.crud.list(\"Contact\", max_size=10)\n    for contact in contacts.list:\n        print(f\"{contact.firstName} {contact.lastName}\")\n```\n\n---\n\n## \ud83d\udd10 Authentication\n\n### API Key Authentication (Recommended)\n\n```python\nfrom espocrm.auth import APIKeyAuth\n\n# Create API User in EspoCRM Admin Panel\nauth = APIKeyAuth(api_key=\"your-api-key-from-espocrm\")\n```\n\n### HMAC Authentication (Most Secure)\n\n```python\nfrom espocrm.auth import HMACAuth\n\nauth = HMACAuth(\n    api_key=\"your-api-key\",\n    secret_key=\"your-secret-key\"\n)\n```\n\n### Basic Authentication\n\n```python\nfrom espocrm.auth import BasicAuth\n\n# Using password\nauth = BasicAuth(\n    username=\"admin\",\n    password=\"your-password\"\n)\n\n# Using token (recommended over password)\nauth = BasicAuth(\n    username=\"admin\",\n    token=\"your-auth-token\"\n)\n```\n\n### Custom Authentication\n\n```python\nfrom espocrm.auth.base import AuthenticationBase\n\nclass CustomAuth(AuthenticationBase):\n    def get_headers(self, method: str, uri: str) -> Dict[str, str]:\n        return {\n            \"X-Custom-Auth\": \"your-custom-token\"\n        }\n```\n\n---\n\n## \ud83d\udcdd CRUD Operations\n\n### Create\n\n```python\n# Simple create\ncontact = client.crud.create(\"Contact\", {\n    \"firstName\": \"Jane\",\n    \"lastName\": \"Smith\",\n    \"emailAddress\": \"jane.smith@example.com\",\n    \"phoneNumber\": \"+1 555 123 4567\",\n    \"title\": \"Sales Manager\",\n    \"description\": \"VIP Customer\"\n})\n\n# Using models\nfrom espocrm.models.entities import Contact\n\ncontact_model = Contact(\n    firstName=\"Jane\",\n    lastName=\"Smith\",\n    emailAddress=\"jane.smith@example.com\"\n)\n\ncontact = client.crud.create(\"Contact\", contact_model)\n```\n\n### Read\n\n```python\n# Get by ID\ncontact = client.crud.read(\"Contact\", \"contact-id-here\")\n\n# Get specific fields only\ncontact = client.crud.read(\n    \"Contact\", \n    \"contact-id-here\",\n    select=[\"firstName\", \"lastName\", \"emailAddress\"]\n)\n\n# Access data\nprint(contact.data.firstName)\nprint(contact.data.emailAddress)\n```\n\n### Update\n\n```python\n# Update specific fields\nupdated = client.crud.update(\n    \"Contact\",\n    \"contact-id-here\",\n    {\n        \"title\": \"Senior Sales Manager\",\n        \"phoneNumber\": \"+1 555 987 6543\"\n    }\n)\n\n# Full update (PUT)\nupdated = client.crud.update(\n    \"Contact\",\n    \"contact-id-here\",\n    full_contact_data,\n    partial=False  # Use PUT instead of PATCH\n)\n```\n\n### Delete\n\n```python\n# Delete by ID\nsuccess = client.crud.delete(\"Contact\", \"contact-id-here\")\n\nif success:\n    print(\"Contact deleted successfully\")\n```\n\n### List\n\n```python\n# Simple list\ncontacts = client.crud.list(\"Contact\", max_size=50)\n\nfor contact in contacts.list:\n    print(f\"{contact.firstName} {contact.lastName}\")\n\n# With pagination\ncontacts = client.crud.list(\n    \"Contact\",\n    offset=0,\n    max_size=20,\n    order_by=\"createdAt\",\n    order=\"desc\"\n)\n\nprint(f\"Total contacts: {contacts.total}\")\nprint(f\"Current page: {len(contacts.list)}\")\n```\n\n### Search\n\n```python\nfrom espocrm.models.search import SearchParams\n\n# Simple search\nsearch_params = SearchParams(\n    query=\"john\",\n    maxSize=10\n)\n\nresults = client.crud.search(\"Contact\", search_params)\n\n# Advanced search with filters\nsearch_params = SearchParams()\nsearch_params.add_equals(\"type\", \"Customer\")\nsearch_params.add_contains(\"name\", \"Corp\")\nsearch_params.add_greater_than(\"createdAt\", \"2024-01-01\")\nsearch_params.set_order(\"createdAt\", \"desc\")\nsearch_params.set_pagination(0, 50)\n\nresults = client.crud.search(\"Account\", search_params)\n\n# Using where clauses\nfrom espocrm.models.search import equals, contains, in_array\n\nsearch_params = SearchParams()\nsearch_params.add_where_clause(equals(\"status\", \"Active\"))\nsearch_params.add_where_clause(contains(\"email\", \"@company.com\"))\nsearch_params.add_where_clause(in_array(\"type\", [\"Customer\", \"Partner\"]))\n\nresults = client.crud.search(\"Contact\", search_params)\n```\n\n---\n\n## \ud83d\udd27 Advanced Features\n\n### Bulk Operations\n\n```python\n# Bulk create\ncontacts_data = [\n    {\"firstName\": \"Alice\", \"lastName\": \"Johnson\"},\n    {\"firstName\": \"Bob\", \"lastName\": \"Williams\"},\n    {\"firstName\": \"Charlie\", \"lastName\": \"Brown\"}\n]\n\nresult = client.crud.bulk_create(\"Contact\", contacts_data)\nprint(f\"Created: {result.successful}/{result.total}\")\n\n# Bulk update\nupdates = [\n    {\"id\": \"id1\", \"status\": \"Active\"},\n    {\"id\": \"id2\", \"status\": \"Active\"},\n    {\"id\": \"id3\", \"status\": \"Inactive\"}\n]\n\nresult = client.crud.bulk_update(\"Contact\", updates)\n\n# Bulk delete\nids = [\"id1\", \"id2\", \"id3\"]\nresult = client.crud.bulk_delete(\"Contact\", ids)\n\n# Check results\nfor item in result.results:\n    if item[\"success\"]:\n        print(f\"\u2713 Processed: {item['id']}\")\n    else:\n        print(f\"\u2717 Failed: {item['id']} - {item.get('error')}\")\n```\n\n### Related Records\n\n```python\n# Get account with contacts\naccount = client.crud.read(\"Account\", \"account-id\")\n\n# Get related contacts\ncontacts = client.crud.list(\n    \"Contact\",\n    where=[{\n        \"type\": \"equals\",\n        \"attribute\": \"accountId\",\n        \"value\": \"account-id\"\n    }]\n)\n\n# Link records\nclient.request(\n    \"POST\",\n    f\"Account/{account_id}/contacts\",\n    json={\"id\": contact_id}\n)\n\n# Unlink records\nclient.request(\n    \"DELETE\",\n    f\"Account/{account_id}/contacts/{contact_id}\"\n)\n```\n\n### Custom Entities\n\n```python\n# Work with custom entities\ncustom_record = client.crud.create(\"CustomEntity\", {\n    \"name\": \"Custom Record\",\n    \"customField1\": \"Value 1\",\n    \"customField2\": 123\n})\n\n# List custom entity records\nrecords = client.crud.list(\"CustomEntity\", max_size=100)\n```\n\n### Stream (Activity Feed)\n\n```python\n# Get stream\nstream = client.request(\"GET\", \"Stream\")\n\n# Post to stream\nclient.request(\"POST\", \"Note\", json={\n    \"post\": \"This is a note\",\n    \"type\": \"Post\",\n    \"parentType\": \"Account\",\n    \"parentId\": \"account-id\"\n})\n```\n\n### File Attachments\n\n```python\nimport base64\n\n# Upload attachment\nwith open(\"document.pdf\", \"rb\") as f:\n    file_content = base64.b64encode(f.read()).decode()\n\nattachment = client.request(\"POST\", \"Attachment\", json={\n    \"name\": \"document.pdf\",\n    \"type\": \"application/pdf\",\n    \"file\": file_content,\n    \"parentType\": \"Contact\",\n    \"parentId\": \"contact-id\"\n})\n\n# Download attachment\nattachment_data = client.request(\"GET\", f\"Attachment/{attachment_id}\")\nfile_content = base64.b64decode(attachment_data[\"file\"])\n```\n\n### Request Interceptors\n\n```python\n# Add custom headers to all requests\ndef add_custom_header(request):\n    request.headers[\"X-Custom-Header\"] = \"custom-value\"\n    return request\n\nclient.http_client.add_request_interceptor(add_custom_header)\n\n# Log all responses\ndef log_response(response):\n    print(f\"Response: {response.status_code}\")\n    return response\n\nclient.http_client.add_response_interceptor(log_response)\n```\n\n### Rate Limiting\n\n```python\n# Configure rate limiting\nconfig = ClientConfig(\n    base_url=\"https://your-espocrm.com\",\n    api_key=\"your-api-key\",\n    rate_limit_per_minute=60  # Max 60 requests per minute\n)\n\n# Handle rate limit errors\nfrom espocrm.exceptions import EspoCRMRateLimitError\n\ntry:\n    contacts = client.crud.list(\"Contact\")\nexcept EspoCRMRateLimitError as e:\n    print(f\"Rate limited. Retry after: {e.retry_after} seconds\")\n    time.sleep(e.retry_after)\n    contacts = client.crud.list(\"Contact\")\n```\n\n---\n\n## \u26a0\ufe0f Error Handling\n\n### Exception Hierarchy\n\n```python\nfrom espocrm.exceptions import (\n    EspoCRMError,           # Base exception\n    EspoCRMAPIError,        # API errors\n    EspoCRMAuthenticationError,  # 401 errors\n    EspoCRMForbiddenError,  # 403 errors\n    EspoCRMNotFoundError,   # 404 errors\n    EspoCRMValidationError, # 400 validation errors\n    EspoCRMRateLimitError,  # 429 rate limit errors\n    EspoCRMServerError,     # 5xx server errors\n    EspoCRMConnectionError  # Connection errors\n)\n```\n\n### Error Handling Examples\n\n```python\nfrom espocrm.exceptions import *\n\ntry:\n    contact = client.crud.read(\"Contact\", \"invalid-id\")\nexcept EspoCRMNotFoundError:\n    print(\"Contact not found\")\nexcept EspoCRMAuthenticationError:\n    print(\"Authentication failed - check your API key\")\nexcept EspoCRMValidationError as e:\n    print(f\"Validation error: {e.message}\")\n    print(f\"Fields: {e.validation_errors}\")\nexcept EspoCRMServerError as e:\n    print(f\"Server error: {e.status_code} - {e.message}\")\nexcept EspoCRMConnectionError:\n    print(\"Connection failed - check network\")\nexcept EspoCRMError as e:\n    print(f\"General error: {e}\")\n```\n\n### Retry on Errors\n\n```python\nfrom espocrm.utils.retry import retry_on_error\n\n@retry_on_error(max_retries=3, delay=1.0)\ndef create_contact(client, data):\n    return client.crud.create(\"Contact\", data)\n\n# Or use built-in retry\nconfig = ClientConfig(\n    base_url=\"https://your-espocrm.com\",\n    api_key=\"your-api-key\",\n    max_retries=3,\n    retry_delay=1.0\n)\n```\n\n---\n\n## \u2699\ufe0f Configuration\n\n### Client Configuration\n\n```python\nfrom espocrm.config import ClientConfig\n\nconfig = ClientConfig(\n    # Required\n    base_url=\"https://your-espocrm.com\",\n    \n    # Authentication (one of these)\n    api_key=\"your-api-key\",\n    # OR\n    username=\"admin\",\n    password=\"password\",\n    \n    # Optional settings\n    timeout=30,                    # Request timeout in seconds\n    verify_ssl=True,               # SSL certificate verification\n    max_retries=3,                 # Maximum retry attempts\n    retry_delay=1.0,              # Delay between retries\n    rate_limit_per_minute=None,   # Rate limiting\n    user_agent=\"MyApp/1.0\",       # Custom user agent\n    extra_headers={                # Additional headers\n        \"X-Custom\": \"value\"\n    },\n    pool_connections=10,           # Connection pool size\n    pool_maxsize=10,              # Max pool size\n    \n    # Logging\n    log_level=\"INFO\",             # Logging level\n    log_requests=False,           # Log all requests\n    log_responses=False,          # Log all responses\n)\n```\n\n### Environment Variables\n\n```bash\n# .env file\nESPO_URL=https://your-espocrm.com\nESPO_API_KEY=your-api-key\n# OR\nESPO_USERNAME=admin\nESPO_PASSWORD=your-password\n\n# Optional\nESPO_TIMEOUT=30\nESPO_VERIFY_SSL=true\nESPO_MAX_RETRIES=3\n```\n\n```python\nimport os\nfrom dotenv import load_dotenv\n\nload_dotenv()\n\nconfig = ClientConfig(\n    base_url=os.getenv(\"ESPO_URL\"),\n    api_key=os.getenv(\"ESPO_API_KEY\")\n)\n```\n\n### Logging Configuration\n\n```python\nimport logging\nimport structlog\n\n# Standard logging\nlogging.basicConfig(\n    level=logging.DEBUG,\n    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'\n)\n\n# Structured logging with structlog\nstructlog.configure(\n    processors=[\n        structlog.stdlib.filter_by_level,\n        structlog.stdlib.add_logger_name,\n        structlog.stdlib.add_log_level,\n        structlog.stdlib.PositionalArgumentsFormatter(),\n        structlog.processors.TimeStamper(fmt=\"iso\"),\n        structlog.processors.StackInfoRenderer(),\n        structlog.processors.format_exc_info,\n        structlog.dev.ConsoleRenderer()\n    ],\n    context_class=dict,\n    logger_factory=structlog.stdlib.LoggerFactory(),\n    cache_logger_on_first_use=True,\n)\n```\n\n---\n\n## \ud83e\uddea Testing\n\n### Running Tests\n\n```bash\n# Install test dependencies\npip install -r requirements-test.txt\n\n# Run all tests\npytest\n\n# Run with coverage\npytest --cov=espocrm --cov-report=html\n\n# Run specific test\npytest tests/test_crud.py::TestContactCRUD::test_create_contact\n\n# Run with verbose output\npytest -v -s\n```\n\n### Writing Tests\n\n```python\nimport pytest\nfrom unittest.mock import Mock, patch\nfrom espocrm import EspoCRMClient\n\n@pytest.fixture\ndef mock_client():\n    \"\"\"Create a mock client for testing\"\"\"\n    with patch('espocrm.client.EspoCRMClient') as mock:\n        client = mock.return_value\n        client.crud.create.return_value = {\"id\": \"test-id\"}\n        yield client\n\ndef test_create_contact(mock_client):\n    \"\"\"Test contact creation\"\"\"\n    result = mock_client.crud.create(\"Contact\", {\n        \"firstName\": \"Test\"\n    })\n    \n    assert result[\"id\"] == \"test-id\"\n    mock_client.crud.create.assert_called_once()\n```\n\n### Integration Tests\n\n```python\n# tests/integration/test_real_api.py\nimport os\nimport pytest\nfrom espocrm import EspoCRMClient\nfrom espocrm.auth import APIKeyAuth\nfrom espocrm.config import ClientConfig\n\n@pytest.mark.integration\ndef test_real_api_connection():\n    \"\"\"Test real API connection\"\"\"\n    config = ClientConfig(\n        base_url=os.getenv(\"ESPO_TEST_URL\"),\n        api_key=os.getenv(\"ESPO_TEST_API_KEY\")\n    )\n    \n    auth = APIKeyAuth(api_key=os.getenv(\"ESPO_TEST_API_KEY\"))\n    \n    with EspoCRMClient(config.base_url, auth, config) as client:\n        # Test connection\n        result = client.test_connection()\n        assert result is True\n        \n        # Test CRUD\n        contact = client.crud.create(\"Contact\", {\n            \"firstName\": \"Test\",\n            \"lastName\": \"User\"\n        })\n        assert contact.get_id() is not None\n        \n        # Cleanup\n        client.crud.delete(\"Contact\", contact.get_id())\n```\n\n---\n\n## \ud83d\udcda API Reference\n\n### Main Classes\n\n#### EspoCRMClient\nMain client class for interacting with EspoCRM API.\n\n```python\nclass EspoCRMClient:\n    def __init__(self, base_url: str, auth: AuthenticationBase, config: ClientConfig = None)\n    def test_connection(self) -> bool\n    def get_server_info(self) -> Dict[str, Any]\n    def request(self, method: str, endpoint: str, **kwargs) -> Dict[str, Any]\n    def close(self)\n```\n\n#### CrudClient\nCRUD operations client.\n\n```python\nclass CrudClient:\n    def create(self, entity_type: str, data: Union[Dict, EntityRecord]) -> EntityResponse\n    def read(self, entity_type: str, entity_id: str, select: List[str] = None) -> EntityResponse\n    def update(self, entity_type: str, entity_id: str, data: Union[Dict, EntityRecord], partial: bool = True) -> EntityResponse\n    def delete(self, entity_type: str, entity_id: str) -> bool\n    def list(self, entity_type: str, search_params: SearchParams = None, **kwargs) -> ListResponse\n    def search(self, entity_type: str, search_params: SearchParams) -> ListResponse\n    def bulk_create(self, entity_type: str, entities: List[Union[Dict, EntityRecord]]) -> BulkOperationResult\n    def bulk_update(self, entity_type: str, updates: List[Dict], partial: bool = True) -> BulkOperationResult\n    def bulk_delete(self, entity_type: str, entity_ids: List[str]) -> BulkOperationResult\n    def count(self, entity_type: str, where: List[Dict] = None) -> int\n    def exists(self, entity_type: str, entity_id: str) -> bool\n```\n\n#### SearchParams\nSearch parameters builder.\n\n```python\nclass SearchParams:\n    def __init__(self, query: str = None, maxSize: int = None, offset: int = None)\n    def add_where_clause(self, clause: WhereClause)\n    def add_equals(self, field: str, value: Any)\n    def add_not_equals(self, field: str, value: Any)\n    def add_contains(self, field: str, value: str)\n    def add_starts_with(self, field: str, value: str)\n    def add_ends_with(self, field: str, value: str)\n    def add_greater_than(self, field: str, value: Any)\n    def add_less_than(self, field: str, value: Any)\n    def add_in_array(self, field: str, values: List[Any])\n    def add_not_in_array(self, field: str, values: List[Any])\n    def add_is_null(self, field: str)\n    def add_is_not_null(self, field: str)\n    def set_order(self, field: str, direction: str = \"asc\")\n    def set_pagination(self, offset: int, limit: int)\n    def to_query_params(self) -> Dict[str, Any]\n```\n\n### Models\n\n#### EntityResponse\nResponse wrapper for single entity operations.\n\n```python\nclass EntityResponse:\n    data: Dict[str, Any]\n    entity_type: str\n    \n    def get_id(self) -> str\n    def get_field(self, field: str) -> Any\n    def to_dict(self) -> Dict[str, Any]\n```\n\n#### ListResponse\nResponse wrapper for list operations.\n\n```python\nclass ListResponse:\n    list: List[Dict[str, Any]]\n    total: int\n    entity_type: str\n    \n    def get_entities(self) -> List[EntityRecord]\n    def get_ids(self) -> List[str]\n    def is_empty(self) -> bool\n```\n\n#### BulkOperationResult\nResult of bulk operations.\n\n```python\nclass BulkOperationResult:\n    success: bool\n    total: int\n    successful: int\n    failed: int\n    results: List[Dict[str, Any]]\n    errors: List[Dict[str, Any]] = None\n```\n\n### Authentication Classes\n\n```python\n# API Key Authentication\nclass APIKeyAuth(AuthenticationBase):\n    def __init__(self, api_key: str)\n\n# HMAC Authentication\nclass HMACAuth(AuthenticationBase):\n    def __init__(self, api_key: str, secret_key: str)\n\n# Basic Authentication\nclass BasicAuth(AuthenticationBase):\n    def __init__(self, username: str, password: str = None, token: str = None)\n```\n\n### Exception Classes\n\n```python\nclass EspoCRMError(Exception): ...\nclass EspoCRMAPIError(EspoCRMError): ...\nclass EspoCRMAuthenticationError(EspoCRMAPIError): ...  # 401\nclass EspoCRMForbiddenError(EspoCRMAPIError): ...       # 403\nclass EspoCRMNotFoundError(EspoCRMAPIError): ...        # 404\nclass EspoCRMValidationError(EspoCRMAPIError): ...      # 400\nclass EspoCRMRateLimitError(EspoCRMAPIError): ...       # 429\nclass EspoCRMServerError(EspoCRMAPIError): ...          # 5xx\nclass EspoCRMConnectionError(EspoCRMError): ...\n```\n\n---\n\n## \ud83d\udcd8 Examples\n\n### Complete CRUD Example\n\n```python\nfrom espocrm import EspoCRMClient\nfrom espocrm.auth import APIKeyAuth\nfrom espocrm.config import ClientConfig\nfrom espocrm.models.search import SearchParams\nfrom espocrm.exceptions import EspoCRMNotFoundError\n\n# Setup\nconfig = ClientConfig(\n    base_url=\"https://your-espocrm.com\",\n    api_key=\"your-api-key\"\n)\nauth = APIKeyAuth(api_key=\"your-api-key\")\n\nwith EspoCRMClient(config.base_url, auth, config) as client:\n    # CREATE\n    print(\"Creating contact...\")\n    contact = client.crud.create(\"Contact\", {\n        \"firstName\": \"John\",\n        \"lastName\": \"Doe\",\n        \"emailAddress\": \"john.doe@example.com\",\n        \"phoneNumber\": \"+1 555 123 4567\",\n        \"title\": \"CEO\",\n        \"addressStreet\": \"123 Main St\",\n        \"addressCity\": \"New York\",\n        \"addressCountry\": \"USA\"\n    })\n    contact_id = contact.get_id()\n    print(f\"\u2713 Created contact: {contact_id}\")\n    \n    # READ\n    print(\"\\nReading contact...\")\n    fetched = client.crud.read(\"Contact\", contact_id)\n    print(f\"\u2713 Contact name: {fetched.data.firstName} {fetched.data.lastName}\")\n    \n    # UPDATE\n    print(\"\\nUpdating contact...\")\n    updated = client.crud.update(\"Contact\", contact_id, {\n        \"title\": \"CTO\",\n        \"description\": \"Updated via API\"\n    })\n    print(f\"\u2713 Updated title to: {updated.data.title}\")\n    \n    # SEARCH\n    print(\"\\nSearching contacts...\")\n    search = SearchParams(query=\"john\", maxSize=5)\n    results = client.crud.search(\"Contact\", search)\n    print(f\"\u2713 Found {len(results.list)} contacts\")\n    \n    # LIST\n    print(\"\\nListing all contacts...\")\n    all_contacts = client.crud.list(\"Contact\", max_size=10)\n    print(f\"\u2713 Total contacts: {all_contacts.total}\")\n    \n    # DELETE\n    print(\"\\nDeleting contact...\")\n    deleted = client.crud.delete(\"Contact\", contact_id)\n    print(f\"\u2713 Contact deleted: {deleted}\")\n    \n    # VERIFY DELETION\n    print(\"\\nVerifying deletion...\")\n    try:\n        client.crud.read(\"Contact\", contact_id)\n        print(\"\u2717 Contact still exists!\")\n    except EspoCRMNotFoundError:\n        print(\"\u2713 Contact successfully deleted\")\n```\n\n### Account with Contacts Example\n\n```python\n# Create account\naccount = client.crud.create(\"Account\", {\n    \"name\": \"Acme Corporation\",\n    \"website\": \"https://acme.com\",\n    \"type\": \"Customer\",\n    \"industry\": \"Technology\"\n})\n\n# Create contacts for the account\ncontacts = []\nfor i in range(3):\n    contact = client.crud.create(\"Contact\", {\n        \"firstName\": f\"Employee {i+1}\",\n        \"lastName\": \"Smith\",\n        \"accountId\": account.get_id(),\n        \"emailAddress\": f\"employee{i+1}@acme.com\"\n    })\n    contacts.append(contact)\n\n# Get account with related contacts\naccount_data = client.crud.read(\"Account\", account.get_id())\nrelated_contacts = client.crud.list(\n    \"Contact\",\n    where=[{\n        \"type\": \"equals\",\n        \"attribute\": \"accountId\",\n        \"value\": account.get_id()\n    }]\n)\n\nprint(f\"Account: {account_data.data.name}\")\nprint(f\"Contacts: {related_contacts.total}\")\nfor contact in related_contacts.list:\n    print(f\"  - {contact.firstName} {contact.lastName}\")\n```\n\n### Lead Conversion Example\n\n```python\n# Create a lead\nlead = client.crud.create(\"Lead\", {\n    \"firstName\": \"Jane\",\n    \"lastName\": \"Prospect\",\n    \"emailAddress\": \"jane@prospect.com\",\n    \"companyName\": \"Prospect Inc\",\n    \"title\": \"CEO\",\n    \"status\": \"New\"\n})\n\n# Update lead status\nclient.crud.update(\"Lead\", lead.get_id(), {\n    \"status\": \"Assigned\",\n    \"assignedUserId\": \"user-id\"\n})\n\n# Convert lead to contact and account\nconversion_result = client.request(\n    \"POST\",\n    f\"Lead/{lead.get_id()}/convert\",\n    json={\n        \"createAccount\": True,\n        \"createContact\": True,\n        \"accountName\": \"Prospect Inc\",\n        \"opportunityName\": \"New Opportunity\"\n    }\n)\n\nprint(f\"Lead converted:\")\nprint(f\"  Account ID: {conversion_result.get('accountId')}\")\nprint(f\"  Contact ID: {conversion_result.get('contactId')}\")\nprint(f\"  Opportunity ID: {conversion_result.get('opportunityId')}\")\n```\n\n---\n\n## \ud83e\udd1d Contributing\n\nWe welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.\n\n### Development Setup\n\n```bash\n# Clone repository\ngit clone https://github.com/yourusername/espocrm-client.git\ncd espocrm-client\n\n# Create virtual environment\npython -m venv venv\nsource venv/bin/activate  # On Windows: venv\\Scripts\\activate\n\n# Install in development mode\npip install -e \".[dev]\"\n\n# Run tests\npytest\n\n# Run linting\nflake8 espocrm\nmypy espocrm\n\n# Format code\nblack espocrm\nisort espocrm\n```\n\n### Pull Request Process\n\n1. Fork the repository\n2. Create a feature branch (`git checkout -b feature/amazing-feature`)\n3. Commit your changes (`git commit -m 'Add amazing feature'`)\n4. Push to the branch (`git push origin feature/amazing-feature`)\n5. Open a Pull Request\n\n---\n\n## \ud83d\udcc4 License\n\nThis project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.\n\n---\n\n## \ud83d\ude4f Acknowledgments\n\n- EspoCRM Team for the excellent CRM platform\n- All contributors who have helped improve this library\n- Python community for the amazing ecosystem\n\n---\n\n## \ud83d\udcde Support\n\n- **Documentation**: [Full Documentation](https://espocrm-python-client.readthedocs.io)\n- **Issues**: [GitHub Issues](https://github.com/yourusername/espocrm-client/issues)\n- **Discussions**: [GitHub Discussions](https://github.com/yourusername/espocrm-client/discussions)\n- **Email**: support@example.com\n\n---\n\n<div align=\"center\">\n\n**Made with \u2764\ufe0f by the Open Source Community**\n\n[\u2b06 Back to Top](#espocrm-python-client)\n\n</div>\n",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "Modern, type-safe and comprehensive EspoCRM API client library for Python",
    "version": "0.2.0",
    "project_urls": {
        "Bug Reports": "https://github.com/espocrm/espocrm-client/issues",
        "Changelog": "https://github.com/espocrm/espocrm-client/blob/main/CHANGELOG.md",
        "Documentation": "https://espocrm-client.readthedocs.io",
        "Download": "https://pypi.org/project/espocrm-client/",
        "Feature Requests": "https://github.com/espocrm/espocrm-client/issues/new?template=feature_request.md",
        "Funding": "https://github.com/sponsors/espocrm",
        "Homepage": "https://github.com/espocrm/espocrm-client",
        "Issues": "https://github.com/espocrm/espocrm-client/issues",
        "Repository": "https://github.com/espocrm/espocrm-client.git",
        "Source Code": "https://github.com/espocrm/espocrm-client"
    },
    "split_keywords": [
        "espocrm",
        " api",
        " client",
        " crm",
        " rest",
        " api-client",
        " customer-relationship-management",
        " business-automation",
        " sales-management",
        " lead-management",
        " contact-management",
        " opportunity-management",
        " type-safe",
        " pydantic",
        " structured-logging"
    ],
    "urls": [
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "ac6dd653e33a96d41a143f7b70fb17af15705fe436156e7b87eac0c958b103e5",
                "md5": "dbdbdacd74d13df631222bb290432914",
                "sha256": "d07599f4a336681b7785651424a49dd9edab34be6a9503730b2d261511b2793e"
            },
            "downloads": -1,
            "filename": "espocrm_client-0.2.0-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "dbdbdacd74d13df631222bb290432914",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.8",
            "size": 88921,
            "upload_time": "2025-08-20T16:19:35",
            "upload_time_iso_8601": "2025-08-20T16:19:35.548938Z",
            "url": "https://files.pythonhosted.org/packages/ac/6d/d653e33a96d41a143f7b70fb17af15705fe436156e7b87eac0c958b103e5/espocrm_client-0.2.0-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "8d131bfba531398eddc1d92ed8dfec2b21216b178fc996f7ace61c6cf0a10eff",
                "md5": "2c9c36edf5aa9cf73f9e471ffa48be85",
                "sha256": "e260a7eb5b481164775e4d77ef838cc51a62de2255952604b3b8cfea09d2b859"
            },
            "downloads": -1,
            "filename": "espocrm_client-0.2.0.tar.gz",
            "has_sig": false,
            "md5_digest": "2c9c36edf5aa9cf73f9e471ffa48be85",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.8",
            "size": 4311616,
            "upload_time": "2025-08-20T16:19:38",
            "upload_time_iso_8601": "2025-08-20T16:19:38.117417Z",
            "url": "https://files.pythonhosted.org/packages/8d/13/1bfba531398eddc1d92ed8dfec2b21216b178fc996f7ace61c6cf0a10eff/espocrm_client-0.2.0.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2025-08-20 16:19:38",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "espocrm",
    "github_project": "espocrm-client",
    "github_not_found": true,
    "lcname": "espocrm-client"
}
        
Elapsed time: 1.65103s