def copy_signatures( target_function: Callable, template_functions: List[TemplateFunction], exclude_args: Iterable[str] = None, ) -> Callable: """A decorator that copies function signatures from one or more template functions to a target function. Args: target_function: Function to modify template_functions: Functions containing params to apply to ``target_function`` """ # Start with 'self' parameter if this is an instance method fparams = {} if 'self' in signature(target_function).parameters or ismethod( target_function): fparams['self'] = forge.self # Add and combine parameters from all template functions, excluding duplicates, self, and *args for func in template_functions: new_fparams = { k: v for k, v in forge.copy(func).signature.parameters.items() if k != 'self' and v.kind != Parameter.VAR_POSITIONAL } fparams.update(new_fparams) # Manually remove any excluded parameters for key in ensure_list(exclude_args): fparams.pop(key, None) fparams = deduplicate_var_kwargs(fparams) revision = forge.sign(*fparams.values()) return revision(target_function)
def validate_ids(ids: Any) -> str: """Ensure ID(s) are all valid, and convert to a comma-delimited string if there are multiple Raises: :py:exc:`ValueError` if any values are not valid integers """ try: ids = [int(value) for value in ensure_list(ids, convert_csv=True)] except (TypeError, ValueError): raise ValueError(f'Invalid ID(s): {ids}; must specify integers only') return convert_csv_list(ids)
def convert_observation_params(params): """Some common parameter conversions needed by observation CRUD endpoints""" params = convert_observation_field_params(params) if params.get('observed_on'): params['observed_on_string'] = params.pop('observed_on') # Split out photos and sounds to upload separately photos = ensure_list(params.pop('local_photos', None)) photos.extend(ensure_list(params.pop('photos', None))) # Alias for 'local_photos' sounds = ensure_list(params.pop('sounds', None)) photo_ids = ensure_list(params.pop('photo_ids', None)) # Split API request params from common function args params, kwargs = split_common_params(params) # ignore_photos must be 1 rather than true; 0 does not work, so just remove if false if params.pop('ignore_photos', True): kwargs['ignore_photos'] = 1 return photos, sounds, photo_ids, params, kwargs
def upload_photos(observation_id: int, photos: MultiFile, **params) -> ListResponse: """Upload a local photo and assign it to an existing observation .. rubric:: Notes * :fa:`lock` :ref:`Requires authentication <auth>` * API reference: :v0:`POST /observation_photos <post-observation_photos>` Example: >>> token = get_access_token() >>> upload_photos(1234, '~/observations/2020_09_01_14003156.jpg', access_token=token) Multiple photos can be uploaded at once: >>> upload_photos( >>> 1234, >>> ['~/observations/2020_09_01_14003156.jpg', '~/observations/2020_09_01_14004223.jpg'], >>> access_token=token, >>> ) .. admonition:: Example Response :class: toggle .. literalinclude:: ../sample_data/post_observation_photos_list.json :language: javascript Args: observation_id: the ID of the observation photo: An image file, file-like object, or path access_token: Access token for user authentication, as returned by :func:`get_access_token()` Returns: Information about the uploaded photo(s) """ responses = [] for photo in ensure_list(photos): response = post( url=f'{API_V0_BASE_URL}/observation_photos', files=photo, raise_for_status=False, **{'observation_photo[observation_id]': observation_id}, **params, ) responses.append(response) # Wait until all uploads complete to raise errors for any failed uploads for response in responses: response.raise_for_status() return [response.json() for response in responses]
def ensure_model_list(values: ResponseOrObjects) -> List[BaseModel]: """If the given values are raw JSON responses, attempt to detect their type and convert to model objects """ if isinstance(values, Paginator): return values.all() values = ensure_list(values) if isinstance(values, BaseModelCollection) or isinstance( values[0], BaseModel): return values # type: ignore cls = detect_type(values[0]) return [cls.from_json(value) for value in values]
def update_observation(observation_id: int, **params) -> ListResponse: """Update a single observation .. rubric:: Notes * :fa:`lock` :ref:`Requires authentication <auth>` * API reference: :v1:`PUT /observations <Observations/put_observations>` Example: >>> token = get_access_token() >>> update_observation( >>> 17932425, >>> access_token=token, >>> description='updated description!', >>> ) .. admonition:: Example Response :class: toggle .. literalinclude:: ../sample_data/update_observation_result.json :language: javascript Returns: JSON response containing the newly updated observation(s) """ photos, sounds, photo_ids, params, kwargs = convert_observation_params( params) payload = {'observation': params} # If adding photos by ID, they must be appended to the list of existing photo IDs if photo_ids: logger.info(f'Adding {len(photo_ids)} existing photos') obs = get_observation(observation_id) combined_photo_ids = [p['id'] for p in obs['photos']] combined_photo_ids.extend(ensure_list(photo_ids)) payload['local_photos'] = {str(observation_id): combined_photo_ids} kwargs.pop('ignore_photos', None) response = put_v1(f'observations/{observation_id}', json=payload, **kwargs) upload(observation_id, photos=photos, sounds=sounds, **kwargs) return response.json()
def from_json_list(cls: Type[T], value: ResponseOrResults) -> List[T]: """Initialize a collection of model objects from an API response or response results""" return [cls.from_json(item) for item in ensure_list(value)]
def test_ensure_list__csv(input, delimiter, expected_output): assert ensure_list(input, convert_csv=True, delimiter=delimiter) == expected_output
def test_ensure_list(input, expected_output): assert ensure_list(input) == expected_output
def upload( observation_id: int, photos: MultiFile = None, sounds: MultiFile = None, photo_ids: MultiIntOrStr = None, **params, ) -> ListResponse: """Upload one or more local photo and/or sound files, and add them to an existing observation. You may also attach a previously uploaded photo by photo ID, e.g. if your photo contains multiple organisms and you want to create a separate observation for each one. .. rubric:: Notes * :fa:`lock` :ref:`Requires authentication <auth>` * API reference: :v1:`POST /observation_photos <Observation_Photos/post_observation_photos>` Example: >>> token = get_access_token() >>> upload( ... 1234, ... photos=['~/observations/2020_09_01_140031.jpg', '~/observations/2020_09_01_140042.jpg'], ... sounds='~/observations/2020_09_01_140031.mp3', ... photo_ids=[1234, 5678], ... access_token=token, ... ) .. admonition:: Example Response :class: toggle .. literalinclude:: ../sample_data/upload_photos_and_sounds.json :language: JSON Args: observation_id: the ID of the observation photos: One or more image files, file-like objects, file paths, or URLs sounds: One or more audio files, file-like objects, file paths, or URLs photo_ids: One or more IDs of previously uploaded photos to attach to the observation access_token: Access token for user authentication, as returned by :func:`get_access_token()` Returns: Information about the uploaded file(s) """ params['raise_for_status'] = False responses = [] photos, sounds = ensure_list(photos), ensure_list(sounds) logger.info(f'Uploading {len(photos)} photos and {len(sounds)} sounds') # Upload photos for photo in photos: response = post_v1( 'observation_photos', files=photo, **{'observation_photo[observation_id]': observation_id}, **params, ) responses.append(response) # Upload sounds for sound in sounds: response = post_v1( 'observation_sounds', files=sound, **{'observation_sound[observation_id]': observation_id}, **params, ) responses.append(response) # Attach previously uploaded photos by ID if photo_ids: response = update_observation(observation_id, photo_ids=photo_ids, access_token=params.get( 'access_token', None)) responses.append(response) # Wait until all uploads complete to raise errors for any failed uploads for response in responses: response.raise_for_status() return [response.json() for response in responses]