Skip to main content

A light-service like project in Python

Project description

pyservice

build PyPI version Coverage License

A light-service influenced project in Python.

Intro

Are you tired of 500 lines long Python code with conditionals, iterators, and function calls? Testing this logic is close to impossible, and with the lack of testing, you don't dare to touch it.

All complex logic can be decomposed into small functions, invoked sequentially. The functions should protect themselves from execution if a previous failure occurs, a more elegant solution exists: Railway-Oriented Programming.

Let's see how that looks with pyservice. There are two functions, one that adds 2 to the initial number, and one that adds 3. The data is carried over between the functions in an extended dictionary we call Context, just like how a conveyor belt would be used in an assembly line:

from pyservice import action, Context, Organizer


@action()
def add_two(ctx: Context) -> Context:
    number = ctx.get("n", 0)

    ctx["result"] = number + 2

    return ctx


@action()
def add_three(ctx: Context) -> Context:
    result = ctx["result"]

    ctx["result"] = result + 3

    return ctx


def test_can_run_functions():
    ctx = Context.make({"n": 4})
    organizer = Organizer([add_two, add_three])
    result_ctx = organizer.run(ctx)

    assert ctx.is_success
    assert result_ctx["result"] == 9

The Context is an extended dictionary, it stores failure and success states in it besides its key-value pairs. This is the "state" that is carried between the actions by the Organizer. All Organizers expose a run function that is responsible for executing the provided actions in order.

This is the happy path, but what happens when there is a failure between the two functions? I add a fail_context function that will fail the context with a message:

@action()
def fail_context(ctx: Context) -> Context:
    ctx.fail("I don't like what I see here")
    return ctx

The context will be in a failure state and only the first action will be executed as processing stops after the second action (4+2=6):

def test_can_run_functions_with_failure():
    ctx = Context.make({"n": 4})
    organizer = Organizer([add_two, fail_context, add_three])
    result_ctx = organizer.run(ctx)

    assert ctx.is_failure
    assert result_ctx["result"] == 6

Look at the actions, no conditional logic was added to them, the function wrapper protects the action from execution once it's in a failure state.

You can find these examples here.

But there is more to it!

Expects and Promises

You can define contracts for the actions with the expects and promises list of keys like this:

@action(expects=["n"], promises=["result"])
def add_two(ctx: Context) -> Context:
    number = ctx.get("n", 0)

    ctx["result"] = number + 2

    return ctx


@action(expects=["result"])
def add_three(ctx: Context) -> Context:
    result = ctx["result"]

    ctx["result"] = result + 3

    return ctx

The action will verify - before it's invoked - that the expected keys are in the Context hash. If there are any missing, ExpectedKeyNotFoundError will be thrown and all of the missing keys will be listed in the exception message. Similarly, PromisedKeyNotFoundError is raised when the action fails to provide a value with the defined promised keys.

You can find the relevant examples here.

Rollback

One of your actions might fail while they have logic that permanently changes state in a data store or in an API resource. A trivial example is charging your customer while you can't complete the order. When that happens, you can leverage pyservice's rollback functionality like this:

def add_two_rollback(ctx: Context) -> Context:
    ctx["result"] -= 2
    return ctx


@action(expects=["n"], promises=["result"], rollback=add_two_rollback)
def add_two(ctx: Context) -> Context:
    number = ctx.get("n", 0)

    ctx["result"] = number + 2

    return ctx


@action()
def fail_context(ctx: Context) -> Context:
    ctx.fail("I don't like what I see here")
    raise Organizer.ContextFailed(fail_context)

The action accepts a function reference to roll back its state changes when a Context fails. The rollback field is optional, nothing happens when you don't provide one.

Take a look at this basic example.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pyservice-0.0.11.tar.gz (5.4 kB view details)

Uploaded Source

Built Distribution

pyservice-0.0.11-py3-none-any.whl (5.8 kB view details)

Uploaded Python 3

File details

Details for the file pyservice-0.0.11.tar.gz.

File metadata

  • Download URL: pyservice-0.0.11.tar.gz
  • Upload date:
  • Size: 5.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.0.10 CPython/3.9.0 Darwin/19.6.0

File hashes

Hashes for pyservice-0.0.11.tar.gz
Algorithm Hash digest
SHA256 a17a9a28f94b985b28aa45683ac12eb9b39f49a8d941cdc87fd78576ecdf9eee
MD5 c58d55c352c19a7f03d055214485f7f7
BLAKE2b-256 96379c30886565cae0d989b3cc6e4d91e00eb48b8deda4d5b105e3d23a3885f5

See more details on using hashes here.

File details

Details for the file pyservice-0.0.11-py3-none-any.whl.

File metadata

  • Download URL: pyservice-0.0.11-py3-none-any.whl
  • Upload date:
  • Size: 5.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.0.10 CPython/3.9.0 Darwin/19.6.0

File hashes

Hashes for pyservice-0.0.11-py3-none-any.whl
Algorithm Hash digest
SHA256 f80afb63383a427ad5d651c15879d245ba4cbce4f60c807b36296b682f1bd3f3
MD5 d154786e4a7f3733bda7a5954921152f
BLAKE2b-256 5fd6752018d769bc14b65643f494254755dd115a3568e48794f64018d878aa05

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page