Example Usage: Microservices Ecosystem
This guide demonstrates pyrig's power in a real-world scenario: building a standardized microservices ecosystem across multiple projects.
Scenario
You're building multiple Python microservices that need:
- Consistent standards across all services
- Custom shared configurations (logging, monitoring, security)
- Automatic synchronization when standards change
- Self-healing infrastructure via autouse fixtures
Architecture Overview
Dependency chain: pyrig → service-base → auth-service,
payment-service, notification-service
Step 1: Create Base Package
Create service-base that extends pyrig with shared standards:
# Create repository
gh repo create myorg/service-base --public # Or private if you want
git clone https://github.com/myorg/service-base.git
cd service-base
# Initialize with pyrig
uv init
uv add pyrig
uv run pyrig init
This creates a complete project structure with all pyrig's defaults.
Step 2: Add Custom Config File
Add a shared logging configuration that all microservices will inherit.
Create: service_base/rig/configs/logging_config.py
"""Shared logging configuration for all services."""
from pathlib import Path
from pyrig.rig.configs.base.base import ConfigDict
from pyrig.rig.configs.base.yaml import YamlConfigFile
class LoggingConfigFile(YamlConfigFile):
"""Logging configuration for all microservices."""
def parent_path(self) -> Path:
"""Place in config/ directory."""
return Path("config")
def _configs(self) -> ConfigDict:
"""Required logging configuration."""
return {
"logging": {
"version": 1,
"formatters": {
"json": {
"class": "pythonjsonlogger.jsonlogger.JsonFormatter",
"format": "%(name)s %(levelname)s %(message)s"
}
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "json",
"level": "INFO"
}
},
"root": {
"level": "INFO",
"handlers": ["console"]
}
}
}
What happens:
- File created at
config/logging.yamlwhen you runuv run pyrig mkroot - Automatically discovered via pyrig's ConfigFile discovery system
- Inherited by all services that depend on
service-base
Step 3: Override MkDocs Theme
Customize documentation theme with custom branding.
Create: service_base/rig/configs/docs/mkdocs.py
"""Custom-branded MkDocs configuration."""
from pathlib import Path
from pyrig.rig.configs.base.base import ConfigDict
from pyrig.rig.configs.docs.mkdocs import MkdocsConfigFile as BaseMkdocsCF
class MkdocsConfigFile(BaseMkdocsCF):
"""Custom-branded documentation theme."""
def _configs(self) -> ConfigDict:
"""Override theme with custom colors."""
config = super()._configs()
# Add custom branding
config["theme"]["palette"] = [
{
"scheme": "slate",
"primary": "deep purple", # Brand color
"accent": "amber", # Brand accent
"toggle": {
"icon": "material/brightness-4",
"name": "Light mode",
},
},
{
"scheme": "default",
"primary": "deep purple",
"accent": "amber",
"toggle": {
"icon": "material/brightness-7",
"name": "Dark mode",
},
},
]
# Add custom logo
config["theme"]["logo"] = "assets/logo.png"
config["theme"]["favicon"] = "assets/favicon.ico"
return config
Key mechanism:
- Subclass pyrig's
MkdocsConfigFilewith the same name - Leaf class override: pyrig's discovery system automatically keeps only the most-derived (leaf) class when multiple classes are found in the inheritance chain
- All microservices automatically get custom branding
Step 4: Adjust Pyproject Settings
Add shared dependencies and settings.
Create: service_base/rig/configs/pyproject.py
"""Base pyproject.toml with additional dependencies."""
from pyrig.rig.configs.base.base import ConfigDict
from pyrig.rig.configs.pyproject import PyprojectConfigFile as BasePyprojectCF
class PyprojectConfigFile(BasePyprojectCF):
"""Base pyproject with monitoring and logging."""
def dependencies(self) -> list[str]:
"""Add shared runtime dependencies."""
deps = super().dependencies()
return [
*deps,
"python-json-logger>=2.0.0", # JSON logging
"prometheus-client>=0.19.0", # Metrics
"sentry-sdk>=1.40.0", # Error tracking
]
def _configs(self) -> ConfigDict:
"""Add shared tool configs."""
config = super()._configs()
# Add custom ruff rules for shared standards
config["tool"]["ruff"]["lint"]["ignore"].extend([
"T201", # Allow print statements in microservices
])
# Adjust coverage threshold for microservices
config["tool"]["pytest"]["ini_options"]["addopts"] = (
"--cov=. --cov-report=term-missing --cov-fail-under=85"
)
return config
What this does:
- Adds dependencies to all microservices automatically
- Adjusts linting rules across all services
- Lowers coverage to 85% (more realistic for microservices)
Step 5: Create First Microservice
Now create auth-service that depends on service-base:
# Create repository
gh repo create myorg/auth-service
git clone https://github.com/myorg/auth-service.git
cd auth-service
# Initialize with pyrig
uv init
# service base brings pyrig bc it is build with it
uv add service-base
uv run pyrig init
Note: If your repos are private you will need tokens and stuff to do uv add. If you manage a package ecosystem for pypi then you can use uv add like usual
What gets created automatically:
auth-service/
├── config/
│ └── logging.yaml # ✓ From service-base
├── mkdocs.yml # ✓ Custom-branded theme
├── pyproject.toml # ✓ With shared dependencies
├── auth_service/
│ ├── rig/
│ │ ├── configs/ # ✓ Can add service-specific configs
│ │ ├── cli/ # ✓ Service-specific commands
│ │ └── tests/ # ✓ Service-specific fixtures
│ └── src/ # ✓ Your auth logic here
└── tests/ # ✓ Mirrored structure
See the full generated project tree at: Getting Started Documentation
All shared standards applied automatically!
Step 6: The Magic - Automatic Synchronization
Here's where pyrig shines. When you update service-base, all services heal
themselves.
Update Shared Standards
In service-base, add a security requirement:
Update: service_base/rig/configs/pyproject.py
def dependencies(self) -> list[str]:
"""Add shared runtime dependencies."""
deps = super().dependencies()
return [
*deps,
"python-json-logger>=2.0.0",
"prometheus-client>=0.19.0",
"sentry-sdk>=1.40.0",
"cryptography>=42.0.0", # NEW: Shared security requirement
]
Commit and release:
cd service-base
git add .
git commit -m "Add cryptography requirement"
git push
# GitHub Actions automatically releases new version
Services Auto-Heal
In auth-service:
# Update service-base dependency
uv add service-base --upgrade
# Run tests (triggers autouse fixtures)
uv run pytest
# or just do
uv run pyrig mkroot
Note: A nice thing is that this can not go unnoticed. Lets say you add a change
like described above here. Then your pipelines in Github that pyrig creates for
you will fail because the ConfigFile is not correct anymore. So the responsible
autouse fixture will fail and GitHub will notify you because the health-check
workflow failed. Then you just need to open your repo in your IDE, run
pyrig mkroot or pytest review the changes and commit and push.
What happens automatically:
assert_root_is_correctfixture runs (session-level autouse)- Detects
pyproject.tomlis missingcryptography>=42.0.0 - Calls
ConfigFile.validate_subclasses()to fix it - Adds the missing dependency to the file
- Tests fails pytest will raise with a descriptive error message of which Config Files were not correct. Then you can check the git diff of what changed
Note: We decided against it to just autoadd these changes as that would be too
much magic and also things possibly can interfere with other customisations you
made, so yprig raises to not do things silently. The only changes pyrig silently
autoadds are uv lock --upgrade in the release workflow, which keeps your
dependencies automatically up to date even when you do not work on a project for
a while.
Only minimal manual intervention needed!
Step 7: Service-Specific Customization
Each service can still customize while keeping shared standards.
In auth-service, add service-specific config:
Create: auth_service/rig/configs/auth_config.py
"""Auth service specific configuration."""
from pathlib import Path
from pyrig.rig.configs.base.base import ConfigDict
from pyrig.rig.configs.base.yaml import YamlConfigFile
class AuthConfigFile(YamlConfigFile):
"""JWT and OAuth configuration."""
def parent_path(self) -> Path:
return Path("config")
def _configs(self) -> ConfigDict:
return {
"auth": {
"jwt_algorithm": "RS256",
"token_expiry": 3600,
"oauth_providers": ["google", "github"]
}
}
Result: auth-service has both:
- ✓ Shared
logging.yaml(fromservice-base) - ✓ Service-specific
auth.yaml(fromauth-service)
Step 8: Create More Services
Create payment-service and notification-service the same way:
# Payment service
uv init && uv add service-base && uv run pyrig init
# Notification service
uv init && uv add service-base && uv run pyrig init
All three services now have:
- ✓ Same logging configuration
- ✓ Same documentation theme
- ✓ Same dependencies (including
cryptography) - ✓ Same linting rules
- ✓ Same coverage threshold
- ✓ Automatic healing via autouse fixtures
How It Works: The Discovery Mechanism
pyrig uses a sophisticated discovery system to find and initialize configurations across package dependencies. For complete technical details, see:
- ConfigFile Architecture - Discovery, validation, and initialization process
- Fixture Sharing - How fixtures are discovered and shared
- Autouse Fixtures - Self-healing validation system
Quick Overview
ConfigFile Discovery:
- Build dependency graph:
pyrig → service-base → auth-service - Find all
<package>.rig.configsmodules - Discover all ConfigFile subclasses
- Keep only leaf classes (classes with no child subclasses in the result set)
- Validate all leaf classes
Autouse Fixture Healing:
tests/conftest.pyactivates pyrig's test plugins- Discover fixtures from all packages in dependency chain
assert_root_is_correctruns on every test session- Validates all ConfigFiles are correct
- Raises descriptive error if validation fails (prompting you to review the
changes from
ConfigFile.validate_subclassesthat the autouse fixture did to fix the issue)
Propagation Flow
When you update the base package, changes automatically propagate to all dependent services:
- Update service-base - Make changes to config files
- Release new version - GitHub Actions automatically creates release
- Services update dependency - Run
uv add service-base --upgrade - Run pytest or pyrig mkroot - Triggers validation
- assert_root_is_correct runs - Autouse fixture validates all configs
- If incorrect -
ConfigFile.validate_subclasses()validates all incorrect ConfigFiles - Files created/updated - Missing configs added, incorrect values fixed
- Tests continue - Or fail with descriptive error showing what changed
See Autouse Fixtures for details on the validation system.
Real-World Benefits
Scenario: Security Audit Requires New Tool Dependency
Problem: Security audit requires all services use safety alongside the
default security checker.
Solution (5 minutes):
- Override
dev_dependencies()on your tool inservice-base:
def dev_dependencies(self) -> list[str]:
return [*super().dev_dependencies(), "safety"]
-
Commit and push: GitHub Actions releases
service-basev1.2.0 -
In each service:
# all you gotta do
uv lock --upgrade && uv sync
uv run pytest # Auto-heals to add safety or do pyrig mkroot
# commit and push and the new release etc happens automatically
Result: All your microservices now have safety without manual edits.
Scenario: Change Documentation Theme
Problem: Rebrand requires new colors and logo.
Solution (2 minutes):
- Update
service-base/rig/configs/docs/mkdocs.py: Change colors, then do the same steps as above - Release new version
- Services update → All docs sites automatically rebranded at GitHub Pages
Scenario: New Microservice
Problem: Need to create inventory-service with all shared standards.
Solution (30 seconds):
uv init && uv add service-base && uv run pyrig init
Result: Production-ready service with all standards in 30 seconds.
Key Takeaways
- One source of truth:
service-basedefines all standards - Automatic propagation: Changes flow to all services via dependency updates
- Self-healing: Autouse fixtures ensure compliance on every test run
- Override flexibility: Services can customize while keeping standards by subclassing with the same class name
- Leaf class pattern: Subclassing with the same name automatically overrides parent configs (pyrig keeps only the most-derived class)
- Discovery mechanism: Dependency graph + module discovery enables multi-package architecture
- Zero manual sync: No need to manually update 50+ services
Advanced: Multi-Level Inheritance
You can create deeper hierarchies:
pyrig
└── service-base (shared standards)
├── backend-base (backend-specific: databases, APIs)
│ ├── auth-service
│ └── payment-service
└── ml-base (ML-specific: models, training)
├── recommendation-service
└── fraud-detection-service
Each level adds/overrides configs, and leaf services inherit the entire chain.
Testing the Setup
Verify everything works if you want:
In service-base:
# Verify config discovery
uv run python -c "
from pyrig.rig.configs.base.base import ConfigFile
configs = ConfigFile.subclasses()
print(f'Found {len(configs)} config files')
for c in configs:
print(f' - {c.__module__}.{c.__name__}')
"
# Run tests to verify autouse fixtures work
uv run pytest -v
In auth-service:
# Verify inherited configs
cat mkdocs.yml # Should show custom theme
cat pyproject.toml # Should show shared dependencies
# Verify auto-healing
rm mkdocs.yml # Delete inherited config
uv run pytest # Auto-recreates it!
cat mkdocs.yml # Should show custom theme
Summary
pyrig's multi-package architecture enables:
- Centralized standards in base packages
- Automatic discovery via dependency graph traversal
- Override mechanism via subclassing with the same class name (leaf classes automatically override parents)
- Self-healing via autouse fixtures on every test run
- Zero manual sync across unlimited services
This pattern scales from 2 services to infinite+ services with the same simplicity.
Note: If you somehow end up creating a structure over 20 dependencies deep in a dependency chain, the health check cron will get confused as the day has only 24 hours and it staggers it per hour. See more at: Health Check Documentation