search1api 0.2.1__tar.gz → 0.3.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: search1api
3
- Version: 0.2.1
3
+ Version: 0.3.0
4
4
  Summary: Official Python client for Search1API
5
5
  Project-URL: Homepage, https://s1.dev
6
6
  Project-URL: Documentation, https://s1.dev/docs/integrations/sdks
@@ -77,6 +77,12 @@ for result in response["results"]:
77
77
  print(result["title"], result["link"])
78
78
  ```
79
79
 
80
+ `search_service` selects one engine (`google` by default), for example `bing`,
81
+ `bingcn`, `yandex`, `reddit`, `github`, `arxiv`, `wikipedia`, or `grokipedia`.
82
+ `page` requests a later results page on engines with native pagination
83
+ (`bing`, `bingcn`, `baidu`, `grokipedia`). Results carry `published_date` when the source
84
+ exposes one.
85
+
80
86
  Use the client as a context manager when it owns the HTTP connection pool:
81
87
 
82
88
  ```python
@@ -93,6 +99,24 @@ async with AsyncSearch1API() as client:
93
99
  response = await client.search("latest AI agent frameworks")
94
100
  ```
95
101
 
102
+ ## Ask
103
+
104
+ `ask` sends a natural-language request and lets Search1API choose the engines
105
+ and time window. It returns at most 10 results ranked by relevance, and
106
+ `intent` reports what was searched:
107
+
108
+ ```python
109
+ answer = client.ask("What are developers saying about Bun 1.3 this month?")
110
+
111
+ print(answer["intent"]["sources"], answer["intent"]["time_range"])
112
+ for result in answer["results"]:
113
+ print(result["relevance"], result["source"], result["title"], result["link"])
114
+ ```
115
+
116
+ A completed request costs 5 credits. Ask is not available with pay-per-request
117
+ payments, and its default timeout is 45 seconds. Use `search` when you already
118
+ know which engine and keywords you want.
119
+
96
120
  ## Deepcrawl
97
121
 
98
122
  `deepcrawl` starts a task and waits for it to finish:
@@ -126,7 +150,21 @@ The clients also support news, crawl, sitemap, trending, extract, usage, and
126
150
  batch operations exposed by the Search1API HTTP API. Requests time out after
127
151
  30 seconds and retry `429` and transient `5xx` responses twice by default.
128
152
  Authentication, payment, and validation errors are never retried. Deepcrawl
129
- task creation is not retried automatically because it is not idempotent.
153
+ task creation and feedback are not retried automatically because they are not
154
+ idempotent.
155
+
156
+ ## Feedback
157
+
158
+ `feedback` reports a Search1API problem, missing capability, or confusing
159
+ documentation. It is free. Do not include credentials or personal data:
160
+
161
+ ```python
162
+ client.feedback(
163
+ "Results for this query have no publication dates",
164
+ category="feature_request",
165
+ request_id="the x-search1api-request-id of the original request",
166
+ )
167
+ ```
130
168
 
131
169
  ## Development
132
170
 
@@ -26,6 +26,12 @@ for result in response["results"]:
26
26
  print(result["title"], result["link"])
27
27
  ```
28
28
 
29
+ `search_service` selects one engine (`google` by default), for example `bing`,
30
+ `bingcn`, `yandex`, `reddit`, `github`, `arxiv`, `wikipedia`, or `grokipedia`.
31
+ `page` requests a later results page on engines with native pagination
32
+ (`bing`, `bingcn`, `baidu`, `grokipedia`). Results carry `published_date` when the source
33
+ exposes one.
34
+
29
35
  Use the client as a context manager when it owns the HTTP connection pool:
30
36
 
31
37
  ```python
@@ -42,6 +48,24 @@ async with AsyncSearch1API() as client:
42
48
  response = await client.search("latest AI agent frameworks")
43
49
  ```
44
50
 
51
+ ## Ask
52
+
53
+ `ask` sends a natural-language request and lets Search1API choose the engines
54
+ and time window. It returns at most 10 results ranked by relevance, and
55
+ `intent` reports what was searched:
56
+
57
+ ```python
58
+ answer = client.ask("What are developers saying about Bun 1.3 this month?")
59
+
60
+ print(answer["intent"]["sources"], answer["intent"]["time_range"])
61
+ for result in answer["results"]:
62
+ print(result["relevance"], result["source"], result["title"], result["link"])
63
+ ```
64
+
65
+ A completed request costs 5 credits. Ask is not available with pay-per-request
66
+ payments, and its default timeout is 45 seconds. Use `search` when you already
67
+ know which engine and keywords you want.
68
+
45
69
  ## Deepcrawl
46
70
 
47
71
  `deepcrawl` starts a task and waits for it to finish:
@@ -75,7 +99,21 @@ The clients also support news, crawl, sitemap, trending, extract, usage, and
75
99
  batch operations exposed by the Search1API HTTP API. Requests time out after
76
100
  30 seconds and retry `429` and transient `5xx` responses twice by default.
77
101
  Authentication, payment, and validation errors are never retried. Deepcrawl
78
- task creation is not retried automatically because it is not idempotent.
102
+ task creation and feedback are not retried automatically because they are not
103
+ idempotent.
104
+
105
+ ## Feedback
106
+
107
+ `feedback` reports a Search1API problem, missing capability, or confusing
108
+ documentation. It is free. Do not include credentials or personal data:
109
+
110
+ ```python
111
+ client.feedback(
112
+ "Results for this query have no publication dates",
113
+ category="feature_request",
114
+ request_id="the x-search1api-request-id of the original request",
115
+ )
116
+ ```
79
117
 
80
118
  ## Development
81
119
 
@@ -4,7 +4,7 @@
4
4
  "title": "Search1API",
5
5
  "description": "Web search, news, crawling, screenshot, and content extraction APIs for AI agents. Supports pay-per-request via MPP and x402 protocols.",
6
6
  "version": "1.0.0",
