def __init__(self,
                 user: str,
                 pwd: Optional[str] = None,
                 app_id: Optional[str] = None,
                 app_secret: Optional[str] = None):

        if flags['USE_SECURE_KEYRING']:

            # TODO: Figure out a better way to store creds in windows that corresponds to the service / user / password model
            #  that keyring uses

            self.app_id = keyring.get_password('iNat_app_id',
                                               '{0}'.format(user))
            self.app_secret = keyring.get_password('iNat_secret',
                                                   '{0}'.format(user))

            # TODO: Add some error checking and logging around this
            self.token = get_access_token(username=user,
                                          password=keyring.get_password(
                                              'iNat', user),
                                          app_id=self.app_id,
                                          app_secret=self.app_secret)
        else:

            self.app_id = app_id
            self.app_secret = app_secret

            self.token = get_access_token(username=user,
                                          password=pwd,
                                          app_id=app_id,
                                          app_secret=app_secret)
Esempio n. 2
0
def test_get_access_token_fail(requests_mock):
    """ If we provide incorrect credentials to get_access_token(), an AuthenticationError is raised"""

    rejection_json = {
        "error": "invalid_client",
        "error_description": "Client authentication failed due to "
        "unknown client, no client authentication "
        "included, or unsupported authentication "
        "method.",
    }
    requests_mock.post(
        "https://www.inaturalist.org/oauth/token", json=rejection_json, status_code=401,
    )

    with pytest.raises(AuthenticationError):
        get_access_token("username", "password", "app_id", "app_secret")
Esempio n. 3
0
    def handle(self, *args, **options):
        self.w(
            "Will push our observations to iNaturalist... (the observations that originate from iNaturalist won't "
            "be pushed.")

        observations = list(Individual.from_vespawatch_objects.all()) + list(
            Nest.from_vespawatch_objects.all())
        self.w(f"We currently have {len(observations)} pushable observations.")

        self.w("Getting an access token for iNaturalist...", ending="")
        token = get_access_token(username=settings.INAT_USER_USERNAME,
                                 password=settings.INAT_USER_PASSWORD,
                                 app_id=settings.INAT_APP_ID,
                                 app_secret=settings.INAT_APP_SECRET)
        self.w("OK")

        for obs in observations:
            self.w(f"Pushing our observation with id={obs.pk}...", ending="")
            if obs.exists_in_inaturalist:
                self.w("This observation was already pushed, we'll update. ",
                       ending="")
                try:
                    obs.update_at_inaturalist(access_token=token)
                    self.w("OK")
                except HTTPError as exc:
                    self.w(
                        self.style.ERROR(
                            "iNat returned an error: does the observation exists there and do we have "
                            "the right to update it? Exception: ") + str(exc))
            else:
                self.w(
                    "This is a new observation, we'll create it at iNaturalist. ",
                    ending="")
                obs.create_at_inaturalist(access_token=token)
                self.w("OK")

        self.w(
            "Will now ensure locally deleted vespawatch observations are also deleted at iNaturalist..."
        )
        for obs in InatObsToDelete.objects.all():
            self.w(
                f"Deleting iNaturalist observation #{obs.inaturalist_id}...",
                ending='')
            try:
                delete_observation(observation_id=obs.inaturalist_id,
                                   access_token=token)
            except JSONDecodeError:
                # (temporary?) iNaturalist API issue...
                pass
            obs.delete()
            self.w("OK")
Esempio n. 4
0
def run_observation_crud_test():
    # TODO: Built-in support for using envars for auth instead of function args might be useful
    token = get_access_token(
        username=getenv("INAT_USERNAME"),
        password=getenv("INAT_PASSWORD"),
        app_id=getenv("INAT_APP_ID"),
        app_secret=getenv("INAT_APP_SECRET"),
    )
    print("Received access token")

    test_obs_id = create_test_obs(token)
    update_test_obs(test_obs_id, token)
    delete_test_obs(test_obs_id, token)
    print("Test complete")
