Basic Example
This guide demonstrates a minimal SPOC application from scratch. You'll build a complete, runnable example that shows the core workflow: project structure, configuration, component registration, and lifecycle management.
What You'll Build
A simple greeting application that:
- Organizes code into a modular app structure
- Defines and registers components
- Manages application lifecycle (startup/shutdown)
- Accesses components through the framework
Project Structure
Create the following directory structure:
my_project/
├── config/
│ ├── __init__.py
│ ├── settings.py
│ └── spoc.toml
├── apps/
│ └── hello/
│ ├── __init__.py
│ └── greetings.py
└── main.py
Step 1: Configuration Files
config/init.py
Create an empty __init__.py to make config a Python package:
config/settings.py
Define your project settings:
"""Project settings configuration."""
from pathlib import Path
# Base directory of the project - used by the framework to locate apps
BASE_DIR: Path = Path(__file__).resolve().parent.parent
# Apps to always load (core apps)
INSTALLED_APPS: list = [
"hello",
]
# Plugins for extending functionality
# Middleware and hooks can be registered here
PLUGINS: dict = {
"middleware": [],
"hooks": [],
}
Key Points:
BASE_DIRpoints to the project root (parent of config directory)INSTALLED_APPSlists apps that load in all environmentsPLUGINSis a dictionary for middleware and hook extensions
config/spoc.toml
Define environment-specific configuration:
# Application Configuration
[spoc]
mode = "development" # Options: development, staging, production
debug = true
# Apps by Environment Mode
# These apps load based on the active mode
[spoc.apps]
production = [] # Apps loaded only in production
staging = [] # Apps loaded in staging + production
development = [] # Apps loaded in development + staging + production
# Additional Plugins
[spoc.plugins]
middleware = []
hooks = []
How Modes Work:
developmentmode loads: development + staging + production appsstagingmode loads: staging + production appsproductionmode loads: production apps onlyINSTALLED_APPSfrom settings.py always loads regardless of mode
Step 2: Create Your App
apps/hello/init.py
Create an empty __init__.py to make hello a Python package:
apps/hello/greetings.py
Define your app's components with decorators:
"""Greetings module - Components for greeting functionality."""
from spoc import Components
# Create a component registry for this module
components = Components()
# Register component type
components.add_type("service")
@components.register("service", config={"default_language": "en"})
class GreetingService:
"""Service for generating greetings in different languages."""
def __init__(self):
"""Initialize the greeting service."""
self.greetings = {
"en": "Hello",
"es": "Hola",
"fr": "Bonjour",
"de": "Hallo",
}
def greet(self, name: str, language: str = "en") -> str:
"""
Generate a greeting message.
Args:
name: Name to greet
language: Language code (en, es, fr, de)
Returns:
Formatted greeting message
"""
greeting = self.greetings.get(language, self.greetings["en"])
return f"{greeting}, {name}!"
def list_languages(self) -> list[str]:
"""
Get list of supported languages.
Returns:
List of language codes
"""
return list(self.greetings.keys())
@components.register("service", config={"max_length": 100})
def farewell_message(name: str) -> str:
"""
Generate a farewell message.
This demonstrates registering a function as a component.
Args:
name: Name to say goodbye to
Returns:
Farewell message
"""
return f"Goodbye, {name}! See you soon!"
Component Registration:
Components()creates a registry for component typesadd_type("service")registers the "service" component type@components.register("service")marks classes/functions as componentsconfigparameter attaches metadata to components
Step 3: Main Application
main.py
Create the framework and run your application:
"""Main application entry point."""
from pathlib import Path
from spoc import Framework, Schema, Hook
# Import settings
from config import settings
def init_greetings(module):
"""
Hook called when greetings module loads.
Args:
module: The loaded module instance
"""
print(f"✓ Initialized: {module.__name__}")
def cleanup_greetings(module):
"""
Hook called when greetings module unloads.
Args:
module: The module being unloaded
"""
print(f"✗ Cleaning up: {module.__name__}")
# Define the application schema
schema = Schema(
# Modules to load from each app
modules=["greetings"],
# Module dependencies (greetings has no dependencies)
dependencies={},
# Lifecycle hooks for each module
hooks={
"greetings": Hook(
startup=init_greetings,
shutdown=cleanup_greetings,
),
},
)
# Create the framework instance
# The framework automatically calls startup() during initialization
framework = Framework(
base_dir=settings.BASE_DIR,
schema=schema,
echo=False, # Set to True to see detailed loading information
mode="strict", # "strict" requires all modules in all apps, "loose" allows missing modules
)
def main():
"""Run the application."""
print("\n" + "=" * 60)
print("SPOC Application - Basic Example")
print("=" * 60 + "\n")
# Display installed apps
print(f"Installed Apps: {framework.installed_apps}")
print()
# Access the component registry
print("Registered Components:")
print("-" * 60)
# Get all service components
if hasattr(framework.components, "service"):
for name, component in framework.components.service.items():
print(f" • {name}: {component}")
print()
# Use a specific component - Get the GreetingService class
print("Using Components:")
print("-" * 60)
greeting_service_cls = framework.get_component("service", "hello.GreetingService")
if greeting_service_cls:
# Instantiate the service
service = greeting_service_cls()
# Use the service
print(f" {service.greet('World')}")
print(f" {service.greet('Mundo', 'es')}")
print(f" {service.greet('Monde', 'fr')}")
print(f" Supported languages: {service.list_languages()}")
print()
# Use the farewell function component
farewell_fn = framework.get_component("service", "hello.farewell_message")
if farewell_fn:
print(f" {farewell_fn('World')}")
print()
# Access component metadata
print("Component Metadata:")
print("-" * 60)
if greeting_service_cls and hasattr(greeting_service_cls, "__spoc__"):
metadata = greeting_service_cls.__spoc__
print(f" Config: {metadata.config}")
print(f" Metadata: {metadata.metadata}")
print()
# Application is running...
print("=" * 60)
print("Application Running - Press Ctrl+C to stop")
print("=" * 60 + "\n")
# Clean shutdown (framework handles this automatically on exit)
# But you can manually call it when needed
framework.shutdown()
print("\n" + "=" * 60)
print("Application Stopped")
print("=" * 60 + "\n")
if __name__ == "__main__":
main()
Step 4: Run the Application
Execute your application from the project root:
Expected Output
apps_path C:\path\to\my_project\apps
✓ Initialized: hello.greetings
============================================================
SPOC Application - Basic Example
============================================================
Installed Apps: ['hello']
Registered Components:
------------------------------------------------------------
• hello.GreetingService: <class 'hello.greetings.GreetingService'>
• hello.farewell_message: <function farewell_message at 0x...>
Using Components:
------------------------------------------------------------
Hello, World!
Hola, Mundo!
Bonjour, Monde!
Supported languages: ['en', 'es', 'fr', 'de']
Goodbye, World! See you soon!
Component Metadata:
------------------------------------------------------------
Config: {'default_language': 'en'}
Metadata: {'type': 'service'}
============================================================
Application Running - Press Ctrl+C to stop
============================================================
✗ Cleaning up: hello.greetings
============================================================
Application Stopped
============================================================
Understanding the Code
Component Registration
Components are the building blocks of your application:
from spoc import Components
components = Components()
components.add_type("service")
@components.register("service", config={"key": "value"})
class MyService:
pass
Process:
- Create a
Componentsregistry - Add component types with
add_type() - Use
@components.register()decorator to mark classes/functions - Optionally attach configuration metadata
Framework Schema
The schema defines your application structure:
schema = Schema(
modules=["greetings"], # Modules to load from each app
dependencies={}, # Module dependencies
hooks={ # Lifecycle hooks
"greetings": Hook(
startup=init_fn,
shutdown=cleanup_fn,
),
},
)
Key Elements:
- modules: List of Python module names to load from each app
- dependencies: Dict mapping modules to their required dependencies
- hooks: Dict mapping modules to startup/shutdown functions
Framework Initialization
The framework orchestrates your application:
framework = Framework(
base_dir=settings.BASE_DIR, # Project root directory
schema=schema, # Application schema
echo=False, # Debug output flag
mode="strict", # Module validation mode
)
Important:
- Framework automatically calls
startup()during initialization mode="strict"requires all modules exist in all appsmode="loose"allows apps to have missing modules- Framework handles cleanup automatically, but you can call
shutdown()manually
Accessing Components
Retrieve components from the framework:
# Get a specific component by type and name
component = framework.get_component("service", "hello.GreetingService")
# Get all components of a type
all_services = framework.components.service.values()
# Check if type exists
if hasattr(framework.components, "service"):
# Access the service registry
services = framework.components.service
Lifecycle Flow
The framework follows this lifecycle:
sequenceDiagram
participant User
participant Framework
participant Importer
participant Module
User->>Framework: Framework(base_dir, schema)
Framework->>Framework: Load configuration
Framework->>Framework: Register apps & modules
Framework->>Framework: startup() (automatic)
Framework->>Importer: Load modules by dependency order
Importer->>Module: Import hello.greetings
Importer->>Module: Execute startup hook
Module-->>Framework: Module loaded
Note over User,Framework: Application runs...
User->>Framework: shutdown()
Framework->>Importer: Unload modules (reverse order)
Importer->>Module: Execute shutdown hook
Module-->>Framework: Module unloaded
Key Takeaways
1. Apps Directory
SPOC automatically looks for apps in the apps/ directory under BASE_DIR:
2. Module Loading
Modules are loaded based on the schema:
- Framework loads modules from each installed app
- Modules load in dependency order
- Startup hooks execute after module import
- Shutdown hooks execute in reverse order
3. Component Discovery
Components are automatically discovered and registered:
- Each module can have its own
Componentsregistry - Framework collects all registered components
- Access components via
framework.components.<type> - Components retain their metadata and configuration
4. Environment Management
Control app loading by environment:
[spoc]
mode = "production"
[spoc.apps]
production = ["core", "api"]
development = ["core", "api", "debug"]
Common Patterns
Pattern 1: Multiple Component Types
Register different types of components:
from spoc import Components
components = Components()
components.add_type("model")
components.add_type("service")
components.add_type("view")
@components.register("model")
class User:
pass
@components.register("service")
class UserService:
pass
@components.register("view")
def user_list_view():
pass
Pattern 2: Shared Component Registry
Create a shared registry in a common module:
# framework/components.py
from spoc import Components
components = Components("model", "service", "view")
# Export decorators for convenience
model = lambda obj: components.register("model", obj)
service = lambda obj: components.register("service", obj)
view = lambda obj: components.register("view", obj)
Use in your apps:
Pattern 3: Component Configuration
Attach metadata to components:
@components.register(
"service",
config={
"timeout": 30,
"retry": 3,
"cache": True,
}
)
class APIService:
def __init__(self):
# Access config from __spoc__ attribute
config = self.__spoc__.config
self.timeout = config.get("timeout", 10)
Next Steps
Now that you understand the basics:
- Add More Apps: Create additional apps in the
apps/directory - Define Dependencies: Explore module dependencies in Quick Start
- Use Lifecycle Hooks: Learn advanced hook patterns in Framework API
- Explore Plugins: Extend functionality in the Advanced Guide
- Review Examples: Check the
examples/directory in the repository
Troubleshooting
App Not Loading
Problem: Your app doesn't appear in framework.installed_apps.
Solution:
- Verify the app directory exists in
apps/ - Check the app has an
__init__.pyfile - Ensure the app is listed in
INSTALLED_APPSor[spoc.apps.<mode>] - Verify
modematches your environment setting
Module Import Error
Problem: ModuleNotFoundError when loading modules.
Solution:
- Check module filename matches schema (e.g.,
greetings.pyfor "greetings") - Ensure module is listed in
schema.modules - Verify no syntax errors in the module
- Check dependencies are declared correctly
Component Not Found
Problem: framework.get_component() returns None.
Solution:
- Verify component is decorated with
@components.register() - Check component type matches (e.g., "service" not "services")
- Ensure component name uses correct format:
"app.ComponentName" - Confirm module loaded successfully (check startup output)
Strict Mode Errors
Problem: Framework raises errors about missing modules.
Solution:
- Set
mode="loose"to allow missing modules - Or create all required modules in all apps
- Or remove apps that don't match the schema
Summary
You've learned how to:
- Structure a minimal SPOC project
- Configure settings and environment modes
- Create apps with components
- Define schemas with modules and hooks
- Initialize and manage the framework lifecycle
- Access and use registered components
This basic example provides a foundation for building more complex SPOC applications. Continue to the Quick Start Guide for multi-app examples and advanced patterns.