[ ] AWS SES, Lambda DynamoDB ( Route53) |
, , . , , - , . , , c , - , , Mint, . , , , .
, , , . , - .
API - - headless-. , API , - - . , . , , . , . , .
. -. , . , - , , AWS.
AWS , : SES, Lambda, DynamoDB. SNS, Kinesis, CloudWatch. : Lambda EC2, DynamoDB RDS (MySQL, PostgreSQL, Oracle, ), BerkleyDB.
? , , , . :
.
Simple Email Service, SES, . , : S3, Lambda-, SNS . , SES MX . , , , AWS Route 53. , Route 53.
SES . SES DNS (MX TXT), . Route 53, . , . : , ccalert@ , SNS ccalerts:
aws> ses describe-receipt-rule --rule-set-name "ccalerts" --rule-name "ccalert"
{
"Rule": {
"Name": "ccalert",
"Recipients": [
"ccalert@=censored=
],
"Enabled": true,
"ScanEnabled": true,
"Actions": [
{
"SNSAction": {
"TopicArn": "arn:aws:sns:us-west-2:=censored=:ccalerts",
"Encoding": "UTF-8"
}
}
],
"TlsPolicy": "Optional"
}
}
SNS-, Lambda- ProcessCharge. .
from __future__ import print_function
import json
import re
import uuid
from datetime import datetime
import boto3
def lambda_handler(event, context):
message = json.loads(event['Records'][0]['Sns']['Message'])
print("Processing email {}".format(message['mail']))
content = message['content']
trn = parse_content(content)
if trn is not None:
print("Transaction: %s" % trn)
process_transaction(trn)
parse_content():
def parse_content(content):
content = content.replace("=\r\n", "")
match = re.search(r'A charge of \(\$USD\) (\d+\.\d+) at (.+?) has been authorized on (\d+/\d+/\d+ \d+:\d+:\d+ \S{2} \S+?)\.', content, re.M)
if match:
print("Matched %s" % match.group(0))
date = match.group(3)
# replace time zone with hour offset because Python can't parse it
date = date.replace("EDT", "-0400")
date = date.replace("EST", "-0500")
dt = datetime.strptime(date, "%m/%d/%Y %I:%M:%S %p %z")
return {'billed': match.group(1), 'merchant': match.group(2), 'datetime': dt.isoformat()}
else:
print("Didn't match")
return None
, , , . :
A charge of ($USD) 100.00 at Amazon.com has been authorized on 07/19/2017 1:55:52 PM EDT.
, , EDT (Eastern Daylight Time) . EDT -0400, , EST. , ISO 8601, DynamoDB.
- , . process_transaction:
def process_transaction(trn):
ddb = boto3.client('dynamodb')
trn_id = uuid.uuid4().hex
ddb.put_item(
TableName='Transactions',
Item={
'id': {'S': trn_id},
'datetime': {'S': trn['datetime']},
'merchant': {'S': trn['merchant']},
'billed': {'N': trn['billed']}
})
Transactions, .
, . :
. :
, . , , . , . , , DynamoDB. N , N , N read capacity units. , , ( ) .
, total , . . , , -:
, , Lambda- UpdateSummary:
from __future__ import print_function
from datetime import datetime
import boto3
def lambda_handler(event, context):
for record in event['Records']:
if record['eventName'] != 'INSERT':
print("Unsupported event {}".format(record))
return
trn = record['dynamodb']['NewImage']
print(trn)
process_transaction(trn)
, .
def process_transaction(trn):
period = get_period(trn)
if period is None:
return
billed = trn['billed']['N']
# update total for current period
update_total(period, billed)
print("Transaction processed")
process_transaction() , -, , total.
def get_period(trn):
try:
# python cannot parse -04:00, it needs -0400
dt = trn['datetime']['S'].replace("-04:00", "-0400")
dt = dt.replace("-05:00", "-0500")
dt = dt.replace("-07:00", "-0700")
dt = datetime.strptime(dt, "%Y-%m-%dT%H:%M:%S%z")
return dt.strftime("%Y-%m")
except ValueError as err:
print("Cannot parse date {}: {}".format(trn['datetime']['S'], err))
return None
, , / -HH:MM, ISO 8601, ( , parse_content()). -HHMM. , . , .
total:
def update_total(period, billed):
ddb = boto3.client('dynamodb')
response = load_summary(ddb, period)
print("Summary: {}".format(response))
if 'Item' not in response:
create_summary(ddb, period, billed)
else:
total = response['Item']['total']['N']
update_summary(ddb, period, total, billed)
(Summary) load_summary(), total . , create_summary(), , update_summary().
def load_summary(ddb, period):
print("Loading summary for period {}".format(period))
return ddb.get_item(
TableName = 'Summary',
Key = {
'period': {'S': period}
},
ConsistentRead = True
)
, , , , .
def create_summary(ddb, period, total):
print("Creating summary for period {} with total {}".format(period, total))
ddb.put_item(
TableName = 'Summary',
Item = {
'period': {'S': period},
'total': {'N': total},
'budget': {'N': "0"}
},
ConditionExpression = 'attribute_not_exists(period)'
)
, , , ConditionExpression = 'attribute_not_exists(period)', , . , - , load_summary() , create_summary(), put_item() Lambda- .
def update_summary(ddb, period, total, billed):
print("Updating summary for period {} with total {} for billed {}".format(period, total, billed))
ddb.update_item(
TableName = 'Summary',
Key = {
'period': {'S': period}
},
UpdateExpression = 'SET #total = #total + :billed',
ConditionExpression = '#total = :total',
ExpressionAttributeValues = {
':billed': {'N': billed},
':total': {'N': total}
},
# total is a reserved word so we create an alias #total to use it in expression
ExpressionAttributeNames = {
'#total': 'total'
}
)
total DynamoDB:
UpdateExpression = 'SET #total = #total + :billed'
, , , , , , :
ConditionExpression = '#total = :total',
total DynamoDB, DynamoDB :
ExpressionAttributeNames = {
'#total': 'total'
}
:
period | budget | total |
---|---|---|
2017-07 | 1000 | 500 |
. , , . , - / . :
from __future__ import print_function
from datetime import date
import boto3
def lambda_handler(event, context):
ddb = boto3.client('dynamodb')
current_date = date.today()
print("Preparing daily notification for {}".format(current_date.isoformat()))
period = current_date.strftime("%Y-%m")
response = load_summary(ddb, period)
print("Summary: {}".format(response))
if 'Item' not in response:
print("No summary available for period {}".format(period))
return
summary = response['Item']
total = summary['total']['N']
budget = summary['budget']['N']
send_email(total, budget)
def load_summary(ddb, period):
print("Loading summary for period {}".format(period))
return ddb.get_item(
TableName = 'Summary',
Key = {
'period': {'S': period}
},
ConsistentRead = True
)
, , . :
def send_email(total, budget):
sender = "Our Budget <ccalert@==censored==>"
recipients = [==censored==]
charset = "UTF-8"
available = float(budget) - float(total)
today = date.today().strftime("%Y-%m-%d")
message = '''
As of {0}, available funds are ${1:.2f}. This month budget is ${2:.2f}, spendings so far totals ${3:.2f}.
More details coming soon!'''
subject = "How are we doing?"
textbody = message.format(today, float(available), float(budget), float(total))
print("Sending email: {}".format(textbody))
client = boto3.client('ses', region_name = 'us-west-2')
try:
response = client.send_email(
Destination = {
'ToAddresses': recipients
},
Message = {
'Body': {
'Text': {
'Charset': charset,
'Data': textbody,
},
},
'Subject': {
'Charset': charset,
'Data': subject,
},
},
Source = sender,
)
# Display an error if something goes wrong.
except Exception as e:
print("Couldn't send email: {}".format(e))
else:
print("Email sent!")
. , , , . , , , , - . - , .