Repository navigation
Expand file tree
/
Copy pathtypes.py
More file actions
287 lines (229 loc) · 8.36 KB
/
Copy pathtypes.py
File metadata and controls
287 lines (229 loc) · 8.36 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
# Copyright 2017 Planet Labs, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import json
import re
import click
from click.types import CompositeParamType
from .util import read
from .util import _split
from planet.api import filters
from planet.api.utils import geometry_from_json
from planet.api.utils import strp_lenient
from planet.scripts.item_asset_types import get_item_types, get_asset_types, \
DEFAULT_ITEM_TYPES, DEFAULT_ASSET_TYPES
metavar_docs = {
'FIELD COMP VALUE...': '''A comparison query format where FIELD is a
property of the item-type and COMP is one of lt, lte, gt, gte and VALUE is
the number or date to compare against.
Note: ISO-8601 variants are supported. For example, ``2017`` is short for
``2017-01-01T00:00:00+00:00``.
''',
'FIELD VALUES...': '''Specifies an 'in' query where FIELD is a property
of the item-type and VALUES is space or comma separated text or numbers.
''',
'GEOM': '''Specify a geometry in GeoJSON format either as an inline value,
stdin, or a file. ``@-`` specifies stdin and ``@filename`` specifies
reading from a file named 'filename'. Otherwise, the value is assumed to
be GeoJSON.
''',
'FILTER': '''Specify a Data API search filter provided as JSON.
``@-`` specifies stdin and ``@filename`` specifies reading from a file
named 'filename'. Otherwise, the value is assumed to be JSON.
''',
'ITEM-TYPE': '''Specify Item-Type(s) of interest. Case-insensitive,
supports glob-matching, e.g. ``psscene*`` means ``PSScene3Band`` and
``PSScene4Band``. The ``all`` value specifies every Item-Type.
''',
'ASSET-TYPE': '''Specify Asset-Type(s) of interest. Case-insenstive,
supports glob-matching, e.g. ``visual*`` specifies ``visual`` and
``visual_xml``.
'''
}
class _LenientChoice(click.Choice):
'''Like click.Choice but allows
case-insensitive prefix matching
optional 'all' matching
optional prefix matching
glob matching
format fail msges for large selection of choices
returns a list unlike choice (to support 'all')
'''
allow_all = False
allow_prefix = False
def get_metavar(self, param):
return self.name.upper()
def _fail(self, msg, val, param, ctx):
self.fail('%s choice: %s.\nChoose from:\n\t%s' %
(msg, val, '\n\t'.join(self.choices)), param, ctx)
def convert(self, val, param, ctx):
matched = set()
for p in _split(val):
matched = matched.union(self._match(p, param, ctx))
return list(matched)
def _match(self, val, param, ctx, update_on_fail=True):
lval = val.lower()
if lval == 'all' and self.allow_all:
return self.choices
if '*' in lval:
pat = lval.replace('*', '.*')
matches = [c for c in self.choices
if re.match(pat, c.lower())]
elif self.allow_prefix:
matches = [c for c in self.choices
if c.lower().startswith(lval)]
else:
matches = [c for c in self.choices if c.lower() == lval]
if not matches and update_on_fail:
self._update_choices() # get choices from a remote source.
return self._match(val, param, ctx, False) # Try one more time
elif not matches:
self._fail('invalid', val, param, ctx)
else:
return matches
def _update_choices(self):
self.choices = self.get_remote_choices()
def get_remote_choices(self):
'''
No-op just returns current choices.
Subclasses should override.
'''
return self.choices
class ItemType(_LenientChoice):
name = 'item-type'
allow_all = True
allow_prefix = True
def __init__(self):
_LenientChoice.__init__(self, DEFAULT_ITEM_TYPES)
def get_remote_choices(self):
return get_item_types()
class AssetType(_LenientChoice):
name = 'asset-type'
def __init__(self):
_LenientChoice.__init__(self, DEFAULT_ASSET_TYPES)
def get_remote_choices(self):
return get_asset_types()
class AssetTypePerm(AssetType):
@staticmethod
def to_permissions(asset_types):
return filters.permission_filter(*[
'assets.%s:download' % a for a in asset_types
])
def convert(self, val, param, ctx):
return AssetTypePerm.to_permissions(
AssetType.convert(self, val, param, ctx))
class _FilterFieldValues(CompositeParamType):
name = 'field values'
arity = 2
def convert(self, val, param, ctx):
field, vals = val
vals = _split(vals)
parsed = []
for v in vals:
v = v.strip()
if not v:
continue
try:
parsed.append(self.val_type(v))
except ValueError:
self.fail('invalid value: %s' % v, param, ctx)
return self._builder(field, *parsed)
class StringIn(_FilterFieldValues):
val_type = str
@property
def _builder(self):
return filters.string_filter
class NumberIn(_FilterFieldValues):
val_type = float
@property
def _builder(self):
return filters.num_filter
class Range(CompositeParamType):
arity = 3
name = 'field comp value'
comp_ops = ['lt', 'lte', 'gt', 'gte']
@property
def _builder(self):
return filters.range_filter
def _parse(self, val, param, ctx):
try:
return float(val)
except ValueError:
self.fail('invalid value for range: "%s", must be number' % val)
def convert(self, vals, param, ctx):
field, comp_op, val = vals
if comp_op not in self.comp_ops:
self.fail('invalid operator: %s. allowed: %s' % (
comp_op, ','.join(self.comp_ops)), param, ctx)
args = dict([(comp_op, self._parse(val, param, ctx))])
return self._builder(field, **args)
class DateRange(Range):
@property
def _builder(self):
return filters.date_range
def _parse(self, val, param, ctx):
parsed = strp_lenient(val)
if parsed is None:
self.fail('invalid date: %s.' % val, param, ctx)
return parsed
class GeomFilter(click.ParamType):
name = 'geom'
def convert(self, val, param, ctx):
val = read(val)
if not val:
return []
try:
geoj = json.loads(val)
except ValueError:
raise click.BadParameter('invalid GeoJSON')
geom = geometry_from_json(geoj)
if geom is None:
raise click.BadParameter('unable to find geometry in input')
return [filters.geom_filter(geom)]
class FilterJSON(click.ParamType):
name = 'filter'
def convert(self, val, param, ctx):
val = read(val)
if not val:
return []
try:
filt = json.loads(val)
except ValueError:
raise click.BadParameter('invalid JSON')
if not filters.is_filter_like(filt):
self.fail('Does not appear to be valid filter', param, ctx)
return filt
class SortSpec(CompositeParamType):
name = 'field order'
arity = 2
def convert(self, val, param, ctx):
if not val:
return ''
field, order = val
fields = ('published', 'acquired')
if field not in fields:
raise click.BadParameter(
'sort only supports: %s' % ' '.join(fields))
orders = ('asc', 'desc')
if order not in orders:
raise click.BadParameter(
'order only supports: %s' % ' '.join(orders))
return ' '.join(val)
class BoundingBox(click.ParamType):
name = 'bbox'
def convert(self, val, param, ctx):
try:
xmin, ymin, xmax, ymax = map(float, val.split(','))
except (TypeError, ValueError):
raise click.BadParameter('Invalid bounding box')
return (xmin, ymin, xmax, ymax)