Skip to content

Commit 1fbddb8

Browse files
authored
Merge pull request #26 from EO-DataHub/token_pagination
Pagination with rotating API keys
2 parents b15d39c + 2224acd commit 1fbddb8

2 files changed

Lines changed: 105 additions & 189 deletions

File tree

stac_planet_api/api.py

Lines changed: 90 additions & 175 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,11 @@
5757

5858
# Load all the planet api keys from the environment and setup a cycle so we can always use the next one.
5959
try:
60-
PLANET_API_KEYS = itertools.cycle(os.environ.get("PLANET_API_KEYS").split(":"))
60+
PLANET_API_KEYS = itertools.cycle(
61+
os.environ.get(
62+
"PLANET_API_KEYS",
63+
).split(":")
64+
)
6165
except NameError:
6266
PLANET_API_KEYS = None
6367

@@ -95,26 +99,27 @@ def get_auth(credentials) -> httpx.BasicAuth:
9599
"""Create a httpx auth for the planet apis."""
96100
# Use the api key if available, otherwise pass through basic credentials from the user.
97101

98-
if PLANET_API_KEYS is not None:
99-
api_key = next(PLANET_API_KEYS)
100-
auth = httpx.BasicAuth(username=api_key, password="")
101-
elif credentials is not None:
102+
if credentials is not None:
103+
api_key = credentials.username
102104
auth = httpx.BasicAuth(
103105
username=credentials.username, password=credentials.password
104106
)
107+
108+
elif PLANET_API_KEYS is not None:
109+
api_key = next(PLANET_API_KEYS)
110+
auth = httpx.BasicAuth(username=api_key, password="")
111+
105112
else:
106113
raise fastapi.HTTPException(
107114
status_code=401, detail="Credentials were not provided."
108115
)
109116

110-
return auth
117+
return auth, api_key
111118

112119

113-
def get_authenticated_client(credentials) -> httpx.Client:
120+
def get_authenticated_client(auth) -> httpx.Client:
114121
"""Create a httpx client with correct auth for the planet apis."""
115122

