diffbot-python 0.2.1__py3-none-any.whl → 0.3.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.
diffbot/__init__.py CHANGED
@@ -2,14 +2,38 @@
2
2
  diffbot - Python client library for the Diffbot APIs.
3
3
  """
4
4
 
5
+ import warnings as _warnings
5
6
  from importlib.metadata import PackageNotFoundError, version as _version
6
7
 
7
- try:
8
- __version__ = _version("diffbot-python")
9
- except PackageNotFoundError: # not installed (e.g. running from a source tree)
10
- __version__ = "0.0.0"
8
+ def _installed_version(dist: str):
9
+ try:
10
+ return _version(dist)
11
+ except PackageNotFoundError:
12
+ return None
13
+
14
+
15
+ # The distribution was renamed from diffbot-python to diffbot. The final
16
+ # diffbot-python release ships this same code (see legacy/diffbot-python), so
17
+ # fall back to its version when that's what is installed.
18
+ _legacy_version = _installed_version("diffbot-python")
19
+ # "0.0.0" when not installed (e.g. running from a source tree).
20
+ __version__ = _installed_version("diffbot") or _legacy_version or "0.0.0"
21
+
22
+ # Both distributions ship this module, so a diffbot-python install (alone, or
23
+ # left over next to diffbot) should migrate. FutureWarning, unlike
24
+ # DeprecationWarning, is shown by default wherever the import happens.
25
+ if _legacy_version is not None:
26
+ _warnings.warn(
27
+ "diffbot-python has been renamed to diffbot and will receive no further "
28
+ "updates. Run `pip uninstall diffbot-python && pip install "
29
+ "--force-reinstall diffbot` and replace diffbot-python with diffbot in "
30
+ "your requirements.",
31
+ FutureWarning,
32
+ stacklevel=2,
33
+ )
11
34
 
12
35
  from ._auth import resolve_token
36
+ from .ask import json_schema_format
13
37
  from .client import Diffbot, DiffbotAsync
14
38
  from .crawl import CrawlEvent, CrawlEventType
15
39
  from .errors import (
@@ -26,6 +50,7 @@ __all__ = [
26
50
  "Diffbot",
27
51
  "DiffbotAsync",
28
52
  "resolve_token",
53
+ "json_schema_format",
29
54
  "CrawlEvent",
30
55
  "CrawlEventType",
31
56
  "Ontology",
diffbot/ask.py CHANGED
@@ -1,15 +1,75 @@
1
1
  """Diffbot LLM RAG API: stream a chat completion."""
2
2
 
3
3
  import json
4
- from typing import TYPE_CHECKING, Any, AsyncIterator, Dict, Iterator, List
4
+ import re
5
+ from typing import TYPE_CHECKING, Any, AsyncIterator, Dict, Iterator, List, Optional
6
+
7
+ from .errors import ValidationError
5
8
 
6
9
  if TYPE_CHECKING:
7
10
  from .client import Diffbot, DiffbotAsync
8
11
 
12
+ MODEL = "diffbot-small-xl"
13
+
14
+ #: `response_format` types accepted by the Diffbot LLM endpoint.
15
+ RESPONSE_FORMAT_TYPES = ("text", "json_object", "json_schema")
16
+
17
+ # The RAG loop may prefix its final answer with a think block, and the model
18
+ # occasionally wraps JSON in a markdown fence despite being told not to.
19
+ _THINK_BLOCK = re.compile(r"<think>.*?</think>", re.DOTALL)
20
+ _JSON_FENCE = re.compile(r"^```(?:json)?\s*|\s*```$", re.MULTILINE)
21
+
22
+
23
+ def json_schema_format(schema: Dict[str, Any], *, name: str = "response") -> Dict[str, Any]:
24
+ """Build a ``response_format`` value that constrains output to ``schema``.
25
+
26
+ The endpoint requires the schema nested under ``json_schema.schema``; passing
27
+ it anywhere else is ignored server-side without an error, so prefer this
28
+ helper over hand-building the dict.
29
+
30
+ Example:
31
+ >>> json_schema_format({"type": "object", "properties": {"city": {"type": "string"}}})
32
+ {'type': 'json_schema', 'json_schema': {'name': 'response', 'schema': {...}}}
33
+ """
34
+ if not isinstance(schema, dict):
35
+ raise ValidationError("schema must be a JSON Schema dict")
36
+ return {"type": "json_schema", "json_schema": {"name": name, "schema": schema}}
37
+
9
38
 
10
- def _build_payload(client: Any, messages: List[Dict[str, str]]) -> tuple:
39
+ def _validate_response_format(response_format: Optional[Dict[str, Any]]) -> None:
40
+ """Reject shapes the endpoint would accept but silently not enforce."""
41
+ if response_format is None:
42
+ return
43
+ if not isinstance(response_format, dict):
44
+ raise ValidationError("response_format must be a dict")
45
+
46
+ fmt_type = response_format.get("type", "text")
47
+ if fmt_type not in RESPONSE_FORMAT_TYPES:
48
+ raise ValidationError(
49
+ f"response_format type must be one of {', '.join(RESPONSE_FORMAT_TYPES)}; got {fmt_type!r}"
50
+ )
51
+
52
+ if fmt_type == "json_schema":
53
+ json_schema = response_format.get("json_schema")
54
+ if not isinstance(json_schema, dict) or "schema" not in json_schema:
55
+ raise ValidationError(
56
+ 'response_format {"type": "json_schema"} requires the schema nested as '
57
+ '{"json_schema": {"schema": {...}}}. Without it the server returns 200 and '
58
+ "ignores the constraint. Use diffbot.json_schema_format(schema) to build it."
59
+ )
60
+
61
+
62
+ def _build_payload(
63
+ client: Any,
64
+ messages: List[Dict[str, str]],
65
+ *,
66
+ response_format: Optional[Dict[str, Any]] = None,
67
+ ) -> tuple:
68
+ _validate_response_format(response_format)
11
69
  headers = {"Authorization": f"Bearer {client.token}"}
12
- payload = {"model": "diffbot-small-xl", "messages": messages, "stream": True}
70
+ payload: Dict[str, Any] = {"model": MODEL, "messages": messages, "stream": True}
71
+ if response_format is not None:
72
+ payload["response_format"] = response_format
13
73
  return headers, payload
14
74
 
15
75
 
@@ -24,9 +84,68 @@ def _parse_chunk(line: str):
24
84
  return None
25
85
 
26
86
 
27
- def ask(client: "Diffbot", messages: List[Dict[str, str]]) -> Iterator[str]:
28
- headers = {"Authorization": f"Bearer {client.token}"}
29
- payload = {"model": "diffbot-small-xl", "messages": messages, "stream": True}
87
+ def _extract_json(text: str) -> Any:
88
+ """Parse the model's final answer as JSON, tolerating think blocks and fences."""
89
+ cleaned = _THINK_BLOCK.sub("", text).strip()
90
+ cleaned = _JSON_FENCE.sub("", cleaned).strip()
91
+
92
+ try:
93
+ return json.loads(cleaned)
94
+ except json.JSONDecodeError:
95
+ pass
96
+
97
+ # Fall back to the outermost object or array span in the response.
98
+ spans = []
99
+ for opener, closer in (("{", "}"), ("[", "]")):
100
+ start, end = cleaned.find(opener), cleaned.rfind(closer)
101
+ if start != -1 and end > start:
102
+ spans.append((start, cleaned[start : end + 1]))
103
+ for _, span in sorted(spans):
104
+ try:
105
+ return json.loads(span)
106
+ except json.JSONDecodeError:
107
+ continue
108
+
109
+ raise ValidationError(f"could not parse JSON from the model response: {text[:200]!r}")
110
+
111
+
112
+ #: Schema used when the caller wants JSON but has no shape in mind. This goes
113
+ #: through the json_schema path rather than {"type": "json_object"} on purpose:
114
+ #: json_object applies no server-side grammar, so the RAG loop's internal
115
+ #: <functioncall> JSON satisfies it and gets returned as the final answer. That
116
+ #: is reproducible whenever the request carries a system message.
117
+ ANY_OBJECT_SCHEMA = {"type": "object"}
118
+
119
+
120
+ def _resolve_format(
121
+ schema: Optional[Dict[str, Any]],
122
+ response_format: Optional[Dict[str, Any]],
123
+ ) -> Dict[str, Any]:
124
+ if schema is not None and response_format is not None:
125
+ raise ValidationError("pass either schema or response_format, not both")
126
+ if response_format is not None:
127
+ return response_format
128
+ return json_schema_format(schema if schema is not None else ANY_OBJECT_SCHEMA)
129
+
130
+
131
+ def _check_tool_call_leak(parsed: Any) -> Any:
132
+ """Catch the internal tool call surfacing as the answer (see ANY_OBJECT_SCHEMA)."""
133
+ if isinstance(parsed, dict) and parsed.get("name") == "functioncall" and "arguments" in parsed:
134
+ raise ValidationError(
135
+ "the model returned its internal tool call instead of an answer; this happens with "
136
+ 'response_format {"type": "json_object"} because the server applies no grammar to it. '
137
+ "Pass a schema instead."
138
+ )
139
+ return parsed
140
+
141
+
142
+ def ask(
143
+ client: "Diffbot",
144
+ messages: List[Dict[str, str]],
145
+ *,
146
+ response_format: Optional[Dict[str, Any]] = None,
147
+ ) -> Iterator[str]:
148
+ headers, payload = _build_payload(client, messages, response_format=response_format)
30
149
  with client._http.stream("POST", client.llm_url, headers=headers, json=payload) as response:
