search1api 0.3.0__tar.gz → 0.5.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.
@@ -0,0 +1,9 @@
1
+ # Changelog
2
+
3
+ ## 0.5.0 - 2026-11-03
4
+
5
+ Adds `maps(query, ...)` on `Search1API` and `AsyncSearch1API` for `POST /maps`: one Google Maps page of up to 20 places with name, place ID and Maps link, plus address, phone, website, rating, review count, coordinates and opening hours when available. Keyword arguments: `country`, `language`, `location` (`latitude`, `longitude`, optional `zoom`), `page` (1-6, pages after the first need `location`) and `max_results`. 3 credits per completed page; API key or OAuth only. The bundled OpenAPI snapshot also records the 50-item batch limit for search, news and crawl.
6
+
7
+ ## 0.4.0 - 2026-10-12
8
+
9
+ 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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: search1api
3
- Version: 0.3.0
3
+ Version: 0.5.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
@@ -81,7 +81,11 @@ for result in response["results"]:
81
81
  `bingcn`, `yandex`, `reddit`, `github`, `arxiv`, `wikipedia`, or `grokipedia`.
82
82
  `page` requests a later results page on engines with native pagination
83
83
  (`bing`, `bingcn`, `baidu`, `grokipedia`). Results carry `published_date` when the source
84
- exposes one.
84
+ exposes one. Engine-specific fields are in each result's optional `metadata`:
85
+ GitHub uses `metadata.kind`, `metadata.stars`, `metadata.language`, and
86
+ `metadata.num_comments`; Hacker News news results use `metadata.story_url`,
87
+ `metadata.points`, and `metadata.num_comments`. The former top-level paths
88
+ are removed in 0.4.0; `published_date` stays at the top level.
85
89
 
86
90
  Use the client as a context manager when it owns the HTTP connection pool:
87
91
 
@@ -117,6 +121,28 @@ A completed request costs 5 credits. Ask is not available with pay-per-request
117
121
  payments, and its default timeout is 45 seconds. Use `search` when you already
118
122
  know which engine and keywords you want.
119
123
 
124
+ ## Maps
125
+
126
+ `maps` searches Google Maps and returns one page of up to 20 places. Each place
127
+ has a name, place ID and Maps link, plus address, phone, website, rating,
128
+ review count, coordinates and opening hours when available:
129
+
130
+ ```python
131
+ places = client.maps(
132
+ "coffee shops",
133
+ location={"latitude": 40.7455, "longitude": -74.0083, "zoom": 15},
134
+ max_results=10,
135
+ )
136
+
137
+ for place in places["results"]:
138
+ print(place["name"], place.get("rating"), place.get("address"))
139
+ ```
140
+
141
+ A completed page costs 3 credits, including a confirmed empty page.
142
+ `max_results` only trims the page and does not lower the price. Request `page`
143
+ 2-6 with the same query and `location` for more places; each page is billed
144
+ separately. Maps is not available with pay-per-request payments.
145
+
120
146
  ## Deepcrawl
121
147
 
122
148
  `deepcrawl` starts a task and waits for it to finish:
@@ -30,7 +30,11 @@ for result in response["results"]:
30
30
  `bingcn`, `yandex`, `reddit`, `github`, `arxiv`, `wikipedia`, or `grokipedia`.
31
31
  `page` requests a later results page on engines with native pagination
32
32
  (`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.
34
38
 
35
39
  Use the client as a context manager when it owns the HTTP connection pool:
36
40
 
@@ -66,6 +70,28 @@ A completed request costs 5 credits. Ask is not available with pay-per-request
66
70
  payments, and its default timeout is 45 seconds. Use `search` when you already
67
71
  know which engine and keywords you want.
68
72
 
73
+ ## Maps
74
+
75
+ `maps` searches Google Maps and returns one page of up to 20 places. Each place
76
+ has a name, place ID and Maps link, plus address, phone, website, rating,
77
+ review count, coordinates and opening hours when available:
78
+
79
+ ```python
80
+ places = client.maps(
81
+ "coffee shops",
82
+ location={"latitude": 40.7455, "longitude": -74.0083, "zoom": 15},
83
+ max_results=10,
84
+ )
85
+
86
+ for place in places["results"]:
87
+ print(place["name"], place.get("rating"), place.get("address"))
88
+ ```
89
+
90
+ A completed page costs 3 credits, including a confirmed empty page.
91
+ `max_results` only trims the page and does not lower the price. Request `page`
92
+ 2-6 with the same query and `location` for more places; each page is billed
93
+ separately. Maps is not available with pay-per-request payments.
94
+
69
95
  ## Deepcrawl
70
96
 
71
97
  `deepcrawl` starts a task and waits for it to finish:
@@ -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 /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."
7
+ "x-guidance": "Search1API provides web search, Maps place discovery, 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 /maps with a \"query\" and optional location coordinates to discover Google Maps places (API key only). 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": []
@@ -159,6 +159,212 @@
159
159
  ],
160
160
  "additionalProperties": false
161
161
  },
162
+ "MapsResponse": {
163
+ "type": "object",
164
+ "properties": {
165
+ "searchParameters": {
166
+ "type": "object",
167
+ "properties": {
168
+ "query": {
169
+ "type": "string",
170
+ "minLength": 1,
171
+ "maxLength": 1000,
172
+ "description": "Google Maps search phrase; leading and trailing whitespace is removed."
173
+ },
174
+ "country": {
175
+ "type": "string",
176
+ "pattern": "^[a-zA-Z]{2}$",
177
+ "default": "us",
178
+ "description": "Two-letter country code, normalized to lowercase. Defaults to us."
179
+ },
180
+ "language": {
181
+ "type": "string",
182
+ "pattern": "^[a-z]{2,3}(-[a-zA-Z]{2,4})?$",
183
+ "default": "en",
184
+ "description": "Result language, for example en, de or zh-CN. Defaults to en."
185
+ },
186
+ "location": {
187
+ "type": "object",
188
+ "properties": {
189
+ "latitude": {
190
+ "type": "number",
191
+ "minimum": -90,
192
+ "maximum": 90
193
+ },
194
+ "longitude": {
195
+ "type": "number",
196
+ "minimum": -180,
197
+ "maximum": 180
198
+ },
199
+ "zoom": {
200
+ "type": "number",
201
+ "minimum": 3,
202
+ "maximum": 30,
203
+ "default": 14
204
+ }
205
+ },
206
+ "required": [
207
+ "latitude",
208
+ "longitude"
209
+ ],
210
+ "additionalProperties": false,
211
+ "description": "Search origin coordinates and map zoom (default 14). Required for page > 1. Latitude must be -90 to 90, longitude -180 to 180, and zoom 3 to 30. Example: {\"latitude\":40.7455,\"longitude\":-74.0083,\"zoom\":15}."
212
+ },
213
+ "page": {
214
+ "type": "integer",
215
+ "minimum": 1,
216
+ "maximum": 6,
217
+ "default": 1,
218
+ "description": "Page number, 1–6; default 1. Accepts an integer or canonical integer string such as \"2\". Empty strings use the default. Pages after the first require location."
219
+ },
220
+ "max_results": {
221
+ "type": "integer",
222
+ "minimum": 1,
223
+ "maximum": 20,
224
+ "default": 20,
225
+ "description": "Maximum places to return from this page, 1–20; default 20. Accepts an integer or canonical integer string such as \"10\". Empty strings use the default. Truncation does not reduce the page charge or carry places to the next page."
226
+ }
227
+ },
228
+ "required": [
229
+ "query"
230
+ ],
231
+ "additionalProperties": false
232
+ },
233
+ "results": {
234
+ "type": "array",
235
+ "items": {
236
+ "type": "object",
237
+ "properties": {
238
+ "position": {
239
+ "type": "integer",
240
+ "minimum": 1,
241
+ "description": "Position in the requested native page, offset by earlier page capacities. Gaps can remain after filtering or deduplication; this is not a stable SEO rank."
242
+ },
243
+ "name": {
244
+ "type": "string",
245
+ "minLength": 1,
246
+ "description": "Business or place name."
247
+ },
248
+ "place_id": {
249
+ "type": "string",
250
+ "minLength": 1,
251
+ "description": "Google Maps place identifier, also used to deduplicate this page."
252
+ },
253
+ "maps_url": {
254
+ "type": "string",
255
+ "format": "uri",
256
+ "description": "Public Google Maps link for this place."
257
+ },
258
+ "address": {
259
+ "type": "string"
260
+ },
261
+ "phone": {
262
+ "type": "string"
263
+ },
264
+ "website": {
265
+ "type": "string",
266
+ "format": "uri"
267
+ },
268
+ "thumbnail": {
269
+ "type": "string",
270
+ "format": "uri"
271
+ },
272
+ "category": {
273
+ "type": "string"
274
+ },
275
+ "categories": {
276
+ "type": "array",
277
+ "items": {
278
+ "type": "string"
279
+ }
280
+ },
281
+ "rating": {
282
+ "type": "number",
283
+ "minimum": 0,
284
+ "maximum": 5
285
+ },
286
+ "review_count": {
287
+ "type": "integer",
288
+ "minimum": 0,
289
+ "maximum": 9007199254740991
290
+ },
291
+ "coordinates": {
292
+ "type": "object",
293
+ "properties": {
294
+ "latitude": {
295
+ "type": "number",
296
+ "minimum": -90,
297
+ "maximum": 90
298
+ },
299
+ "longitude": {
300
+ "type": "number",
301
+ "minimum": -180,
302
+ "maximum": 180
303
+ }
304
+ },
305
+ "required": [
306
+ "latitude",
307
+ "longitude"
308
+ ],
309
+ "additionalProperties": false
310
+ },
311
+ "opening_hours": {
312
+ "type": "object",
313
+ "properties": {
314
+ "monday": {
315
+ "type": "string"
316
+ },
317
+ "tuesday": {
318
+ "type": "string"
319
+ },
320
+ "wednesday": {
321
+ "type": "string"
322
+ },
323
+ "thursday": {
324
+ "type": "string"
325
+ },
326
+ "friday": {
327
+ "type": "string"
328
+ },
329
+ "saturday": {
330
+ "type": "string"
331
+ },
332
+ "sunday": {
333
+ "type": "string"
334
+ }
335
+ },
336
+ "additionalProperties": false,
337
+ "description": "Opening hours by weekday, as displayed by the source; omitted when unavailable."
338
+ },
339
+ "open_state": {
340
+ "type": "string",
341
+ "description": "Source display text for the current opening state, not a normalized boolean."
342
+ },
343
+ "price": {
344
+ "type": "string",
345
+ "description": "Source display text for the business price range, not a numeric amount or normalized currency."
346
+ },
347
+ "description": {
348
+ "type": "string"
349
+ }
350
+ },
351
+ "required": [
352
+ "position",
353
+ "name",
354
+ "place_id",
355
+ "maps_url"
356
+ ],
357
+ "additionalProperties": false
358
+ },
359
+ "maxItems": 20
360
+ }
361
+ },
362
+ "required": [
363
+ "searchParameters",
364
+ "results"
365
+ ],
366
+ "additionalProperties": false
367
+ },
162
368
  "NewsParameters": {
163
369
  "type": "object",
164
370
  "properties": {
@@ -348,6 +554,16 @@
348
554
  "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
555
  "example": "2026-09-03"
350
556
  },
557
+ "metadata": {
558
+ "$ref": "#/components/schemas/SearchResultMetadata"
559
+ }
560
+ },
561
+ "additionalProperties": true
562
+ },
563
+ "SearchResultMetadata": {
564
+ "type": "object",
565
+ "description": "Optional engine-specific fields. Omitted when none apply. GitHub and Reddit fields appear on POST /search; Hacker News fields appear on POST /news.",
566
+ "properties": {
351
567
  "kind": {
352
568
  "type": "string",
353
569
  "enum": [
@@ -360,20 +576,20 @@
360
576
  },
361
577
  "stars": {
362
578
  "type": "integer",
363
- "description": "Stargazer count of a `github` repository result (`kind: \"repo\"`)."
579
+ "description": "Stargazer count of a `github` repository result (`metadata.kind: \"repo\"`)."
364
580
  },
