graflo 1.3.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of graflo might be problematic. Click here for more details.

Files changed (70) hide show
  1. graflo/README.md +18 -0
  2. graflo/__init__.py +70 -0
  3. graflo/architecture/__init__.py +38 -0
  4. graflo/architecture/actor.py +1276 -0
  5. graflo/architecture/actor_util.py +450 -0
  6. graflo/architecture/edge.py +418 -0
  7. graflo/architecture/onto.py +376 -0
  8. graflo/architecture/onto_sql.py +54 -0
  9. graflo/architecture/resource.py +163 -0
  10. graflo/architecture/schema.py +135 -0
  11. graflo/architecture/transform.py +292 -0
  12. graflo/architecture/util.py +89 -0
  13. graflo/architecture/vertex.py +562 -0
  14. graflo/caster.py +736 -0
  15. graflo/cli/__init__.py +14 -0
  16. graflo/cli/ingest.py +203 -0
  17. graflo/cli/manage_dbs.py +197 -0
  18. graflo/cli/plot_schema.py +132 -0
  19. graflo/cli/xml2json.py +93 -0
  20. graflo/data_source/__init__.py +48 -0
  21. graflo/data_source/api.py +339 -0
  22. graflo/data_source/base.py +95 -0
  23. graflo/data_source/factory.py +304 -0
  24. graflo/data_source/file.py +148 -0
  25. graflo/data_source/memory.py +70 -0
  26. graflo/data_source/registry.py +82 -0
  27. graflo/data_source/sql.py +183 -0
  28. graflo/db/__init__.py +44 -0
  29. graflo/db/arango/__init__.py +22 -0
  30. graflo/db/arango/conn.py +1025 -0
  31. graflo/db/arango/query.py +180 -0
  32. graflo/db/arango/util.py +88 -0
  33. graflo/db/conn.py +377 -0
  34. graflo/db/connection/__init__.py +6 -0
  35. graflo/db/connection/config_mapping.py +18 -0
  36. graflo/db/connection/onto.py +717 -0
  37. graflo/db/connection/wsgi.py +29 -0
  38. graflo/db/manager.py +119 -0
  39. graflo/db/neo4j/__init__.py +16 -0
  40. graflo/db/neo4j/conn.py +639 -0
  41. graflo/db/postgres/__init__.py +37 -0
  42. graflo/db/postgres/conn.py +948 -0
  43. graflo/db/postgres/fuzzy_matcher.py +281 -0
  44. graflo/db/postgres/heuristics.py +133 -0
  45. graflo/db/postgres/inference_utils.py +428 -0
  46. graflo/db/postgres/resource_mapping.py +273 -0
  47. graflo/db/postgres/schema_inference.py +372 -0
  48. graflo/db/postgres/types.py +148 -0
  49. graflo/db/postgres/util.py +87 -0
  50. graflo/db/tigergraph/__init__.py +9 -0
  51. graflo/db/tigergraph/conn.py +2365 -0
  52. graflo/db/tigergraph/onto.py +26 -0
  53. graflo/db/util.py +49 -0
  54. graflo/filter/__init__.py +21 -0
  55. graflo/filter/onto.py +525 -0
  56. graflo/logging.conf +22 -0
  57. graflo/onto.py +312 -0
  58. graflo/plot/__init__.py +17 -0
  59. graflo/plot/plotter.py +616 -0
  60. graflo/util/__init__.py +23 -0
  61. graflo/util/chunker.py +807 -0
  62. graflo/util/merge.py +150 -0
  63. graflo/util/misc.py +37 -0
  64. graflo/util/onto.py +422 -0
  65. graflo/util/transform.py +454 -0
  66. graflo-1.3.7.dist-info/METADATA +243 -0
  67. graflo-1.3.7.dist-info/RECORD +70 -0
  68. graflo-1.3.7.dist-info/WHEEL +4 -0
  69. graflo-1.3.7.dist-info/entry_points.txt +5 -0
  70. graflo-1.3.7.dist-info/licenses/LICENSE +126 -0
