Skip to main content

Sioba IO Abstraction Layer

Project description

sioba — IO abstraction layer for terminal-like interfaces

A small Python library that unifies interactive IO (functions, TCP/SSL sockets) behind a single async Interface API with pluggable scrollback buffers (terminal emulator or simple line buffer).


Overview

  • Interfaces: concrete implementations for echo://, tcp://host:port, and ssl://host:port, plus a FunctionInterface to wrap regular Python functions into an interactive session.
  • Buffers (scrollback/state): choose terminal:// (ANSI/VT via pyte) or line:// (raw lines). Both keep cursor position and bounded history.
  • URI-driven config: InterfaceContext.from_uri() parses connection + runtime options (rows/cols/title/encoding/convertEol/auto_shutdown, etc.).
  • Callbacks: hook on_send_to_frontend, on_receive_from_frontend, on_shutdown, on_set_terminal_title.
  • Plugin registries: register new interface schemes or buffers via decorators, or expose them via entry points.
  • Rich integration: Interface.filehandle() returns a TTY-like handle (isatty=True) so rich.Console writes render with color.

Installation

From source (recommended until published):

pip install sioba

Using uv:

uv pip install sioba
  • Requires Python ≥ 3.10.
  • Runtime deps: loguru, rich, pyte, janus.

Quickstart

Echo interface (minimal)

import asyncio
from sioba import interface_from_uri, Interface

async def main():
    # uri-like creation of interface instances for consistent
    # invocation syntax that can be easily put into a conf
    echo = await interface_from_uri("echo://").start()

    # Example of callback
    captured = []
    async def on_send_to_frontend(_i: Interface, data: bytes):
        captured.append(data)
    echo.on_send_to_frontend(on_send_to_frontend)

    # Manually injec tdata into the interface instance
    await echo.receive_from_frontend(b"Hello, World!")

    # We should have received the data in our callback
    print(captured[0])           # b"Hello, World!"

    # Finally cleanup
    await echo.shutdown()

asyncio.run(main())

Core Concepts / Features

  • Interface & lifecycle

    • sioba.interface_from_uri("tcp://host:1234?rows=52&cols=100") parses URI + query into fields (scheme, host, port, rows, cols, etc.) creates InterfaceContext, Buffer, and the Interface itself.
    • Interface manages state (INITIALIZEDSTARTEDSHUTDOWN), send/receive, callbacks, and a buffer.
    • await interface.start() then interact via receive_from_frontend() (input) and on_send_to_frontend() (output).
    • get_terminal_buffer() returns a snapshot of current screen/buffer; get_terminal_cursor_position() exposes (row, col).
  • Context

    • InterfaceContext.from_uri("tcp://host:1234?rows=52&cols=100") parses URI + query into fields (scheme, host, port, rows, cols, etc.).
    • Defaults: rows=24, cols=80, encoding=utf-8, convertEol=True, auto_shutdown=True, scrollback_buffer_uri="terminal://", scrollback_buffer_size=10000.
  • Buffers

    • terminal:// uses pyte to emulate ANSI terminals (title changes, scrollback, style attributes, cursor updates).
    • line:// stores raw lines with simple newline splitting.
  • Registries & plugins

    • Interfaces: decorate with @register_scheme("myscheme") or provide entry points under sioba.interface.
    • Buffers: @register_buffer("mybuffer") or entry point sioba.buffer.

High Level Architecture

sioba is composed of 3 primary concepts: transport/control from screen state and configuration:

  • Interface (sioba.interface.base.Interface) owns the lifecycle, transports, and callback wiring.
  • Buffer (sioba.buffer.base.Buffer and implementations) is a sidecar that maintains a screen/scrollback view of what the user should see.
  • InterfaceContext (sioba.context.InterfaceContext) is the shared config/runtime state that both Interface and Buffer read and update.

1 Interface (transport + lifecycle)

