Thank you for your interest in contributing to Scadable! This guide will help you get started with development, testing, and contributing to the project.
- Getting Started
- Development Setup
- Architecture & Design
- Code Style
- Testing
- Making Contributions
- Release Process
Before you start, please:
- Read the main Organization Docs for general Scadable contribution guidelines
- Check existing issues to see if your bug/feature is already being discussed
- For major changes, open an issue first to discuss your proposed changes
- Python 3.10 or higher
- pip package manager
- git
-
Fork and clone the repository:
git clone https://github.com/your-username/library-python.git cd library-python -
Create a virtual environment:
python -m venv .venv # On macOS/Linux source .venv/bin/activate # On Windows .venv\Scripts\activate
-
Install development dependencies:
pip install -e ".[dev]"This installs the package in editable mode along with:
pytest- Testing frameworkcoverage- Code coverage reportingpytest-cov- Coverage plugin for pytestpytest-asyncio- Async test supportpre-commit- Git hooks for code quality
-
Set up pre-commit hooks:
pre-commit install
This will automatically run code style checks before each commit.
Run the test suite to ensure everything is working:
pytest testsYou should see all tests passing.
The Scadable Python SDK is designed with modularity, extensibility, and ease of use in mind. It follows a layered architecture:
┌─────────────────────────────────────────────────────────┐
│ Application Layer │
│ (User's Python application) │
└────────────────────────┬────────────────────────────────┘
│
┌────────────────────────▼────────────────────────────────┐
│ SDK Layer │
│ ┌──────────┐ ┌──────────────┐ ┌─────────────────┐ │
│ │ Facility │──│DeviceManager │ │ConnectionFactory│ │
│ └────┬─────┘ └──────┬───────┘ └────────┬────────┘ │
│ │ │ │ │
│ ┌────▼───────────────▼────────────────────▼────────┐ │
│ │ Device (Telemetry Bus) │ │
│ └──────────────────────┬───────────────────────────┘ │
└─────────────────────────┼──────────────────────────────┘
│
┌─────────────────────────▼────────────────────────────────┐
│ Transport Layer │
│ (WebSocket Connection to Scadable Platform) │
└──────────────────────────────────────────────────────────┘
Purpose: Main entry point and orchestrator for the SDK.
Responsibilities:
- API key management and authentication
- Device lifecycle management (create, delete)
- Connection factory coordination
- Decorator API for telemetry subscriptions
Key Methods:
create_device()- Creates a single device with optional connectioncreate_many_devices()- Batch device creationlive_telemetry()- Decorator for subscribing to device telemetry
Design Pattern: Facade pattern - provides a simplified interface to the complex subsystem of devices and connections.
Device:
- Represents a single IoT device/data source
- Manages two event buses:
raw_bus: For raw, unparsed telemetry dataparsed_bus: For parsed telemetry data
- Handles asynchronous message routing to subscribers
DeviceManager:
- Registry pattern for device instances
- Prevents duplicate device creation
- Provides dictionary-like access to devices
Design Pattern: Observer pattern - devices maintain lists of subscribers and notify them of data events.
Abstract Classes:
Connection: Base interface for all connection typesConnectionFactory: Abstract factory for creating connections
Concrete Implementations:
WebsocketConnection: WebSocket-based connection implementationWebsocketConnectionFactory: Factory for WebSocket connections
Design Pattern:
- Abstract Factory - allows for different connection types (WebSocket, HTTP, custom)
- Strategy pattern - connection behavior can be swapped at runtime
Extension Point: To add a new connection type (e.g., MQTT, HTTP polling):
- Subclass
Connectionand implementconnect(),send_message(),stop() - Subclass
ConnectionFactoryand implementcreate_connection() - Pass your factory to
Facility(connection_factory=YourFactory())
1. User creates Facility with API key and ConnectionFactory
2. User calls create_device(id, create_connection=True)
3. Facility asks ConnectionFactory for a Connection
4. Connection is injected into Device
5. User decorates handler with @facility.live_telemetry("device-id")
6. Handler is added to Device's parsed_bus
7. User calls device.start_live_telemetry()
8. Connection establishes WebSocket, listens for messages
9. Messages flow: WebSocket → Device._handle_raw() → raw_bus → parsed_bus
10. All subscribed handlers are called asynchronously
| Component | Technology | Version | Purpose |
|---|---|---|---|
| Language | Python | 3.10+ | Core implementation |
| Async Runtime | asyncio | stdlib | Asynchronous I/O |
| WebSocket Client | websockets | ≥13.0 | Real-time communication |
| Testing | pytest | latest | Unit and integration tests |
| Async Testing | pytest-asyncio | latest | Testing async code |
| Code Coverage | pytest-cov | latest | Coverage reporting |
| Linting | ruff | latest | Code quality enforcement |
| Git Hooks | pre-commit | latest | Automated checks |
- Async-First: All I/O operations are asynchronous for scalability
- Type Safety: Full type hints throughout for better IDE support and fewer bugs
- Extensibility: Abstract base classes allow for custom implementations
- Simplicity: Clean decorator API hides complexity from end users
- Testability: Dependency injection and abstract interfaces enable easy mocking
This project uses ruff to enforce code style and quality. Configuration is managed through pre-commit.
- PEP 8: Follow Python's standard style guide
- Type Hints: All public APIs must have type hints
- Docstrings: Use Google-style docstrings for all public classes and methods
- Line Length: 88 characters (Black default)
- Import Order: stdlib → third-party → local (managed by ruff)
Automatic (via pre-commit hooks):
pre-commit install # One-time setup
# Now checks run automatically on git commitManual:
# Check all files
pre-commit run --all-files
# Check only staged files
pre-commit rundef create_device(self, device_id: str, create_connection: bool = False) -> Device:
"""
Creates a device associated with the facility.
Args:
device_id: Unique identifier for the device
create_connection: Whether to create a WebSocket connection for live telemetry
Returns:
The created Device instance
Raises:
RuntimeError: If create_connection=True but no connection factory was provided
Example:
>>> facility = Facility("api-key")
>>> device = facility.create_device("sensor-001")
"""Run all tests:
pytest testsRun with coverage:
pytest tests --cov=scadable --cov-report=htmlCoverage report will be available in htmlcov/index.html.
Run specific test file:
pytest tests/test_facility.pyRun specific test:
pytest tests/test_facility.py::test_create_device_no_conn- Location: Place tests in
tests/directory - Naming: Test files must start with
test_ - Async Tests: Use
async defand@pytest.mark.asynciodecorator - Mocking: Use
mock_connection.pyfor connection mocks - Coverage: Aim for >80% code coverage
Example Test:
import pytest
from scadable import Facility
def test_create_device():
facility = Facility("test-api-key")
device = facility.create_device("device-1")
assert device.device_id == "device-1"
assert device in facility.device_manager.devices.values()
@pytest.mark.asyncio
async def test_live_telemetry():
# Test async functionality
facility = Facility("key", connection_factory=MockFactory())
device = facility.create_device("dev-1", create_connection=True)
received_data = []
@facility.live_telemetry("dev-1")
async def handler(data: str):
received_data.append(data)
# Assert handler was registered
assert len(device.parsed_bus) == 1tests/
├── __init__.py
├── mock_connection.py # Mock connection implementations
├── test_connection_type.py # Connection tests
├── test_device.py # Device and DeviceManager tests
├── test_facility.py # Facility tests
└── test_import.py # Import and basic smoke tests
-
Create a branch:
git checkout -b feature/your-feature-name
-
Make your changes:
- Write code following style guidelines
- Add tests for new functionality
- Update documentation if needed
-
Test your changes:
pytest tests pre-commit run --all-files
-
Commit your changes:
git add . git commit -m "feat: add new feature"
Use conventional commits:
feat:- New featurefix:- Bug fixdocs:- Documentation changestest:- Test additions/changesrefactor:- Code refactoringchore:- Build/tooling changes
-
Push and create a PR:
git push origin feature/your-feature-name
Then create a Pull Request on GitHub.
- Title: Use conventional commit format
- Description: Explain what changes were made and why
- Tests: Ensure all tests pass
- Coverage: Don't decrease overall code coverage
- Documentation: Update README.md if user-facing changes
- Review: Be responsive to code review feedback
Good First Issues:
- Documentation improvements
- Test coverage improvements
- Bug fixes
- Example code
Feature Requests:
- New connection types (MQTT, HTTP polling)
- Additional telemetry parsing formats
- Performance optimizations
- Developer tooling improvements
We follow Semantic Versioning:
- MAJOR: Breaking API changes
- MINOR: New features, backward compatible
- PATCH: Bug fixes, backward compatible
For Maintainers Only:
-
Update version in
pyproject.toml:version = "1.2.3"
-
Update CHANGELOG (if exists) with release notes
-
Commit changes:
git add pyproject.toml git commit -m "chore: bump version to 1.2.3" git push -
Create a GitHub Release:
- Go to Releases
- Click "Draft a new release"
- Tag version:
v1.2.3 - Release title:
v1.2.3 - Description: Summarize changes
- Publish release
-
Automated deployment:
- GitHub Actions workflow automatically publishes to PyPI
- Verify at pypi.org/project/scadable
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Organization Docs: Scadable Org
Thank you for contributing to Scadable!