Getting Started¶
This guide will help you set up a development environment for contributing to studiorum or building applications that integrate with it.
Development Setup¶
Prerequisites¶
- Python 3.12+: Required for modern typing features
- uv: Recommended package manager (faster than pip)
- Git: For version control and contributing
- LaTeX: For PDF generation (optional for development)
Clone and Setup¶
- Fork and clone the repository:
- Install dependencies with uv:
# Install uv if you haven't already
curl -LsSf https://astral.sh/uv/install.sh | sh
# Install project dependencies
uv sync
# Install pre-commit hooks
uv run pre-commit install
- Verify installation:
Development Dependencies¶
The project uses several development tools:
# pyproject.toml - dev dependencies
[project.optional-dependencies]
dev = [
"pytest>=8.0.0",
"pytest-asyncio>=0.23.0",
"pytest-xdist>=3.5.0",
"mypy>=1.8.0",
"ruff>=0.3.0",
"pre-commit>=3.6.0",
"mkdocs>=1.6.1",
"mkdocs-cinder>=1.2.0",
]
Project Structure¶
Understanding the codebase layout:
studiorum/
├── src/studiorum/ # Main package (src layout)
│ ├── core/ # Core business logic
│ │ ├── container.py # Service container
│ │ ├── models/ # Pydantic data models
│ │ └── services/ # Service protocols
│ ├── cli/ # Command-line interface
│ ├── mcp/ # MCP server implementation
│ ├── renderers/ # Output renderers (LaTeX, etc.)
│ └── latex_engine/ # LaTeX compilation
├── tests/ # Test suite
│ ├── unit/ # Fast unit tests
│ ├── integration/ # Integration tests
│ └── latex/ # LaTeX compilation tests
├── docs/ # Documentation
├── private/ # Private development docs
└── pyproject.toml # Project configuration
Core Concepts¶
Service Container¶
Studiorum uses dependency injection through a service container:
from studiorum.core.container import get_global_container
from studiorum.core.services.protocols import OmnidexerProtocol
# Get the global container
container = get_global_container()
# Resolve services by protocol
omnidexer = container.get_omnidexer_sync()
# For async contexts (MCP server)
from studiorum.core.async_request_context import AsyncRequestContext
async def my_async_function(ctx: AsyncRequestContext):
omnidexer = await ctx.get_service(OmnidexerProtocol)
# Use omnidexer...
Content Models¶
All D&D content uses Pydantic models for validation:
from studiorum.core.models.creatures import Creature
from studiorum.core.models.spells import Spell
from studiorum.core.models.adventures import Adventure
# Models have full type safety
creature = Creature(
name="Ancient Red Dragon",
size=CreatureSize.GARGANTUAN,
creature_type=CreatureType.DRAGON,
challenge_rating=ChallengeRating(24),
# ... other required fields
)
# Validation happens automatically
try:
invalid_creature = Creature(
name="Test",
challenge_rating="invalid" # This will raise ValidationError
)
except ValidationError as e:
print(f"Validation failed: {e}")
Result Pattern¶
All operations that can fail use Result types:
from studiorum.core.result import Result, Success, Error
def convert_creature(creature_name: str) -> Result[str, str]:
"""Convert a creature to LaTeX, returning Result type."""
# Check if creature exists
if not creature_exists(creature_name):
return Error(f"Creature not found: {creature_name}")
# Perform conversion
latex_output = do_conversion(creature_name)
return Success(latex_output)
# Usage with isinstance pattern (recommended)
result = convert_creature("Ancient Red Dragon")
if isinstance(result, Error):
print(f"Conversion failed: {result.error}")
return
# Type checker knows this is Success
latex_content = result.unwrap()
print(f"Generated {len(latex_content)} characters of LaTeX")
Development Workflows¶
Running Tests¶
Studiorum has a comprehensive test suite:
# Fast unit tests (no external dependencies)
uv run pytest tests/unit/ -v
# Integration tests (requires 5etools data)
uv run pytest tests/integration/ -v -m requires_data
# Full test suite (includes LaTeX compilation)
make test
# Run tests in parallel
uv run pytest -n auto
# Test specific functionality
uv run pytest tests/unit/core/test_omnidexer.py -v
# Test with coverage
uv run pytest --cov=studiorum tests/
Code Quality¶
The project uses several code quality tools:
# Linting with ruff
uv run ruff check src/ tests/
uv run ruff format src/ tests/
# Type checking with mypy
uv run mypy src/
# Pre-commit hooks (run automatically)
uv run pre-commit run --all-files
# Full quality check
make lint
Documentation¶
Build and serve documentation locally:
# Serve docs with hot reload
uv run mkdocs serve
# Build static docs
uv run mkdocs build
# Deploy to GitHub Pages (maintainers only)
make docs-deploy
Testing with Real Data¶
Some tests require 5etools data:
# Download test data (first time only)
uv run studiorum sources update
# Run integration tests
uv run pytest tests/integration/ -v -m requires_data
# Test MCP server functionality
uv run studiorum mcp test lookup_creature "Ancient Red Dragon"
Building Features¶
Adding a New Content Type¶
- Define the data model:
# src/studiorum/core/models/new_content.py
from pydantic import BaseModel
from typing import Literal
class NewContent(BaseModel):
"""Model for new D&D content type."""
name: str
content_type: Literal["new_content"] = "new_content"
source: str
# ... other fields
class Config:
validate_assignment = True
extra = "forbid"
- Add to Omnidexer:
# src/studiorum/core/omnidexer.py
def get_new_content_by_name(self, name: str) -> NewContent | None:
"""Get new content by name."""
return self._search_content("new_content", {"name": name})
- Create renderer:
# src/studiorum/renderers/latex/new_content.py
from studiorum.renderers.core.interfaces import RenderingContext
def render_new_content(content: NewContent, context: RenderingContext) -> str:
"""Render new content type to LaTeX."""
return f"\\section{{{content.name}}}\n% New content rendering"
- Add CLI command:
# src/studiorum/cli/commands/convert.py
@app.command()
def new_content(
name: str,
output: str | None = None
):
"""Convert new content type."""
# Implementation here
- Write tests:
# tests/unit/models/test_new_content.py
def test_new_content_validation():
content = NewContent(name="Test", source="MY-HOMEBREW")
assert content.name == "Test"
assert content.content_type == "new_content"
Adding MCP Tools¶
Create new MCP tools for AI integration:
# src/studiorum/mcp/tools/new_tool.py
from studiorum.mcp.core.decorators import mcp_tool
from studiorum.core.async_request_context import AsyncRequestContext
@mcp_tool("new_content_lookup")
async def lookup_new_content(
ctx: AsyncRequestContext,
name: str,
source: str | None = None
) -> dict[str, Any]:
"""Look up new content by name."""
# Get omnidexer from context
omnidexer = await ctx.get_service(OmnidexerProtocol)
# Perform lookup
content = omnidexer.get_new_content_by_name(name)
if not content:
return {"error": f"New content not found: {name}"}
return {
"name": content.name,
"source": content.source,
# ... other fields
}
Custom Renderers¶
Implement custom output formats:
# src/studiorum/renderers/custom/my_renderer.py
from studiorum.renderers.core.interfaces import (
DocumentRenderer,
RenderingContext
)
class MyCustomRenderer(DocumentRenderer):
"""Custom renderer for specific output format."""
def get_supported_formats(self) -> list[str]:
return ["my_format"]
def render_document(
self,
content: list[Any],
context: RenderingContext
) -> str:
"""Render content to custom format."""
output_parts = []
for item in content:
rendered = self._render_item(item, context)
output_parts.append(rendered)
return "\n\n".join(output_parts)
def _render_item(self, item: Any, context: RenderingContext) -> str:
"""Render individual content item."""
# Custom rendering logic
return f"CUSTOM: {item}"
# Register the renderer
from studiorum.renderers.registry import get_renderer_registry
registry = get_renderer_registry()
registry.register_renderer("my_format", MyCustomRenderer())
Advanced Development¶
Service Container Patterns¶
Create custom services:
# Define protocol
from typing import Protocol, runtime_checkable
@runtime_checkable
class MyServiceProtocol(Protocol):
def do_something(self, data: str) -> str: ...
# Implement service
class MyService:
def __init__(self, dependency: SomeDependency):
self.dependency = dependency
def do_something(self, data: str) -> str:
return f"Processed: {data}"
# Register service
def create_my_service() -> MyService:
dependency = get_some_dependency()
return MyService(dependency)
# In service registration
container.register_service(
MyServiceProtocol,
create_my_service,
lifecycle=ServiceLifecycle.SINGLETON
)
Async Context Management¶
For MCP tools and async operations:
from studiorum.core.async_request_context import AsyncRequestContext
class MyAsyncService:
def __init__(self, ctx: AsyncRequestContext):
self.ctx = ctx
async def process_data(self, data: str) -> str:
# Get other services as needed
omnidexer = await self.ctx.get_service(OmnidexerProtocol)
# Process with type safety
result = await self._do_async_work(data, omnidexer)
return result
Error Handling Patterns¶
Use Result types consistently:
from studiorum.core.result import Result, Success, Error
def risky_operation(input_data: str) -> Result[str, str]:
"""Operation that might fail."""
if not input_data:
return Error("Input data cannot be empty")
try:
result = process_data(input_data)
return Success(result)
except Exception as e:
return Error(f"Processing failed: {e}")
# Chain operations
def chain_operations(data: str) -> Result[str, str]:
result1 = risky_operation(data)
if isinstance(result1, Error):
return result1.with_context(
"First operation failed",
operation="risky_operation",
input=data
)
result2 = another_operation(result1.unwrap())
if isinstance(result2, Error):
return result2.with_context("Second operation failed")
return result2
Debugging Tips¶
Logging Configuration¶
Enable detailed logging for development:
import logging
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
# Enable specific loggers
logging.getLogger('studiorum.core.omnidexer').setLevel(logging.DEBUG)
logging.getLogger('studiorum.mcp').setLevel(logging.DEBUG)
MCP Server Debugging¶
Debug MCP server issues:
# Run MCP server with debug logging
uv run studiorum mcp run --debug --log-level DEBUG
# Test MCP tools directly
uv run studiorum mcp test lookup_creature "Ancient Red Dragon"
# Monitor MCP logs
tail -f ~/.studiorum/logs/mcp-debug-*.log
Service Container Debugging¶
Debug service resolution issues:
from studiorum.core.container import get_global_container
container = get_global_container()
# Check registered services
print("Registered services:")
for protocol in container.get_registered_protocols():
print(f" - {protocol}")
# Check service instances
print("Singleton instances:")
for protocol, instance in container.get_singleton_instances().items():
print(f" - {protocol}: {instance}")
Contributing Guidelines¶
Code Standards¶
- Type hints: All functions must have type annotations
- Docstrings: Use numpy-style docstrings for public APIs
- Error handling: Use Result types for operations that can fail
- Testing: Write tests for all new functionality
Pull Request Process¶
- Create feature branch:
- Make changes with tests:
- Quality checks:
- Commit and push:
git add .
git commit -m "feat: add new feature with comprehensive tests"
git push origin feat/my-new-feature
- Create pull request targeting
develop
branch
Release Process¶
For maintainers:
- Create release branch:
- Update version and changelog:
- Final testing:
- Merge to main and tag:
Getting Help¶
- Documentation: Read the full developer guide
- GitHub Issues: Report bugs or request features
- Discussions: Ask questions and share ideas
- Discord: Join our development community
Next Steps¶
- Architecture Guide: Deep dive into system design
- API Reference: Complete API documentation
- AI Agent Development: Build MCP integrations
- Contributing Guidelines: Detailed contribution process