Where it lives: sioba.interface.base.Interface (base class) with concrete types like:

  • EchoInterface (echo://) (sioba.interface.echo)
  • SocketInterface (tcp://host:port?args) & SecureSocketInterface (ssl://host:port?args) (sioba.interface.socket)
  • FunctionInterface (sioba.interface.function): Base class, doesn't provide direct functionality but makes scripted behaviors possible

What it does:

  • Manages state (InterfaceState.INITIALIZED/STARTED/SHUTDOWN) and lifecycle: await start(), await shutdown().

  • Normalizes IO:

    • receive_from_frontend(data) handles incoming user data (e.g., keystrokes). If convertEol=True, \r\n/\r\n. Calls an overridable receive_from_frontend_handle.
    • send_to_frontend(data) handles outgoing data (e.g., server output). If convertEol=True, \n\r\n. Calls an overridable send_to_frontend_handle.
    • Always feeds outgoing bytes to the Buffer (await buffer.feed(data)) before dispatching to registered on_send_to_frontend callbacks.
  • Wires callbacks: on_send_to_frontend, on_receive_from_frontend, on_shutdown, on_set_terminal_title.

  • Owns a Buffer instance, created from the context’s scrollback_buffer_uri via buffer_from_uri(...).

  • Exposes screen helpers backed by the Buffer:

    • get_terminal_buffer() → bytes snapshot (after optional EOL conversion).
    • get_terminal_cursor_position()(row, col).
    • set_terminal_size(...) → delegates to the Buffer.
    • update_terminal_metadata({...}) → merges per-client sizes and applies the smallest rows/cols.
  • Provides a file-like handle (filehandle()) via VirtualIO so libraries like rich can write colored output to the interface.

Transport examples:

  • SocketInterface opens an asyncio TCP stream, reads in a background task, and echoes user input locally while writing to the socket.
  • SecureSocketInterface does the same over TLS; its scheme is registered with a custom context class (SecureSocketConfig) to accept a create_ssl_context callable.
  • FunctionInterface runs your function in a thread; it offers print(), input(), getpass() built on internal queues and capture modes.

Discovery / plugins:

  • New schemes register with @register_scheme("myscheme", context_class=...).
  • Or ship via entry points (sioba.interface)—loaded by interface_from_uri("myscheme://...").

2 Buffer (screen/scrollback model)

Where it lives: sioba.buffer.base.Buffer (base) with two built-ins:

  • LineBuffer (sioba.buffer.line, URI: line://) – minimal, newline-split lines.
  • TerminalBuffer (sioba.buffer.terminal, URI: terminal://) – ANSI/VT handling via pyte.

What it does:

  • Receives every outgoing byte stream from the Interface (feed(data)) and updates a persistent view of the screen.

  • Maintains scrollback bounded by context.scrollback_buffer_size (+rows for the line buffer).

  • Provides dump_screen_state() to serialize the view (used by Interface.get_terminal_buffer()).

  • TerminalBuffer is based upon pyte to provide a terminal code aware buffer.

    • It overrides set_title to call interface.set_terminal_title(...), which updates context.title and fires title callbacks.
    • Tracks cursor position and title and writes these into the shared Context:
    • Responds to window resize: set_terminal_size(rows, cols, ...).

Discovery / plugins:

  • New buffers register with @register_buffer("mybuffer") or via entry points under sioba.buffer.
  • Selected by the context’s scrollback_buffer_uri and created through buffer_from_uri("terminal://", interface=..., ...).

3 InterfaceContext (shared config + runtime state)

Where it lives: sioba.context.InterfaceContext (a dataclass).

How it’s created:

  • From a URI: InterfaceContext.from_uri("tcp://host:1234?rows=52&cols=100")
  • Or filled with defaults: InterfaceContext.with_defaults(...)

What it holds:

  • Connection/identity: uri, scheme, host, port, username, password, query, extra_params.
  • Terminal geometry & behavior: rows, cols, encoding, convertEol, auto_shutdown.
  • Buffer config: scrollback_buffer_uri (default "terminal://"), scrollback_buffer_size (default 10000).
  • Runtime metadata written by buffers: cursor_row, cursor_col, title.

Type-aware URI parsing:

  • Query params are cast to the right types (int/bool/etc.) via cast_str_to_type.
  • update(), copy(), and asdict() help compose and inspect context instances.

Per-scheme context:

  • An Interface can declare a specialized context_class (e.g., SecureSocketConfig for SSL) when registering the scheme; interface_from_uri uses it to build the context from the URI.

Data flow (two directions)

1) Frontend → Interface (user input)

bytes from UI
   └─► Interface.receive_from_frontend(...)
        ├─ if convertEol: normalize CR/LF to '\n'
        ├─ call receive_from_frontend_handle(...)  # transport-specific
        └─ fire on_receive_from_frontend callbacks
  • Example behaviors:

    • SocketInterface: writes to the socket (and locally echoes).
    • FunctionInterface: parses input by line / control chars (Enter, Ctrl-C, Backspace) and pushes through its queues.

2) Transport/app → Interface → Buffer → UI (output)

producer (socket read, function print)
   └─► Interface.send_to_frontend(data)
        ├─ if convertEol: '\n' → '\r\n'
        ├─ send_to_frontend_handle(...)   # optional, per interface
        ├─ Buffer.feed(data)              # updates scrollback, cursor, title
        └─ fire on_send_to_frontend callbacks (UI emit)
  • Because Buffer.feed sits in the send path, the UI’s screen snapshot and cursor/title are always consistent with what was emitted.

Lifecycle & sizing

  • await interface.start() transitions to STARTED and lets the concrete interface initialize its tasks/threads (e.g., open sockets, start read loop, spawn function thread).
  • await interface.shutdown() runs shutdown_handle(), switches to SHUTDOWN, then notifies on_shutdown callbacks.
  • UI can call update_terminal_metadata({"rows": R, "cols": C}, client_id=...). The Interface chooses the smallest rows/cols across all clients (tmux-like behavior) and updates the Buffer via set_terminal_size(...).

Extending: new Interfaces & Buffers

  • New transport: subclass Interface, implement start_interface, receive_from_frontend_handle, optional send_to_frontend_handle/shutdown_handle, then @register_scheme("yours") (optionally with a custom context_class).
  • New buffer: subclass Buffer, implement initialize, feed, dump_screen_state, and optional set_terminal_size, then @register_buffer("yours").
  • Plugin packaging: expose classes through entry points sioba.interface or sioba.buffer so interface_from_uri(...) / buffer_from_uri(...) can discover them dynamically.

Absolutely—here’s a focused “how-to extend sioba” that shows subclassing, decorators, entry points, and how InterfaceContext.from_uri() is used under the hood to create instances.


How discovery works (registry + entry points)

  • At import time, using the decorators:

    • @register_scheme("name", context_class=...) adds an Interface subclass to the in-process registry.
    • @register_buffer("name") adds a Buffer subclass to the buffer registry.
  • Lazy discovery via entry points (recommended for plugins):

    • If a scheme/buffer isn’t already in the registry, interface_from_uri / buffer_from_uri will look up the relevant entry point group and ep.load() the class:

      • Interfaces: group sioba.interface
      • Buffers: group sioba.buffer

This means your plugin can be installed as a separate package and only loaded on demand when a user calls interface_from_uri("yourscheme://...") or selects your buffer via scrollback_buffer_uri="yourbuffer://".


The creation pipeline (what happens under the hood)

When you call:

iface = interface_from_uri("myscheme://host:1234?rows=40&cols=100", my_flag=True)
  1. interface_from_uri parses the URI (urlparse), extracts the scheme (myscheme).

  2. If not already registered, it searches entry points in group sioba.interface, loads the matching class, verifies it subclasses Interface, and caches it in the registry.

  3. It picks a context class:

    • If your handler set context_class in @register_scheme(..., context_class=MyContext), that’s used.
    • Otherwise the default InterfaceContext is used.
  4. It builds the context with context_class.from_uri(uri, **kwargs):

    • from_uri parses host/port/query and casts known fields to the right types.
    • Any extra kwargs you pass (e.g., my_flag=True) are merged if they match fields on the context dataclass.
    • Defaults are filled via with_defaults(...) (rows=24, cols=80, convertEol=True, auto_shutdown=True, scrollback_buffer_uri="terminal://", etc.).
  5. It constructs your Interface with that context and any provided callbacks.

  6. Inside Interface.__init__, the chosen Buffer is created via buffer_from_uri(context.scrollback_buffer_uri, interface=self, ...). If the buffer scheme isn’t registered, the loader resolves it via entry points in group sioba.buffer.

  7. You then await iface.start(); your subclass’s start_interface() is called to actually connect/launch resources.


Example 1: A custom Interface with a custom Context

1a) Define a context (extend InterfaceContext)

# mypkg/myproto/context.py
from dataclasses import dataclass
from sioba import InterfaceContext

@dataclass
class MyProtoContext(InterfaceContext):
    # Parsed from URI query or kwargs in interface_from_uri(...)
    my_flag: bool = False
    timeout_ms: int = 5000

Notes:

  • InterfaceContext.from_uri() will cast ?my_flag=true&timeout_ms=2500 to the right types.
  • You get all the base fields too (host, port, rows, cols, encoding, scrollback_buffer_uri, etc.).

1b) Implement the Interface

