Metadata-Version: 2.4
Name: orval
Version: 0.0.13
Summary: 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.
Author: Maarten Huijsmans
Author-email: Maarten Huijsmans <maarten@lukin.be>
License-Expression: MIT
Classifier: Typing :: Typed
Classifier: Development Status :: 3 - Alpha
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Information Technology
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Internet
Classifier: Topic :: Utilities
Requires-Python: >=3.11.0, <4.0
Project-URL: homepage, https://github.com/lukin0110/orval/
Project-URL: source, https://github.com/lukin0110/orval/
Description-Content-Type: text/markdown

[![Lint and Test](https://github.com/lukin0110/orval/actions/workflows/test.yml/badge.svg)](https://github.com/lukin0110/orval/actions)

# 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](https://en.wikipedia.org/wiki/Orval_Brewery) 🤘❤️

## 🚀 Using

To install this package, run:
```bash
pip install orval
```

### String utils
```python
from orval import kebab_case

kebab_case("Great Scott")
# Output: great-scott
kebab_case("Gréat Scött")
# Output: gréat-scött
```

```python
# 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
```

```python
from orval import camel_case

camel_case(" Great scott ")
# Output: greatScott
```

```python
from orval import snake_case

snake_case(" Great  Scott ")
# Output: great_scott
```

```python
# Train-Case is well-suited for HTTP headers.
from orval import train_case

train_case(" content type ")
# Output: Content-Type
```

```python
# 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é
```

```python
# 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 こんにちは
```

```python
# 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
```

```python
# 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
```

```python
# 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
```

```python
# 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: ***
```

```python
# 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
```

```python
# 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: 日…
```

`````python
# 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

```python
# 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
```

```python
# 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

```python
from orval import chunkify

chunkify([1, 2, 3, 4, 5, 6], 2)
# Output: [[1, 2], [3, 4], [5, 6]]
```

```python
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]
```

```python
# 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]
```

```python
# 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'}]
```

```python
# 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
```

```python
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}}
```

```python
# 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]}
```

```python
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
```

```python
# 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

```python
# 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.
```

```python
# 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
```python
# 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
```

```python
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
```

```python
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)
```

```python
# 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
```

```python
# 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)
```

```python
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
```

```python
# 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)
```

```python
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](src/orval/__init__.py).

## 🧑‍💻 Contributing

<details>
<summary>Prerequisites</summary>

<details>
<summary>1. Install Docker</summary>

1. Go to [Docker](https://www.docker.com/get-started), download and install docker.
2. [Configure Docker to use the BuildKit build system](https://docs.docker.com/build/buildkit/#getting-started). On macOS and Windows, BuildKit is enabled by default in Docker Desktop.

</details>

<details>
<summary>2. Install VS Code</summary>

Go to [VS Code](https://code.visualstudio.com/), download and install VS Code.
</details>

</details>

#### 1. Open DevContainer with VS Code
Open this repository with VS Code, and run <kbd>Ctrl/⌘</kbd> + <kbd>⇧</kbd> + <kbd>P</kbd> → _Dev Containers: Reopen in Container_.

The following commands can be used inside a DevContainer.

#### 2. Run linters
```bash
poe lint
```

#### 3. Run tests
```bash
poe test
```

#### 4. Update uv lock file
```bash
uv lock
```

---
See how to develop with [PyCharm or any other IDE](https://github.com/lukin0110/uv-copier/tree/main/docs/ide.md).

---
️⚡️ Scaffolded with [Uv Copier](https://github.com/lukin0110/uv-copier/).\
🛠️ [Open an issue](https://github.com/lukin0110/uv-copier/issues/new) if you have any questions or suggestions.
