Skip to main content

Chain-structured data and pattern matching with .xxx.yyy syntax

Project description

latychain

⚠️ 早期开发阶段 — API 可能发生变化。

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(lambda c: str(c).startswith('.x'))
)

# ── Matching (call .match() as a method on the Chain object) ──
data = .x.uuu.x1
data.match(rule)                         # True

Table of Contents


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.yyy reads 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 (call .match() on a Chain variable, not in the chain expression) ──
user_data = Chain(['user', 'login', '123'])
user_data.match(rule)                 # True

admin_data = Chain(['admin', 'delete', '456'])
admin_data.match(rule)                # True

guest_data = Chain(['guest', 'abc'])
guest_data.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: seg[0] != seg[1], long=2)
# two consecutive elements, check they differ

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

Limitations

  • .match() is a Chain method, not a chain segment. Always call .match() on a separate Chain variable, not inside a chain expression:

    # ✅ Correct
    data = .user.login.id123
    data.match(rule)
    
    # ❌ Wrong — .match(rule) becomes ChainRuleAtom.match(rule) which doesn't exist
    # .user.login.id123.match(rule)
    
  • Numeric-only segments (.123, .42) are not supported. Use alphanumeric segments (.id123, .val42) or explicit Chain(['123']) construction.

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]')

Chain(['heading', 'h1']).match(heading_rule)          # True
Chain(['body', 'heading', 'h3']).match(heading_rule)  # True
Chain(['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)
)

Chain(['a', 'user', 'profile']).match(allow_rule)         # True
Chain(['a', 'admin', 'dashboard']).match(allow_rule)       # True
Chain(['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)
)

Chain(['2024', '01', '15', 'ERROR', 'timeout']).match(error_pattern)   # True
Chain(['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

  1. Single type for data and rulesChain holds both strings and ChainRuleAtom instances, no separate DSL
  2. Compile-time transformation — import hook uses tokenize, not runtime evaluation; safe and performant
  3. Backtracking engine — non-greedy depth-first search for any() matching
  4. 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
│   ├── _test_doc_examples.py # Doc example tests (explicit API)
│   └── _test_doc_sugar.py   # Doc example tests (sugar syntax)
├── docs/
│   ├── api-reference.md     # Complete API reference
│   └── guide.md             # Usage guide and practical patterns
├── pyproject.toml           # Project metadata
└── README.md                # This file

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

latychain-0.1.0a0.tar.gz (26.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

latychain-0.1.0a0-py3-none-any.whl (19.6 kB view details)

Uploaded Python 3

File details

Details for the file latychain-0.1.0a0.tar.gz.

File metadata

  • Download URL: latychain-0.1.0a0.tar.gz
  • Upload date:
  • Size: 26.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for latychain-0.1.0a0.tar.gz
Algorithm Hash digest
SHA256 89b24553f3ac734a97da02e769135298d9b628845cffe04a0d9d06f3a09f51f8
MD5 4a87c6dc6ca0ac3158b1525599fb882c
BLAKE2b-256 90b5ad364821db5242daaf5b0066615f9ddae5daab14f00bc919e3973866b0ba

See more details on using hashes here.

Provenance

The following attestation bundles were made for latychain-0.1.0a0.tar.gz:

Publisher: publish.yml on fnsii/latychain

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file latychain-0.1.0a0-py3-none-any.whl.

File metadata

  • Download URL: latychain-0.1.0a0-py3-none-any.whl
  • Upload date:
  • Size: 19.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for latychain-0.1.0a0-py3-none-any.whl
Algorithm Hash digest
SHA256 38761220c52189734b08fdf5a11632b9c19e6955ef7e8e24a6dc946b60aeac24
MD5 2100bf0e71d91667bb091193306b8eec
BLAKE2b-256 3b28e34f72806ccf600ce7c82ac4b0d9ab16e2734e1fbf3b98e078c12af050d6

See more details on using hashes here.

Provenance

The following attestation bundles were made for latychain-0.1.0a0-py3-none-any.whl:

Publisher: publish.yml on fnsii/latychain

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page