# mypkg/myproto/interface.py
import asyncio
from sioba.interface.base import Interface, InterfaceState, register_scheme
from .context import MyProtoContext

@register_scheme("myproto", context_class=MyProtoContext)
class MyProtoInterface(Interface):
    reader: asyncio.StreamReader | None = None
    writer: asyncio.StreamWriter | None = None
    _recv_task: asyncio.Task | None = None

    async def start_interface(self) -> bool:
        # Mark started (base does this as well; harmless to be explicit).
        self.state = InterfaceState.STARTED

        ctx: MyProtoContext = self.context  # typed context
        # Use URI parts parsed into context (e.g., host/port) and custom fields:
        self.reader, self.writer = await asyncio.open_connection(ctx.host, ctx.port)
        self._recv_task = asyncio.create_task(self._receive_loop(timeout_ms=ctx.timeout_ms))
        return True

    async def _receive_loop(self, timeout_ms: int):
        try:
            while self.state == InterfaceState.STARTED:
                data = await asyncio.wait_for(self.reader.read(4096), timeout_ms/1000)
                if not data:
                    break
                await self.send_to_frontend(data)   # feeds Buffer, fires callbacks
        except asyncio.TimeoutError:
            # Optional: emit a heartbeat or warning
            pass
        finally:
            await self.shutdown()

    async def receive_from_frontend_handle(self, data: bytes) -> None:
        # normalize CR/LF already handled; you may add protocol framing here:
        if self.writer:
            self.writer.write(data)
            await self.writer.drain()
            # Local echo if you want (SocketInterface does this):
            await self.send_to_frontend(data)

    async def shutdown_handle(self) -> None:
        if self._recv_task:
            self._recv_task.cancel()
        if self.writer:
            self.writer.close()
            try:
                await self.writer.wait_closed()
            except ConnectionAbortedError:
                pass