365
581
  "language": {
366
582
  "type": "string",
367
- "description": "Primary language of a `github` repository result (`kind: \"repo\"`). Omitted when GitHub reports none."
583
+ "description": "Primary language of a `github` repository result (`metadata.kind: \"repo\"`). Omitted when GitHub reports none."
368
584
  },
369
585
  "num_comments": {
370
586
  "type": "integer",
371
- "description": "Comment count on a `github` issue, pull request or discussion, or on a `hackernews` thread (POST /news)."
587
+ "description": "Comment count on a `github` issue, pull request or discussion, on a `reddit` post, or on a `hackernews` thread (POST /news)."
372
588
  },
373
589
  "story_url": {
374
590
  "type": "string",
375
591
  "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."
592
+ "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."
377
593
  },
378
594
  "points": {
379
595
  "type": "integer",
@@ -631,7 +847,7 @@
631
847
  "properties": {
632
848
  "search_query": {
633
849
  "type": "string",
634
- "description": "Keywords sent to the engines, with platform and time phrases removed."
850
+ "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."
635
851
  },
636
852
  "sources": {
637
853
  "type": "array",
@@ -652,7 +868,7 @@
652
868
  "year",
653
869
  null
654
870
  ],
655
- "description": "Publication window applied, or null for none."
871
+ "description": "Publication window applied, or null for none. year means the past 365 days, not the current calendar year."
656
872
  }
657
873
  }
658
874
  },
@@ -954,6 +1170,189 @@
954
1170
  }
955
1171
  },
956
1172
  "paths": {
1173
+ "/maps": {
1174
+ "post": {
1175
+ "operationId": "maps",
1176
+ "summary": "Search Google Maps for local businesses and places",
1177
+ "description": "Discover businesses and places with names, place IDs and public Maps links, plus addresses, contact details, ratings, review counts, coordinates and opening hours when available. Optional result fields are omitted when unavailable. Use POST /search for webpages and POST /crawl to read a business website.\n\nEach request fetches one Google Maps page of up to 20 places. page is 1-based (1–6); page > 1 requires location coordinates. max_results only truncates that page and does not change the price or carry unused places into the next page. Send the same query, country, language and location with the next page number for another separately billed request. There is no automatic pagination, retry or filling from another engine. Results can change between requests; an empty or short page does not guarantee that no later results exist.\n\nThe response contains searchParameters and results, following the single-request format of POST /search and POST /news. searchParameters includes the requested page and normalized defaults. Result positions preserve native page order with possible gaps, not a stable SEO rank. A short or empty results array does not assert a total count or a next page.\n\nA confirmed empty page is completed and charged. Technical failures return an error and release the credit reservation. GET /usage separately returns the remaining account balance.\n\nAccount credits only; no batch requests or payment protocols. Unknown fields are rejected; validation failures return 422 with errors entries containing field, message and code. Normal account limits apply (200 requests per minute). Set your client timeout above 20 seconds. SDK and MCP methods are not included in this release. Costs 3 credits per request.",
1178
+ "tags": [
1179
+ "Maps"
1180
+ ],
1181
+ "responses": {
1182
+ "200": {
1183
+ "description": "Successful response",
1184
+ "content": {
1185
+ "application/json": {
1186
+ "schema": {
1187
+ "$ref": "#/components/schemas/MapsResponse"
1188
+ }
1189
+ }
1190
+ }
1191
+ },
1192
+ "400": {
1193
+ "description": "Bad Request",
1194
+ "content": {
1195
+ "application/json": {
1196
+ "schema": {
1197
+ "$ref": "#/components/schemas/ApiError"
1198
+ }
1199
+ }
1200
+ }
1201
+ },
1202
+ "401": {
1203
+ "description": "Unauthorized",
1204
+ "content": {
1205
+ "application/json": {
1206
+ "schema": {
1207
+ "$ref": "#/components/schemas/ApiError"
1208
+ }
1209
+ }
1210
+ }
1211
+ },
1212
+ "402": {
1213
+ "description": "Payment Required",
1214
+ "content": {
1215
+ "application/problem+json": {
1216
+ "schema": {
1217
+ "$ref": "#/components/schemas/ApiError"
1218
+ }
1219
+ }
1220
+ }
1221
+ },
1222
+ "422": {
1223
+ "description": "Validation Error",
1224
+ "content": {
1225
+ "application/json": {
1226
+ "schema": {
1227
+ "$ref": "#/components/schemas/ApiError"
1228
+ }
1229
+ }
1230
+ }
1231
+ },
1232
+ "429": {
1233
+ "description": "Too Many Requests",
1234
+ "content": {
1235
+ "application/json": {
1236
+ "schema": {
1237
+ "$ref": "#/components/schemas/ApiError"
1238
+ }
1239
+ }
1240
+ }
1241
+ },
1242
+ "500": {
1243
+ "description": "Internal Server Error",
1244
+ "content": {
1245
+ "application/json": {
1246
+ "schema": {
1247
+ "$ref": "#/components/schemas/ApiError"
1248
+ }
1249
+ }
1250
+ }
1251
+ },
1252
+ "502": {
1253
+ "description": "Bad Gateway",
1254
+ "content": {
1255
+ "application/json": {
1256
+ "schema": {
1257
+ "$ref": "#/components/schemas/ApiError"
1258
+ }
1259
+ }
1260
+ }
1261
+ },
1262
+ "503": {
1263
+ "description": "Service Unavailable",
1264
+ "content": {
1265
+ "application/json": {
1266
+ "schema": {
1267
+ "$ref": "#/components/schemas/ApiError"
1268
+ }
1269
+ }
1270
+ }
1271
+ }
1272
+ },
1273
+ "security": [
1274
+ {
1275
+ "bearerAuth": []
1276
+ }
1277
+ ],
1278
+ "requestBody": {
1279
+ "required": true,
1280
+ "content": {
1281
+ "application/json": {
1282
+ "schema": {
1283
+ "type": "object",
1284
+ "properties": {
1285
+ "query": {
1286
+ "type": "string",
1287
+ "minLength": 1,
1288
+ "maxLength": 1000,
1289
+ "description": "Google Maps search phrase; leading and trailing whitespace is removed.",
1290
+ "example": "coffee in Manhattan"
1291
+ },
1292
+ "country": {
1293
+ "type": "string",
1294
+ "pattern": "^[a-zA-Z]{2}$",
1295
+ "default": "us",
1296
+ "description": "Two-letter country code, normalized to lowercase. Defaults to us."
1297
+ },
1298
+ "language": {
1299
+ "type": "string",
1300
+ "pattern": "^[a-z]{2,3}(-[a-zA-Z]{2,4})?$",
1301
+ "default": "en",
1302
+ "description": "Result language, for example en, de or zh-CN. Defaults to en."
1303
+ },
1304
+ "location": {
1305
+ "type": "object",
1306
+ "properties": {
1307
+ "latitude": {
1308
+ "type": "number",
1309
+ "minimum": -90,
1310
+ "maximum": 90
1311
+ },
1312
+ "longitude": {
1313
+ "type": "number",
1314
+ "minimum": -180,
1315
+ "maximum": 180
1316
+ },
1317
+ "zoom": {
1318
+ "type": "number",
1319
+ "minimum": 3,
1320
+ "maximum": 30,
1321
+ "default": 14
1322
+ }
1323
+ },
1324
+ "required": [
1325
+ "latitude",
1326
+ "longitude"
1327
+ ],
1328
+ "additionalProperties": false,
1329
+ "description": "Search origin coordinates and map zoom (default 14). Required for page > 1. Latitude must be -90 to 90, longitude -180 to 180, and zoom 3 to 30. Example: {\"latitude\":40.7455,\"longitude\":-74.0083,\"zoom\":15}."
1330
+ },
1331
+ "page": {
1332
+ "type": "integer",
1333
+ "minimum": 1,
1334
+ "maximum": 6,
1335
+ "default": 1,
1336
+ "description": "Page number, 1–6; default 1. Accepts an integer or canonical integer string such as \"2\". Empty strings use the default. Pages after the first require location."
1337
+ },
1338
+ "max_results": {
1339
+ "type": "integer",
1340
+ "minimum": 1,
1341
+ "maximum": 20,
1342
+ "default": 20,
1343
+ "description": "Maximum places to return from this page, 1–20; default 20. Accepts an integer or canonical integer string such as \"10\". Empty strings use the default. Truncation does not reduce the page charge or carry places to the next page."
1344
+ }
1345
+ },
1346
+ "required": [
1347
+ "query"
1348
+ ],
1349
+ "additionalProperties": false
1350
+ }
1351
+ }
1352
+ }
1353
+ }
1354
+ }
1355
+ },
957
1356
  "/feedback": {
958
1357
  "post": {
959
1358
  "operationId": "feedback",
@@ -1739,7 +2138,8 @@
1739
2138
  ],
1740
2139
  "additionalProperties": false
1741
2140
  },
1742
- "minItems": 1
2141
+ "minItems": 1,
2142
+ "maxItems": 50
1743
2143
  }
