Skip to main content

A lightweight, strongly typed event library.

Project description

Eventic

Version Python Coverage MyPy License

A high-performance, strongly-typed event library for Python supporting both synchronous and asynchronous events.
Eventic provides an easy way to implement the event pattern with full IDE support and thread safety.

Table of Contents

Features

  • Type Safety: Strongly typed, fully compatible with many type checkers, with autocomplete for event signatures.
  • Lifecycle Management: Built in lifecycle event hooks allow you to easily manage resources only when listeners are present.
  • Flexible Invocation: Choose between invoke() (awaitable), fire() (background execution), and standard call syntax ().
  • Sync/Async: Handle both synchronous and asynchronous events and handlers.
  • One-Time Listeners: Use once() to automatically unsubscribe after first execution. Perfect for initialization logic.
  • Scoped Subscriptions: Use context managers (with event.subscribed(...)) to ensure handlers are cleaned up automatically.
  • Zero Dependencies: A lightweight, pure Python implementation with no external requirements, making it easy to use in any project.
  • Descriptor Support: Automatic per-instance event creation when used as a class attribute.
  • Thread-Safe: Safe to use in multi-threaded environments.
  • Memory Safe: No memory leaks or "forgotten" background tasks.
  • No Warning Pollution: No RuntimeWarning warnings for un-awaited coroutines.

Installation

  1. Make sure pip is installed on your system.
  2. Run the following command
    pip install pyeventic
    

[!TIP] See here for more details on installing packages.

API Reference

Methods

Method Description
__init__() Initialize a new Event.
subscribe() Subscribe to this event.
unsubscribe() Unsubscribe from this event.
invoke() Invoke the event.
fire() Invoke the event and schedule async work in the background.
wait() Waits for all background tasks to complete.
once() Subscribe to this event for a single invocation.
subscribed() Context manager for temporary subscription.
__iadd__() (+=) Operator overload for subscribing to this event.
__isub__() (-=) Operator overload for unsubscribing from this event.
__call__() (e()) Operator overload to invoke this event
__len__() (len(e)) Return the number of handlers subscribed to this event.
__str__() (str(e)) Return a user-friendly string representation of this event.
__repr__() (repr(e)) Return a detailed string representation of this event.

Attributes

Attribute Description
on_subscribe An event that is invoked when a handler subscribes to this event.
on_unsubscribe An event that is invoked when a handler unsubscribes from this event.

Generics

Variable Type Description
P ParamSpec The signature of an Event.
R TypeVar The return type of an Event. None for synchronous Events, Awaitable[None] for asynchronous Events..

Other

Name Type Description
event() Method Create an Event from a instance or class method.

Strong Typing

Eventic is a strongly typed library. This helps prevent runtime errors while writing your code, and provides nice feature like autocomplete and full docstring support.
Below are a few examples of how the typing system works in Eventic:

Handler mismatch

Handler mismatch example

Awaiting a sync event

Await sync example

Not awaiting an async event

Not awaiting async example

Passing the incorrect arguments to invoke

Incorrect invoke arguments example

Autocomplete and full docstring

docstring example

Getting Started

The Basics

An Event is a dispatcher. You can define a "template" for the event using type hints or function signatures, and then "subscribe" other functions that will be triggered when the event is fired.

from eventic import Event

# Define an event using a function signature
@Event
def on_config_changed(key: str, value: str) -> None: ...

# Subscribe a handler to the event
@on_config_changed.subscribe
def log(key: str, value: str) -> None:
    print(f"[Config] {key} was updated to: {value}")

# Trigger the event (using .invoke() or ())
on_config_changed("theme", "dark-mode")

# Output:
# [Config] theme was updated to: dark-mode

Asynchronous Tasks

Eventic has the ability to mix both sync and async handlers. You can await an event to ensure all async tasks finish, or "fire" it to let them run in the background without blocking the main logic.

Blocking tasks with .invoke() or ()

import asyncio
from eventic import Event

# Define an async event using a function signature
@Event
async def on_user_signup(user_id: int) -> None: ...

# Subscribe a sync handler
@on_user_signup.subscribe
def update_local_cache(user_id: int):
    print(f"User {user_id} added to local cache.")

# Subscribe an async handler
@on_user_signup.subscribe
async def provision_storage(user_id: int):
    await asyncio.sleep(1) # Simulate API call
    print(f"Storage ready for {user_id}!")

async def main():
    # invoke awaits all async handlers before continuing
    await on_user_signup.invoke(42)
    print("Signup flow complete.")

asyncio.run(main())

# Output:
# User 42 added to local cache.
# Storage ready for 42!
# Signup flow complete.

Non-blocking tasks with .fire()

Use .fire() for "Fire and Forget" scenarios, like telemetry or logging, where you don't want to make the user wait for a task to complete.

import asyncio
from eventic import Event

# Define an async event using a function signature
@Event
async def on_video_play(video_id: str) -> None: ...

@on_video_play.subscribe
async def upload_telemetry(video_id: str):
    await asyncio.sleep(2) # Simulate slow network
    print(f"Telemetry: Analytics for {video_id} uploaded.")

async def main():
    print("User clicked play.")
    
    # fire() schedules the async work but doesn't block the UI
    on_video_play.fire("vid_001")
    
    print("Video started! (UI is not blocked)")
    
    # Optionally wait for background tasks before closing the app
    await on_video_play.wait()
    print("All background tasks finished.")

asyncio.run(main())

