Example Usage: Microservices Ecosystem
This guide demonstrates pyrig's power in a real-world scenario: building a standardized microservices ecosystem for a company.
Scenario
You're building multiple Python microservices that need:
- Consistent standards across all services
- Custom company-wide configurations (logging, monitoring, security)
- Automatic synchronization when standards change
- Self-healing infrastructure via autouse fixtures
Architecture Overview
Dependency chain: pyrig → company-base → auth-service,
payment-service, notification-service
Step 1: Create Base Package
Create company-base that extends pyrig with company standards:
# Create repository
gh repo create company/company-base --public # Or private if you want
git clone https://github.com/company/company-base.git
cd company-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 company-wide logging configuration that all microservices will inherit.
Create: company_base/dev/configs/logging_config.py
"""Company-wide logging configuration."""
from pathlib import Path
from typing import Any
from pyrig.dev.configs.base.yaml import YamlConfigFile
class LoggingConfigFile(YamlConfigFile):
"""Logging configuration for all company microservices."""
@classmethod
def get_parent_path(cls) -> Path:
"""Place in config/ directory."""
return Path("config")
@classmethod
def _get_configs(cls) -> dict[str, Any]:
"""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_config.yamlwhen you runuv run pyrig mkroot - Automatically discovered via pyrig's ConfigFile discovery system
- Inherited by all services that depend on
company-base
Step 3: Override MkDocs Theme
Customize documentation theme for company branding.
Create: company_base/dev/configs/docs/mkdocs.py
"""Company-branded MkDocs configuration."""
from pathlib import Path
from typing import Any
from pyrig.dev.configs.docs.mkdocs import MkdocsConfigFile as BaseMkdocsCF
class MkdocsConfigFile(BaseMkdocsCF):
"""Company-branded documentation theme."""
@classmethod
def _get_configs(cls) -> dict[str, Any]:
"""Override theme with company colors."""
config = super()._get_configs()
# Add company branding
config["theme"]["palette"] = [
{
"scheme": "slate",
"primary": "deep purple", # Company color
"accent": "amber", # Company 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 company 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 company branding
Step 4: Adjust Pyproject Settings
Add company-specific dependencies and settings.
Create: company_base/dev/configs/pyproject.py
"""Company pyproject.toml with additional dependencies."""
from typing import Any
from pyrig.dev.configs.pyproject import PyprojectConfigFile as BasePyprojectCF
class PyprojectConfigFile(BasePyprojectCF):
"""Company pyproject with monitoring and logging."""
@classmethod
def get_dependencies(cls) -> list[str]:
"""Add company-standard runtime dependencies."""
deps = super().get_dependencies()
return [
*deps,
"python-json-logger>=2.0.0", # JSON logging
"prometheus-client>=0.19.0", # Metrics
"sentry-sdk>=1.40.0", # Error tracking
]
@classmethod
def _get_configs(cls) -> dict[str, Any]:
"""Add company-specific tool configs."""
config = super()._get_configs()
# Add custom ruff rules for company 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 company-wide
- Lowers coverage to 85% (more realistic for microservices)
Step 5: Create First Microservice
Now create auth-service that depends on company-base:
# Create repository
gh repo create company/auth-service
git clone https://github.com/company/auth-service.git
cd auth-service
# Initialize with pyrig
uv init
uv add git+https://github.com/company/company-base.git # This also brings in pyrig
uv run pyrig init
Note: If you are a company your repos probably will not be public so 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_config.yaml # ✓ From company-base
├── mkdocs.yml # ✓ Company-branded theme
├── pyproject.toml # ✓ With company dependencies
├── auth_service/
│ ├── dev/
│ │ ├── 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 company standards applied automatically!
Step 6: The Magic - Automatic Synchronization
Here's where pyrig shines. When you update company-base, all services heal
themselves.
Update Company Standards
In company-base, add a security requirement:
Update: company_base/dev/configs/pyproject.py
@classmethod
def get_dependencies(cls) -> list[str]:
"""Add company-standard runtime dependencies."""
deps = super().get_dependencies()
return [
*deps,
"python-json-logger>=2.0.0",
"prometheus-client>=0.19.0",
"sentry-sdk>=1.40.0",
"cryptography>=42.0.0", # NEW: Company security requirement
]
Commit and release:
cd company-base
git add .
git commit -m "Add cryptography requirement"
git push
# GitHub Actions automatically releases new version
Services Auto-Heal
In auth-service:
# Update company-base dependency
uv add company-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
make_project_root()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
dependecies 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 company standards.
In auth-service, add service-specific config:
Create: auth_service/dev/configs/auth_config.py
"""Auth service specific configuration."""
from pathlib import Path
from typing import Any
from pyrig.dev.configs.base.yaml import YamlConfigFile
class AuthConfigFile(YamlConfigFile):
"""JWT and OAuth configuration."""
@classmethod
def get_parent_path(cls) -> Path:
return Path("config")
@classmethod
def _get_configs(cls) -> dict[str, Any]:
return {
"auth": {
"jwt_algorithm": "RS256",
"token_expiry": 3600,
"oauth_providers": ["google", "github"]
}
}
Result: auth-service has both:
- ✓ Company-wide
logging_config.yaml(fromcompany-base) - ✓ Service-specific
auth_config.yaml(fromauth-service)
Step 8: Create More Services
Create payment-service and notification-service the same way:
# Payment service
uv init && uv add company-base && uv run pyrig init
# Notification service
uv init && uv add company-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 → company-base → auth-service - Find all
<package>.dev.configsmodules - Discover all ConfigFile subclasses
- Keep only leaf classes (classes with no subclasses sharing the same name)
- Initialize 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
make_project_rootthat the autouse fixture did to fix the issue)
Propagation Flow
When you update the base package, changes automatically propagate to all dependent services:
- Update company-base - Make changes to config files
- Release new version - GitHub Actions automatically creates release
- Services update dependency - Run
uv add company-base --upgrade - Run pytest or pyrig mkroot - Triggers validation
- assert_root_is_correct runs - Autouse fixture validates all configs
- If incorrect -
make_project_root()discovers and initializes all 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 Dependency
Problem: Security audit requires all services use bandit for security
scanning.
Solution (5 minutes):
- Update
company-base/dev/configs/pyproject.py:
python
@classmethod
def get_dev_dependencies(cls) -> list[str]:
deps = super().get_dev_dependencies()
return [*deps, "bandit>=1.7.0"]
-
Commit and push → GitHub Actions releases
company-basev1.2.0 -
In each service:
bash
# all you gotta do
uv lock --upgrade && uv sync
uv run pytest # Auto-heals to add bandit or do pyrig mkroot
# commit and push and the new release etc happens automatically
Result: All your microservices now have bandit without manual edits.
Scenario: Change Documentation Theme
Problem: Rebrand requires new colors and logo.
Solution (2 minutes):
- Update
company-base/dev/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 company standards.
Solution (30 seconds):
uv init && uv add company-base && uv run pyrig init
Result: Production-ready service with all standards in 30 seconds.
Key Takeaways
- One source of truth:
company-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
└── company-base (company 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 company-base:
# Verify config discovery
uv run python -c "
from pyrig.dev.configs.base.base import ConfigFile
configs = ConfigFile.get_all_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 company theme
cat pyproject.toml # Should show company dependencies
# Verify auto-healing
rm mkdocs.yml # Delete inherited config
uv run pytest # Auto-recreates it!
cat mkdocs.yml # Should show company 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