1744
2144
  ]
1745
2145
  }
@@ -2015,7 +2415,8 @@
2015
2415
  ],
2016
2416
  "additionalProperties": false
2017
2417
  },
2018
- "minItems": 1
2418
+ "minItems": 1,
2419
+ "maxItems": 50
2019
2420
  }
2020
2421
  ]
2021
2422
  }
@@ -2332,7 +2733,8 @@
2332
2733
  ],
2333
2734
  "additionalProperties": false
2334
2735
  },
2335
- "minItems": 1
2736
+ "minItems": 1,
2737
+ "maxItems": 50
2336
2738
  }
2337
2739
  ]
2338
2740
  }
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "search1api"
7
- version = "0.3.0"
7
+ version = "0.5.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.3.0"
38
+ __version__ = "0.5.0"
@@ -33,6 +33,8 @@ from .types import (
33
33
  FeedbackCategory,
34
34
  FeedbackResponse,
35
35
  HealthResponse,
36
+ MapsLocation,
37
+ MapsResponse,
36
38
  NewsEngine,
37
39
  NewsRequest,
38
40
  NewsResponse,
@@ -64,6 +66,27 @@ def _compact(payload: Mapping[str, Any]) -> Dict[str, Any]:
64
66
  return {key: value for key, value in payload.items() if value is not None}
65
67
 
66
68
 
69
+ def _maps_payload(
70
+ query: str,
71
+ *,
72
+ country: Optional[str],
73
+ language: Optional[str],
74
+ location: Optional[MapsLocation],
75
+ page: Optional[int],
76
+ max_results: Optional[int],
77
+ ) -> Dict[str, Any]:
78
+ return _compact(
79
+ {
80
+ "query": query,
81
+ "country": country,
82
+ "language": language,
83
+ "location": dict(location) if location is not None else None,
84
+ "page": page,
85
+ "max_results": max_results,
86
+ }
87
+ )
88
+
89
+
67
90
  def _search_payload(
68
91
  query: str,
69
92
  *,
@@ -321,6 +344,30 @@ class Search1API(_ClientConfig):
321
344
  self._request_json("POST", "/ask", json={"query": query}, timeout=timeout),
322
345
  )
323
346
 
347
+ def maps(
348
+ self,
349
+ query: str,
350
+ *,
351
+ country: Optional[str] = None,
352
+ language: Optional[str] = None,
353
+ location: Optional[MapsLocation] = None,
354
+ page: Optional[int] = None,
355
+ max_results: Optional[int] = None,
356
+ ) -> MapsResponse:
357
+ """Google Maps place search: one page of up to 20 places.
358
+
359
+ A completed page costs 3 credits. Pages after the first need ``location``.
360
+ """
361
+ payload = _maps_payload(
362
+ query,
363
+ country=country,
364
+ language=language,
365
+ location=location,
366
+ page=page,
367
+ max_results=max_results,
368
+ )
369
+ return cast(MapsResponse, self._request_json("POST", "/maps", json=payload))
370
+
324
371
  def crawl(
325
372
  self, url: str, *, enable_fallback: Optional[bool] = None
326
373
  ) -> CrawlResponse:
@@ -683,6 +730,36 @@ class AsyncSearch1API(_ClientConfig):
683
730
  ),
