fd-open-data-protocol 0.1.0__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.
- fd_open_data_protocol/__init__.py +8 -0
- fd_open_data_protocol/loader.py +69 -0
- fd_open_data_protocol/provider.py +49 -0
- fd_open_data_protocol/schema.py +67 -0
- fd_open_data_protocol-0.1.0.dist-info/METADATA +91 -0
- fd_open_data_protocol-0.1.0.dist-info/RECORD +8 -0
- fd_open_data_protocol-0.1.0.dist-info/WHEEL +5 -0
- fd_open_data_protocol-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""fd-open-data-protocol: the open-data datasource protocol.
|
|
2
|
+
|
|
3
|
+
A manifest contract a datasource exposes (datasource + functions + columns +
|
|
4
|
+
concept hints + fetch reference) so that fd-open-data-mcp - or any consumer -
|
|
5
|
+
can ingest it via ``register_datasource``. Ship one manifest file -> the
|
|
6
|
+
datasource is added; no consumer-side wiring.
|
|
7
|
+
"""
|
|
8
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Catalog loader: parse a manifest from YAML, JSON, a Python file/module, or a dict.
|
|
2
|
+
|
|
3
|
+
``load_catalog(source)`` returns a validated ``DatasourceManifest``. This is
|
|
4
|
+
the consumer entry point: a consumer (fd-open-data-mcp's ``register_datasource``,
|
|
5
|
+
or any other) calls ``load_catalog`` then ingests.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import importlib
|
|
10
|
+
import importlib.util
|
|
11
|
+
import json
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Union
|
|
14
|
+
|
|
15
|
+
import yaml
|
|
16
|
+
|
|
17
|
+
from fd_open_data_protocol.schema import DatasourceManifest
|
|
18
|
+
|
|
19
|
+
ManifestSource = Union[str, Path, dict, DatasourceManifest]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _manifest_from_catalog(catalog: Any) -> DatasourceManifest:
|
|
23
|
+
if isinstance(catalog, DatasourceManifest):
|
|
24
|
+
return catalog
|
|
25
|
+
if isinstance(catalog, dict):
|
|
26
|
+
return DatasourceManifest(**catalog)
|
|
27
|
+
raise ValueError(f"CATALOG is {type(catalog).__name__}, expected dict or DatasourceManifest")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def load_catalog(source: ManifestSource) -> DatasourceManifest:
|
|
31
|
+
"""Load + validate a ``DatasourceManifest`` from a path, module, dict, or manifest."""
|
|
32
|
+
if isinstance(source, DatasourceManifest):
|
|
33
|
+
return source
|
|
34
|
+
if isinstance(source, dict):
|
|
35
|
+
return DatasourceManifest(**source)
|
|
36
|
+
if isinstance(source, (str, Path)):
|
|
37
|
+
s = str(source)
|
|
38
|
+
p = Path(s)
|
|
39
|
+
if p.is_file():
|
|
40
|
+
suffix = p.suffix.lower()
|
|
41
|
+
if suffix in (".yaml", ".yml"):
|
|
42
|
+
data = yaml.safe_load(p.read_text())
|
|
43
|
+
if not isinstance(data, dict):
|
|
44
|
+
raise ValueError(f"YAML manifest did not parse to a dict: {s}")
|
|
45
|
+
return DatasourceManifest(**data)
|
|
46
|
+
if suffix == ".json":
|
|
47
|
+
return DatasourceManifest(**json.loads(p.read_text()))
|
|
48
|
+
if suffix == ".py":
|
|
49
|
+
# exec the file, read CATALOG
|
|
50
|
+
spec = importlib.util.spec_from_file_location("_fd_odp_manifest", p)
|
|
51
|
+
if spec is None or spec.loader is None:
|
|
52
|
+
raise ValueError(f"cannot create module spec for {s}")
|
|
53
|
+
mod = importlib.util.module_from_spec(spec)
|
|
54
|
+
spec.loader.exec_module(mod)
|
|
55
|
+
catalog = getattr(mod, "CATALOG", None)
|
|
56
|
+
if catalog is None:
|
|
57
|
+
raise ValueError(f"{s} has no CATALOG attribute")
|
|
58
|
+
return _manifest_from_catalog(catalog)
|
|
59
|
+
raise ValueError(f"unsupported manifest file type: {suffix} ({s})")
|
|
60
|
+
# treat as a Python module path "pkg.mod" exposing CATALOG
|
|
61
|
+
try:
|
|
62
|
+
mod = importlib.import_module(s)
|
|
63
|
+
except ImportError as e:
|
|
64
|
+
raise ValueError(f"neither a file nor an importable module: {s}") from e
|
|
65
|
+
catalog = getattr(mod, "CATALOG", None)
|
|
66
|
+
if catalog is None:
|
|
67
|
+
raise ValueError(f"module {s} has no CATALOG attribute")
|
|
68
|
+
return _manifest_from_catalog(catalog)
|
|
69
|
+
raise TypeError(f"unsupported source type: {type(source).__name__}")
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""The DataProvider interface (optional companion to a manifest).
|
|
2
|
+
|
|
3
|
+
A manifest is the *declarative* catalog. A datasource that also ships fetch
|
|
4
|
+
code implements ``DataProvider`` (or subclasses ``BaseDataProvider``) so the
|
|
5
|
+
consumer can call ``run()``. The manifest's ``fetch`` field references the
|
|
6
|
+
runner (a built-in name or a module path); the class is for providers that
|
|
7
|
+
prefer to bundle catalog + code.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any, Optional
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DataProvider:
|
|
15
|
+
"""Structural interface (duck-typed). A provider has ``name``,
|
|
16
|
+
``registry()``, and ``run(command, params)``."""
|
|
17
|
+
|
|
18
|
+
name: str
|
|
19
|
+
|
|
20
|
+
def registry(self) -> dict:
|
|
21
|
+
raise NotImplementedError
|
|
22
|
+
|
|
23
|
+
def run(self, command: str, params: dict) -> Any:
|
|
24
|
+
raise NotImplementedError
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class BaseDataProvider:
|
|
28
|
+
"""Base class with empty defaults for optional facets. Subclass and
|
|
29
|
+
override ``registry`` + ``run``; the rest are no-ops by default."""
|
|
30
|
+
|
|
31
|
+
name: str = ""
|
|
32
|
+
|
|
33
|
+
def registry(self) -> dict:
|
|
34
|
+
raise NotImplementedError
|
|
35
|
+
|
|
36
|
+
def run(self, command: str, params: dict) -> Any:
|
|
37
|
+
raise NotImplementedError
|
|
38
|
+
|
|
39
|
+
def introspect(self) -> list[dict]:
|
|
40
|
+
return []
|
|
41
|
+
|
|
42
|
+
def seed_entities(self, session: Any) -> dict:
|
|
43
|
+
return {}
|
|
44
|
+
|
|
45
|
+
def concept_rules(self) -> list:
|
|
46
|
+
return []
|
|
47
|
+
|
|
48
|
+
def build_params(self, fn: Any, identifier: str, date: str, binding: Any = None) -> dict:
|
|
49
|
+
return {}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Pydantic schema for the datasource manifest (the protocol contract).
|
|
2
|
+
|
|
3
|
+
A manifest declares a datasource's identity, functions, columns, concept
|
|
4
|
+
hints, and fetch reference. Column-level ``frequency``/``datasource`` come from
|
|
5
|
+
``enrich-concept-identity``; ``measure``/``entity_type`` are concept-level
|
|
6
|
+
(via ``ConceptHint``).
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any, Optional
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel, Field
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ParamSpec(BaseModel):
|
|
16
|
+
name: str
|
|
17
|
+
type: Optional[str] = None
|
|
18
|
+
required: bool = False
|
|
19
|
+
description: Optional[str] = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ColumnSpec(BaseModel):
|
|
23
|
+
name: str
|
|
24
|
+
type: Optional[str] = None
|
|
25
|
+
description: Optional[str] = None
|
|
26
|
+
meaning: Optional[str] = None
|
|
27
|
+
semantic_type: Optional[str] = None
|
|
28
|
+
frequency: Optional[str] = None # column-level cadence (defaults to the function's)
|
|
29
|
+
datasource: Optional[str] = None # column-level source (defaults to the manifest's)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class FunctionSpec(BaseModel):
|
|
33
|
+
command: str
|
|
34
|
+
category: Optional[str] = None
|
|
35
|
+
description: Optional[str] = None
|
|
36
|
+
parameters: list[ParamSpec] = Field(default_factory=list)
|
|
37
|
+
columns: list[ColumnSpec] = Field(default_factory=list)
|
|
38
|
+
frequency: Optional[str] = None
|
|
39
|
+
verified: bool = True
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ConceptHint(BaseModel):
|
|
43
|
+
column: str
|
|
44
|
+
concept: str
|
|
45
|
+
entity_type: str
|
|
46
|
+
measure: Optional[str] = None
|
|
47
|
+
unit: Optional[str] = None
|
|
48
|
+
frequency: Optional[str] = None
|
|
49
|
+
confidence: float = 0.9
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class FetchRef(BaseModel):
|
|
53
|
+
runner: Optional[str] = None # built-in runner name
|
|
54
|
+
module: Optional[str] = None # "pkg.mod:func" import path
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class DatasourceManifest(BaseModel):
|
|
58
|
+
version: str = "1"
|
|
59
|
+
name: str
|
|
60
|
+
label: str
|
|
61
|
+
source_url: Optional[str] = None
|
|
62
|
+
scanner_mode: str = "upstream-curated"
|
|
63
|
+
requires: list[str] = Field(default_factory=list)
|
|
64
|
+
ranking_seed: list[float] = Field(default_factory=lambda: [0.5, 0.5])
|
|
65
|
+
functions: list[FunctionSpec]
|
|
66
|
+
concepts: list[ConceptHint] = Field(default_factory=list)
|
|
67
|
+
fetch: Optional[FetchRef] = None
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fd-open-data-protocol
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: The open-data datasource protocol: a manifest contract a datasource exposes to be ingested by fd-open-data-mcp (or any consumer).
|
|
5
|
+
Author: FindDataOfficial
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: pydantic>=2.0
|
|
10
|
+
Requires-Dist: pyyaml>=6.0
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
13
|
+
|
|
14
|
+
# fd-open-data-protocol
|
|
15
|
+
|
|
16
|
+
The **open-data datasource protocol**: a manifest contract a datasource exposes
|
|
17
|
+
(datasource + functions + columns + concept hints + fetch reference) so that
|
|
18
|
+
`fd-open-data-mcp` - or any consumer - can ingest it via `register_datasource`.
|
|
19
|
+
|
|
20
|
+
**Ship one manifest file -> the datasource is added. No consumer-side wiring.**
|
|
21
|
+
|
|
22
|
+
## The manifest
|
|
23
|
+
|
|
24
|
+
A YAML/JSON file (or a Python module exposing `CATALOG`):
|
|
25
|
+
|
|
26
|
+
```yaml
|
|
27
|
+
version: "1"
|
|
28
|
+
name: my-source
|
|
29
|
+
label: My Source
|
|
30
|
+
ranking_seed: [0.7, 0.7] # [quality, accessibility] heuristic seed
|
|
31
|
+
functions:
|
|
32
|
+
- command: get_data
|
|
33
|
+
frequency: daily
|
|
34
|
+
parameters: [{name: symbol, type: str, required: true}]
|
|
35
|
+
columns:
|
|
36
|
+
- {name: close, type: float, frequency: daily}
|
|
37
|
+
concepts: # column -> concept hints (measure/entity_type here)
|
|
38
|
+
- {column: close, concept: price.close, entity_type: stock, unit: currency, frequency: daily}
|
|
39
|
+
fetch:
|
|
40
|
+
runner: my-source # built-in runner name, OR module: "pkg.mod:run"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
See `examples/example_stock.yaml` (declarative) and `examples/example_macro.py`
|
|
44
|
+
(a `DataProvider` class with `run()`).
|
|
45
|
+
|
|
46
|
+
## Load + validate
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from fd_open_data_protocol.loader import load_catalog
|
|
50
|
+
manifest = load_catalog("examples/example_stock.yaml")
|
|
51
|
+
print(manifest.name, len(manifest.functions))
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`load_catalog` accepts a YAML/JSON file path, a `.py` file exposing `CATALOG`,
|
|
55
|
+
a `"pkg.mod"` module path, or a dict.
|
|
56
|
+
|
|
57
|
+
## Register with fd-open-data-mcp
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
fd-open-data-mcp register-datasource examples/example_stock.yaml
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
or the MCP tool `register_datasource(path)`.
|
|
64
|
+
|
|
65
|
+
## Publish a datasource from another project
|
|
66
|
+
|
|
67
|
+
In your datasource package's `pyproject.toml`:
|
|
68
|
+
|
|
69
|
+
```toml
|
|
70
|
+
[project.entry-points."fd_open_data_mcp.datasources"]
|
|
71
|
+
my-source = "my_pkg.catalog:CATALOG"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`pip install my-pkg` -> fd-open-data-mcp auto-registers it on `import_catalog`.
|
|
75
|
+
|
|
76
|
+
## Schema
|
|
77
|
+
|
|
78
|
+
- **`DatasourceManifest`**: name, label, source_url, scanner_mode, ranking_seed, functions[], concepts[], fetch.
|
|
79
|
+
- **`FunctionSpec`**: command, category, description, parameters[], columns[], frequency, verified.
|
|
80
|
+
- **`ColumnSpec`**: name, type, description, meaning, semantic_type, `frequency` + `datasource` (column-level).
|
|
81
|
+
- **`ConceptHint`**: column, concept, `entity_type`, `measure`, unit, frequency, confidence.
|
|
82
|
+
- **`FetchRef`**: runner (built-in name) | module (`"pkg.mod:func"`).
|
|
83
|
+
|
|
84
|
+
`measure` + `entity_type` are **concept-level** (disambiguate GDP-nominal vs
|
|
85
|
+
GDP-PPP; stock close vs fund NAV). Column-level `frequency`/`datasource` support
|
|
86
|
+
composite functions whose columns come from different sources at different cadences.
|
|
87
|
+
|
|
88
|
+
## Template
|
|
89
|
+
|
|
90
|
+
Copy `template/datasource.template.yaml` (declarative) or
|
|
91
|
+
`template/provider_template.py` (a `BaseDataProvider` class with `run()`).
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
fd_open_data_protocol/__init__.py,sha256=lj15qAY5NVW7-1dfhfHRwMkNUTS0KFsthJmjsK9kz7c,362
|
|
2
|
+
fd_open_data_protocol/loader.py,sha256=oBzhyNVLriyJoe_oGIOdv-tQatXpeNm-nG-g2WJfkmY,2928
|
|
3
|
+
fd_open_data_protocol/provider.py,sha256=3BGCknL2-FYmpauGXssC_zq9-j3lLznl0SmJY29JmNE,1433
|
|
4
|
+
fd_open_data_protocol/schema.py,sha256=FADg28wE9aozL1AVXDjCHCwajSmEievIrK9y-y2jTKM,2048
|
|
5
|
+
fd_open_data_protocol-0.1.0.dist-info/METADATA,sha256=iGfuX45q5Uydr3ob0rw5eIyq30RM0xNx-A8S5Vyj9ls,3197
|
|
6
|
+
fd_open_data_protocol-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
7
|
+
fd_open_data_protocol-0.1.0.dist-info/top_level.txt,sha256=FYtTIWYkMLzYyUoYKbxJQQU2yozvrWTntfDZ5jKmjvQ,22
|
|
8
|
+
fd_open_data_protocol-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fd_open_data_protocol
|