Skip to main content

Python package & cli for syncing between ActionNetwork & AirTable

Project description

an-at-sync

Python package & cli for syncing between ActionNetwork & AirTable.

How to Use

To set up a new project with an-at-sync, create a new folder for your project:

mkdir project-name && cd project-name

In that folder, create a requirements.txt and add an-at-sync as a dependency:

an-at-sync

Install it with pip:

pip install -r requirements.txt

Create a folder for your project namespace:

mkdir project_name

In that folder, create a models.py with this default content:

from datetime import datetime
from typing import Any, Dict, Optional

from an_at_sync.format import convert_adr, standardize_phone
from an_at_sync.model import BaseActivist, BaseEvent, BaseRSVP
from dateutil import tz
from pyairtable.utils import datetime_to_iso_str
from pydantic import HttpUrl, validator
from pydantic.networks import EmailStr

eastern = tz.gettz("America/New_York")
utc = tz.gettz("UTC")


class Activist(BaseActivist):
    first_name: Optional[str]
    last_name: Optional[str]
    email: EmailStr
    zip_code: Optional[str]
    phone_number: Optional[str]
    address: Optional[str]
    city: Optional[str]
    state: Optional[str]

    @classmethod
    def from_actionnetwork(cls, source: dict, **kwargs: Any):
        address, city, state, zip_code = convert_adr(source["postal_addresses"][0])
        return cls(
            first_name=source.get("given_name"),
            last_name=source.get("family_name"),
            email=source["email_addresses"][0]["address"].strip(),
            address=address,
            city=city,
            state=state,
            zip_code=zip_code,
            phone_number=standardize_phone(source["phone_numbers"][0].get("number")),
        )

    def display_name(self) -> str:
        return f"{self.first_name} {self.last_name}"

    def pk(self) -> Dict:
        return {"Email": self.email}

    def to_airtable(self):
        return {
            "Email": self.email,
            "First Name": self.first_name,
            "Last Name": self.last_name,
            "Phone": self.phone_number,
            "Address": self.address,
            "City": self.city,
            "State": self.state,
            "Zip": self.zip_code,
        }


class RSVP(BaseRSVP):
    id: str
    rsvpd_at: datetime

    @classmethod
    def from_actionnetwork(cls, source, **kwargs: Any):
        return cls(
            id=f"{kwargs['activist_record']['id']}-{kwargs['event_record']['id']}",
            activist=kwargs["activist"],
            event=kwargs["event"],
            rsvpd_at=source["created_date"],
        )

    def display_name(self) -> str:
        return f"{self.activist.display_name()} to {self.event.display_name()}"

    def pk(self):
        return {"Id": self.id}

    def to_airtable(self) -> dict:
        return {
            "Id": self.id,
            "RSVP'd At": datetime_to_iso_str(self.rsvpd_at.replace(tzinfo=None)),
        }

    def activist_column(self) -> str:
        return "Volunteer"

    def event_column(self) -> str:
        return "Event"


class Event(BaseEvent):
    url: HttpUrl
    name: str
    location: str
    start_date: datetime
    end_date: Optional[datetime]
    status: str

    @validator("start_date", "end_date")
    def dates_must_be_eastern(cls, v: datetime):
        return v.replace(tzinfo=eastern) if v is not None else v

    @classmethod
    def from_actionnetwork(cls, source: dict, **kwargs):
        address, city, state, *_ = convert_adr(source["location"])
        event_location = "Zoom" if not address else f"{address}, {city}, {state}"
        return cls(
            url=source["browser_url"],
            name=source["title"],
            start_date=source["start_date"],
            end_date=source.get("end_date"),
            status=source["status"],
            location=event_location,
        )

    def display_name(self) -> str:
        return self.name

    def pk(self) -> Dict:
        return {"Url": str(self.url)}

    def to_airtable(self) -> dict:
        return {
            "Url": str(self.url),
            "Name": self.name,
            "Start At": self.start_date.astimezone(tz=utc)
            .replace(tzinfo=None)
            .isoformat(timespec="milliseconds")
            + "Z",
            "End At": self.end_date.astimezone(tz=utc)
            .replace(tzinfo=None)
            .isoformat(timespec="milliseconds")
            + "Z"
            if self.end_date
            else None,
            "Status": self.status.capitalize(),
            "Location": self.location,
        }

These models represent the various part of the system we're going to interact independently from the two systems that will use them. The Activist represents an individual person in your campaign. The Event represents a single campaign event, and an RSVP represents a response from an Activist to attend an event.

You can use this as a baseline for your own models, allowing you to customize which fields are synced to AirTable & how.

Next, create this .env file:

AN_AT_SYNC_MODELS="project_name.models"
AN_API_KEY="TODO: ActionNetwork API Key"
AT_API_KEY="TODO: Airtable API Key"
AT_BASE="TODO: Base"
AT_ACTIVISTS_TABLE="TODO: Volunteers Table"
AT_EVENTS_TABLE="TODO: Events Table"
AT_RSVP_TABLE="TODO: RSVP Table"

Given the project_name above, the AN_AT_SYNC_MODELS points to the module that an-at-sync can load our custom models from. The rest of the env vars require you to get the proper keys and configure your AirTable account.

Getting Your ActionNetwork API Key

Details -> API & Sync. "Your API Key" -> Generate Key. Copy into .env.

Creating Your AirTable Base

Need to create the base first. Create Volunteers, Events, & RSVPs table. Get base id from first part of the URL (starts with app).

Getting Your AirTable API Key

User stetings -> Developer Hub -> Personal Access token. data.records:read & data.records:write. Access to the created base. Copy the token, add to .env.

Creating Your AirTable Volunteers Table

Create fields returned by to_airtable method. Copy from second part of URL and add to .env.

Creating Your AirTAble Events Table

Create fields returned by to_airtable method. Copy from second part of URL and add to .env.

Creating Your AirTable RSVP Table

Create fields returned by to_airtable method. Copy from second part of URL and add to .env.

Run your first sync!

python -m an_at_sync sync events --rsvps

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

an_at_sync-0.8.0.tar.gz (12.3 kB view details)

Uploaded Source

Built Distribution

an_at_sync-0.8.0-py3-none-any.whl (12.5 kB view details)

Uploaded Python 3

File details

Details for the file an_at_sync-0.8.0.tar.gz.

File metadata

  • Download URL: an_at_sync-0.8.0.tar.gz
  • Upload date:
  • Size: 12.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/4.0.2 CPython/3.11.7

File hashes

Hashes for an_at_sync-0.8.0.tar.gz
Algorithm Hash digest
SHA256 2824707c3f1c6f152bd7697a7e97a0e1b3a47f3e13efee8d6fc34ef6e9cb312f
MD5 0dcb1fba9f185bf1ca539cbfc942d745
BLAKE2b-256 506b0f749ea6fd6166af6d9f259462b1e79701e807fc1b402ff5e1a621cf98fa

See more details on using hashes here.

File details

Details for the file an_at_sync-0.8.0-py3-none-any.whl.

File metadata

  • Download URL: an_at_sync-0.8.0-py3-none-any.whl
  • Upload date:
  • Size: 12.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/4.0.2 CPython/3.11.7

File hashes

Hashes for an_at_sync-0.8.0-py3-none-any.whl
Algorithm Hash digest
SHA256 5297312143384141b89209b105129c865744268b568c7ef36b58a9eebde9991f
MD5 2f1c76e6883e4a8885fb8d700c633c40
BLAKE2b-256 61a9e14c88b5d2e1cbd1c4df757244e6a4eb5c8b6990035836a38a9000533969

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page