684
731
  )
685
732
 
733
+ async def maps(
734
+ self,
735
+ query: str,
736
+ *,
737
+ country: Optional[str] = None,
738
+ language: Optional[str] = None,
739
+ location: Optional[MapsLocation] = None,
740
+ page: Optional[int] = None,
741
+ max_results: Optional[int] = None,
742
+ ) -> MapsResponse:
743
+ """Google Maps place search: one page of up to 20 places.
744
+
745
+ A completed page costs 3 credits. Pages after the first need ``location``.
746
+ """
747
+ return cast(
748
+ MapsResponse,
749
+ await self._request_json(
750
+ "POST",
751
+ "/maps",
752
+ json=_maps_payload(
753
+ query,
754
+ country=country,
755
+ language=language,
756
+ location=location,
757
+ page=page,
758
+ max_results=max_results,
759
+ ),
760
+ ),
761
+ )
762
+
686
763
  async def crawl(
687
764
  self, url: str, *, enable_fallback: Optional[bool] = None
688
765
  ) -> CrawlResponse:
@@ -54,9 +54,7 @@ class SearchResultRequired(TypedDict):
54
54
  snippet: str
55
55
 
56
56
 
57
- class SearchResult(SearchResultRequired, total=False):
58
- content: str
59
- published_date: str
57
+ class SearchResultMetadata(TypedDict, total=False):
60
58
  kind: Literal["repo", "issue", "pr", "discussion"]
