Python Dynamic DSL for data access and manipulation
Project description
PynDD (Python Dynamic DSL)
A lightweight Python library for dynamic data structure parsing and manipulation using a custom Domain Specific Language (DSL).
Installation
pip install pyndd
Quick Start
from pyndd.parser import parse, translate
# Basic usage
data = {'users': [{'name': 'Alice', 'age': 30}, {'name': 'Bob', 'age': 25}]}
names = parse('data:users:[#name]', data=data)
print(names) # ['Alice', 'Bob']
DSL Syntax Guide
Basic Structure
The DSL uses a colon-separated syntax: variable:accessor1:accessor2:...
Accessors
1. Dictionary/Object Access (#key)
data = {'user': {'name': 'Alice', 'age': 30}}
name = parse('data:#user:#name', data=data)
print(name) # 'Alice'
2. List/Array Access by Index (number)
data = {'items': ['a', 'b', 'c', 'd']}
item = parse('data:#items:1', data=data)
print(item) # 'b'
3. Slice Access ([start..end])
data = {'items': ['a', 'b', 'c', 'd', 'e']}
subset = parse('data:#items:[1..4]', data=data)
print(subset) # ['b', 'c', 'd']
# Open-ended slices
beginning = parse('data:#items:[..2]', data=data) # ['a', 'b']
ending = parse('data:#items:[2..]', data=data) # ['c', 'd', 'e']
all_items = parse('data:#items:[..]', data=data) # ['a', 'b', 'c', 'd', 'e']
4. Map Operations ([#key])
Extract specific fields from each item in a list:
data = {'users': [
{'name': 'Alice', 'age': 30},
{'name': 'Bob', 'age': 25}
]}
names = parse('data:#users:[#name]', data=data)
print(names) # ['Alice', 'Bob']
ages = parse('data:#users:[#age]', data=data)
print(ages) # [30, 25]
5. Pattern Matching (*pattern*)
Match keys using wildcards:
data = {
'user_alice': {'score': 100},
'user_bob': {'score': 85},
'admin_charlie': {'score': 95}
}
# Get all user_* entries
users = parse('data:user_*', data=data)
print(users) # {'user_alice': {'score': 100}, 'user_bob': {'score': 85}}
# Get scores from user_* entries
user_scores = parse('data:user_*:[#score]', data=data)
print(user_scores) # [100, 85]
6. Multi-Selector Operations ([selector1,selector2,...])
Combine multiple selectors to extract or access multiple elements at once:
# Multi-index selection
data = {'items': list(range(10))}
selected = parse('data:#items:[1,3,5]', data=data)
print(selected) # [1, 3, 5]
# Multi-slice selection
ranges = parse('data:#items:[1..3,5..8]', data=data)
print(ranges) # [[1, 2], [5, 6, 7]]
# Mixed selectors (indices, slices, keys)
mixed = parse('data:#items:[0,2..4,7]', data=data)
print(mixed) # [0, [2, 3], 7]
7. Multi-Key Dictionary Extraction
Extract multiple fields to create structured objects:
users = [
{'name': 'Alice', 'age': 30, 'job': 'engineer'},
{'name': 'Bob', 'age': 25, 'job': 'designer'}
]
# Extract multiple keys as structured objects
subset = parse('users:[#name,#age]', users=users)
print(subset) # [{'name': 'Alice', 'age': 30}, {'name': 'Bob', 'age': 25}]
8. First-Match Selector (@variable)
Extract first occurrence of each key across list items:
# Data with inconsistent key presence
teams = [
{'frontend': 'React', 'backend': 'Node.js', 'db': 'MongoDB'},
{'frontend': 'Vue', 'backend': 'Python', 'mobile': 'React Native'},
{'backend': 'Java', 'db': 'PostgreSQL', 'cloud': 'AWS'}
]
tech_keys = ['frontend', 'backend', 'cloud']
# Find first occurrence of each key
values = parse('teams:@tech_keys', teams=teams, tech_keys=tech_keys)
print(values) # ['React', 'Node.js', 'AWS']
# In multi-selector context - returns structured format
structured = parse('teams:[@tech_keys,]', teams=teams, tech_keys=tech_keys)
print(structured) # [{'frontend': 'React'}, {'backend': 'Node.js'}, {'cloud': 'AWS'}]
9. Variable-based Key Access
Use variables to specify keys dynamically:
data = {'items': ['x', 'y', 'z']}
indices = [0, 2]
selected = parse('data:#items:indices', data=data, indices=indices)
print(selected) # ['x', 'z']
Complex Examples
Advanced Multi-Selector Combinations
# Complex nested data processing
teams = [
{'name': 'Frontend', 'lead': 'Alice', 'tech': 'React', 'size': 5},
{'name': 'Backend', 'lead': 'Bob', 'tech': 'Python', 'budget': 100000},
{'name': 'DevOps', 'lead': 'Charlie', 'cloud': 'AWS', 'size': 3}
]
# Extract specific fields with fallback handling
fields = ['tech', 'cloud', 'budget']
result = parse('teams:[#name,#lead,@fields]', teams=teams, fields=fields)
print(result)
# [['Frontend', 'Backend', 'DevOps'],
# ['Alice', 'Bob', 'Charlie'],
# [{'tech': 'React'}, {'tech': 'Python'}, {'cloud': 'AWS'}]]
# Slice the results
subset = parse('teams:[#name,#lead]:[1..3]', teams=teams)
print(subset) # [{'name': 'Backend', 'lead': 'Bob'}, {'name': 'DevOps', 'lead': 'Charlie'}]
Chaining Operations
data = {
'departments': [
{
'name': 'Engineering',
'employees': [
{'name': 'Alice', 'skills': ['Python', 'JavaScript']},
{'name': 'Bob', 'skills': ['Java', 'C++']}
]
},
{
'name': 'Marketing',
'employees': [
{'name': 'Charlie', 'skills': ['SEO', 'Content']}
]
}
]
}
# Get all employee names
all_names = parse('data:#departments:[#employees]:[#name]', data=data)
print(all_names) # [['Alice', 'Bob'], ['Charlie']]
# Get skills of first employee in each department
first_skills = parse('data:#departments:[#employees]:0:[#skills]', data=data)
print(first_skills) # [['Python', 'JavaScript'], ['SEO', 'Content']]
Nested Slicing
data = {
'matrix': [
[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12]
]
}
# Get middle 2x2 submatrix
submatrix = parse('data:#matrix:[1..3]:[1..3]', data=data)
print(submatrix) # [[6, 7], [10, 11]]
Data Modification with translate()
The translate() function allows you to modify data using assignment operations.
Basic Assignment
data = {'user': {'name': 'Alice'}}
translate('data:#user:#age < 30', data=data)
print(data) # {'user': {'name': 'Alice', 'age': 30}}
Bulk Assignment
data = {'users': [{'name': 'Alice'}, {'name': 'Bob'}]}
translate('data:#users:[#age] < 25', data=data)
print(data) # {'users': [{'name': 'Alice', 'age': 25}, {'name': 'Bob', 'age': 25}]}
Copy Data Between Structures
source = {'items': [1, 2, 3]}
target = {}
translate('target:#copied < source:#items', source=source, target=target)
print(target) # {'copied': [1, 2, 3]}
Multi-Selector Assignment
# Assign to multiple indices at once
data = {'items': [0, 0, 0, 0, 0]}
translate('data:#items:[1,3] < [10,30]', data=data)
print(data) # {'items': [0, 10, 0, 30, 0]}
# Copy from multi-selector to slice
source = {'values': [1, 2, 3, 4, 5]}
target = {'result': [0, 0, 0]}
translate('source:#values:[1,3,4] > target:#result:[..]', source=source, target=target)
print(target) # {'result': [2, 4, 5]}
Advanced Features
Robust Data Handling
The DSL gracefully handles missing keys and inconsistent data structures:
# Mixed data structures
data = [
{'name': 'Alice', 'age': 25, 'role': 'Engineer'},
{'name': 'Bob', 'role': 'Designer'}, # missing 'age'
{'name': 'Charlie', 'age': 30} # missing 'role'
]
# Safely extract available data
names_ages = parse('data:[#name,#age]', data=data)
print(names_ages)
# [{'name': 'Alice', 'age': 25}, {'name': 'Bob'}, {'name': 'Charlie', 'age': 30}]
Pattern-based Operations
config = {
'db_host': 'localhost',
'db_port': 5432,
'db_name': 'myapp',
'cache_host': 'redis-server',
'cache_port': 6379
}
# Get all database-related configs
db_config = parse('config:db_*', config=config)
print(db_config) # {'db_host': 'localhost', 'db_port': 5432, 'db_name': 'myapp'}
Identity Operation ([-])
The identity operation [-] can be used to pass through values unchanged:
data = {'items': [1, 2, 3]}
same = parse('data:#items:[-]', data=data)
print(same) # [1, 2, 3]
Error Handling
The parser will raise ValueError for malformed expressions:
try:
parse('invalid syntax here', data={})
except ValueError as e:
print(f"Parse error: {e}")
Performance Notes
- The DSL parser is lightweight and suitable for runtime data manipulation
- Complex nested operations are supported but consider performance for deeply nested structures
- Pattern matching uses Python's
fnmatchmodule internally
License
MIT License
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
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 pyndd-0.3.0.tar.gz.
File metadata
- Download URL: pyndd-0.3.0.tar.gz
- Upload date:
- Size: 8.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
dc36576e429551230038886531fe04b7c850d0efbacd676d8d962b313095bd8d
|
|
| MD5 |
93879f6bb04248f7d82fe8a5bf87f092
|
|
| BLAKE2b-256 |
766fe94320cc36e6e8f034ff0bfd5ce0f2a6edbe0344be84ee3d334cdc3702a5
|
File details
Details for the file pyndd-0.3.0-py3-none-any.whl.
File metadata
- Download URL: pyndd-0.3.0-py3-none-any.whl
- Upload date:
- Size: 7.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ff7e3efb42af6248c39a2b7b4cfe9f6f222e10509a9ec62c0a250b0ae0e1b8a5
|
|
| MD5 |
62c0ee5bdc6d61a02403d981a78c418d
|
|
| BLAKE2b-256 |
3a23b0e69a9f93db9b6e2e90acbaf6c1b320924c7a573562c050b14fb701e457
|