Chain-structured data and pattern matching with .xxx.yyy syntax
Project description
latychain
Chain-structured data and pattern matching with .xxx.yyy syntax.
latychain provides two core types — Chain (immutable ordered container) and ChainRuleAtom (rule atoms) — plus an optional import hook that enables the concise .xxx.yyy syntax for constructing chains.
import latychain.ChainDotRule # enable .xxx.yyy sugar
from latychain import Chain
# ── Data chain ──
heading = .heading.h1 # → Chain(['heading', 'h1'])
# ── Rule chain ──
rule = .any(0).uuu.rex(r'x\d') # → Chain([any(0), 'uuu', rex(...)])
# ── Nested rules ──
r2 = .any(0).enum(
.hi.rex(r'x[0-9]'),
.wuhu.apply(f)
)
# ── Matching ──
.x.uuu.x1.match(rule) # True
Table of Contents
- Why latychain?
- Installation
- Quick Start
- Chain
- ChainRuleAtom
- Matching
.xxx.yyySyntax Sugar- Examples
- API Reference
- Design & Implementation
- Development
Why latychain?
Many domains deal with hierarchical path-like data: CSS selectors, filesystem paths, JSON paths, routing rules, config keys, log categories, etc. Representing these as plain strings is error-prone; representing them as lists is verbose.
latychain gives you:
- Immutability — chains are hashable, thread-safe, usable as dict keys
- Pattern matching — declarative rules with backtracking, regex, custom predicates
- Concise syntax —
.xxx.yyyreads naturally as a path - No external dependencies — pure Python, uses only standard library
Installation
pip install latychain
Requires Python 3.10+.
Quick Start
import latychain.ChainDotRule
from latychain import Chain
# ── Construct data chains ──
path = .user.profile.avatar
# → Chain(['user', 'profile', 'avatar'])
# ── Construct rule chains ──
rule = .any(0).enum(
.admin.any(0),
.user.any(0)
).rex(r'\d+')
# ── Match ──
.user.login.123.match(rule) # True
.admin.delete.456.match(rule) # True
.guest.abc.match(rule) # False
Chain
Chain is an immutable, ordered container. Elements can be plain strings (data) or ChainRuleAtom instances (rules).
Construction
from latychain import Chain
Chain() # empty chain
Chain(['a']) # single element
Chain(['a', 'b', 'c']) # multi-element
Chain([ChainRuleAtom.any(0)]) # with rule atoms
Or with the .xxx.yyy sugar:
import latychain.ChainDotRule
.a.b.c # → Chain(['a', 'b', 'c'])
.any(0).uuu.rex(r'x\d') # → Chain([any(0), 'uuu', rex(...)])
Operations
c = Chain(['a', 'b', 'c'])
len(c) # 3
c[0] # 'a'
c[-1] # 'c'
list(c) # ['a', 'b', 'c']
c.elements # ('a', 'b', 'c')
str(c) # ".a.b.c"
repr(c) # "Chain(['a', 'b', 'c'])"
Chain(['a', 'b']) == Chain(['a', 'b']) # True
Chain(['a', 'b']) + Chain(['c', 'd']) # → Chain(['a','b','c','d'])
d = {Chain(['a']): 1} # hashable, usable as dict key
Methods
chain.to_list() # → list of elements
chain.startswith(prefix) # prefix match (partial)
chain.match(pattern) # full match (see Matching section)
chain.match(pattern, partial=True) # prefix match
ChainRuleAtom
ChainRuleAtom is the minimal unit of a rule pattern. All atoms are immutable and hashable.
| Factory | Purpose |
|---|---|
any(min, max) |
Match N arbitrary elements (with backtracking) |
rex(pattern) |
Regex fullmatch on a single element |
enum(*chains) |
Pick one of several alternatives |
apply(func, long) |
Custom predicate on N elements |
long(min, max) |
String length constraint |
un(value) |
Negation: not equal to value |
ext(chain) |
Optional segment (match or skip) |
from latychain import ChainRuleAtom
ChainRuleAtom.any(0)
ChainRuleAtom.rex(r'x\d')
ChainRuleAtom.enum(Chain(['a']), Chain(['b']))
ChainRuleAtom.apply(lambda c: len(c) > 2)
ChainRuleAtom.long(3, 5)
ChainRuleAtom.un('admin')
ChainRuleAtom.ext(Chain(['a', 'b']))
any — arbitrary elements
Match between min and max arbitrary elements. Non-greedy with backtracking.
.any() # at least 1
.any(0) # 0 or more
.any(2) # at least 2
.any(1, 3) # 1 to 3
.any(0, 5) # 0 to 5
rex — regex match
Regex fullmatch on a single string element.
.rex(r'h[12]') # matches 'h1', 'h2'
.rex(r'\d+') # matches '123', '0'
enum — choice
Match one of several alternatives. Each alternative is a Chain (data or rule).
.enum(
.type.h1,
.type.h2
)
# matches .type.h1 or .type.h2
apply — custom predicate
Apply a user function to long consecutive elements. The function receives a Chain object.
.apply(lambda seg: str(seg).startswith('.x'))
# single element starting with 'x'
.apply(lambda seg: len(seg) > 2, long=2)
# two consecutive elements, total chain length > 2
long — string length
Constrain the string length of a single element.
.long(3) # exactly 3 characters
.long(2, 5) # 2 to 5 characters
un — negation
Match any single element except the given value.
.un('admin') # matches 'user', 'guest'; does NOT match 'admin'
ext — optional segment
Try to match the inner chain; if it fails, skip (consume 0 elements).
.a.ext(.pi).b
# matches .a.pi.b (ext matched)
# matches .a.b (ext skipped)
# does NOT match .a.x.b
Matching
Full match vs partial match
data = .a.b.c.d
data.match(.a.b) # False — does not consume c.d
data.match(.a.b, partial=True) # True — prefix matches
Backtracking engine
The matcher uses depth-first backtracking with non-greedy priority. any() tries shorter matches first, then longer ones if the rest of the pattern fails.
Rule: .any(0).uuu.rex(r'x\d')
Data: .pre.uuu.x1
Attempts:
any=0 → uuu ≠ 'pre' → backtrack
any=1 → uuu = 'uuu' ✓ → rex(r'x\d') matches 'x1' ✓ → success
.xxx.yyy Syntax Sugar
Enabling
import latychain.ChainDotRule
This registers a meta path finder (import hook) that transforms all subsequently loaded .py files. Only needs to be done once, at the entry point.
How it works
The import hook uses Python's tokenize module to safely identify .xxx expressions and transform them into Chain([...]) calls at compile time (not runtime).
| Source | Transformed to |
|---|---|
.heading.h1 |
Chain(['heading', 'h1']) |
.any(0).uuu |
Chain([ChainRuleAtom.any(0), 'uuu']) |
.any(0).uuu.rex(r'x\d') |
Chain([ChainRuleAtom.any(0), 'uuu', ChainRuleAtom.rex(r'x\d')]) |
Rule: segments without () become strings; segments with () become ChainRuleAtom.xxx() calls.
What is (and isn't) transformed
| Code | Transformed? | Reason |
|---|---|---|
.heading.h1 |
✅ Yes | chain expression |
.any(0).uuu |
✅ Yes | chain expression |
obj.attr |
❌ No | attribute access |
.5 + .3 |
❌ No | float literals |
func().attr |
❌ No | method return value access |
"strings .here" |
❌ No | inside string literals |
# comments .here |
❌ No | inside comments |
Nested expressions
Arguments inside enum(), ext(), etc. are recursively transformed:
.enum(
.hi.rex(r'x[0-9]'),
.wuhu.apply(f)
)
# → Chain([ChainRuleAtom.enum(
# Chain(['hi', ChainRuleAtom.rex(r'x[0-9]')]),
# Chain(['wuhu', ChainRuleAtom.apply(f)])
# )])
Examples
HTML headings
import latychain.ChainDotRule
heading_rule = .any(0).heading.rex(r'h[1-6]')
.heading.h1.match(heading_rule) # True
.body.heading.h3.match(heading_rule) # True
.heading.h7.match(heading_rule) # False
Path permissions
import latychain.ChainDotRule
# Allow /user/* and /admin/*, but reject /admin/secret
allow_rule = .any(0).enum(
.user.any(0),
.admin.un('secret').any(0)
)
.a.user.profile.match(allow_rule) # True
.a.admin.dashboard.match(allow_rule) # True
.a.admin.secret.match(allow_rule) # False
Log classification
import latychain.ChainDotRule
# Match error logs: YYYY.MM.DD.ERROR.xxx
error_pattern = (
.rex(r'\d{4}')
.rex(r'\d{2}')
.rex(r'\d{2}')
.ERROR
.any(0)
)
.2024.01.15.ERROR.timeout.match(error_pattern) # True
.2024.01.15.INFO.request.match(error_pattern) # False
API Reference
Chain
class Chain:
def __init__(self, elements: Iterable = ()) -> None
# Read
def __getitem__(self, index: int) -> str | ChainRuleAtom
def __len__(self) -> int
def __iter__(self) -> Iterator
@property
def elements(self) -> tuple
# String
def __str__(self) -> str # ".a.b.c"
def __repr__(self) -> str # "Chain(['a', 'b', 'c'])"
# Value semantics
def __eq__(self, other) -> bool
def __hash__(self) -> int
def __bool__(self) -> bool
# Operations
def __add__(self, other) -> Chain
def match(self, pattern: Chain, partial: bool = False) -> bool
# Utilities
def to_list(self) -> list
def startswith(self, prefix: Chain) -> bool
ChainRuleAtom
class ChainRuleAtom:
@staticmethod
def any(min: int = 0, max: int = 0) -> ChainRuleAtom
@staticmethod
def rex(pattern: str) -> ChainRuleAtom
@staticmethod
def enum(*alternatives: Chain) -> ChainRuleAtom
@staticmethod
def apply(func: callable, long: int = 1) -> ChainRuleAtom
@staticmethod
def long(min: int, max: int | None = None) -> ChainRuleAtom
@staticmethod
def un(value: str) -> ChainRuleAtom
@staticmethod
def ext(chain: Chain | None = None) -> ChainRuleAtom
latychain.ChainDotRule
import latychain.ChainDotRule # registers the import hook globally
Design & Implementation
Detailed documentation is in the docs/ directory:
| Document | Description |
|---|---|
docs/api-reference.md |
Complete API reference for Chain, ChainRuleAtom, and the import hook |
docs/guide.md |
Usage guide with practical patterns, migration tips, and deep dives |
Key design decisions
- Single type for data and rules —
Chainholds both strings andChainRuleAtominstances, no separate DSL - Compile-time transformation — import hook uses
tokenize, not runtime evaluation; safe and performant - Backtracking engine — non-greedy depth-first search for
any()matching - Immutability — chains are hashable, thread-safe, usable as dict keys
Development
Setup
git clone <repo>
cd latychain
uv venv
source .venv/bin/activate # or .venv\Scripts\activate on Windows
Running tests
uv run python test/run_all.py
Project structure
latychain/
├── src/latychain/
│ ├── __init__.py # Public API: Chain, ChainRuleAtom
│ ├── _chain.py # Chain class + backtracking matcher
│ ├── _atoms.py # ChainRuleAtom + 7 rule atom types
│ ├── _hook.py # Import hook (tokenize transformer)
│ └── ChainDotRule.py # Entry point: import to enable sugar
├── test/
│ ├── run_all.py # Test runner
│ ├── test_core.py # Core API tests (30 cases)
│ └── _test_sugar.py # Sugar syntax integration tests
├── docs/
│ ├── api-reference.md # Complete API reference
│ └── guide.md # Usage guide and practical patterns
├── pyproject.toml # Project metadata
└── README.md # This file
Project details
Release history Release notifications | RSS feed
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 latychain-0.1.0.tar.gz.
File metadata
- Download URL: latychain-0.1.0.tar.gz
- Upload date:
- Size: 21.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d9626b3af0a8024598efcb1bbd0ba253a351afb0e275b19335fc4b84be39897c
|
|
| MD5 |
66141ee90d5b1936a5fba624b3982c4d
|
|
| BLAKE2b-256 |
2b805d006109c5691590767f3c6f9c1bf173c5245e9897570e3f0eb3953b54de
|
Provenance
The following attestation bundles were made for latychain-0.1.0.tar.gz:
Publisher:
publish.yml on fnsii/latychain
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
latychain-0.1.0.tar.gz -
Subject digest:
d9626b3af0a8024598efcb1bbd0ba253a351afb0e275b19335fc4b84be39897c - Sigstore transparency entry: 1790878980
- Sigstore integration time:
-
Permalink:
fnsii/latychain@430d969a54962175f4804746271a441831ccfa70 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/fnsii
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@430d969a54962175f4804746271a441831ccfa70 -
Trigger Event:
push
-
Statement type:
File details
Details for the file latychain-0.1.0-py3-none-any.whl.
File metadata
- Download URL: latychain-0.1.0-py3-none-any.whl
- Upload date:
- Size: 18.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f18683df4bc5e81a3a02f48a84a02a32e463eaeff62c18eb489309c3e4806591
|
|
| MD5 |
2352f48862a7240558f85421675a41c7
|
|
| BLAKE2b-256 |
5733aecbe58a749096436926b926793d47949afaf1a9aeb00ef742cca26759e4
|
Provenance
The following attestation bundles were made for latychain-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on fnsii/latychain
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
latychain-0.1.0-py3-none-any.whl -
Subject digest:
f18683df4bc5e81a3a02f48a84a02a32e463eaeff62c18eb489309c3e4806591 - Sigstore transparency entry: 1790878996
- Sigstore integration time:
-
Permalink:
fnsii/latychain@430d969a54962175f4804746271a441831ccfa70 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/fnsii
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@430d969a54962175f4804746271a441831ccfa70 -
Trigger Event:
push
-
Statement type: