Skip to main content

Tool to simplify the communication of event based services.

Project description

EventPeople

CircleCI

EventPeople is a tool to simplify the communication of event based services. It is an based on the EventBus gem.

The main idea is to provide a tool that can emit or consume events based on its names, the event name has 4 words (resource.origin.action.destination) which defines some important info about what kind of event it is, where it comes from and who is eligible to consume it:

  • resource: Defines which resource this event is related like a user, a product, company or anything that you want;
  • origin: Defines the name of the system which emitted the event;
  • action: What action is made on the resource like create, delete, update, etc. PS: It is recommended to use the Simple Present tense for actions;
  • destination (Optional): This word is optional and if not provided EventPeople will add a .all to the end of the event name. It defines which service should consume the event being emitted, so if it is defined and there is a service whith the given name only this service will receive it. It is very helpful when you need to re-emit some events. Also if it is .all all services will receive it.

As of today EventPeople uses RabbitMQ as its datasource, but there are plans to add support for other Brokers in the future.

Installation

Add this line to your project's requirements.txt file:

event_people>=0.0.3

And then execute

python -m pip install -r requirements.txt

Or install it with:

python -m pip install event_people

And set env vars:

export RABBIT_URL='amqp://guest:guest@localhost:5672'
export RABBIT_EVENT_PEOPLE_APP_NAME='service_name'
export RABBIT_EVENT_PEOPLE_VHOST='event_people'
export RABBIT_EVENT_PEOPLE_TOPIC_NAME='event_people'

Usage

Events

The main component of EventPeople is the Event class which wraps all the logic of an event and whenever you receive or want to send an event you will use it.

It has 2 attributes name and payload:

  • name: The name must follow our conventions, being it 3 (resource.origin.action) or 4 words (resource.origin.action.destination);
  • payload: It is the body of the massage, it should be a Hash object for simplicity and flexibility.
from event_people import Event
from event_people import Emitter

name = 'user.users.create'
body = { id: 42, name: 'John Doe', age: 35 }

event = Event(name, body)

There are 3 main interfaces to use EventPeople on your project:

  • Calling event_people.Emitter.trigger(event) inside your project;
  • Calling event_people.Listener.on(event_name) inside your project;
  • Or extending event_people.ListenersBase and use it as a daemon.

Using the Emitter

You can emit events on your project passing an event_people.event.Event instance to the event_people.emitter.trigger method. Doing this other services that are subscribed to these events will receive it.

from event_people import Event
from event_people import Emitter

event_name = 'receipt.payments.pay.users'
body = { amount: 350.76 }
event = Event(event_name, body)

Emitter.trigger(event)

See more details

Listeners

You can subscribe to events based on patterns for the event names you want to consume or you can use the full name of the event to consume single events.

We follow the RabbitMQ pattern matching model, so given each word of the event name is separated by a dot (.), you can use the following symbols:

  • * (star): to match exactly one word. Example resource.*.*.all;
  • # (hash): to match zero or more words. Example resource.#.all.

Other important aspect of event consumming is the result of the processing we provide 3 methods so you can inform the Broker what to do with the event next:

  • success!: should be called when the event was processed successfuly and the can be discarded;
  • fail!: should be called when an error ocurred processing the event and the message should be requeued;
  • reject!: should be called whenever a message should be discarded without being processed.

Given you want to consume a single event inside your project you can use the event_people.Listener.on method. It consumes a single event, given there are events available to be consumed with the given name pattern.

from event_people import Listener
from event_people import Event


def callback_method(event, context):
    print("")
    print("  - Received the %r message from %r:", event.name, event.origin)
    print("     Message: %r", event.body)
    print("")

    context.success()


event_name = 'resource.origin.action.all'

Listener.on(event_name, callback_method)

See more details

You can also receive all available messages using a loop:

from event_people import Listener
from event_people import Event

has_events = true

def callback_method(event, context):
    has_events = true
    print("")
    print("  - Received the %r message from %r:", event.name, event.origin)
    print("     Message: %r", event.body)
    print("")

    context.success()


event_name = 'resource.origin.action.all'


while(has_events):
    has_events = false
    Listener.on(event_name, callback_method)

Multiple events routing

If your project needs to handle lots of events you can extend eventPeople.ListenersBase class to bind how many events you need to instance methods, so whenever an event is received the method will be called automatically.

from event_people import ListenersBase
from event_people import Event

class CustomEventListener(Base):
    self.bind('resource.custom.pay', self.pay)
    self.bind('resource.custom.receive', self.receive)
    self.bind('resource.custom.private.service', self.private_channel)

    def pay(event):
        print("Paid %r for %r ~> %r", event.body['amount'], event.body['name'], event.name)

        self.success()

    def receive(event):
        if (event.body.amount > 500):
          print("Received %r from %r ~> %r", event.body['amount'], event.body['name'], event.name)
      else:
          print("[consumer] Got SKIPPED message")
          return self.reject()

          self.success();

  def private_channel(event):
    print("[consumer] Got a private message: \"%r\" ~> %r", event.body['message'], event.name)

    self.success();

See more details