31
150
  client._raise_for_status(response)
32
151
  for line in response.iter_lines():
@@ -36,9 +155,13 @@ def ask(client: "Diffbot", messages: List[Dict[str, str]]) -> Iterator[str]:
36
155
  yield content
37
156
 
38
157
 
39
- async def ask_async(client: "DiffbotAsync", messages: List[Dict[str, str]]) -> AsyncIterator[str]:
40
- headers = {"Authorization": f"Bearer {client.token}"}
41
- payload = {"model": "diffbot-small-xl", "messages": messages, "stream": True}
158
+ async def ask_async(
159
+ client: "DiffbotAsync",
160
+ messages: List[Dict[str, str]],
161
+ *,
162
+ response_format: Optional[Dict[str, Any]] = None,
163
+ ) -> AsyncIterator[str]:
164
+ headers, payload = _build_payload(client, messages, response_format=response_format)
42
165
  async with client._http.stream("POST", client.llm_url, headers=headers, json=payload) as response:
43
166
  client._raise_for_status(response)
44
167
  async for line in response.aiter_lines():
@@ -46,3 +169,27 @@ async def ask_async(client: "DiffbotAsync", messages: List[Dict[str, str]]) -> A
46
169
  content = _parse_chunk(line)
47
170
  if content:
48
171
  yield content
