mattergraph-connectors 0.1.0__tar.gz

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.
Files changed (28) hide show
  1. mattergraph_connectors-0.1.0/.gitignore +48 -0
  2. mattergraph_connectors-0.1.0/PKG-INFO +83 -0
  3. mattergraph_connectors-0.1.0/README.md +48 -0
  4. mattergraph_connectors-0.1.0/mattergraph_connectors/__init__.py +213 -0
  5. mattergraph_connectors-0.1.0/mattergraph_connectors/base.py +183 -0
  6. mattergraph_connectors-0.1.0/mattergraph_connectors/http_policy.py +160 -0
  7. mattergraph_connectors-0.1.0/mattergraph_connectors/jarvis.py +170 -0
  8. mattergraph_connectors-0.1.0/mattergraph_connectors/lematerial.py +240 -0
  9. mattergraph_connectors-0.1.0/mattergraph_connectors/local_csv.py +71 -0
  10. mattergraph_connectors-0.1.0/mattergraph_connectors/local_import.py +605 -0
  11. mattergraph_connectors-0.1.0/mattergraph_connectors/materials_project.py +212 -0
  12. mattergraph_connectors-0.1.0/mattergraph_connectors/nomad.py +294 -0
  13. mattergraph_connectors-0.1.0/mattergraph_connectors/optimade.py +571 -0
  14. mattergraph_connectors-0.1.0/mattergraph_connectors/oqmd.py +33 -0
  15. mattergraph_connectors-0.1.0/mattergraph_connectors/py.typed +0 -0
  16. mattergraph_connectors-0.1.0/mattergraph_connectors/schema_generation.py +33 -0
  17. mattergraph_connectors-0.1.0/pyproject.toml +56 -0
  18. mattergraph_connectors-0.1.0/tests/test_connector_contract.py +268 -0
  19. mattergraph_connectors-0.1.0/tests/test_connectors_optional_imports.py +59 -0
  20. mattergraph_connectors-0.1.0/tests/test_http_policy.py +50 -0
  21. mattergraph_connectors-0.1.0/tests/test_jarvis.py +156 -0
  22. mattergraph_connectors-0.1.0/tests/test_lematerial.py +122 -0
  23. mattergraph_connectors-0.1.0/tests/test_lematerial_examples.py +24 -0
  24. mattergraph_connectors-0.1.0/tests/test_local_csv.py +12 -0
  25. mattergraph_connectors-0.1.0/tests/test_local_import.py +90 -0
  26. mattergraph_connectors-0.1.0/tests/test_materials_project.py +193 -0
  27. mattergraph_connectors-0.1.0/tests/test_nomad.py +207 -0
  28. mattergraph_connectors-0.1.0/tests/test_optimade.py +606 -0
