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.
- mattergraph_connectors-0.1.0/.gitignore +48 -0
- mattergraph_connectors-0.1.0/PKG-INFO +83 -0
- mattergraph_connectors-0.1.0/README.md +48 -0
- mattergraph_connectors-0.1.0/mattergraph_connectors/__init__.py +213 -0
- mattergraph_connectors-0.1.0/mattergraph_connectors/base.py +183 -0
- mattergraph_connectors-0.1.0/mattergraph_connectors/http_policy.py +160 -0
- mattergraph_connectors-0.1.0/mattergraph_connectors/jarvis.py +170 -0
- mattergraph_connectors-0.1.0/mattergraph_connectors/lematerial.py +240 -0
- mattergraph_connectors-0.1.0/mattergraph_connectors/local_csv.py +71 -0
- mattergraph_connectors-0.1.0/mattergraph_connectors/local_import.py +605 -0
- mattergraph_connectors-0.1.0/mattergraph_connectors/materials_project.py +212 -0
- mattergraph_connectors-0.1.0/mattergraph_connectors/nomad.py +294 -0
- mattergraph_connectors-0.1.0/mattergraph_connectors/optimade.py +571 -0
- mattergraph_connectors-0.1.0/mattergraph_connectors/oqmd.py +33 -0
- mattergraph_connectors-0.1.0/mattergraph_connectors/py.typed +0 -0
- mattergraph_connectors-0.1.0/mattergraph_connectors/schema_generation.py +33 -0
- mattergraph_connectors-0.1.0/pyproject.toml +56 -0
- mattergraph_connectors-0.1.0/tests/test_connector_contract.py +268 -0
- mattergraph_connectors-0.1.0/tests/test_connectors_optional_imports.py +59 -0
- mattergraph_connectors-0.1.0/tests/test_http_policy.py +50 -0
- mattergraph_connectors-0.1.0/tests/test_jarvis.py +156 -0
- mattergraph_connectors-0.1.0/tests/test_lematerial.py +122 -0
- mattergraph_connectors-0.1.0/tests/test_lematerial_examples.py +24 -0
- mattergraph_connectors-0.1.0/tests/test_local_csv.py +12 -0
- mattergraph_connectors-0.1.0/tests/test_local_import.py +90 -0
- mattergraph_connectors-0.1.0/tests/test_materials_project.py +193 -0
- mattergraph_connectors-0.1.0/tests/test_nomad.py +207 -0
- 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
|
+
]
|