7
- "x-guidance": "Search1API provides web search, news aggregation, URL crawling, webpage screenshots, sitemap extraction, trending topics, content extraction, and deep crawling. All paid endpoints accept POST with a JSON body. Use POST /search with a \"query\" field for web search. Use POST /news with a \"query\" field for news. Use POST /crawl with a \"url\" field to crawl a page. Use POST /screenshot with a \"url\" field to render a PNG, JPEG, or WebP image. Use POST /sitemap with a \"url\" field to extract sitemap URLs. Use POST /trending with a \"search_service\" field for trends. Use POST /extract with a \"url\" field for structured content extraction. Use POST /deepcrawl with a \"url\" field for deep multi-page crawling."
7
+ "x-guidance": "Search1API provides web search, news aggregation, URL crawling, webpage screenshots, sitemap extraction, trending topics, content extraction, and deep crawling. All paid endpoints accept POST with a JSON body. Use POST /search with a \"query\" field for web search. Use POST /news with a \"query\" field for news. Use POST /ask with a natural-language \"query\" to have Search1API choose the engines and time window and return only relevant results (API key only). Use POST /crawl with a \"url\" field to crawl a page. Use POST /screenshot with a \"url\" field to render a PNG, JPEG, or WebP image. Use POST /sitemap with a \"url\" field to extract sitemap URLs. Use POST /trending with a \"search_service\" field for trends. Use POST /extract with a \"url\" field for structured content extraction. Use POST /deepcrawl with a \"url\" field for deep multi-page crawling."
8
8
  },
9
9
  "x-discovery": {
10
10
  "ownershipProofs": []
@@ -25,7 +25,7 @@
25
25
  "schemas": {
26
26
  "ApiError": {
27
27
  "type": "object",
28
- "description": "Search1API error. Payment challenges may use RFC 9457 problem detail fields.",
28
+ "description": "Search1API error. Every JSON error carries `ok: false`, `error` (a short status-derived label) and `message` (human-readable detail); validation failures add `errors`. Payment challenges may use RFC 9457 problem detail fields.",
29
29
  "properties": {
30
30
  "ok": {
31
31
  "type": "boolean",
@@ -84,8 +84,10 @@
84
84
  "enum": [
85
85
  "google",
86
86
  "bing",
87
+ "bingcn",
87
88
  "duckduckgo",
88
89
  "yahoo",
90
+ "yandex",
89
91
  "youtube",
90
92
  "x",
91
93
  "reddit",
@@ -95,8 +97,8 @@
95
97
  "bilibili",
96
98
  "imdb",
97
99
  "wikipedia",
100
+ "grokipedia",
98
101
  "",
99
- "sogou",
100
102
  "baidu",
101
103
  "360",
102
104
  "quark"
@@ -108,6 +110,12 @@
108
110
  "maximum": 50,
109
111
  "default": 5
110
112
  },
113
+ "page": {
114
+ "type": "integer",
115
+ "minimum": 1,
116
+ "maximum": 100,
117
+ "default": 1
118
+ },
111
119
  "crawl_results": {
112
120
  "type": "integer",
113
121
  "minimum": 0,
@@ -334,6 +342,42 @@
334
342
  },
335
343
  "content": {
336
344
  "type": "string"
345
+ },
346
+ "published_date": {
347
+ "type": "string",
348
+ "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.",
349
+ "example": "2026-09-03"
350
+ },
351
+ "kind": {
352
+ "type": "string",
353
+ "enum": [
354
+ "repo",
355
+ "issue",
356
+ "pr",
357
+ "discussion"
358
+ ],
359
+ "description": "What a `github` result is: a repository, an issue, a pull request, or a discussion. Only present for `search_service: \"github\"`."
360
+ },
361
+ "stars": {
362
+ "type": "integer",
363
+ "description": "Stargazer count of a `github` repository result (`kind: \"repo\"`)."
364
+ },
365
+ "language": {
366
+ "type": "string",
367
+ "description": "Primary language of a `github` repository result (`kind: \"repo\"`). Omitted when GitHub reports none."
368
+ },
369
+ "num_comments": {
370
+ "type": "integer",
371
+ "description": "Comment count on a `github` issue, pull request or discussion, or on a `hackernews` thread (POST /news)."
372
+ },
373
+ "story_url": {
374
+ "type": "string",
375
+ "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."
377
+ },
378
+ "points": {
379
+ "type": "integer",
380
+ "description": "POST /news with `search_service: \"hackernews\"` only: upvotes on the thread."
337
381
  }
338
382
  },
339
383
  "additionalProperties": true
@@ -526,6 +570,120 @@
526
570
  }
527
571
  }
528
572
  },
573
+ "AskResult": {
574
+ "type": "object",
575
+ "required": [
576
+ "title",
577
+ "link",
578
+ "snippet",
579
+ "source",
580
+ "relevance"
581
+ ],
582
+ "properties": {
583
+ "title": {
584
+ "type": "string"
585
+ },
586
+ "link": {
587
+ "type": "string",
588
+ "format": "uri"
589
+ },
590
+ "snippet": {
591
+ "type": "string"
592
+ },
593
+ "published_date": {
594
+ "type": "string",
595
+ "description": "When the page was published, as ISO 8601 (`YYYY-MM-DD` or `YYYY-MM-DDTHH:MM:SSZ`). Omitted when the engine exposes no date.",
596
+ "example": "2026-09-03"
597
+ },
598
+ "source": {
599
+ "type": "string",
600
+ "description": "Engine that returned the result."
601
+ },
602
+ "relevance": {
603
+ "type": "number",
604
+ "minimum": 0,
605
+ "maximum": 1,
606
+ "description": "How likely the result is to be about the query, two decimals. Only results scoring at least 0.5 are returned."
607
+ }
608
+ }
609
+ },
610
+ "AskResponse": {
611
+ "type": "object",
612
+ "required": [
613
+ "query",
614
+ "intent",
615
+ "results",
616
+ "errors"
617
+ ],
618
+ "properties": {
619
+ "query": {
620
+ "type": "string",
621
+ "description": "The query as sent."
622
+ },
623
+ "intent": {
624
+ "type": "object",
625
+ "required": [
626
+ "search_query",
627
+ "sources",
628
+ "time_range"
629
+ ],
630
+ "description": "How the request was interpreted and searched.",
631
+ "properties": {
632
+ "search_query": {
633
+ "type": "string",
634
+ "description": "Keywords sent to the engines, with platform and time phrases removed."
635
+ },
636
+ "sources": {
637
+ "type": "array",
638
+ "items": {
639
+ "type": "string"
640
+ },
641
+ "description": "Engines searched."
642
+ },
643
+ "time_range": {
644
+ "type": [
645
+ "string",
646
+ "null"
647
+ ],
648
+ "enum": [
649
+ "day",
650
+ "week",
651
+ "month",
652
+ "year",
653
+ null
654
+ ],
655
+ "description": "Publication window applied, or null for none."
656
+ }
657
+ }
658
+ },
659
+ "results": {
660
+ "type": "array",
661
+ "items": {
662
+ "$ref": "#/components/schemas/AskResult"
663
+ },
664
+ "description": "Relevant results from every engine, best first, with duplicates merged. May be empty."
665
+ },
666
+ "errors": {
667
+ "type": "array",
668
+ "description": "Engines that failed while others completed. The request is still charged.",
669
+ "items": {
670
+ "type": "object",
671
+ "required": [
672
+ "source",
673
+ "message"
674
+ ],
675
+ "properties": {
676
+ "source": {
677
+ "type": "string"
678
+ },
679
+ "message": {
680
+ "type": "string"
681
+ }
682
+ }
683
+ }
684
+ }
685
+ }
686
+ },
529
687
  "CrawlResult": {
530
688
  "type": "object",
531
689
  "required": [
@@ -771,10 +929,191 @@
771
929
  ]
772
930
  }
773
931
  }
