Exemplo n.º 1
0
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)
Exemplo n.º 2
0
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)
Exemplo n.º 3
0
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
Exemplo n.º 4
0
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]
Exemplo n.º 5
0
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]
Exemplo n.º 6
0
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()
Exemplo n.º 7
0
 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)]
Exemplo n.º 8
0
def test_ensure_list__csv(input, delimiter, expected_output):
    assert ensure_list(input, convert_csv=True,
                       delimiter=delimiter) == expected_output
Exemplo n.º 9
0
def test_ensure_list(input, expected_output):
    assert ensure_list(input) == expected_output
Exemplo n.º 10
0
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]