-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit a57f8f3
Showing
16 changed files
with
694 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
name: Get parks data | ||
|
||
jobs: | ||
notify: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- name: Check out this repo | ||
uses: actions/checkout@v2 | ||
with: | ||
fetch-depth: 0 | ||
- name: Notify backcountry subscriptions | ||
run: make notify | ||
env: | ||
GMAIL_APP_PASSWORD: ${{ secrets.GMAIL_APP_PASSWORD }} | ||
- name: Commit and push if it changed | ||
run: |- | ||
git config user.name "Automated" | ||
git config user.email "[email protected]" | ||
git add -A | ||
timestamp=$(date -u) | ||
git commit -m "Latest data: ${timestamp}" || exit 0 | ||
git push | ||
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
__pycache__ | ||
.env |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
destination_ids=4675321 | ||
|
||
.INTERMEDIATE : data/notifications.json data/finished/backcountry_sites.json data/intermediate/backcountry_sites.json | ||
|
||
notify : data/notifications.json | ||
python -m backcountry.notify $< | ||
cp $< data/finished/notifications-$(shell date +%s).json | ||
|
||
data/notifications.json : data/finished/backcountry_sites.json | ||
python -m backcountry.subscriptions $< > $@ | ||
|
||
data/finished/backcountry_sites.json : data/intermediate/backcountry_sites.json | ||
python -m backcountry.get_site_availability $< > $@ | ||
|
||
data/intermediate/backcountry_sites.json : | ||
python backcountry/get_backcountry_sites.py > $@ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
# parks | ||
|
||
A lightweight data pipeline that sends email notifications to subscribers whenever a reservation opens up at their favorite Glacier National Park backcountry campsite. The pipeline is orchestrated by a Makefile that is setup to run automatically as a Github Action. | ||
|
||
### Scripts overview | ||
1. Manually add new subscribers with `backcountry/add_user.py`, passing arguments with their preferred campsites and dates. | ||
2. `backcountry/get_backcountry_sites.py` gets a list of valid campsites and dumps it into a file. | ||
3. `backcountry/get_site_availability.py` downloads the availability of each campsite. | ||
4. `backcountry/subscriptions.py` checks to see if any of the subscribers' chosen campsites have a reservation opening on their preferred dates. | ||
5. `backcountry/notify.py` notifies subscribers if there was an opening. | ||
|
||
Thank you robot! Time to go outside! |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
from datetime import datetime | ||
import json | ||
import argparse | ||
|
||
# Set up the argument parser | ||
parser = argparse.ArgumentParser( | ||
description='Add a user to a list of backcountry sites.' | ||
) | ||
parser.add_argument('--email', type=str, help='The user\'s email address') | ||
parser.add_argument( | ||
'--sites', | ||
type=str, | ||
help='The list of sites the user is registered for, separated by commas', | ||
) | ||
parser.add_argument( | ||
'--dates', | ||
type=str, | ||
help='The dates the user is registered for, separated by commas', | ||
) | ||
|
||
# Parse the arguments | ||
args = parser.parse_args() | ||
|
||
email = args.email | ||
sites = args.sites.split(',') | ||
dates = [ | ||
datetime.strptime(date_str, '%m/%d/%Y').strftime('%Y-%m-%d') | ||
for date_str in args.dates.split(',') | ||
] | ||
|
||
with open('data/admin/backcountry_sites.json', 'r') as f: | ||
sites_map = json.load(f) | ||
|
||
new_subscription = {email: {'sites': {}}} | ||
|
||
for id, site in sites_map.items(): | ||
|
||
if site['campsite'] in sites: | ||
if id not in new_subscription[email]: | ||
new_subscription[email]['sites'][id] = {'dates': {}} | ||
|
||
for date in dates: | ||
if date not in new_subscription[email]['sites'][id]['dates']: | ||
new_subscription[email]['sites'][id]['dates'][date] = {} | ||
|
||
subscriptions_file = 'data/subscriptions.json' | ||
with open(subscriptions_file, 'r') as f: | ||
subscriptions = json.load(f) | ||
|
||
subscriptions.update(new_subscription) | ||
|
||
with open(subscriptions_file, 'w') as f: | ||
json.dump(subscriptions, f, indent=4) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import requests | ||
import sys | ||
import json | ||
import re | ||
|
||
|
||
|
||
with open('data/finished/permit_content.json') as f: | ||
data = json.load(f)['payload']['divisions'] | ||
|
||
skip_sites = [ | ||
'AEF - Elizabeth Lake Foot Administrative Site', | ||
'AGC - Gable Creek Administrative Site', | ||
'AHO - Hole in the Wall (Admin. Site)', | ||
'AFM - Fifty Mountain Administrative Site', | ||
] | ||
|
||
campsites = {} | ||
|
||
for division_id, division in data.items(): | ||
campsite = division['name'] | ||
|
||
if re.match(r'[A-Z]+\s-', campsite) and campsite not in skip_sites: | ||
campsites.update( | ||
{division_id: {'district': division['district'], 'campsite': campsite}} | ||
) | ||
|
||
sys.stdout.write(json.dumps(campsites)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import sys | ||
import json | ||
import requests | ||
|
||
from util.models import Campsite | ||
|
||
sites_file = sys.argv[1] | ||
|
||
with open(sites_file) as f: | ||
sites = json.load(f) | ||
|
||
permit_itenerary_url = 'https://www.recreation.gov/api/permititinerary/4675321/division/{division_id}/availability/month?month={month}&year={year}' | ||
year = 2023 | ||
|
||
campsites = {} | ||
|
||
for site_id, site in sites.items(): | ||
campsite = Campsite( | ||
name=site['campsite'], | ||
district=site['district'], | ||
division_id=site_id, | ||
) | ||
|
||
for month in []: | ||
|
||
url = permit_itenerary_url.format( | ||
division_id=campsite.division_id, month=month, year=year | ||
) | ||
response = requests.get( | ||
url, | ||
headers={ | ||
'Accept-Language': 'en-US', | ||
'Host': 'www.recreation.gov', | ||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8', | ||
'Accept-Encoding': 'gzip, deflate, br', | ||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:109.0) Gecko/20100101 Firefox/110.0', | ||
}, | ||
) | ||
days = response.json()['payload']['quota_type_maps'].get( | ||
'ConstantQuotaUsageDaily' | ||
) | ||
|
||
d = {} | ||
|
||
for date, status in days.items(): | ||
d.update( | ||
{date: {'remaining': status['remaining'], 'total': status['total']}} | ||
) | ||
|
||
campsite.dates.update(d) | ||
|
||
campsites.update({campsite.division_id: campsite.to_dict()}) | ||
|
||
sys.stdout.write(json.dumps(campsites)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
# import sendgrid | ||
|
||
# from sendgrid.helpers.mail import Mail, Email, To, Content | ||
|
||
# my_sg = sendgrid.SendGridAPIClient(api_key=os.environ.get('SENDGRID_API_KEY')) | ||
|
||
# # Change to your verified sender | ||
# from_email = Email("[email protected]") | ||
|
||
# # Change to your recipient | ||
# to_email = To("[email protected]") | ||
|
||
# subject = "Lorem ipsum dolor sit amet" | ||
# content = Content("text/plain", "consectetur adipiscing elit") | ||
|
||
# mail = Mail(from_email, to_email, subject, content) | ||
|
||
# # Get a JSON-ready representation of the Mail object | ||
# mail_json = mail.get() | ||
|
||
# # Send an HTTP POST request to /mail/send | ||
# response = my_sg.client.mail.send.post(request_body=mail_json) | ||
|
||
import os | ||
import json | ||
import sys | ||
|
||
try: | ||
from dotenv import load_dotenv | ||
|
||
load_dotenv('.env') | ||
except ModuleNotFoundError: | ||
pass | ||
|
||
import smtplib | ||
from email.mime.text import MIMEText | ||
|
||
|
||
def make_message(email, text): | ||
return { | ||
"from_email": "[email protected]", | ||
"from_name": "Sam McAlilly", | ||
"subject": "Your campsite status alerts", | ||
"text": text, | ||
"to": [{"email": email, "type": "to"}], | ||
} | ||
|
||
|
||
def send_email(subject, body, sender, recipients, password): | ||
msg = MIMEText(body) | ||
msg['Subject'] = subject | ||
msg['From'] = sender | ||
msg['To'] = ', '.join(recipients) | ||
smtp_server = smtplib.SMTP_SSL('smtp.gmail.com', 465) | ||
smtp_server.login(sender, password) | ||
smtp_server.sendmail(sender, recipients, msg.as_string()) | ||
smtp_server.quit() | ||
|
||
|
||
# def send_email(message): | ||
# key = os.getenv('MAILCHIMP_API_KEY') | ||
# try: | ||
# mailchimp = MailchimpTransactional.Client(api_key=key) | ||
# response = mailchimp.messages.send({"message": message}) | ||
# except ApiClientError as error: | ||
# print("An exception occurred: {}".format(error.text)) | ||
|
||
|
||
target_file = sys.argv[1] | ||
with open(target_file) as f: | ||
notifications = json.load(f) | ||
|
||
print('notifications', notifications) | ||
|
||
if notifications: | ||
for email, subscription in notifications.items(): | ||
sites = {} | ||
for site_name, dates in subscription.items(): | ||
sites[site_name] = [] | ||
for date, status in dates.items(): | ||
sites[site_name].append((date, status['remaining'])) | ||
|
||
text = f"" | ||
|
||
for site_name, details in sites.items(): | ||
for d in details: | ||
text += f"{site_name} has {d[1]} available for {d[0]}\n" | ||
|
||
text += "\nU better hurry up and book ur campsite. Visit https://www.recreation.gov/permits/4675321/registration/detailed-availability/ & grab that permit.\n\nO yea here is a map: https://www.nps.gov/glac/planyourvisit/upload/Wilderness-Campground-Map-2023.pdf\n\n" | ||
|
||
send_email( | ||
"Your tracked campsite(s) in Glacier changed", | ||
text, | ||
'[email protected]', | ||
[email], | ||
os.getenv('GMAIL_APP_PASSWORD'), | ||
) | ||
print('sent email to', email) | ||
else: | ||
print('no notifications to send') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import json | ||
import sys | ||
|
||
with open('data/subscriptions.json') as f: | ||
subscriptions = json.load(f) | ||
|
||
with open('data/finished/backcountry_sites.json') as f: | ||
sites = json.load(f) | ||
|
||
notify = {} | ||
|
||
|
||
for email, subscription in subscriptions.items(): | ||
# subscription = Subscription(email=email, dates=s['dates'], campsite=sites.get(s['site_id']) | ||
notify.update({email: {}}) | ||
|
||
for id, subscription_dates in subscription['sites'].items(): | ||
site = sites.get(id) | ||
|
||
for date, status in subscription_dates['dates'].items(): | ||
current = site['dates'].get(date) | ||
|
||
sub_date = subscription_dates['dates'].get(date) | ||
|
||
if not current: | ||
continue | ||
|
||
if not sub_date: | ||
if current['remaining'] > 0: | ||
try: | ||
notify[email][site['name']].update({date: current}) | ||
except KeyError: | ||
notify[email].update({site['name']: {date: current}}) | ||
elif sub_date.get('remaining') != current['remaining']: | ||
if current['remaining'] > 0: | ||
try: | ||
notify[email][site['name']].update({date: current}) | ||
except KeyError: | ||
notify[email].update({site['name']: {date: current}}) | ||
|
||
subscription['sites'][id]['dates'].update({date: current}) | ||
|
||
if notify[email] == {}: | ||
del notify[email] | ||
|
||
with open('data/subscriptions.json', 'w') as f: | ||
json.dump(subscriptions, f) | ||
|
||
sys.stdout.write(json.dumps(notify)) |
Oops, something went wrong.