1c) Use it

from sioba import interface_from_uri

iface = await interface_from_uri(
    "myproto://example.com:9000?rows=40&cols=100&my_flag=true&timeout_ms=2000"
).start()

# interact
await iface.receive_from_frontend(b"HELLO\n")
buf = iface.get_terminal_buffer()
await iface.shutdown()

Everything above mirrors the built-in SocketInterface/SecureSocketInterface pattern, but with your own knobs on the context.


Example 2: A custom Buffer

A buffer transforms outgoing bytes into a durable “screen” snapshot and updates cursor/title on the shared Context.

# mypkg/mybuffer/buffer.py
from sioba.buffer.base import Buffer, register_buffer

@register_buffer("ring")
class RingBuffer(Buffer):
    """
    A tiny byte ring buffer (no ANSI processing).
    """
    def initialize(self, **extra):
        self.capacity = int(extra.get("capacity", 8192))
        self._buf = bytearray()

    async def feed(self, data: bytes) -> None:
        # Called for every Interface.send_to_frontend(data)
        self._buf.extend(data)
        if len(self._buf) > self.capacity:
            # drop oldest
            drop = len(self._buf) - self.capacity
            del self._buf[:drop]

        # Maintain a “cursor” approximation on the shared context
        # (optional but consistent with built-ins):
        text = bytes(self._buf).split(b"\n")[-1]
        col = len(text)
        # rows/cols are bounds in Context; keep within them:
        rows = self.interface.context.rows
        cols = self.interface.context.cols
        row = min(rows - 1, len(bytes(self._buf).splitlines()))
        col = min(cols - 1, col)
        self.interface.context.cursor_row = row
        self.interface.context.cursor_col = col

    def dump_screen_state(self) -> bytes:
        return bytes(self._buf)

    def set_terminal_size(self, rows: int, cols: int, xpix: int = 0, ypix: int = 0) -> None:
        # Nothing needed for a raw ring buffer; could trim to new “visible” size if desired.
        pass