932
+ },
933
+ "FeedbackResponse": {
934
+ "type": "object",
935
+ "required": [
936
+ "id",
937
+ "status"
938
+ ],
939
+ "properties": {
940
+ "id": {
941
+ "type": "string",
942
+ "pattern": "^fb_[a-zA-Z0-9_-]+$",
943
+ "maxLength": 80
944
+ },
945
+ "status": {
946
+ "type": "string",
947
+ "enum": [
948
+ "new"
949
+ ]
950
+ }
951
+ },
952
+ "additionalProperties": false
774
953
  }
775
954
  }
776
955
  },
777
956
  "paths": {
957
+ "/feedback": {
958
+ "post": {
959
+ "operationId": "feedback",
960
+ "summary": "Report a Search1API problem or feature request",
961
+ "description": "Report a Search1API problem, missing capability, or confusing documentation encountered during a task. Only message is required. Optional request_id refers to the original problem request. Do not include credentials, personal data, or private user content. Reports are stored in Agentback and linked to an opaque account reference. Do not report every empty result or retry the same report automatically; feedback should not block the main task. Returns 201 only after storage succeeds. Maximum JSON body size: 16 KiB. Limited to 10 submissions per account per minute at a Cloudflare location. Free to call.",
962
+ "tags": [
963
+ "Feedback"
964
+ ],
965
+ "responses": {
966
+ "201": {
967
+ "description": "Successful response",
968
+ "content": {
969
+ "application/json": {
970
+ "schema": {
971
+ "$ref": "#/components/schemas/FeedbackResponse"
972
+ }
973
+ }
974
+ }
975
+ },
976
+ "400": {
977
+ "description": "Bad Request",
978
+ "content": {
979
+ "application/json": {
980
+ "schema": {
981
+ "$ref": "#/components/schemas/ApiError"
982
+ }
983
+ }
984
+ }
985
+ },
986
+ "401": {
987
+ "description": "Unauthorized",
988
+ "content": {
989
+ "application/json": {
990
+ "schema": {
991
+ "$ref": "#/components/schemas/ApiError"
992
+ }
993
+ }
994
+ }
995
+ },
996
+ "403": {
997
+ "description": "Forbidden",
998
+ "content": {
999
+ "application/json": {
1000
+ "schema": {
1001
+ "$ref": "#/components/schemas/ApiError"
1002
+ }
1003
+ }
1004
+ }
1005
+ },
1006
+ "413": {
1007
+ "description": "Payload Too Large",
1008
+ "content": {
1009
+ "application/json": {
1010
+ "schema": {
1011
+ "$ref": "#/components/schemas/ApiError"
1012
+ }
1013
+ }
1014
+ }
1015
+ },
1016
+ "415": {
1017
+ "description": "Unsupported Media Type",
1018
+ "content": {
1019
+ "application/json": {
1020
+ "schema": {
1021
+ "$ref": "#/components/schemas/ApiError"
1022
+ }
1023
+ }
1024
+ }
1025
+ },
1026
+ "429": {
1027
+ "description": "Too Many Requests",
1028
+ "content": {
1029
+ "application/json": {
1030
+ "schema": {
1031
+ "$ref": "#/components/schemas/ApiError"
1032
+ }
1033
+ }
1034
+ }
1035
+ },
1036
+ "500": {
1037
+ "description": "Internal Server Error",
1038
+ "content": {
1039
+ "application/json": {
1040
+ "schema": {
1041
+ "$ref": "#/components/schemas/ApiError"
1042
+ }
1043
+ }
1044
+ }
1045
+ },
1046
+ "503": {
1047
+ "description": "Service Unavailable",
1048
+ "content": {
1049
+ "application/json": {
1050
+ "schema": {
1051
+ "$ref": "#/components/schemas/ApiError"
1052
+ }
1053
+ }
1054
+ }
1055
+ }
1056
+ },
1057
+ "security": [
1058
+ {
1059
+ "bearerAuth": []
1060
+ }
1061
+ ],
1062
+ "requestBody": {
1063
+ "required": true,
1064
+ "content": {
1065
+ "application/json": {
1066
+ "schema": {
1067
+ "type": "object",
1068
+ "properties": {
1069
+ "message": {
1070
+ "type": "string",
1071
+ "minLength": 1,
1072
+ "maxLength": 4000
1073
+ },
1074
+ "intent": {
1075
+ "type": "string",
1076
+ "maxLength": 2000
1077
+ },
1078
+ "category": {
1079
+ "type": "string",
1080
+ "enum": [
1081
+ "bug",
1082
+ "feature_request",
1083
+ "docs",
1084
+ "other"
1085
+ ],
1086
+ "default": "other"
1087
+ },
1088
+ "request_id": {
1089
+ "type": "string",
1090
+ "maxLength": 256
1091
+ },
1092
+ "agent": {
1093
+ "type": "object",
1094
+ "properties": {
1095
+ "name": {
1096
+ "type": "string",
1097
+ "maxLength": 128
1098
+ },
1099
+ "model": {
1100
+ "type": "string",
1101
+ "maxLength": 128
1102
+ }
1103
+ },
1104
+ "additionalProperties": false
1105
+ }
1106
+ },
1107
+ "required": [
1108
+ "message"
1109
+ ],
1110
+ "additionalProperties": false
1111
+ }
1112
+ }
1113
+ }
1114
+ }
1115
+ }
1116
+ },
778
1117
  "/screenshot": {
779
1118
  "post": {
780
1119
  "operationId": "screenshot",
@@ -1099,7 +1438,7 @@
1099
1438
  "post": {
1100
1439
  "operationId": "search",
1101
1440
  "summary": "Search the web using multiple search engines",
1102
- "description": "Search the live public web when the answer depends on current information, sources, or research a model's training data cannot cover. Returns ranked results with id, title, URL, and snippet across 13+ engines, with optional images. Set `crawl_results` to pull the top N result pages in the same call — each crawled page is billed as an additional crawl — or pass a result URL to POST /crawl separately. Costs 1 credit per request.",
1441
+ "description": "Search the live public web when the answer depends on current information, sources, or research a model's training data cannot cover. Returns ranked results with id, title, URL, snippet, and `published_date` (ISO 8601, when the engine exposes one) across 13+ engines, with optional images. Set `crawl_results` to pull the top N result pages in the same call — each crawled page is billed as an additional crawl — or pass a result URL to POST /crawl separately. Costs 1 credit per request.",
1103
1442
  "tags": [
1104
1443
  "Search"
1105
1444
  ],
@@ -1236,8 +1575,10 @@
1236
1575
  "enum": [
1237
1576
  "google",
1238
1577
  "bing",
1578
+ "bingcn",
1239
1579
  "duckduckgo",
1240
1580
  "yahoo",
1581
+ "yandex",
1241
1582
  "youtube",
1242
1583
  "x",
1243
1584
  "reddit",
@@ -1247,8 +1588,8 @@
1247
1588
  "bilibili",
1248
1589
  "imdb",
1249
1590
  "wikipedia",
1591
+ "grokipedia",
1250
1592
  "",
1251
- "sogou",
1252
1593
  "baidu",
1253
1594
  "360",
1254
1595
  "quark"
@@ -1260,6 +1601,12 @@
1260
1601
  "maximum": 50,
1261
1602
  "default": 5
1262
1603
  },
1604
+ "page": {
1605
+ "type": "integer",
1606
+ "minimum": 1,
1607
+ "maximum": 100,
1608
+ "default": 1
1609
+ },
1263
1610
  "crawl_results": {
1264
1611
  "type": "integer",
1265
1612
  "minimum": 0,
@@ -1317,8 +1664,10 @@
1317
1664
  "enum": [
1318
1665
  "google",
1319
1666
  "bing",
1667
+ "bingcn",
1320
1668
  "duckduckgo",
1321
1669
  "yahoo",
1670
+ "yandex",
1322
1671
  "youtube",
1323
1672
  "x",
1324
1673
  "reddit",
@@ -1328,8 +1677,8 @@
1328
1677
  "bilibili",
1329
1678
  "imdb",
1330
1679
  "wikipedia",
1680
+ "grokipedia",
1331
1681
  "",
1332
- "sogou",
1333
1682
  "baidu",
1334
1683
  "360",
1335
1684
  "quark"
@@ -1341,6 +1690,12 @@
1341
1690
  "maximum": 50,
1342
1691
  "default": 5
1343
1692
  },
1693
+ "page": {
1694
+ "type": "integer",
1695
+ "minimum": 1,
1696
+ "maximum": 100,
1697
+ "default": 1
1698
+ },
1344
1699
  "crawl_results": {
1345
1700
  "type": "integer",
1346
1701
  "minimum": 0,
@@ -1397,7 +1752,7 @@
1397
1752
  "post": {
1398
1753
  "operationId": "news",
1399
1754
  "summary": "Search news articles across multiple sources",
1400
- "description": "Search recent news when the question is about events, announcements, or coverage rather than reference material. Returns articles from verified publishers with title, URL, snippet, and optional full page content, filterable by site, language, and time range. For questions that are not time-sensitive, prefer POST /search. Costs 1 credit per request.",
1755
+ "description": "Search recent news when the question is about events, announcements, or coverage rather than reference material. Returns articles from verified publishers with title, URL, snippet, `published_date` (ISO 8601, when known), and optional full page content, filterable by site, language, and time range. For questions that are not time-sensitive, prefer POST /search. Costs 1 credit per request.",
1401
1756
  "tags": [
1402
1757
  "Search"
1403
1758
  ],
@@ -1669,11 +2024,129 @@
1669
2024
  }
1670
2025
  }
1671
2026
  },
2027
+ "/ask": {
2028
+ "post": {
2029
+ "operationId": "ask",
2030
+ "summary": "Search the engines a natural-language request calls for",
2031
+ "description": "Agentic search. Describe what you are looking for in plain language and let a decision model (currently TypeSafe Jev) decide where to look. It picks up to five engines (web search, Hacker News, Reddit, GitHub, X, arXiv, Wikipedia, IMDb, WeChat, YouTube), rewrites the request into search keywords, infers a publication window from phrases such as \"this week\", searches the engines in parallel, and returns only the results judged relevant, merged and ranked, so agents read fewer off-topic results. The request takes only `query`; engines, keywords, and the window are always chosen by the model, and the response reports them in `intent`. Use POST /search instead when you already know the engine and keywords and want its raw ranking. Costs 5 credits per request.",
2032
+ "tags": [
2033
+ "Search"
2034
+ ],
2035
+ "responses": {
2036
+ "200": {
2037
+ "description": "Successful response",
2038
+ "content": {
2039
+ "application/json": {
2040
+ "schema": {
2041
+ "$ref": "#/components/schemas/AskResponse"
2042
+ }
2043
+ }
2044
+ }
2045
+ },
2046
+ "400": {
2047
+ "description": "Bad Request",
2048
+ "content": {
2049
+ "application/json": {
2050
+ "schema": {
2051
+ "$ref": "#/components/schemas/ApiError"
2052
+ }
2053
+ }
2054
+ }
2055
+ },
2056
+ "401": {
2057
+ "description": "Unauthorized",
2058
+ "content": {
2059
+ "application/json": {
2060
+ "schema": {
2061
+ "$ref": "#/components/schemas/ApiError"
2062
+ }
2063
+ }
2064
+ }
2065
+ },
2066
+ "402": {
2067
+ "description": "Payment Required",
2068
+ "content": {
2069
+ "application/problem+json": {
2070
+ "schema": {
2071
+ "$ref": "#/components/schemas/ApiError"
2072
+ }
2073
+ }
2074
+ }
2075
+ },
2076
+ "429": {
2077
+ "description": "Too Many Requests",
2078
+ "content": {
2079
+ "application/json": {
2080
+ "schema": {
2081
+ "$ref": "#/components/schemas/ApiError"
2082
+ }
2083
+ }
2084
+ }
2085
+ },
2086
+ "500": {
2087
+ "description": "Internal Server Error",
2088
+ "content": {
2089
+ "application/json": {
2090
+ "schema": {
2091
+ "$ref": "#/components/schemas/ApiError"
2092
+ }
2093
+ }
2094
+ }
2095
+ },
2096
+ "502": {
2097
+ "description": "Bad Gateway",
2098
+ "content": {
2099
+ "application/json": {
2100
+ "schema": {
2101
+ "$ref": "#/components/schemas/ApiError"
2102
+ }
2103
+ }
2104
+ }
2105
+ }
2106
+ },
2107
+ "security": [
2108
+ {
2109
+ "bearerAuth": []
2110
+ }
2111
+ ],
2112
+ "requestBody": {
2113
+ "required": true,
2114
+ "content": {
2115
+ "application/json": {
2116
+ "schema": {
2117
+ "type": "object",
2118
+ "properties": {
2119
+ "query": {
2120
+ "type": "string",
2121
+ "minLength": 1,
2122
+ "maxLength": 500,
2123
+ "description": "What you are looking for, in natural language. Platform hints (\"on Reddit\", \"papers\") and time hints (\"this week\") steer engine and time-range selection."
2124
+ }
2125
+ },
2126
+ "required": [
2127
+ "query"
2128
+ ],
2129
+ "additionalProperties": false
2130
+ },
2131
+ "examples": {
2132
+ "community": {
2133
+ "summary": "Let Search1API choose",
2134
+ "description": "Engines and the time window are inferred from the wording: community discussion, within the past month.",
2135
+ "value": {
2136
+ "query": "What are developers saying about Bun 1.3 this month?"
2137
+ }
2138
+ }
2139
+ }
2140
+ }
2141
+ }
2142
+ }
2143
+ }
2144
+ },
1672
2145
  "/crawl": {
1673
2146
  "post": {
1674
2147
  "operationId": "crawl",
1675
2148
  "summary": "Crawl a URL and extract its content",
1676
- "description": "Fetch one public URL and return its readable title and body as clean text, with navigation, boilerplate, and scripts stripped. Use it on a URL the user supplied or one returned by POST /search. To ingest a whole site rather than a single page, use POST /deepcrawl. Costs 1 credit per request.",
2149
+ "description": "Fetch one public URL and return its readable title and body as clean text, with navigation, boilerplate, and scripts stripped. Use it on a URL the user supplied or one returned by POST /search. To ingest a whole site rather than a single page, use POST /deepcrawl. Send an array of request objects to crawl several URLs in one call: the response is an array containing only the URLs that succeeded, in no guaranteed order, so match items back to your input by `crawlParameters.url`; failed URLs are omitted and not charged. `enable_fallback` is accepted as a snake_case alias of `enableFallback`. Costs 1 credit per request.",
1677
2150
  "tags": [
1678
2151
  "Crawl"
1679
2152
  ],
@@ -1742,6 +2215,26 @@
1742
2215
  }
1743
2216
  }
1744
2217
  },
2218
+ "404": {
2219
+ "description": "Not Found",
2220
+ "content": {
2221
+ "application/json": {
2222
+ "schema": {
2223
+ "$ref": "#/components/schemas/ApiError"
2224
+ }
2225
+ }
2226
+ }
2227
+ },
2228
+ "410": {
2229
+ "description": "Gone",
2230
+ "content": {
2231
+ "application/json": {
2232
+ "schema": {
2233
+ "$ref": "#/components/schemas/ApiError"
2234
+ }
2235
+ }
2236
+ }
2237
+ },
1745
2238
  "422": {
1746
2239
  "description": "Validation Error",
1747
2240
  "content": {
@@ -1852,7 +2345,7 @@
1852
2345
  "post": {
1853
2346
  "operationId": "sitemap",
1854
2347
  "summary": "Extract sitemap URLs from a website",
1855
- "description": "Discover the public URLs of a site when you need to know which pages exist before fetching any of them — scoping a crawl, auditing coverage, or locating a section. Returns the links discovered for the given page or domain; it does not fetch their content. Costs 1 credit per request.",
2348
+ "description": "Discover the public URLs of a site when you need to know which pages exist before fetching any of them — scoping a crawl, auditing coverage, or locating a section. With the default `type: \"sitemap\"` this reads the site's own sitemap: `Sitemap:` directives in robots.txt first, then the conventional paths, following sitemap index files and gzipped sitemaps, and returns the URLs it declares for the requested host. A site that publishes no sitemap falls back to the links found on the given page. Use `type: \"all\"` to skip the sitemap and get every link on that one page instead, including links to other hosts. Neither mode fetches page content. Costs 1 credit per request.",
1856
2349
  "tags": [
1857
2350
  "Crawl"
1858
2351
  ],
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "search1api"
7
- version = "0.2.1"
7
+ version = "0.3.0"
8
8
  description = "Official Python client for Search1API"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -35,4 +35,4 @@ __all__ = [
35
35
  "UnprocessableEntityError",
36
36
  ]
37
37
 
38
- __version__ = "0.2.1"
38
+ __version__ = "0.3.0"
@@ -21,6 +21,7 @@ from .errors import (
21
21
  api_status_error,
22
22
  )
23
23
  from .types import (
24
+ AskResponse,
24
25
  BatchResponse,
25
26
  CrawlRequest,
26
27
  CrawlResponse,
@@ -28,6 +29,9 @@ from .types import (
28
29
  DeepcrawlAcceptedResponse,
29
30
  DeepcrawlStatusResponse,
30
31
  ExtractResponse,
32
+ FeedbackAgent,
33
+ FeedbackCategory,
34
+ FeedbackResponse,
31
35
  HealthResponse,
32
36
  NewsEngine,
33
37
  NewsRequest,
@@ -47,6 +51,9 @@ from .types import (
47
51
 
48
52
  DEFAULT_BASE_URL = "https://api.search1api.com"
49
53
  DEFAULT_TIMEOUT = 30.0
54
+ # The gateway gives Ask up to 35 seconds, so the default 30 seconds would abort
55
+ # requests the API would still answer.
56
+ DEFAULT_ASK_TIMEOUT = 45.0
50
57
  DEFAULT_MAX_RETRIES = 2
51
58
  DEFAULT_RETRY_DELAY = 0.5
52
59
  DEFAULT_DEEPCRAWL_POLL_INTERVAL = 2.0
@@ -62,6 +69,7 @@ def _search_payload(
62
69
  *,
63
70
  search_service: Optional[str] = None,
64
71
  max_results: Optional[int] = None,
72
+ page: Optional[int] = None,
65
73
  crawl_results: Optional[int] = None,
66
74
  image: Optional[bool] = None,
67
75
  include_sites: Optional[List[str]] = None,
@@ -74,6 +82,7 @@ def _search_payload(
74
82
  "query": query,
75
83
  "search_service": search_service,
76
84
  "max_results": max_results,
85
+ "page": page,
77
86
  "crawl_results": crawl_results,
78
87
  "image": image,
79
88
  "include_sites": include_sites,
@@ -84,6 +93,25 @@ def _search_payload(
84
93
  )
85
94
 
86
95
 
96
+ def _feedback_payload(
97
+ message: str,
98
+ *,
99
+ intent: Optional[str] = None,
100
+ category: Optional[FeedbackCategory] = None,
101
+ request_id: Optional[str] = None,
102
+ agent: Optional[FeedbackAgent] = None,
103
+ ) -> Dict[str, Any]:
104
+ return _compact(
105
+ {
106
+ "message": message,
107
+ "intent": intent,
108
+ "category": category,
109
+ "request_id": request_id,
110
+ "agent": _compact(cast(Mapping[str, Any], agent)) if agent else None,
111
+ }
112
+ )
113
+
114
+
87
115
  def _screenshot_payload(
88
116
  url: str,
89
117
  *,
@@ -179,7 +207,7 @@ class _ClientConfig:
179
207
  return {
180
208
  "Accept": accept,
181
209
  "Authorization": f"Bearer {self.api_key}",
182
- "X-Search1API-Client": "python/0.2.0",
210
+ "X-Search1API-Client": "python/0.3.0",
183
211
  **self.headers,
184
212
  }
185
213
 
@@ -229,6 +257,7 @@ class Search1API(_ClientConfig):
229
257
  *,
230
258
  search_service: Optional[SearchEngine] = None,
231
259
  max_results: Optional[int] = None,
260
+ page: Optional[int] = None,
232
261
  crawl_results: Optional[int] = None,
233
262
  image: Optional[bool] = None,
234
263
  include_sites: Optional[List[str]] = None,
@@ -240,6 +269,7 @@ class Search1API(_ClientConfig):
240
269
  query,
241
270
  search_service=search_service,
242
271
  max_results=max_results,
272
+ page=page,
243
273
  crawl_results=crawl_results,
244
274
  image=image,
245
275
  include_sites=include_sites,
@@ -281,6 +311,16 @@ class Search1API(_ClientConfig):
281
311
  def news_batch(self, requests: List[NewsRequest]) -> BatchResponse:
282
312
  return cast(BatchResponse, self._request_json("POST", "/news", json=requests))
283
313
 
314
+ def ask(self, query: str, *, timeout: float = DEFAULT_ASK_TIMEOUT) -> AskResponse:
315
+ """Agentic search: Search1API picks the engines and time window.
316
+
317
+ A completed request costs 5 credits.
318
+ """
319
+ return cast(
320
+ AskResponse,
321
+ self._request_json("POST", "/ask", json={"query": query}, timeout=timeout),
322
+ )
323
+
284
324
  def crawl(
285
325
  self, url: str, *, enable_fallback: Optional[bool] = None
286
326
  ) -> CrawlResponse:
@@ -442,6 +482,32 @@ class Search1API(_ClientConfig):
442
482
  def health(self) -> HealthResponse:
443
483
  return cast(HealthResponse, self._request_json("GET", "/health"))
444
484
 
485
+ def feedback(
486
+ self,
487
+ message: str,
488
+ *,
489
+ intent: Optional[str] = None,
490
+ category: Optional[FeedbackCategory] = None,
491
+ request_id: Optional[str] = None,
492
+ agent: Optional[FeedbackAgent] = None,
493
+ ) -> FeedbackResponse:
494
+ """Report a Search1API problem or missing capability. Free.
495
+
496
+ Never retried automatically: the API has no idempotency key, so a retry
497
+ after a lost response would file a duplicate report.
498
+ """
499
+ payload = _feedback_payload(
500
+ message,
501
+ intent=intent,
502
+ category=category,
503
+ request_id=request_id,
504
+ agent=agent,
505
+ )
506
+ return cast(
507
+ FeedbackResponse,
508
+ self._request_json("POST", "/feedback", json=payload, retryable=False),
509
+ )
510
+
445
511
  def _request_json(self, method: str, path: str, **kwargs: Any) -> Any:
446
512
  response = self._request(method, path, **kwargs)
447
513
  try:
@@ -454,6 +520,7 @@ class Search1API(_ClientConfig):
454
520
  def _request(self, method: str, path: str, **kwargs: Any) -> httpx.Response:
455
521
  retryable = bool(kwargs.pop("retryable", True))
456
522
  accept = str(kwargs.pop("accept", "application/json"))
523
+ timeout = float(kwargs.pop("timeout", self.timeout))
457
524
  max_retries = self.max_retries if retryable else 0
458
525
  for attempt in range(max_retries + 1):
459
526
  try:
@@ -461,7 +528,7 @@ class Search1API(_ClientConfig):
461
528
  method,
462
529
  f"{self.base_url}{path}",
463
530
  headers=self._request_headers(accept),
464
- timeout=self.timeout,
531
+ timeout=timeout,
465
532
  **kwargs,
466
533
  )
467
534
  except httpx.TimeoutException as exc:
@@ -469,7 +536,7 @@ class Search1API(_ClientConfig):
469
536
  time.sleep(self._retry_sleep(attempt))
470
537
  continue
471
538
  raise APITimeoutError(
472
- f"Search1API request timed out after {self.timeout:g}s"
539
+ f"Search1API request timed out after {timeout:g}s"
473
540
  ) from exc
474
541
  except httpx.RequestError as exc:
475
542
  if attempt < max_retries:
@@ -530,6 +597,7 @@ class AsyncSearch1API(_ClientConfig):
530
597
  *,
531
598
  search_service: Optional[SearchEngine] = None,
532
599
  max_results: Optional[int] = None,
600
+ page: Optional[int] = None,
533
601
  crawl_results: Optional[int] = None,
534
602
  image: Optional[bool] = None,
535
603
  include_sites: Optional[List[str]] = None,
@@ -546,6 +614,7 @@ class AsyncSearch1API(_ClientConfig):
546
614
  query,
547
615
  search_service=search_service,
548
616
  max_results=max_results,
617
+ page=page,
549
618
  crawl_results=crawl_results,
550
619
  image=image,
551
620
  include_sites=include_sites,
@@ -600,6 +669,20 @@ class AsyncSearch1API(_ClientConfig):
600
669
  await self._request_json("POST", "/news", json=requests),
601
670
  )
602
671
 
672
+ async def ask(
673
+ self, query: str, *, timeout: float = DEFAULT_ASK_TIMEOUT
674
+ ) -> AskResponse:
675
+ """Agentic search: Search1API picks the engines and time window.
676
+
677
+ A completed request costs 5 credits.
678
+ """
679
+ return cast(
680
+ AskResponse,
681
+ await self._request_json(
682
+ "POST", "/ask", json={"query": query}, timeout=timeout
683
+ ),
684
+ )
685
+
603
686
  async def crawl(
604
687
  self, url: str, *, enable_fallback: Optional[bool] = None
605
688
  ) -> CrawlResponse:
@@ -775,6 +858,34 @@ class AsyncSearch1API(_ClientConfig):
775
858
  async def health(self) -> HealthResponse:
776
859
  return cast(HealthResponse, await self._request_json("GET", "/health"))
777
860
 
861
+ async def feedback(
862
+ self,
863
+ message: str,
864
+ *,
865
+ intent: Optional[str] = None,
866
+ category: Optional[FeedbackCategory] = None,
867
+ request_id: Optional[str] = None,
868
+ agent: Optional[FeedbackAgent] = None,
869
+ ) -> FeedbackResponse:
870
+ """Report a Search1API problem or missing capability. Free.
871
+
872
+ Never retried automatically: the API has no idempotency key, so a retry
873
+ after a lost response would file a duplicate report.
874
+ """
875
+ payload = _feedback_payload(
876
+ message,
877
+ intent=intent,
878
+ category=category,
879
+ request_id=request_id,
880
+ agent=agent,
881
+ )
882
+ return cast(
883
+ FeedbackResponse,
884
+ await self._request_json(
885
+ "POST", "/feedback", json=payload, retryable=False
886
+ ),
887
+ )
888
+
778
889
  async def _request_json(self, method: str, path: str, **kwargs: Any) -> Any:
779
890
  response = await self._request(method, path, **kwargs)
780
891
  try:
@@ -787,6 +898,7 @@ class AsyncSearch1API(_ClientConfig):
787
898
  async def _request(self, method: str, path: str, **kwargs: Any) -> httpx.Response:
788
899
  retryable = bool(kwargs.pop("retryable", True))
789
900
  accept = str(kwargs.pop("accept", "application/json"))
901
+ timeout = float(kwargs.pop("timeout", self.timeout))
790
902
  max_retries = self.max_retries if retryable else 0
791
903
  for attempt in range(max_retries + 1):
792
904
  try:
@@ -794,7 +906,7 @@ class AsyncSearch1API(_ClientConfig):
794
906
  method,
795
907
  f"{self.base_url}{path}",
796
908
  headers=self._request_headers(accept),
797
- timeout=self.timeout,
909
+ timeout=timeout,
798
910
  **kwargs,
799
911
  )
800
912
  except httpx.TimeoutException as exc:
@@ -802,7 +914,7 @@ class AsyncSearch1API(_ClientConfig):
802
914
  await asyncio.sleep(self._retry_sleep(attempt))
803
915
  continue
804
916
  raise APITimeoutError(
805
- f"Search1API request timed out after {self.timeout:g}s"
917
+ f"Search1API request timed out after {timeout:g}s"
806
918
  ) from exc
807
919
  except httpx.RequestError as exc:
808
920
  if attempt < max_retries:
@@ -8,8 +8,10 @@ TimeRange = Literal["day", "week", "month", "year"]
8
8
  SearchEngine = Literal[
9
9
  "google",
10
10
  "bing",
11
+ "bingcn",
11
12
  "duckduckgo",
12
13
  "yahoo",
14
+ "yandex",
13
15
  "youtube",
14
16
  "x",
15
17
  "reddit",
@@ -19,7 +21,7 @@ SearchEngine = Literal[
19
21
  "bilibili",
20
22
  "imdb",
21
23
  "wikipedia",
22
- "sogou",
24
+ "grokipedia",
23
25
  "baidu",
24
26
  "360",
25
27
  "quark",
@@ -37,6 +39,7 @@ class SearchRequestRequired(TypedDict):
37
39
  class SearchRequest(SearchRequestRequired, total=False):
38
40
  search_service: SearchEngine
39
41
  max_results: int
42
+ page: int
40
43
  crawl_results: int
41
44
  image: bool
42
45
  include_sites: List[str]
@@ -53,6 +56,13 @@ class SearchResultRequired(TypedDict):
53
56
 
54
57
  class SearchResult(SearchResultRequired, total=False):
55
58
  content: str
59
+ published_date: str
60
+ kind: Literal["repo", "issue", "pr", "discussion"]
61
+ stars: int
62
+ language: str
63
+ num_comments: int
64
+ points: int
65
+ story_url: str
56
66
 
57
67
 
58
68
  class SearchResponseRequired(TypedDict):
@@ -82,6 +92,49 @@ class NewsRequest(NewsRequestRequired, total=False):
82
92
  time_range: TimeRange
83
93
 
84
94
 
95
+ class AskIntent(TypedDict):
96
+ search_query: str
97
+ sources: List[str]
98
+ time_range: Optional[TimeRange]
99
+
100
+
101
+ class AskResultRequired(TypedDict):
102
+ title: str
103
+ link: str
104
+ snippet: str
105
+ source: str
106
+ relevance: float
107
+
108
+
109
+ class AskResult(AskResultRequired, total=False):
110
+ published_date: str
111
+
112
+
113
+ class AskError(TypedDict):
114
+ source: str
115
+ message: str
116
+
117
+
118
+ class AskResponse(TypedDict):
119
+ query: str
120
+ intent: AskIntent
121
+ results: List[AskResult]
122
+ errors: List[AskError]
123
+
124
+
125
+ FeedbackCategory = Literal["bug", "feature_request", "docs", "other"]
126
+
127
+
128
+ class FeedbackAgent(TypedDict, total=False):
129
+ name: str
130
+ model: str
131
+
132
+
133
+ class FeedbackResponse(TypedDict):
134
+ id: str
135
+ status: Literal["new"]
136
+
137
+
85
138
  class BatchItemRequired(TypedDict):
86
139
  success: bool
87
140
  cost: int
@@ -215,6 +215,123 @@ def test_deepcrawl_task_creation_is_not_retried():
215
215
  http_client.close()
216
216
 
217
217
 
218
+ def test_search_sends_page_and_new_engines():
219
+ bodies = []
220
+
221
+ def handler(request):
222
+ bodies.append(json.loads(request.content))
223
+ return json_response(200, {"results": []}, request)
224
+
225
+ http_client = httpx.Client(transport=httpx.MockTransport(handler))
226
+ client = Search1API("test-key", client=http_client)
227
+
228
+ client.search("kubernetes", search_service="bingcn", page=2)
229
+ client.search("Alan Turing", search_service="grokipedia")
230
+
231
+ assert bodies == [
232
+ {"query": "kubernetes", "search_service": "bingcn", "page": 2},
233
+ {"query": "Alan Turing", "search_service": "grokipedia"},
234
+ ]
235
+ http_client.close()
236
+
237
+
238
+ def test_ask_sends_only_the_query_with_a_longer_timeout():
239
+ seen = []
240
+ body = {
241
+ "query": "recent papers on speculative decoding",
242
+ "intent": {
243
+ "search_query": "speculative decoding",
244
+ "sources": ["arxiv"],
245
+ "time_range": None,
246
+ },
247
+ "results": [
248
+ {
249
+ "title": "Speculative decoding survey",
250
+ "link": "https://arxiv.org/abs/1",
251
+ "snippet": "Survey",
252
+ "source": "arxiv",
253
+ "relevance": 0.88,
254
+ }
255
+ ],
256
+ "errors": [],
257
+ }
258
+
259
+ def handler(request):
260
+ seen.append(request)
261
+ return json_response(200, body, request)
262
+
263
+ http_client = httpx.Client(transport=httpx.MockTransport(handler))
264
+ client = Search1API("test-key", client=http_client)
265
+
266
+ assert client.ask(body["query"]) == body
267
+ request = seen[0]
268
+ assert request.method == "POST"
269
+ assert str(request.url) == "https://api.search1api.com/ask"
270
+ assert json.loads(request.content) == {"query": body["query"]}
271
+ assert request.extensions["timeout"]["read"] == 45.0
272
+ http_client.close()
273
+
274
+
275
+ def test_feedback_is_sent_once_without_retries():
276
+ calls = []
277
+
278
+ def handler(request):
279
+ calls.append(json.loads(request.content))
280
+ return json_response(503, {"ok": False, "message": "unavailable"}, request)
281
+
282
+ http_client = httpx.Client(transport=httpx.MockTransport(handler))
283
+ client = Search1API("test-key", client=http_client, max_retries=2, retry_delay=0)
284
+
285
+ with pytest.raises(InternalServerError, match="unavailable"):
286
+ client.feedback(
287
+ "Need publication dates",
288
+ category="feature_request",
289
+ request_id="req_1",
290
+ agent={"name": "codex"},
291
+ )
292
+
293
+ assert calls == [
294
+ {
295
+ "message": "Need publication dates",
296
+ "category": "feature_request",
297
+ "request_id": "req_1",
298
+ "agent": {"name": "codex"},
299
+ }
300
+ ]
301
+ http_client.close()
302
+
303
+
304
+ def test_async_ask_and_feedback():
305
+ async def run():
306
+ paths = []
307
+
308
+ async def handler(request):
309
+ paths.append(request.url.path)
310
+ if request.url.path == "/feedback":
311
+ return json_response(201, {"id": "fb_1", "status": "new"}, request)
312
+ return json_response(
313
+ 200,
314
+ {
315
+ "query": "q",
316
+ "intent": {"search_query": "q", "sources": [], "time_range": None},
317
+ "results": [],
318
+ "errors": [],
319
+ },
320
+ request,
321
+ )
322
+
323
+ http_client = httpx.AsyncClient(transport=httpx.MockTransport(handler))
324
+ client = AsyncSearch1API("test-key", client=http_client)
325
+ try:
326
+ assert (await client.ask("q"))["results"] == []
327
+ assert await client.feedback("docs typo") == {"id": "fb_1", "status": "new"}
328
+ finally:
329
+ await http_client.aclose()
330
+ assert paths == ["/ask", "/feedback"]
331
+
332
+ asyncio.run(run())
333
+
334
+
218
335
  def test_async_client_uses_the_same_api_shape():
219
336
  async def run():
220
337
  async def handler(request):
@@ -252,10 +369,12 @@ def test_sdk_covers_every_public_openapi_operation():
252
369
  )
253
370
 
254
371
  operation_methods = {
372
+ "ask": "ask",
255
373
  "crawl": "crawl",
256
374
  "deepcrawl": "start_deepcrawl",
257
375
  "deepcrawlStatus": "get_deepcrawl_status",
258
376
  "extract": "extract",
377
+ "feedback": "feedback",
259
378
  "health": "health",
260
379
  "news": "news",
261
380
  "search": "search",
File without changes
File without changes
File without changes