Skip to content

Commit a2ba72b

Browse files
committed
Merge pull request planetlabs#71 from planetlabs/sync-more-products
sync tool handles all ortho products
2 parents 8c6029e + 76044b5 commit a2ba72b

File tree

3 files changed

+56
-30
lines changed

3 files changed

+56
-30
lines changed

planet/api/sync.py

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,13 @@
2424

2525
class _SyncTool(object):
2626

27-
def __init__(self, client, destination, aoi, scene_type, **filters):
27+
def __init__(self, client, destination, aoi, scene_type, products,
28+
**filters):
2829
self.client = client
2930
self.destination = destination
3031
self.aoi = aoi
3132
self.scene_type = scene_type
33+
self.products = products
3234
self.filters = filters
3335
self.workspace = filters.get('workspace', None)
3436
self._init()
@@ -75,13 +77,14 @@ def init(self, limit=-1):
7577
self._scenes = resp
7678
count = resp.get()['count']
7779
self._scene_count = count if limit < 0 else limit
78-
return count
80+
return count * len(self.products)
7981

8082
def get_scenes_to_sync(self):
8183
return self._scenes.items_iter(limit=self._scene_count)
8284

8385
def sync(self, callback):
84-
summary = _SyncSummary(self._scene_count)
86+
# init with number of scenes * products
87+
summary = _SyncSummary(self._scene_count * len(self.products))
8588

8689
all_scenes = self.get_scenes_to_sync()
8790
while True:
@@ -93,9 +96,12 @@ def sync(self, callback):
9396
_SyncHandler(self.destination, summary, scene, callback) for
9497
scene in scenes
9598
]
96-
futures = [h.run(self.client, self.scene_type) for h in handlers]
97-
for f in futures:
98-
f.await()
99+
# start all downloads asynchronously
100+
for h in handlers:
101+
h.run(self.client, self.scene_type, self.products)
102+
# synchronously await them and then write metadata
103+
for h in handlers:
104+
h.finish()
99105

100106
if summary.latest:
101107
sync = self._read_sync_file()
@@ -132,23 +138,29 @@ def __init__(self, destination, summary, metadata, user_callback):
132138
self.metadata = metadata
133139
self.user_callback = user_callback or (lambda *args: None)
134140

135-
def run(self, client, scene_type):
136-
return client.fetch_scene_geotiffs(
137-
[self.metadata['id']],
138-
scene_type,
139-
callback=self)[0]
141+
def run(self, client, scene_type, products):
142+
self.futures = []
143+
for product in products:
144+
self.futures.extend(client.fetch_scene_geotiffs(
145+
[self.metadata['id']],
146+
scene_type, product,
147+
callback=self))
140148

141-
def __call__(self, body):
142-
'''implement the callback that runs when the scene response is ready'''
143-
# first write scene to disk
144-
write_to_file(self.destination)(body)
149+
def finish(self):
150+
for f in self.futures:
151+
f.await()
145152

146153
# write out metadata
147154
metadata = os.path.join(self.destination,
148155
'%s_metadata.json' % self.metadata['id'])
149156
with atomic_open(metadata, 'wb') as fp:
150157
fp.write(json.dumps(self.metadata, indent=2).encode('utf-8'))
151158

159+
def __call__(self, body):
160+
'''implement the callback that runs when the scene response is ready'''
161+
# first write scene to disk
162+
write_to_file(self.destination)(body)
163+
152164
# summarize
153165
self.summary.transfer_complete(body, self.metadata)
154166

planet/scripts/__init__.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030

3131
client_params = {}
3232

33+
ORTHO_PRODUCTS = ['visual', 'analytic', 'unrectified']
34+
3335

3436
def client():
3537
return api.Client(**client_params)
@@ -268,7 +270,7 @@ def get_scenes_list(scene_type, pretty, aoi, limit, where, workspace):
268270

269271
echo_json_response(call_and_wrap(
270272
client().get_scenes_list,
271-
scene_type=scene_type, intersects=aoi,
273+
scene_type=scene_type, intersects=aoi, count=limit,
272274
**conditions), pretty, limit=limit)
273275

274276

@@ -289,7 +291,7 @@ def metadata(scene_id, scene_type, pretty):
289291
@click.option('--product',
290292
type=click.Choice(
291293
["band_%d" % i for i in range(1, 12)] +
292-
['visual', 'analytic', 'unrectified', 'qa']
294+
ORTHO_PRODUCTS + ['qa']
293295
), default='visual')
294296
@cli.command('download')
295297
def fetch_scene_geotiff(scene_ids, scene_type, product, dest):
@@ -333,20 +335,28 @@ def fetch_scene_thumbnails(scene_ids, scene_type, size, fmt, dest):
333335
@click.argument("destination")
334336
@limit_option(default=-1)
335337
@click.option("--dryrun", is_flag=True, help='Do not actually download')
338+
@click.option("--products", multiple=True,
339+
type=click.Choice(ORTHO_PRODUCTS + ['all']),
340+
help='Specifiy products to download, default is visual')
336341
@cli.command('sync')
337-
def sync(destination, workspace, scene_type, limit, dryrun):
342+
def sync(destination, workspace, scene_type, limit, dryrun, products):
338343
'''Synchronize a directory to a specified AOI or workspace'''
339344
aoi = None
340345
filters = {'workspace': workspace}
341346