61
59
  stars: int
62
60
  language: str
@@ -65,6 +63,12 @@ class SearchResult(SearchResultRequired, total=False):
65
63
  story_url: str
66
64
 
67
65
 
66
+ class SearchResult(SearchResultRequired, total=False):
67
+ content: str
68
+ published_date: str
69
+ metadata: SearchResultMetadata
70
+
71
+
68
72
  class SearchResponseRequired(TypedDict):
69
73
  searchParameters: SearchRequest
70
74
  results: List[SearchResult]
@@ -122,6 +126,70 @@ class AskResponse(TypedDict):
122
126
  errors: List[AskError]
123
127
 
124
128
 
129
+ class MapsLocationRequired(TypedDict):
130
+ latitude: float
131
+ longitude: float
132
+
133
+
134
+ class MapsLocation(MapsLocationRequired, total=False):
135
+ zoom: float
136
+
137
+
138
+ class MapsCoordinates(TypedDict):
139
+ latitude: float
140
+ longitude: float
141
+
142
+
143
+ class MapsOpeningHours(TypedDict, total=False):
144
+ monday: str
145
+ tuesday: str
146
+ wednesday: str
147
+ thursday: str
148
+ friday: str
149
+ saturday: str
150
+ sunday: str
151
+
152
+
153
+ class MapsPlaceRequired(TypedDict):
154
+ position: int
155
+ name: str
156
+ place_id: str
157
+ maps_url: str
158
+
159
+
160
+ class MapsPlace(MapsPlaceRequired, total=False):
161
+ address: str
162
+ phone: str
163
+ website: str
164
+ thumbnail: str
165
+ category: str
166
+ categories: List[str]
167
+ rating: float
168
+ review_count: int
169
+ coordinates: MapsCoordinates
170
+ opening_hours: MapsOpeningHours
171
+ open_state: str
172
+ price: str
173
+ description: str
174
+
175
+
176
+ class MapsParametersRequired(TypedDict):
177
+ query: str
178
+ country: str
179
+ language: str
180
+ page: int
181
+ max_results: int
182
+
183
+
184
+ class MapsParameters(MapsParametersRequired, total=False):
185
+ location: MapsLocation
186
+
187
+
188
+ class MapsResponse(TypedDict):
189
+ searchParameters: MapsParameters
190
+ results: List[MapsPlace]
191
+
192
+
125
193
  FeedbackCategory = Literal["bug", "feature_request", "docs", "other"]
