Design Principles¶
The Provide Foundry is built on a foundation of carefully considered design principles that guide every architectural decision, API design, and implementation choice. These principles ensure consistency, maintainability, and excellent developer experience across all packages.
Core Philosophy¶
Developer Experience First¶
Every decision prioritizes the developer experience. This means:
- Intuitive APIs: Functions and classes should work as developers expect
- Excellent Error Messages: Clear, actionable error messages with suggestions
- Comprehensive Documentation: Examples, guides, and API references for everything
- Fast Feedback Loops: Quick iteration with immediate feedback
# Good: Clear, intuitive API
@resource
class WebServer:
port: int = Attribute(description="Port to listen on", default=8080)
# Bad: Unclear, complex API
class WebServerResource(BaseResource):
def __init__(self):
super().__init__()
self.add_attribute("port", IntegerAttribute(default_value=8080))
Type Safety Everywhere¶
Static typing catches errors early and improves code quality:
- Modern Python Typing: Use
str | Noneinstead ofOptional[str] - Runtime Validation: Validate types at runtime where appropriate
- Schema-Driven: All data structures defined with comprehensive schemas
- No
AnyTypes: AvoidAnyexcept where absolutely necessary
# Good: Comprehensive type annotations
def create_resource(
config: ResourceConfig,
provider: Provider,
timeout: float = 30.0
) -> ResourceState:
pass
# Bad: Missing or weak typing
def create_resource(config, provider, timeout=30):
pass
Composition Over Inheritance¶
Favor composition and mixins over deep inheritance hierarchies:
- Small, Focused Classes: Each class has a single responsibility
- Mixin Pattern: Combine behavior through mixins
- Dependency Injection: Pass dependencies explicitly
- Protocol-Based Design: Use protocols instead of abstract base classes
# Good: Composition with protocols
class ResourceManager:
def __init__(self, validator: Validator, logger: Logger):
self.validator = validator
self.logger = logger
# Bad: Deep inheritance
class ResourceManager(BaseManager, ValidatorMixin, LoggerMixin):
pass
API Design Principles¶
Consistency¶
Consistent APIs across all packages reduce cognitive load:
Naming Conventions¶
- Functions:
snake_casefor functions and variables - Classes:
PascalCasefor classes and types - Constants:
UPPER_SNAKE_CASEfor constants - Modules:
snake_casefor module names
Parameter Patterns¶
- Required parameters first: Most important parameters come first
- Keyword-only for complex functions: Use
*to force keyword arguments - Sensible defaults: Provide defaults for optional parameters
- No boolean traps: Use enums instead of boolean flags
# Good: Clear parameter pattern
def create_provider(
name: str,
*,
config_path: Path | None = None,
log_level: LogLevel = LogLevel.INFO,
enable_telemetry: bool = True
) -> Provider:
pass
# Bad: Boolean trap and unclear parameters
def create_provider(name, config, log, telemetry=True):
pass
Discoverability¶
Make functionality easy to discover:
- Flat Import Structure: Important functions available at package root
- Clear Module Organization: Related functionality grouped together
- Comprehensive
__all__: Explicit public API definition - Type Hints in Signatures: Rich type information for IDE support
# Good: Clear public API
from pyvider import provider, resource, data_source, function
from pyvider.schema import Attribute, Block
# Bad: Deep imports required
from pyvider.core.decorators.provider import provider
from pyvider.schema.types.attribute import Attribute
Extensibility¶
Design for extension without modification:
- Plugin Architecture: Use the hub pattern for extensibility
- Event System: Allow hooking into lifecycle events
- Configuration Points: Provide configuration for customization
- Protocol-Based: Define clear interfaces for extension
# Good: Extensible through protocols
class CustomProcessor(LogProcessor):
def process(self, record: LogRecord) -> LogRecord:
# Custom processing logic
return record
# Register the processor
get_hub().register_processor(CustomProcessor())
Implementation Principles¶
No Backward Compatibility Burden¶
The foundry prioritizes clean, modern code over backward compatibility:
- Direct Implementation: Implement the target state directly
- Modern Python Features: Use Python 3.11+ features without hesitation
- Clean Abstractions: Don't compromise design for compatibility
- Breaking Changes: Accept breaking changes to improve the design
# Good: Modern Python features
def process_items(items: list[Item]) -> dict[str, Any]:
return {item.name: item.value for item in items}
# Bad: Backward compatibility
def process_items(items):
# Type: (List[Item]) -> Dict[str, Any]
return dict((item.name, item.value) for item in items)
Configuration Over Convention¶
Explicit configuration is better than implicit behavior:
- No Magic Defaults: All defaults defined in configuration files
- Explicit Dependencies: Dependencies declared explicitly
- Environment-Specific: Configuration varies by environment
- Type-Safe Config: All configuration validated against schemas
# Good: Explicit configuration
@attrs.define
class DatabaseConfig:
host: str
port: int = 5432
timeout: float = 30.0
# Bad: Hidden defaults
class Database:
def __init__(self, host):
self.host = host
self.port = 5432 # Hidden default
Fail Fast and Loud¶
Detect and report errors as early as possible:
- Input Validation: Validate inputs at API boundaries
- Schema Validation: Validate data against schemas
- Type Checking: Use runtime type checking where appropriate
- Clear Error Messages: Provide actionable error information
# Good: Early validation with clear errors
def create_server(port: int) -> Server:
if not (1 <= port <= 65535):
raise ValueError(
f"Port {port} is invalid. Must be between 1 and 65535."
)
return Server(port)
# Bad: Silent failures or late errors
def create_server(port):
return Server(port) # May fail later
Testing Principles¶
Test-Driven Development¶
Write tests before implementation:
- Red-Green-Refactor: Write failing test, make it pass, refactor
- Behavior-Driven: Test behavior, not implementation details
- Comprehensive Coverage: Aim for high test coverage
- Fast Feedback: Tests should run quickly
# Good: Test behavior
def test_resource_creation_with_valid_config():
config = ResourceConfig(name="test", port=8080)
resource = create_resource(config)
assert resource.name == "test"
assert resource.port == 8080
# Bad: Test implementation
def test_resource_creation_calls_constructor():
config = ResourceConfig(name="test", port=8080)
with mock.patch('Resource.__init__') as mock_init:
create_resource(config)
mock_init.assert_called_once()
Property-Based Testing¶
Use property-based testing for complex logic:
- Hypothesis Integration: Use Hypothesis for property-based tests
- Edge Case Discovery: Let tools find edge cases
- Invariant Testing: Test properties that should always hold
- Shrinking: Minimal failing examples for debugging
from hypothesis import given, strategies as st
@given(st.integers(min_value=1, max_value=65535))
def test_server_accepts_valid_ports(port):
server = create_server(port)
assert server.port == port
Integration Testing¶
Test real interactions between components:
- End-to-End Tests: Test complete workflows
- Cross-Package Tests: Test interactions between packages
- Real Dependencies: Use real external services where possible
- Conformance Tests: Test compatibility with external systems
Documentation Principles¶
Documentation as Code¶
Treat documentation with the same care as code:
- Version Controlled: All documentation in version control
- Tested Examples: All code examples are tested
- Automated Generation: Generate documentation from code where possible
- Continuous Integration: Documentation builds on every change
Multiple Audiences¶
Different audiences need different documentation:
- Tutorials: For learning step-by-step
- How-To Guides: For solving specific problems
- Reference: For looking up details
- Explanations: For understanding concepts
<!-- Good: Clear audience targeting -->
# Tutorial: Your First Provider
*For developers new to Pyvider*
This tutorial walks you through creating your first Terraform provider...
# Reference: Provider API
*For developers implementing providers*
## Provider Class
```python
class Provider:
"""Base class for all providers."""
Provider Documentation¶
This explains providers and also shows how to create them...
## Performance Principles
### Async by Default
Embrace asynchronous programming for scalability:
- **Non-Blocking Operations**: Use async/await for I/O operations
- **Concurrent Execution**: Process operations concurrently where possible
- **Backpressure Handling**: Handle backpressure gracefully
- **Resource Management**: Use context managers for resource cleanup
```python
# Good: Async operations
async def fetch_resource_data(resource_id: str) -> ResourceData:
async with httpx.AsyncClient() as client:
response = await client.get(f"/resources/{resource_id}")
return ResourceData.from_json(response.json())
# Bad: Blocking operations
def fetch_resource_data(resource_id: str) -> ResourceData:
response = requests.get(f"/resources/{resource_id}")
return ResourceData.from_json(response.json())
Lazy Initialization¶
Initialize resources only when needed:
- Lazy Loading: Load modules and components on first use
- Singleton Pattern: Use singletons for expensive resources
- Connection Pooling: Pool expensive connections
- Caching: Cache expensive computations
Memory Efficiency¶
Be conscious of memory usage:
- Generator Functions: Use generators for large datasets
- Streaming: Stream large files instead of loading into memory
- Weak References: Use weak references to break reference cycles
- Resource Cleanup: Explicitly clean up resources
Security Principles¶
Security by Default¶
Security should be the default, not an option:
- Secure Defaults: All defaults should be secure
- Principle of Least Privilege: Grant minimal necessary permissions
- Input Validation: Validate all inputs thoroughly
- No Secrets in Logs: Never log sensitive information
# Good: Secure by default
@attrs.define
class DatabaseConfig:
password: str = attrs.field(repr=False) # Hidden from repr
ssl_mode: str = "require" # Secure default
# Bad: Insecure defaults
@attrs.define
class DatabaseConfig:
password: str
ssl_mode: str = "disable" # Insecure default
Defense in Depth¶
Implement multiple layers of security:
- Input Validation: Validate at API boundaries
- Authentication: Verify identity before access
- Authorization: Check permissions for operations
- Audit Logging: Log security-relevant events
Transparent Security¶
Security should be visible and understandable:
- Clear Policies: Document security policies clearly
- Audit Trails: Provide comprehensive audit trails
- Error Messages: Security errors should be informative
- Documentation: Document security considerations
Ecosystem Principles¶
Interoperability¶
Components should work well together:
- Standard Interfaces: Use common interfaces across packages
- Data Format Compatibility: Use compatible data formats
- Protocol Adherence: Follow established protocols
- Graceful Degradation: Work with missing optional dependencies
Modularity¶
Each package should be independently useful:
- Single Responsibility: Each package has a clear purpose
- Minimal Dependencies: Avoid unnecessary dependencies
- Optional Features: Make advanced features optional
- Clear Boundaries: Well-defined package boundaries
Evolution¶
The foundry should evolve gracefully:
- Versioned APIs: Use semantic versioning for APIs
- Deprecation Process: Clear process for deprecating features
- Migration Guides: Help users migrate between versions
- Experimental Features: Mark experimental features clearly
These principles guide every decision in the provide.io foundry. They ensure that the tools remain consistent, maintainable, and enjoyable to use as the foundry grows and evolves.
Continue exploring with our architecture overview or see these principles in action in our development guides.