Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Text Me Maybe: Smarter Real-World Integrations with Python

Text Me Maybe: Smarter Real-World Integrations with Python

Slides from my PyOhio 2016 talk about combining IFTTT (If This Then That), AWS Lambda and API Gateway, Twilio, and Python to make a location-triggered SMS notifier that's also sensitive to days and times.

E4c5e3c69566ff80db62a4ab521b6e5a?s=128

Mike Pirnat

July 30, 2016
Tweet

Transcript

  1. text me MAYBE smarter real-world integrations with python Mike Pirnat

    ··· @mpirnat ··· http://mike.pirnat.com
  2. – my wife (more or less) Hey–you just left work

    And this is crazy But it’s your drive home So text me maybe?” “
  3. It’s Simple, Right? • Buy a domain • Set up

    a server • Build a TCPA-compliant SMS integration • Write and deploy a REST API • Write a geofenced mobile app and get it deployed to your device • Write the code you actually care about • Probably more things you don’t care about
  4. It’s Simple, Right? • Buy a domain • Set up

    a server • Build a TCPA-compliant SMS integration • Write and deploy a REST API • Write a geofenced mobile app and get it deployed to your device • Write the code you actually care about • Probably more things you don’t care about X
  5. + + https://ifttt.com mobile app - geofence trigger Maker channel

    - HTTP POST action https://aws.amazon.com/lambda AWS API Gateway - instant HTTP API AWS Lambda - serverless Python 2.7 https://www.twilio.com send SMS messages Python SDK
  6. None
  7. 123abcCopyThisButKeepItSecret 456CopyThisTooButDoNotShareIt

  8. None
  9. None
  10. None
  11. Python! the fun part:

  12. Project Setup

  13. Virtualenv Create and activate a virtual environment: $ virtualenv textmemaybe

    $ cd textmemaybe $ source bin/activate Or perhaps: $ mkvirtualenv textmemaybe $ workon textmemaybe
  14. config.py # Twilio account data... account_sid = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" auth_token =

    "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" from_phone = "+1XXXXXXXXXXXX" # Log level import logging log_level = logging.DEBUG
  15. messages.py GOT_EVENT = "got event {}" IN_WINDOW = "current time

    within window, will attempt to send message" NOT_IN_WINDOW = "current time not within window, ignoring message" SENT = "sent" NOT_SENT = "unsent"
  16. textmemaybe.py """ Your screen was glowing New files Vim was

    loading Late night Brain was rolling Where you think you're coding baby? """
  17. API Definition

  18. POST some JSON: { "to_phone": "+19995551234", "message": "Mike has left

    the office", "t1": "1500", "t2": "2200", "tz": "US/Eastern", "days": "12345" } Get some JSON back: { "result": "sent" }
  19. POST some JSON: { "to_phone": "+19995551234", "message": "Mike has left

    the office", "t1": "1500", "t2": "2200", "tz": "US/Eastern", "days": "12345" } Get some JSON back: { "result": "sent" }
  20. POST some JSON: { "to_phone": "+19995551234", "message": "Mike has left

    the office", "t1": "1500", "t2": "2200", "tz": "US/Eastern", "days": "12345" } Get some JSON back: { "result": "sent" }
  21. POST some JSON: { "to_phone": "+19995551234", "message": "Mike has left

    the office", "t1": "1500", "t2": "2200", "tz": "US/Eastern", "days": "12345" } Get some JSON back: { "result": "sent" }
  22. Sending a Message

  23. from twilio.rest import TwilioRestClient from config import account_sid, auth_token, from_phone

    client = TwilioRestClient(account_sid, auth_token) def send_sms(to_phone, from_phone, body): """ Send an SMS message containing a particular body to a phone number from a phone number. """ return client.messages.create(to=to_phone, from_=from_phone, body=body) message = send_sms(to_phone, from_phone, message_body)
  24. from twilio.rest import TwilioRestClient from config import account_sid, auth_token, from_phone

    client = TwilioRestClient(account_sid, auth_token) def send_sms(to_phone, from_phone, body): """ Send an SMS message containing a particular body to a phone number from a phone number. """ return client.messages.create(to=to_phone, from_=from_phone, body=body) message = send_sms(to_phone, from_phone, message_body)
  25. from twilio.rest import TwilioRestClient from config import account_sid, auth_token, from_phone

    client = TwilioRestClient(account_sid, auth_token) def send_sms(to_phone, from_phone, body): """ Send an SMS message containing a particular body to a phone number from a phone number. """ return client.messages.create(to=to_phone, from_=from_phone, body=body) message = send_sms(to_phone, from_phone, message_body)
  26. from twilio.rest import TwilioRestClient from config import account_sid, auth_token, from_phone

    client = TwilioRestClient(account_sid, auth_token) def send_sms(to_phone, from_phone, body): """ Send an SMS message containing a particular body to a phone number from a phone number. """ return client.messages.create(to=to_phone, from_=from_phone, body=body) message = send_sms(to_phone, from_phone, message_body)
  27. Checking the Time

  28. def is_time_between(now, t1, t2): """ Is a time (from a

    datetime) between two other times (as 24-hour 'HHMM' strings)? """ # Convert HHMM strings into tuples hhmm1 = tuple([int(x) for x in [t1[:2], t1[2:]]]) hhmm2 = tuple([int(x) for x in [t2[:2], t2[2:]]]) hhmm = now.hour, now.minute # Start time before end time; span stays within day if hhmm1 <= hhmm2: return hhmm1 <= hhmm <= hhmm2 # End time before start time; span crosses midnight else: return (hhmm1 <= hhmm <= (23, 59)) or \ ((0, 0) <= hhmm <= hhmm2) import datetime now = datetime.datetime.utcnow() if is_time_between(now, t1, t2): message = send_sms(to_phone, from_phone, message_body)
  29. def is_time_between(now, t1, t2): """ Is a time (from a

    datetime) between two other times (as 24-hour 'HHMM' strings)? """ # Convert HHMM strings into tuples hhmm1 = tuple([int(x) for x in [t1[:2], t1[2:]]]) hhmm2 = tuple([int(x) for x in [t2[:2], t2[2:]]]) hhmm = now.hour, now.minute # Start time before end time; span stays within day if hhmm1 <= hhmm2: return hhmm1 <= hhmm <= hhmm2 # End time before start time; span crosses midnight else: return (hhmm1 <= hhmm <= (23, 59)) or \ ((0, 0) <= hhmm <= hhmm2) import datetime now = datetime.datetime.utcnow() if is_time_between(now, t1, t2): message = send_sms(to_phone, from_phone, message_body)
  30. def is_time_between(now, t1, t2): """ Is a time (from a

    datetime) between two other times (as 24-hour 'HHMM' strings)? """ # Convert HHMM strings into tuples hhmm1 = tuple([int(x) for x in [t1[:2], t1[2:]]]) hhmm2 = tuple([int(x) for x in [t2[:2], t2[2:]]]) hhmm = now.hour, now.minute # Start time before end time; span stays within day if hhmm1 <= hhmm2: return hhmm1 <= hhmm <= hhmm2 # End time before start time; span crosses midnight else: return (hhmm1 <= hhmm <= (23, 59)) or \ ((0, 0) <= hhmm <= hhmm2) import datetime now = datetime.datetime.utcnow() if is_time_between(now, t1, t2): message = send_sms(to_phone, from_phone, message_body)
  31. def is_time_between(now, t1, t2): """ Is a time (from a

    datetime) between two other times (as 24-hour 'HHMM' strings)? """ # Convert HHMM strings into tuples hhmm1 = tuple([int(x) for x in [t1[:2], t1[2:]]]) hhmm2 = tuple([int(x) for x in [t2[:2], t2[2:]]]) hhmm = now.hour, now.minute # Start time before end time; span stays within day if hhmm1 <= hhmm2: return hhmm1 <= hhmm <= hhmm2 # End time before start time; span crosses midnight else: return (hhmm1 <= hhmm <= (23, 59)) or \ ((0, 0) <= hhmm <= hhmm2) import datetime now = datetime.datetime.utcnow() if is_time_between(now, t1, t2): message = send_sms(to_phone, from_phone, message_body)
  32. def is_time_between(now, t1, t2): """ Is a time (from a

    datetime) between two other times (as 24-hour 'HHMM' strings)? """ # Convert HHMM strings into tuples hhmm1 = tuple([int(x) for x in [t1[:2], t1[2:]]]) hhmm2 = tuple([int(x) for x in [t2[:2], t2[2:]]]) hhmm = now.hour, now.minute # Start time before end time; span stays within day if hhmm1 <= hhmm2: return hhmm1 <= hhmm <= hhmm2 # End time before start time; span crosses midnight else: return (hhmm1 <= hhmm <= (23, 59)) or \ ((0, 0) <= hhmm <= hhmm2) import datetime now = datetime.datetime.utcnow() if is_time_between(now, t1, t2): message = send_sms(to_phone, from_phone, message_body)
  33. Anatomy of a Shortcut hhmm1 = tuple([int(x) for x in

    [t1[:2], t1[2:]]])
  34. Anatomy of a Shortcut hhmm1 = tuple([int(x) for x in

    [t1[:2], t1[2:]]]) get the first 2 characters
  35. Anatomy of a Shortcut hhmm1 = tuple([int(x) for x in

    [t1[:2], t1[2:]]]) get the first 2 characters get the last 2 characters
  36. Anatomy of a Shortcut hhmm1 = tuple([int(x) for x in

    [t1[:2], t1[2:]]]) get the first 2 characters get the last 2 characters put them in a list
  37. Anatomy of a Shortcut hhmm1 = tuple([int(x) for x in

    [t1[:2], t1[2:]]]) get the first 2 characters get the last 2 characters list comprehension: turn everything into a list of integers put them in a list
  38. Anatomy of a Shortcut hhmm1 = tuple([int(x) for x in

    [t1[:2], t1[2:]]]) get the first 2 characters get the last 2 characters list comprehension: turn everything into a list of integers turn the list into a tuple put them in a list
  39. Checking the Day

  40. def is_day_allowed(now, days): """ Is a day (from a datetime)

    among particular ISO 8601 (Monday = 1) days? """ return not days or now.isoweekday() in days now = datetime.datetime.utcnow() days = [1, 2, 3, 4, 5] if (is_time_between(now, t1, t2) and is_day_allowed(now, days)): message = send_sms(to_phone, from_phone, message_body)
  41. def is_day_allowed(now, days): """ Is a day (from a datetime)

    among particular ISO 8601 (Monday = 1) days? """ return not days or now.isoweekday() in days now = datetime.datetime.utcnow() days = [1, 2, 3, 4, 5] if (is_time_between(now, t1, t2) and is_day_allowed(now, days)): message = send_sms(to_phone, from_phone, message_body)
  42. def is_day_allowed(now, days): """ Is a day (from a datetime)

    among particular ISO 8601 (Monday = 1) days? """ return not days or now.isoweekday() in days now = datetime.datetime.utcnow() days = [1, 2, 3, 4, 5] if (is_time_between(now, t1, t2) and is_day_allowed(now, days)): message = send_sms(to_phone, from_phone, message_body)
  43. Lambda Integration

  44. def lambda_handler(event, context): """ AWS Lambda handler function; accepts an

    event (dict) and context (LambdaContext) and sends an SMS message to the specified phone number(s) if the time is within the specified window. """ now = datetime.datetime.utcnow() t1, t2 = event.get('t1', '0000'), event.get('t2', '0000') days = [int(x) for x in event.get('days', '')] if (is_time_between(now, t1, t2) and is_day_allowed(now, days)): to_phone = event['to_phone'] message = send_sms(to_phone, from_phone, event['message']) return {'result': messages.SENT} else: return {'result': messages.NOT_SENT}
  45. def lambda_handler(event, context): """ AWS Lambda handler function; accepts an

    event (dict) and context (LambdaContext) and sends an SMS message to the specified phone number(s) if the time is within the specified window. """ now = datetime.datetime.utcnow() t1, t2 = event.get('t1', '0000'), event.get('t2', '0000') days = [int(x) for x in event.get('days', '')] if (is_time_between(now, t1, t2) and is_day_allowed(now, days)): to_phone = event['to_phone'] message = send_sms(to_phone, from_phone, event['message']) return {'result': messages.SENT} else: return {'result': messages.NOT_SENT}
  46. def lambda_handler(event, context): """ AWS Lambda handler function; accepts an

    event (dict) and context (LambdaContext) and sends an SMS message to the specified phone number(s) if the time is within the specified window. """ now = datetime.datetime.utcnow() t1, t2 = event.get('t1', '0000'), event.get('t2', '0000') days = [int(x) for x in event.get('days', '')] if (is_time_between(now, t1, t2) and is_day_allowed(now, days)): to_phone = event['to_phone'] message = send_sms(to_phone, from_phone, event['message']) return {'result': messages.SENT} else: return {'result': messages.NOT_SENT}
  47. def lambda_handler(event, context): """ AWS Lambda handler function; accepts an

    event (dict) and context (LambdaContext) and sends an SMS message to the specified phone number(s) if the time is within the specified window. """ now = datetime.datetime.utcnow() t1, t2 = event.get('t1', '0000'), event.get('t2', '0000') days = [int(x) for x in event.get('days', '')] if (is_time_between(now, t1, t2) and is_day_allowed(now, days)): to_phone = event['to_phone'] message = send_sms(to_phone, from_phone, event['message']) return {'result': messages.SENT} else: return {'result': messages.NOT_SENT}
  48. More Shortcuts t1, t2 = event.get('t1', '0000'), event.get('t2', '0000') days

    = [int(x) for x in event.get('days', '')]
  49. More Shortcuts t1, t2 = event.get('t1', '0000'), event.get('t2', '0000') days

    = [int(x) for x in event.get('days', '')] try to get t1 from the event dictionary, fall back to '0000'
  50. More Shortcuts t1, t2 = event.get('t1', '0000'), event.get('t2', '0000') days

    = [int(x) for x in event.get('days', '')] try to get t1 from the event dictionary, fall back to '0000' try to get t2 from the event dictionary, fall back to '0000'
  51. More Shortcuts t1, t2 = event.get('t1', '0000'), event.get('t2', '0000') days

    = [int(x) for x in event.get('days', '')] try to get t1 from the event dictionary, fall back to '0000' try to get t2 from the event dictionary, fall back to '0000' assign the values into t1 and t2 in a single step
  52. More Shortcuts t1, t2 = event.get('t1', '0000'), event.get('t2', '0000') days

    = [int(x) for x in event.get('days', '')] try to get t1 from the event dictionary, fall back to '0000' try to get t2 from the event dictionary, fall back to '0000' try to get days from the event dictionary, fall back to '' assign the values into t1 and t2 in a single step
  53. More Shortcuts t1, t2 = event.get('t1', '0000'), event.get('t2', '0000') days

    = [int(x) for x in event.get('days', '')] try to get t1 from the event dictionary, fall back to '0000' try to get t2 from the event dictionary, fall back to '0000' try to get days from the event dictionary, fall back to '' list comprehension: turn everything into a list of integers assign the values into t1 and t2 in a single step
  54. Logging

  55. import logging from config import log_level logger = logging.getLogger() logger.setLevel(log_level)

    def lambda_handler(event, context): logger.info(messages.GOT_EVENT.format(event)) ... if (is_time_between(...) and is_day_allowed(...)): logger.info(messages.IN_WINDOW) message = send_sms(...) logger.debug(message) return {'result': messages.SENT} else: logger.info(messages.NOT_IN_WINDOW) return {'result': messages.NOT_SENT}
  56. import logging from config import log_level logger = logging.getLogger() logger.setLevel(log_level)

    def lambda_handler(event, context): logger.info(messages.GOT_EVENT.format(event)) ... if (is_time_between(...) and is_day_allowed(...)): logger.info(messages.IN_WINDOW) message = send_sms(...) logger.debug(message) return {'result': messages.SENT} else: logger.info(messages.NOT_IN_WINDOW) return {'result': messages.NOT_SENT}
  57. import logging from config import log_level logger = logging.getLogger() logger.setLevel(log_level)

    def lambda_handler(event, context): logger.info(messages.GOT_EVENT.format(event)) ... if (is_time_between(...) and is_day_allowed(...)): logger.info(messages.IN_WINDOW) message = send_sms(...) logger.debug(message) return {'result': messages.SENT} else: logger.info(messages.NOT_IN_WINDOW) return {'result': messages.NOT_SENT}
  58. None
  59. Time Zones

  60. import arrow def lambda_handler(event, context): ... # Shift to client

    local timezone if specified now = arrow.utcnow() tz = event.get('tz') now = now.to(tz) if tz else now ...
  61. import arrow def lambda_handler(event, context): ... # Shift to client

    local timezone if specified now = arrow.utcnow() tz = event.get('tz') now = now.to(tz) if tz else now ...
  62. import arrow def lambda_handler(event, context): ... # Shift to client

    local timezone if specified now = arrow.utcnow() tz = event.get('tz') now = now.to(tz) if tz else now ...
  63. import arrow def lambda_handler(event, context): ... # Shift to client

    local timezone if specified now = arrow.utcnow() tz = event.get('tz') now = now.to(tz) if tz else now ...
  64. None
  65. None
  66. None
  67. None
  68. None
  69. None
  70. None
  71. module.function

  72. None
  73. None
  74. None
  75. Deploying

  76. None
  77. None
  78. makezip.sh #!/bin/bash PWD=`pwd` TIMESTAMP=`date -j +%Y%m%dT%H%M` ZIPFILE=$PWD/textMeMaybe-$TIMESTAMP.zip zip $ZIPFILE config.py

    messages.py textmemaybe.py SITE_PACKAGES=$VIRTUAL_ENV/lib/python2.7/site-packages/ cd $SITE_PACKAGES zip -gr $ZIPFILE ./
  79. None
  80. None
  81. None
  82. None
  83. None
  84. None
  85. None
  86. None
  87. Double quotes are critical!

  88. None
  89. None
  90. Now What?

  91. make them smarter! When your smart things aren’t smart enough...

  92. Links • https://www.twilio.com • https://aws.amazon.com/lambda • https://ifttt.com • http://arrow.readthedocs.io •

    http://docs.aws.amazon.com/lambda/latest/dg/lambda-python- how-to-create-deployment-package.html • http://docs.aws.amazon.com/lambda/latest/dg/with-s3-example- deployment-pkg.html#with-s3-example-deployment-pkg-python
  93. Contact Me Maybe Mike Pirnat http://mike.pirnat.com @mpirnat Thanks! :-)