def upsert_participants( self, program_id: str, records_df: pd.DataFrame, new_flag: str = "add", mode_flag: str = "tx", ): """ From https://support.signalvine.com/hc/en-us/articles/360023207353-API-documentation It's on you to format the dates correctly in the dataframe. Also, this drops duplicates. No quarter. It's mayhem in SV if a phone already exists. Mode is 'row' or 'tx'. In tx mode, it's all or nothing. """ # Drop the duplicates... TODO log duplicate numbers # records_df = records_df.drop_duplicates(subset=["phone"], keep=False) participant_path = f"/v2/programs/{program_id}/participants" body = make_body( program_id=program_id, content_df=records_df, new_flag=new_flag, mode_flag=mode_flag, ) header_body = json.dumps(body, separators=(",", ":"), sort_keys=False) headers = build_headers( token=self.account_token, secret=self.account_secret, action="POST", path_no_query=participant_path, body=header_body, ) url = f"{self.api_hostname}{participant_path}" r = requests.post(url, json=body, headers=headers) # Things get funky here. We're looking for a 202, and if so, # get a Location from the headers, then GET that (in a loop?!) # until we see "complete". if r.status_code == 202: # return the location path so we can orchestrate it outside of here. location_path = r.headers["Location"] return location_path else: raise APIError(r.status_code, f"API reason: {r.text}")
def test_build_headers(self): result = build_headers( token="INVENTED_TOKEN", secret="INVENTED_TOKEN", path_no_query="/bogus", action="GET", ) # The cryptostring changes with the date from sign_request assert result["Authorization"].startswith("SignalVine INVENTED_TOKEN:") # The content-type is json assert result["Content-Type"] == "application/json" # A date should exist assert result["SignalVine-Date"]
def get_programs(self, include_active: bool = True) -> List: """ Get the program info for a specific account. """ participant_path = f"/v1/accounts/{self.account_number}/programs" headers = build_headers( self.account_token, self.account_secret, "GET", participant_path ) url = f"{self.api_hostname}{participant_path}" if include_active: # To ensure we get a list of all programs, not just the active ones. url += "?active=all" r = requests.get(url, headers=headers) if r.status_code == 200: return r.json()["items"] else: raise APIError(r.status_code, f"API reason: {r.text}")
def get_location_status(self, location_path: str) -> Tuple[bool, str]: url = f"{self.api_hostname}{location_path}" headers = build_headers( token=self.account_token, secret=self.account_secret, path_no_query=location_path, ) r = requests.get(url, headers=headers) if r.status_code == 200: status_json = r.json() if status_json["complete"] == True: # only if this is complete do we care to sift through it. if status_json["error"] == True: return True, status_json["message"] else: return True, None else: # not complete; just say so return False, None else: raise APIError(r.status_code, f"API reason: {r.text}")
def get_participants_chunk( self, program_id: str, chunk_size: int = 500, offset: int = 0, include_active: bool = True, ) -> List: """ A helper function that lets us get a page of contacts at a time. If used to page, ensure the chunk_size is the same, or the 'pages' don't work anymore. Returns a list of json records. The column names we're looking for are in a field called profile, so we'll straighten that out later. This method returns 'raw' participant info, mostly exactly what comes from SV. """ participant_path = f"/v1/programs/{program_id}/participants" headers = build_headers( self.account_token, self.account_secret, "GET", participant_path ) url = f"{self.api_hostname}{participant_path}?type=full&count={chunk_size}&offset={offset}" if include_active: # Otherwise we only get the active fields url += "&active=all" r = requests.get(url, headers=headers) if r.status_code == 200: return r.json()["items"] else: raise APIError(r.status_code, f"API reason: {r.text}")
def get_program_schema(self, program_id, convert_to_python_types=False) -> Dict: """ Get the schema for the participant records Return it as a dictionary in the form {name:type}. Type is still the SignalVine type. Set convert_to_python_types to True to return Python primative types instead """ schema_path = f"/v1/programs/{program_id}" # We build the headers off the path alone without query headers = build_headers( self.account_token, self.account_secret, "GET", schema_path ) url = f"{self.api_hostname}{schema_path}" # We could also just tack these to the end of the url params = {"type": "schema"} r = requests.get(url, params=params, headers=headers) if r.status_code == 200: # convert the fields to a dict fields_dict = {} for item in r.json()["fields"]: fields_dict[item["name"]] = item["type"] if convert_to_python_types: return convert_sv_types(fields_dict) else: return fields_dict else: raise APIError(r.status_code, f"API reason: {r.text}")