172
+
173
+
174
+ def ask_json(
175
+ client: "Diffbot",
176
+ messages: List[Dict[str, str]],
177
+ schema: Optional[Dict[str, Any]] = None,
178
+ *,
179
+ response_format: Optional[Dict[str, Any]] = None,
180
+ ) -> Any:
181
+ fmt = _resolve_format(schema, response_format)
182
+ text = "".join(ask(client, messages, response_format=fmt))
183
+ return _check_tool_call_leak(_extract_json(text))
184
+
185
+
186
+ async def ask_json_async(
187
+ client: "DiffbotAsync",
188
+ messages: List[Dict[str, str]],
189
+ schema: Optional[Dict[str, Any]] = None,
190
+ *,
191
+ response_format: Optional[Dict[str, Any]] = None,
192
+ ) -> Any:
193
+ fmt = _resolve_format(schema, response_format)
194
+ chunks = [chunk async for chunk in ask_async(client, messages, response_format=fmt)]
195
+ return _check_tool_call_leak(_extract_json("".join(chunks)))
diffbot/cli/__init__.py CHANGED
@@ -11,7 +11,15 @@ from rich.markdown import Markdown
11
11
  from rich.progress import Progress, SpinnerColumn, TextColumn
12
12
  from datetime import datetime
13
13
 
14
- from diffbot import CrawlEventType, AuthError, ExtractionError, APIError
14
+ from diffbot import (
15
+ __version__,
16
+ CrawlEventType,
17
+ AuthError,
18
+ ExtractionError,
19
+ APIError,
20
+ ValidationError,
21
+ json_schema_format,
22
+ )
15
23
 
16
24
  from ._common import get_client
17
25
 
@@ -28,6 +36,7 @@ def print_markdown(text):
28
36
 
29
37
 
30
38
  @click.group()
39
+ @click.version_option(__version__, "-V", "--version", prog_name="db")
31
40
  def main():
