Skip to main content

A modern annotation-based library for registering FastAPI services with Consul service discovery

Project description

Consul Registration Library for FastAPI

A modern, decorator-based library for registering FastAPI services with Consul service discovery. This library provides a Pythonic, annotation-driven approach to automatically register workers, indexers, and general services with Consul.

๐Ÿš€ Features

  • Decorator-Based Registration: Simple decorators like @register_worker, @register_indexer, @register_service
  • Automatic Route Discovery: Extracts base routes from FastAPI routers
  • Flexible Health Configuration: Support for separate access and health check networking
  • Zero Configuration: Sensible defaults with optional customization
  • FastAPI Native Integration: Uses FastAPI's lifespan events for registration
  • Type Safety: Full type hints and Pydantic models
  • Async Support: Built on async Consul client for optimal performance
  • Testing Support: Comprehensive testing utilities with testcontainers

๐Ÿ“ฆ Installation

pip install consul-registration

๐Ÿš€ Quick Start

1. Basic Usage

from fastapi import FastAPI, APIRouter
from consul_registration import register_worker, register_service, create_consul_lifespan

# Create your FastAPI app with Consul registration
app = FastAPI(lifespan=create_consul_lifespan)

# Register a worker service
worker_router = APIRouter(prefix="/api/workers/v1")

@register_worker("pdf-processor")
class PDFWorker:
    router = worker_router
    
    @router.post("/process")
    async def process_pdf(file_path: str):
        return {"status": "processing", "file": file_path}

# Register a general service
api_router = APIRouter(prefix="/api/v1")

@register_service("user-service")
class UserService:
    router = api_router
    
    @router.get("/users")
    async def get_users():
        return {"users": ["alice", "bob"]}

# Don't forget to include the routers!
app.include_router(worker_router)
app.include_router(api_router)

# Add health endpoint (required for Consul health checks)
@app.get("/health")
async def health():
    return {"status": "healthy"}

2. Configuration

Set environment variables for basic configuration:

# Basic configuration
export CONSUL_HOST=localhost
export CONSUL_PORT=8500
export ACCESS_HOST=my-service.local
export ACCESS_PORT=8000
export ENABLE_REGISTRATION=true

Or use a .env file:

# Consul connection
CONSUL_HOST=consul.local
CONSUL_PORT=8500

# How other services reach this service
ACCESS_HOST=my-service.public.local
ACCESS_PORT=443

# Optional: Separate health check network
HEALTH_HOST=my-service.internal.local
HEALTH_PORT=8000

# Enable registration
ENABLE_REGISTRATION=true

3. Advanced Configuration

For complex networking scenarios with separate health check endpoints:

from consul_registration import DiscoveryConfig, create_consul_lifespan

# Create custom configuration
config = DiscoveryConfig(
    consul__host="consul.internal",
    consul__port=8500,
    access__host="api.example.com",
    access__port=443,
    health__host="internal.example.com",
    health__port=8080,
    enable_registration=True
)

# Use with FastAPI
app = FastAPI(lifespan=lambda app: create_consul_lifespan(app, config))

๐Ÿ“‹ Decorator Reference

@register_worker

Registers a worker service that processes background tasks.

@register_worker(
    name="pdf-processor",              # Required: Unique service name
    base_route="/api/workers/v1",      # Optional: Override route extraction
    health_endpoint="/health",         # Optional: Health check path
    enabled=True,                      # Optional: Enable/disable registration
)
class WorkerService:
    pass

@register_indexer

Registers an indexer service that manages searchable content.

@register_indexer(
    name="search-indexer",
    base_route="/api/indexers/v1",
    health_endpoint="/health",
)
class SearchIndexer:
    pass

@register_service

Registers a general service (APIs, web services, etc.).

@register_service(
    name="user-service",
    base_route="/api/v1",
)
class UserAPI:
    pass

๐Ÿ”ง Configuration Options

Environment Variables

Variable Description Default Required
CONSUL_HOST Consul server hostname localhost No
CONSUL_PORT Consul server port 8500 No
ACCESS_HOST Public hostname for this service - Yes
ACCESS_PORT Public port for this service - Yes
HEALTH_HOST Health check hostname Uses ACCESS_HOST No
HEALTH_PORT Health check port Uses ACCESS_PORT No
ENABLE_REGISTRATION Enable Consul registration false No

Service Registration Details

