1- """File-based storage backend using YAML files in a directory tree .
1+ """File-based storage backend — entities in Parquet, flags/runs in YAML .
22
3- Wraps the existing library behavior: entities stored as YAML files in
4- entity-type subdirectories (elements/, schemas/, values/, valuesets/),
5- curation flags in curation-flags/, run summaries in runs/ .
3+ FileEntityStore is a thin wrapper around ParquetStore for all entity
4+ operations. FileFlagStore and FileRunStore still use YAML for flags and
5+ run summaries respectively .
66"""
77
88from __future__ import annotations
99
10+ import logging
1011import uuid
1112from datetime import datetime , timezone
1213from pathlib import Path
1516
1617from ..models import CurationFlag , FlagStatus , FlagType , RunSummary
1718from ..utils import safe_load_yaml , write_yaml
19+ from .parquet_store import ParquetStore
1820from .protocol import VALID_ENTITY_TYPES
1921
22+ logger = logging .getLogger (__name__ )
23+
2024
2125class FileEntityStore :
22- """EntityStore implementation backed by YAML files."""
26+ """EntityStore implementation backed by ParquetStore.
27+
28+ All entity read/write operations delegate to ParquetStore.
29+ This class exists to satisfy the EntityStore protocol and provide
30+ a consistent interface with FileBackend.
31+ """
2332
2433 def __init__ (self , base_dir : Path ) -> None :
2534 self ._base = base_dir
35+ self ._pq = ParquetStore (base_dir )
2636
27- def _type_dir (self , entity_type : str ) -> Path :
37+ def _validate_type (self , entity_type : str ) -> None :
2838 if entity_type not in VALID_ENTITY_TYPES :
2939 raise ValueError (
3040 f"Invalid entity type: { entity_type !r} . Must be one of { VALID_ENTITY_TYPES } "
3141 )
32- d = self ._base / entity_type
33- d .mkdir (parents = True , exist_ok = True )
34- return d
3542
3643 def read (self , entity_type : str , identifier : str ) -> dict | None :
37- d = self ._type_dir (entity_type )
38- path = d / f"{ identifier } .yaml"
39- if not path .exists ():
40- # Try with .yaml extension already in identifier
41- if not identifier .endswith (".yaml" ):
42- return None
43- path = d / identifier
44- if not path .exists ():
45- return None
46- return safe_load_yaml (path )
44+ self ._validate_type (entity_type )
45+ return self ._pq .read (entity_type , identifier )
4746
4847 def write (self , entity_type : str , data : dict , identifier : str | None = None ) -> str :
49- d = self ._type_dir (entity_type )
48+ self ._validate_type (entity_type )
5049 if identifier is None :
51- identifier = str (uuid .uuid4 ())
52- path = d / f"{ identifier } .yaml"
53- write_yaml (path , data )
50+ identifier = data .get ("sha256" ) or str (uuid .uuid4 ())
51+ # Ensure sha256 is set on the entity
52+ if not data .get ("sha256" ):
53+ data ["sha256" ] = identifier
54+ # Determine source from provenance
55+ prov = data .get ("provenance" , [])
56+ source = "unknown"
57+ if prov and isinstance (prov [0 ], dict ):
58+ source = prov [0 ].get ("source" , "unknown" )
59+ self ._pq .write_batch (entity_type , [data ], source = source )
5460 return identifier
5561
5662 def list (self , entity_type : str , ** filters : object ) -> Iterator [dict ]:
57- d = self ._type_dir (entity_type )
58- if not d .exists ():
59- return
60-
63+ self ._validate_type (entity_type )
6164 source_filter = filters .get ("source" )
6265 has_annotations = filters .get ("has_annotations" )
6366 data_type_filter = filters .get ("data_type" )
6467
65- # Yield from Parquet files first
66- from .parquet_store import ParquetStore
67-
68- pq_store = ParquetStore (self ._base )
69- seen_sha : set [str ] = set ()
70- for entity in pq_store .list (entity_type , source = source_filter if source_filter else None ):
71- sha = entity .get ("sha256" , "" )
72- if sha :
73- seen_sha .add (sha )
74-
75- # Apply remaining filters
68+ for entity in self ._pq .list (
69+ entity_type ,
70+ source = source_filter if source_filter else None ,
71+ ):
72+ # Apply additional filters
7673 if has_annotations is not None :
7774 anns = entity .get ("ontology_annotations" , [])
7875 if has_annotations != bool (anns ):
@@ -81,125 +78,63 @@ def list(self, entity_type: str, **filters: object) -> Iterator[dict]:
8178 if entity .get ("data_type" ) != data_type_filter :
8279 continue
8380
84- entity ["_identifier" ] = sha or entity .get ("file_name" , "" )
81+ entity ["_identifier" ] = entity . get ( "sha256" , "" ) or entity .get ("file_name" , "" )
8582 yield entity
8683
87- # Then yield from YAML files (skip if already seen in Parquet)
88- for f in sorted (d .glob ("*.yaml" )):
89- data = safe_load_yaml (f )
90- if data is None :
91- continue
92-
93- # Skip if already yielded from Parquet
94- sha = data .get ("sha256" , data .get ("semantic" , {}).get ("sha256" , "" ))
95- if sha and sha in seen_sha :
96- continue
97-
98- # Inject _identifier for downstream use
99- data ["_identifier" ] = f .stem
100-
101- # Apply filters
102- if source_filter is not None :
103- provenance = data .get ("provenance" , [])
104- sources = {p .get ("source" , "" ) for p in provenance if isinstance (p , dict )}
105- if source_filter not in sources :
106- continue
107-
108- if has_annotations is not None :
109- annotations = data .get ("semantic" , {}).get ("ontology_annotations" , [])
110- has_any = bool (annotations )
111- if has_annotations != has_any :
112- continue
113-
114- if data_type_filter is not None :
115- dt = data .get ("semantic" , {}).get ("data_type" )
116- if dt != data_type_filter :
117- continue
118-
119- yield data
120-
12184 def exists (self , entity_type : str , identifier : str ) -> bool :
122- d = self ._type_dir (entity_type )
123- return ( d / f" { identifier } .yaml" ) .exists ()
85+ self ._validate_type (entity_type )
86+ return self . _pq .exists (entity_type , identifier )
12487
12588 def delete (self , entity_type : str , identifier : str ) -> bool :
126- d = self . _type_dir ( entity_type )
127- path = d / f" { identifier } .yaml"
128- if path . exists ():
129- path . unlink ()
130- return True
89+ logger . warning (
90+ "delete() is not supported for Parquet-backed store; ignoring delete for %s/%s" ,
91+ entity_type ,
92+ identifier ,
93+ )
13194 return False
13295
13396 def merge_provenance (self , entity_type : str , identifier : str , provenance : list [dict ]) -> dict :
134- data = self .read (entity_type , identifier )
135- if data is None :
97+ self ._validate_type (entity_type )
98+ entity = self ._pq .read (entity_type , identifier )
99+ if entity is None :
136100 raise KeyError (f"Entity not found: { entity_type } /{ identifier } " )
137101
138- existing_prov = data .get ("provenance" , [])
139- # Deduplicate by (source, name)
102+ existing_prov = entity .get ("provenance" , [])
140103 existing_keys = {(p .get ("source" , "" ), p .get ("name" , "" )) for p in existing_prov }
141104 for p in provenance :
142105 key = (p .get ("source" , "" ), p .get ("name" , "" ))
143106 if key not in existing_keys :
144107 existing_prov .append (p )
145108 existing_keys .add (key )
146109
147- data ["provenance" ] = existing_prov
148- # Remove internal metadata before writing
149- clean = {k : v for k , v in data .items () if not k .startswith ("_" )}
150- self .write (entity_type , clean , identifier )
151- return data
110+ return self ._pq .update (entity_type , identifier , {"provenance" : existing_prov }) or entity
152111
153112 def count (self , entity_type : str , ** filters : object ) -> int :
113+ self ._validate_type (entity_type )
154114 if not filters :
155- d = self ._type_dir (entity_type )
156- return len (list (d .glob ("*.yaml" )))
115+ return self ._pq .count (entity_type )
157116 return sum (1 for _ in self .list (entity_type , ** filters ))
158117
159118 def find_by_hash (self , entity_type : str , short_key : str ) -> dict | None :
160- d = self ._type_dir (entity_type )
161- matches = list (d .glob (f"*_{ short_key } .yaml" ))
162- if not matches :
163- # Also try exact match on identifier
164- exact = d / f"{ short_key } .yaml"
165- if exact .exists ():
166- return safe_load_yaml (exact )
167- # Try Parquet files
168- from .parquet_store import ParquetStore
169-
170- pq_store = ParquetStore (self ._base )
171- return pq_store .read (entity_type , short_key )
172- return safe_load_yaml (matches [0 ])
119+ self ._validate_type (entity_type )
120+ return self ._pq .read (entity_type , short_key )
173121
174122 def write_batch (
175123 self ,
176124 entity_type : str ,
177125 entities : list [dict ],
178126 source : str | None = None ,
179127 ) -> int :
180- """Write a batch of entities using Parquet format ."""
128+ """Write a batch of entities to Parquet."""
181129 if not entities :
182130 return 0
183-
184- from .parquet_store import ParquetStore
185-
186- pq_store = ParquetStore (self ._base )
187- return pq_store .write_batch (entity_type , entities , source = source or "unknown" )
131+ self ._validate_type (entity_type )
132+ return self ._pq .write_batch (entity_type , entities , source = source or "unknown" )
188133
189134 def read_batch (self , entity_type : str , source : str | None = None ) -> list [dict ]:
190- """Read all entities of a type, from both YAML files and Parquet."""
191- results = list (self .list (entity_type , ** ({"source" : source } if source else {})))
192-
193- # Also read from Parquet files
194- from .parquet_store import ParquetStore
195-
196- pq_store = ParquetStore (self ._base )
197- seen = {r .get ("sha256" , r .get ("_identifier" , "" )) for r in results }
198- for entity in pq_store .list (entity_type , source = source ):
199- if entity .get ("sha256" ) not in seen :
200- results .append (entity )
201- seen .add (entity .get ("sha256" , "" ))
202- return results
135+ """Read all entities of a type from Parquet."""
136+ self ._validate_type (entity_type )
137+ return list (self ._pq .list (entity_type , source = source ))
203138
204139
205140class FileFlagStore :
@@ -385,14 +320,14 @@ def _dict_to_summary(data: dict) -> RunSummary:
385320
386321
387322class FileBackend :
388- """StorageBackend implementation using YAML files in a directory tree .
323+ """StorageBackend implementation — entities in Parquet, flags/runs in YAML .
389324
390325 Directory layout:
391326 base_dir/
392- ├── elements/*.yaml
393- ├── schemas/*.yaml
394- ├── values/*.yaml
395- ├── valuesets/*.yaml
327+ ├── elements/*.parquet
328+ ├── schemas/*.parquet
329+ ├── values/*.parquet
330+ ├── valuesets/*.parquet
396331 ├── curation-flags/*.yaml
397332 └── runs/*.yaml
398333 """
0 commit comments