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)
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")
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")
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")
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")
# 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)
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
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
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]
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})