126
194
 
127
195
 
@@ -24,6 +24,39 @@ def test_requires_api_key(monkeypatch):
24
24
  Search1API()
25
25
 
26
26
 
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
+
27
60
  def test_rejects_invalid_retry_and_timeout_settings():
28
61
  with pytest.raises(Search1APIConfigurationError, match="max_retries"):
29
62
  Search1API("test-key", max_retries=-1)
@@ -272,6 +305,82 @@ def test_ask_sends_only_the_query_with_a_longer_timeout():
272
305
  http_client.close()
273
306
 
274
307
 
308
+ def test_maps_sends_location_and_paging():
309
+ seen = []
310
+ body = {
311
+ "searchParameters": {
312
+ "query": "coffee shops",
313
+ "country": "us",
314
+ "language": "en",
315
+ "location": {"latitude": 40.7455, "longitude": -74.0083, "zoom": 15},
316
+ "page": 2,
317
+ "max_results": 5,
318
+ },
319
+ "results": [
320
+ {
321
+ "position": 21,
322
+ "name": "Example Coffee",
323
+ "place_id": "ChIJ123",
324
+ "maps_url": "https://www.google.com/maps/search/?api=1&query=Example",
325
+ "rating": 4.6,
326
+ }
327
+ ],
328
+ }
329
+
330
+ def handler(request):
331
+ seen.append(request)
332
+ return json_response(200, body, request)
333
+
334
+ http_client = httpx.Client(transport=httpx.MockTransport(handler))
335
+ client = Search1API("test-key", client=http_client)
336
+
337
+ response = client.maps(
338
+ "coffee shops",
339
+ location={"latitude": 40.7455, "longitude": -74.0083, "zoom": 15},
340
+ page=2,
341
+ max_results=5,
342
+ )
343
+
344
+ assert response == body
345
+ assert str(seen[0].url) == "https://api.search1api.com/maps"
346
+ assert json.loads(seen[0].content) == {
347
+ "query": "coffee shops",
348
+ "location": {"latitude": 40.7455, "longitude": -74.0083, "zoom": 15},
349
+ "page": 2,
350
+ "max_results": 5,
351
+ }
352
+ http_client.close()
353
+
354
+
355
+ def test_async_maps():
356
+ async def run():
357
+ async def handler(request):
358
+ assert json.loads(request.content) == {"query": "ramen", "country": "jp"}
359
+ return json_response(
360
+ 200,
361
+ {
362
+ "searchParameters": {
363
+ "query": "ramen",
364
+ "country": "jp",
365
+ "language": "en",
366
+ "page": 1,
367
+ "max_results": 20,
368
+ },
369
+ "results": [],
370
+ },
371
+ request,
372
+ )
373
+
374
+ http_client = httpx.AsyncClient(transport=httpx.MockTransport(handler))
375
+ client = AsyncSearch1API("test-key", client=http_client)
376
+ try:
377
+ assert (await client.maps("ramen", country="jp"))["results"] == []
378
+ finally:
379
+ await http_client.aclose()
380
+
381
+ asyncio.run(run())
382
+
383
+
275
384
  def test_feedback_is_sent_once_without_retries():
276
385
  calls = []
277
386
 
@@ -376,6 +485,7 @@ def test_sdk_covers_every_public_openapi_operation():
376
485
  "extract": "extract",
377
486
  "feedback": "feedback",
378
487
  "health": "health",
488
+ "maps": "maps",
379
489
  "news": "news",
380
490
  "search": "search",
381
491
  "screenshot": "screenshot",
File without changes
File without changes
File without changes