Tiny framework for bootstrapping apps
Project description
bean.core
bean.core is a tiny Python toolkit to bootstrap small apps.
It gives you ready-to-use building blocks so you can focus on the business logic, instead of boilerplate.
Overview
With bean.core you get:
- App lifecycle: start, run, shutdown
- Config: load from multiple sources (
env,.py, ...) with validation. - Logging.
- Pipes: composable data flows.
- Shell commands: run commands directly in your flows.
- Scheduler: jobs & tasks.
Just enough to cook some beans.
Installation
Requirements:
- Python
3.14+
Using pip:
pip install --upgrade bean-core
Using curl (direct download):
FILE="src/bean/core.py"
mkdir -p "$(dirname "$FILE")"
curl -Lso "$FILE" \
https://raw.githubusercontent.com/numen-0/bean/refs/heads/main/bean-core/src/bean/core.py
Quick Examples
This is a quick reference for the main API.
For full details, peek at the source code :).
Simple Loop App
In this example:
- define a custom app by overriding the
runmethod and optionallystartupandshutdown. - set up logging.
- script a infinite loop on run.
- run
bean.mainwith ourApp()
So, the app will loop until a SIGINT or SIGTERM signal is received. On
shutdown, the shutdown method is called, but you can force termination by
sending another signal.
from time import sleep
from bean.core import BeanApp, Log, Logger, main, shutdown_requested
class App(BeanApp):
def startup(self):
Log.init(
level=Logger.Level.from_debug(self.DEBUG),
handlers=[Logger.TermHandler(Logger.fmt(color=True))]
)
Log.debug("starting...")
def shutdown(self):
Log.debug("ending...")
def run(self):
while not shutdown_requested():
Log.info(":D doing nothing")
sleep(2)
Log.warning("xD shutdown requested, exiting run loop")
return 0
if __name__ == "__main__":
main(App("bean-app", debug=True))
Config
BeanConfig can be populated from multiple sources, with a defined priority. Available sources:
.from_args(): parse command-line arguments withargparse.from_dict(): load configuration from a Python dictionary.from_env(): load environment variables matchingprefix + filedname.from_ini(): Load from aINIfile.from_json(): load from aJSONfile.from_py(): load from a Python file exposing aConfigobject by default.from_toml(): Load from aTOMLfile
Note: File loaders will skip missing files unless
force=Trueis passed
from enum import Enum
from bean.core import BeanConfig, ConfigField, isHost, isPort, isEmail
class Color(Enum):
RED = "#ff0000"
GREEN = "#00ff00"
BLUE = "#0000ff"
class MyConfig(BeanConfig):
NAME = ConfigField(str)
DEBUG = ConfigField(bool, default=False, short_flag="-d")
PORT = ConfigField(int, default=8080, validator=isPort)
HOST = ConfigField(str, default="localhost", validator=isHost)
EMAIL = ConfigField(str, validator=isEmail)
COLOR = ConfigField(Color, default=Color.RED)
@BeanConfig.validate("NAME")
def check_empty_name(name):
return len(name) > 0
( MyConfig.load() # load priority:
.from_env("APP_") # 1. environment variables
.from_py("./config.py") # 2. Python file (ignored if not found)
.from_args() # 3. command-line arguments (auto --help)
).build()
MyConfig.print_config()
Note: If your type checker complains about check_empty_name, add
@staticmethodor# type: ignore.
Pipes
Pipe is a small, composable transformation pipeline.
Each stage:
- Receives a value
- Returns either:
Success(value)ortuple(value, True)tuple(value, ok)
- If
ok == False, the pipeline short-circuits
This makes it easy to build safe, expressive and composable data flows.
from bean.core import Pipe
result = (
Pipe()
.guard(lambda x: x != 0)
.map(lambda x: 10 / x)
)(5)
print(result) # Success(2.0, ok=True)
Note: pipes can be typed, e.g:
Pipe[float, float]()
If the guard fails:
result = (
Pipe()
.guard(lambda x: x != 0)
.map(lambda x: 10 / x)
)(0)
print(result) # Success(value=0, ok=False)
The pipe short-circuits and the division step is never executed.
Available Pipe helpers:
.map(fn): transform value.guard(fn): validate value (may short-circuit).peek(fn): side-effect without modifying value.retry(fn, attempts, delay): retry a stage.fallback(fn, fallback_value): recover from failure.branch(cond, success_fn, fail_fn): conditional logic.trigger(fn, ex, msg): raise exception if condition matches
Pipes are fully composable using the
|operator.
Shell Commands
Shell commands integrate directly into pipes.
sh(cmd) returns a Pipe that executes a shell command.
from bean.core import sh, cat, tee, stdout
res = ()
print(
(sh("echo 'hello bean'")
| sh("grep -F 'hello'")
| tee("copy.txt")
| stdout()
)(None)
)
Available shell helpers:
sh(cmd): run commandcat(*paths): read filestee(*paths): write to filesstdout(): extract stdoutstderr(): extract stderr
Scheduler
Scheduler provides a minimal threaded task runner for delayed and periodic execution.
It supports:
- One-shot tasks
- Delayed tasks
- Periodic jobs
- Limited or infinite runs
- Graceful shutdown
Basic example:
from bean.core import Scheduler
( Scheduler()
.task(lambda: print("Bean task once"))
).start()
Periodic Job:
from bean.core import Scheduler
schr = (
Scheduler()
.job(
fn=lambda: print("Bean job running!"),
interval=2.0,
runs=5
)
).start()
# ...
schr.join() # wait until finite jobs complete
Mixed example:
import time
from bean.core import Scheduler
with Scheduler()
.task(lambda: (print("Init task"), time.sleep(5), print("Done")))
.job(lambda: print("Heartbeat"), interval=1.0)
) as schr:
...
Note: When exiting the
withblock, the scheduler automatically callsschr.stop().join()to gracefully stop infinite jobs.
API Overview:
-
.task(fn, runs=1, delay=0)-> Schedule a task that runs:- Runs
runstimes (default: once) or indefinitely ifruns=None - Starts after an optional
delay
- Runs
-
.job(fn, interval, runs=None)-> Schedule a periodic job:- Runs every
intervalseconds - Runs indefinitely by default (
runs=None)
- Runs every
Lifecycle Methods:
.start(): start all tasks.join(timeout=None): wait for completion (non-infinite only).stop(): signal stop.clear(): remove all tasks
All methods return
Selffor chaining.
Behavior Notes:
- Each task runs in its own daemon thread
- Finite tasks (
runs != None) blocks.join()until completion or timeout - Infinite tasks (
runs=None) are ignored by.join() - Tasks must exit voluntarily, blocking calls may delay shutdown
- Because threads are daemon, all tasks terminate when the main program exits
License
All the repo falls under the MIT 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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file bean_core-0.2.0.tar.gz.
File metadata
- Download URL: bean_core-0.2.0.tar.gz
- Upload date:
- Size: 24.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
880ef6ad2188619e00996add87551297395ed3d7ffbcdc541ea73ed448bfdeb0
|
|
| MD5 |
543e8cf9b07e8408644d153b4b201252
|
|
| BLAKE2b-256 |
7c1ba92acd71189fa97da33a4fce4237d55dfa7533a73e6299d3cd3a99aabc0e
|
File details
Details for the file bean_core-0.2.0-py3-none-any.whl.
File metadata
- Download URL: bean_core-0.2.0-py3-none-any.whl
- Upload date:
- Size: 20.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
35c601e2cfa7c0929104c09678c902f18c4e7865dd1213e691bd2fbf4094ccfc
|
|
| MD5 |
71c81484908c9131106f17a90674c44e
|
|
| BLAKE2b-256 |
9a3f80d21da2ea4cfcbc217ff97b51285ea3690400a6e6a4751266d87c7cca5c
|