Creating a Daemon

If you have the need to create a deamon to consume messages on background you can use the eventPeople.Daemon.start method to do so with ease. Just remember to define or import all the event bindings before starting the daemon.

from event_people import Daemon
from event_people import BaseListener

class CustomEventListener(BaseListener):
    def pay(self, event):
        print(f"Paid {event.body['amount']} for {event.body['name']} ~> {event.name}")

        self.success()

    def receive(self, event):
        if event.body['amount'] > 500:
            print(f"Received {event.body['amount']} from {event.body['name']} ~> {event.name}")
        else:
            print('[consumer] Got SKIPPED message')

            return self.reject()

        self.success()

    def private_channel(self, event):
        print(f"[consumer] Got a private message: \"{event.body['message']}\" ~> {event.name}")

        self.success()

    def ignore_me(self, event):
        print(f"This should never be called...")
        print(f"Spying on other systems: \"{event.body['message']}\" ~> {event.name}")

        self.success()

CustomEventListener.bind_event('resource.*.pay', 'pay')
CustomEventListener.bind_event('resource.custom.receive', 'receive')
CustomEventListener.bind_event('resource.custom.private.service_name', 'private_channel')
CustomEventListener.bind_event('resource.custom.ignored.other_service', 'ignore_me')
CustomEventListener.bind_event('resource.custom.pay.all', 'receive')

print('****************** Daemon Ready ******************');

Daemon.start()

See more details

Retry and Dead Letter Queue (DLQ)

Environment variables

Variable Description Default
RABBIT_EVENT_PEOPLE_MAX_RETRIES Max retry attempts before dead-lettering 3
RABBIT_EVENT_PEOPLE_RETRY_TTL_MS Base delay in ms for retry backoff 1000

How it works

On context.fail():

  • If retries remain → message is published to {queue}_retry with exponential backoff delay, then acked
  • If retries exhausted → nacked to DLQ via RabbitMQ DLX

On context.reject() → nacked directly to DLQ (no retries)

Delay strategies:

  • exponential (default): min(initialDelay × 5^retryCount, 600000) ms
  • fixed: constant initialDelay ms

Queue topology (auto-created on subscribe)

Queue/Exchange Name Purpose
Exchange (DLX) {appName}_dlx Fanout, receives dead-lettered messages
DLQ {appName}_dlq Final resting place for failed messages
Retry queue {queue_name}_retry Holds messages until backoff delay expires

Usage

from event_people import Listener, Event

def handle_event(event: Event, context):
    print(f"Attempt {event.retry_count + 1} of {context.max_retries}")

    if event.body.get("invalid"):
        context.reject()   # → DLQ immediately, no retries
        return

    try:
        process(event)
        context.success()
    except Exception:
        if context.is_last_retry:
            print("Final attempt failed, sending to DLQ")
        context.fail()     # → retry queue (or DLQ if exhausted)

# Per-listener retry config (overrides env var defaults)
Listener.on("order.service.created", handle_event,
            max_attempts=5,
            delay_strategy="exponential")

# Fixed delay
Listener.on("user.service.updated", handle_event,
            max_attempts=3,
            delay_strategy="fixed")

Development

After checking out the repo, run bin/setup to install dependencies. Then, run bin/test to run the tests.

To install this package onto your local machine, run python -m pip install -e ..

Contributing

  • Fork it
  • Create your feature branch (git checkout -b my-new-feature)
  • Commit your changes (git commit -am 'Add some feature')
  • Push to the branch (git push origin my-new-feature)
  • Create new Pull Request

License

The package is available as open source under the terms of the LGPL 3.0 License.

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

event_people-0.1.3.tar.gz (18.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

event_people-0.1.3-py3-none-any.whl (17.5 kB view details)

Uploaded Python 3

File details

Details for the file event_people-0.1.3.tar.gz.

File metadata

  • Download URL: event_people-0.1.3.tar.gz
  • Upload date:
  • Size: 18.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/4.0.2 CPython/3.10.2

File hashes

Hashes for event_people-0.1.3.tar.gz
Algorithm Hash digest
SHA256 ae184d542dc6829667410ec6beab3a8298abddf1faca209a2f4b66b9566b66b9
MD5 1635b04b06e5b8fa5296432d115c42c5
BLAKE2b-256 02b99e88fa9dcad58518b84309461c45cb2a7b612078af5acaed9cff90d64adf

See more details on using hashes here.

File details

Details for the file event_people-0.1.3-py3-none-any.whl.

File metadata

  • Download URL: event_people-0.1.3-py3-none-any.whl
  • Upload date:
  • Size: 17.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/4.0.2 CPython/3.10.2

File hashes

Hashes for event_people-0.1.3-py3-none-any.whl
Algorithm Hash digest
SHA256 2d7d46a4115e5a16e3ccb271654426e4f262cc6559d4106bc99d4572892352e1
MD5 2eba1b359e55d77927f43cbb51c0710e
BLAKE2b-256 5ecc61fec5531fa5da5ef51da4d666dd2497452d99d9437fff38018e702e5a24

See more details on using hashes here.

Supported by

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