@@ -0,0 +1,304 @@
1
+ """Factory for creating data source instances.
2
+
3
+ This module provides a factory for creating appropriate data source instances
4
+ based on configuration. It supports file-based, API, and SQL data sources.
5
+ """
6
+
7
+ import logging
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ import pandas as pd
12
+
13
+ from graflo.architecture.onto import EncodingType
14
+ from graflo.data_source.api import APIConfig, APIDataSource
15
+ from graflo.data_source.base import AbstractDataSource, DataSourceType
16
+ from graflo.data_source.file import (
17
+ JsonFileDataSource,
18
+ JsonlFileDataSource,
19
+ ParquetFileDataSource,
20
+ TableFileDataSource,
21
+ )
22
+ from graflo.data_source.memory import InMemoryDataSource
23
+ from graflo.data_source.sql import SQLConfig, SQLDataSource
24
+ from graflo.util.chunker import ChunkerFactory, ChunkerType
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ class DataSourceFactory:
30
+ """Factory for creating data source instances.
31
+
32
+ This factory creates appropriate data source instances based on the
33
+ provided configuration. It supports file-based, API, and SQL data sources.
34
+ """
35
+
36
+ @staticmethod
37
+ def _guess_file_type(filename: Path) -> ChunkerType:
38
+ """Guess the file type based on file extension.
39
+
40
+ Args:
41
+ filename: Path to the file
42
+
43
+ Returns:
44
+ ChunkerType: Guessed file type
45
+
46
+ Raises:
47
+ ValueError: If file extension is not recognized
48
+ """
49
+ return ChunkerFactory._guess_chunker_type(filename)
50
+
51
+ @classmethod
52
+ def create_file_data_source(
53
+ cls,
54
+ path: Path | str,
55
+ file_type: str | ChunkerType | None = None,
56
+ encoding: EncodingType = EncodingType.UTF_8,
57
+ sep: str | None = None,
58
+ ) -> (
59
+ JsonFileDataSource
60
+ | JsonlFileDataSource
61
+ | TableFileDataSource
62
+ | ParquetFileDataSource
63
+ ):
64
+ """Create a file-based data source.
65
+
66
+ Args:
67
+ path: Path to the file
68
+ file_type: Type of file ('json', 'jsonl', 'table', 'parquet') or ChunkerType.
69
+ If None, will be guessed from file extension.
70
+ encoding: File encoding (default: UTF_8)
71
+ sep: Field separator for table files (default: ',').
72
+ Only used for table files.
73
+
74
+ Returns:
75
+ Appropriate file data source instance (JsonFileDataSource,
76
+ JsonlFileDataSource, TableFileDataSource, or ParquetFileDataSource)
77
+
78
+ Raises:
79
+ ValueError: If file type cannot be determined
80
+ """
81
+ if isinstance(path, str):
82
+ path = Path(path)
83
+
84
+ # Determine file type
85
+ if file_type is None:
86
+ try:
87
+ file_type_enum = cls._guess_file_type(path)
88
+ except ValueError as e:
89
+ raise ValueError(
90
+ f"Could not determine file type for {path}. "
91
+ f"Please specify file_type explicitly. Error: {e}"
92
+ )
93
+ elif isinstance(file_type, str):
94
+ file_type_enum = ChunkerType(file_type.lower())
95
+ else:
96
+ file_type_enum = file_type
97
+
98
+ # Create appropriate data source
99
+ if file_type_enum == ChunkerType.JSON:
100
+ return JsonFileDataSource(path=path, encoding=encoding)
101
+ elif file_type_enum == ChunkerType.JSONL:
102
+ return JsonlFileDataSource(path=path, encoding=encoding)
103
+ elif file_type_enum == ChunkerType.TABLE:
104
+ # sep is only for table files
105
+ return TableFileDataSource(path=path, encoding=encoding, sep=sep or ",")
106
+ elif file_type_enum == ChunkerType.PARQUET:
107
+ return ParquetFileDataSource(path=path)
108
+ else:
109
+ raise ValueError(f"Unsupported file type: {file_type_enum}")
110
+
111
+ @classmethod
112
+ def create_api_data_source(cls, config: APIConfig) -> APIDataSource:
113
+ """Create an API data source.
114
+
115
+ Args:
116
+ config: API configuration
117
+
118
+ Returns:
119
+ APIDataSource instance
120
+ """
121
+ return APIDataSource(config=config)
122
+
123
+ @classmethod
124
+ def create_sql_data_source(cls, config: SQLConfig) -> SQLDataSource:
125
+ """Create a SQL data source.
126
+
127
+ Args:
128
+ config: SQL configuration
129
+
130
+ Returns:
131
+ SQLDataSource instance
132
+ """
133
+ return SQLDataSource(config=config)
134
+
135
+ @classmethod
136
+ def create_in_memory_data_source(
137
+ cls,
138
+ data: list[dict] | list[list] | pd.DataFrame,
139
+ columns: list[str] | None = None,
140
+ ) -> InMemoryDataSource:
141
+ """Create an in-memory data source.
142
+
143
+ Args:
144
+ data: Data to process (list[dict], list[list], or pd.DataFrame)
145
+ columns: Optional column names for list[list] data
146
+
147
+ Returns:
148
+ InMemoryDataSource instance
149
+ """
150
+ return InMemoryDataSource(data=data, columns=columns)
151
+
152
+ @classmethod
153
+ def create_data_source(
154
+ cls,
155
+ source_type: DataSourceType | str | None = None,
156
+ **kwargs: Any,
157
+ ) -> AbstractDataSource:
158
+ """Create a data source of the specified type.
159
+
160
+ This is a general factory method that routes to specific factory methods
161
+ based on the source type.
162
+
163
+ Args:
164
+ source_type: Type of data source to create. If None, will be inferred
165
+ from kwargs (e.g., 'path' -> FILE, 'data' -> IN_MEMORY, 'config' with url -> API)
166
+ **kwargs: Configuration parameters for the data source
167
+
168
+ Returns:
169
+ Data source instance
170
+
171
+ Raises:
172
+ ValueError: If source type is not supported or required parameters are missing
173
+ """
174
+ # Auto-detect source type if not provided
175
+ if source_type is None:
176
+ if "path" in kwargs or "file_type" in kwargs:
177
+ source_type = DataSourceType.FILE
178
+ elif "data" in kwargs:
179
+ source_type = DataSourceType.IN_MEMORY
180
+ elif "config" in kwargs:
181
+ config = kwargs["config"]
182
+ # Check if it's an API config (has 'url') or SQL config (has 'connection_string')
183
+ if isinstance(config, dict):
184
+ if "url" in config:
185
+ source_type = DataSourceType.API
186
+ elif "connection_string" in config or "query" in config:
187
+ source_type = DataSourceType.SQL
188
+ else:
189
+ # Try to create from dict
190
+ if "source_type" in config:
191
+ source_type = DataSourceType(config["source_type"].lower())
192
+ else:
193
+ raise ValueError(
194
+ "Cannot determine source type from config. "
195
+ "Please specify source_type or provide 'url' (API) "
196
+ "or 'connection_string'/'query' (SQL) in config."
197
+ )
198
+ elif hasattr(config, "url"):
199
+ source_type = DataSourceType.API
200
+ elif hasattr(config, "connection_string") or hasattr(config, "query"):
201
+ source_type = DataSourceType.SQL
202
+ else:
203
+ raise ValueError(
204
+ "Cannot determine source type from config. "
205
+ "Please specify source_type explicitly."
206
+ )
207
+ else:
208
+ raise ValueError(
209
+ "Cannot determine source type. Please specify source_type or "
210
+ "provide one of: path (FILE), data (IN_MEMORY), or config (API/SQL)."
211
+ )
212
+
213
+ if isinstance(source_type, str):
214
+ source_type = DataSourceType(source_type.lower())
215
+
216
+ if source_type == DataSourceType.FILE:
217
+ return cls.create_file_data_source(**kwargs)
218
+ elif source_type == DataSourceType.API:
219
+ if "config" not in kwargs:
220
+ # Create APIConfig from kwargs
221
+ from graflo.data_source.api import APIConfig, PaginationConfig
222
+
223
+ # Handle nested pagination config manually
224
+ api_kwargs = kwargs.copy()
225
+ pagination_dict = api_kwargs.pop("pagination", None)
226
+ pagination = None
227
+ if pagination_dict is not None:
228
+ if isinstance(pagination_dict, dict):
229
+ # Manually construct PaginationConfig to avoid dataclass_wizard issues
230
+ pagination = PaginationConfig(**pagination_dict)
231
+ else:
232
+ pagination = pagination_dict
233
+ api_kwargs["pagination"] = pagination
234
+ config = APIConfig(**api_kwargs)
235
+ return cls.create_api_data_source(config=config)
236
+ config = kwargs["config"]
237
+ if isinstance(config, dict):
238
+ from graflo.data_source.api import APIConfig, PaginationConfig
239
+
240
+ # Handle nested pagination config manually
241
+ config_copy = config.copy()
242
+ pagination_dict = config_copy.pop("pagination", None)
243
+ pagination = None
244
+ if pagination_dict is not None:
245
+ if isinstance(pagination_dict, dict):
246
+ # Manually construct PaginationConfig to avoid dataclass_wizard issues
247
+ pagination = PaginationConfig(**pagination_dict)
248
+ else:
249
+ pagination = pagination_dict
250
+ config_copy["pagination"] = pagination
251
+ config = APIConfig(**config_copy)
252
+ return cls.create_api_data_source(config=config)
253
+ elif source_type == DataSourceType.SQL:
254
+ if "config" not in kwargs:
255
+ # Create SQLConfig from kwargs
256
+ from graflo.data_source.sql import SQLConfig
257
+
258
+ config = SQLConfig.from_dict(kwargs)
259
+ return cls.create_sql_data_source(config=config)
260
+ config = kwargs["config"]
261
+ if isinstance(config, dict):
262
+ from graflo.data_source.sql import SQLConfig
263
+
264
+ config = SQLConfig.from_dict(config)
265
+ return cls.create_sql_data_source(config=config)
266
+ elif source_type == DataSourceType.IN_MEMORY:
267
+ if "data" not in kwargs:
268
+ raise ValueError("In-memory data source requires 'data' parameter")
269
+ return cls.create_in_memory_data_source(**kwargs)
270
+ else:
271
+ raise ValueError(f"Unsupported data source type: {source_type}")
272
+
273
+ @classmethod
274
+ def create_data_source_from_config(
275
+ cls, config: dict[str, Any]
276
+ ) -> AbstractDataSource:
277
+ """Create a data source from a configuration dictionary.
278
+
279
+ The configuration dict should contain:
280
+ - 'source_type': Type of data source (FILE, API, SQL, IN_MEMORY)
281
+ - Other parameters specific to the data source type
282
+
283
+ Examples:
284
+ File source:
285
+ {"source_type": "file", "path": "data.json"}
286
+ API source:
287
+ {"source_type": "api", "config": {"url": "https://api.example.com"}}
288
+ SQL source:
289
+ {"source_type": "sql", "config": {"connection_string": "...", "query": "..."}}
290
+ In-memory source:
291
+ {"source_type": "in_memory", "data": [...]}
292
+
293
+ Args:
294
+ config: Configuration dictionary
295
+
296
+ Returns:
297
+ Data source instance
298
+
299
+ Raises:
300
+ ValueError: If configuration is invalid
301
+ """
302
+ config = config.copy()
303
+ source_type = config.pop("source_type", None)
304
+ return cls.create_data_source(source_type=source_type, **config)
@@ -0,0 +1,148 @@
1
+ """File-based data source implementations.
2
+
3
+ This module provides data source implementations for file-based data sources,
4
+ including JSON, JSONL, and CSV/TSV files. It integrates with the existing
5
+ chunker logic for efficient batch processing.
6
+ """
7
+
8
+ import dataclasses
9
+ from pathlib import Path
10
+ from typing import Iterator
11
+
12
+ from graflo.architecture.onto import EncodingType
13
+ from graflo.data_source.base import AbstractDataSource, DataSourceType
14
+ from graflo.util.chunker import ChunkerFactory, ChunkerType
15
+
16
+
17
+ @dataclasses.dataclass
18
+ class FileDataSource(AbstractDataSource):
19
+ """Base class for file-based data sources.
20
+
21
+ This class provides a common interface for file-based data sources,
22
+ integrating with the existing chunker system for batch processing.
23
+
24
+ Attributes:
25
+ path: Path to the file
26
+ file_type: Type of file (json, jsonl, table)
27
+ encoding: File encoding (default: UTF_8)
28
+ """
29
+
30
+ path: Path | str
31
+ file_type: str | None = None
32
+ encoding: EncodingType = EncodingType.UTF_8
33
+
34
+ def __post_init__(self):
35
+ """Initialize the file data source."""
36
+ self.source_type = DataSourceType.FILE
37
+ if isinstance(self.path, str):
38
+ self.path = Path(self.path)
39
+
40
+ def iter_batches(
41
+ self, batch_size: int = 1000, limit: int | None = None
42
+ ) -> Iterator[list[dict]]:
43
+ """Iterate over file data in batches.
44
+
45
+ Args:
46
+ batch_size: Number of items per batch
47
+ limit: Maximum number of items to retrieve
48
+
49
+ Yields:
50
+ list[dict]: Batches of documents as dictionaries
51
+ """
52
+ # Determine chunker type
53
+ chunker_type = None
54
+ if self.file_type:
55
+ chunker_type = ChunkerType(self.file_type.lower())
56
+
57
+ # Create chunker using factory
58
+ chunker_kwargs = {
59
+ "resource": self.path,
60
+ "type": chunker_type,
61
+ "batch_size": batch_size,
62
+ "limit": limit,
63
+ "encoding": self.encoding,
64
+ }
65
+ # Only add sep for table files
66
+ if chunker_type == ChunkerType.TABLE and hasattr(self, "sep"):
67
+ chunker_kwargs["sep"] = self.sep
68
+
69
+ chunker = ChunkerFactory.create_chunker(**chunker_kwargs)
70
+
71
+ # Yield batches
72
+ for batch in chunker:
73
+ yield batch
74
+
75
+
76
+ @dataclasses.dataclass
77
+ class JsonFileDataSource(FileDataSource):
78
+ """Data source for JSON files.
79
+
80
+ JSON files are expected to contain hierarchical data structures,
81
+ similar to REST API responses. The chunker handles nested structures
82
+ and converts them to dictionaries.
83
+
84
+ Attributes:
85
+ path: Path to the JSON file
86
+ encoding: File encoding (default: UTF_8)
87
+ """
88
+
89
+ def __post_init__(self):
90
+ """Initialize the JSON file data source."""
91
+ super().__post_init__()
92
+ self.file_type = ChunkerType.JSON.value
93
+
94
+
95
+ @dataclasses.dataclass
96
+ class JsonlFileDataSource(FileDataSource):
97
+ """Data source for JSONL (JSON Lines) files.
98
+
99
+ JSONL files contain one JSON object per line, making them suitable
100
+ for streaming and batch processing.
101
+
102
+ Attributes:
103
+ path: Path to the JSONL file
104
+ encoding: File encoding (default: UTF_8)
105
+ """
106
+
107
+ def __post_init__(self):
108
+ """Initialize the JSONL file data source."""
109
+ super().__post_init__()
110
+ self.file_type = ChunkerType.JSONL.value
111
+
112
+
113
+ @dataclasses.dataclass
114
+ class TableFileDataSource(FileDataSource):
115
+ """Data source for CSV/TSV files.
116
+
117
+ Table files are converted to dictionaries with column headers as keys.
118
+ Each row becomes a dictionary.
119
+
120
+ Attributes:
121
+ path: Path to the CSV/TSV file
122
+ encoding: File encoding (default: UTF_8)
123
+ sep: Field separator (default: ',')
124
+ """
125
+
126
+ sep: str = ","
127
+
128
+ def __post_init__(self):
129
+ """Initialize the table file data source."""
130
+ super().__post_init__()
131
+ self.file_type = ChunkerType.TABLE.value
132
+
133
+
134
+ @dataclasses.dataclass
135
+ class ParquetFileDataSource(FileDataSource):
136
+ """Data source for Parquet files.
137
+
138
+ Parquet files are columnar storage format files that are read using pandas.
139
+ Each row becomes a dictionary with column names as keys.
140
+
141
+ Attributes:
142
+ path: Path to the Parquet file
143
+ """
144
+
145
+ def __post_init__(self):
146
+ """Initialize the Parquet file data source."""
147
+ super().__post_init__()
148
+ self.file_type = ChunkerType.PARQUET.value
@@ -0,0 +1,70 @@
1
+ """In-memory data source implementations.
2
+
3
+ This module provides data source implementations for in-memory data structures,
4
+ including lists of dictionaries, lists of lists, and Pandas DataFrames.
5
+ """
6
+
7
+ import dataclasses
8
+ from typing import Iterator
9
+
10
+ import pandas as pd
11
+
12
+ from graflo.data_source.base import AbstractDataSource, DataSourceType
13
+ from graflo.util.chunker import ChunkerFactory
14
+
15
+
16
+ @dataclasses.dataclass
17
+ class InMemoryDataSource(AbstractDataSource):
18
+ """Data source for in-memory data structures.
19
+
20
+ This class provides a data source for Python objects that are already
21
+ in memory, including lists of dictionaries, lists of lists, and Pandas DataFrames.
22
+
23
+ Attributes:
24
+ data: Data to process (list[dict], list[list], or pd.DataFrame)
25
+ columns: Optional column names for list[list] data
26
+ """
27
+
28
+ data: list[dict] | list[list] | pd.DataFrame
29
+ columns: list[str] | None = None
30
+
31
+ def __post_init__(self):
32
+ """Initialize the in-memory data source."""
33
+ self.source_type = DataSourceType.IN_MEMORY
34
+
35
+ def iter_batches(
36
+ self, batch_size: int = 1000, limit: int | None = None
37
+ ) -> Iterator[list[dict]]:
38
+ """Iterate over in-memory data in batches.
39
+
40
+ Args:
41
+ batch_size: Number of items per batch
42
+ limit: Maximum number of items to retrieve
43
+
44
+ Yields:
45
+ list[dict]: Batches of documents as dictionaries
46
+ """
47
+ # Normalize data: convert list[list] to list[dict] if needed
48
+ data = self.data
49
+ if isinstance(data, list) and len(data) > 0 and isinstance(data[0], list):
50
+ # list[list] - convert to list[dict] using columns
51
+ if self.columns is None:
52
+ raise ValueError(
53
+ "columns parameter is required when data is list[list]"
54
+ )
55
+ data = [{k: v for k, v in zip(self.columns, item)} for item in data]
56
+
57
+ # Create chunker using factory (only pass columns if it's a DataFrame)
58
+ chunker_kwargs = {
59
+ "resource": data,
60
+ "batch_size": batch_size,
61
+ "limit": limit,
62
+ }
63
+ # Note: columns is not passed to chunker - we handle list[list] conversion above
64
+ # DataFrame chunker doesn't need columns either
65
+
66
+ chunker = ChunkerFactory.create_chunker(**chunker_kwargs)
67
+
68
+ # Yield batches
69
+ for batch in chunker:
70
+ yield batch
@@ -0,0 +1,82 @@
1
+ """Data source registry for mapping data sources to resources.
2
+
3
+ This module provides a registry for mapping data sources to resource names.
4
+ Many data sources can map to the same resource, allowing flexible data
5
+ ingestion from multiple sources.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import dataclasses
11
+ from typing import TYPE_CHECKING
12
+
13
+ from graflo.onto import BaseDataclass
14
+
15
+ if TYPE_CHECKING:
16
+ from graflo.data_source.base import AbstractDataSource
17
+
18
+
19
+ @dataclasses.dataclass
20
+ class DataSourceRegistry(BaseDataclass):
21
+ """Registry for mapping data sources to resource names.
22
+
23
+ This class maintains a mapping from resource names to lists of data sources.
24
+ Many data sources can map to the same resource, allowing data to be ingested
25
+ from multiple sources and combined.
26
+
27
+ Attributes:
28
+ sources: Dictionary mapping resource names to lists of data sources
29
+ """
30
+
31
+ sources: dict[str, list[AbstractDataSource]] = dataclasses.field(
32
+ default_factory=dict
33
+ )
34
+
35
+ def register(self, data_source: AbstractDataSource, resource_name: str) -> None:
36
+ """Register a data source for a resource.
37
+
38
+ Args:
39
+ data_source: Data source to register
40
+ resource_name: Name of the resource to map to
41
+ """
42
+ if resource_name not in self.sources:
43
+ self.sources[resource_name] = []
44
+ self.sources[resource_name].append(data_source)
45
+ data_source.resource_name = resource_name
46
+
47
+ def get_data_sources(self, resource_name: str) -> list[AbstractDataSource]:
48
+ """Get all data sources for a resource.
49
+
50
+ Args:
51
+ resource_name: Name of the resource
52
+
53
+ Returns:
54
+ List of data sources for the resource (empty list if none found)
55
+ """
56
+ return self.sources.get(resource_name, [])
57
+
58
+ def get_all_data_sources(self) -> list[AbstractDataSource]:
59
+ """Get all registered data sources.
60
+
61
+ Returns:
62
+ List of all registered data sources
63
+ """
64
+ all_sources = []
65
+ for sources_list in self.sources.values():
66
+ all_sources.extend(sources_list)
67
+ return all_sources
68
+
69
+ def has_resource(self, resource_name: str) -> bool:
70
+ """Check if a resource has any data sources.
71
+
72
+ Args:
73
+ resource_name: Name of the resource
74
+
75
+ Returns:
76
+ True if the resource has data sources, False otherwise
77
+ """
78
+ return resource_name in self.sources and len(self.sources[resource_name]) > 0
79
+
80
+ def clear(self) -> None:
81
+ """Clear all registered data sources."""
82
+ self.sources.clear()