Esempio n. 5
0
def test_get_access_token(requests_mock):
    """ Test a successful call to get_access_token() """

    accepted_json = {
        "access_token": "604e5df329b98eecd22bb0a84f88b68a075a023ac437f2317b02f3a9ba414a08",
        "token_type": "Bearer",
        "scope": "write",
        "created_at": 1539352135,
    }
    requests_mock.post(
        "https://www.inaturalist.org/oauth/token", json=accepted_json, status_code=200,
    )

    token = get_access_token("valid_username", "valid_password", "valid_app_id", "valid_app_secret")

    assert token == "604e5df329b98eecd22bb0a84f88b68a075a023ac437f2317b02f3a9ba414a08"
    def handle(self, *args, **options):
        pyinaturalist.user_agent = USER_AGENT

        if settings.INATURALIST_PUSH:
            token = get_access_token(username=settings.INAT_USER_USERNAME,
                                     password=settings.INAT_USER_PASSWORD,
                                     app_id=settings.INAT_APP_ID,
                                     app_secret=settings.INAT_APP_SECRET)
        else:
            token = None

        if settings.INATURALIST_PUSH:
            self.push_deletes(token)
            self.push_created(token)
        else:
            self.w("Not pushing objects because of settings.INATURALIST_PUSH")

        if not options['pushonly']:
            pulled_inat_ids = self.pull()
            self.check_all_missing(pulled_inat_ids)

            config.LAST_PULL_COMPLETED_AT = datetime.datetime.now()
        self.w("\ndone\n")
Esempio n. 7
0
# the taxon number by looking at the folder name and taking all the digits it
# sees. This allows you to name the folder "##### species name" to quickly
# tell where photos go. For example anything in '52381-Aphididae' is uploaded
# as an aphid.
def get_taxon(folder):
    taxon = ''
    folder = os.path.split(os.path.dirname(folder_name))[-1]
    for character in folder:
        if character.isdigit():
            taxon = taxon + character
    return taxon


# This is getting a token to allow photos to be uploaded.
token = get_access_token(username=user,
                         password=passw,
                         app_id=app,
                         app_secret=secret)

# This goes to every file, checks if it is a jpg, gets the gps coordinates,
# get the time, and uploads it to iNaturalist.
for file in file_paths:
    if file[-3:] == 'jpg' or file[-3:] == 'JPG' or file[-3:] == 'Jpg':
        print('Uploading ' + file)
        try:
            img = PIL.Image.open(file)
            coordinates = get_lat_long(img)
        except:
            coordinates = 'No Coordinates'
        try:
            img = PIL.Image.open(file)
            date_time = get_date(img)
Esempio n. 8
0
def test_user_agent(requests_mock):
    # TODO: test for all functions that access the inaturalist API?
    requests_mock.get(
        urljoin(INAT_NODE_API_BASE_URL, "observations"),
        json=load_sample_data("get_observation.json"),
        status_code=200,
    )
    accepted_json = {
        "access_token": "604e5df329b98eecd22bb0a84f88b68a075a023ac437f2317b02f3a9ba414a08",
        "token_type": "Bearer",
        "scope": "write",
        "created_at": 1539352135,
    }
    requests_mock.post(
        "https://www.inaturalist.org/oauth/token", json=accepted_json, status_code=200,
    )

    default_ua = "Pyinaturalist/{v}".format(v=pyinaturalist.__version__)

    # By default, we have a 'Pyinaturalist' user agent:
    get_observation(observation_id=16227955)
    assert requests_mock._adapter.last_request._request.headers["User-Agent"] == default_ua
    get_access_token("valid_username", "valid_password", "valid_app_id", "valid_app_secret")
    assert requests_mock._adapter.last_request._request.headers["User-Agent"] == default_ua

    # But if the user sets a custom one, it is indeed used:
    get_observation(observation_id=16227955, user_agent="CustomUA")
    assert requests_mock._adapter.last_request._request.headers["User-Agent"] == "CustomUA"
    get_access_token(
        "valid_username",
        "valid_password",
        "valid_app_id",
        "valid_app_secret",
        user_agent="CustomUA",
    )
    assert requests_mock._adapter.last_request._request.headers["User-Agent"] == "CustomUA"

    # We can also set it globally:
    pyinaturalist.user_agent = "GlobalUA"
    get_observation(observation_id=16227955)
    assert requests_mock._adapter.last_request._request.headers["User-Agent"] == "GlobalUA"
    get_access_token("valid_username", "valid_password", "valid_app_id", "valid_app_secret")
    assert requests_mock._adapter.last_request._request.headers["User-Agent"] == "GlobalUA"

    # And it persists across requests:
    get_observation(observation_id=16227955)
    assert requests_mock._adapter.last_request._request.headers["User-Agent"] == "GlobalUA"
    get_access_token("valid_username", "valid_password", "valid_app_id", "valid_app_secret")
    assert requests_mock._adapter.last_request._request.headers["User-Agent"] == "GlobalUA"

    # But if we have a global and local one, the local has priority
    get_observation(observation_id=16227955, user_agent="CustomUA 2")
    assert requests_mock._adapter.last_request._request.headers["User-Agent"] == "CustomUA 2"
    get_access_token(
        "valid_username",
        "valid_password",
        "valid_app_id",
        "valid_app_secret",
        user_agent="CustomUA 2",
    )
    assert requests_mock._adapter.last_request._request.headers["User-Agent"] == "CustomUA 2"

    # We can reset the global settings to the default:
    pyinaturalist.user_agent = pyinaturalist.DEFAULT_USER_AGENT
    get_observation(observation_id=16227955)
    assert requests_mock._adapter.last_request._request.headers["User-Agent"] == default_ua
    get_access_token("valid_username", "valid_password", "valid_app_id", "valid_app_secret")
    assert requests_mock._adapter.last_request._request.headers["User-Agent"] == default_ua
