Orval (beta)

September 14, 2026 · View on GitHub

Lint and Test

Orval (beta)

A Python package containing a small set of utility functions not found in Python's standard library. It is lightweight, written in pure Python, and has no dependencies.

Why is it named orval? Because other utility names are boring and it's a tasty Belgian beer 🤘❤️

🚀 Using

To install this package, run:

pip install orval

String utils

from orval import kebab_case

kebab_case("Great Scott")
# Output: great-scott
kebab_case("Gréat Scött")
# Output: gréat-scött
# Slightly different from kebab_case. It does not allow Unicode characters.
# Slugify is well-suited for URL paths or infrastructure resource names (e.g., database names).
from orval import slugify

slugify("Great scott !! 🤘")
# Output: great-scott
slugify("Gréat scött !! 🤘")
# Output: great-scott
from orval import camel_case

camel_case(" Great scott ")
# Output: greatScott
from orval import snake_case

snake_case(" Great  Scott ")
# Output: great_scott
# Train-Case is well-suited for HTTP headers.
from orval import train_case

train_case(" content type ")
# Output: Content-Type
# Strip styling (HTML tags, entities, Unicode-styled chars) from copy/pasted text.
from orval import strip_styling

strip_styling("<b>𝐡𝐞𝐥𝐥𝐨</b> &amp; <i>𝑤𝑜𝑟𝑙𝑑</i>")
# Output: hello & world
strip_styling("𝓯𝓪𝓷𝓬𝔂 café")
# Output: fancy café
# Remove accents/diacritics while preserving non-Latin scripts.
from orval import strip_accents

strip_accents("Héllo Wörld")
# Output: Hello World
strip_accents("café こんにちは")
# Output: cafe こんにちは
# Remove or replace ASCII control characters (newline, tab, NUL, escape, DEL)
# before putting untrusted values in a log line or terminal.
from orval import strip_control

strip_control("user\nname\x00")
# Output: username
strip_control("user\nname", replacement=" ")
# Output: user name
# Check for ASCII control characters instead of removing them, for values that
# should be rejected rather than repaired (e.g. a filename or an identifier).
from orval import has_control

has_control("user\nname")
# Output: True
has_control("café こんにちは")
# Output: False
# Collapse runs of whitespace to a single space and strip both ends, leaving
# punctuation, case and accents untouched.
from orval import squish

squish("  Great   Scott  ")
# Output: Great Scott
squish("hello\nworld")
# Output: hello world
# Redact sensitive values (API keys, tokens, card numbers) while keeping a few
# characters visible. Strings with 'show' or fewer characters are fully masked.
from orval import mask

mask("sk-abc123xyz", show=4)
# Output: ********3xyz
mask("sk-abc123xyz", show=4, side="l")
# Output: sk-a********
mask("abc", show=4)
# Output: ***
# Truncate a string to at most 'number' characters, suffix included.
from orval import truncate

truncate("hello world", 8)
# Output: hello...
truncate("hello world", 8, suffix="")
# Output: hello wo
# Truncate so the UTF-8 encoding fits in 'max_bytes' without splitting a character.
# For the limits protocols and storage impose: a 75-octet iCalendar (RFC 5545) fold,
# a 4096-byte web push payload, a VARCHAR column measured in bytes.
from orval import truncate_bytes

truncate_bytes("héllo wörld", 9)
# Output: héllo w
truncate_bytes("日本語", 7)
# Output: 日本
truncate_bytes("日本語", 7, suffix="…")
# Output: 日…
# The shortest fence that 'content' cannot close early: one longer than the longest run
# of the fence character inside, never shorter than 'minimum'. Use it whenever untrusted
# text goes into a fenced block, so a message carrying its own ``` cannot break out.
from orval import fence

fence("no fences here")
# Output: ```
fence("```python\nprint('hi')\n```")
# Output: ````
fence("~~~", char="~")
# Output: ~~~~

Token utils

# Estimate LLM token counts without a tokenizer dependency (~4 chars or ~0.75
# words per token, slightly denser for code). Not exact, but perfect for
# "will this fit in the context window" guards.
from orval import estimate_tokens

estimate_tokens("Will this prompt fit in the context window?")
# Output: 11
estimate_tokens('def greet(name: str) -> str:\n    return f"Hello {name}"')
# Output: 18
# Truncate a text so its estimated token count fits within a budget.
# Cuts at a word boundary and returns the text unchanged if it already fits.
from orval import truncate_tokens

truncate_tokens("The quick brown fox jumps over the lazy dog.", 5)
# Output: The quick brown fox
truncate_tokens("Short enough.", 1000)
# Output: Short enough.

Collection utils

from orval import chunkify

chunkify([1, 2, 3, 4, 5, 6], 2)
# Output: [[1, 2], [3, 4], [5, 6]]
from orval import flatten