@@ -0,0 +1,48 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .eggs/
6
+ dist/
7
+ build/
8
+ .venv/
9
+ venv/
10
+ .mypy_cache/
11
+ .ruff_cache/
12
+ .pytest_cache/
13
+ .hypothesis/
14
+ .coverage
15
+ coverage.xml
16
+ htmlcov/
17
+
18
+ # Docs build output
19
+ site/
20
+
21
+ # Env
22
+ .env
23
+ .env.local
24
+ *.local
25
+
26
+ # Node
27
+ node_modules/
28
+ apps/web/dist/
29
+ apps/web/playwright-report/
30
+ apps/web/test-results/
31
+ *.tsbuildinfo
32
+ .next/
33
+ out/
34
+
35
+ # IDE
36
+ .idea/
37
+ .vscode/
38
+ *.swp
39
+
40
+ # OS
41
+ .DS_Store
42
+
43
+ # Data artifacts (keep demo/ tracked)
44
+ data/cache/
45
+ *.sqlite3
46
+
47
+ .claude
48
+ apps/private-platform-ui/
@@ -0,0 +1,83 @@
1
+ Metadata-Version: 2.4
2
+ Name: mattergraph-connectors
3
+ Version: 0.1.0
4
+ Summary: Connectors to public materials databases for MatterGraph.
5
+ Project-URL: Homepage, https://github.com/cyrusmo/MatterGraph
6
+ Project-URL: Repository, https://github.com/cyrusmo/MatterGraph
7
+ Project-URL: Issues, https://github.com/cyrusmo/MatterGraph/issues
8
+ Project-URL: Changelog, https://github.com/cyrusmo/MatterGraph/blob/main/CHANGELOG.md
9
+ Author: MatterGraph contributors
10
+ License-Expression: Apache-2.0
11
+ Keywords: jarvis,lematerial,materials-project,materials-science,nomad
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Scientific/Engineering :: Chemistry
20
+ Classifier: Topic :: Scientific/Engineering :: Physics
21
+ Requires-Python: >=3.10
22
+ Requires-Dist: httpx>=0.27.0
23
+ Requires-Dist: mattergraph-core~=0.1.0
24
+ Requires-Dist: pandas>=2.0
25
+ Requires-Dist: pydantic>=2.5
26
+ Requires-Dist: pymatgen>=2024.1.1
27
+ Provides-Extra: all
28
+ Requires-Dist: jarvis-tools>=2024.1.1; extra == 'all'
29
+ Requires-Dist: mp-api>=0.40.0; extra == 'all'
30
+ Provides-Extra: jarvis
31
+ Requires-Dist: jarvis-tools>=2024.1.1; extra == 'jarvis'
32
+ Provides-Extra: mp
33
+ Requires-Dist: mp-api>=0.40.0; extra == 'mp'
34
+ Description-Content-Type: text/markdown
35
+
36
+ # mattergraph-connectors
37
+
38
+ Connectors to public materials databases for [MatterGraph](https://github.com/cyrusmo/MatterGraph).
39
+
40
+ Each connector returns normalized `Material` records from `mattergraph-core`, so downstream filtering, graph export, and scoring work identically regardless of source.
41
+
42
+ ## Sources
43
+
44
+ | Connector | Status |
45
+ |---|---|
46
+ | Materials Project (`mp-api`) | Supported — requires `MP_API_KEY` |
47
+ | JARVIS-DFT (`jarvis-tools`) | Supported |
48
+ | NOMAD | Public metadata reads, no API key needed. Metadata-only in v0.1 — does not fetch archives or derived scalar properties. |
49
+ | LeMat-Bulk | Companion adapter (records, parquet, or Hugging Face `datasets`) |
50
+ | Local CSV | Supported |
51
+ | OQMD | Stub — preserves the API surface, returns no records |
52
+
53
+ Heavy source SDKs are imported lazily, so importing the package stays cheap and a missing optional dependency produces a readable install hint rather than an `ImportError` traceback.
54
+
55
+ ## Install
56
+
57
+ ```bash
58
+ pip install mattergraph-connectors
59
+ ```
60
+
61
+ ## Example
62
+
63
+ ```python
64
+ import json
65
+ from pathlib import Path
66
+
67
+ from mattergraph_connectors import LeMatBulk
68
+
69
+ artifact = json.loads(Path("data/demo/spc_real_snapshot.json").read_text())
70
+ dataset = LeMatBulk.from_records(artifact["records"], subset="compatible_pbe")
71
+
72
+ candidates = (
73
+ dataset
74
+ .filter_elements(include=["Ti", "Al", "N"])
75
+ .filter_complexity(max_nsites=16, max_nelements=3)
76
+ .create_slice("spc_tialn_candidates_v1", target="energy_above_hull")
77
+ )
78
+ print(candidates.report())
79
+ ```
80
+
81
+ ## License
82
+
83
+ Apache-2.0
@@ -0,0 +1,48 @@
1
+ # mattergraph-connectors
2
+
3
+ Connectors to public materials databases for [MatterGraph](https://github.com/cyrusmo/MatterGraph).
4
+
5
+ Each connector returns normalized `Material` records from `mattergraph-core`, so downstream filtering, graph export, and scoring work identically regardless of source.
6
+
7
+ ## Sources
8
+
9
+ | Connector | Status |
10
+ |---|---|
11
+ | Materials Project (`mp-api`) | Supported — requires `MP_API_KEY` |
12
+ | JARVIS-DFT (`jarvis-tools`) | Supported |
13
+ | NOMAD | Public metadata reads, no API key needed. Metadata-only in v0.1 — does not fetch archives or derived scalar properties. |
14
+ | LeMat-Bulk | Companion adapter (records, parquet, or Hugging Face `datasets`) |
15
+ | Local CSV | Supported |
16
+ | OQMD | Stub — preserves the API surface, returns no records |
17
+
18
+ Heavy source SDKs are imported lazily, so importing the package stays cheap and a missing optional dependency produces a readable install hint rather than an `ImportError` traceback.
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ pip install mattergraph-connectors
24
+ ```
25
+
26
+ ## Example
27
+
28
+ ```python
29
+ import json
30
+ from pathlib import Path
31
+
32
+ from mattergraph_connectors import LeMatBulk
33
+
34
+ artifact = json.loads(Path("data/demo/spc_real_snapshot.json").read_text())
35
+ dataset = LeMatBulk.from_records(artifact["records"], subset="compatible_pbe")
36
+
37
+ candidates = (
38
+ dataset
39
+ .filter_elements(include=["Ti", "Al", "N"])
40
+ .filter_complexity(max_nsites=16, max_nelements=3)
41
+ .create_slice("spc_tialn_candidates_v1", target="energy_above_hull")
42
+ )
43
+ print(candidates.report())
44
+ ```
45
+
46
+ ## License
47
+
48
+ Apache-2.0
@@ -0,0 +1,213 @@
1
+ from __future__ import annotations
2
+
3
+ from importlib import import_module
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ if TYPE_CHECKING:
7
+ from mattergraph_connectors.base import (
8
+ Connector,
9
+ ConnectorQuery,
10
+ apply_property_filter,
11
+ coerce_query,
12
+ connector_provenance,
13
+ )
14
+ from mattergraph_connectors.http_policy import ConnectorHTTPPolicy, MemoryResponseCache
15
+ from mattergraph_connectors.jarvis import JarvisConnector
16
+ from mattergraph_connectors.lematerial import LeMatBulk
17
+ from mattergraph_connectors.local_csv import load_materials_from_csv
18
+ from mattergraph_connectors.local_import import (
19
+ DatasetImportMapping,
20
+ ImportIssue,
21
+ ImportLimitError,
22
+ ImportReport,
23
+ ImportResult,
24
+ ImportValidationError,
25
+ NormalizedImport,
26
+ PropertyColumnMapping,
27
+ import_local_content,
28
+ inspect_local_content,
29
+ )
30
+ from mattergraph_connectors.materials_project import MaterialsProjectConnector
31
+ from mattergraph_connectors.nomad import (
32
+ NOMADConnector,
33
+ NOMADConnectorError,
34
+ NOMADHTTPError,
35
+ NOMADMappingError,
36
+ NOMADPayloadError,
37
+ NOMADStubConnector,
38
+ )
39
+ from mattergraph_connectors.optimade import (
40
+ PROVIDERS,
41
+ OptimadeConnector,
42
+ OptimadeConnectorError,
43
+ OptimadeHTTPError,
44
+ OptimadeMappingError,
45
+ OptimadePayloadError,
46
+ )
47
+ from mattergraph_connectors.oqmd import OQMDStubConnector
48
+
49
+ _EXPORTS: dict[str, tuple[str, str, str | None]] = {
50
+ "Connector": ("mattergraph_connectors.base", "Connector", None),
51
+ "ConnectorQuery": ("mattergraph_connectors.base", "ConnectorQuery", None),
52
+ "apply_property_filter": ("mattergraph_connectors.base", "apply_property_filter", None),
53
+ "coerce_query": ("mattergraph_connectors.base", "coerce_query", None),
54
+ "connector_provenance": ("mattergraph_connectors.base", "connector_provenance", None),
55
+ "MaterialsProjectConnector": (
56
+ "mattergraph_connectors.materials_project",
57
+ "MaterialsProjectConnector",
58
+ (
59
+ "MaterialsProjectConnector needs the optional `mp-api` dependency: install the extra "
60
+ "with `pip install 'mattergraph-connectors[mp]'`, or run "
61
+ "`uv sync --all-packages --group dev --extra all` for a full workspace environment."
62
+ ),
63
+ ),
64
+ "JarvisConnector": (
65
+ "mattergraph_connectors.jarvis",
66
+ "JarvisConnector",
67
+ (
68
+ "JarvisConnector needs the optional `jarvis-tools` dependency: install the extra "
69
+ "with `pip install 'mattergraph-connectors[jarvis]'`, or run "
70
+ "`uv sync --all-packages --group dev --extra all` for a full workspace environment."
71
+ ),
72
+ ),
73
+ "load_materials_from_csv": (
74
+ "mattergraph_connectors.local_csv",
75
+ "load_materials_from_csv",
76
+ None,
77
+ ),
78
+ "ConnectorHTTPPolicy": (
79
+ "mattergraph_connectors.http_policy",
80
+ "ConnectorHTTPPolicy",
81
+ None,
82
+ ),
83
+ "MemoryResponseCache": (
84
+ "mattergraph_connectors.http_policy",
85
+ "MemoryResponseCache",
86
+ None,
87
+ ),
88
+ "DatasetImportMapping": ("mattergraph_connectors.local_import", "DatasetImportMapping", None),
89
+ "ImportIssue": ("mattergraph_connectors.local_import", "ImportIssue", None),
90
+ "ImportLimitError": ("mattergraph_connectors.local_import", "ImportLimitError", None),
91
+ "ImportReport": ("mattergraph_connectors.local_import", "ImportReport", None),
92
+ "ImportResult": ("mattergraph_connectors.local_import", "ImportResult", None),
93
+ "ImportValidationError": (
94
+ "mattergraph_connectors.local_import",
95
+ "ImportValidationError",
96
+ None,
97
+ ),
98
+ "NormalizedImport": ("mattergraph_connectors.local_import", "NormalizedImport", None),
99
+ "PropertyColumnMapping": (
100
+ "mattergraph_connectors.local_import",
101
+ "PropertyColumnMapping",
102
+ None,
103
+ ),
104
+ "import_local_content": ("mattergraph_connectors.local_import", "import_local_content", None),
105
+ "inspect_local_content": ("mattergraph_connectors.local_import", "inspect_local_content", None),
106
+ "LeMatBulk": (
107
+ "mattergraph_connectors.lematerial",
108
+ "LeMatBulk",
109
+ None,
110
+ ),
111
+ "OQMDStubConnector": (
112
+ "mattergraph_connectors.oqmd",
113
+ "OQMDStubConnector",
114
+ None,
115
+ ),
116
+ "NOMADConnector": (
117
+ "mattergraph_connectors.nomad",
118
+ "NOMADConnector",
119
+ None,
120
+ ),
121
+ "NOMADConnectorError": (
122
+ "mattergraph_connectors.nomad",
123
+ "NOMADConnectorError",
124
+ None,
125
+ ),
126
+ "NOMADHTTPError": (
127
+ "mattergraph_connectors.nomad",
128
+ "NOMADHTTPError",
129
+ None,
130
+ ),
131
+ "NOMADMappingError": (
132
+ "mattergraph_connectors.nomad",
133
+ "NOMADMappingError",
134
+ None,
135
+ ),
136
+ "NOMADPayloadError": (
137
+ "mattergraph_connectors.nomad",
138
+ "NOMADPayloadError",
139
+ None,
140
+ ),
141
+ "NOMADStubConnector": (
142
+ "mattergraph_connectors.nomad",
143
+ "NOMADStubConnector",
144
+ None,
145
+ ),
146
+ "OptimadeConnector": ("mattergraph_connectors.optimade", "OptimadeConnector", None),
147
+ "OptimadeConnectorError": (
148
+ "mattergraph_connectors.optimade",
149
+ "OptimadeConnectorError",
150
+ None,
151
+ ),
152
+ "OptimadeHTTPError": ("mattergraph_connectors.optimade", "OptimadeHTTPError", None),
153
+ "OptimadeMappingError": ("mattergraph_connectors.optimade", "OptimadeMappingError", None),
154
+ "OptimadePayloadError": ("mattergraph_connectors.optimade", "OptimadePayloadError", None),
155
+ "PROVIDERS": ("mattergraph_connectors.optimade", "PROVIDERS", None),
156
+ }
157
+
158
+ __all__ = [
159
+ "Connector",
160
+ "ConnectorQuery",
161
+ "apply_property_filter",
162
+ "coerce_query",
163
+ "connector_provenance",
164
+ "MaterialsProjectConnector",
165
+ "JarvisConnector",
166
+ "LeMatBulk",
167
+ "load_materials_from_csv",
168
+ "ConnectorHTTPPolicy",
169
+ "MemoryResponseCache",
170
+ "DatasetImportMapping",
171
+ "ImportIssue",
172
+ "ImportLimitError",
173
+ "ImportReport",
174
+ "ImportResult",
175
+ "ImportValidationError",
176
+ "NormalizedImport",
177
+ "PropertyColumnMapping",
178
+ "import_local_content",
179
+ "inspect_local_content",
180
+ "OQMDStubConnector",
181
+ "NOMADConnector",
182
+ "NOMADConnectorError",
183
+ "NOMADHTTPError",
184
+ "NOMADMappingError",
185
+ "NOMADPayloadError",
186
+ "NOMADStubConnector",
187
+ "OptimadeConnector",
188
+ "OptimadeConnectorError",
189
+ "OptimadeHTTPError",
190
+ "OptimadeMappingError",
191
+ "OptimadePayloadError",
192
+ "PROVIDERS",
193
+ ]
194
+
195
+
196
+ def __getattr__(name: str) -> Any:
197
+ if name not in _EXPORTS:
198
+ msg = f"module {__name__!r} has no attribute {name!r}"
199
+ raise AttributeError(msg)
200
+ module_name, attr_name, hint = _EXPORTS[name]
201
+ try:
202
+ module = import_module(module_name)
203
+ except ImportError as e:
204
+ if hint is None:
205
+ raise
206
+ raise ImportError(hint) from e
207
+ value = getattr(module, attr_name)
208
+ globals()[name] = value
209
+ return value
210
+
211
+
212
+ def __dir__() -> list[str]:
213
+ return sorted(set(globals()) | set(__all__))
@@ -0,0 +1,183 @@
1
+ """The contract every MatterGraph connector implements.
2
+
3
+ Before this module the connector "interface" was a convention: a class with a ``fetch``
4
+ returning ``list[Material]``. Nothing enforced it, and the signatures had already drifted
5
+ four ways — Materials Project took ``material_ids``/``properties``/``num_chunks``/``chunk_size``,
6
+ JARVIS took ``max_records``, NOMAD took ``max_records``/``page_size``, and the OQMD stub took
7
+ ``*args, **kwargs`` and ignored all of them. :class:`ConnectorQuery` gives those one shape and
8
+ :class:`Connector` makes conformance testable.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import warnings
14
+ from typing import Any, Protocol, runtime_checkable
15
+
16
+ from mattergraph.normalization.properties import canonical_property_name
17
+ from mattergraph.schema.material import Material
18
+ from mattergraph.schema.property import PropertyMethod
19
+ from mattergraph.schema.provenance import ProvenanceRecord
20
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
21
+
22
+ # Legacy keyword names accepted by the pre-contract fetch() signatures, mapped onto their
23
+ # ConnectorQuery field. Callers using these get a DeprecationWarning, not a break.
24
+ _LEGACY_ALIASES: dict[str, str] = {
25
+ "material_ids": "source_ids",
26
+ "chunk_size": "page_size",
27
+ }
28
+
29
+
30
+ class ConnectorQuery(BaseModel):
31
+ """A source-independent description of what to fetch.
32
+
33
+ Every field is a *request*, not a guarantee: a connector that cannot honor one must say so
34
+ by raising, never by silently ignoring it.
35
+ """
36
+
37
+ model_config = ConfigDict(extra="forbid", validate_assignment=True)
38
+
39
+ elements: list[str] | None = Field(
40
+ default=None,
41
+ description="Restrict to records containing these element symbols",
42
+ )
43
+ source_ids: list[str] | None = Field(
44
+ default=None,
45
+ description="Fetch these upstream identifiers directly, e.g. mp-149 or JVASP-1002",
46
+ )
47
+ properties: list[str] | None = Field(
48
+ default=None,
49
+ description="Canonical property names to return; connectors that cannot filter must raise",
50
+ )
51
+ max_records: int = Field(default=50, description="Upper bound on returned materials")
52
+ page_size: int = Field(default=25, description="Records per upstream request where paged")
53
+
54
+ @field_validator("elements", "source_ids", "properties")
55
+ @classmethod
56
+ def _clean_list(cls, value: list[str] | None) -> list[str] | None:
57
+ if value is None:
58
+ return None
59
+ cleaned = [item.strip() for item in value if str(item).strip()]
60
+ return cleaned or None
61
+
62
+ @field_validator("max_records", "page_size")
63
+ @classmethod
64
+ def _positive(cls, value: int, info: Any) -> int:
65
+ if value < 1:
66
+ msg = f"{info.field_name} must be at least 1"
67
+ raise ValueError(msg)
68
+ return value
69
+
70
+
71
+ @runtime_checkable
72
+ class Connector(Protocol):
73
+ """A source of :class:`Material` records.
74
+
75
+ ``isinstance(obj, Connector)`` checks that the members exist, not that their signatures
76
+ match — see ``tests/test_connector_contract.py``, which also exercises the call itself.
77
+ """
78
+
79
+ source_name: str
80
+
81
+ def fetch(self, query: ConnectorQuery | None = None, **legacy: Any) -> list[Material]:
82
+ """Return at most ``query.max_records`` materials matching ``query``."""
83
+ ...
84
+
85
+
86
+ def coerce_query(
87
+ query: ConnectorQuery | None,
88
+ legacy: dict[str, Any],
89
+ *,
90
+ source_name: str,
91
+ ) -> ConnectorQuery:
92
+ """Accept either a :class:`ConnectorQuery` or the pre-contract keyword form.
93
+
94
+ The keyword form is kept working because ``scripts/ingest_*.py`` and ``examples/`` use it;
95
+ it warns rather than breaking. Passing both forms at once is a caller bug and raises.
96
+ """
97
+ if query is not None and legacy:
98
+ msg = (
99
+ f"{source_name}.fetch() got both a ConnectorQuery and keyword arguments "
100
+ f"({', '.join(sorted(legacy))}); pass one or the other"
101
+ )
102
+ raise TypeError(msg)
103
+ if query is not None:
104
+ return query
105
+ if not legacy:
106
+ return ConnectorQuery()
107
+
108
+ warnings.warn(
109
+ f"Calling {source_name}.fetch() with keyword arguments is deprecated; "
110
+ f"pass a ConnectorQuery instead, e.g. fetch(ConnectorQuery(elements=['Ti'])).",
111
+ DeprecationWarning,
112
+ stacklevel=3,
113
+ )
114
+ mapped: dict[str, Any] = {}
115
+ for key, value in legacy.items():
116
+ field = _LEGACY_ALIASES.get(key, key)
117
+ if field not in ConnectorQuery.model_fields:
118
+ msg = f"{source_name}.fetch() got an unexpected keyword argument {key!r}"
119
+ raise TypeError(msg)
120
+ if value is not None:
121
+ mapped[field] = value
122
+ return ConnectorQuery(**mapped)
123
+
124
+
125
+ def apply_property_filter(
126
+ materials: list[Material],
127
+ query: ConnectorQuery,
128
+ *,
129
+ supported: frozenset[str],
130
+ source_name: str,
131
+ ) -> list[Material]:
132
+ """Restrict each material to the requested properties, or raise if it cannot be done.
133
+
134
+ A connector that accepts ``properties`` and returns everything anyway is the failure this
135
+ guards against: the caller has no way to tell that the filter did nothing.
136
+ """
137
+ if not query.properties:
138
+ return materials
139
+
140
+ requested = {canonical_property_name(name) for name in query.properties}
141
+ unsupported = sorted(requested - supported)
142
+ if unsupported:
143
+ msg = (
144
+ f"{source_name} cannot return {', '.join(unsupported)}; "
145
+ f"supported properties are {', '.join(sorted(supported))}"
146
+ )
147
+ raise ValueError(msg)
148
+
149
+ for material in materials:
150
+ material.properties = [p for p in material.properties if p.name in requested]
151
+ return materials
152
+
153
+
154
+ def connector_provenance(
155
+ source: str,
156
+ *,
157
+ source_id: str | None = None,
158
+ method: PropertyMethod = PropertyMethod.DFT,
159
+ notes: str | None = None,
160
+ parameters: dict[str, Any] | None = None,
161
+ ) -> ProvenanceRecord:
162
+ """Build the lineage record every ingested material carries.
163
+
164
+ ``constraint.provenance`` asks that a value say where it came from. Connectors previously
165
+ put lineage in ``Material.metadata``, which is untyped and invisible to anything that
166
+ reasons about provenance, leaving ``Material.provenance`` empty on every ingested record.
167
+ """
168
+ return ProvenanceRecord(
169
+ source=source,
170
+ method=method,
171
+ source_id=source_id,
172
+ notes=notes,
173
+ parameters=parameters,
174
+ )
175
+
176
+
177
+ __all__ = [
178
+ "Connector",
179
+ "ConnectorQuery",
180
+ "apply_property_filter",
181
+ "coerce_query",
182
+ "connector_provenance",
183
+ ]