No project description provided
Project description
Saboteur
A simple and extensible data mutation library for Chaos Engineering in Python.
🤔 What is Saboteur?
Saboteur is a lightweight Python library designed to test the robustness of your data processing logic. It helps you practice Chaos Engineering by intentionally and randomly injecting faulty or unexpected data into your system.
By "attacking" your data with various mutation strategies, Saboteur helps you uncover hidden bugs, handle edge cases gracefully, and build more resilient applications.
✨ Key Features
- Simple API: Get started in seconds with the intuitive
.run()or.run_async()methods. - Randomized Mutations: Automatically selects a random field and applies a random, applicable mutation to simulate real-world unpredictability.
- API Load Testing: Simulate heavy traffic and test the performance of your endpoints using customizable load strategies.
- Extensible: Easily create and add your own custom mutation or load strategies.
- Lightweighted Dependencies: A pure Python library that can be dropped into any project without extra baggage.
💾 Installation
Install Saboteur directly from PyPI:
# pip
pip install saboteur
# poetry
pip install poetry && poetry add saboteur
🚀 Quick Start
Using Saboteur is straightforward. Import the Saboteur class and the desired strategies, then call the mutate method on your data.
from saboteur.application.facade import Saboteur
from saboteur.infrastructure.mutation.strategies.injections import NullInjectionStrategy
from saboteur.infrastructure.mutation.strategies.flippings import TypeFlipStrategy
from saboteur.domain.mutation.configs import MutationConfig
from saboteur.domain.mutation.runners import MutationRunner
# 0. Prepare your data
mock_data = {
"user_id": 12345,
"username": "test_user",
"is_active": True,
"score": 987
}
# 1. Define the strategies you want to use
strategies = [
NullInjectionStrategy(),
TypeFlipStrategy(),
]
# 2. Set configuration what you want
config = MutationConfig(
strategies=strategies,
apply_all_strategies=True,
original_data=mock_data,
)
# 3. Initialize runners with corresponding configuration within Sabeteur Facade.
saboteur = Saboteur(
runners=[
MutationRunner(
config=config,
)
]
)
# 4. Mutate the data! (Run all runners respectively)
# Saboteur will randomly pick one key (e.g., "user_id") and apply one
# applicable strategy (e.g., TypeFlipStrategy).
results = saboteur.run()
# 5. Each runner's results is returned! (Key is id of runner object)
# Example output: {4332166016: MutationResult(result={'age': 25, 'name': 'John', 'active': None, 'score': None, 'nested': {'height': 175.5, 'weight': 70, 'hobbies': ['reading', 'gaming'], 'nested_level_2': {'city': 'New York', 'country': 'USA'}}}, created_at='2026-03-02T07:47:15.115957+09:00', elapsed_time=3.724999260157347e-05)}
print(results)
⚡ Load Testing Quick Start
Saboteur now supports API load testing. You can run linear or custom load strategies asynchronously.
import asyncio
from saboteur.application.facade import Saboteur
from saboteur.domain.load.configs import LoadConfig
from saboteur.domain.load.runners import LoadRunner
from saboteur.infrastructure.load.strategies.linear import LinearLoadStrategy
async def main():
# 1. Define load strategies
strategies = [LinearLoadStrategy()]
# 2. Configure the load test
config = LoadConfig(
strategies=strategies,
url="https://api.example.com/data",
method="GET",
duration_seconds=10,
interval_seconds=1.0,
concurrency=5,
)
# 3. Initialize runner
runner = LoadRunner(config=config)
saboteur = Saboteur(async_runners=[runner])
# 4. Run the load test!
results = await saboteur.run_async()
print(results)
if __name__ == "__main__":
asyncio.run(main())
🛠️ Available Strategies
Saboteur comes with a set of built-in strategies to get you started.
Strategies for MutationRunner
NullInjectionStrategy
Replaces the original value of a field with None. This is useful for testing how your code handles missing or null data.
- Applicable when: The original value is not
None. - Mutation:
original_value->None
TypeFlipStrategy
Changes the data type of a field. Currently supports int to str and str to int conversions. This helps test for TypeError exceptions and weak typing issues.
- Applicable when: The original value is an
intor astr. - Mutation:
int->str(e.g.,123->'123')str->int(e.g.,'456'->456). If the string is not a digit, it returns-1.
RandomizationStrategy
Replaces a value with a randomized value of the same type. This is useful for testing how your system handles a wide range of valid but unexpected inputs. Saboteur provides several randomization strategies based on data type.
-
IntegerRandomizationStrategy(from_value: int = -1000, to_value: int = 1000)- Applicable when: The original value is an
int. - Mutation: Replaces the value with a random integer within the specified range (
from_valuetoto_value).
- Applicable when: The original value is an
-
FloatRandomizationStrategy(from_value: float = -1000.0, to_value: float = 1000.0)- Applicable when: The original value is a
float. - Mutation: Replaces the value with a random float within the specified range.
- Applicable when: The original value is a
-
StringRandomizationStrategy(length: int = 10)- Applicable when: The original value is a
str. - Mutation: Replaces the value with a random alphanumeric string of the specified
length.
- Applicable when: The original value is a
-
BooleanRandomizationStrategy- Applicable when: The original value is a
bool. - Mutation: Replaces the value with a randomly chosen
TrueorFalse.
- Applicable when: The original value is a
-
ListRandomizationStrategy- Applicable when: The original value is a
list. - Mutation: Creates a new list of the same length by randomly sampling elements from the original list (with replacement).
- Applicable when: The original value is a
-
DictRandomizationStrategy- Applicable when: The original value is a
dict. - Mutation: Replaces the dictionary with a new one where the order of keys is shuffled.
- Applicable when: The original value is a
Strategies for LoadRunner
LinearLoadStrategy
Sends requests at a fixed interval for a specified duration.
- Description: Distributes
concurrencynumber of requests everyinterval_seconds. - Configurable via:
LoadConfig(duration, interval, concurrency).
✍️ Creating a Custom Strategy
You can easily create your own strategies by inheriting from MutationStrategy and implementing two methods: is_applicable and apply.
Here's an example of a BooleanFlipStrategy that flips True to False and vice-versa.
# custom_strategies.py
from typing import Any
from saboteur.domain.mutation.strategies import MutationStrategy
from saboteur.domain.mutation.contexts import MutationContext
from saboteur.domain.mutation.configs import MutationConfig
class BooleanFlipStrategy(MutationStrategy):
"""Flips a boolean value."""
def is_applicable(self, context: MutationContext) -> bool:
# This strategy only applies to boolean types
return isinstance(context.original_value, bool)
def apply(self, context: MutationContext) -> Any:
# The mutation logic is simple: flip the boolean
return not context.original_value
# You can then use it with Saboteur:
strategies = [
BooleanFlipStrategy(),
# ... other strategies
]
config = MutationConfig(
strategies=strategies,
... # other options
)
🤝 Contributing
Contributions are welcome! Whether it's adding new strategies, improving documentation, or fixing bugs, please feel free to open an issue or submit a pull request.
- Fork the repository.
- Create your feature branch (
git checkout -b feature/AmazingFeature). - Commit your changes (
git commit -m 'Add some AmazingFeature'). - Push to the branch (
git push origin feature/AmazingFeature). - Open a Pull Request.
🗺️ Roadmap
Saboteur is expanding to become a comprehensive resiliency testing tool. Future versions will include:
- More Load Strategies: Poisson distribution, ramping (step-up) load, etc.
- Enhanced Reporting: Visual summaries and performance metrics for load tests.
- Protocol Support: Expanding beyond HTTP (e.g., gRPC, WebSocket).
📄 License
This project is licensed under the MIT License. See the LICENSE file for details.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file saboteur-0.1.8.tar.gz.
File metadata
- Download URL: saboteur-0.1.8.tar.gz
- Upload date:
- Size: 13.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/2.3.2 CPython/3.13.12 Linux/6.14.0-1017-azure
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
62dae01a1dbc188361e646ed4e803fc0cd0633748ced300e6caed6e006f4af24
|
|
| MD5 |
8aea485904384031181e16afa7512d63
|
|
| BLAKE2b-256 |
97ce44971b84fc6394c2a6ac51e81b87efe489ad1f7c8c10d4b80e9cb0f2168c
|
File details
Details for the file saboteur-0.1.8-py3-none-any.whl.
File metadata
- Download URL: saboteur-0.1.8-py3-none-any.whl
- Upload date:
- Size: 18.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/2.3.2 CPython/3.13.12 Linux/6.14.0-1017-azure
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bdbb67df814b25ba1d0ddbcd1b0b720f397e04430bc472b78f3a0bdb4bb82389
|
|
| MD5 |
e76cc758712aa6de8c082e05fe39962f
|
|
| BLAKE2b-256 |
3245e458a1ab80df5ba0ea9ff8f1dc57240a7c981ccc057c5b52ebee4c40c0dd
|