Each registered service includes:

  • Service ID: Unique identifier ({name}-{uuid})
  • Tags: Service type (WORKER, INDEXER, SERVICE)
  • Base Route: Extracted from router or explicitly provided
  • Health Check: HTTP check with configurable interval
    • Check Interval: 15 seconds
    • Timeout: 10 seconds
    • Deregister after: 1 minute of failures

๐Ÿ—๏ธ Common Patterns

Pattern 1: Router-Based Services

from fastapi import APIRouter
from consul_registration import register_service

# Create a router
user_router = APIRouter(prefix="/api/users/v1")

# Decorate router endpoints
@register_service("user-service")
@user_router.get("/")
async def list_users():
    return {"users": []}

@user_router.post("/")
async def create_user(name: str):
    return {"user": {"name": name}}

# Include in app
app.include_router(user_router)

Pattern 2: Class-Based Services

from fastapi import APIRouter
from consul_registration import register_worker

@register_worker("data-processor")
class DataProcessor:
    def __init__(self):
        self.router = APIRouter(prefix="/api/workers/v1")
        self._setup_routes()
    
    def _setup_routes(self):
        @self.router.post("/process")
        async def process(data: dict):
            return {"processed": True}

# Create instance and include router
processor = DataProcessor()
app.include_router(processor.router)

Pattern 3: Multiple Services in One App

from consul_registration import register_worker, register_indexer, register_service

# Worker for background tasks
@register_worker("pdf-worker", base_route="/api/workers/pdf/v1")
class PDFWorker:
    router = APIRouter()

# Indexer for search functionality  
@register_indexer("document-indexer", base_route="/api/indexers/v1")
class DocumentIndexer:
    router = APIRouter()

# General API service
@register_service("api-gateway", base_route="/api/v1")
class APIGateway:
    router = APIRouter()

# Include all routers
for service in [PDFWorker(), DocumentIndexer(), APIGateway()]:
    app.include_router(service.router)

๐Ÿงช Testing

Running Tests

# Run all tests (requires Docker)
make test

# Run only unit tests (no Docker required)
make test-unit

# Run integration tests with Docker Consul
make test-integration

# Run CI integration tests (requires Consul on localhost:8500)
make test-ci

# Run formatting, linting, type checking, and all tests
make all

Unit Tests

import pytest
from fastapi import FastAPI
from consul_registration import register_service, get_service_registry

def test_service_registration():
    # Clear any existing registrations
    get_service_registry().clear()
    
    @register_service("test-service", base_route="/api/test/v1")
    class TestService:
        pass
    
    # Verify registration
    services = get_service_registry().get_all_services()
    assert len(services) == 1
    assert services[0].name == "test-service"
    assert services[0].base_route == "/api/test/v1"

Integration Tests

import pytest
from testcontainers.consul import ConsulContainer
from fastapi.testclient import TestClient

@pytest.mark.asyncio
async def test_consul_integration():
    # Start Consul container
    with ConsulContainer() as consul:
        consul_url = consul.get_consul_url()
        
        # Configure your app
        config = DiscoveryConfig(
            consul__host=consul.get_container_host_ip(),
            consul__port=consul.get_exposed_port(8500),
            access__host="localhost",
            access__port=8000,
            enable_registration=True
        )
        
        # Create app with test config
        app = FastAPI(lifespan=lambda app: create_consul_lifespan(app, config))
        
        # Test your app
        with TestClient(app) as client:
            # Verify service is registered
            response = client.get("/health")
            assert response.status_code == 200

Testing Library for Consumers

This library provides a comprehensive testing framework to help you verify your Consul registrations are working correctly. Install it with:

pip install consul-registration[testing]

Base Test Class

The ConsulRegistrationTestBase class provides a declarative way to test your service registrations:

from consul_registration.testing import ConsulRegistrationTestBase, ExpectedService
from fastapi import FastAPI

class TestMyAppRegistration(ConsulRegistrationTestBase):
    """Test that my application correctly registers with Consul."""
    
    def get_expected_services(self) -> list[ExpectedService]:
        """Define the services you expect to be registered."""
        return [
            ExpectedService.worker(
                name="pdf-processor",
                port=8000,
                version="1.0.0"
            ),
            ExpectedService.api_service(
                name="user-api",
                port=8000,
                tags={"api", "v1", "users"},
            ),
        ]
    
    def create_app(self) -> FastAPI:
        """Create your FastAPI application."""
        from my_app import app  # Import your app
        return app

The base class automatically tests:

  • โœ… All expected services are registered in Consul
  • โœ… Services have correct tags
  • โœ… Health checks are passing
  • โœ… Services are properly deregistered on shutdown

