Litestar Integration¶
Full plugin integration for Litestar 2+ with dependency injection, auto-generated routes, health checks, and DTOs.
Installation¶
Install with the Litestar extra:
pip install civicrm-py[litestar]
Plugin Setup¶
Basic Usage¶
Add the plugin to your Litestar application:
from litestar import Litestar, get
from civicrm_py.core.client import CiviClient
from civicrm_py.contrib.litestar import CiviPlugin
@get("/contacts")
async def list_contacts(civi_client: CiviClient) -> dict:
response = await civi_client.get("Contact", limit=10)
return {"contacts": response.values}
app = Litestar(
route_handlers=[list_contacts],
plugins=[CiviPlugin()],
)
The plugin:
Manages client lifecycle (startup/shutdown)
Provides dependency injection via type hints
Reads configuration from environment variables
With Configuration¶
Customize the plugin behavior with CiviPluginConfig:
from civicrm_py.contrib.litestar import CiviPlugin, CiviPluginConfig
config = CiviPluginConfig(
api_prefix="/api/v1/civi", # CRUD routes prefix
enable_health_check=True, # Enable /health/civi endpoint
include_entities=["Contact", "Activity"], # Limit auto-generated routes
)
app = Litestar(plugins=[CiviPlugin(config)])
Environment Variables¶
Set these environment variables for automatic configuration:
CIVI_BASE_URL=https://example.org/civicrm/ajax/api4
CIVI_API_KEY=your-api-key
CIVI_SITE_KEY=your-site-key # Optional
CIVI_TIMEOUT=30 # Default: 30 seconds
CIVI_VERIFY_SSL=true # Default: true
Dependency Injection¶
The plugin provides CiviClient via Litestar’s dependency injection. Declare the dependency using a type hint:
from litestar import get, post, delete
from civicrm_py.core.client import CiviClient
@get("/contacts")
async def list_contacts(civi_client: CiviClient) -> dict:
response = await civi_client.get("Contact", limit=25)
return {"contacts": response.values}
@get("/contacts/{contact_id:int}")
async def get_contact(contact_id: int, civi_client: CiviClient) -> dict:
response = await civi_client.get(
"Contact",
where=[["id", "=", contact_id]],
limit=1,
)
if not response.values:
from litestar.exceptions import NotFoundException
raise NotFoundException(f"Contact {contact_id} not found")
return response.values[0]
@post("/contacts")
async def create_contact(data: dict, civi_client: CiviClient) -> dict:
response = await civi_client.create("Contact", data)
return response.values[0]
@delete("/contacts/{contact_id:int}")
async def delete_contact(contact_id: int, civi_client: CiviClient) -> None:
await civi_client.delete("Contact", where=[["id", "=", contact_id]])
Manual Dependency Provider¶
For custom dependency logic:
from litestar import Litestar
from civicrm_py.contrib.litestar import provide_civi_client, CIVI_CLIENT_STATE_KEY
app = Litestar(
route_handlers=[...],
dependencies={"civi_client": provide_civi_client},
)
Auto-Generated Routes¶
When api_prefix is set, the plugin generates CRUD routes for CiviCRM entities:
config = CiviPluginConfig(api_prefix="/api/civi")
Generated routes for each entity:
Method |
Path |
Action |
|---|---|---|
GET |
|
List contacts |
GET |
|
Get single contact |
POST |
|
Create contact |
PUT |
|
Update contact |
DELETE |
|
Delete contact |
The same pattern applies to Activity, Contribution, Event, Membership, Participant, and Group.
Limiting Entities¶
Control which entities get routes:
# Only generate routes for specific entities
config = CiviPluginConfig(
api_prefix="/api/civi",
include_entities=["Contact", "Activity", "Contribution"],
)
Custom Controllers¶
Use the built-in controllers directly:
from litestar import Litestar
from civicrm_py.contrib.litestar import (
CiviPlugin,
ContactController,
ActivityController,
)
app = Litestar(
route_handlers=[ContactController, ActivityController],
plugins=[CiviPlugin()],
)
Create custom controllers:
from civicrm_py.contrib.litestar import create_entity_controller
# Create a controller for a custom entity
CustomEntityController = create_entity_controller(
entity_name="CustomEntity",
path="/custom-entities",
tags=["Custom"],
)
Health Checks¶
Enable the health check endpoint:
config = CiviPluginConfig(enable_health_check=True)
This adds GET /health/civi which returns:
{
"status": "healthy",
"civi_connected": true,
"response_time_ms": 45.2,
"api_version": "4",
"timestamp": "2025-01-22T10:30:00Z"
}
When unhealthy:
{
"status": "unhealthy",
"civi_connected": false,
"timestamp": "2025-01-22T10:30:00Z",
"error": "Connection refused"
}
Manual Health Check¶
Use the health check function in custom routes:
from litestar import get
from civicrm_py.contrib.litestar import civi_health_check, HealthCheckResponse
@get("/my-health")
async def custom_health(civi_client: CiviClient) -> HealthCheckResponse:
return await civi_health_check(civi_client)
DTOs¶
The integration provides msgspec DTOs for request/response validation.
Filter DTOs¶
from civicrm_py.contrib.litestar import ContactFilterDTO, ActivityFilterDTO
@get("/contacts")
async def list_contacts(
filters: ContactFilterDTO,
civi_client: CiviClient,
) -> list[ContactResponseDTO]:
# filters.contact_type, filters.is_deleted, etc.
...
Response DTOs¶
from civicrm_py.contrib.litestar import (
ContactResponseDTO,
ActivityResponseDTO,
ContributionResponseDTO,
APIResponseDTO,
)
@get("/contacts/{contact_id:int}")
async def get_contact(
contact_id: int,
civi_client: CiviClient,
) -> ContactResponseDTO:
response = await civi_client.get(
"Contact",
where=[["id", "=", contact_id]],
limit=1,
)
return ContactResponseDTO(**response.values[0])
Create/Update DTOs¶
from civicrm_py.contrib.litestar import ContactCreateDTO, ContactUpdateDTO
@post("/contacts")
async def create_contact(
data: ContactCreateDTO,
civi_client: CiviClient,
) -> ContactResponseDTO:
response = await civi_client.create("Contact", data.to_dict())
return ContactResponseDTO(**response.values[0])
Pagination DTO¶
from civicrm_py.contrib.litestar import PaginationDTO
@get("/contacts")
async def list_contacts(
pagination: PaginationDTO,
civi_client: CiviClient,
) -> APIResponseDTO:
response = await civi_client.get(
"Contact",
limit=pagination.limit,
offset=pagination.offset,
)
return APIResponseDTO(
values=response.values,
count=response.count,
count_fetched=response.countFetched,
)
Complete Example¶
from litestar import Litestar, get, post
from litestar.exceptions import NotFoundException
from civicrm_py.core.client import CiviClient
from civicrm_py.contrib.litestar import (
CiviPlugin,
CiviPluginConfig,
ContactCreateDTO,
ContactResponseDTO,
PaginationDTO,
)
@get("/api/contacts")
async def list_contacts(
pagination: PaginationDTO,
civi_client: CiviClient,
) -> dict:
response = await civi_client.get(
"Contact",
select=["id", "display_name", "email_primary.email"],
where=[["is_deleted", "=", False]],
limit=pagination.limit,
offset=pagination.offset,
)
return {
"contacts": response.values,
"count": response.count,
}
@get("/api/contacts/{contact_id:int}")
async def get_contact(
contact_id: int,
civi_client: CiviClient,
) -> ContactResponseDTO:
response = await civi_client.get(
"Contact",
where=[["id", "=", contact_id]],
limit=1,
)
if not response.values:
raise NotFoundException(f"Contact {contact_id} not found")
return ContactResponseDTO(**response.values[0])
@post("/api/contacts")
async def create_contact(
data: ContactCreateDTO,
civi_client: CiviClient,
) -> ContactResponseDTO:
response = await civi_client.create("Contact", {
"first_name": data.first_name,
"last_name": data.last_name,
"email_primary": data.email_primary,
"contact_type": data.contact_type,
})
return ContactResponseDTO(**response.values[0])
config = CiviPluginConfig(
enable_health_check=True,
)
app = Litestar(
route_handlers=[list_contacts, get_contact, create_contact],
plugins=[CiviPlugin(config)],
)
Manual Lifecycle Management¶
For fine-grained control over client lifecycle:
from litestar import Litestar
from civicrm_py.contrib.litestar import (
provide_civi_client,
cleanup_client,
CIVI_CLIENT_STATE_KEY,
)
from civicrm_py.core.client import CiviClient
from civicrm_py.core.config import CiviSettings
async def on_startup(app: Litestar) -> None:
settings = CiviSettings.from_env()
client = CiviClient(settings)
app.state[CIVI_CLIENT_STATE_KEY] = client
async def on_shutdown(app: Litestar) -> None:
await cleanup_client(app)
app = Litestar(
route_handlers=[...],
dependencies={"civi_client": provide_civi_client},
on_startup=[on_startup],
on_shutdown=[on_shutdown],
)
Available Controllers¶
Built-in controllers for common entities:
ContactControllerActivityControllerContributionControllerEventControllerMembershipControllerParticipantControllerGroupController
Access via:
from civicrm_py.contrib.litestar import (
ContactController,
ActivityController,
DEFAULT_CONTROLLERS,
get_entity_controllers,
)
# Get all default controllers
controllers = get_entity_controllers()
# Or use specific ones
app = Litestar(
route_handlers=[ContactController, ActivityController],
plugins=[CiviPlugin()],
)
CLI Commands¶
The plugin registers CLI commands with the Litestar CLI. These commands are available when running litestar with an application that uses CiviPlugin.
Check Command¶
Verify CiviCRM connection and configuration:
# Basic check
litestar civi check
# Verbose output with entity listing
litestar civi check --verbose
# Quiet mode (only errors)
litestar civi check --quiet
# Test with specific entity
litestar civi check --test-entity Activity
The check command validates:
Configuration (plugin settings or environment variables)
API connectivity
Entity accessibility
Available entities (with
--verbose)
Shell Command¶
Start an interactive Python shell with CiviCRM client pre-configured:
# Standard Python shell
litestar civi shell
# Use IPython if available
litestar civi shell --notebook
# Skip auto-imports
litestar civi shell --no-startup
The shell provides:
Pre-configured
client(SyncCiviClient) andsettingsobjectsAll entity classes imported (Contact, Activity, Contribution, etc.)
Query utilities (QuerySet, EntityManager)
Exception classes for error handling
Example session:
CiviCRM Interactive Shell
========================================
Entities: Activity, Address, Contact, Contribution, ...
Utilities: CiviClient, SyncCiviClient, EntityDiscovery, QuerySet
A sync client is available as 'client'.
Use 'settings' to view current configuration.
>>> result = client.get('Contact', limit=5)
>>> for contact in result.values:
... print(contact['display_name'])
Sync Command¶
Discover CiviCRM entities and generate Python stubs:
# List all available entities
litestar civi sync --list-entities
# Filter by entity type
litestar civi sync --list-entities --filter-type primary
# Show only custom entities
litestar civi sync --list-entities --custom-only
# Inspect a specific entity
litestar civi sync --entity Contact
# Show field definitions
litestar civi sync --entity Contact --show-fields
# Show available actions
litestar civi sync --entity Contact --show-actions
# Generate Python stub file
litestar civi sync --entity Custom_MyEntity --generate-stub
# Save stub to file
litestar civi sync --entity Custom_MyEntity --generate-stub --output my_entity.py
# Include readonly fields in stub
litestar civi sync --entity Contact --generate-stub --include-readonly
Generated stubs provide type-safe entity classes:
"""Auto-generated entity stub for CiviCRM.
Entity: Custom_MyEntity
Generated by: litestar civi sync --generate-stub
"""
from __future__ import annotations
from typing import Any
from civicrm_py.entities.base import BaseEntity
class Custom_MyEntity(BaseEntity):
"""CiviCRM Custom_MyEntity entity."""
__entity_name__ = "Custom_MyEntity"
id: int
name: str
description: str | None = None
SQLSpec Integration¶
Enable local database caching with the sqlspec integration. This requires the sqlspec extra:
pip install 'civicrm-py[litestar,sqlspec]'
Configuration¶
Add SQLSpec configuration to CiviPluginConfig:
from civicrm_py.contrib.litestar import CiviPlugin, CiviPluginConfig
from civicrm_py.contrib.litestar.sqlspec_integration import SQLSpecPluginConfig
from civicrm_py.contrib.sqlspec import CiviSQLSpecConfig
config = CiviPluginConfig(
sqlspec=SQLSpecPluginConfig(
enabled=True,
sqlspec_config=CiviSQLSpecConfig(
adapter="aiosqlite",
database="./civi_cache.db",
),
auto_run_migrations=True,
provide_repositories=True,
),
)
app = Litestar(plugins=[CiviPlugin(config)])
Using Repositories¶
When provide_repositories=True, repositories are injected automatically:
from typing import Annotated
from litestar import get
from litestar.params import Dependency
from civicrm_py.contrib.sqlspec import ContactRepository
@get("/cached-contacts")
async def list_cached_contacts(
contact_repository: Annotated[ContactRepository, Dependency(skip_validation=True)],
) -> dict:
async with contact_repository.get_session() as session:
contacts = await contact_repository.filter(session, is_deleted=False)
return {"contacts": [c.to_dict() for c in contacts]}
Available repository dependencies:
contact_repository: ContactRepositoryactivity_repository: ActivityRepositorycontribution_repository: ContributionRepositoryevent_repository: EventRepositorymembership_repository: MembershipRepository
Sync from Handlers¶
Sync CiviCRM data to local cache:
@post("/sync-contacts")
async def sync_contacts(
contact_repository: Annotated[ContactRepository, Dependency(skip_validation=True)],
civi_client: CiviClient,
) -> dict:
async with contact_repository.get_session() as session:
result = await contact_repository.sync_from_civi(
session,
civi_client,
is_deleted=False,
limit=500,
)
return {"synced": result.synced_count, "errors": result.error_count}
See SQLSpec Integration for complete sqlspec documentation.
Workflow Integration¶
Enable workflow automation with the workflows integration. This requires the workflows extra:
pip install 'civicrm-py[litestar,workflows]'
Configuration¶
Add workflow configuration to CiviPluginConfig:
from civicrm_py.contrib.litestar import CiviPlugin, CiviPluginConfig
from civicrm_py.contrib.litestar.workflows_integration import WorkflowPluginConfig
config = CiviPluginConfig(
workflows=WorkflowPluginConfig(
enabled=True,
api_prefix="/api/workflows",
register_builtins=True,
execution_timeout=300,
),
)
app = Litestar(plugins=[CiviPlugin(config)])
REST API Endpoints¶
When enabled, the workflow integration adds these endpoints:
Method |
Path |
Description |
|---|---|---|
GET |
|
List available workflow definitions |
POST |
|
Start a new workflow instance |
GET |
|
List workflow instances |
GET |
|
Get instance details and history |
POST |
|
Complete a pending human task |
GET |
|
List all pending human tasks |
Custom Workflows¶
Register custom workflows via the configuration:
from civicrm_py.contrib.workflows.templates import ContactSyncWorkflow
config = CiviPluginConfig(
workflows=WorkflowPluginConfig(
enabled=True,
api_prefix="/api/workflows",
register_builtins=True,
custom_workflows=[
ContactSyncWorkflow,
# Add your custom workflow classes here
],
),
)
See Workflow Automation for complete workflow documentation.
Combined Integration Example¶
Use all integrations together:
from litestar import Litestar, get, post
from civicrm_py.contrib.litestar import CiviPlugin, CiviPluginConfig
from civicrm_py.contrib.litestar.sqlspec_integration import SQLSpecPluginConfig
from civicrm_py.contrib.litestar.workflows_integration import WorkflowPluginConfig
from civicrm_py.contrib.sqlspec import CiviSQLSpecConfig
config = CiviPluginConfig(
# Health check endpoint
enable_health_check=True,
# Local database caching
sqlspec=SQLSpecPluginConfig(
enabled=True,
sqlspec_config=CiviSQLSpecConfig(
adapter="aiosqlite",
database="./civi_cache.db",
),
auto_run_migrations=True,
),
# Workflow automation
workflows=WorkflowPluginConfig(
enabled=True,
api_prefix="/api/workflows",
register_builtins=True,
),
)
app = Litestar(
route_handlers=[...],
plugins=[CiviPlugin(config)],
)