gildea 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.
- gildea-0.1.0.dist-info/METADATA +130 -0
- gildea-0.1.0.dist-info/RECORD +17 -0
- gildea-0.1.0.dist-info/WHEEL +4 -0
- gildea-0.1.0.dist-info/entry_points.txt +2 -0
- gildea_sdk/__init__.py +21 -0
- gildea_sdk/client.py +49 -0
- gildea_sdk/exceptions.py +34 -0
- gildea_sdk/mcp/__init__.py +0 -0
- gildea_sdk/mcp/__main__.py +5 -0
- gildea_sdk/mcp/server.py +328 -0
- gildea_sdk/mcp/tools.py +164 -0
- gildea_sdk/pagination.py +77 -0
- gildea_sdk/resources/__init__.py +6 -0
- gildea_sdk/resources/entities.py +50 -0
- gildea_sdk/resources/search.py +44 -0
- gildea_sdk/resources/signals.py +47 -0
- gildea_sdk/resources/themes.py +18 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gildea
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client and MCP server for the Gildea AI market intelligence API
|
|
5
|
+
Project-URL: Homepage, https://gildea.ai
|
|
6
|
+
Project-URL: Documentation, https://docs.gildea.ai
|
|
7
|
+
Project-URL: Repository, https://github.com/hjones20/gildea-api
|
|
8
|
+
Project-URL: Issues, https://github.com/hjones20/gildea-api/issues
|
|
9
|
+
Author: Holly Jones
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
Keywords: ai,api-client,competitive-intelligence,market-intelligence,mcp
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Requires-Dist: httpx>=0.27
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: mcp[server]>=1.3; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest-httpx>=0.35; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
28
|
+
Requires-Dist: ruff>=0.15; extra == 'dev'
|
|
29
|
+
Provides-Extra: mcp
|
|
30
|
+
Requires-Dist: mcp[server]>=1.3; extra == 'mcp'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# Gildea
|
|
34
|
+
|
|
35
|
+
Python client and MCP server for the [Gildea](https://gildea.ai) AI market intelligence API.
|
|
36
|
+
|
|
37
|
+
Gildea tracks 500+ expert sources on AI, decomposes every signal into verified reasoning chains (thesis, arguments, claims, evidence), and serves it through a REST API. This package gives you a Python client and an MCP server so AI assistants can use the data directly.
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# Python client only
|
|
43
|
+
pip install gildea
|
|
44
|
+
|
|
45
|
+
# With MCP server
|
|
46
|
+
pip install gildea[mcp]
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Quick Start
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from gildea_sdk import Gildea
|
|
53
|
+
|
|
54
|
+
client = Gildea(api_key="gld_your_key_here")
|
|
55
|
+
|
|
56
|
+
# Search verified text units
|
|
57
|
+
results = client.search.query(q="data center infrastructure spending")
|
|
58
|
+
for hit in results.data:
|
|
59
|
+
print(f"{hit.unit.text}")
|
|
60
|
+
print(f" Source: {hit.citation.signal_title} ({hit.citation.registrable_domain})")
|
|
61
|
+
|
|
62
|
+
# Get full signal decomposition with evidence
|
|
63
|
+
signal = client.signals.get("signal_id", include="evidence")
|
|
64
|
+
|
|
65
|
+
# Entity intelligence with trend analytics
|
|
66
|
+
entity = client.entities.get("NVIDIA")
|
|
67
|
+
print(f"{entity.display_name}: {entity.direction}, {entity.scale} scale, {entity.priority} priority")
|
|
68
|
+
|
|
69
|
+
# Cross-source consensus mapping
|
|
70
|
+
similar = client.search.query(similar_to="unit_id")
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## MCP Server
|
|
74
|
+
|
|
75
|
+
Use Gildea as a tool in Claude, ChatGPT, Cursor, VS Code, or any MCP-compatible client.
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# Run directly
|
|
79
|
+
gildea-mcp
|
|
80
|
+
|
|
81
|
+
# Or via uvx (no install needed)
|
|
82
|
+
uvx --from gildea[mcp] gildea-mcp
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Set your API key:
|
|
86
|
+
```bash
|
|
87
|
+
export GILDEA_API_KEY=gld_your_key_here
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Claude Desktop
|
|
91
|
+
|
|
92
|
+
Add to your `claude_desktop_config.json`:
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"mcpServers": {
|
|
97
|
+
"gildea": {
|
|
98
|
+
"command": "uvx",
|
|
99
|
+
"args": ["--from", "gildea[mcp]", "gildea-mcp"],
|
|
100
|
+
"env": {
|
|
101
|
+
"GILDEA_API_KEY": "gld_your_key_here"
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Available MCP Tools
|
|
109
|
+
|
|
110
|
+
| Tool | Description |
|
|
111
|
+
|------|-------------|
|
|
112
|
+
| `search_text_units` | Semantic search across verified text units, or vector similarity via `similar_to` |
|
|
113
|
+
| `list_signals` | Browse signals by entity, theme, date, content type |
|
|
114
|
+
| `get_signal_detail` | Full decomposition: thesis, arguments, claims, evidence |
|
|
115
|
+
| `get_entity_profile` | Entity trend analytics, co-occurrence, theme distribution |
|
|
116
|
+
| `list_entities` | Discover entities by trend direction, priority, scale |
|
|
117
|
+
| `get_themes` | Theme overview across value chain and market force axes |
|
|
118
|
+
| `get_theme_detail` | Single theme trend analytics and cross-theme relationships |
|
|
119
|
+
|
|
120
|
+
## API Key
|
|
121
|
+
|
|
122
|
+
Get your API key at [gildea.ai](https://gildea.ai). Free tier includes 5 requests/minute and 200 requests/month.
|
|
123
|
+
|
|
124
|
+
## Documentation
|
|
125
|
+
|
|
126
|
+
Full API docs at [docs.gildea.ai](https://docs.gildea.ai).
|
|
127
|
+
|
|
128
|
+
## License
|
|
129
|
+
|
|
130
|
+
MIT
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
gildea_sdk/__init__.py,sha256=53NUhdCq50QD1meHc_ge5HuLycGkGLM_nQLJ2EUzpAw,407
|
|
2
|
+
gildea_sdk/client.py,sha256=ZD8DkgFNP1qfOdRSU1qUoY-tKVWnhXCH2n_D5ccxNfI,1571
|
|
3
|
+
gildea_sdk/exceptions.py,sha256=NOMrpRwSEn1iBbavmmz0vKBSJl_B8Mv9lEXxaCx6-Qw,857
|
|
4
|
+
gildea_sdk/pagination.py,sha256=h3-swBnUhKm8MRr6cm7SEMJ6shkga14PYZbuETukqn0,2368
|
|
5
|
+
gildea_sdk/mcp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
gildea_sdk/mcp/__main__.py,sha256=WMQP7_MpK6gabqrvB1g5Is78a2rAmazjOC9DXG6BBgg,108
|
|
7
|
+
gildea_sdk/mcp/server.py,sha256=AWunr6kPXUWrJendrJ7EGuEYPx_wZ-utdstFxX5Q5Tc,16232
|
|
8
|
+
gildea_sdk/mcp/tools.py,sha256=7DyQWnoytNqCYJw0TBpP_J1UGDZHLUUGWk--CioWNfE,4447
|
|
9
|
+
gildea_sdk/resources/__init__.py,sha256=P27eKn1HiLXW_DC3LWPH915RE3fmncBMzHv7SC6vubw,233
|
|
10
|
+
gildea_sdk/resources/entities.py,sha256=2iigKaVY920ij9Ss8vWXBTds6ehN1IfvjXhMK7E3Igo,1353
|
|
11
|
+
gildea_sdk/resources/search.py,sha256=rpqlUMvE-9popFSoLjyUKpsjOPk6jHKHiAB1lkLhB_A,1254
|
|
12
|
+
gildea_sdk/resources/signals.py,sha256=bEbz3_xPnziolcFOmR83qzAxjD5R0Yee02x2X9GQ7So,1371
|
|
13
|
+
gildea_sdk/resources/themes.py,sha256=HWSmgfzBqdpltSbD4vqZTgAto0peUmg0D2l4MV3Nd8Y,489
|
|
14
|
+
gildea-0.1.0.dist-info/METADATA,sha256=Linn2AV_LnO8rLMMcgV_Yli26gPrZFC2jl9nBN-PSOg,3984
|
|
15
|
+
gildea-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
16
|
+
gildea-0.1.0.dist-info/entry_points.txt,sha256=QiEXWZnOXrTFA63upviecr_w_cOMfwzYUOjSWxgNaZw,58
|
|
17
|
+
gildea-0.1.0.dist-info/RECORD,,
|
gildea_sdk/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Gildea Python SDK — client for the Gildea AI market intelligence API."""
|
|
2
|
+
|
|
3
|
+
from .client import Gildea
|
|
4
|
+
from .exceptions import (
|
|
5
|
+
APIError,
|
|
6
|
+
AuthenticationError,
|
|
7
|
+
BadRequestError,
|
|
8
|
+
GildeaError,
|
|
9
|
+
NotFoundError,
|
|
10
|
+
RateLimitError,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"Gildea",
|
|
15
|
+
"GildeaError",
|
|
16
|
+
"AuthenticationError",
|
|
17
|
+
"NotFoundError",
|
|
18
|
+
"RateLimitError",
|
|
19
|
+
"BadRequestError",
|
|
20
|
+
"APIError",
|
|
21
|
+
]
|
gildea_sdk/client.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Main Gildea client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from .exceptions import AuthenticationError
|
|
10
|
+
from .resources import EntitiesResource, SearchResource, SignalsResource, ThemesResource
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Gildea:
|
|
14
|
+
"""Synchronous client for the Gildea AI market intelligence API."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, api_key=None, base_url="https://api.gildea.ai", timeout=30.0):
|
|
17
|
+
self.api_key = api_key or os.environ.get("GILDEA_API_KEY")
|
|
18
|
+
if not self.api_key:
|
|
19
|
+
raise AuthenticationError(
|
|
20
|
+
"No API key provided. Set GILDEA_API_KEY or pass api_key=."
|
|
21
|
+
)
|
|
22
|
+
self._client = httpx.Client(
|
|
23
|
+
base_url=base_url,
|
|
24
|
+
headers={"X-API-Key": self.api_key},
|
|
25
|
+
timeout=timeout,
|
|
26
|
+
)
|
|
27
|
+
self.signals = SignalsResource(self._client)
|
|
28
|
+
self.entities = EntitiesResource(self._client)
|
|
29
|
+
self.themes = ThemesResource(self._client)
|
|
30
|
+
self._search = SearchResource(self._client)
|
|
31
|
+
|
|
32
|
+
def search(self, query=None, *, similar_to=None, **kwargs):
|
|
33
|
+
"""Search across all verified text units.
|
|
34
|
+
|
|
35
|
+
Two modes: pass ``query`` for hybrid semantic + keyword search,
|
|
36
|
+
or ``similar_to`` with a text unit ID for pure vector similarity.
|
|
37
|
+
Exactly one is required.
|
|
38
|
+
"""
|
|
39
|
+
return self._search.query(query, similar_to=similar_to, **kwargs)
|
|
40
|
+
|
|
41
|
+
def close(self):
|
|
42
|
+
"""Close the underlying HTTP connection."""
|
|
43
|
+
self._client.close()
|
|
44
|
+
|
|
45
|
+
def __enter__(self):
|
|
46
|
+
return self
|
|
47
|
+
|
|
48
|
+
def __exit__(self, *args):
|
|
49
|
+
self.close()
|
gildea_sdk/exceptions.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Exception classes for the Gildea SDK."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class GildeaError(Exception):
|
|
5
|
+
"""Base exception for all Gildea SDK errors."""
|
|
6
|
+
|
|
7
|
+
def __init__(self, message, *, code=None, status=None):
|
|
8
|
+
super().__init__(message)
|
|
9
|
+
self.code = code
|
|
10
|
+
self.status = status
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AuthenticationError(GildeaError):
|
|
14
|
+
"""Raised on 401/403 responses or missing API key."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class NotFoundError(GildeaError):
|
|
18
|
+
"""Raised on 404 responses."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class RateLimitError(GildeaError):
|
|
22
|
+
"""Raised on 429 responses."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, message, *, code=None, status=None, retry_after=None):
|
|
25
|
+
super().__init__(message, code=code, status=status)
|
|
26
|
+
self.retry_after = retry_after
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class BadRequestError(GildeaError):
|
|
30
|
+
"""Raised on 400 responses."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class APIError(GildeaError):
|
|
34
|
+
"""Raised on 5xx or other unexpected responses."""
|
|
File without changes
|
gildea_sdk/mcp/server.py
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
"""MCP server for Gildea market intelligence data.
|
|
2
|
+
|
|
3
|
+
Modes:
|
|
4
|
+
stdio (default): python -m gildea_sdk.mcp
|
|
5
|
+
sse: python -m gildea_sdk.mcp --sse
|
|
6
|
+
sse (custom): python -m gildea_sdk.mcp --sse --port 8001
|
|
7
|
+
|
|
8
|
+
Configuration via environment variables:
|
|
9
|
+
GILDEA_API_KEY - Required. API key for authentication.
|
|
10
|
+
GILDEA_API_BASE_URL - Base URL for the REST API (default: https://api.gildea.ai)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import asyncio
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
|
|
20
|
+
import httpx
|
|
21
|
+
from mcp.server import Server
|
|
22
|
+
from mcp.server.stdio import stdio_server
|
|
23
|
+
from mcp.types import TextContent, Tool
|
|
24
|
+
|
|
25
|
+
from gildea_sdk.mcp import tools
|
|
26
|
+
|
|
27
|
+
server = Server("gildea")
|
|
28
|
+
_http_client: httpx.AsyncClient | None = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _json_result(data: dict) -> list[TextContent]:
|
|
32
|
+
return [TextContent(type="text", text=json.dumps(data, default=str, indent=2))]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _error_result(error: str) -> list[TextContent]:
|
|
36
|
+
return _json_result({"error": error})
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _ensure_client() -> httpx.AsyncClient:
|
|
40
|
+
global _http_client
|
|
41
|
+
if _http_client is None:
|
|
42
|
+
api_key = os.getenv("GILDEA_API_KEY", "")
|
|
43
|
+
base_url = os.getenv("GILDEA_API_BASE_URL", "https://api.gildea.ai")
|
|
44
|
+
if not api_key:
|
|
45
|
+
raise ValueError("GILDEA_API_KEY environment variable is required")
|
|
46
|
+
_http_client = httpx.AsyncClient(
|
|
47
|
+
base_url=base_url,
|
|
48
|
+
headers={"X-API-Key": api_key},
|
|
49
|
+
timeout=30.0,
|
|
50
|
+
)
|
|
51
|
+
return _http_client
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@server.list_tools()
|
|
55
|
+
async def list_tools() -> list[Tool]:
|
|
56
|
+
return [
|
|
57
|
+
Tool(
|
|
58
|
+
name="search_text_units",
|
|
59
|
+
description="Semantic search across all of Gildea's verified extractions — thesis sentences, arguments, summaries, and claims. Text units are Gildea's own analytical extractions from source material, independently verified for factual accuracy. They are not direct quotes. Two modes: q='search query' finds text units matching a natural language question; similar_to='unit_id' finds text units semantically similar to a known unit (best for consensus mapping and cross-source synthesis). Use cases: 'What do multiple sources say about X?' → q with broad query. 'Who else is saying something like this claim?' → similar_to with unit ID. Returns sentence-level results with source attribution. Set recency_boost (0-1) to favor newer signals. Set verification_detail=full for complete verification metadata.",
|
|
60
|
+
inputSchema={
|
|
61
|
+
"type": "object",
|
|
62
|
+
"properties": {
|
|
63
|
+
"q": {"type": "string", "description": "Search query text. Required unless similar_to is provided."},
|
|
64
|
+
"similar_to": {"type": "string", "description": "Text unit ID to find similar units for. Uses the unit's stored embedding for pure vector similarity search. Required unless q is provided."},
|
|
65
|
+
"unit_type": {
|
|
66
|
+
"type": "string",
|
|
67
|
+
"enum": [
|
|
68
|
+
"thesis_sentence",
|
|
69
|
+
"argument_sentence",
|
|
70
|
+
"summary_sentence",
|
|
71
|
+
"analysis_claim",
|
|
72
|
+
"event_claim",
|
|
73
|
+
],
|
|
74
|
+
"description": "Filter by text unit type",
|
|
75
|
+
},
|
|
76
|
+
"entity": {"type": "string", "description": "Filter by entity ID"},
|
|
77
|
+
"theme": {"type": "string", "description": "Filter by theme label"},
|
|
78
|
+
"published_after": {"type": "string", "description": "ISO 8601 date — only results published after this date"},
|
|
79
|
+
"published_before": {"type": "string", "description": "ISO 8601 date — only results published before this date"},
|
|
80
|
+
"recency_boost": {
|
|
81
|
+
"type": "number",
|
|
82
|
+
"description": "Recency weight (0=none, 1=max). Boosts newer signals in ranking.",
|
|
83
|
+
"default": 0.0,
|
|
84
|
+
"minimum": 0,
|
|
85
|
+
"maximum": 1,
|
|
86
|
+
},
|
|
87
|
+
"verification_detail": {
|
|
88
|
+
"type": "string",
|
|
89
|
+
"enum": ["full"],
|
|
90
|
+
"description": "Include full verification metadata",
|
|
91
|
+
},
|
|
92
|
+
"limit": {"type": "integer", "default": 10, "maximum": 25},
|
|
93
|
+
},
|
|
94
|
+
"oneOf": [
|
|
95
|
+
{"required": ["q"]},
|
|
96
|
+
{"required": ["similar_to"]},
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
),
|
|
100
|
+
Tool(
|
|
101
|
+
name="list_signals",
|
|
102
|
+
description="Find intelligence signals about the AI economy from 500+ expert sources. Each signal is a structured analysis or event report that has been decomposed into verified components (thesis → arguments → claims for analysis; summary → claims for events). This endpoint returns signal metadata — use get_signal_detail to access the full verified decomposition tree. All filters are optional. Without filters, returns the most recent signals sorted by date. Use cases: 'What's happening with [company]?' → filter by entity. 'What's new in AI regulation?' → filter by theme. 'Anything about open source models?' → text query. 'What's new today?' → no filter, most recent. Each result includes title, source, date, tags (value chain + market force), mentioned entities, and count of verified text units.",
|
|
103
|
+
inputSchema={
|
|
104
|
+
"type": "object",
|
|
105
|
+
"properties": {
|
|
106
|
+
"entity": {"type": "string", "description": "Entity ID filter"},
|
|
107
|
+
"theme": {"type": "string", "description": "Theme label filter"},
|
|
108
|
+
"q": {"type": "string", "description": "Text search query"},
|
|
109
|
+
"content_type": {"type": "string", "enum": ["analysis", "event"]},
|
|
110
|
+
"published_after": {"type": "string", "description": "ISO date"},
|
|
111
|
+
"published_before": {"type": "string", "description": "ISO date"},
|
|
112
|
+
"limit": {"type": "integer", "default": 25, "maximum": 50},
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
),
|
|
116
|
+
Tool(
|
|
117
|
+
name="get_signal_detail",
|
|
118
|
+
description="Get the full verified reasoning chain for a signal. This is where Gildea's value lives. Analysis signals decompose into: thesis → supporting arguments → verified claims. Event signals decompose into: summary → verified claims. Each piece has been independently verified against source evidence. Use this tool liberally — it's the difference between showing a user metadata and showing them the actual verified intelligence. Set include=evidence to see the source snippets each claim was verified against. Set verification_detail=full for complete verification metadata.",
|
|
119
|
+
inputSchema={
|
|
120
|
+
"type": "object",
|
|
121
|
+
"properties": {
|
|
122
|
+
"signal_id": {"type": "string", "description": "Signal ID"},
|
|
123
|
+
"include": {
|
|
124
|
+
"type": "string",
|
|
125
|
+
"enum": ["evidence"],
|
|
126
|
+
"description": "Include evidence snippets",
|
|
127
|
+
},
|
|
128
|
+
"verification_detail": {
|
|
129
|
+
"type": "string",
|
|
130
|
+
"enum": ["full"],
|
|
131
|
+
"description": "Include full verification metadata",
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
"required": ["signal_id"],
|
|
135
|
+
},
|
|
136
|
+
),
|
|
137
|
+
Tool(
|
|
138
|
+
name="get_entity_profile",
|
|
139
|
+
description="Get an intelligence profile for a company, person, or product. Returns: whether the entity is rising, stable, or declining in expert coverage; how much attention it's getting relative to others (scale); whether the trend is statistically significant; and a priority assessment. Use this to understand an entity's trajectory before diving into specific signals. Accepts display name (e.g. 'NVIDIA') or entity ID. Note: entities with fewer than 8 mentions in the 12-week window return limited trend data.",
|
|
140
|
+
inputSchema={
|
|
141
|
+
"type": "object",
|
|
142
|
+
"properties": {
|
|
143
|
+
"name_or_id": {
|
|
144
|
+
"type": "string",
|
|
145
|
+
"description": "Entity name (fuzzy matched) or entity UUID",
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
"required": ["name_or_id"],
|
|
149
|
+
},
|
|
150
|
+
),
|
|
151
|
+
Tool(
|
|
152
|
+
name="list_entities",
|
|
153
|
+
description="Discover which companies, people, and products are getting expert attention in the AI economy. Filter by trend direction (Rising/Stable/Declining), priority level, coverage scale, and more. Use this for discovery — finding what's worth paying attention to. Common patterns: 'What's trending?' → direction=Rising, confidence=Significant. 'What should I watch?' → priority=High. 'Any new entrants?' → direction=New.",
|
|
154
|
+
inputSchema={
|
|
155
|
+
"type": "object",
|
|
156
|
+
"properties": {
|
|
157
|
+
"q": {"type": "string", "description": "Fuzzy name search"},
|
|
158
|
+
"theme": {"type": "string", "description": "Filter by theme label"},
|
|
159
|
+
"type_primary": {
|
|
160
|
+
"type": "string",
|
|
161
|
+
"description": "Filter by entity type: 'Organization', 'Person', 'Product', etc.",
|
|
162
|
+
},
|
|
163
|
+
"sort": {
|
|
164
|
+
"type": "string",
|
|
165
|
+
"enum": ["signal_count", "first_seen", "trend"],
|
|
166
|
+
"default": "signal_count",
|
|
167
|
+
},
|
|
168
|
+
"direction": {
|
|
169
|
+
"type": "string",
|
|
170
|
+
"enum": ["Rising", "Stable", "Declining", "New"],
|
|
171
|
+
"description": "Filter by trend direction",
|
|
172
|
+
},
|
|
173
|
+
"confidence": {
|
|
174
|
+
"type": "string",
|
|
175
|
+
"enum": ["Significant", "Insignificant"],
|
|
176
|
+
"description": "Filter by trend statistical significance",
|
|
177
|
+
},
|
|
178
|
+
"stability": {
|
|
179
|
+
"type": "string",
|
|
180
|
+
"enum": ["Volatile", "Steady"],
|
|
181
|
+
"description": "Filter by coverage consistency",
|
|
182
|
+
},
|
|
183
|
+
"scale": {
|
|
184
|
+
"type": "string",
|
|
185
|
+
"enum": ["High", "Medium", "Low"],
|
|
186
|
+
"description": "Filter by share-of-voice scale",
|
|
187
|
+
},
|
|
188
|
+
"priority": {
|
|
189
|
+
"type": "string",
|
|
190
|
+
"enum": ["High", "Medium", "Low", "Negligible"],
|
|
191
|
+
"description": "Filter by combined priority assessment",
|
|
192
|
+
},
|
|
193
|
+
"limit": {"type": "integer", "default": 25, "maximum": 50},
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
),
|
|
197
|
+
Tool(
|
|
198
|
+
name="get_themes",
|
|
199
|
+
description="List all taxonomy themes across two axes that categorize the AI economy. Value chain axis (where in the stack): Infrastructure, Foundation Models, Orchestration, Data & Labeling, Applications, Distribution. Market force axis (what's driving change): Capital & Investment, Regulatory & Legal, Competitive Dynamics, Talent & Labor, Geopolitical Strategy, Trust & Societal Impact. Each theme includes signal counts, trend direction, and priority. Use get_theme_detail for full trend analytics and co-occurring themes.",
|
|
200
|
+
inputSchema={
|
|
201
|
+
"type": "object",
|
|
202
|
+
"properties": {
|
|
203
|
+
"axis": {
|
|
204
|
+
"type": "string",
|
|
205
|
+
"enum": ["value_chain", "market_force"],
|
|
206
|
+
"description": "Filter by taxonomy axis",
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
),
|
|
211
|
+
Tool(
|
|
212
|
+
name="get_theme_detail",
|
|
213
|
+
description="Get a specific theme's full trend analytics and co-occurring themes from both axes. Returns trend direction, statistical confidence, stability, priority with reasoning, and which themes from the other axis most frequently co-occur. Use this to understand how themes interconnect. Example: 'Foundation Models' co-occurring heavily with 'Competitive Dynamics' signals a competitive landscape shift in the model layer.",
|
|
214
|
+
inputSchema={
|
|
215
|
+
"type": "object",
|
|
216
|
+
"properties": {
|
|
217
|
+
"axis": {"type": "string", "enum": ["value_chain", "market_force"]},
|
|
218
|
+
"label": {"type": "string", "description": "Theme label"},
|
|
219
|
+
},
|
|
220
|
+
"required": ["axis", "label"],
|
|
221
|
+
},
|
|
222
|
+
),
|
|
223
|
+
]
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@server.call_tool()
|
|
227
|
+
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
|
|
228
|
+
try:
|
|
229
|
+
client = _ensure_client()
|
|
230
|
+
except ValueError as e:
|
|
231
|
+
return _error_result(f"Authentication failed: {e}")
|
|
232
|
+
|
|
233
|
+
handlers = {
|
|
234
|
+
"search_text_units": lambda: tools.search_text_units(client, **arguments),
|
|
235
|
+
"list_signals": lambda: tools.list_signals(client, **arguments),
|
|
236
|
+
"get_signal_detail": lambda: tools.get_signal_detail(client, **arguments),
|
|
237
|
+
"get_entity_profile": lambda: tools.get_entity_profile(client, **arguments),
|
|
238
|
+
"list_entities": lambda: tools.list_entities(client, **arguments),
|
|
239
|
+
"get_themes": lambda: tools.get_themes(client, **arguments),
|
|
240
|
+
"get_theme_detail": lambda: tools.get_theme_detail(client, **arguments),
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
handler = handlers.get(name)
|
|
244
|
+
if not handler:
|
|
245
|
+
return _error_result(f"Unknown tool: {name}")
|
|
246
|
+
|
|
247
|
+
try:
|
|
248
|
+
result = await handler()
|
|
249
|
+
return _json_result(result)
|
|
250
|
+
except httpx.HTTPStatusError as e:
|
|
251
|
+
status = e.response.status_code
|
|
252
|
+
try:
|
|
253
|
+
body = e.response.json()
|
|
254
|
+
message = body.get("error", {}).get("message", e.response.text)
|
|
255
|
+
except Exception:
|
|
256
|
+
message = e.response.text
|
|
257
|
+
if status in (401, 403):
|
|
258
|
+
return _error_result(f"Authentication failed: {message}")
|
|
259
|
+
if status == 429:
|
|
260
|
+
return _error_result(f"Rate limit exceeded: {message}")
|
|
261
|
+
if status == 400:
|
|
262
|
+
return _error_result(message)
|
|
263
|
+
return _error_result(f"API error: {message}")
|
|
264
|
+
except httpx.RequestError as e:
|
|
265
|
+
return _error_result(f"API connection error: {e}")
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
# --- Transport modes ---
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
async def _run_stdio() -> None:
|
|
272
|
+
async with stdio_server() as (read_stream, write_stream):
|
|
273
|
+
await server.run(
|
|
274
|
+
read_stream, write_stream, server.create_initialization_options()
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _create_sse_app(host: str, port: int):
|
|
279
|
+
"""Create a Starlette app that serves the MCP server over SSE."""
|
|
280
|
+
from mcp.server.sse import SseServerTransport
|
|
281
|
+
from starlette.applications import Starlette
|
|
282
|
+
from starlette.routing import Mount, Route
|
|
283
|
+
|
|
284
|
+
sse_transport = SseServerTransport("/messages/")
|
|
285
|
+
|
|
286
|
+
async def handle_sse(scope, receive, send):
|
|
287
|
+
async with sse_transport.connect_sse(scope, receive, send) as (
|
|
288
|
+
read_stream,
|
|
289
|
+
write_stream,
|
|
290
|
+
):
|
|
291
|
+
await server.run(
|
|
292
|
+
read_stream, write_stream, server.create_initialization_options()
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
return Starlette(
|
|
296
|
+
routes=[
|
|
297
|
+
Route("/sse", app=handle_sse),
|
|
298
|
+
Mount("/messages/", app=sse_transport.handle_post_message),
|
|
299
|
+
],
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _run_sse(host: str, port: int) -> None:
|
|
304
|
+
import uvicorn
|
|
305
|
+
|
|
306
|
+
app = _create_sse_app(host, port)
|
|
307
|
+
uvicorn.run(app, host=host, port=port)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def main() -> None:
|
|
311
|
+
parser = argparse.ArgumentParser(description="Gildea MCP Server")
|
|
312
|
+
parser.add_argument(
|
|
313
|
+
"--sse", action="store_true", help="Run in SSE mode (HTTP server)"
|
|
314
|
+
)
|
|
315
|
+
parser.add_argument("--host", default="0.0.0.0", help="SSE host (default: 0.0.0.0)")
|
|
316
|
+
parser.add_argument(
|
|
317
|
+
"--port", type=int, default=8001, help="SSE port (default: 8001)"
|
|
318
|
+
)
|
|
319
|
+
args = parser.parse_args()
|
|
320
|
+
|
|
321
|
+
if args.sse:
|
|
322
|
+
_run_sse(args.host, args.port)
|
|
323
|
+
else:
|
|
324
|
+
asyncio.run(_run_stdio())
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
if __name__ == "__main__":
|
|
328
|
+
main()
|
gildea_sdk/mcp/tools.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""MCP tool implementations — thin HTTP proxies to the REST API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
from urllib.parse import quote
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
async def search_text_units(
|
|
12
|
+
client: httpx.AsyncClient,
|
|
13
|
+
*,
|
|
14
|
+
q: str | None = None,
|
|
15
|
+
similar_to: str | None = None,
|
|
16
|
+
unit_type: str | None = None,
|
|
17
|
+
entity: str | None = None,
|
|
18
|
+
theme: str | None = None,
|
|
19
|
+
published_after: str | None = None,
|
|
20
|
+
published_before: str | None = None,
|
|
21
|
+
recency_boost: float | None = None,
|
|
22
|
+
verification_detail: str | None = None,
|
|
23
|
+
limit: int = 10,
|
|
24
|
+
) -> dict[str, Any]:
|
|
25
|
+
params: dict[str, Any] = {"limit": limit}
|
|
26
|
+
if q:
|
|
27
|
+
params["q"] = q
|
|
28
|
+
if similar_to:
|
|
29
|
+
params["similar_to"] = similar_to
|
|
30
|
+
if unit_type:
|
|
31
|
+
params["unit_type"] = unit_type
|
|
32
|
+
if entity:
|
|
33
|
+
params["entity"] = entity
|
|
34
|
+
if theme:
|
|
35
|
+
params["theme"] = theme
|
|
36
|
+
if published_after:
|
|
37
|
+
params["published_after"] = published_after
|
|
38
|
+
if published_before:
|
|
39
|
+
params["published_before"] = published_before
|
|
40
|
+
if recency_boost is not None:
|
|
41
|
+
params["recency_boost"] = recency_boost
|
|
42
|
+
if verification_detail:
|
|
43
|
+
params["verification_detail"] = verification_detail
|
|
44
|
+
resp = await client.get("/v1/search", params=params)
|
|
45
|
+
resp.raise_for_status()
|
|
46
|
+
return resp.json()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async def list_signals(
|
|
50
|
+
client: httpx.AsyncClient,
|
|
51
|
+
*,
|
|
52
|
+
entity: str | None = None,
|
|
53
|
+
theme: str | None = None,
|
|
54
|
+
q: str | None = None,
|
|
55
|
+
content_type: str | None = None,
|
|
56
|
+
published_after: str | None = None,
|
|
57
|
+
published_before: str | None = None,
|
|
58
|
+
limit: int = 25,
|
|
59
|
+
) -> dict[str, Any]:
|
|
60
|
+
params: dict[str, Any] = {"limit": limit}
|
|
61
|
+
if entity:
|
|
62
|
+
params["entity"] = entity
|
|
63
|
+
if theme:
|
|
64
|
+
params["theme"] = theme
|
|
65
|
+
if q:
|
|
66
|
+
params["q"] = q
|
|
67
|
+
if content_type:
|
|
68
|
+
params["content_type"] = content_type
|
|
69
|
+
if published_after:
|
|
70
|
+
params["published_after"] = published_after
|
|
71
|
+
if published_before:
|
|
72
|
+
params["published_before"] = published_before
|
|
73
|
+
resp = await client.get("/v1/signals", params=params)
|
|
74
|
+
resp.raise_for_status()
|
|
75
|
+
return resp.json()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
async def get_signal_detail(
|
|
79
|
+
client: httpx.AsyncClient,
|
|
80
|
+
*,
|
|
81
|
+
signal_id: str,
|
|
82
|
+
include: str | None = None,
|
|
83
|
+
verification_detail: str | None = None,
|
|
84
|
+
) -> dict[str, Any]:
|
|
85
|
+
params: dict[str, str] = {}
|
|
86
|
+
if include:
|
|
87
|
+
params["include"] = include
|
|
88
|
+
if verification_detail:
|
|
89
|
+
params["verification_detail"] = verification_detail
|
|
90
|
+
resp = await client.get(f"/v1/signals/{quote(signal_id, safe='')}", params=params)
|
|
91
|
+
resp.raise_for_status()
|
|
92
|
+
return resp.json()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
async def get_entity_profile(
|
|
96
|
+
client: httpx.AsyncClient,
|
|
97
|
+
*,
|
|
98
|
+
name_or_id: str,
|
|
99
|
+
) -> dict[str, Any]:
|
|
100
|
+
resp = await client.get(f"/v1/entities/{quote(name_or_id, safe='')}")
|
|
101
|
+
resp.raise_for_status()
|
|
102
|
+
return resp.json()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
async def list_entities(
|
|
106
|
+
client: httpx.AsyncClient,
|
|
107
|
+
*,
|
|
108
|
+
q: str | None = None,
|
|
109
|
+
theme: str | None = None,
|
|
110
|
+
type_primary: str | None = None,
|
|
111
|
+
sort: str = "signal_count",
|
|
112
|
+
limit: int = 25,
|
|
113
|
+
direction: str | None = None,
|
|
114
|
+
confidence: str | None = None,
|
|
115
|
+
stability: str | None = None,
|
|
116
|
+
scale: str | None = None,
|
|
117
|
+
priority: str | None = None,
|
|
118
|
+
) -> dict[str, Any]:
|
|
119
|
+
params: dict[str, Any] = {"sort": sort, "limit": limit}
|
|
120
|
+
if q:
|
|
121
|
+
params["q"] = q
|
|
122
|
+
if theme:
|
|
123
|
+
params["theme"] = theme
|
|
124
|
+
if type_primary:
|
|
125
|
+
params["type_primary"] = type_primary
|
|
126
|
+
if direction:
|
|
127
|
+
params["direction"] = direction
|
|
128
|
+
if confidence:
|
|
129
|
+
params["confidence"] = confidence
|
|
130
|
+
if stability:
|
|
131
|
+
params["stability"] = stability
|
|
132
|
+
if scale:
|
|
133
|
+
params["scale"] = scale
|
|
134
|
+
if priority:
|
|
135
|
+
params["priority"] = priority
|
|
136
|
+
resp = await client.get("/v1/entities", params=params)
|
|
137
|
+
resp.raise_for_status()
|
|
138
|
+
return resp.json()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
async def get_themes(
|
|
142
|
+
client: httpx.AsyncClient,
|
|
143
|
+
*,
|
|
144
|
+
axis: str | None = None,
|
|
145
|
+
) -> dict[str, Any]:
|
|
146
|
+
params: dict[str, str] = {}
|
|
147
|
+
if axis:
|
|
148
|
+
params["axis"] = axis
|
|
149
|
+
resp = await client.get("/v1/themes", params=params)
|
|
150
|
+
resp.raise_for_status()
|
|
151
|
+
return resp.json()
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
async def get_theme_detail(
|
|
155
|
+
client: httpx.AsyncClient,
|
|
156
|
+
*,
|
|
157
|
+
axis: str,
|
|
158
|
+
label: str,
|
|
159
|
+
) -> dict[str, Any]:
|
|
160
|
+
resp = await client.get(
|
|
161
|
+
f"/v1/themes/{quote(axis, safe='')}/{quote(label, safe='')}"
|
|
162
|
+
)
|
|
163
|
+
resp.raise_for_status()
|
|
164
|
+
return resp.json()
|
gildea_sdk/pagination.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Shared request helpers and auto-pagination for the Gildea SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from urllib.parse import quote
|
|
6
|
+
|
|
7
|
+
from .exceptions import (
|
|
8
|
+
APIError,
|
|
9
|
+
AuthenticationError,
|
|
10
|
+
BadRequestError,
|
|
11
|
+
NotFoundError,
|
|
12
|
+
RateLimitError,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = ["_request", "_paginate", "_clean", "_quote"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _clean(params: dict) -> dict:
|
|
19
|
+
"""Remove None values from a dict of query parameters."""
|
|
20
|
+
return {k: v for k, v in params.items() if v is not None}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _quote(segment: str) -> str:
|
|
24
|
+
"""URL-encode a path segment."""
|
|
25
|
+
return quote(segment, safe="")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _request(client, method: str, path: str, *, params: dict | None = None):
|
|
29
|
+
"""Send a request and return parsed JSON, raising typed errors on failure."""
|
|
30
|
+
resp = client.request(method, path, params=params)
|
|
31
|
+
|
|
32
|
+
if resp.status_code == 204:
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
body = resp.json()
|
|
37
|
+
except Exception:
|
|
38
|
+
if resp.is_success:
|
|
39
|
+
raise APIError(
|
|
40
|
+
f"Invalid JSON in response: {resp.text[:200]}",
|
|
41
|
+
code="invalid_response",
|
|
42
|
+
status=resp.status_code,
|
|
43
|
+
)
|
|
44
|
+
raise APIError(resp.text[:200] or "Unknown error", code="server_error", status=resp.status_code)
|
|
45
|
+
|
|
46
|
+
if resp.is_success:
|
|
47
|
+
return body
|
|
48
|
+
|
|
49
|
+
error = body.get("error", {})
|
|
50
|
+
msg = error.get("message", resp.text)
|
|
51
|
+
code = error.get("code")
|
|
52
|
+
status = resp.status_code
|
|
53
|
+
|
|
54
|
+
if status in (401, 403):
|
|
55
|
+
raise AuthenticationError(msg, code=code, status=status)
|
|
56
|
+
if status == 404:
|
|
57
|
+
raise NotFoundError(msg, code=code, status=status)
|
|
58
|
+
if status == 429:
|
|
59
|
+
retry_after = resp.headers.get("Retry-After")
|
|
60
|
+
if retry_after is not None:
|
|
61
|
+
retry_after = int(retry_after)
|
|
62
|
+
raise RateLimitError(msg, code=code, status=status, retry_after=retry_after)
|
|
63
|
+
if status == 400:
|
|
64
|
+
raise BadRequestError(msg, code=code, status=status)
|
|
65
|
+
|
|
66
|
+
raise APIError(msg, code=code, status=status)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _paginate(client, path: str, params: dict):
|
|
70
|
+
"""Auto-paginating generator that yields items from paginated list endpoints."""
|
|
71
|
+
params = dict(params) # copy so we can mutate
|
|
72
|
+
while True:
|
|
73
|
+
resp = _request(client, "GET", path, params=_clean(params))
|
|
74
|
+
yield from resp["data"]
|
|
75
|
+
if not resp.get("has_more"):
|
|
76
|
+
break
|
|
77
|
+
params["cursor"] = resp["cursor"]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Entities resource."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..pagination import _clean, _paginate, _quote, _request
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class EntitiesResource:
|
|
9
|
+
def __init__(self, client):
|
|
10
|
+
self._client = client
|
|
11
|
+
|
|
12
|
+
def list(
|
|
13
|
+
self,
|
|
14
|
+
*,
|
|
15
|
+
theme=None,
|
|
16
|
+
type_primary=None,
|
|
17
|
+
q=None,
|
|
18
|
+
sort="signal_count",
|
|
19
|
+
cursor=None,
|
|
20
|
+
limit=25,
|
|
21
|
+
direction=None,
|
|
22
|
+
confidence=None,
|
|
23
|
+
stability=None,
|
|
24
|
+
scale=None,
|
|
25
|
+
priority=None,
|
|
26
|
+
):
|
|
27
|
+
params = _clean(
|
|
28
|
+
{
|
|
29
|
+
"theme": theme,
|
|
30
|
+
"type_primary": type_primary,
|
|
31
|
+
"q": q,
|
|
32
|
+
"sort": sort,
|
|
33
|
+
"cursor": cursor,
|
|
34
|
+
"limit": limit,
|
|
35
|
+
"direction": direction,
|
|
36
|
+
"confidence": confidence,
|
|
37
|
+
"stability": stability,
|
|
38
|
+
"scale": scale,
|
|
39
|
+
"priority": priority,
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
return _request(self._client, "GET", "/v1/entities", params=params)
|
|
43
|
+
|
|
44
|
+
def get(self, name_or_id):
|
|
45
|
+
path = f"/v1/entities/{_quote(name_or_id)}"
|
|
46
|
+
return _request(self._client, "GET", path)
|
|
47
|
+
|
|
48
|
+
def list_all(self, **kwargs):
|
|
49
|
+
"""Auto-paginating iterator that yields every entity matching the filters."""
|
|
50
|
+
return _paginate(self._client, "/v1/entities", kwargs)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Search resource."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..pagination import _clean, _request
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SearchResource:
|
|
9
|
+
def __init__(self, client):
|
|
10
|
+
self._client = client
|
|
11
|
+
|
|
12
|
+
def query(
|
|
13
|
+
self,
|
|
14
|
+
q=None,
|
|
15
|
+
*,
|
|
16
|
+
similar_to=None,
|
|
17
|
+
unit_type=None,
|
|
18
|
+
entity=None,
|
|
19
|
+
theme=None,
|
|
20
|
+
published_after=None,
|
|
21
|
+
published_before=None,
|
|
22
|
+
recency_boost=None,
|
|
23
|
+
verification_detail=None,
|
|
24
|
+
limit=10,
|
|
25
|
+
):
|
|
26
|
+
if not q and not similar_to:
|
|
27
|
+
raise ValueError("Exactly one of q or similar_to is required")
|
|
28
|
+
if q and similar_to:
|
|
29
|
+
raise ValueError("Exactly one of q or similar_to is required")
|
|
30
|
+
params = _clean(
|
|
31
|
+
{
|
|
32
|
+
"q": q,
|
|
33
|
+
"similar_to": similar_to,
|
|
34
|
+
"unit_type": unit_type,
|
|
35
|
+
"entity": entity,
|
|
36
|
+
"theme": theme,
|
|
37
|
+
"published_after": published_after,
|
|
38
|
+
"published_before": published_before,
|
|
39
|
+
"recency_boost": recency_boost,
|
|
40
|
+
"verification_detail": verification_detail,
|
|
41
|
+
"limit": limit,
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
return _request(self._client, "GET", "/v1/search", params=params)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Signals resource."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..pagination import _clean, _paginate, _quote, _request
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SignalsResource:
|
|
9
|
+
def __init__(self, client):
|
|
10
|
+
self._client = client
|
|
11
|
+
|
|
12
|
+
def list(
|
|
13
|
+
self,
|
|
14
|
+
*,
|
|
15
|
+
entity=None,
|
|
16
|
+
theme=None,
|
|
17
|
+
q=None,
|
|
18
|
+
content_type=None,
|
|
19
|
+
published_after=None,
|
|
20
|
+
published_before=None,
|
|
21
|
+
cursor=None,
|
|
22
|
+
limit=25,
|
|
23
|
+
):
|
|
24
|
+
params = _clean(
|
|
25
|
+
{
|
|
26
|
+
"entity": entity,
|
|
27
|
+
"theme": theme,
|
|
28
|
+
"q": q,
|
|
29
|
+
"content_type": content_type,
|
|
30
|
+
"published_after": published_after,
|
|
31
|
+
"published_before": published_before,
|
|
32
|
+
"cursor": cursor,
|
|
33
|
+
"limit": limit,
|
|
34
|
+
}
|
|
35
|
+
)
|
|
36
|
+
return _request(self._client, "GET", "/v1/signals", params=params)
|
|
37
|
+
|
|
38
|
+
def get(self, signal_id, *, include=None, verification_detail=None):
|
|
39
|
+
path = f"/v1/signals/{_quote(signal_id)}"
|
|
40
|
+
params = _clean(
|
|
41
|
+
{"include": include, "verification_detail": verification_detail}
|
|
42
|
+
)
|
|
43
|
+
return _request(self._client, "GET", path, params=params)
|
|
44
|
+
|
|
45
|
+
def list_all(self, **kwargs):
|
|
46
|
+
"""Auto-paginating iterator that yields every signal matching the filters."""
|
|
47
|
+
return _paginate(self._client, "/v1/signals", kwargs)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Themes resource."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..pagination import _clean, _quote, _request
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ThemesResource:
|
|
9
|
+
def __init__(self, client):
|
|
10
|
+
self._client = client
|
|
11
|
+
|
|
12
|
+
def list(self, *, axis=None):
|
|
13
|
+
params = _clean({"axis": axis})
|
|
14
|
+
return _request(self._client, "GET", "/v1/themes", params=params)
|
|
15
|
+
|
|
16
|
+
def get(self, axis, label):
|
|
17
|
+
path = f"/v1/themes/{_quote(axis)}/{_quote(label)}"
|
|
18
|
+
return _request(self._client, "GET", path)
|