ExpectedService Models

Use factory methods for common service types:

# Worker service
ExpectedService.worker(
    name="data-processor",
    port=8000,
    version="2.0.0"
)

# Indexer service
ExpectedService.indexer(
    name="search-indexer",
    port=8000
)

# API service
ExpectedService.api_service(
    name="auth-service",
    port=8000,
    tags={"api", "v1", "auth", "security"}
)

# Custom service with all options
ExpectedService(
    name="custom-service",
    port=9000,
    host="custom.local",
    tags={"custom", "special"},
    health_check_passing=True,
    health_check_timeout=60.0
)

Test Container Support

The testing library includes a custom ConsulTestContainer for integration tests:

from consul_registration.testing import ConsulTestContainer

async def test_with_consul():
    async with ConsulTestContainer() as consul:
        # Get connection details
        consul_host = consul.get_consul_host()
        consul_port = consul.get_consul_port()
        
        # Wait for service registration
        await consul.wait_for_service_registration("my-service")
        
        # Check service health
        health = await consul.get_service_health("my-service")
        assert health == "passing"

Complete Example

See the example/tests directory for a complete example of using the testing library. The example demonstrates:

  • Testing multiple service types (worker, indexer, API)
  • Verifying tags
  • Ensuring disabled services aren't registered
  • Integration with pytest fixtures

To run the example tests:

cd example
pip install -e ../[testing]
pytest tests -v

๐Ÿ” Service Discovery

Query your registered services via Consul:

# List all services
curl http://localhost:8500/v1/catalog/services

# Get service details
curl http://localhost:8500/v1/catalog/service/pdf-processor

# Filter by tags
curl http://localhost:8500/v1/catalog/services?tag=WORKER
curl http://localhost:8500/v1/catalog/services?tag=INDEXER
curl http://localhost:8500/v1/catalog/services?tag=SERVICE

๐Ÿšจ Troubleshooting

Services Not Registering

  1. Check ENABLE_REGISTRATION=true is set
  2. Verify Consul is accessible at configured host/port
  3. Ensure ACCESS_HOST and ACCESS_PORT are configured
  4. Check logs for registration errors

Health Checks Failing

  1. Verify /health endpoint exists and returns 200
  2. Check health endpoint is accessible at configured host/port
  3. Ensure health check URL is correct in Consul UI

Route Extraction Issues

If automatic route extraction fails:

# Explicitly provide base_route
@register_service("my-service", base_route="/api/v1")
class MyService:
    pass

๐Ÿ“š Comparison with Java Library

This Python library provides equivalent functionality to the Java consul-registration-lib:

Feature Java Python
Decorators/Annotations @RegisterWorker @register_worker
Service Types โœ… Worker, Indexer, Service โœ… Worker, Indexer, Service
Route Inference โœ… From @Path โœ… From APIRouter
Health Checks โœ… Configurable โœ… Configurable
Async Support โœ… Vert.x โœ… asyncio
Configuration โœ… SmallRye Config โœ… Pydantic Settings
Testing โœ… Testcontainers โœ… Testcontainers

๐Ÿ› ๏ธ Development Setup

Prerequisites

  • Python 3.8 or higher
  • Docker (for running integration tests)
  • Make (optional, for using Makefile commands)

Setting Up Your Development Environment

  1. Clone the repository:

    git clone https://github.com/perceptic/consul-registration-python.git
    cd consul-registration-python
    
  2. Create a virtual environment:

    python -m venv venv
    source venv/bin/activate  # On Windows: venv\Scripts\activate
    
  3. Install the package in development mode:

    pip install -e ".[dev,test]"
    

Running Tests

  1. Run unit tests (no external dependencies required):

    make test-unit
    # Or directly: pytest tests/ -v -k "not integration"
    
  2. Run integration tests (requires Docker):

    make test-integration
    # Or directly: ./scripts/run_integration_tests.sh
    
  3. Run all tests:

    make test
    

Code Quality

  1. Format code with Black:

    make format
    # Or directly: black src/ tests/ examples/
    
  2. Run linting with Ruff:

    make lint
    # Or directly: ruff check src/ tests/ examples/
    
  3. Type checking with mypy:

    make type-check
    # Or directly: mypy src/ --ignore-missing-imports
    
  4. Run all quality checks and tests:

    make all
    

