Skip to content

Commit a3dabb4

Browse files
authored
Refactor data handling in meshly (#15)
- Consolidated ReadHandler and WriteHandler into a single DataHandler class for improved clarity and usability. - Updated all references in the codebase to use the new DataHandler interface. - Modified the Packable class to utilize DataHandler for loading and saving nested packables. - Enhanced TypeScript definitions to reflect the changes in data handling, including the introduction of a DataHandler interface. - Updated tests to align with the new DataHandler structure and ensure functionality remains intact. - Bumped version to 2.4.0-alpha to reflect significant changes in data handling architecture.
1 parent a68bb0f commit a3dabb4

14 files changed

Lines changed: 486 additions & 329 deletions

File tree

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: Publish to Test PyPI
2+
3+
on:
4+
workflow_dispatch:
5+
6+
jobs:
7+
deploy:
8+
runs-on: ubuntu-latest
9+
steps:
10+
- uses: actions/checkout@v3
11+
- name: Set up Python
12+
uses: actions/setup-python@v4
13+
with:
14+
python-version: '3.x'
15+
- name: Install dependencies
16+
run: |
17+
python -m pip install --upgrade pip
18+
cd python
19+
pip install build
20+
pip install .
21+
22+
- name: Build package
23+
run: cd python && python -m build --sdist
24+
- name: Publish package to Test PyPI
25+
uses: pypa/gh-action-pypi-publish@release/v1
26+
with:
27+
password: ${{ secrets.TEST_PYPI_API_TOKEN }}
28+
repository_url: https://test.pypi.org/legacy/
29+
packages_dir: python/dist/

python/README.md

Lines changed: 165 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ pip install meshly
1616
- **`Mesh`**: 3D mesh representation extending Packable with meshoptimizer encoding for vertices/indices
1717
- **`CustomFieldConfig`**: Configuration for custom field encoding/decoding
1818
- **`ArrayUtils`**: Utility class for encoding/decoding individual arrays
19+
- **`DataHandler`**: Unified interface for reading and writing files or zip archives
1920

2021
### Key Capabilities
2122

@@ -220,17 +221,16 @@ print(loaded.physics.mass) # 2.5
220221
For large projects with shared nested Packables, use caching to deduplicate data using SHA256 content-addressable storage:
221222

222223
```python
223-
from meshly import ReadHandler, WriteHandler
224+
from meshly import DataHandler
224225

225-
# Create cache functions from a directory path
226-
cache_saver = WriteHandler.create_cache_saver("/path/to/cache")
227-
cache_loader = ReadHandler.create_cache_loader("/path/to/cache")
226+
# Create cache handlers from a directory path
227+
cache_handler = DataHandler.create("/path/to/cache")
228228

229229
# Save with caching - nested Packables stored separately by hash
230-
mesh.save_to_zip("mesh.zip", cache_saver=cache_saver)
230+
mesh.save_to_zip("mesh.zip", cache_handler=cache_handler)
231231

232232
# Load with caching - nested Packables loaded from cache
233-
loaded = PhysicsMesh.load_from_zip("mesh.zip", cache_loader=cache_loader)
233+
loaded = PhysicsMesh.load_from_zip("mesh.zip", cache_handler=cache_handler)
234234
```
235235

236236
**Deduplication example:**
@@ -242,29 +242,61 @@ shared_physics = PhysicsProperties(mass=1.0, inertia_tensor=np.eye(3))
242242
mesh1 = PhysicsMesh(vertices=v1, indices=i1, physics=shared_physics)
243243
mesh2 = PhysicsMesh(vertices=v2, indices=i2, physics=shared_physics)
244244

245-
# Save both with the same cache - physics stored only once!
246-
mesh1.save_to_zip("mesh1.zip", cache_saver=cache_saver)
247-
mesh2.save_to_zip("mesh2.zip", cache_saver=cache_saver)
245+
# Save both with the same cache handler - physics stored only once!
246+
mesh1.save_to_zip("mesh1.zip", cache_handler=cache_handler)
247+
mesh2.save_to_zip("mesh2.zip", cache_handler=cache_handler)
248248
```
249249

250-
**Custom cache functions:**
250+
**Custom cache handlers:**
251+
252+
You can implement custom `DataHandler` subclasses for different storage backends:
251253

252254
```python
253-
from meshly import CacheLoader, CacheSaver
255+
from meshly.data_handler import DataHandler
256+
from typing import Optional, List
257+
from pathlib import Path
258+
259+
class RedisDataHandler(DataHandler):
260+
"""Data handler backed by Redis."""
261+
def __init__(self, redis_client, prefix="packable:"):
262+
super().__init__(source="", rel_path="")
263+
self.redis = redis_client
264+
self.prefix = prefix
265+
266+
def read_binary(self, subpath) -> bytes:
267+
data = self.redis.get(f"{self.prefix}{subpath}")
268+
if data is None:
269+
raise FileNotFoundError(f"Key not found: {self.prefix}{subpath}")
270+
return data
271+
272+
def read_text(self, subpath, encoding="utf-8") -> str:
273+
return self.read_binary(subpath).decode(encoding)
274+
275+
def list_files(self, subpath="", recursive=False) -> List[Path]:
276+
raise NotImplementedError("File listing not supported")
277+
254278

255-
# Type signatures:
256-
# CacheLoader = Callable[[str], Optional[bytes]] # hash -> bytes or None
257-
# CacheSaver = Callable[[str, bytes], None] # hash, bytes -> None
279+
class RedisWriteHandler(WriteHandler):
280+
"""Write handler backed by Redis."""
281+
def __init__(self, redis_client, prefix="packable:"):
282+
super().__init__(destination="", rel_path="")
283+
self.redis = redis_client
284+
self.prefix = prefix
285+
286+
def write_binary(self, subpath, content, executable=False) -> None:
287+
data = content if isinstance(content, bytes) else content.read()
288+
self.redis.set(f"{self.prefix}{subpath}", data)
289+
290+
def write_text(self, subpath, content, executable=False) -> None:
291+
self.redis.set(f"{self.prefix}{subpath}", content.encode('utf-8'))
258292

259-
# Example: Redis-backed cache
260-
def redis_loader(hash: str) -> Optional[bytes]:
261-
return redis_client.get(f"packable:{hash}")
262293

263-
def redis_saver(hash: str, data: bytes) -> None:
264-
redis_client.set(f"packable:{hash}", data)
294+
# Usage with Redis
295+
cache_writer = RedisWriteHandler(redis_client)
296+
cache_reader = RedisReadHandler(redis_client)
265297

266-
mesh.save_to_zip("mesh.zip", cache_saver=redis_saver)
267-
loaded = PhysicsMesh.load_from_zip("mesh.zip", cache_loader=redis_loader)
298+
mesh.save_to_zip("mesh.zip", cache_handler=cache_writer)
299+
loaded = PhysicsMesh.load_from_zip("mesh.zip", cache_handler=cache_reader)
268300
```
269301

270302
## Architecture
@@ -573,6 +605,106 @@ ReadHandler.create_cache_loader(source: PathLike) -> CacheLoader
573605
WriteHandler.create_cache_saver(destination: PathLike) -> CacheSaver
574606
```
575607

608+
### Data Handlers
609+
610+
The `data_handler` module provides abstract interfaces for reading and writing data, supporting both regular files and zip archives.
611+
612+
```python
613+
from meshly import ReadHandler, WriteHandler
614+
615+
# ReadHandler - Abstract base for reading files
616+
class ReadHandler:
617+
def __init__(self, source: PathLike | BytesIO, rel_path: str = "")
618+
619+
# Abstract methods (implemented by FileReadHandler, ZipReadHandler)
620+
def read_text(self, subpath: PathLike, encoding: str = "utf-8") -> str
621+
def read_binary(self, subpath: PathLike) -> bytes
622+
def list_files(self, subpath: PathLike = "", recursive: bool = False) -> List[Path]
623+
624+
# Navigate to subdirectory
625+
def to_path(self, rel_path: str) -> ReadHandler
626+
627+
# Factory method - automatically creates FileReadHandler or ZipReadHandler
628+
@staticmethod
629+
def create_handler(source: PathLike | BytesIO, rel_path: str = "") -> ReadHandler
630+
631+
# Create cache loader for nested Packables
632+
@staticmethod
633+
def create_cache_loader(source: PathLike | BytesIO) -> CacheLoader
634+
635+
# WriteHandler - Abstract base for writing files
636+
class WriteHandler:
637+
def __init__(self, destination: PathLike | BytesIO, rel_path: str = "")
638+
639+
# Abstract methods (implemented by FileWriteHandler, ZipWriteHandler)
640+
def write_text(self, subpath: PathLike, content: str, executable: bool = False) -> None
641+
def write_binary(self, subpath: PathLike, content: bytes | BytesIO, executable: bool = False) -> None
642+
643+
# Navigate to subdirectory
644+
def to_path(self, rel_path: str) -> WriteHandler
645+
646+
# Factory method - automatically creates FileWriteHandler or ZipWriteHandler
647+
@staticmethod
648+
def create_handler(destination: PathLike | BytesIO, rel_path: str = "") -> WriteHandler
649+
650+
# Create cache saver for nested Packables
651+
@staticmethod
652+
def create_cache_saver(destination: PathLike | BytesIO) -> CacheSaver
653+
654+
# Close resources (important for ZipWriteHandler)
655+
def finalize(self) -> None
656+
```
657+
658+
#### Concrete Implementations
659+
660+
```python
661+
# FileReadHandler - Read from filesystem
662+
handler = FileReadHandler("/path/to/directory")
663+
data = handler.read_binary("subdir/file.bin")
664+
files = handler.list_files("subdir", recursive=True)
665+
666+
# ZipReadHandler - Read from zip archives
667+
with open("archive.zip", "rb") as f:
668+
handler = ZipReadHandler(BytesIO(f.read()))
669+
metadata = handler.read_text("metadata.json")
670+
array_data = handler.read_binary("arrays/vertices/array.bin")
671+
672+
# FileWriteHandler - Write to filesystem
673+
handler = FileWriteHandler("/path/to/output")
674+
handler.write_text("config.json", '{"version": 1}')
675+
handler.write_binary("data.bin", compressed_bytes)
676+
677+
# ZipWriteHandler - Write to zip archives
678+
buf = BytesIO()
679+
handler = ZipWriteHandler(buf)
680+
handler.write_text("metadata.json", json_string)
681+
handler.write_binary("data.bin", array_bytes)
682+
handler.finalize() # Important: closes the zip file
683+
zip_bytes = buf.getvalue()
684+
```
685+
686+
#### Advanced Usage
687+
688+
```python
689+
# Use handlers for custom storage backends
690+
class S3ReadHandler(ReadHandler):
691+
"""Custom handler for reading from S3."""
692+
def __init__(self, bucket: str, prefix: str = ""):
693+
self.bucket = bucket
694+
self.prefix = prefix
695+
696+
def read_binary(self, subpath: PathLike) -> bytes:
697+
key = f"{self.prefix}/{subpath}" if self.prefix else str(subpath)
698+
return s3_client.get_object(Bucket=self.bucket, Key=key)['Body'].read()
699+
700+
# ... implement other methods
701+
702+
# Deterministic zip output (ZipWriteHandler uses fixed timestamps)
703+
# This ensures identical content produces identical zip files
704+
handler = ZipWriteHandler(buf)
705+
# All files get timestamp (2020, 1, 1, 0, 0, 0) for reproducibility
706+
```
707+
576708
## Examples
577709

578710
See the [examples/](examples/) directory:
@@ -583,11 +715,20 @@ See the [examples/](examples/) directory:
583715
## Development
584716

585717
```bash
718+
# Install dev dependencies
719+
pip install -e ".[dev]"
720+
586721
# Run tests
587-
python -m unittest discover tests -v
722+
pytest
723+
724+
# Run tests with verbose output
725+
pytest -v
726+
727+
# Run specific test file
728+
pytest tests/test_mesh.py -v
588729

589-
# Run specific test
590-
python -m unittest tests.test_mesh -v
730+
# Run tests with coverage
731+
pytest --cov=meshly --cov-report=html
591732
```
592733

593734
## License

python/meshly/__init__.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,23 +42,17 @@
4242
)
4343

