@@ -54,21 +54,26 @@ def __init__(
5454 source : SourceProtocol ,
5555 cache_database : ArrowDatabaseProtocol ,
5656 cache_path_prefix : tuple [str , ...] = (),
57+ cache_path : tuple [str , ...] | None = None ,
58+ source_id : str | None = None ,
5759 label : str | None = None ,
5860 data_context : str | contexts .DataContext | None = None ,
5961 config : Config | None = None ,
6062 ) -> None :
6163 if data_context is None :
6264 data_context = source .data_context_key
65+ if source_id is None :
66+ source_id = source .source_id
6367 super ().__init__ (
64- source_id = source . source_id ,
68+ source_id = source_id ,
6569 label = label ,
6670 data_context = data_context ,
6771 config = config ,
6872 )
69- self ._source = source
73+ self ._source : SourceProtocol = source
7074 self ._cache_database = cache_database
7175 self ._cache_path_prefix = cache_path_prefix
76+ self ._explicit_cache_path = cache_path
7277 self ._cached_stream : ArrowTableStream | None = None
7378
7479 # -------------------------------------------------------------------------
@@ -79,20 +84,28 @@ def to_config(self) -> dict[str, Any]:
7984 """Serialize this CachedSource configuration to a JSON-compatible dict.
8085
8186 Returns:
82- Dict containing the inner source config, cache database config, and
83- cache path prefix.
87+ Dict containing the inner source config, cache database config,
88+ cache path prefix, and resolved cache path (for cache-only loading) .
8489 """
8590 return {
8691 "source_type" : "cached" ,
8792 "inner_source" : self ._source .to_config (),
8893 "cache_database" : self ._cache_database .to_config (),
8994 "cache_path_prefix" : list (self ._cache_path_prefix ),
95+ "cache_path" : list (self .cache_path ),
96+ "source_id" : self .source_id ,
97+ ** self ._identity_config (),
9098 }
9199
92100 @classmethod
93- def from_config (cls , config : dict [str , Any ]) -> " CachedSource" :
101+ def from_config (cls , config : dict [str , Any ]) -> CachedSource :
94102 """Reconstruct a CachedSource from a config dict.
95103
104+ If the inner source cannot be resolved (e.g. it requires live data
105+ that is unavailable), ``resolve_source_from_config`` returns a
106+ ``SourceProxy`` preserving the original source's identity. The
107+ CachedSource can still serve data from its cache database.
108+
96109 Args:
97110 config: Dict as produced by :meth:`to_config`.
98111
@@ -104,12 +117,17 @@ def from_config(cls, config: dict[str, Any]) -> "CachedSource":
104117 resolve_source_from_config ,
105118 )
106119
107- inner_source = resolve_source_from_config (config ["inner_source" ])
108120 cache_db = resolve_database_from_config (config ["cache_database" ])
121+ inner_source = resolve_source_from_config (
122+ config ["inner_source" ], fallback_to_proxy = True
123+ )
124+
109125 return cls (
110126 source = inner_source ,
111127 cache_database = cache_db ,
112128 cache_path_prefix = tuple (config .get ("cache_path_prefix" , ())),
129+ cache_path = tuple (config ["cache_path" ]) if "cache_path" in config else None ,
130+ source_id = config .get ("source_id" ),
113131 )
114132
115133 # -------------------------------------------------------------------------
@@ -126,6 +144,8 @@ def identity_structure(self) -> Any:
126144 @property
127145 def cache_path (self ) -> tuple [str , ...]:
128146 """Cache table path, scoped to the source's content hash."""
147+ if self ._explicit_cache_path is not None :
148+ return self ._explicit_cache_path
129149 return self ._cache_path_prefix + (
130150 "source" ,
131151 f"node:{ self ._source .content_hash ().to_string ()} " ,
@@ -151,12 +171,12 @@ def keys(
151171 ) -> tuple [tuple [str , ...], tuple [str , ...]]:
152172 return self ._source .keys (columns = columns , all_info = all_info )
153173
154- def _build_merged_stream (self ) -> ArrowTableStream :
155- """
156- Run the live source, store new rows in the cache, load all cached
157- rows, and return the merged result as an ArrowTableStream.
174+ def _ingest_live_data (self ) -> None :
175+ """Fetch live data from the source and store new rows in the cache.
176+
177+ Raises if the source cannot provide data (e.g. an unbound
178+ ``SourceProxy``).
158179 """
159- # Get live source table with source info and system tags
160180 live_table = self ._source .as_table (
161181 columns = {"source" : True , "system_tags" : True }
162182 )
@@ -184,16 +204,38 @@ def _build_merged_stream(self) -> ArrowTableStream:
184204 )
185205 self ._cache_database .flush ()
186206
187- # Load all cached records (union of current + prior runs)
207+ def _build_merged_stream (self ) -> ArrowTableStream :
208+ """Ingest live data (if available), then return all cached records.
209+
210+ If the inner source cannot provide data (e.g. an unbound
211+ ``SourceProxy``), the method falls back to returning whatever is
212+ already stored in the cache database. If the cache is empty, an
213+ empty stream is returned.
214+ """
215+ try :
216+ self ._ingest_live_data ()
217+ except NotImplementedError :
218+ logger .info (
219+ "Inner source %r cannot provide data; serving from cache only." ,
220+ self ._source .source_id ,
221+ )
222+
188223 all_records = self ._cache_database .get_all_records (self .cache_path )
189- assert all_records is not None , (
190- "Cache should contain records after storing live data."
191- )
224+ if all_records is None :
225+ all_records = self ._empty_table ()
192226
193- # Build stream from merged table
194227 tag_keys = self ._source .keys ()[0 ]
195228 return ArrowTableStream (all_records , tag_columns = tag_keys )
196229
230+ def _empty_table (self ) -> pa .Table :
231+ """Build an empty Arrow table matching the source's output schema."""
232+ tag_schema , packet_schema = self ._source .output_schema ()
233+ merged = dict (tag_schema )
234+ merged .update (packet_schema )
235+ type_converter = self .data_context .type_converter
236+ arrow_schema = type_converter .python_schema_to_arrow_schema (merged )
237+ return pa .Table .from_pylist ([], schema = arrow_schema )
238+
197239 @property
198240 def is_stale (self ) -> bool :
199241 """True if the wrapped source has been modified since the last build.
0 commit comments