├── README.md └── affinity.py /README.md: -------------------------------------------------------------------------------- 1 | # Affinity Python Wrapper 2 | 3 | A Python wrapper for Affinity's CRM platform. Includes functions for all available requests to their API and an OOD representation of their data model. Refer to: https://api-docs.affinity.co/ 4 | 5 | # Classes 6 | 7 | List: The Affinity equivalent of a spreadsheet. It contains a collection of either people or organizations (aka entities). 8 | List Entry: The Affinity equivalent of a row in a spreadsheet. 9 | 10 | Field: The Affinity equivalent of a column in a spreadsheet. A field can be specific to a given list, or it can be global. 11 | 12 | Field Value: The Affinity equivalent of the data in a spreadsheet cell. 13 | 14 | Person: Contacts of your organization. 15 | 16 | Organization: An external company that your team is in touch with - this could be an organization that you are trying to invest in, sell to, or establish a relationship with. 17 | 18 | Opportunity: A potential future sale or deal for your team, generally used to track the progress of and revenue generated from sales and deals in your pipeline with a specific organization. 19 | 20 | Note: A note object contains content, which is a string containing the note body. In addition, a note can be associated with multiple people or organizations. 21 | 22 | # API Methods 23 | 24 | LISTS 25 | 26 | get_all_lists() 27 | 28 | get_list(list_id) 29 | 30 | populate_list(json_in) 31 | 32 | 33 | LIST ENTRIES 34 | 35 | get_all_list_entries(list_id) 36 | 37 | get_list_entry(list_id, list_entry_id) 38 | 39 | create_list_entry(list_id, entity_id, creator_id) 40 | 41 | delete_list_entry(list_id, list_entry_id) 42 | 43 | populate_list_entry(json_in) 44 | 45 | 46 | FIELDS 47 | 48 | populate_field(json_in) 49 | 50 | 51 | FIELD VALUES 52 | 53 | get_field_values(person_id, organization_id, list_entry_id) 54 | 55 | create_field_value(field_id, entity_id, list_entry_id, value) 56 | 57 | update_field_value(field_value_id, value) 58 | 59 | delete_field_value(field_value_id) 60 | 61 | populate_field_value(json_in) 62 | 63 | 64 | PEOPLE 65 | 66 | get_persons(term, page_size, page_token) 67 | 68 | create_person(first_name, last_name, emails, phone_numbers, organization_ids) 69 | 70 | update_person(person_id, first_name, last_name, emails, phone_numbers, organization_ids) 71 | 72 | delete_person(person_id) 73 | 74 | get_people_global_fields(person_id) 75 | 76 | populate_person(json_in) 77 | 78 | 79 | ORGANIZATION 80 | 81 | get_organizations(term, page_size, page_token) 82 | 83 | create_organization(name, domain, person_ids) 84 | 85 | update_organization(organization_id, name, domain, person_ids) 86 | 87 | delete_organization(organizaton_id) 88 | 89 | get_organizations_global_fields() 90 | 91 | populate_organization(json_in) 92 | 93 | 94 | OPPORTUNITIES 95 | 96 | get_opportunities(term, page_size, page_token) 97 | 98 | get_opportunity(opportunity_id) 99 | 100 | create_opportunities(name, person_ids, organization_ids) 101 | 102 | update_opportunity(organization_id, name, person_ids, orgnaization_ids) 103 | 104 | delete_opportunity(opportunity_id) 105 | 106 | 107 | NOTES 108 | 109 | populate_note(json_in) 110 | 111 | 112 | RELATIONSHIP STRENGTHS 113 | 114 | get_relationship_strength(internal_id, external_id) 115 | 116 | -------------------------------------------------------------------------------- /affinity.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | __author__ = "Justin Stals" 3 | 4 | # ------------------------------------------------------ # 5 | # AFFINITY PYTHON WRAPPER 6 | # ------------------------------------------------------ # 7 | 8 | 9 | # ------------------------------------------------------ # 10 | # KEY DEPENDENCIES 11 | # ------------------------------------------------------ # 12 | 13 | import json 14 | import datetime 15 | import requests as re 16 | from requests.auth import HTTPBasicAuth 17 | 18 | # ------------------------------------------------------ # 19 | # GLOBAL VARIABLES 20 | # ------------------------------------------------------ # 21 | 22 | api_key = '' 23 | daily_rate_limit = 150000 24 | endpoint_base = 'https://api.affinity.vc/' 25 | headers = {'Content-Type': 'application/json'} 26 | 27 | # ------------------------------------------------------ # 28 | # API CALLS 29 | # ------------------------------------------------------ # 30 | 31 | def get(call): 32 | r = re.get(call, auth=HTTPBasicAuth('', api_key), headers=headers) 33 | if check_err(r): 34 | return None 35 | return r.json() 36 | 37 | def post(call, data): 38 | r = re.post(call, data=data, auth=HTTPBasicAuth('', api_key)) 39 | if check_err(r): 40 | return None 41 | return r.json() 42 | 43 | def put(call, data): 44 | r = re.put(call, data=data, auth=HTTPBasicAuth('', api_key)) 45 | if check_err(r): 46 | return None 47 | return r.json() 48 | 49 | def delete(call): 50 | r = re.delete(call, auth=HTTPBasicAuth('', api_key)) 51 | if check_err(r): 52 | return None 53 | return r.json() 54 | 55 | # ------------------------------------------------------ # 56 | # LISTS 57 | # ------------------------------------------------------ # 58 | 59 | class List: 60 | 61 | 'The Affinity equivalent of a spreadsheet. It contains a collection of either people or organizations (aka entities).' 62 | 63 | def __init__(self, list_id, list_type, name, public, owner_id, list_size): 64 | self.id = list_id 65 | # (integer) The unique identifier of the list object. 66 | self.type = list_type 67 | # (integer) The type of the entities contained within the list. A list can contain people or organizations, but not both. 68 | self.name = name 69 | # (string) The title of the list that is displayed in Affinity. 70 | self.public = public 71 | # (boolean) If the list is publicly accessible to all users in your team, this is true. Otherwise, this is false. 72 | self.owner_id = owner_id 73 | # (integer) The unique id of the internal person who created this list. 74 | self.list_size = list_size 75 | # (integer) The number of list entries contained within the list. 76 | 77 | def populate_list(json_in): 78 | list_out = List( 79 | json_in['id'], 80 | json_in['type'], 81 | json_in['name'], 82 | json_in['public'], 83 | json_in['owner_id'], 84 | json_in['list_size'] 85 | ) 86 | return list_out 87 | 88 | def get_all_lists(): 89 | # Returns a collection of all the lists visible to you. 90 | call = endpoint_base + '/lists' 91 | r = get(call) 92 | return r 93 | 94 | def get_list(list_id): 95 | # Gets the details for a specific list given the existing list id. 96 | call = endpoint_base + '/lists/' + str(list_id) 97 | return get(call) 98 | 99 | # ------------------------------------------------------ # 100 | # LIST ENTRIES 101 | # ------------------------------------------------------ # 102 | 103 | class ListEntry: 104 | 105 | 'The Affinity equivalent of a row in a spreadsheet.' 106 | 107 | def __init__(self, list_entry_id, list_id, creator_id, entity_id, entity, created_at): 108 | self.id = list_entry_id 109 | # (integer) The unique identifier of the list entry object. 110 | self.list_id = list_id 111 | # (integer) The unique identifier of the list on which the list entry resides. 112 | self.creator_id = creator_id 113 | # (integer) The unique identifier of the user who created the list entry. 114 | self.entity_id = entity_id 115 | # (integer) The unique identifier of the entity corresponding to the list entry. 116 | self.entity = entity 117 | # (object) Object containing entity-specific details like name, email address, domain etc. 118 | self.created_at = created_at 119 | # (datetime) The time when the list entry was created. 120 | 121 | def populate_list_entry(json_in): 122 | list_entry_out = ListEntry( 123 | json_in['id'], 124 | json_in['list_id'], 125 | json_in['creator_id'], 126 | json_in['entity_id'], 127 | json_in['entity'], 128 | json_in['created_at'] 129 | ) 130 | return list_entry_out 131 | 132 | def get_all_list_entries(list_id): 133 | # Fetches all the list entries in the list with the supplied list id. 134 | call = endpoint_base + '/lists/' + str(list_id) + '/list-entries' 135 | return get(call) 136 | 137 | def get_list_entry(list_id, list_entry_id): 138 | # Fetches a list entry with a specified id. 139 | call = endpoint_base + '/lists/' + str(list_id) + '/list-entries/' + list_entry_id 140 | return get(call) 141 | 142 | def create_list_entry(list_id, entity_id, creator_id): 143 | # Creates a new list entry in the list with the supplied list id. 144 | call = endpoint_base + '/lists/' + str(list_id) + '/list-entries' 145 | data = {'entity_id':entity_id, 'creator_id':creator_id} 146 | return post(call, data) 147 | 148 | def delete_list_entry(list_id, list_entry_id): 149 | # Deletes a list entry with a specified list_entry_id. 150 | call = endpoint_base + '/lists/' + str(list_id) + '/list-entries/' + str(list_entry_id) 151 | return delete(call) 152 | 153 | # ------------------------------------------------------ # 154 | # FIELDS 155 | # ------------------------------------------------------ # 156 | 157 | class Field: 158 | 159 | 'The Affinity equivalent of a column in a spreadsheet. A field can be specific to a given list, or it can be global.' 160 | 161 | def __init__(self, field_id, name, allows_multiple, dropdown_options, value_type): 162 | self.id = field_id 163 | # (integer) The unique identifier of the field object. 164 | self.name = name 165 | # (string) The name of the field. 166 | self.allows_multiple = allows_multiple 167 | # (boolean) This determines whether multiple values can be added to a single cell for the field. 168 | self.dropdown_options = dropdown_options 169 | # (object[]) Affinity supports pre-entered dropdown options for fields of the 'Ranked Dropdowon value_type. 170 | self.value_type = value_type 171 | # (integer) Describes what values can be associated with the field. 172 | 173 | def populate_field(json_in): 174 | field_out = Field( 175 | json_in['field_id'], 176 | json_in['name'], 177 | json_in['allows_multiple'], 178 | json_in['dropdown_options'], 179 | json_in['value_type'] 180 | ) 181 | return field_out 182 | 183 | # ------------------------------------------------------ # 184 | # FIELD VALUES 185 | # ------------------------------------------------------ # 186 | 187 | class FieldValue: 188 | 189 | 'The Affinity equivalent of the data in a spreadsheet cell.' 190 | 191 | def __init__(self, field_value_id, field_id, entity_id, list_entry_id, value): 192 | self.id = field_value_id 193 | # (integer) The unique identifier of the field value object. 194 | self.field_id = field_id 195 | # (integer) The unique identifier of the field the value is associated with. 196 | self.entity_id = entity_id 197 | # (integer) The unique identifier of the person or organization object the field value is associated with. 198 | self.list_entry_id = list_entry_id 199 | # (integer) The unique identifier of the list entry object the field value is associated with. 200 | self.value = value 201 | # (One of many) The value attribute can take on many different types, depending on the field value_type. 202 | 203 | def populate_field_value(json_in): 204 | field_value_out = FieldValue( 205 | json_in['field_value_id'], 206 | json_in['field_id'], 207 | json_in['entity_id'], 208 | json_in['list_entry_id'], 209 | json_in['value'] 210 | ) 211 | return field_value_out 212 | 213 | def get_field_values(person_id, organization_id, list_entry_id): 214 | # Returns all field values attached to a person, organization or list_entry. 215 | call = endpoint_base + '/field-values' 216 | if person_id: 217 | call += '?person_id=' + str(person_id) 218 | if organization_id: 219 | call += '?organization_id=' + str(organization_id) 220 | if list_entry_id: 221 | call += '?list_entry_id=' + str(list_entry_id) 222 | return get(call) 223 | 224 | def create_field_value(field_id, entity_id, list_entry_id, value): 225 | # Creates a new field value with the supplied parameters. 226 | call = endpoint_base + '/field-values' 227 | data = {'field_id':field_id, 'entity_id':entity_id, 'list_entry_id':list_entry_id, 'value':value} 228 | return post(call, data) 229 | 230 | def update_field_value(field_value_id, value): 231 | # Updates the existing field value with field_value_id with the supplied parameters. 232 | call = endpoint_base + '/field-values/' + str(field_value_id) 233 | data = {'value':value} 234 | return put(call, data) 235 | 236 | def delete_field_value(field_value_id): 237 | # Deletes an field value with the specified field_value_id. 238 | call = endpoint_base + '/field-values/' + str(field_value_id) 239 | return delete(call) 240 | 241 | # ------------------------------------------------------ # 242 | # PEOPLE 243 | # ------------------------------------------------------ # 244 | 245 | class Person: 246 | 247 | 'Contacts of your organization.' 248 | 249 | def __init__(self, person_id, person_type, first_name, last_name, emails, phone_numbers, primary_email, organization_ids, list_entries): 250 | self.id = person_id 251 | # (integer) The unique identifier of the person object. 252 | self.type = person_type 253 | # (integer) The type of person, either (0 - external) or (1 - internal) to your organization. 254 | self.first_name = first_name 255 | # (string) The first name of the person. 256 | self.last_name = last_name 257 | # (string) The last name of the person. 258 | self.emails = emails 259 | # (string[]) The email addresses of the person. 260 | self.phone_numbers = phone_numbers 261 | # (string[]) The phone numbers of the person. 262 | self.primary_email = primary_email 263 | # (string) The email (automatically computed) that is most likely to the current active email address of the person. 264 | self.organization_ids = organization_ids 265 | # (integer[]) An array of unique identifiers of organizations that the person is associated with. 266 | self.list_entries = list_entries 267 | # (ListEntry[]) An array of list entry resources associated with the person, only returned as part of the 'Get a specific person' endpoint. 268 | 269 | def populate_person(json_in): 270 | person_out = Person( 271 | json_in['field_value_id'], 272 | json_in['field_id'], 273 | json_in['entity_id'], 274 | json_in['list_entry_id'], 275 | json_in['value'] 276 | ) 277 | return person_out 278 | 279 | def get_persons(term, page_size, page_token): 280 | # Searches your teams data and fetches all the persons that meet the search criteria. 281 | call = endpoint_base + 'persons' 282 | if term: 283 | call += '?term=' + term 284 | else: 285 | print('Usage: get_persons(term, page_size, page_token)') 286 | return 287 | if page_size: 288 | call += '&page_size=' + page_size 289 | if page_token: 290 | call += '&page_token=' + page_token 291 | return get(call) 292 | 293 | def get_person(person_id): 294 | # Fetches a person with a specified person_id. 295 | call = endpoint_base + '/persons/' + str(person_id) 296 | return get(call) 297 | 298 | def create_person(first_name, last_name, emails, phone_numbers, organization_ids): 299 | # Creates a new person with the supplied parameters. 300 | call = endpoint_base + '/persons' 301 | data = {'first_name':first_name, 'last_name':last_name, 'emails[]':emails, 'phone_numbers[]':phone_numbers, 'organization_ids[]':organization_ids} 302 | return post(call, data) 303 | 304 | def update_person(person_id, first_name, last_name, emails, phone_numbers, organization_ids): 305 | # Updates an existing person with person_id with the supplied parameters. Only attributes that need to be changed must be passed in. 306 | call = endpoint_base + '/persons/' + str(person_id) 307 | data = {'first_name':first_name, 'last_name':last_name, 'emails':emails, 'phone_numbers[]':phone_numbers, 'organization_ids[]':organization_ids} 308 | return put(call, data) 309 | 310 | def delete_person(person_id): 311 | # Deletes a person with a specified person_id. 312 | call = endpoint_base + '/persons/' + str(person_id) 313 | return delete(call) 314 | 315 | def get_people_global_fields(): 316 | # Fetches an array of all the global fields that exist on people. 317 | call = endpoint_base + '/persons/fields' 318 | return get(call) 319 | 320 | # ------------------------------------------------------ # 321 | # ORGANIZATIONS 322 | # ------------------------------------------------------ # 323 | 324 | class Organization: 325 | 326 | 'An external company that your team is in touch with - this could be an organization that you are trying to invest in, sell to, or establish a relationship with.' 327 | 328 | def __init__(self, organization_id, name, domain, person_ids, org_global, list_entries): 329 | self.id = organization_id 330 | # (integer) The unique identifier of the organization object. 331 | self.name = name 332 | # (integer) The name of the organization (see below). 333 | self.domain = domain 334 | # (string) The website name of the organization. 335 | self.person_ids = person_ids 336 | # (string[]) An array of unique identifiers of people that are associated with the organization 337 | self.org_global = org_global 338 | # (boolean) Returns whether this organization is a part of Affinity's global dataset of organizations. 339 | self.list_entries = list_entries 340 | # (ListEntry[]) An array of list entry resources associated with the organization, only returned as part of the Get a specific organization endpoint. 341 | 342 | def populate_organization(json_in): 343 | try: 344 | person_ids = json_in['person_ids'] 345 | except KeyError: 346 | person_ids = '' 347 | try: 348 | list_entries = json_in['list_entries'] 349 | except KeyError: 350 | list_entries = '' 351 | 352 | organization_out = Organization( 353 | json_in['id'], 354 | json_in['name'], 355 | json_in['domain'], 356 | person_ids, 357 | json_in['global'], 358 | list_entries 359 | ) 360 | return organization_out 361 | 362 | def get_organizations(term, page_size, page_token): 363 | # Searches your team's data and fetches all the organizations that meet the search criteria. 364 | call = endpoint_base + '/organizations' 365 | if term: 366 | call += '?term=' + term 367 | else: 368 | print('Usage: get_organizations(term, page_size, page_token)') 369 | return 370 | if page_size: 371 | call += '&page_size=' + page_size 372 | if page_token: 373 | call += '&page_token=' + page_token 374 | return get(call) 375 | 376 | def get_organization(organization_id): 377 | # Fetches an organization with a specified organization_id. 378 | call = endpoint_base + '/organizations/' + str(organization_id) 379 | return get(call) 380 | 381 | def create_organization(name, domain, person_ids): 382 | # Creates a new organization with the supplied parameters. 383 | call = endpoint_base + '/organizations' 384 | data = {'name':name, 'domain':domain, 'person_ids':person_ids} 385 | return post(call, data) 386 | 387 | def update_organization(organization_id, name, domain, person_ids): 388 | # Updates an existing organization with organization_id with the supplied parameters. 389 | call = endpoint_base + '/organizations/' + str(organization_id) 390 | data = {'name':name, 'domain':domain, 'person_ids':person_ids} 391 | return put(call, data) 392 | 393 | def delete_organization(organization_id): 394 | # Deletes an organization with a specified organization_id. 395 | call = endpoint_base + '/organizations/' + str(organization_id) 396 | return delete(call) 397 | 398 | def get_organizations_global_fields(): 399 | # Fetches an array of all the global fields that exist on organizations. 400 | call = endpoint_base + '/organizations/fields' 401 | return get(call) 402 | 403 | 404 | # ------------------------------------------------------ # 405 | # OPPORTUNITIES 406 | # ------------------------------------------------------ # 407 | 408 | class Opportunity: 409 | 410 | 'A potential future sale or deal for your team, generally used to track the progress of and revenue generated from sales and deals in your pipeline with a specific organization.' 411 | 412 | def __init__(self, opportunity_id, name, person_ids, organization_ids, list_entries): 413 | self.id = opportunity_id 414 | # (integer) The unique identifier of the opportunity object. 415 | self.name = name 416 | # (integer) The name of the opprtunity (see below). 417 | self.person_ids = person_ids 418 | # (string[]) An array of unique identifiers of people that are associated with the opportunity. 419 | self.organization_ids = organization_ids 420 | # (string[]) An array of unique identifiers of organizations that are associated with the opportunity. 421 | self.list_entries = list_entries 422 | # (ListEntry[]) An array of list entry resources associated with the organization, only returned as part of the Get a specific organization endpoint. 423 | 424 | def populate_opportunity(json_in): 425 | try: 426 | person_ids = json_in['person_ids'] 427 | except KeyError: 428 | person_ids = '' 429 | try: 430 | organization_ids = json_in['organization_ids'] 431 | except KeyError: 432 | organization_ids = '' 433 | try: 434 | list_entries = json_in['list_entries'] 435 | except KeyError: 436 | list_entries = '' 437 | 438 | opportunity_out = Opportunity( 439 | json_in['id'], 440 | json_in['name'], 441 | person_ids, 442 | organization_ids, 443 | list_entries 444 | ) 445 | return opportunity_out 446 | 447 | def get_opportunities(term, page_size, page_token): 448 | # Searches your team's data and fetches all the opportunities that meet the search criteria. 449 | call = endpoint_base + '/opportunities' 450 | if term: 451 | call += '?term=' + term 452 | else: 453 | print('Usage: get_opportunities(term, page_size, page_token)') 454 | return 455 | if page_size: 456 | call += '&page_size=' + page_size 457 | if page_token: 458 | call += '&page_token=' + page_token 459 | return get(call) 460 | 461 | def get_opportunity(opportunity_id): 462 | # Fetches an organization with a specified opportunity_id. 463 | call = endpoint_base + '/opportunities/' + str(opportunity_id) 464 | return get(call) 465 | 466 | def create_opportunities(name, person_ids, organization_ids): 467 | # Creates a new opportunity with the supplied parameters. 468 | call = endpoint_base + '/opportunities' 469 | data = {'name':name, 'list_id':list_id, 'person_ids':person_ids, 'organization_ids':organization_ids} 470 | return post(call, data) 471 | 472 | def update_opportunity(organization_id, name, person_ids, orgnaization_ids): 473 | # Updates an existing opportunity with opportunity_id with the supplied parameters. 474 | call = endpoint_base + '/opportunities/' + str(opportunity_id) 475 | data = {'name':name, 'person_ids':person_ids, 'organization_ids':organization_ids} 476 | return put(call, data) 477 | 478 | def delete_opportunity(opportunity_id): 479 | # Deletes an opportunity with a specified opportunity_id. 480 | call = endpoint_base + '/opportunities/' + str(opportunity_id) 481 | return delete(call) 482 | 483 | # ------------------------------------------------------ # 484 | # NOTES 485 | # ------------------------------------------------------ # 486 | 487 | class Note: 488 | 489 | 'A note object contains content, which is a string containing the note body. In addition, a note can be associated with multiple people or organizations.' 490 | 491 | def __init__(self, note_id, creator_id, person_ids, organization_ids, content, created_at): 492 | self.id = note_id 493 | # (integer) The unique identifier of the note object. 494 | self.creator_id = creator_id 495 | # (integer) The unique identifier of the person object who created the note. 496 | self.person_ids = person_ids 497 | # (integer[]) An array of unique identifiers of person objects that are associated with the note. 498 | self.organization_ids = organization_ids 499 | # (integer[]) An array of unique identifiers of organization objects that are associated with the note. 500 | self.content = content 501 | # (string) The string containing the content of the note. 502 | self.created_at = created_at 503 | # (datetime) The string representing the time when the note was created. 504 | 505 | def populate_note(json_in): 506 | note_out = Note( 507 | json_in['note_id'], 508 | json_in['creator_id'], 509 | json_in['person_ids'], 510 | json_in['organization_ids'], 511 | json_in['content'], 512 | json_in['created_at'] 513 | ) 514 | 515 | def create_note(person_ids, organization_ids, opportunity_ids, content, gmail_id, creator_id): 516 | # Creates a new organization with the supplied parameters. 517 | call = endpoint_base + '/notes' 518 | data = {'person_ids':person_ids, 'organization_ids':organization_ids, 'opportunity_ids':opportunity_ids, 'content':content, 'gmail_id':gmail_id, 'creator_id':creator_id} 519 | return post(call, data) 520 | 521 | # ------------------------------------------------------ # 522 | # RELATIONSHIP STRENGTHS 523 | # ------------------------------------------------------ # 524 | 525 | def get_relationship_strength(internal_id, external_id): 526 | call = endpoint_base + '/relationship-strengths' 527 | if internal_id and external_id: 528 | call += '?external_id=' + str(external_id) + '&internal_id=' + str(internal_id) 529 | else: 530 | print('Usage: get_relationship_strength(internal_id, external_id)') 531 | return 532 | return get(call) 533 | 534 | # ------------------------------------------------------ # 535 | # ERRORS 536 | # ------------------------------------------------------ # 537 | 538 | def check_err(r): 539 | if r.status_code != 200: 540 | print "Error:",r.status_code 541 | print r.text 542 | return True 543 | else: 544 | return False 545 | --------------------------------------------------------------------------------