list(flatten([[1, 2], [3, [4]]]))
# Output: [1, 2, 3, 4]
list(flatten([[1, 2], [3, [4]]], depth=1))
# Output: [1, 2, 3, [4]]
list(flatten([{1, 2}, [{3}, (4,)]]))
# Output: [1, 2, 3, 4]
# Drop None values, or all falsy values, from an iterable or the given arguments.
from orval import compact

compact([0, 1, None, 2, False, 3, ""])
# Output: [0, 1, 2, False, 3, '']
compact(0, 1, None, 2)
# Output: [0, 1, 2]
compact([0, 1, None, 2, False, 3, ""], none_only=False)
# Output: [1, 2, 3]
# Order-preserving deduplication, where set() loses the order and dict.fromkeys()
# needs hashable items. 'key' picks what makes two items duplicates: one callable,
# or several to deduplicate on any of them (e.g. an id or an email).
from orval import unique

unique([3, 1, 3, 2, 1])
# Output: [3, 1, 2]
unique(["Great", "great", "Scott"], key=str.lower)
# Output: ['Great', 'Scott']
unique([{"a": 1}, {"a": 1}, {"b": 2}])
# Output: [{'a': 1}, {'b': 2}]
rows = [{"id": 1, "email": "marty@bttf.com"}, {"id": 2, "email": "marty@bttf.com"}]
unique(rows, key=[lambda r: r["id"], lambda r: r["email"]])
# Output: [{'id': 1, 'email': 'marty@bttf.com'}]
# Check whether a value is empty: None or a sized container without elements.
# Unlike truthiness, 0 and False are not empty.
from orval import is_empty

is_empty(None)
# Output: True
is_empty([])
# Output: True
is_empty("")
# Output: True
is_empty(0)
# Output: False
is_empty(False)
# Output: False
from orval import pick

pick({"a": {"b": [1, 2, 3], "c": 4}, "d": 5}, "a.b[0]", "d")
# Output: {'a': {'b': {0: 1}}, 'd': 5}
pick({"a": {"b": 1, "c": 2}}, "a.c", "a.x")
# Output: {'a': {'c': 2}}
# The opposite of pick: drop nested paths, keep everything else.
from orval import omit

omit({"a": {"b": 1, "c": 2}, "d": 5}, "a.b")
# Output: {'a': {'c': 2}, 'd': 5}
omit({"a": [10, 20, 30]}, "a[1]")
# Output: {'a': [10, 30]}
from orval import deep_get

deep_get({"a": {"b": [1, 2, 3]}}, "a.b[0]")
# Output: 1
deep_get({"a": {"b": 1}}, "a.x", default=42)
# Output: 42
# Returns a new dictionary, creating intermediate dictionaries as needed.
from orval import deep_set

deep_set({"a": {"b": 1}}, "a.c", 2)
# Output: {'a': {'b': 1, 'c': 2}}
deep_set({}, "a.b[0]", 1)
# Output: {'a': {'b': {0: 1}}}

Datetime utils

# Normalise a datetime to UTC. Naive datetimes are assumed to already be UTC (the stdlib
# hands those out freely); pass assume_utc=False to raise instead of guessing.
import datetime
from orval import to_utc, utcnow

utcnow()
# Output: datetime.datetime(2024, 1, 1, 12, 0, 0, 123456, tzinfo=datetime.timezone.utc)
to_utc(datetime.datetime(2024, 1, 1, 12, 0))
# Output: datetime.datetime(2024, 1, 1, 12, 0, tzinfo=datetime.timezone.utc)
to_utc(datetime.datetime(2024, 1, 1, 12, 0, tzinfo=datetime.timezone(datetime.timedelta(hours=1))))
# Output: datetime.datetime(2024, 1, 1, 11, 0, tzinfo=datetime.timezone.utc)
to_utc(datetime.datetime(2024, 1, 1, 12, 0), assume_utc=False)
# Raises: ValueError: Datetime must be timezone-aware.
# Convert a datetime to a target time zone. A naive datetime is first assumed to be in `assume`,
# or in the target zone itself when `assume` is omitted; an aware datetime is converted, which a
# bare replace(tzinfo=...) would not do. ZoneInfo reads an IANA time zone database, which some
# platforms lack; install the tzdata package there.
import datetime
from zoneinfo import ZoneInfo
from orval import to_tz