Use it by pointing a context at ring://:

from sioba import Interface, InterfaceContext

ctx = InterfaceContext.with_defaults(
    title="RB",
    rows=10, cols=40,
    scrollback_buffer_uri="ring://",    # uses our RingBuffer
)
iface = Interface(context=ctx)
await iface.start()
await iface.send_to_frontend(b"hello\nworld\n")
print(iface.get_terminal_buffer())     # bytes from our ring
await iface.shutdown()

Internals: Interface.__init__ calls buffer_from_uri(context.scrollback_buffer_uri, interface=self, on_set_terminal_title=self.set_terminal_title). If your buffer scheme isn’t in the process registry, it’s resolved via the sioba.buffer entry point group.


Example 3: Packaging as a plugin with entry points

To make your myproto interface and ring buffer discoverable without importing your module explicitly, expose them via entry points in your package’s pyproject.toml:

[project]
name = "mypkg-sioba-plugins"
version = "0.1.0"
requires-python = ">=3.10"
dependencies = ["sioba>=0.3"]  # pin to your desired version

[project.entry-points]
"sioba.interface".myproto = "mypkg.myproto.interface:MyProtoInterface"
"sioba.buffer".ring = "mypkg.mybuffer.buffer:RingBuffer"

Now, anywhere sioba is installed:

from sioba import interface_from_uri

# "myproto" is lazily discovered via entry points:
iface = await interface_from_uri("myproto://example.com:9000?rows=40&cols=100").start()

No need to import mypkg.myproto.interface manually; interface_from_uri will load it on demand.


Tips & conventions (from sioba internals)

  • Context casting: add fields to your custom context (dataclass) and pass values via URI query (?flag=true) or kwargs to interface_from_uri(...). from_uri will cast strings to the declared types (int/bool/float).

  • EOL policy: Interface.receive_from_frontend normalizes CR/LF to \n; send_to_frontend converts \n to \r\n if convertEol=True. Your transport code should assume normalized \n on input and rely on send_to_frontend for outbound normalization.

  • Screen & title:

    • Buffers should call/trigger interface.set_terminal_title(title) when titles change (the terminal buffer does this inside EventsScreen.set_title).
    • Buffers should update context.cursor_row/col so Interface.get_terminal_cursor_position() remains accurate.
  • Lifecycle: override start_interface() and shutdown_handle() for resources; always handle cancellation and closed streams cleanly.

  • Async vs thread: if you need sync code, see FunctionInterface for a queue-driven pattern and capture modes (ECHO, DISCARD, INPUT, GETPASS).