347+
if 'all' in products:
348+
products = ORTHO_PRODUCTS
349+
else:
350+
products = products or ('visual',)
351+
342352
sync_tool = _SyncTool(client(), destination, aoi,
343-
scene_type, **filters)
353+
scene_type, products, **filters)
344354

345355
try:
346356
to_fetch = sync_tool.init(limit)
347357
except ValueError as ve:
348358
raise click.ClickException(str(ve))
349-
click.echo('total scenes to fetch: %s' % to_fetch)
359+
click.echo('total scene products to fetch: %s' % to_fetch)
350360
if limit > -1:
351361
click.echo('limiting to %s' % limit)
352362

tests/test_sync.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,22 +26,22 @@
2626
def test_sync_tool(tmpdir):
2727
# test non-existing destination
2828
try:
29-
_SyncTool(client, 'should-not-exist', None, None)
29+
_SyncTool(client, 'should-not-exist', None, None, None)
3030
except ValueError as ve:
3131
assert str(ve) == 'destination must exist and be a directory'
3232

3333
# test existing destination, no aoi.geojson
3434
td = tmpdir.mkdir('sync-dest')
3535
try:
36-
_SyncTool(client, td.strpath, None, None)
36+
_SyncTool(client, td.strpath, None, None, None)
3737
except ValueError as ve:
3838
assert str(ve) == 'no aoi provided and no aoi.geojson file'
3939

4040
# test existing destination, invalid aoi.geojson
4141
aoi_file = td.join('aoi.geojson')
4242
aoi_file.write('not geojson')
4343
try:
44-
_SyncTool(client, td.strpath, None, None)
44+
_SyncTool(client, td.strpath, None, None, None)
4545
except ValueError as ve:
4646
assert str(ve) == '%s does not contain valid JSON' % aoi_file
4747

@@ -51,19 +51,22 @@ def test_sync_tool(tmpdir):
5151
def test_sync_tool_init(tmpdir):
5252
td = tmpdir.mkdir('sync-dest')
5353
aoi = read_fixture('aoi.geojson')
54-
st = _SyncTool(client, td.strpath, aoi, 'ortho')
54+
st = _SyncTool(client, td.strpath, aoi, 'ortho', ('visual', 'analytic'))
5555
search_json = json.loads(read_fixture('search.geojson'))
5656
response = MagicMock(spec=Scenes)
5757
response.get.return_value = search_json
5858
client.get_scenes_list.return_value = response
5959

6060
# init w/ no limit should return count from response
6161
count = st.init()
62-
assert search_json['count'] == count
62+
# confusing but we'll download 2 products one for each scene
63+
assert search_json['count'] * 2 == count
6364

64-
# expect limiting
65+
# expect limiting to 10 ids
6566
count = st.init(limit=10)
66-
assert count == search_json['count']
67+
# still replies with total jobs despite the limit
68+
assert search_json['count'] * 2 == count
69+
# this tracks the internal number of ids, still 10
6770
assert 10 == st._scene_count
6871

6972
# create a stored 'latest' date and ensure it's used
@@ -82,7 +85,7 @@ def test_sync_tool_init(tmpdir):
8285
def test_sync_tool_sync(tmpdir):
8386
td = tmpdir.mkdir('sync-dest')
8487
aoi = read_fixture('aoi.geojson')
85-
st = _SyncTool(client, td.strpath, aoi, 'ortho')
88+
st = _SyncTool(client, td.strpath, aoi, 'ortho', ('visual',))
8689
search_json = json.loads(read_fixture('search.geojson'))
8790

8891
class Page:
@@ -146,7 +149,7 @@ def callback(name, remaining):
146149

147150
# because process is normally async, as a sideeffect of this mock getting
148151
# called, we have to dispatch the callback
149-
def run_callbacks(ids, scene_type, callback):
152+
def run_callbacks(ids, scene_type, product, callback):
150153
resp = responses.pop(0)
151154
callback(resp)
152155
return DEFAULT
@@ -172,7 +175,8 @@ def run_callbacks(ids, scene_type, callback):
172175
# implementation detail - because we're making requests separately,
173176
# fetch_scene_geotiffs will be called once for each id (instead of in bulk)
174177
assert items == len(args)
175-
assert [a[0] for a in args] == [([f['id']], 'ortho') for f in first_items]
178+
assert [a[0] for a in args] == \
179+
[([f['id']], 'ortho', 'visual') for f in first_items]
176180
# callbacks should be made - arguments are 'tiff name', remaining
177181
assert called_back == [(str(i), 4 - i) for i in range(5)]
178182

0 commit comments

Comments
 (0)