Skip to main content

Foundational base classes/protocols used to manage state and provide clear errors

Project description

whatamithinking-protolib

Foundational base classes and protocols for managing lifecycle state in Python objects, with clear, descriptive errors and support for both synchronous and asynchronous patterns.


Installation

pip install whatamithinking-protolib

Requires Python 3.12+

whatamithinking-aiotools is installed automatically as a dependency (required by AsyncLockable).


Overview

protolib provides a consistent pattern for objects with managed lifecycle states. Each module follows the same design:

  • An enum describing the valid states.
  • Exception classes that describe exactly what went wrong and why.
  • Guard decorators (ensure_*) that protect methods from being called in an invalid state.
  • State-transition decorators (opener, connector, etc.) that handle state changes automatically.
  • Abstract base classes in both sync (Openable, Connectable, ...) and async (AsyncOpenable, AsyncConnectable, ...) variants.
import whatamithinking.protolib as protolib

Modules

Openable

Manages an open/close lifecycle, suitable for resources like file handles, serial ports, or network sockets.

States — OpenStateType

State Description
CLOSED Initial/default state. The object is not in use.
OPENING Transitioning to open.
OPEN The object is open and operational.
CLOSING Transitioning to closed.
ERROR An error occurred during open or close.

Exceptions

Exception Raised when...
OpenStateError Base class for all open-state errors.
NotOpenError A method requires OPEN state but the object is not open.
NotClosedError open() was called but the object is not currently CLOSED.
ClosedError A method was called while the object is CLOSED.

Guard Decorators

These decorators check state before executing the method and raise an exception if the check fails.

Decorator Raises if...
@ensure_open Object is not in OPEN state → NotOpenError
@ensure_closed Object is not in CLOSED state → NotClosedError
@ensure_not_closed Object is in CLOSED state → ClosedError

Standalone check functions (check_open, check_closed, check_not_closed) are also available for use without decorators.

State-Transition Decorators

Decorator Usage
@opener Apply to open(). Handles OPENINGOPEN (or ERROR) transitions. No-op if already OPEN. Requires object to be CLOSED first.
@closer Apply to close(). Handles CLOSINGCLOSED (or ERROR) transitions. No-op if already CLOSED.

Base Classes

Openable — Synchronous. Implement open() and close(). Supports use as a context manager (with).

AsyncOpenable — Asynchronous. Implement open() and close() as coroutines. Supports use as an async context manager (async with).

Example

from typing import Optional
import whatamithinking.protolib as protolib


class MyResource(protolib.Openable):
    @protolib.opener
    def open(self, timeout: Optional[float] = None) -> None:
        # allocate resources here
        ...

    @protolib.closer
    def close(self, timeout: Optional[float] = None) -> None:
        # release resources here
        ...

    @protolib.ensure_open
    def read(self) -> bytes:
        # only runs when object is OPEN
        ...


# Using as a context manager
with MyResource() as r:
    data = r.read()

# Or manually
r = MyResource()
r.open()
try:
    data = r.read()
finally:
    r.close()

Connectable

Manages a connect/disconnect lifecycle with support for polling-based keepalive, suitable for network clients, database connections, etc.

States — ConnectionStateType

State Description
DISCONNECTED Initial/default state.
CONNECTING Transitioning to connected.
CONNECTED The object has an active connection.
DISCONNECTING Transitioning to disconnected.
ERROR A connection or disconnection error occurred.

Exceptions

Exception Raised when...
ConnectionError Base class for all connection-related errors.
NotConnectedError A method requires CONNECTED state but is not connected.
NotDisconnectedError connect() was called but the object is not DISCONNECTED.
NotBadConnectionError A method requires DISCONNECTED or ERROR state but neither applies.
DisconnectedError A method was called while the object is DISCONNECTED.

Guard Decorators

Decorator Raises if...
@ensure_connected Object is not CONNECTEDNotConnectedError
@ensure_not_disconnected Object is DISCONNECTEDDisconnectedError
@ensure_bad_connection Object is not in DISCONNECTED or ERRORNotBadConnectionError

Standalone check functions (check_connected, check_not_disconnected, check_bad_connection) are also available.

State-Transition Decorators

Decorator Usage
@connector Apply to connect(). Handles CONNECTINGCONNECTED (or ERROR) transitions. No-op if already CONNECTED. Requires DISCONNECTED state.
@disconnector Apply to disconnect(). Handles DISCONNECTINGDISCONNECTED (or ERROR) transitions. No-op if already DISCONNECTED.
@poller Apply to poll(). Updates state to CONNECTED or ERROR based on the method's return value.

Base Classes

Connectable — Synchronous. Implement connect(), disconnect(), and poll(). Supports use as a context manager. Includes a keepalive() method for smart polling.

AsyncConnectable — Asynchronous. Same interface with coroutines. Supports use as an async context manager.

Keepalive

Both Connectable and AsyncConnectable include a keepalive() method. Set connection_keepalive_timeout (a datetime.timedelta) on the class, then call keepalive() periodically. It will only call poll() if the connection has not been used recently — avoiding redundant I/O.

import datetime

class MyClient(protolib.Connectable):
    connection_keepalive_timeout = datetime.timedelta(seconds=30)

    @protolib.connector
    def connect(self, timeout=None): ...

    @protolib.disconnector
    def disconnect(self, timeout=None): ...

    @protolib.poller
    def poll(self, timeout=None) -> bool:
        # return True if connected, False otherwise
        return self._ping()

Example

import whatamithinking.protolib as protolib