End-to-end checklist for a new scheme

  1. Create a @dataclass class MyContext(InterfaceContext) with any extra fields you need.

  2. Implement @register_scheme("myscheme", context_class=MyContext) on your subclass of Interface.

  3. (Optional) Implement a custom buffer and @register_buffer("mybuffer").

  4. Package both via pyproject.toml entry points:

    • "sioba.interface".myscheme = "pkg.mod:Class"
    • "sioba.buffer".mybuffer = "pkg.mod:Class"
  5. Use it:

    await interface_from_uri("myscheme://host:1234?rows=40&my_flag=true").start()
    

That’s it—your interface/buffer will be discoverable and type-safe, created with a fully-populated InterfaceContext derived from the URI plus any kwargs you pass.


A few concrete behaviors tied together

  • EOL policy is centralized:

    • Incoming keystrokes normalized (\r\n / \r\n) before reaching a transport.
    • Outgoing data normalized (\n\r\n) before it hits the Buffer/UI.
  • Title changes originate in the Buffer (e.g., TerminalBuffer’s EventsScreen.set_title) and propagate up through Interface.set_terminal_title(...) to the Context and any registered callbacks.

  • Cursor position is a Context field (cursor_row, cursor_col) maintained by the Buffer (via EventsCursor in the terminal buffer, or computed in the line buffer).

  • Rich integration uses Interface.filehandle() (a TextIOBase that sets isatty=True) so things like rich.Console(file=...) print straight into the Interface; internally that just calls send_to_frontend(...), which flows through the Buffer and callbacks.


That’s the architectural core: Interface moves bytes and orchestrates the session, Buffer turns bytes into a durable screen model (plus cursor/title), and Context binds configuration and runtime metadata that both of the others read and write.


Usage

1) FunctionInterface (interactive script)

import asyncio, time
from sioba import FunctionInterface, Interface

def app(ui: FunctionInterface):
    ui.print("Welcome!")
    name = ui.input("What's your name? ")
    ui.print(f"Hello, {name}!")
    hidden = ui.getpass("Enter your hidden word: ")
    ui.print(f"Length noted: {len(hidden)}")
    time.sleep(0.2)

async def main():
    f = FunctionInterface(app)
    await f.start()
    # Simulate terminal input for the two prompts:
    await f.receive_from_frontend(b"Mochi\r\n")
    await f.receive_from_frontend(b"Wasabi\r\n")
    # Read what the function printed to the screen:
    print(f.get_terminal_buffer().decode("utf-8", errors="replace"))
    await f.shutdown()

asyncio.run(main())
  • Input capture modes (internal): ECHO, DISCARD, INPUT, GETPASS. The prompts above demonstrate INPUT and GETPASS.

2) TCP / SSL sockets

import asyncio, ssl
from sioba import interface_from_uri, SocketInterface, SecureSocketInterface

async def tcp_demo():
    sock = await interface_from_uri("tcp://localhost:12345").start()
    out = []
    sock.on_send_to_frontend(lambda _i, d: out.append(d))
    await sock.receive_from_frontend(b"HELLO\n")
    await asyncio.sleep(0.1)
    await sock.shutdown()

async def ssl_demo():
    ctx = ssl._create_unverified_context()  # example from tests
    ssli = await interface_from_uri(
        "ssl://localhost:12345",
        create_ssl_context=lambda _cfg: ctx,  # SecureSocketConfig hook
    ).start()
    await ssli.shutdown()

3) Choosing a buffer & reading state

from sioba import Interface, InterfaceContext

ctx = InterfaceContext.with_defaults(
    title="Demo",
    scrollback_buffer_uri="terminal://",   # or "line://"
    rows=5, cols=80,
)
iface = Interface(context=ctx)
await iface.start()
await iface.send_to_frontend(b"Rich text and ANSI go here\n")
print(iface.get_terminal_buffer())         # bytes snapshot
await iface.shutdown()

