@@ -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
220221For 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))
242242mesh1 = PhysicsMesh(vertices = v1, indices = i1, physics = shared_physics)
243243mesh2 = 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
573605WriteHandler.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
578710See 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
0 commit comments