Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

an-at-sync

Package Overview
Dependencies
Maintainers
1
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

an-at-sync

Python package & cli for syncing between ActionNetwork & AirTable

  • 0.9.2
  • PyPI
  • Socket score

Maintainers
1

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

Configuring the Webhook Handler on Fly.io

Create a Dockerfile in your repo:

FROM maadhattah/an-at-sync:latest

COPY ./project_name /app/project_name

This will copy your configuration onto the Docker image.

Next, deploy it to Fly.io:

fly launch

It will create an app based on the Dockerfile image. Next, import your secrets:

cat .env | fly secrets import

Lastly, create the webhook in ActionNetwork with the url https://<projet-name>.fly.dev/api/webhooks/actionnetwork.

FAQs


Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc