afscgap.convert

Logic to convert types when interacting with the AFSC GAP REST service.

(c) 2023 Regents of University of California / The Eric and Wendy Schmidt Center for Data Science and the Environment at UC Berkeley.

This file is part of afscgap released under the BSD 3-Clause License. See LICENSE.md.

  1"""
  2Logic to convert types when interacting with the AFSC GAP REST service.
  3
  4(c) 2023 Regents of University of California / The Eric and Wendy Schmidt Center
  5for Data Science and the Environment at UC Berkeley.
  6
  7This file is part of afscgap released under the BSD 3-Clause License. See
  8LICENSE.md.
  9"""
 10import re
 11
 12from afscgap.typesdef import FLOAT_PARAM
 13from afscgap.typesdef import OPT_FLOAT
 14from afscgap.typesdef import STR_PARAM
 15
 16DATE_REGEX = re.compile('(?P<month>\\d{2})\\/(?P<day>\\d{2})\\/' + \
 17    '(?P<year>\\d{4}) (?P<hours>\\d{2})\\:(?P<minutes>\\d{2})\\:' + \
 18    '(?P<seconds>\\d{2})')
 19DATE_TEMPLATE = '%s/%s/%s %s:%s:%s'
 20ISO_8601_REGEX = re.compile('(?P<year>\\d{4})\\-(?P<month>\\d{2})\\-' + \
 21    '(?P<day>\\d{2})T(?P<hours>\\d{2})\\:(?P<minutes>\\d{2})\\:' + \
 22    '(?P<seconds>\\d{2})')
 23ISO_8601_TEMPLATE = '%s-%s-%sT%s:%s:%s'
 24
 25AREA_CONVERTERS = {
 26    'ha': lambda x: x,
 27    'm2': lambda x: x * 10000,
 28    'km2': lambda x: x * 0.01
 29}
 30
 31AREA_UNCONVERTERS = {
 32    'ha': lambda x: x,
 33    'm2': lambda x: x / 10000,
 34    'km2': lambda x: x / 0.01
 35}
 36
 37DISTANCE_CONVERTERS = {
 38    'm': lambda x: x,
 39    'km': lambda x: x / 1000
 40}
 41
 42DISTANCE_UNCONVERTERS = {
 43    'm': lambda x: x,
 44    'km': lambda x: x * 1000
 45}
 46
 47TEMPERATURE_CONVERTERS = {
 48    'c': lambda x: x,
 49    'f': lambda x: x * 9 / 5 + 32
 50}
 51
 52TEMPERATURE_UNCONVERTERS = {
 53    'c': lambda x: x,
 54    'f': lambda x: (x - 32) * 5 / 9
 55}
 56
 57TIME_CONVERTERS = {
 58    'day': lambda x: x / 24,
 59    'hr': lambda x: x,
 60    'min': lambda x: x * 60
 61}
 62
 63TIME_UNCONVERTERS = {
 64    'day': lambda x: x * 24,
 65    'hr': lambda x: x,
 66    'min': lambda x: x / 60
 67}
 68
 69WEIGHT_CONVERTERS = {
 70    'g': lambda x: x * 1000,
 71    'kg': lambda x: x
 72}
 73
 74WEIGHT_UNCONVERTERS = {
 75    'g': lambda x: x / 1000,
 76    'kg': lambda x: x
 77}
 78
 79
 80def convert_from_iso8601(target: STR_PARAM) -> STR_PARAM:
 81    """Convert strings from ISO 8601 format to API format.
 82
 83    Args:
 84        target: The string or dictionary in which to perform the
 85            transformations.
 86
 87    Returns:
 88        If given an ISO 8601 string, will convert from ISO 8601 to the API
 89        datetime string format. Similarly, if given a dictionary, all values
 90        matching an ISO 8601 string will be converted to the API datetime string
 91        format. If given None, returns None.
 92    """
 93    if target is None:
 94        return None
 95    elif isinstance(target, str):
 96        return convert_from_iso8601_str(target)
 97    elif isinstance(target, dict):
 98        items = target.items()
 99        output_dict = {}
100
101        for key, value in items:
102            if isinstance(value, str):
103                output_dict[key] = convert_from_iso8601_str(value)
104            else:
105                output_dict[key] = value
106
107        return output_dict
108    else:
109        return target
110
111
112def convert_from_iso8601_str(target: str) -> str:
113    """Attempt converting an ISO 8601 string to an API-provided datetime.
114
115    Args:
116        target: The datetime string to try to interpret.
117
118    Returns:
119        The datetime input string as a ISO 8601 string or the original value of
120        target if it could not be parsed.
121    """
122    match = ISO_8601_REGEX.match(target)
123
124    if not match:
125        return target
126
127    year = match.group('year')
128    month = match.group('month')
129    day = match.group('day')
130    hours = match.group('hours')
131    minutes = match.group('minutes')
132    seconds = match.group('seconds')
133
134    return DATE_TEMPLATE % (month, day, year, hours, minutes, seconds)
135
136
137def convert_to_iso8601(target: str) -> str:
138    """Attempt converting an API-provided datetime to ISO 8601.
139
140    Args:
141        target: The datetime string to try to interpret.
142    Returns:
143        The datetime input string as a ISO 8601 string or the original value of
144        target if it could not be parsed.
145    """
146    match = DATE_REGEX.match(target)
147
148    if not match:
149        return target
150
151    year = match.group('year')
152    month = match.group('month')
153    day = match.group('day')
154    hours = match.group('hours')
155    minutes = match.group('minutes')
156    seconds = match.group('seconds')
157
158    return ISO_8601_TEMPLATE % (year, month, day, hours, minutes, seconds)
159
160
161def is_iso8601(target: str) -> bool:
162    """Determine if a string matches an expected ISO 8601 format.
163
164    Args:
165        target: The string to test.
166
167    Returns:
168        True if it matches the expected format and false otherwise.
169    """
170    return ISO_8601_REGEX.match(target) is not None
171
172
173def convert_area(target: OPT_FLOAT, units: str) -> OPT_FLOAT:
174    """Convert an area.
175
176    Args:
177        target: The value to convert in hectares.
178        units: Desired units.
179
180    Returns:
181        The converted value. Note that, if target is None, will return None.
182    """
183    if target is None:
184        return None
185
186    return AREA_CONVERTERS[units](target)
187
188
189def unconvert_area(target: FLOAT_PARAM, units: str) -> FLOAT_PARAM:
190    """Standardize an area to the API-native units (hectare).
191
192    Args:
193        target: The value to convert in hectares.
194        units: The units of value.
195
196    Returns:
197        The converted value. Note that, if target is None, will return None.
198    """
199    if target is None:
200        return None
201
202    if isinstance(target, dict):
203        return target
204
205    return AREA_UNCONVERTERS[units](target)
206
207
208def convert_degrees(target: OPT_FLOAT, units: str) -> OPT_FLOAT:
209    """Convert targets from degrees to another units.
210
211    Args:
212        target: The value to convert which may be None.
213        units: Desired units.
214
215    Returns:
216        The same value input after asserting that units are dd, the only
217        supported units.
218    """
219    assert units == 'dd'
220    return target
221
222
223def unconvert_degrees(target: FLOAT_PARAM, units: str) -> FLOAT_PARAM:
224    """Standardize a degree to the API-native units (degrees).
225
226    Args:
227        target: The value to convert which may be None.
228        units: The units of value.
229
230    Returns:
231        The same value input after asserting that units are dd, the only
232        supported units.
233    """
234    assert units == 'dd'
235    return target
236
237
238def convert_distance(target: OPT_FLOAT, units: str) -> OPT_FLOAT:
239    """Convert a linear distance.
240
241    Args:
242        target: The value to convert in meters.
243        units: Desired units.
244
245    Returns:
246        The converted value. Note that, if target is None, will return None.
247    """
248    if target is None:
249        return None
250
251    return DISTANCE_CONVERTERS[units](target)
252
253
254def unconvert_distance(target: FLOAT_PARAM, units: str) -> FLOAT_PARAM:
255    """Convert a linear distance to the API-native units (meters).
256
257    Args:
258        target: The value to convert in meters.
259        units: The units of value.
260
261    Returns:
262        The converted value. Note that, if target is None, will return None.
263    """
264    if target is None:
265        return None
266
267    if isinstance(target, dict):
268        return target
269
270    return DISTANCE_UNCONVERTERS[units](target)
271
272
273def convert_temperature(target: OPT_FLOAT, units: str) -> OPT_FLOAT:
274    """Convert a temperature.
275
276    Args:
277        target: The value to convert in Celcius.
278        units: Desired units.
279
280    Returns:
281        The converted value. Note that, if target is None, will return None.
282    """
283    if target is None:
284        return None
285
286    return TEMPERATURE_CONVERTERS[units](target)
287
288
289def unconvert_temperature(target: FLOAT_PARAM, units: str) -> FLOAT_PARAM:
290    """Convert a linear temperature to the API-native units (Celsius).
291
292    Args:
293        target: The value to convert in Celcius.
294        units: The units of value.
295
296    Returns:
297        The converted value. Note that, if target is None, will return None.
298    """
299    if target is None:
300        return None
301
302    if isinstance(target, dict):
303        return target
304
305    return TEMPERATURE_UNCONVERTERS[units](target)
306
307
308def convert_time(target: OPT_FLOAT, units: str) -> OPT_FLOAT:
309    """Convert a time.
310
311    Args:
312        target: The value to convert in hours.
313        units: Desired units.
314
315    Returns:
316        The converted value. Note that, if target is None, will return None.
317    """
318    if target is None:
319        return None
320
321    return TIME_CONVERTERS[units](target)
322
323
324def unconvert_time(target: FLOAT_PARAM, units: str) -> FLOAT_PARAM:
325    """Convert a time to the API-native units (hours).
326
327    Args:
328        target: The value to convert in hours.
329        units: The units of value.
330
331    Returns:
332        The converted value. Note that, if target is None, will return None.
333    """
334    if target is None:
335        return None
336
337    if isinstance(target, dict):
338        return target
339
340    return TIME_UNCONVERTERS[units](target)
341
342
343def convert_weight(target: OPT_FLOAT, units: str) -> OPT_FLOAT:
344    """Convert a weight.
345
346    Args:
347        target: The value to convert in kilograms.
348        units: Desired units.
349
350    Returns:
351        The converted value. Note that, if target is None, will return None.
352    """
353    if target is None:
354        return None
355
356    return WEIGHT_CONVERTERS[units](target)
357
358
359def unconvert_weight(target: FLOAT_PARAM, units: str) -> FLOAT_PARAM:
360    """Convert a weight to the API-native units (kilograms).
361
362    Args:
363        target: The value to convert in kilograms.
364        units: The units of value.
365
366    Returns:
367        The converted value. Note that, if target is None, will return None.
368    """
369    if target is None:
370        return None
371
372    if isinstance(target, dict):
373        return target
374
375    return WEIGHT_UNCONVERTERS[units](target)
DATE_REGEX = re.compile('(?P<month>\\d{2})\\/(?P<day>\\d{2})\\/(?P<year>\\d{4}) (?P<hours>\\d{2})\\:(?P<minutes>\\d{2})\\:(?P<seconds>\\d{2})')
DATE_TEMPLATE = '%s/%s/%s %s:%s:%s'
ISO_8601_REGEX = re.compile('(?P<year>\\d{4})\\-(?P<month>\\d{2})\\-(?P<day>\\d{2})T(?P<hours>\\d{2})\\:(?P<minutes>\\d{2})\\:(?P<seconds>\\d{2})')
ISO_8601_TEMPLATE = '%s-%s-%sT%s:%s:%s'
AREA_CONVERTERS = {'ha': <function <lambda>>, 'm2': <function <lambda>>, 'km2': <function <lambda>>}
AREA_UNCONVERTERS = {'ha': <function <lambda>>, 'm2': <function <lambda>>, 'km2': <function <lambda>>}
DISTANCE_CONVERTERS = {'m': <function <lambda>>, 'km': <function <lambda>>}
DISTANCE_UNCONVERTERS = {'m': <function <lambda>>, 'km': <function <lambda>>}
TEMPERATURE_CONVERTERS = {'c': <function <lambda>>, 'f': <function <lambda>>}
TEMPERATURE_UNCONVERTERS = {'c': <function <lambda>>, 'f': <function <lambda>>}
TIME_CONVERTERS = {'day': <function <lambda>>, 'hr': <function <lambda>>, 'min': <function <lambda>>}
TIME_UNCONVERTERS = {'day': <function <lambda>>, 'hr': <function <lambda>>, 'min': <function <lambda>>}
WEIGHT_CONVERTERS = {'g': <function <lambda>>, 'kg': <function <lambda>>}
WEIGHT_UNCONVERTERS = {'g': <function <lambda>>, 'kg': <function <lambda>>}
def convert_from_iso8601(target: Union[str, dict, NoneType]) -> Union[str, dict, NoneType]:
 81def convert_from_iso8601(target: STR_PARAM) -> STR_PARAM:
 82    """Convert strings from ISO 8601 format to API format.
 83
 84    Args:
 85        target: The string or dictionary in which to perform the
 86            transformations.
 87
 88    Returns:
 89        If given an ISO 8601 string, will convert from ISO 8601 to the API
 90        datetime string format. Similarly, if given a dictionary, all values
 91        matching an ISO 8601 string will be converted to the API datetime string
 92        format. If given None, returns None.
 93    """
 94    if target is None:
 95        return None
 96    elif isinstance(target, str):
 97        return convert_from_iso8601_str(target)
 98    elif isinstance(target, dict):
 99        items = target.items()
100        output_dict = {}
101
102        for key, value in items:
103            if isinstance(value, str):
104                output_dict[key] = convert_from_iso8601_str(value)
105            else:
106                output_dict[key] = value
107
108        return output_dict
109    else:
110        return target

Convert strings from ISO 8601 format to API format.

Arguments:
  • target: The string or dictionary in which to perform the transformations.
Returns:

If given an ISO 8601 string, will convert from ISO 8601 to the API datetime string format. Similarly, if given a dictionary, all values matching an ISO 8601 string will be converted to the API datetime string format. If given None, returns None.

def convert_from_iso8601_str(target: str) -> str:
113def convert_from_iso8601_str(target: str) -> str:
114    """Attempt converting an ISO 8601 string to an API-provided datetime.
115
116    Args:
117        target: The datetime string to try to interpret.
118
119    Returns:
120        The datetime input string as a ISO 8601 string or the original value of
121        target if it could not be parsed.
122    """
123    match = ISO_8601_REGEX.match(target)
124
125    if not match:
126        return target
127
128    year = match.group('year')
129    month = match.group('month')
130    day = match.group('day')
131    hours = match.group('hours')
132    minutes = match.group('minutes')
133    seconds = match.group('seconds')
134
135    return DATE_TEMPLATE % (month, day, year, hours, minutes, seconds)

