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.
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ fd_open_data_protocol