Esempio n. 9
0
def upload_folder_multiple(species_folder, folder, uploaded_folder, time_zone,
                           accuracy, user, passw, app, secret):
    # Makes a list of all files in the folder inside element 2 of a tuple
    for file in os.walk(folder):
        if file[0] == folder:
            files = file

    # Creates list of all the file paths for every file in the folder.
    file_paths = []
    for file in files[2]:  # All files are in files[2]
        file_path = files[0] + file  # files[0] has the path to the folder
        file_paths.append(file_path)  # Makes a big list of paths

    # This is getting a token to allow photos to be uploaded.
    token = get_access_token(username=user,
                             password=passw,
                             app_id=app,
                             app_secret=secret)

    # This goes to every file, checks if it is a jpg, gets the gps coordinates,
    # get the time, and uploads it to iNaturalist.
    jpgs = []
    for file in file_paths:
        if file[-3:].lower() == 'jpg':
            jpgs.append(file)

    try:
        img = PIL.Image.open(jpgs[0])
        coordinates = get_lat_long(img)
        img.close()
    except:
        coordinates = 'No Coordinates'
    try:
        date_time = get_date(file)
        img.close()
    except:
        date_time = 'No Date or Time'

    [species, taxon] = get_taxon(species_folder)
    print(species)
    #    print(coordinates)
    #    print(date_time)
    #    print(' the taxon is ' + str(taxon))

    params = {
        'observation': {
            'taxon_id':
            taxon,
            'species_guess':
            species,
            'observed_on_string':
            date_time,
            'time_zone':
            time_zone,
            'description':
            '',
            'tag_list':
            '',
            'latitude':
            coordinates[0],
            'longitude':
            coordinates[1],
            'positional_accuracy':
            int(accuracy),  # meters,
            'observation_field_values_attributes': [{
                'observation_field_id': '',
                'value': ''
            }],
        },
    }
    r = create_observations(params=params, access_token=token)

    new_observation_id = r[0]['id']

    print('Uploaded as observation #' + str(new_observation_id))
    print('Uploading photos')
    for file in jpgs:
        print('uploading ' + str(file) + ' TO ' + str(new_observation_id))
        r = add_photo_to_observation(observation_id=new_observation_id,
                                     file_object=open(file, 'rb'),
                                     access_token=token)

    folder_name = os.path.split(folder)
    folder1_name = os.path.split(folder_name[0])
    folder2_name = os.path.split(folder1_name[0])

    new_species_folder = uploaded_folder + folder2_name[1] + '/'
    destination = new_species_folder + folder1_name[1]

    if new_observation_id:

        try:
            os.mkdir(new_species_folder)
        except:
            pass
        try:
            shutil.move(folder, destination)
        except:
            print('failed file move')
            pass
Esempio n. 10
0
Extra dependencies:
    `pip install beautifulsoup4`
