Railway Oriented Programming Library
Project description
safeguard
This is a simple library for building happy path in python, this looks like a Rust's Result
type.
This library is inspired by two other libraries that are returns
and result.
TODO
- update README to provide more examples
- generate Documentation
- add more tests
- add tox for testing
- add CI/CD
Installation
Latest release:
$ pip install safeguard
Latest GitHub main
branch version:
$ pip install git+https://github.com/demetere/safeguard
TL/DR
The main purpose of this library is to provide a way to build happy path without thinking about exceptions. Because of
that we have two main Containers Maybe
and Result
.
Maybe
Maybe
is a way to work with optional values. It can be either Some(value)
or Nothing
.
Some
is a class encapsulating an arbitrary value. Maybe[T]
is a generic type alias
for typing.Union[Some[T], None]
. Lets imagine having function for division
def divide(a: int, b: int) -> Optional[int]:
if b == 0:
return None
return a // b
We want to do some operation on the result but we dont know if it returned None or not. We can do it like this:
def upstream_function():
result = divide(10, 0)
if result is None:
return None
return result * 2
or we can use Maybe
like this:
from safeguard.maybe import Maybe, Some, Nothing, maybe
def divide(a: int, b: int) -> Maybe[int]:
if b == 0:
return Nothing
return Some(a // b)
def upstream_function():
result = divide(10, 0)
return result.map(lambda x: x * 2)
What maybe does in this context is that it will return Nothing
if the result is None
and Some
if the result is
not None
.
This way we can chain operations on the result without thinking about handling None
values. Alternatively we can
use decorators to use Maybe in a more elegant way:
from safeguard.maybe import maybe
@maybe
def divide(a: int, b: int) -> int:
if b == 0:
return None
return a // b
def upstream_function():
return divide(10, 0).map(lambda x: x * 2)
This will automatically handle None values and return Nothing
if the result is None
.
Result
The idea is that a result value can be either Ok(value)
or
Err(error)
, with a way to differentiate between the two. Ok
and
Err
are both classes inherited from Result[T, E]
.
We can use Result
to handle errors in a more elegant way. Lets imagine having function for fetching user by email:
def get_user_by_email(email: str) -> Tuple[Optional[User], Optional[str]]:
"""
Return the user instance or an error message.
"""
if not user_exists(email):
return None, 'User does not exist'
if not user_active(email):
return None, 'User is inactive'
user = get_user(email)
return user, None
user, reason = get_user_by_email('ueli@example.com')
if user is None:
raise RuntimeError('Could not fetch user: %s' % reason)
else:
do_something(user)
We can refactor this code to use Result
like this:
from safeguard.result import Ok, Err, Result
def get_user_by_email(email: str) -> Result[User, str]:
"""
Return the user instance or an error message.
"""
if not user_exists(email):
return Err('User does not exist')
if not user_active(email):
return Err('User is inactive')
user = get_user(email)
return Ok(user)
user_result = get_user_by_email(email)
if isinstance(user_result, Ok): # or `user_result.is_ok()`
# type(user_result.unwrap()) == User
do_something(user_result.unwrap())
else: # or `elif user_result.is_err()`
# type(user_result.unwrap_err()) == str
raise RuntimeError('Could not fetch user: %s' % user_result.unwrap_err())
If you're using python version 3.10
or later, you can use the
elegant match
statement as well:
from result import Result, Ok, Err
def divide(a: int, b: int) -> Result[int, str]:
if b == 0:
return Err("Cannot divide by zero")
return Ok(a // b)
values = [(10, 0), (10, 5)]
for a, b in values:
match divide(a, b):
case Ok(value):
print(f"{a} // {b} == {value}")
case Err(e):
print(e)
Contributing
Everyone is welcome to contribute.
You just need to fork the repository, run poetry install
so you can have the same environment,
make your changes and create a pull request. We will review your changes and merge them if they are good.
Related Projects
The inspiration was taken from following libraries, some of the ideas and code fragments are from their codebase:
License
MIT License
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
File details
Details for the file safeguard-0.1.0.tar.gz
.
File metadata
- Download URL: safeguard-0.1.0.tar.gz
- Upload date:
- Size: 15.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/1.8.2 CPython/3.12.0 Linux/6.5.0-35-generic
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | b8cc27b1c99fb272d74ed4039c55c5104ca3e3bbfeecb7b5e46aff46d25129ee |
|
MD5 | 7984d050b1d57bd9c65109482f625bf7 |
|
BLAKE2b-256 | b2999566eb7097dbbf078e1d8c9e2de953d30a7e04a8ceb08daa7455efaef55f |
File details
Details for the file safeguard-0.1.0-py3-none-any.whl
.
File metadata
- Download URL: safeguard-0.1.0-py3-none-any.whl
- Upload date:
- Size: 15.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/1.8.2 CPython/3.12.0 Linux/6.5.0-35-generic
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | e0ffb620a10ff3ed22004b82bc4404e4ceae2776fa7e8f613bdf138d59ece981 |
|
MD5 | b7d47a7f6e451292be95b29f6365b3da |
|
BLAKE2b-256 | 178f12d24894b7af7e67bc1e76e067dbaa1aece5d0ca4d954c522c366dd4c9b7 |