Genro-Toolbox
Essential utilities for the Genro ecosystem
Part of Genro Kyō ecosystem.
A lightweight, zero-dependency Python library providing core utilities that can be used across all Genro projects.
Installation
pip install genro-toolbox
Features
- extract_kwargs - Decorator to group kwargs by prefix
- metadata - Decorator to stamp keyword arguments as attributes on functions or classes
- dictExtract - Extract dict items by key prefix
- NamedList - Mutable list with access by column name or position and a shared index (guide)
- smartsplit - Split strings honoring escaped separators
- get_uuid - Sortable 22-char unique identifiers for distributed systems
- sign / verify - HMAC-signed payloads with optional expiry, for data that leaves the server and comes back
- smartasync - Unified sync/async API with automatic context detection
- safe_is_instance - isinstance() without importing the class
- tags_match - Boolean expression matcher for tag-based filtering
- dates.parse_period - Natural-language date periods ("mese scorso", "from Q1 to Q2") to a start/end date pair, multilingual
Examples
NamedList
Available since 0.18.0: mutable rows with access by name and position, iteration over values, and explicit conversion to a dictionary.
from genro_toolbox import NamedList
index = {"code": 0, "total": 1}
row = NamedList(index, ["ABC", 10])
row["total"] = 20
assert row[1] == 20
assert tuple(row) == ("ABC", 20)
assert dict(row) == {"code": "ABC", "total": 20}
The constructor raises ValueError if positions are not unique integers
covering 0 .. len(index)-1, or the supplied value count differs from the
index size. values=None initializes every field to None. This rejects
misaligned rows produced by duplicate query aliases before data can be overwritten.
Rows can share the same index. Adding a named field extends the index;
existing sibling rows read the new field as None until assigned.
Structural list mutations do not maintain the index, and pop is deprecated.
See the guide for copying and remaining legacy limits.
extract_kwargs Decorator
from genro_toolbox import extract_kwargs
@extract_kwargs(logging=True, cache=True)
def my_function(name, logging_kwargs=None, cache_kwargs=None, **kwargs):
print(f"logging: {logging_kwargs}")
print(f"cache: {cache_kwargs}")
my_function(
"test",
logging_level="INFO",
logging_format="json",
cache_ttl=300,
)
# logging: {'level': 'INFO', 'format': 'json'}
# cache: {'ttl': 300}
metadata Decorator
Stamp keyword arguments as attributes on a function or class:
from genro_toolbox import metadata
@metadata(mixin_order=10)
class Core:
pass
Core.mixin_order # 10
@metadata(prefix="rpc", public=True)
def handler():
pass
handler.rpc_public # True
safe_is_instance
from genro_toolbox import safe_is_instance
# Check type without importing
safe_is_instance(42, "builtins.int") # True
safe_is_instance(my_obj, "mypackage.BaseClass") # True (includes subclasses)
tags_match
from genro_toolbox import tags_match
# Simple tag check
tags_match("admin", {"admin", "user"}) # True
# OR (comma, pipe, or keyword)
tags_match("admin,public", {"public"}) # True
tags_match("admin or public", {"admin"}) # True
# AND (ampersand or keyword)
tags_match("admin&internal", {"admin", "internal"}) # True
tags_match("admin and internal", {"admin"}) # False
# NOT (exclamation or keyword)
tags_match("!admin", {"public"}) # True
tags_match("not admin", {"admin"}) # False
# Complex expressions
tags_match("(admin|public)&!internal", {"admin"}) # True
tags_match("(admin or public) and not internal", {"admin", "internal"}) # False
get_uuid
from genro_toolbox import get_uuid
# Generate sortable unique identifiers
uid = get_uuid() # e.g., "Z00005KmLxHj7F9aGbCd3e"
len(uid) # 22 characters
uid[0] # 'Z' (version marker, sorts after legacy UUIDs)
uid.isalnum() # True (URL-safe)
# IDs are lexicographically sortable by creation time
ids = [get_uuid() for _ in range(3)]
sorted(ids) == ids # True (already sorted)
sign / verify
Authenticate data that leaves the server and comes back — a payload handed to a client, put in a URL or a cookie, then returned to be acted upon. The key stays server-side: sign on the way out, verify on the way in.
from genro_toolbox import sign, verify, SignatureError, SignatureExpired
token = sign("/srv/data", key=SECRET, expires_in=300)
# 'L3Nydi9kYXRh.MTc4NTMyMzIyMQ.FmMftICRb9MQVrFUwoo9qPCg-R5SYVpKmXA2oXQdmLg'
verify(token, key=SECRET) # '/srv/data'
Tampering and expiry are both rejected:
try:
path = verify(token_from_client, key=SECRET)
except SignatureExpired:
... # signature was valid, the token is just too old
except SignatureError:
... # forged, altered, or signed with another key
The token is three base64url fields joined by ., so the payload round-trips
byte for byte whatever it contains — separators, unicode, XML-unsafe characters:
payload = '{"exclude": "*.tmp;*.bak"}'
verify(sign(payload, key=SECRET), key=SECRET) == payload # True
The expiry sits inside the signed area, so it cannot be stripped or extended.
Omit expires_in for a token that never expires.
smartasync
from genro_toolbox import smartasync
class DataManager:
@smartasync
async def fetch_data(self, url: str):
async with httpx.AsyncClient() as client:
return await client.get(url).json()
manager = DataManager()
# Sync context - no await needed!
data = manager.fetch_data("https://api.example.com")
# Async context - use await
async def main():
data = await manager.fetch_data("https://api.example.com")
# Also works with sync methods in async context (offloaded to thread)
class LegacyProcessor:
@smartasync
def cpu_intensive(self, data):
return process(data) # Blocking operation
async def main():
proc = LegacyProcessor()
result = await proc.cpu_intensive(data) # Won't block event loop
smartsplit
from genro_toolbox import smartsplit
smartsplit("a.b.c", ".") # ['a', 'b', 'c']
smartsplit(r"a\.b.c", ".") # ['a\\.b', 'c'] (escaped separator preserved)
smartsplit("one , two , three", ",") # ['one', 'two', 'three'] (strips whitespace)
dictExtract
from genro_toolbox import dictExtract
kwargs = {"logging_level": "INFO", "logging_format": "json", "cache_ttl": 300}
dictExtract(kwargs, "logging_") # {'level': 'INFO', 'format': 'json'}
dates.parse_period
Parses a period written in natural language into a start and an end date,
relative to a workdate. Either bound may be None (open period); both None
means no filter. Unrecognized text raises PeriodError (a ValueError)
carrying value and locale.
from datetime import date
from genro_toolbox.dates import parse_period
wd = date(2026, 4, 15)
parse_period("mese scorso", wd, "it") # 2026-03-01 .. 2026-03-31
parse_period("oggi-30;", wd, "it") # 2026-03-16 .. None
parse_period("from january to march", wd, "en")
parse_period("ottobre scorso", wd, "it") # 2025-10-01 .. 2025-10-31
parse_period("Q1 2024", wd, "en") # 2024-01-01 .. 2024-03-31
parse_period("10/1/26", wd, "it") # 2026-01-10 .. 2026-01-10
Supported forms: years, today/oggi with +n/-n, this/last/next week and
month, quarters, months with year or last/next, weekdays, ISO and locale
dates, ranges with ; or from ... to / dal ... al. The full rules are in
the genro_toolbox.dates.period_parser module docstring. Two-digit years use a
window around the workdate set by pivot_year (default 20).
Languages live in JSON files (genro_toolbox/dates/locales/it.json,
en.json). To add one, write <language>.json with the same keys; pass
locale_dir= to load it from your own folder. A missing file or key raises
PeriodLocaleError.
Philosophy
If you write a generic helper that could be useful elsewhere, put it in genro-toolbox.
This library serves as the foundation for utilities shared across:
- genro-asgi
- genro-routes
- genro-api
- Other Genro Kyō projects
License
Apache License 2.0 - See LICENSE for details.
Copyright 2025 Softwell S.r.l.
Metadata
Release files for genro-toolbox 0.18.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| genro_toolbox-0.18.0.tar.gz | 1.7 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| genro_toolbox-0.18.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 1.8 MB
Release files / genro_toolbox-0.18.0.tar.gz
| Download URL | genro_toolbox-0.18.0.tar.gz |
|---|---|
| Size | 1.7 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
0b8203cfed4b7dde27463dcd4af11ea6947190256eaf06c9b4b094a41cc6e94b
|
|
BLAKE2b-256 checksum How to use checksums |
c67ca72acaf60965b136e8763abddaa5a5055af68e625357458127cdfb8c7655
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.
Transparency logRelease files / genro_toolbox-0.18.0-py3-none-any.whl
| Download URL | genro_toolbox-0.18.0-py3-none-any.whl |
|---|---|
| Size | 34.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8436eccde8a8a384111443601583ff687b46fb4a503a7915dc7c385a1a291679
|
|
BLAKE2b-256 checksum How to use checksums |
b29b0b48b880d348cc7a9c9dcaa25f4e112ff2c238f0e10316759b2235f4b4b0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.
Transparency log