brussels = ZoneInfo("Europe/Brussels")
to_tz(datetime.datetime(2024, 7, 15, 9, 0), brussels)
# Output: datetime.datetime(2024, 7, 15, 9, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/Brussels'))
to_tz(datetime.datetime(2024, 7, 15, 9, 0), datetime.UTC, assume=brussels)
# Output: datetime.datetime(2024, 7, 15, 7, 0, tzinfo=datetime.timezone.utc)
to_tz(datetime.datetime(2024, 1, 15, 9, 0), datetime.UTC, assume=brussels)
# Output: datetime.datetime(2024, 1, 15, 8, 0, tzinfo=datetime.timezone.utc)
to_tz(datetime.datetime(2024, 7, 15, 9, 0, tzinfo=datetime.UTC), brussels)
# Output: datetime.datetime(2024, 7, 15, 11, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/Brussels'))

Misc utils

# Hash any Python object.
from orval import hashify

hashify("great scott")
# Output: 6617ae826b0b76ba9f3a568a2bbf6c67aec8f575eec69badaf7110091d3f5cc6
hashify(b"great scott")
# Output: 6617ae826b0b76ba9f3a568a2bbf6c67aec8f575eec69badaf7110091d3f5cc6 (raw bytes, same digest as the str above)
hashify({"great": "scott"})
# Output: 1d63b966aa065f76392c3e4a7caa7b1bfce39c889e5faf0df0198b9ff5d0f434

def marty():
    return "McFly"

hashify(marty)
# Output: f2f21c93c543f023db0ab78ded26bbc5dabb59bb65b0b458b503cdcb0c3389e4
from orval import pretty_bytes

pretty_bytes(1000)
# Output: 1.00 KB (The "human" decimal format, using base 1000)
pretty_bytes(1000, "bs")
# Output: 1000.00 B (Binary format, using base 1024)
pretty_bytes(20000000, "dl", precision=0)
# Output: 20 Megabytes
pretty_bytes(20000000, "bl", precision=0)
# Output: 19 Mebibytes
from orval import parse_bytes

parse_bytes("1.5 GiB")
# Output: 1610612736
parse_bytes("1.54 KB")
# Output: 1540
parse_bytes("20 Megabytes")
# Output: 20000000
parse_bytes("512")
# Output: 512 (a bare number is interpreted as bytes)
# Coerce loosely-typed input (env vars, query params, config files).
from orval import safe_float, safe_int, to_bool

to_bool("yes")
# Output: True
to_bool("off")
# Output: False
safe_int("3.7")
# Output: 3
safe_int("oops", default=0)
# Output: 0
safe_float("3.14")
# Output: 3.14
safe_float(None, default=1.0)
# Output: 1.0
# First value that is not None (like SQL's COALESCE, or chaining ?? in JS).
# Unlike `a or b or c`, falsy values such as 0, "" and False are kept.
from orval import coalesce, coalesce_lazy

coalesce(None, None, 0, 5)
# Output: 0
coalesce(None, None, 8080)
# Output: 8080

# The lazy variant takes callables, so expensive fallbacks only run when needed.
coalesce_lazy(lambda: cache.get(key), lambda: db.fetch(key))

# When the last value cannot be None, the result is typed as `T` rather than `T | None`,
# so a fallback chain that ends in a constant passes strict type checkers as-is.
def workspace_dir(explicit: Path | None = None) -> Path:
    return coalesce_lazy(lambda: explicit, workspace_from_env, lambda: DEFAULT_WORKSPACE)
from orval import pretty_duration

pretty_duration(9000)
# Output: 2h 30m
pretty_duration(9000, "l")
# Output: 2 hours 30 minutes
pretty_duration(93784)
# Output: 1d 2h 3m 4s
pretty_duration(0.000042)
# Output: 42µs
# The inverse of pretty_duration.
from orval import parse_duration

parse_duration("1h30m")
# Output: 5400.0
parse_duration("2 hours 30 minutes")
# Output: 9000.0
parse_duration("250ms")
# Output: 0.25
parse_duration("90")
# Output: 90.0 (a bare number is interpreted as seconds)
from orval import pretty_number

pretty_number(1234567)
# Output: 1.2M
pretty_number(1234567, "l")
# Output: 1.2 million
pretty_number(1234567890)
# Output: 1.2B
pretty_number(1234567, precision=2)
# Output: 1.23M
pretty_number(999)
# Output: 999

See all available functions in __init__.py.

🧑‍💻 Contributing

Prerequisites
1. Install Docker
  1. Go to Docker, download and install docker.
  2. Configure Docker to use the BuildKit build system. On macOS and Windows, BuildKit is enabled by default in Docker Desktop.
2. Install VS Code

Go to VS Code, download and install VS Code.

1. Open DevContainer with VS Code

Open this repository with VS Code, and run Ctrl/⌘ + + PDev Containers: Reopen in Container.

The following commands can be used inside a DevContainer.

2. Run linters

poe lint

3. Run tests

poe test

4. Update uv lock file

uv lock

See how to develop with PyCharm or any other IDE.


️⚡️ Scaffolded with Uv Copier.
🛠️ Open an issue if you have any questions or suggestions.