Skip to content

Commit 0d306f9

Browse files
authored
Release 0.4.0 with nested result metadata (#4)
* Move engine-specific search fields into result metadata * Prepare metadata release versions and migration notes
1 parent 56241a8 commit 0d306f9

7 files changed

Lines changed: 67 additions & 11 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Changelog
2+
3+
## 0.4.0 - 2026-10-12
4+
5+
Breaking: search/news engine-specific fields move from each result's top level to optional `metadata` (`kind`, `stars`, `language`, `num_comments`, `points`, `story_url`). Read e.g. `result.metadata.stars` instead of `result.stars`. `published_date` stays at the top level. No legacy field aliases.

‎README.md‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,11 @@ for result in response["results"]:
3030
`bingcn`, `yandex`, `reddit`, `github`, `arxiv`, `wikipedia`, or `grokipedia`.
3131
`page` requests a later results page on engines with native pagination
3232
(`bing`, `bingcn`, `baidu`, `grokipedia`). Results carry `published_date` when the source
33-
exposes one.
33+
exposes one. Engine-specific fields are in each result's optional `metadata`:
34+
GitHub uses `metadata.kind`, `metadata.stars`, `metadata.language`, and
35+
`metadata.num_comments`; Hacker News news results use `metadata.story_url`,
36+
`metadata.points`, and `metadata.num_comments`. The former top-level paths
37+
are removed in 0.4.0; `published_date` stays at the top level.
3438

3539
Use the client as a context manager when it owns the HTTP connection pool:
3640

‎openapi/search1api.openapi.json‎

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,16 @@
348348
"description": "When the page was published, as ISO 8601: `YYYY-MM-DD` when only the day is known (web search engines), or `YYYY-MM-DDTHH:MM:SSZ` in UTC when the source carries a time (x, reddit, github, arxiv, youtube, bilibili, wechat, news feeds). Omitted when the source exposes no date, so treat it as optional per result.",
349349
"example": "2026-09-03"
350350
},
351+
"metadata": {
352+
"$ref": "#/components/schemas/SearchResultMetadata"
353+
}
354+
},
355+
"additionalProperties": true
356+
},
357+
"SearchResultMetadata": {
358+
"type": "object",
359+
"description": "Optional engine-specific fields. Omitted when none apply. GitHub fields appear on POST /search; Hacker News fields appear on POST /news.",
360+
"properties": {
351361
"kind": {
352362
"type": "string",
353363
"enum": [
@@ -360,11 +370,11 @@
360370
},
361371
"stars": {
362372
"type": "integer",
363-
"description": "Stargazer count of a `github` repository result (`kind: \"repo\"`)."
373+
"description": "Stargazer count of a `github` repository result (`metadata.kind: \"repo\"`)."
364374
},
365375
"language": {
366376
"type": "string",
367-
"description": "Primary language of a `github` repository result (`kind: \"repo\"`). Omitted when GitHub reports none."
377+
"description": "Primary language of a `github` repository result (`metadata.kind: \"repo\"`). Omitted when GitHub reports none."
368378
},
369379
"num_comments": {
370380
"type": "integer",
@@ -373,7 +383,7 @@
373383
"story_url": {
374384
"type": "string",
375385
"format": "uri",
376-
"description": "POST /news with `search_service: \"hackernews\"` only: the submitted article. `link` is the Hacker News discussion thread; `story_url` is empty for Ask HN / Show HN text posts."
386+
"description": "POST /news with `search_service: \"hackernews\"` only: the submitted article. `link` is the Hacker News discussion thread; `metadata.story_url` is omitted for Ask HN / Show HN text posts."
377387
},
378388
"points": {
379389
"type": "integer",
@@ -631,7 +641,7 @@
631641
"properties": {
632642
"search_query": {
633643
"type": "string",
634-
"description": "Keywords sent to the engines, with platform and time phrases removed."
644+
"description": "Query sent to the first engine in `sources`. Each engine searches its own span of the request words, without platform names; the time window is applied as a filter."
635645
},
636646
"sources": {
637647
"type": "array",
@@ -652,7 +662,7 @@
652662
"year",
653663
null
654664
],
655-
"description": "Publication window applied, or null for none."
665+
"description": "Publication window applied, or null for none. year means the past 365 days, not the current calendar year."
656666
}
657667
}
658668
},

‎pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "search1api"
7-
version = "0.3.0"
7+
version = "0.4.0"
88
description = "Official Python client for Search1API"
99
readme = "README.md"
1010
license = { file = "LICENSE" }

‎src/search1api/__init__.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,4 @@
3535
"UnprocessableEntityError",
3636
]
3737

38-
__version__ = "0.3.0"
38+
__version__ = "0.4.0"

‎src/search1api/types.py‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,7 @@ class SearchResultRequired(TypedDict):
5454
snippet: str
5555

5656

57-
class SearchResult(SearchResultRequired, total=False):
58-
content: str
59-
published_date: str
57+
class SearchResultMetadata(TypedDict, total=False):
6058
kind: Literal["repo", "issue", "pr", "discussion"]
6159
stars: int
6260
language: str
@@ -65,6 +63,12 @@ class SearchResult(SearchResultRequired, total=False):
6563
story_url: str
6664

6765

66+
class SearchResult(SearchResultRequired, total=False):
67+
content: str
68+
published_date: str
69+
metadata: SearchResultMetadata
70+
71+
6872
class SearchResponseRequired(TypedDict):
6973
searchParameters: SearchRequest
7074
results: List[SearchResult]

‎tests/test_client.py‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,39 @@ def test_requires_api_key(monkeypatch):
2424
Search1API()
2525

2626

27+
@pytest.mark.parametrize("method", ["search", "news"])
28+
@pytest.mark.parametrize("asynchronous", [False, True])
29+
def test_preserves_nested_metadata(method, asynchronous):
30+
body = {
31+
"searchParameters": {"query": "q"},
32+
"results": [
33+
{
34+
"title": "Thread",
35+
"link": "https://example.com/thread",
36+
"snippet": "Discussion",
37+
"published_date": "2026-10-12",
38+
"metadata": {"num_comments": 0},
39+
}
40+
],
41+
}
42+
transport = httpx.MockTransport(lambda request: json_response(200, body, request))
43+
44+
async def run():
45+
async with httpx.AsyncClient(transport=transport) as http_client:
46+
async with AsyncSearch1API("test-key", client=http_client) as client:
47+
return await getattr(client, method)("q")
48+
49+
if asynchronous:
50+
response = asyncio.run(run())
51+
else:
52+
with httpx.Client(transport=transport) as http_client:
53+
with Search1API("test-key", client=http_client) as client:
54+
response = getattr(client, method)("q")
55+
assert response == body
56+
assert response["results"][0]["metadata"]["num_comments"] == 0
57+
assert "num_comments" not in response["results"][0]
58+
59+
2760
def test_rejects_invalid_retry_and_timeout_settings():
2861
with pytest.raises(Search1APIConfigurationError, match="max_retries"):
2962
Search1API("test-key", max_retries=-1)

0 commit comments

Comments
 (0)