"""
from pprint import pprint

import requests
from bs4 import BeautifulSoup
from pyinaturalist.node_api import get_observation
from pyinaturalist.rest_api import get_access_token

# !! Replace values here !!
OBSERVATION_IDS = [99]
ACCESS_TOKEN = get_access_token(
    username='',
    password='',
    app_id='',
    app_secret='',
)

IGNORE_ATTRIBUTES = ['Associated observations', 'Sizes']
PHOTO_INFO_BASE_URL = 'https://www.inaturalist.org/photos'


def get_photo_metadata(photo_url, access_token):
    """Scrape content from a photo info URL, and attempt to get its metadata"""
    print(f'Fetching {photo_url}')
    photo_page = requests.get(
        photo_url, headers={'Authorization': f'Bearer {access_token}'})
    soup = BeautifulSoup(photo_page.content, 'html.parser')
    table = soup.find(id='wrapper').find_all('table')[1]
Esempio n. 11
0
def inaturalist_api():
    json_data = json.loads(request.data)
    print(json_data)
    utc_key = json_data['utc_key']
    print(f'utc_key: {utc_key}')
    if utc_key is None:
        return
    print(f'key: {utc_key}')
    conn = sqlite3.connect(DB_PATH, timeout=15)
    query = '''SELECT datetime, file_name, prediction, true_label, inaturalist_id
               FROM results 
               WHERE utc_datetime = ?
               LIMIT 1;'''
    c = conn.cursor()
    c.execute(query, (utc_key, ))
    row = c.fetchone()
    if row is None:
        return
    else:
        obs_timestamp = row[0]
        img_fn = row[1]
        pred_label = row[2]
        true_label = row[3]
        existing_inat_id = row[4]
    if true_label is not None:
        obs_label = true_label
    else:
        obs_label = pred_label

    # Get a token for the inaturalist API
    token = get_access_token(
        username=INAT_USERNAME,
        password=INAT_PASSWORD,
        app_id=INAT_APP_ID,
        app_secret=INAT_APP_SECRET,
    )

    obs_file_name = f'{DATA_DIR}/imgs/{img_fn}'

    # Upload the observation to iNaturalist
    if existing_inat_id is None:
        # Check if there's an existing inat id within 5 minutes of this image
        # upload this image to that observation if so.
        window_timestamp = dt.datetime.fromisoformat(utc_key) - dt.timedelta(
            minutes=10)
        window_timestamp = window_timestamp.strftime(dt_fmt)
        query = '''SELECT inaturalist_id
                   FROM results 
                   WHERE utc_datetime <= :utc_dt
                   AND utc_datetime >= :prev_dt
                   AND inaturalist_id IS NOT NULL
                   AND (true_label = :lab OR (true_label IS NULL AND prediction = :lab))
                   ORDER BY utc_datetime DESC
                   LIMIT 1;'''
        c.execute(query, {
            'utc_dt': utc_key,
            'prev_dt': window_timestamp,
            'lab': obs_label
        })
        row = c.fetchone()
        if row is None or row[0] is None:
            response = create_observation(
                taxon_id=species_map[obs_label]['taxa_id'],
                observed_on_string=obs_timestamp,
                time_zone='Mountain Time (US & Canada)',
                description=
                'Birb Cam image upload: https://github.com/evjrob/birbcam',
                tag_list=f'{obs_label}, Canada',
                latitude=INAT_LATITUDE,
                longitude=INAT_LONGITUDE,
                positional_accuracy=INAT_POSITIONAL_ACCURACY,  # meters,
                access_token=token,
            )
            inat_observation_id = response[0]['id']
            print(
                f'No iNaturalist id found in previous ten minutes, creating new row with id {inat_observation_id}.'
            )
        else:
            inat_observation_id = row[0]
            print(
                f'Found iNaturalist id in previous ten minutes, adding to id {inat_observation_id}.'
            )
        # Upload the image captured
        r = add_photo_to_observation(
            inat_observation_id,
            access_token=token,
            photo=obs_file_name,
        )
        # Update the row in the database with the inaturalist id
        c.execute("UPDATE results SET inaturalist_id=? WHERE utc_datetime=?;",
                  (inat_observation_id, utc_key))
    else:
        # This image had already been uploaded, we do not want to upload it again
        inat_observation_id = existing_inat_id
        print(
            f'Found existing iNaturalist id {inat_observation_id} for row, skipping.'
        )

    conn.commit()
    conn.close()
    return jsonify({'inat_id': inat_observation_id})