Attempt converting an ISO 8601 string to an API-provided datetime.

Arguments:
  • target: The datetime string to try to interpret.
Returns:

The datetime input string as a ISO 8601 string or the original value of target if it could not be parsed.

def convert_to_iso8601(target: str) -> str:
138def convert_to_iso8601(target: str) -> str:
139    """Attempt converting an API-provided datetime to ISO 8601.
140
141    Args:
142        target: The datetime string to try to interpret.
143    Returns:
144        The datetime input string as a ISO 8601 string or the original value of
145        target if it could not be parsed.
146    """
147    match = DATE_REGEX.match(target)
148
149    if not match:
150        return target
151
152    year = match.group('year')
153    month = match.group('month')
154    day = match.group('day')
155    hours = match.group('hours')
156    minutes = match.group('minutes')
157    seconds = match.group('seconds')
158
159    return ISO_8601_TEMPLATE % (year, month, day, hours, minutes, seconds)

Attempt converting an API-provided datetime to ISO 8601.

Arguments:
  • target: The datetime string to try to interpret.
Returns:

The datetime input string as a ISO 8601 string or the original value of target if it could not be parsed.

def is_iso8601(target: str) -> bool:
162def is_iso8601(target: str) -> bool:
163    """Determine if a string matches an expected ISO 8601 format.
164
165    Args:
166        target: The string to test.
167
168    Returns:
169        True if it matches the expected format and false otherwise.
170    """
171    return ISO_8601_REGEX.match(target) is not None

Determine if a string matches an expected ISO 8601 format.

Arguments:
  • target: The string to test.
Returns:

True if it matches the expected format and false otherwise.

def convert_area(target: Optional[float], units: str) -> Optional[float]:
174def convert_area(target: OPT_FLOAT, units: str) -> OPT_FLOAT:
175    """Convert an area.
176
177    Args:
178        target: The value to convert in hectares.
179        units: Desired units.
180
181    Returns:
182        The converted value. Note that, if target is None, will return None.
183    """
184    if target is None:
185        return None
186
187    return AREA_CONVERTERS[units](target)

Convert an area.

Arguments:
  • target: The value to convert in hectares.
  • units: Desired units.
Returns:

The converted value. Note that, if target is None, will return None.

def unconvert_area( target: Union[float, dict, Tuple[float], NoneType], units: str) -> Union[float, dict, Tuple[float], NoneType]:
190def unconvert_area(target: FLOAT_PARAM, units: str) -> FLOAT_PARAM:
191    """Standardize an area to the API-native units (hectare).
192
193    Args:
194        target: The value to convert in hectares.
195        units: The units of value.
196
197    Returns:
198        The converted value. Note that, if target is None, will return None.
199    """
200    if target is None:
201        return None
202
203    if isinstance(target, dict):
204        return target
205
206    return AREA_UNCONVERTERS[units](target)

Standardize an area to the API-native units (hectare).

Arguments:
  • target: The value to convert in hectares.
  • units: The units of value.
Returns:

The converted value. Note that, if target is None, will return None.

def convert_degrees(target: Optional[float], units: str) -> Optional[float]:
209def convert_degrees(target: OPT_FLOAT, units: str) -> OPT_FLOAT:
210    """Convert targets from degrees to another units.
211
212    Args:
213        target: The value to convert which may be None.
214        units: Desired units.
215
216    Returns:
217        The same value input after asserting that units are dd, the only
218        supported units.
219    """
220    assert units == 'dd'
221    return target

Convert targets from degrees to another units.

Arguments:
  • target: The value to convert which may be None.
  • units: Desired units.
Returns:

The same value input after asserting that units are dd, the only supported units.

def unconvert_degrees( target: Union[float, dict, Tuple[float], NoneType], units: str) -> Union[float, dict, Tuple[float], NoneType]:
224def unconvert_degrees(target: FLOAT_PARAM, units: str) -> FLOAT_PARAM:
225    """Standardize a degree to the API-native units (degrees).
226
227    Args:
228        target: The value to convert which may be None.
229        units: The units of value.
230
231    Returns:
232        The same value input after asserting that units are dd, the only
233        supported units.
234    """
235    assert units == 'dd'
236    return target

Standardize a degree to the API-native units (degrees).

Arguments:
  • target: The value to convert which may be None.
  • units: The units of value.
Returns:

The same value input after asserting that units are dd, the only supported units.

def convert_distance(target: Optional[float], units: str) -> Optional[float]:
239def convert_distance(target: OPT_FLOAT, units: str) -> OPT_FLOAT:
240    """Convert a linear distance.
241
242    Args:
243        target: The value to convert in meters.
244        units: Desired units.
245
246    Returns:
247        The converted value. Note that, if target is None, will return None.
248    """
249    if target is None:
250        return None
251
252    return DISTANCE_CONVERTERS[units](target)

Convert a linear distance.

Arguments:
  • target: The value to convert in meters.
  • units: Desired units.
Returns:

The converted value. Note that, if target is None, will return None.

def unconvert_distance( target: Union[float, dict, Tuple[float], NoneType], units: str) -> Union[float, dict, Tuple[float], NoneType]:
255def unconvert_distance(target: FLOAT_PARAM, units: str) -> FLOAT_PARAM:
256    """Convert a linear distance to the API-native units (meters).
257
258    Args:
259        target: The value to convert in meters.
260        units: The units of value.
261
262    Returns:
263        The converted value. Note that, if target is None, will return None.
264    """
265    if target is None:
266        return None
267
268    if isinstance(target, dict):
269        return target
270
271    return DISTANCE_UNCONVERTERS[units](target)

Convert a linear distance to the API-native units (meters).

Arguments:
  • target: The value to convert in meters.
  • units: The units of value.
Returns:

The converted value. Note that, if target is None, will return None.

def convert_temperature(target: Optional[float], units: str) -> Optional[float]:
274def convert_temperature(target: OPT_FLOAT, units: str) -> OPT_FLOAT:
275    """Convert a temperature.
276
277    Args:
278        target: The value to convert in Celcius.
279        units: Desired units.
280
281    Returns:
282        The converted value. Note that, if target is None, will return None.
283    """
284    if target is None:
285        return None
286
287    return TEMPERATURE_CONVERTERS[units](target)

Convert a temperature.

Arguments:
  • target: The value to convert in Celcius.
  • units: Desired units.
Returns:

The converted value. Note that, if target is None, will return None.

def unconvert_temperature( target: Union[float, dict, Tuple[float], NoneType], units: str) -> Union[float, dict, Tuple[float], NoneType]:
290def unconvert_temperature(target: FLOAT_PARAM, units: str) -> FLOAT_PARAM:
291    """Convert a linear temperature to the API-native units (Celsius).
292
293    Args:
294        target: The value to convert in Celcius.
295        units: The units of value.
296
297    Returns:
298        The converted value. Note that, if target is None, will return None.
299    """
300    if target is None:
301        return None
302
303    if isinstance(target, dict):
304        return target
305
306    return TEMPERATURE_UNCONVERTERS[units](target)

Convert a linear temperature to the API-native units (Celsius).

Arguments:
  • target: The value to convert in Celcius.
  • units: The units of value.
Returns:

The converted value. Note that, if target is None, will return None.

def convert_time(target: Optional[float], units: str) -> Optional[float]:
309def convert_time(target: OPT_FLOAT, units: str) -> OPT_FLOAT:
310    """Convert a time.
311
312    Args:
313        target: The value to convert in hours.
314        units: Desired units.
315
316    Returns:
317        The converted value. Note that, if target is None, will return None.
318    """
319    if target is None:
320        return None
321
322    return TIME_CONVERTERS[units](target)

Convert a time.

Arguments:
  • target: The value to convert in hours.
  • units: Desired units.
Returns:

The converted value. Note that, if target is None, will return None.

def unconvert_time( target: Union[float, dict, Tuple[float], NoneType], units: str) -> Union[float, dict, Tuple[float], NoneType]:
325def unconvert_time(target: FLOAT_PARAM, units: str) -> FLOAT_PARAM:
326    """Convert a time to the API-native units (hours).
327
328    Args:
329        target: The value to convert in hours.
330        units: The units of value.
331
332    Returns:
333        The converted value. Note that, if target is None, will return None.
334    """
335    if target is None:
336        return None
337
338    if isinstance(target, dict):
339        return target
340
341    return TIME_UNCONVERTERS[units](target)

Convert a time to the API-native units (hours).

Arguments:
  • target: The value to convert in hours.
  • units: The units of value.
Returns:

The converted value. Note that, if target is None, will return None.

def convert_weight(target: Optional[float], units: str) -> Optional[float]:
344def convert_weight(target: OPT_FLOAT, units: str) -> OPT_FLOAT:
345    """Convert a weight.
346
347    Args:
348        target: The value to convert in kilograms.
349        units: Desired units.
350
351    Returns:
352        The converted value. Note that, if target is None, will return None.
353    """
354    if target is None:
355        return None
356
357    return WEIGHT_CONVERTERS[units](target)

Convert a weight.

Arguments:
  • target: The value to convert in kilograms.
  • units: Desired units.
Returns:

The converted value. Note that, if target is None, will return None.

def unconvert_weight( target: Union[float, dict, Tuple[float], NoneType], units: str) -> Union[float, dict, Tuple[float], NoneType]:
360def unconvert_weight(target: FLOAT_PARAM, units: str) -> FLOAT_PARAM:
361    """Convert a weight to the API-native units (kilograms).
362
363    Args:
364        target: The value to convert in kilograms.
365        units: The units of value.
366
367    Returns:
368        The converted value. Note that, if target is None, will return None.
369    """
370    if target is None:
371        return None
372
373    if isinstance(target, dict):
374        return target
375
376    return WEIGHT_UNCONVERTERS[units](target)

Convert a weight to the API-native units (kilograms).

Arguments:
  • target: The value to convert in kilograms.
  • units: The units of value.
Returns:

The converted value. Note that, if target is None, will return None.