Package: an-at-sync
an-at-syncPython package & cli for syncing between ActionNetwork & AirTable.How to UseTo set up a new project withan-at-sync, create a new folder for your project:mkdirproject-name&&cdproject-nameIn that folder, create arequirements.txtand addan-at-syncas a dependency:an-at-syncInstall it with pip:pipinstall-rrequirements.txtCreate a folder for your project namespace:mkdir project_nameIn that folder, create amodels.pywith this default content:fromdatetimeimportdatetimefromtypingimportAny,Dict,Optionalfroman_at_sync.formatimportconvert_adr,standardize_phonefroman_at_sync.modelimportBaseActivist,BaseEvent,BaseRSVPfromdateutilimporttzfrompyairtable.utilsimportdatetime_to_iso_strfrompydanticimportHttpUrl,validatorfrompydantic.networksimportEmailStreastern=tz.gettz("America/New_York")utc=tz.gettz("UTC")classActivist(BaseActivist):first_name:Optional[str]last_name:Optional[str]email:EmailStrzip_code:Optional[str]phone_number:Optional[str]address:Optional[str]city:Optional[str]state:Optional[str]@classmethoddeffrom_actionnetwork(cls,source:dict,**kwargs:Any):address,city,state,zip_code=convert_adr(source["postal_addresses"][0])returncls(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")),)defdisplay_name(self)->str:returnf"{self.first_name}{self.last_name}"defpk(self)->Dict:return{"Email":self.email}defto_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,}classRSVP(BaseRSVP):id:strrsvpd_at:datetime@classmethoddeffrom_actionnetwork(cls,source,**kwargs:Any):returncls(id=f"{kwargs['activist_record']['id']}-{kwargs['event_record']['id']}",activist=kwargs["activist"],event=kwargs["event"],rsvpd_at=source["created_date"],)defdisplay_name(self)->str:returnf"{self.activist.display_name()}to{self.event.display_name()}"defpk(self):return{"Id":self.id}defto_airtable(self)->dict:return{"Id":self.id,"RSVP'd At":datetime_to_iso_str(self.rsvpd_at.replace(tzinfo=None)),}defactivist_column(self)->str:return"Volunteer"defevent_column(self)->str:return"Event"classEvent(BaseEvent):url:HttpUrlname:strlocation:strstart_date:datetimeend_date:Optional[datetime]status:str@validator("start_date","end_date")defdates_must_be_eastern(cls,v:datetime):returnv.replace(tzinfo=eastern)ifvisnotNoneelsev@classmethoddeffrom_actionnetwork(cls,source:dict,**kwargs):address,city,state,*_=convert_adr(source["location"])event_location="Zoom"ifnotaddresselsef"{address},{city},{state}"returncls(url=source["browser_url"],name=source["title"],start_date=source["start_date"],end_date=source.get("end_date"),status=source["status"],location=event_location,)defdisplay_name(self)->str:returnself.namedefpk(self)->Dict:return{"Url":str(self.url)}defto_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"ifself.end_dateelseNone,"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.envfile: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 theproject_nameabove, theAN_AT_SYNC_MODELSpoints to the module thatan-at-synccan 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 KeyDetails -> API & Sync. "Your API Key" -> Generate Key. Copy into.env.Creating Your AirTable BaseNeed to create the base first. Create Volunteers, Events, & RSVPs table. Get base id from first part of the URL (starts withapp).Getting Your AirTable API KeyUser 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 TableCreate fields returned byto_airtablemethod. Copy from second part of URL and add to.env.Creating Your AirTAble Events TableCreate fields returned byto_airtablemethod. Copy from second part of URL and add to.env.Creating Your AirTable RSVP TableCreate fields returned byto_airtablemethod. Copy from second part of URL and add to.env.Run your first sync!python-man_at_syncsyncevents--rsvpsConfiguring the Webhook Handler on Fly.ioCreate a Dockerfile in your repo:FROMmaadhattah/an-at-sync:latestCOPY./project_name/app/project_nameThis will copy your configuration onto the Docker image.Next, deploy it to Fly.io:flylaunchIt will create an app based on the Dockerfile image. Next, import your secrets:cat.env|flysecretsimportLastly, create the webhook in ActionNetwork with the urlhttps://<projet-name>.fly.dev/api/webhooks/actionnetwork.