116-
auth = get_auth(credentials)
117-
118123
return httpx.AsyncClient(
119124
auth=auth,
120125
verify=False,
@@ -237,14 +242,15 @@ async def get_search(
237242
includes.add(field[1:] if field[0] in "+ " else field)
238243
search_request["fields"] = {"include": includes, "exclude": excludes}
239244

240-
return await prepare_search(
245+
return await post_search(
241246
search_request=POST_REQUEST_MODEL(**search_request),
242247
request=request,
243248
credentials=credentials,
244249
)
245250

246251

247-
async def prepare_search(
252+
@app.post("/search")
253+
async def post_search(
248254
search_request: POST_REQUEST_MODEL,
249255
request: Request,
250256
credentials: Annotated[
@@ -259,162 +265,85 @@ async def prepare_search(
259265
Returns:
260266
ItemCollection: The items.
261267
"""
262-
263-
client = get_authenticated_client(credentials)
264268
base_url = get_base_url(request)
265269

266-
auth = get_auth(credentials)
267-
268-
search_request.limit = (
269-
MAX_ITEMS if search_request.limit > MAX_ITEMS else search_request.limit
270-
)
270+
if token := search_request.token:
271+
token_parts = FERNET.decrypt(token).decode("utf-8").split("\\")
271272

272-
if search_request.ids:
273-
274-
all_collections = (
275-
search_request.collections
276-
if search_request.collections
277-
else await get_collections(client)
273+
credentials = fastapi.security.HTTPBasicCredentials(
274+
username=token_parts[1], password=""
278275
)
279276

280-
all_items = []
281-
282-
for item_id in search_request.ids:
283-
for collection_id in all_collections:
284-
try:
285-
item = await get_item(
286-
collection_id=collection_id,
287-
item_id=item_id,
288-
request=request,
289-
credentials=credentials,
290-
)
291-
all_items.append(item)
292-
except httpx.HTTPStatusError: # unable to find item in catalogue
293-
pass
294-
295-
return ItemCollection(
296-
**{
297-
"type": "FeatureCollection",
298-
"features": all_items,
299-
"links": [
300-
{
301-
"rel": "self",
302-
"href": f"{base_url}search",
303-
"type": "application/geo+json",
304-
},
305-
{"rel": "root", "href": base_url, "type": "application/json"},
306-
],
307-
}
308-
)
309-
310-
else:
311-
if token := search_request.token:
312-
token_url = FERNET.decrypt(token).decode("utf-8")
313-
planet_response = await client.get(token_url)
277+
auth, api_key = get_auth(credentials)
278+
client = get_authenticated_client(auth=auth)
314279

315-
else:
316-
planet_parameters, planet_request = stac_to_planet_request(
317-
stac_request=search_request
318-
)
280+
planet_response = await client.get(token_parts[0])
319281

320-
planet_response = await client.post(
321-
"https://api.planet.com/data/v1/quick-search",
322-
params=planet_parameters,
323-
json=planet_request,
324-
)
282+
else:
325283

326-
planet_response.raise_for_status()
284+
auth, api_key = get_auth(credentials)
285+
client = get_authenticated_client(auth)
327286

328-
return planet_to_stac_response(
329-
planet_response=planet_response.json(), base_url=base_url, auth=auth
287+
search_request.limit = (
288+
MAX_ITEMS if search_request.limit > MAX_ITEMS else search_request.limit
330289
)
331290

291+
if search_request.ids:
332292

333-
@app.post("/search")
334-
async def post_search(
335-
search_request: POST_REQUEST_MODEL,
336-
request: Request,
337-
credentials: Annotated[
338-
fastapi.security.HTTPBasicCredentials, fastapi.Depends(security)
339-
],
340-
) -> ItemCollection:
341-
"""Search planet items.
342-
343-
Args:
344-
search request (BaseSearchPostRequest): The search request.
345-
346-
Returns:
347-
ItemCollection: The items.
348-
"""
349-
350-
client = get_authenticated_client(credentials)
351-
base_url = get_base_url(request)
352-
353-
auth = get_auth(credentials)
354-
355-
search_request.limit = (
356-
MAX_ITEMS if search_request.limit > MAX_ITEMS else search_request.limit
357-
)
293+
all_collections = (
294+
search_request.collections
295+
if search_request.collections
296+
else await get_collections(client)
297+
)
358298

359-
if search_request.ids:
299+
all_items = []
300+
301+
for item_id in search_request.ids:
302+
for collection_id in all_collections:
303+
try:
304+
item = await get_item(
305+
collection_id=collection_id,
306+
item_id=item_id,
307+
request=request,
308+
credentials=credentials,
309+
)
310+
all_items.append(item)
311+
except httpx.HTTPStatusError: # unable to find item in catalogue
312+
pass
313+
314+
return ItemCollection(
315+
**{
316+
"type": "FeatureCollection",
317+
"features": all_items,
318+
"links": [
319+
{
320+
"rel": "self",
321+
"href": f"{base_url}search",
322+
"type": "application/geo+json",
323+
},
324+
{"rel": "root", "href": base_url, "type": "application/json"},
325+
],
326+
}
327+
)
360328

361-
all_collections = (
362-
search_request.collections
363-
if search_request.collections
364-
else await get_collections(client)
329+
planet_parameters, planet_request = stac_to_planet_request(
330+
stac_request=search_request
365331
)
366332

367-
all_items = []
368-
369-
for item_id in search_request.ids:
370-
for collection_id in all_collections:
371-
try:
372-
item = await get_item(
373-
collection_id=collection_id,
374-
item_id=item_id,
375-
request=request,
376-
credentials=credentials,
377-
)
378-
all_items.append(item)
379-
except httpx.HTTPStatusError: # unable to find item in catalogue
380-
pass
381-
382-
return ItemCollection(
383-
**{
384-
"type": "FeatureCollection",
385-
"features": all_items,
386-
"links": [
387-
{
388-
"rel": "self",
389-
"href": f"{base_url}search",
390-
"type": "application/geo+json",
391-
},
392-
{"rel": "root", "href": base_url, "type": "application/json"},
393-
],
394-
}
333+
planet_response = await client.post(
334+
"https://api.planet.com/data/v1/quick-search",
335+
params=planet_parameters,
336+
json=planet_request,
395337
)
396338

397-
else:
398-
if token := search_request.token:
399-
token_url = FERNET.decrypt(token).decode("utf-8")
400-
planet_response = await client.get(token_url)
401-
402-
else:
403-
planet_parameters, planet_request = stac_to_planet_request(
404-
stac_request=search_request
405-
)
406-
407-
planet_response = await client.post(
408-
"https://api.planet.com/data/v1/quick-search",
409-
params=planet_parameters,
410-
json=planet_request,
411-
)
412-
413-
planet_response.raise_for_status()
339+
planet_response.raise_for_status()
414340

415-
return planet_to_stac_response(
416-
planet_response=planet_response.json(), base_url=base_url, auth=auth
417-
)
341+
return planet_to_stac_response(
342+
planet_response=planet_response.json(),
343+
base_url=base_url,
344+
auth=auth,
345+
api_key=api_key,
346+
)
418347

419348

420349
@app.get("/collections/{collection_id}/items")
@@ -432,30 +361,18 @@ async def get_item_collection(
432361
Returns:
433362
ItemCollection: The items.
434363
"""
435-
client = get_authenticated_client(credentials)
436-
base_url = get_base_url(request)
437-
438-
auth = get_auth(credentials)
439-
440364
query_params = dict(request._query_params)
441365

442-
limit = int(query_params.get("limit", MAX_ITEMS))
443-
query_params["limit"] = MAX_ITEMS if limit > MAX_ITEMS else limit
444-
445-
planet_parameters, planet_request = stac_to_planet_request(
446-
stac_request=BaseSearchPostRequest(collections=[collection_id], **query_params)
447-
)
448-
449-
planet_response = await client.post(
450-
"https://api.planet.com/data/v1/quick-search",
451-
params=planet_parameters,
452-
json=planet_request,
453-
)
454-
455-
planet_response.raise_for_status()
366+
search_request = {
367+
"collections": [collection_id],
368+
"limit": int(query_params.get("limit", MAX_ITEMS)),
369+
"token": query_params.get("token", None),
370+
}
456371

457-
return planet_to_stac_response(
458-
planet_response=planet_response.json(), base_url=base_url, auth=auth
372+
return await post_search(
373+
search_request=POST_REQUEST_MODEL(**search_request),
374+
request=request,
375+
credentials=credentials,
459376
)
460377

461378

@@ -476,11 +393,10 @@ async def get_item(
476393
Returns:
477394
Item: The item.
478395
"""
479-
client = get_authenticated_client(credentials)
396+
auth, _ = get_auth(credentials)
397+
client = get_authenticated_client(auth)
480398
base_url = get_base_url(request)
481399

482-
auth = get_auth(credentials)
483-
484400
planet_response = await client.get(
485401
f"https://api.planet.com/data/v1/item-types/{collection_id}/items/{item_id}",
486402
)
@@ -511,11 +427,10 @@ async def get_item_thumbnail(
511427
Returns:
512428
Response: Thumbnail image
513429
"""
514-
client = get_authenticated_client(credentials)
430+
auth, _ = get_auth(credentials)
431+
client = get_authenticated_client(auth)
515432
base_url = get_base_url(request)
516433

517-
auth = get_auth(credentials)
518-
519434
planet_response = await client.get(
520435
f"https://api.planet.com/data/v1/item-types/{collection_id}/items/{item_id}",
521436
)

0 commit comments

Comments
 (0)