Skip to main content

Modern, type-safe and comprehensive EspoCRM API client library for Python

Project description

EspoCRM Python Client

Python Version License EspoCRM

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

InstallationQuick StartDocumentationExamplesAPI Reference


📋 Table of Contents


✨ 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)

pip install espocrm-python-client

From Source

git clone https://github.com/yourusername/espocrm-client.git
cd espocrm-client
pip install -e .

Development Installation

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

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

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)

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)

from espocrm.auth import HMACAuth

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

Basic Authentication

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

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

# 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

# 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

# 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

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

if success:
    print("Contact deleted successfully")

List

# 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

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

# 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

# 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

# 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)

# 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

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

# 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

# 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

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

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

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

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

# .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
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

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

# 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

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

# 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.

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.

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.

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.

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.

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.

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

Authentication Classes

# 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

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

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

# 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

# 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 for details.

Development Setup

# 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 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


Made with ❤️ by the Open Source Community

⬆ Back to Top

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

espocrm_client-0.2.1.tar.gz (101.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

espocrm_client-0.2.1-py3-none-any.whl (88.9 kB view details)

Uploaded Python 3

File details

Details for the file espocrm_client-0.2.1.tar.gz.

File metadata

  • Download URL: espocrm_client-0.2.1.tar.gz
  • Upload date:
  • Size: 101.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for espocrm_client-0.2.1.tar.gz
Algorithm Hash digest
SHA256 1433653bffb0c213ab520a99e350aa5a0b8806826c4c164251a7fb92fc8f76b1
MD5 25c7f39ee4489cb463fab62944348331
BLAKE2b-256 a69b300808db8599981c7b380215da2162d13ca8f9551027ab7e5cbd9dfc71cb

See more details on using hashes here.

File details

Details for the file espocrm_client-0.2.1-py3-none-any.whl.

File metadata

  • Download URL: espocrm_client-0.2.1-py3-none-any.whl
  • Upload date:
  • Size: 88.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for espocrm_client-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 2dd15fc03a911e6dd15770ab7c090344b589d47df436ffa5d631dcf26f97a8f3
MD5 298e06bd6a8172b275d2db780d80228d
BLAKE2b-256 24540f31a62602f50c29fad59162921aa9f4ea6636f47fe1034fe490166f4305

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page