DEV Community

Cover image for contractsPY — Python library for handling business transactions with railway-oriented approach
Arzu Huseynov
Arzu Huseynov

Posted on

contractsPY — Python library for handling business transactions with railway-oriented approach

contractsPY — Python library for handling business transactions with railway-oriented approach

In this tutorial, I’ll show you how to handle business transactions properly on Django (also applies to other frameworks.)

class CreateUserDjango(APIView):
    serializer_class = UserSerializer

    def post(self, request):

        username=request.data.get('username')
        password=request.data.get('password')

        if not username or not password:
            return Response({'error': 'Input values are not valid.'})

        user = user_repo.generate_user(username,password)
        data = {'error' : 'User not generated'}
        if user:
            data = {'error' : 'User already exists'}
            user_not_exists = user_repo.user_exists(username)
            if not user_not_exists:
                user_created = user_repo.save_user(user)
                if user_created:
                    data = self.serializer_class(user).data
                else:
                    data = {'message': 'User not persisted.'}
        return Response(data)
Enter fullscreen mode Exit fullscreen mode

Look at this code. What do you see? Yes, it’s a Django Rest Framework’s APIView class that is intended to create a new user in our project. But, I want you to focus on details. What do you see? Is it readable code? Is it handling all the edge cases you might have? (Probably not.) Or what if your manager said that we need to add new functionalities in this particular view? What do you gonna do?

Or let’s assume that you’re the first developer who wrote the code. You know what to do in here, right? This is a familiar codebase and you can update it as your manager requests. But, what if you’re the new guy on the team and your manager wants you to update this code. Well, you know what I mean. We have all once been there.

Writing this kind of code is nasty and makes the development process expensive and complex. Projects, which have been written like this are a pain in the ass. But can we find a solution for this? Yeah, people develop software projects for the last 50 years. Some bright-minded people had offered tons of useful patterns, approaches for us to develop things properly. Proper implementation of those patterns and approaches is life-saving.

Recently, I have developed a tiny python library for handling business transactions on my own projects. Then I decided to share it with other developers for payback to our beloved python community.

class CreateUserContracts(APIView):
    """ Simple APIView to create a user with contractsPY. """
    serializer_class = UserSerializer

    def post(self, request):
        username = request.data.get('username')
        password = request.data.get('password')
        result = create_user.apply(username=username, password=password)
        if result.is_success():
            data = self.serializer_class(result.state.user).data
        else:
            data = {'message': result.message}    
        return Response(data)
Enter fullscreen mode Exit fullscreen mode

Now, look at this code. What do you see? This is the same view as the previous one. Except, there are not too many details in here. In this code, the view is the only interface for our actual business transaction. It gets data from users and returns manipulated data. Not manipulating data in the view.
The second approach is easy to implement and makes our codebase readable, extendible and testable. I can hear you’re telling “Yeah, that’s nice and I can implement a specific object for handling my use-case. Why do I need to use additional dependency for stuff like that?”. Well, I’m glad you asked.

Railway-Oriented Programming

contractsPY library uses the Railway-Oriented Programming approach to handle your business transactions. But, what is this approach? What does it mean after all?

Every step(function) in your project has two possible returns. (Success or Failure). All systems are basically built on these returns. Like Lego. If you want to handle every possible scenario in your system, the railway approach helps you do it. I think its name tells a lot. I don’t want to go deeper about this topic. But, you should watch this youtube video and learn about it if you don’t know about it yet.

On the first code example, we have seen multiple if-else statements in our view. Remember, this is the simple register API. If you want to implement a real-world registration API, there will be many more conditional statements in your view class. Because there could be different types of inputs to validate, check and create. On the other hand, in our second code example, there are no checks, validations or DB operations. But where are these conditional statements? Well, actually we don’t need to write them. At least, like the first code example.

from contractsPY import if_fails, Usecase

from users.repository import UserRepository


user_repo = UserRepository()


if_fails(message="Input values are not valid.")
def validate_inputs(state):
    if state.username and state.password:
        return True
    return True

@if_fails(message="User already exists.")
def validate_user_exists(state):
    exists = user_repo.user_exists(state.username)
    return True if not exists else False

@if_fails(message="User not generated.")
def generate_user(state):
    state.user = user_repo.generate_user(state.username, state.password)
    return True if state.user else False

@if_fails(message="User not persisted.")
def persist_user(state):
    user_created = user_repo.save_user(state.user)
    return True if user_created else False


# Usecase 1 - Create user
create_user = Usecase()
create_user.contract = [
    validate_inputs,
    validate_user_exists,
    generate_user,
    persist_user,
]
Enter fullscreen mode Exit fullscreen mode

This is the contractsPY implementation for the second code example. I want you to focus on the initialization of Usecase class and the settings of the contract. With contractsPY, you can create a chain of simple python functions, that accepts only one argument (state) and returns a boolean value. Also, consider that, you can use these simple functions on other use-cases too. So, we can say these functions are reusable components.

Functions are called by respectively and change the current state of the use-case until it finishes. If every function returned True, then it means that your use-case is successful.

>>> Result(state={'username': 'johndoe', 'password': 'foobar', 'user': User(username=johndoe, password=foobar)}, case=success, message='Ok')
Enter fullscreen mode Exit fullscreen mode

This is the result object. You can see three different fields. State data, case and message. After a successful transaction, you can use state values to return desired data. (in this case, it’s user value.)

>>> Result(state={'username': 'johndoe', 'password': 'foobar', 'user': User(username=johndoe, password=foobar)}, case=error, message='User exists.')

result.state = {'username': 'johndoe', 'password': 'foobar', 'user': User(username=johndoe, password=foobar)}

result.case = error

result.message = 'User exists.'
Enter fullscreen mode Exit fullscreen mode

What if our transaction fails? You have seen if_fails decorator in the previous example. With help of this decorator, you can print human-readable messages. This decorator is completely optional and the contractsPY library doesn’t force you to use it.

Thanks for reading. Every feedback, contribution, stars, forks are more than welcomed.

Project Github

Top comments (0)