4444
from .data_handler import (
45-
CacheLoader,
46-
CacheSaver,
47-
ReadHandler,
48-
WriteHandler,
45+
DataHandler,
4946
)
5047

5148

5249
__all__ = [
5350
# Packable base class
5451
"Packable",
5552
"PackableMetadata",
56-
"CacheLoader",
57-
"CacheSaver",
5853
"ArrayType",
5954
# Data handlers
60-
"ReadHandler",
61-
"WriteHandler",
55+
"DataHandler",
6256
# Mesh classes
6357
"Mesh",
6458
# Array types and utilities

python/meshly/array.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from pydantic import BaseModel, Field
1313
from meshoptimizer._loader import lib
1414

15-
from .data_handler import WriteHandler, ReadHandler, ZipBuffer
15+
from .data_handler import DataHandler, ZipBuffer
1616
from .common import PathLike
1717

1818
# Optional JAX support
@@ -297,15 +297,15 @@ def decode_array(encoded_array: EncodedArray) -> np.ndarray:
297297

298298
@staticmethod
299299
def save_array(
300-
handler: WriteHandler,
300+
handler: DataHandler,
301301
name: str,
302302
encoded_array: EncodedArray,
303303
) -> None:
304304
"""
305305
Save a single encoded array using a write handler.
306306
307307
Args:
308-
handler: WriteHandler for writing files
308+
handler: DataHandler for writing files
309309
name: Array name (e.g., "normals" or "markerIndices.boundary")
310310
encoded_array: EncodedArray to save
311311
"""
@@ -320,15 +320,15 @@ def save_array(
320320

321321
@staticmethod
322322
def load_array(
323-
handler: ReadHandler,
323+
handler: DataHandler,
324324
name: str,
325325
array_type: Optional[ArrayType] = None
326326
) -> Any:
327327
"""
328328
Load and decode a single array using a read handler.
329329
330330
Args:
331-
handler: ReadHandler for reading files
331+
handler: DataHandler for reading files
332332
name: Array name (e.g., "normals" or "markerIndices.boundary")
333333
array_type: Target array backend type ("numpy" or "jax"). If None (default), uses
334334
the array_type stored in the array's metadata.
@@ -403,9 +403,9 @@ def load_from_zip(
403403
"""
404404
if isinstance(source, BytesIO):
405405
source.seek(0)
406-
handler = ReadHandler.create_handler(ZipBuffer(source.read()))
406+
handler = DataHandler.create(ZipBuffer(source.read()))
407407
else:
408408
with open(source, "rb") as f:
409-
handler = ReadHandler.create_handler(ZipBuffer(f.read()))
409+
handler = DataHandler.create(ZipBuffer(f.read()))
410410

411411
return ArrayUtils.load_array(handler, "array", array_type)

0 commit comments

Comments
 (0)