4) Registering a custom scheme / buffer

from sioba import register_scheme, register_buffer
from sioba.interface.base import Interface

@register_scheme("myproto")
class MyProto(Interface):
    async def receive_from_frontend_handle(self, data: bytes):
        await self.send_to_frontend(b"ok:" + data)

@register_buffer("dummy")
class DummyBuffer:
    pass

API Highlights

  • sioba.Interface — base class: lifecycle, callbacks, buffer, screen helpers.
  • sioba.InterfaceContext — dataclass; parse/update/copy context; from_uri, with_defaults, asdict, get.
  • sioba.interface_from_uri(uri, **kw) — build interface from scheme://… (+ optional context overrides).
  • sioba.register_scheme(*schemes, context_class=None) / sioba.list_schemes() — plugin registration & discovery.
  • sioba.FunctionInterface — wrap a Python function with print(), input(), getpass() over an interface.
  • sioba.EchoInterfaceecho:// passthrough for testing.
  • sioba.SocketInterfacetcp://host:port using asyncio streams.
  • sioba.SecureSocketInterfacessl://host:port with optional create_ssl_context.
  • sioba.buffer_from_uri(uri, **kw) / sioba.register_buffer(*names) / sioba.list_buffer_schemes() — buffer plugins.
  • sioba.errorsInterfaceNotStarted, InterfaceShutdown, TerminalClosedError, etc.
  • sioba.Interface.filehandle() — TTY-like stream (used by rich.Console(file=...)).

CLI

TODO: No CLI entry points found; add one if a command-line tool is intended.


Security & Limits

  • tcp:// and ssl:// interfaces open network connections; user input is echoed locally and forwarded to the remote server.
  • SecureSocketInterface accepts a custom SSL context; using an “unverified” context (as in tests) disables certificate verification—unsafe for production.
  • FunctionInterface runs your function in a separate thread and can execute arbitrary code; there is no sandboxing.

Compatibility & Requirements

  • Python ≥ 3.10.
  • Tested interfaces/buffers use asyncio, pyte, rich, loguru, janus.
  • Defaults: convertEol=True (outgoing \n\r\n), encoding utf-8.

Contributing

# clone
git clone https://github.com/amimoto/sioba
cd sioba

# (dev) install with uv
uv sync

# run tests
uv run pytest -q
# or, if not using uv:
pytest -q

License

sioba is available under the MIT license. See the LICENSE file for more info.


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

sioba-0.4.2050906.4.tar.gz (24.7 kB view details)

Uploaded Source

Built Distribution

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

sioba-0.4.2050906.4-py3-none-any.whl (30.7 kB view details)

Uploaded Python 3

File details

Details for the file sioba-0.4.2050906.4.tar.gz.

File metadata

  • Download URL: sioba-0.4.2050906.4.tar.gz
  • Upload date:
  • Size: 24.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.8.14

File hashes

Hashes for sioba-0.4.2050906.4.tar.gz
Algorithm Hash digest
SHA256 988b83adb9afddc290479eb256d004983bcc03c844b3b2f7cda9147ba51d2890
MD5 c90aad88458e7dd384a266d9a36fffa7
BLAKE2b-256 7f6eb19f8d99e815673bbae0cfda164c19316ac58e90f81055de63c186885b56

See more details on using hashes here.

File details

Details for the file sioba-0.4.2050906.4-py3-none-any.whl.

File metadata

File hashes

Hashes for sioba-0.4.2050906.4-py3-none-any.whl
Algorithm Hash digest
SHA256 fc2ab35bf3cb66c20ec0a2eb6ddb1ed99480ce18ba065cc95f70cb49297a6513
MD5 ba1625e9a6e34e7fb41a4a1cd0d85b1e
BLAKE2b-256 6f7dddeb6ff0eeb932c13f953c58fd74cd0d54dcbab86ba81498ce5475998b07

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