class DatabaseClient(protolib.Connectable):
    @protolib.connector
    def connect(self, timeout=None) -> None:
        # establish connection
        ...

    @protolib.disconnector
    def disconnect(self, timeout=None) -> None:
        # teardown connection
        ...

    @protolib.poller
    def poll(self, timeout=None) -> bool:
        # return True if still connected
        return self._ping()

    @protolib.ensure_connected
    def query(self, sql: str):
        ...


with DatabaseClient() as db:
    results = db.query("SELECT 1")

Enableable

Manages an enable/disable lifecycle, suitable for toggling features or subsystems on and off.

States — EnabledStateType

State Description
DISABLED Initial/default state.
ENABLED The object is enabled and operational.

Exceptions

Exception Raised when...
EnableableError Base class for enable-state errors.
NotEnabledError A method requires ENABLED state but the object is disabled.

Guard Decorators

Decorator Raises if...
@ensure_enabled Object is not ENABLEDNotEnabledError

A standalone check_enabled() function is also available.

State-Transition Decorators

Decorator Usage
@enabler Apply to enable(). Sets state to ENABLED after the method runs. No-op if already ENABLED.
@disabler Apply to disable(). Sets state to DISABLED after the method runs. No-op if already DISABLED.

Base Classes

Enableable — Synchronous. Implement enable() and disable().

AsyncEnableable — Asynchronous. Implement enable() and disable() as coroutines.

Example

import whatamithinking.protolib as protolib


class Subsystem(protolib.Enableable):
    @protolib.enabler
    def enable(self, timeout=None) -> None:
        # start up subsystem
        ...

    @protolib.disabler
    def disable(self, timeout=None) -> None:
        # shut down subsystem
        ...

    @protolib.ensure_enabled
    def run(self) -> None:
        ...

Lockable

Provides thread-safe (or async-safe) locking for objects using a simple method decorator.

Decorator

Decorator Description
@locked Acquires the object's lock before executing the method and releases it when done. Works with sync, async, generator, and async generator methods.

Base Classes

Lockable — Synchronous. Uses a threading.RLock (reentrant). Pass a custom lock to __init__, or one is created automatically.

AsyncLockable — Asynchronous. Uses an aiotools.RLock (reentrant). Pass a custom lock to __init__, or one is created automatically.

Example

import whatamithinking.protolib as protolib


class SafeCounter(protolib.Lockable):
    def __init__(self):
        super().__init__()
        self._count = 0

    @protolib.locked
    def increment(self):
        self._count += 1

    @protolib.locked
    def value(self) -> int:
        return self._count

Logable

Provides a structured logging interface built on Python's standard logging module.

LogLevelType

An enum mirroring standard log levels: DEBUG, INFO, WARNING, ERROR, CRITICAL.

Base Class — Logable

Pass a log_name to __init__ to configure a named logger with a NullHandler (following best practices for libraries). Logging output is controlled by the consumer of the library.

Method Description
_log(level, msg, **kwargs) Log a message at the given level with any extra context.
_log_extra() Override to return a dict of additional metadata included in every log record for this object.

At ERROR level or above, exc_info is automatically captured.

Example

import whatamithinking.protolib as protolib


class MyService(protolib.Logable):
    def __init__(self):
        super().__init__(log_name="myservice")

    def do_work(self):
        self._log(protolib.LogLevelType.INFO, "Starting work")
        try:
            ...
        except Exception:
            self._log(protolib.LogLevelType.ERROR, "Work failed")
            raise

Combining Mixins

All base classes are implemented with cooperative multiple inheritance (super().__init__(**kwargs)), so they can be freely combined:

import whatamithinking.protolib as protolib


class MyDevice(protolib.Openable, protolib.Lockable, protolib.Logable):
    def __init__(self):
        super().__init__(log_name="mydevice")

    @protolib.opener
    def open(self, timeout=None) -> None:
        self._log(protolib.LogLevelType.INFO, "Opening device")
        ...

    @protolib.closer
    def close(self, timeout=None) -> None:
        self._log(protolib.LogLevelType.INFO, "Closing device")
        ...

    @protolib.locked
    @protolib.ensure_open
    def read(self) -> bytes:
        ...

leaf_method

The @leaf_method utility ensures that state-transition decorators (like @opener, @connector, @enabler) only execute their wrapping logic at the leaf (most-derived) class in an inheritance chain. This means intermediate super().open() calls in a subclass hierarchy won't trigger redundant state transitions — only the outermost call does.


License

MIT — see 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

whatamithinking_protolib-1.0.0.tar.gz (17.1 kB view details)

Uploaded Source

Built Distribution

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

whatamithinking_protolib-1.0.0-py3-none-any.whl (15.8 kB view details)

Uploaded Python 3

File details

Details for the file whatamithinking_protolib-1.0.0.tar.gz.

File metadata

  • Download URL: whatamithinking_protolib-1.0.0.tar.gz
  • Upload date:
  • Size: 17.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.9

File hashes

Hashes for whatamithinking_protolib-1.0.0.tar.gz
Algorithm Hash digest
SHA256 2f53514ac94529420f6d65368968dc1971499cf8485d9f26e5ece36a85a187c9
MD5 6039680d3a0f3d9fd075da20a1835d24
BLAKE2b-256 1349d1c7ae8e140167b7d06a588958ca98a25fb982689f503155f1da90279b6c

See more details on using hashes here.

File details

Details for the file whatamithinking_protolib-1.0.0-py3-none-any.whl.

File metadata

File hashes

Hashes for whatamithinking_protolib-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 5b5ddd521f9e2f5741bfbbcaff7d63ca4d09afcfb67cf721c310e26d6715ba9f
MD5 d70f84502e3faed1f6ce105dd47cf802
BLAKE2b-256 b588f36bcfa874bc25db4aac253569de01da9633416655b3a411ec57eb7a018d

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