Testing Your Changes

  1. Test with the example application:

    cd examples
    # Start Consul
    docker run -d -p 8500:8500 --name consul-dev hashicorp/consul:latest
    
    # Set environment variables
    export ENABLE_REGISTRATION=true
    export ACCESS_HOST=localhost
    export ACCESS_PORT=8000
    
    # Run the example
    python example_app.py
    
    # Check Consul UI at http://localhost:8500/ui
    
  2. Manual integration testing:

    # Use the debug script to test registration
    python -c "
    import asyncio
    from consul_registration import register_service, DiscoveryConfig
    from consul_registration.service import ConsulRegistrationService
    
    @register_service('test-dev-service')
    class TestService: pass
    
    config = DiscoveryConfig(
        ENABLE_REGISTRATION=True,
        ACCESS__HOST='localhost',
        ACCESS__PORT=8000
    )
    
    async def test():
        service = ConsulRegistrationService(config)
        await service.register_services()
        print('Service registered! Check http://localhost:8500/ui')
        await asyncio.sleep(60)  # Keep registered for 60 seconds
        await service.deregister_services()
    
    asyncio.run(test())
    "
    

Project Structure

consul-registration-python/
โ”œโ”€โ”€ src/consul_registration/   # Main package source
โ”‚   โ”œโ”€โ”€ __init__.py
โ”‚   โ”œโ”€โ”€ config.py             # Configuration classes
โ”‚   โ”œโ”€โ”€ decorators.py         # Service registration decorators
โ”‚   โ”œโ”€โ”€ discovery.py          # Service registry
โ”‚   โ”œโ”€โ”€ models.py             # Data models
โ”‚   โ””โ”€โ”€ service.py            # Consul registration service
โ”œโ”€โ”€ tests/                    # Test suite
โ”‚   โ”œโ”€โ”€ test_*.py            # Unit tests
โ”‚   โ””โ”€โ”€ test_integration*.py  # Integration tests
โ”œโ”€โ”€ examples/                 # Example applications
โ”œโ”€โ”€ scripts/                  # Helper scripts
โ””โ”€โ”€ Makefile                 # Development commands

Making Changes

  1. Create a new branch for your changes
  2. Make your changes and add tests
  3. Ensure all tests pass (make test)
  4. Format and lint your code (make format lint)
  5. Commit your changes with a clear message

Publishing to PyPI

This package is automatically published to PyPI when a tag is pushed. The version is automatically extracted from the git tag.

To publish a new version:

# Create and push a tag (with or without 'v' prefix)
git tag v0.1.1  # or just 0.1.1
git push origin v0.1.1

The GitHub Actions workflow will:

  1. Extract the version from the tag (removing 'v' prefix if present)
  2. Update the VERSION file with the tag version
  3. Build the package
  4. Publish to PyPI

Note: Publishing requires the PYPI_TOKEN secret to be configured in the GitHub repository settings.

Debugging Tips

  • Enable debug logging:

    import logging
    logging.basicConfig(level=logging.DEBUG)
    
  • Check Consul API directly:

    # List all services
    curl http://localhost:8500/v1/catalog/services
    
    # Get service details
    curl http://localhost:8500/v1/catalog/service/your-service-name
    
  • View Consul logs:

    docker logs consul-dev
    

๐Ÿ“„ License

This software is proprietary and confidential. Copyright (c) 2025 Perceptic Technologies Ltd. All rights reserved.

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

consul_registration-0.1.0.tar.gz (36.1 kB view details)

Uploaded Source

Built Distribution

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

consul_registration-0.1.0-py3-none-any.whl (21.2 kB view details)

Uploaded Python 3

File details

Details for the file consul_registration-0.1.0.tar.gz.

File metadata

  • Download URL: consul_registration-0.1.0.tar.gz
  • Upload date:
  • Size: 36.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.12.9

File hashes

Hashes for consul_registration-0.1.0.tar.gz
Algorithm Hash digest
SHA256 a1cb5427cb1e21da64ddbd24e4e3def5056b72550b4c06f85a8f774759f7a423
MD5 de0198102159419306e517c9e0d50eee
BLAKE2b-256 9ac1eed9f326cb9aa721d363af2b67b42220f1ee915124398bceb47696b0c7ce

See more details on using hashes here.

File details

Details for the file consul_registration-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for consul_registration-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 b28528050a91aa9b70a77724221cd319cd99a10770efc3eccdef0e34eccda974
MD5 40a84aaebf179c30d6f2d650dc236e0c
BLAKE2b-256 9c60df85e16d1f5c984e640de90ff7a44a75fbfda5704e2dc4b926489e3a6092

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