# Output:
# User clicked play.
# Video started! (UI is not blocked)
# Telemetry: Analytics for vid_001 uploaded.
# All background tasks finished.

Managing Subscriptions

Eventic provides several ways to manage the lifecycle of a subscription beyond regular subscriptions.

Once

With .once(), handlers will run exactly once and then automatically unsubscribe.

from eventic import Event

class Database:
    def __init__(self):
		# Define a sync event with type hinting
        self.on_connected = Event[[str], None]()
    
    def connect(self) -> None:
        print("Connecting to database...")
		# Invoke the event
        self.on_connected("v1.4.2")

db = Database()

# Subscribe to the event using .once()
@db.on_connected.once
def run_initial_migration(version: str) -> None:
    print(f"Running initial migrations for database version {version}...")

db.connect()
db.connect()

# Output:
# Connecting to database...
# Running initial migrations for database version v1.4.2...
# Connecting to database...

Context Manager

The context manager provides scoped subscriptions. Handlers will subscribe when the with block is entered, and unsubscribe when it is exited.

from eventic import Event, event

class Button:
	# Define an event with a method signature
    @event
    def on_click(self, x: int, y: int) -> None: ...

    def click(self, x: int, y: int) -> None:
		# Invoke the event
        self.on_click(x, y)

button = Button()

# The handler is only subscribed within this block
with button.on_click.subscribed(lambda x, y: print(f"handler: Clicked at ({x}, {y})")):
    button.click(10, 20)

button.click(30, 40)

# Output:
# handler: Clicked at 10, 20

Events within Classes

Event can be used as a Descriptor, meaning it can define events at the class level and will automatically handle creating unique events for each object instance.

from eventic import Event

class Battery:
    # Defining an event on the class, but will be unique for every instance of Battery
    on_low_battery = Event[[int], None]()

    def on_power_off(self, percentage: int):
        if percentage <= 5:
            print("Powering off due to low battery.")

    def __init__(self):
        self.percentage = 100
        # Subscribe instance method to its own event
        self.on_low_battery += self.on_power_off

    def drain(self, amount: int):
        self.percentage -= amount
        if self.percentage <= 20:
            # Invoke its own event
            self.on_low_battery.invoke(self.percentage)

laptop = Battery()
phone = Battery() # phone and laptop have independent events
phone.on_low_battery += lambda p: print(f"Low battery: {p}%!")

phone.drain(85)
phone.drain(10)

# Output:
# Low battery: 15%!
# Low battery: 5%!
# Powering off due to low battery.

Subscribe Hooks

Sometimes a system needs to perform work only when something is actually listening (e.g., opening a websocket). on_subscribe and on_unsubscribe can be used to monitor the events listener count.

from eventic import Event

class GameManager:
    def __init__(self) -> None:
        # Define an event for when a player joins the game
        self.on_player_join = Event[[str], None]()

        # Only start listening when someone actually subscribes
        self.on_player_join.on_subscribe.subscribe(self.start_listening)

        # Stop listening when there are no more subscribers
        self.on_player_join.on_unsubscribe.subscribe(self.stop_listening)

    def start_listening(self, event, callable) -> None:
        # If the length of event is 1, it means this is the first subscriber
        if len(event) == 1:
            print(f"Started listening for player_join notifications from the server.")

    def stop_listening(self, event, callable) -> None:
        # If the length of event is 0, it means there are no more subscribers
        if len(event) == 0:
            print(f"Stopped listening for player_join notifications from the server.")

    def join(self, player_name: str) -> None:
        self.on_player_join(player_name)

game_manager = GameManager()

func = lambda name: print(f"Player {name} has joined the game!")
game_manager.on_player_join += func

game_manager.join("Alice")

game_manager.on_player_join -= func

# Output:
# Started listening for player_join notifications from the server.
# Player Alice has joined the game!
# Stopped listening for player_join notifications from the server.

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

pyeventic-0.0.1.tar.gz (837.2 kB view details)

Uploaded Source

Built Distribution

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

pyeventic-0.0.1-py3-none-any.whl (12.2 kB view details)

Uploaded Python 3

File details

Details for the file pyeventic-0.0.1.tar.gz.

File metadata

  • Download URL: pyeventic-0.0.1.tar.gz
  • Upload date:
  • Size: 837.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for pyeventic-0.0.1.tar.gz
Algorithm Hash digest
SHA256 e8ee935e26209d00b7412a9e4356422c2f98b32cdf0f6a2c807a9d0bbd25593f
MD5 71c8650241600022915decc455d261ae
BLAKE2b-256 1e00ec2f000327dd8d2277b9276c400d47ab5fba7b3476aed0dddfdebace0631

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyeventic-0.0.1.tar.gz:

Publisher: cicd.yml on TechnoBro03/Eventic

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pyeventic-0.0.1-py3-none-any.whl.

File metadata

  • Download URL: pyeventic-0.0.1-py3-none-any.whl
  • Upload date:
  • Size: 12.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for pyeventic-0.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 0933c6ef292899cfb19bde9ec21664f435881b59c648b30c4a04c8d121d8ecb8
MD5 1fa277d6f88344dcc191e57634ec388a
BLAKE2b-256 4741c888773b30fb4d0b5968145009c64f1f9e8c5012849d3cd39f31a2889ed7

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyeventic-0.0.1-py3-none-any.whl:

Publisher: cicd.yml on TechnoBro03/Eventic

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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