32
41
  """
33
42
  Diffbot 🤖 Structure the world's knowledge.
@@ -85,10 +94,29 @@ def extract(url, output, fmt, api):
85
94
  @click.argument("prompt")
86
95
  @click.option("-o", "--output", type=click.Path(), help="Write output to file instead of stdout")
87
96
  @click.option("--json", "as_json", is_flag=True, help="Output from LLM as JSON")
88
- def ask(prompt: str, output: str = None, as_json: bool = False):
97
+ @click.option(
98
+ "--schema",
99
+ "schema_path",
100
+ type=click.Path(exists=True, dir_okay=False),
101
+ help="JSON Schema file constraining the output (implies --json)",
102
+ )
103
+ def ask(prompt: str, output: str = None, as_json: bool = False, schema_path: str = None):
89
104
  """Ask a question to the Diffbot LLM"""
90
105
  db = get_client()
91
106
  stdin_content = sys.stdin.read() if not sys.stdin.isatty() else None
107
+
108
+ response_format = None
109
+ if schema_path:
110
+ as_json = True
111
+ try:
112
+ with open(schema_path) as f:
113
+ response_format = json_schema_format(json.load(f))
114
+ except json.JSONDecodeError as e:
115
+ click.echo(f"Error: {schema_path} is not valid JSON: {e}", err=True)
116
+ raise click.Abort()
117
+
118
+ # Without --schema, response_format stays None and ask_json picks its own
119
+ # permissive default; --json alone must not become {"type": "json_object"}.
92
120
  interactive_mode = is_interactive and not output and not as_json
93
121
 
94
122
  messages = [
@@ -99,9 +127,6 @@ def ask(prompt: str, output: str = None, as_json: bool = False):
99
127
  {"role": "user", "content": prompt},
100
128
  ]
101
129
 
102
- if as_json:
103
- messages[1]["content"] += "\nReturn the output as a JSON object. Do not include any other text outside of the JSON object."
104
-
105
130
  if stdin_content:
106
131
  messages[1]["content"] = f"<input>{stdin_content}</input>\n" + messages[1]["content"]
107
132
 
@@ -136,17 +161,20 @@ def ask(prompt: str, output: str = None, as_json: bool = False):
136
161
  else:
137
162
  response = ""
138
163
  if output:
139
- for chunk in db.ask(messages):
140
- response += chunk
164
+ if as_json:
165
+ response = json.dumps(
166
+ db.ask_json(messages, response_format=response_format), indent=2
167
+ )
168
+ else:
169
+ for chunk in db.ask(messages):
170
+ response += chunk
141
171
  with open(output, "w") as f:
142
172
  f.write(response)
143
173
  click.echo(f"Output written to {output}")
144
174
  elif as_json:
145
- buffer = ""
146
- for chunk in db.ask(messages):
147
- buffer += chunk
148
- buffer = buffer[buffer.find("{") : buffer.rfind("}") + 1]
149
- sys.stdout.write(buffer)
175
+ sys.stdout.write(
176
+ json.dumps(db.ask_json(messages, response_format=response_format), indent=2)
177
+ )
150
178
  else:
151
179
  for chunk in db.ask(messages):
152
180
  sys.stdout.write(chunk)
@@ -155,6 +183,9 @@ def ask(prompt: str, output: str = None, as_json: bool = False):
155
183
  except AuthError:
156
184
  click.echo("Error: Invalid or unauthorized API token.", err=True)
157
185
  raise click.Abort()
186
+ except ValidationError as e:
187
+ click.echo(f"Error: {e}", err=True)
188
+ raise click.Abort()
158
189
  except APIError as e:
159
190
  click.echo(f"API error {e.status_code}: {e.message or e.body}", err=True)
160
191
  raise click.Abort()
diffbot/client.py CHANGED
@@ -9,7 +9,12 @@ import httpx
9
9
  from . import __version__
10
10
  from .errors import APIError, AuthError, RateLimitError, ValidationError
11
11
  from .extract import extract as _extract, extract_async as _extract_async
12
- from .ask import ask as _ask, ask_async as _ask_async
12
+ from .ask import (
13
+ ask as _ask,
14
+ ask_async as _ask_async,
15
+ ask_json as _ask_json,
16
+ ask_json_async as _ask_json_async,
17
+ )
13
18
  from .crawl import (
14
19
  CrawlEvent,
15
20
  crawl as _crawl,
@@ -22,6 +27,8 @@ from .crawl import (
22
27
  crawl_list_jobs_async as _crawl_list_jobs_async,
23
28
  )
24
29
  from .kg import (
30
+ KG_DQL_ENDPOINT,
31
+ KG_ONTOLOGY_ENDPOINT,
25
32
  dql as _dql,
26
33
  dql_async as _dql_async,
27
34
  dql_fetch_ontology as _dql_fetch_ontology,
@@ -67,6 +74,8 @@ class Diffbot:
67
74
  crawler_url: str = CRAWL_BASE,
68
75
  web_search_url: str = WEB_SEARCH_BASE,
69
76
  nlp_url: str = NLP_BASE,
77
+ dql_url: str = KG_DQL_ENDPOINT,
78
+ ontology_url: str = KG_ONTOLOGY_ENDPOINT,
70
79
  transport: Optional[httpx.BaseTransport] = None,
71
80
  ):
72
81
  if not token:
@@ -77,6 +86,8 @@ class Diffbot:
77
86
  self.crawler_url = crawler_url
78
87
  self.web_search_url = web_search_url
79
88
  self.nlp_url = nlp_url
89
+ self.dql_url = dql_url
90
+ self.ontology_url = ontology_url
80
91
  self._http = httpx.Client(
81
92
  timeout=timeout,
82
93
  headers={"User-Agent": f"diffbot-python/{__version__}"},
@@ -112,9 +123,32 @@ class Diffbot:
112
123
  """Extract structured content from a URL. Returns the raw Diffbot API response."""
113
124
  return _extract(self, url, api=api, fmt=fmt)
114
125
 
115
- def ask(self, messages: List[Dict[str, str]]) -> Iterator[str]:
116
- """Stream a response from the Diffbot LLM RAG API."""
117
- yield from _ask(self, messages)
126
+ def ask(
127
+ self,
128
+ messages: List[Dict[str, str]],
129
+ *,
130
+ response_format: Optional[Dict[str, Any]] = None,
131
+ ) -> Iterator[str]:
132
+ """Stream a response from the Diffbot LLM RAG API.
133
+
134
+ Pass ``response_format`` to constrain the output, e.g.
135
+ ``{"type": "json_object"}`` or ``diffbot.json_schema_format(schema)``.
136
+ """
137
+ yield from _ask(self, messages, response_format=response_format)
138
+
139
+ def ask_json(
140
+ self,
141
+ messages: List[Dict[str, str]],
142
+ schema: Optional[Dict[str, Any]] = None,
143
+ *,
144
+ response_format: Optional[Dict[str, Any]] = None,
145
+ ) -> Any:
146
+ """Ask the Diffbot LLM and return the answer parsed as JSON.
147
+
148
+ With ``schema`` (a JSON Schema dict) the model is constrained to it by
149
+ grammar; without one it falls back to unconstrained JSON output.
150
+ """
151
+ return _ask_json(self, messages, schema, response_format=response_format)
118
152
 
119
153
  def crawl(self, site: str, **kwargs: Any) -> Iterator[CrawlEvent]:
120
154
  """Start a crawl job."""
@@ -194,6 +228,8 @@ class DiffbotAsync:
194
228
  crawler_url: str = CRAWL_BASE,
195
229
  web_search_url: str = WEB_SEARCH_BASE,
196
230
  nlp_url: str = NLP_BASE,
231
+ dql_url: str = KG_DQL_ENDPOINT,
232
+ ontology_url: str = KG_ONTOLOGY_ENDPOINT,
197
233
  transport: Optional[httpx.AsyncBaseTransport] = None,
198
234
  ):
199
235
  if not token:
@@ -204,6 +240,8 @@ class DiffbotAsync:
204
240
  self.crawler_url = crawler_url
205
241
  self.web_search_url = web_search_url
206
242
  self.nlp_url = nlp_url
243
+ self.dql_url = dql_url
244
+ self.ontology_url = ontology_url
207
245
  self._http = httpx.AsyncClient(
208
246
  timeout=timeout,
209
247
  headers={"User-Agent": f"diffbot-python/{__version__}"},
@@ -239,11 +277,34 @@ class DiffbotAsync:
239
277
  """Extract structured content from a URL. Returns the raw Diffbot API response."""
240
278
  return await _extract_async(self, url, api=api, fmt=fmt)
241
279
 
242
- async def ask(self, messages: List[Dict[str, str]]) -> AsyncIterator[str]:
243
- """Stream a response from the Diffbot LLM RAG API."""
244
- async for chunk in _ask_async(self, messages):
280
+ async def ask(
281
+ self,
282
+ messages: List[Dict[str, str]],
283
+ *,
284
+ response_format: Optional[Dict[str, Any]] = None,
285
+ ) -> AsyncIterator[str]:
286
+ """Stream a response from the Diffbot LLM RAG API.
287
+
288
+ Pass ``response_format`` to constrain the output, e.g.
289
+ ``{"type": "json_object"}`` or ``diffbot.json_schema_format(schema)``.
290
+ """
291
+ async for chunk in _ask_async(self, messages, response_format=response_format):
245
292
  yield chunk
246
293
 
294
+ async def ask_json(
295
+ self,
296
+ messages: List[Dict[str, str]],
297
+ schema: Optional[Dict[str, Any]] = None,
298
+ *,
299
+ response_format: Optional[Dict[str, Any]] = None,
300
+ ) -> Any:
301
+ """Ask the Diffbot LLM and return the answer parsed as JSON.
302
+
303
+ With ``schema`` (a JSON Schema dict) the model is constrained to it by
304
+ grammar; without one it falls back to unconstrained JSON output.
305
+ """
306
+ return await _ask_json_async(self, messages, schema, response_format=response_format)
307
+
247
308
  async def crawl(self, site: str, **kwargs: Any) -> AsyncIterator[CrawlEvent]:
248
309
  """Start a crawl job. Pass watch=True to poll until completion and yield URL_PROCESSED events."""
249
310
  async for event in _crawl_async(self, site, **kwargs):
diffbot/kg.py CHANGED
@@ -51,7 +51,7 @@ def dql(
51
51
  raw: bool = False,
52
52
  ) -> Union[Dict[str, Any], bytes]:
53
53
  params = _build_dql_params(client, query, size, from_, format, filter, exportspec, extra)
54
- response = client._http.get(KG_DQL_ENDPOINT, params=params)
54
+ response = client._http.get(client.dql_url, params=params)
55
55
  client._raise_for_status(response)
56
56
  return response.content if raw else response.json()
57
57
 
@@ -69,7 +69,7 @@ async def dql_async(
69
69
  raw: bool = False,
70
70
  ) -> Union[Dict[str, Any], bytes]:
71
71
  params = _build_dql_params(client, query, size, from_, format, filter, exportspec, extra)
72
- response = await client._http.get(KG_DQL_ENDPOINT, params=params)
72
+ response = await client._http.get(client.dql_url, params=params)
73
73
  client._raise_for_status(response)
74
74
  return response.content if raw else response.json()
75
75
 
@@ -104,7 +104,7 @@ async def dql_parallel_async(
104
104
 
105
105
 
106
106
  def dql_refresh_ontology(client: "Diffbot", dest: pathlib.Path) -> None:
107
- response = client._http.get(KG_ONTOLOGY_ENDPOINT)
107
+ response = client._http.get(client.ontology_url)
108
108
  client._raise_for_status(response)
109
109
  dest.parent.mkdir(parents=True, exist_ok=True)
110
110
  dest.write_bytes(response.content)
@@ -116,13 +116,13 @@ def dql_fetch_ontology(client: "Diffbot") -> Ontology:
116
116
  Performs no caching — the caller decides whether and where to hold onto the
117
117
  result. Use :func:`dql_refresh_ontology` instead to persist raw bytes to disk.
118
118
  """
119
- response = client._http.get(KG_ONTOLOGY_ENDPOINT)
119
+ response = client._http.get(client.ontology_url)
120
120
  client._raise_for_status(response)
121
121
  return Ontology.from_json(response.content)
122
122
 
123
123
 
124
124
  async def dql_fetch_ontology_async(client: "DiffbotAsync") -> Ontology:
125
125
  """Async variant of :func:`dql_fetch_ontology`."""
126
- response = await client._http.get(KG_ONTOLOGY_ENDPOINT)
126
+ response = await client._http.get(client.ontology_url)
127
127
  client._raise_for_status(response)
128
128
  return Ontology.from_json(response.content)
@@ -0,0 +1,54 @@
1
+ Metadata-Version: 2.4
2
+ Name: diffbot-python
3
+ Version: 0.3.0
4
+ Summary: ⚠️ LEGACY — renamed to diffbot. Final release, no further updates: pip install diffbot
5
+ Project-URL: Homepage, https://pypi.org/project/diffbot/
6
+ Project-URL: Repository, https://github.com/diffbot/diffbot-python
7
+ Author-email: Jerome Choo <jerome@diffbot.com>, Mike Tung <miket@diffbot.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 7 - Inactive
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3 :: Only
13
+ Requires-Python: >=3.9
14
+ Requires-Dist: click>=8.1.0
15
+ Requires-Dist: httpx>=0.27.0
16
+ Requires-Dist: rich>=13.0.0
17
+ Description-Content-Type: text/markdown
18
+
19
+ # ⚠️ LEGACY PACKAGE — `diffbot-python` is now `diffbot` ⚠️
20
+
21
+ > ## 🛑 Do not install `diffbot-python`. Install [`diffbot`](https://pypi.org/project/diffbot/) instead:
22
+ >
23
+ > ```bash
24
+ > pip uninstall diffbot-python
25
+ > pip install diffbot
26
+ > ```
27
+ >
28
+ > **This is the final release of `diffbot-python` (0.3.0). It will never be updated again**:
29
+ > no bug fixes, no new Diffbot API features, no security fixes.
30
+
31
+ ---
32
+
33
+ The Diffbot Python library and `db` CLI now live at
34
+ **[diffbot](https://pypi.org/project/diffbot/)**. The import name is unchanged
35
+ (`import diffbot`), so no code changes are needed.
36
+
37
+ This release contains the same code as `diffbot` 3.0.0 so existing installs keep
38
+ working, but it prints a warning on every import until you migrate.
39
+
40
+ ## Migrating
41
+
42
+ 1. Replace `diffbot-python` with `diffbot` in your `requirements.txt`, `pyproject.toml`, etc.
43
+ 2. Uninstall the old package **before** installing the new one:
44
+
45
+ ```bash
46
+ pip uninstall diffbot-python
47
+ pip install diffbot
48
+ ```
49
+
50
+ Both packages provide the same `diffbot` module, so uninstalling
51
+ `diffbot-python` after `diffbot` is installed deletes files that `diffbot`
52
+ needs. If that happened, run `pip install --force-reinstall diffbot`.
53
+
54
+ Source and issues: <https://github.com/diffbot/diffbot-python>.
@@ -1,22 +1,22 @@
1
- diffbot/__init__.py,sha256=OUDi3bVvKowQAQB4m8I4t761ct4pC-3de461qPI4Zok,827
1
+ diffbot/__init__.py,sha256=LAZr0P760URw34JXMIcN-2fTMIELq_-IUd1gj8KgVfI,1875
2
2
  diffbot/_auth.py,sha256=dzNMWqIjtG3YtRBb_EKpPRiBxvZgogxvoE0ABLvzfOk,1251
3
- diffbot/ask.py,sha256=iNv613j4CoIfdDTOE-pl9KUkjqI-2AxGDMR1prm3DGM,1853
4
- diffbot/client.py,sha256=G6vYUEDEaIqjJXcIRxAjwS_cvaXvM7KdQFVMIedkgUY,11315
3
+ diffbot/ask.py,sha256=taLISU0BUHzi9SwESZmgDST8PUHoLGJnI02aHngljQM,7411
4
+ diffbot/client.py,sha256=k3orUmOv1RG_xHInyEt0bS0CIYyTOBW56ZDEMgvX8nc,13379
5
5
  diffbot/crawl.py,sha256=iYMFmf7HKrbefJrGg14VnlfiBFLOE_Z1pfO4Rn_cDXc,8893
6
6
  diffbot/errors.py,sha256=5-AceX5MyNVUhe9pvR_4rnQQmBhvLfwWmrRl7dRZUSg,1576
7
7
  diffbot/extract.py,sha256=R9SVxaOi4FjHOQIX5ho_75OwaJ7VdX-mdAoV_UY-lrM,1452
8
- diffbot/kg.py,sha256=my-5vR4Vbe3LHzBDYVHLHo99KTtyiRXCT5625glsrlQ,3930
8
+ diffbot/kg.py,sha256=WPvy3B_ywdoM0uEmAgN7-3uOvf4aC4l2mjS2v_tWPN4,3925
9
9
  diffbot/nlp.py,sha256=lZJW4MkjhVklIEM2OBfhc8LvresXTy7RsuImPteYsOA,1153
10
10
  diffbot/ontology.py,sha256=GWD5m2rz7ECWY6RrO1TeN3kk3DYTgmEHz6gFKASMvAU,6283
11
11
  diffbot/web_search.py,sha256=RSaEK0pdAdLgtilc500Mf2MNmJ1DmiYYmIlBQBWvOpE,1356
12
- diffbot/cli/__init__.py,sha256=UVGD3uevKTHmqEdbDhhR2PzO6-3i0xu8d7D_94jLLRo,16488
12
+ diffbot/cli/__init__.py,sha256=uCngMioFdxTSeUrBxPrxepVqqwllAjXVr8W-zThcYaE,17435
13
13
  diffbot/cli/__main__.py,sha256=5BjNuyet8AY-POwoF5rGt722rHQ7tJ0Vf0UFUfzzi-I,58
14
14
  diffbot/cli/_common.py,sha256=_s8WRukzSX3krjJW6HJmT7_3hEjNyv1i_y45tm81b2A,654
15
15
  diffbot/cli/dql.py,sha256=iIMaGysMWTScnbAHAdWei2iVUJpMV5lA1pk1E9uQg4s,11439
16
16
  diffbot/cli/entities.py,sha256=tsHKexF0b6NnsoUEZJUS2rJidTIE8lt6xrcSenPbwtY,5630
17
17
  diffbot/cli/ontology.py,sha256=cLuRQ0KTsOJnFCXdG_zIX9J1ODK8ThPznWVDDEpO1pg,2055
18
- diffbot_python-0.2.1.dist-info/METADATA,sha256=xNpRnsnbGtPxU-zUAPwozdiHprTBS82zjUD87BMO36U,7402
19
- diffbot_python-0.2.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
20
- diffbot_python-0.2.1.dist-info/entry_points.txt,sha256=FCxJhrbl7VNEsTK7zl7qYvXID7gQ1_wxhiw5_Vllb_M,40
21
- diffbot_python-0.2.1.dist-info/licenses/LICENSE,sha256=UZlamI1XGeiG0Mit8dsHssNhOuMGKfmNOp5qpf1533w,1063
22
- diffbot_python-0.2.1.dist-info/RECORD,,
18
+ diffbot_python-0.3.0.dist-info/METADATA,sha256=ryJb4Ai_rjjbtmngp1o6cYnennDxK3dJWsokcKZ7-U0,1985
19
+ diffbot_python-0.3.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
20
+ diffbot_python-0.3.0.dist-info/entry_points.txt,sha256=FCxJhrbl7VNEsTK7zl7qYvXID7gQ1_wxhiw5_Vllb_M,40
21
+ diffbot_python-0.3.0.dist-info/licenses/LICENSE,sha256=UZlamI1XGeiG0Mit8dsHssNhOuMGKfmNOp5qpf1533w,1063
22
+ diffbot_python-0.3.0.dist-info/RECORD,,
@@ -1,266 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: diffbot-python
3
- Version: 0.2.1
4
- Summary: Python client library for Diffbot APIs
5
- Project-URL: Homepage, https://github.com/diffbot/diffbot-python
6
- Project-URL: Documentation, https://github.com/diffbot/diffbot-python#readme
7
- Project-URL: Repository, https://github.com/diffbot/diffbot-python
8
- Project-URL: Issues, https://github.com/diffbot/diffbot-python/issues
9
- Author-email: Jerome Choo <jerome@diffbot.com>, Mike Tung <miket@diffbot.com>
10
- License-Expression: MIT
11
- License-File: LICENSE
12
- Keywords: api-client,crawler,diffbot,extract,knowledge-graph,llm,nlp,web-scraping
13
- Classifier: Development Status :: 3 - Alpha
14
- Classifier: Intended Audience :: Developers
15
- Classifier: Operating System :: OS Independent
16
- Classifier: Programming Language :: Python :: 3
17
- Classifier: Programming Language :: Python :: 3 :: Only
18
- Classifier: Programming Language :: Python :: 3.10
19
- Classifier: Programming Language :: Python :: 3.11
20
- Classifier: Programming Language :: Python :: 3.12
21
- Classifier: Programming Language :: Python :: 3.13
22
- Classifier: Topic :: Internet :: WWW/HTTP
23
- Classifier: Topic :: Internet :: WWW/HTTP :: Indexing/Search
24
- Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
25
- Classifier: Topic :: Software Development :: Libraries :: Python Modules
26
- Classifier: Topic :: Text Processing :: Markup :: HTML
27
- Classifier: Typing :: Typed
28
- Requires-Python: >=3.10
29
- Requires-Dist: click>=8.1.0
30
- Requires-Dist: httpx>=0.27.0
31
- Requires-Dist: rich>=13.0.0
32
- Provides-Extra: dev
33
- Requires-Dist: pytest>=8.0.0; extra == 'dev'
34
- Description-Content-Type: text/markdown
35
-
36
- # Diffbot Python Library
37
-
38
- Python client library for [Diffbot](https://www.diffbot.com) APIs.
39
-
40
-
41
- ## Installation
42
-
43
- ```bash
44
- python3 -m pip install diffbot-python
45
- ```
46
-
47
- Or, for local development:
48
-
49
- ```bash
50
- pip install -e ".[dev]"
51
- ```
52
-
53
- ## Usage
54
-
55
- ### Authentication
56
-
57
- The CLI and the library can share a single credential. The token always has to be
58
- passed to the client explicitly, but `resolve_token()` gives you the same lookup the
59
- CLI uses, in this order:
60
-
61
- 1. An explicit token passed to `resolve_token(token)`.
62
- 2. The `DIFFBOT_API_TOKEN` environment variable.
63
- 3. A `DIFFBOT_API_TOKEN=...` line in `~/.diffbot/credentials`.
64
-
65
- Set it once and it works for both the CLI and your scripts. Either export it:
66
-
67
- ```bash
68
- export DIFFBOT_API_TOKEN=<TOKEN>
69
- ```
70
-
71
- …or write it to the shared credentials file (handy for keeping it out of your shell environment):
72
-
73
- ```bash
74
- mkdir -p ~/.diffbot
75
- printf 'DIFFBOT_API_TOKEN=%s\n' '<TOKEN>' > ~/.diffbot/credentials
76
- chmod 600 ~/.diffbot/credentials
77
- ```
78
-
79
- With either in place, resolve the token and pass it to the client:
80
-
81
- ```python
82
- from diffbot import Diffbot, resolve_token
83
-
84
- db = Diffbot(token=resolve_token()) # from env var or ~/.diffbot/credentials
85
- data = db.extract("https://www.example.com")
86
- ```
87
-
88
- ### Extract structured content
89
- ```python
90
- from diffbot import Diffbot
91
-
92
- db = Diffbot(token="YOUR_TOKEN")
93
- data = db.extract("https://www.example.com")
94
- ```
95
-
96
- ### Ask Diffbot LLM
97
- ```python
98
- from diffbot import Diffbot
99
-
100
- db = Diffbot(token="YOUR_TOKEN")
101
- for chunk in db.ask([{"role": "user", "content": "What's the capital of France?"}]):
102
- print(chunk, end="")
103
- ```
104
-
105
- ### Crawl a site for structured content
106
- ```python
107
- from diffbot import Diffbot
108
-
109
- db = Diffbot(token="YOUR_TOKEN")
110
- for event in db.crawl("https://www.example.com", hops=1):
111
- print(event)
112
- ```
113
-
114
- ### Query the Knowledge Graph
115
- ```python
116
- from diffbot import Diffbot
117
-
118
- db = Diffbot(token="YOUR_TOKEN")
119
- results = db.dql('type:Organization name:"Diffbot"')
120
- ```
121
-
122
- ### Web Search
123
- ```python
124
- from diffbot import Diffbot
125
-
126
- db = Diffbot(token="YOUR_TOKEN")
127
- results = db.web_search("diffbot knowledge graph")
128
- for r in results["search_results"]:
129
- print(r["score"], r["title"], r["pageUrl"])
130
- print(r["content"])
131
- ```
132
-
133
- ### Entities (NLP)
134
- ```python
135
- from diffbot import Diffbot
136
-
137
- db = Diffbot(token="YOUR_TOKEN")
138
- result = db.entities("Apple CEO Tim Cook announced record quarterly earnings.")
139
- for entity in result["entities"]:
140
- print(entity["name"], entity.get("type"), entity.get("id"))
141
- print("sentiment:", result.get("sentiment"))
142
- ```
143
-
144
- ## Async Usage
145
-
146
- ### Extract structured content
147
- ```python
148
- import asyncio
149
- from diffbot import DiffbotAsync
150
-
151
- async def main():
152
- async with DiffbotAsync(token="YOUR_TOKEN") as db:
153
- data = await db.extract("https://www.example.com")
154
- print(data)
155
-
156
- asyncio.run(main())
157
- ```
158
-
159
- ### Ask Diffbot LLM
160
- ```python
161
- import asyncio
162
- from diffbot import DiffbotAsync
163
-
164
- async def main():
165
- async with DiffbotAsync(token="YOUR_TOKEN") as db:
166
- async for chunk in db.ask([{"role": "user", "content": "What's the capital of France?"}]):
167
- print(chunk, end="")
168
-
169
- asyncio.run(main())
170
- ```
171
-
172
- ### Crawl a site for structured content
173
- ```python
174
- import asyncio
175
- from diffbot import DiffbotAsync
176
-
177
- async def main():
178
- async with DiffbotAsync(token="YOUR_TOKEN") as db:
179
- async for event in db.crawl("https://www.example.com", hops=1):
180
- print(event)
181
-
182
- asyncio.run(main())
183
- ```
184
-
185
- ### Query the Knowledge Graph
186
- ```python
187
- import asyncio
188
- from diffbot import DiffbotAsync
189
-
190
- async def main():
191
- async with DiffbotAsync(token="YOUR_TOKEN") as db:
192
- results = await db.dql('type:Organization name:"Diffbot"')
193
- print(results)
194
-
195
- asyncio.run(main())
196
- ```
197
-
198
- ### Web Search
199
- ```python
200
- import asyncio
201
- from diffbot import DiffbotAsync
202
-
203
- async def main():
204
- async with DiffbotAsync(token="YOUR_TOKEN") as db:
205
- results = await db.web_search("diffbot knowledge graph")
206
- for r in results["search_results"]:
207
- print(r["score"], r["title"], r["pageUrl"])
208
- print(r["content"])
209
-
210
- asyncio.run(main())
211
- ```
212
-
213
- ### Entities (NLP)
214
- ```python
215
- import asyncio
216
- from diffbot import DiffbotAsync
217
-
218
- async def main():
219
- async with DiffbotAsync(token="YOUR_TOKEN") as db:
220
- result = await db.entities("Apple CEO Tim Cook announced record quarterly earnings.")
221
- for entity in result["entities"]:
222
- print(entity["name"], entity.get("type"), entity.get("id"))
223
- print("sentiment:", result.get("sentiment"))
224
-
225
- asyncio.run(main())
226
- ```
227
-
228
- ## CLI
229
-
230
- This library also includes a CLI exposed as the `db` command.
231
-
232
- To make `db` available from anywhere, install it as an isolated tool with [uv](https://docs.astral.sh/uv/):
233
-
234
- ```bash
235
- uv tool install .
236
- ```
237
-
238
- This drops a `db` executable into `~/.local/bin` (ensure it is on your `PATH`). Use `--force` to reinstall or upgrade after changes, or `--editable` to have source edits take effect immediately. Alternatively, a plain `pip install .` (or `pip install -e .`) also installs the `db` entry point into the active environment.
239
-
240
- ```bash
241
- export DIFFBOT_API_TOKEN=your-token-here
242
-
243
- db extract https://www.example.com
244
- db ask "What's the capital of France?"
245
- db crawl https://www.example.com --hops 1
246
- db crawl-list-jobs
247
- db crawl-delete-job crawl-1234567890
248
- db web-search "diffbot knowledge graph"
249
- db web-search "diffbot knowledge graph" -n 5 -f json
250
- db entities "Apple CEO Tim Cook announced record quarterly earnings."
251
- db entities "Apple CEO Tim Cook announced record quarterly earnings." -f dql
252
- ```
253
-
254
- ## Tests
255
-
256
- Run the mock test suite:
257
- ```bash
258
- python -m pytest
259
- ```
260
-
261
- Run live integration tests against the real API (requires a valid token).
262
- The token is resolved the same way as everywhere else — the `DIFFBOT_API_TOKEN`
263
- environment variable or `~/.diffbot/credentials`:
264
- ```bash
265
- DIFFBOT_API_TOKEN=your_token python -m pytest -m live
266
- ```