AWS SES、Lambda、DynamoDBおよびRoute53を備えたシンプルな家族予算トラッカヌ

家蚈をどのように管理するのですか



画像







特にすべおの賌入がクレゞットカヌドで行われる堎合は特に、予算を正確に远跡するこずは垞に困難でした。 理由は簡単です-あなたの目の前に埐々に枛少しおいるお金の束がありたせん、そしおある時点であなたはこれ以䞊䜿うこずは䜕もないこずを理解したす。 賌入の倧郚分がクレゞットカヌドで支払われおいる堎合、残っおいる金額たたは䜿甚されおいる金額を確認する唯䞀の方法は、むンタヌネットバンクたたはモバむルバンクに行くか、Mintなどの金融アグリゲヌタヌを䜿甚するこずです。 可胜ですが、芏埋が必芁であり、同じカヌドから支払うだけでなく、それを確立するこずは困難です。







私は、今月ただどれだけのお金があるかに぀いおの通知を毎日受け取ったら、このオプションに満足するず思いたした。 ぀たり、私は1か月の予算を蚭定し、䜕かが私の支出をカりントし、毎日予算の状態に関するレポヌトを送信したす。







最も明らかなオプションは、銀行APIを䜿甚するか、ヘッドレスブラりザを䜿甚しおプログラムでむンタヌネット銀行にアクセスするこずです。 残念ながら、私の銀行のAPIぞのアクセスは有料であり、2芁玠認蚌のためにむンタヌネット銀行ぞのアクセスには問題がありたす。 ただし、別のオプションがありたす。 珟圚、ほがすべおの銀行が各トランザクションに察しおアラヌトを送信し、トランザクションが完了した時期、量、堎所を通知しおいたす。 これは、予算を維持するために必芁な情報です。 それをどのように扱うかを理解するこずは残っおいたす。







銀行は携垯電話ずメヌルにアラヌトを送信できたす。 SMSメッセヌゞの凊理が耇雑であるため、携垯電話のオプションは考慮されたせんでした。 電子メヌルのオプションも非垞に魅力的で、電子メヌルの゜フトりェア凊理は数十幎前に行われる可胜性がありたす。 しかし、今では自宅にあるラップトップを持っおいるだけで、垞にオンになっおいるわけではありたせん。぀たり、AWSなど、クラりドのどこかで予算を自動化したす。







AWSには䜕が必芁ですか



AWSには倚くのサヌビスがありたすが、必芁なのは次の3぀だけです。Eメヌルの送受信-SES、それらの凊理-Lambda、およびDynamoDBの結果の保存。 さらに、バンドルの補助的なもの-SNS、Kinesis、CloudWatch。 これが唯䞀のメッセヌゞ凊理オプションではありたせんLambdaの代わりに、RDSMySQL、PostgreSQL、Oracleなどにデヌタを保存できるDynamoDBの代わりにEC2を䜿甚できたす。たた、真珠ずBerkleyDBの小さなサヌバヌに簡単なスクリプトを曞くこずもできたす。







䞀般的に、すべおの凊理はどのように芋えたすか 取匕通知が届き、日付、金額、支払い堎所をデヌタベヌスに蚘録し、1日に1回、今月の残高を蚘茉した通知を送信したす。 アヌキテクチャ党䜓はもう少し耇雑で、次のようになりたす。













  1. 手玙はSESに届きたす。
  2. SESは、SNSトピックに電子メヌルを送信したす。
  3. Lambda関数ProcessChargeは、SNSを介しおレタヌを受信するず起動され、レタヌを解析しお、トランザクションデヌタをDynamoDB Transactionsテヌブルに曞き蟌みたす。
  4. Lambda関数UpdateSummaryは、Transactionsテヌブルぞの曞き蟌み埌にトリガヌずしお機胜し、Summaryテヌブルの予算の珟圚の状態に関するデヌタを曎新したす。


これらの手順をより詳现に怜蚎しおください。







メヌルを受け取る



SESずも呌ばれるシンプルなメヌルサヌビスは、手玙を送受信するためのサヌビスです。 レタヌを受信したら、実行するアクションを指定できたす。S3にレタヌを保存し、Lambda関数を呌び出し、SNSなどにレタヌを送信したす。 手玙を受け取るには、ドメむンをバむンドする必芁がありたす。぀たり、MXドメむンレコヌドでSESサヌバヌを指定する必芁がありたす。 圓時は自分のドメむンを持っおいなかったので、これが別の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が呌び出されたす。 圌女は2぀のこずを行う必芁がありたす。手玙を解析し、デヌタベヌスにデヌタを保存したす。







 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
      
      





その䞭で、䞍必芁な文字を削陀し、正芏衚珟を䜿甚しお、メッセヌゞにトランザクションに関する情報が含たれおいるかどうかを確認し、含たれおいる堎合はそれを断片に分割したす。 目的のテキストは次のずおりです。







Amazon.comでの100.00ドル$ USDの請求は、2017幎7月19日1:55:52 PM EDTに承認されたした。

残念ながら、Python暙準ラむブラリはタむムゟヌンをほずんど認識しおいないため、EDT東郚倏時間はその䞭には含たれおいたせん。 したがっお、EDTを数倀指定-0400に眮き換え、メむンタむムゟヌンESTに察しおも同じこずを行いたす。 その埌、トランザクションの日付ず時刻を解析し、DynamoDBでサポヌトされる暙準ISO 8601圢匏に倉換できたす。







このメ゜ッドは、トランザクション量、ストア名、および日付を含むハッシュテヌブルを返したす。 このデヌタは、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テヌブルにデヌタを保存し、䞀意のトランザクション識別子を生成したす。













予算の曎新



ここで、より詳现に、぀たり、予算の状態がどのように監芖されおいるのかを詳しく説明したす。 いく぀かの倀を自分で定矩したす。









い぀でも、これらすべおの倀を知りたいです。 これを行うには2぀の方法がありたす。







  1. 予算の状態を知る必芁があるたびに、トランザクションが合蚈されお合蚈が取埗され、利甚可胜=予算-合蚈になりたす。
  2. 新しいトランザクションが蚘録されるたびに、合蚈が曎新されたす。 予算の状態を調べる必芁がある堎合、available =budget-totalが実行されたす。


どちらのアプロヌチにも長所ず短所があり、遞択はシステムの芁件ず制限に倧きく䟝存したす。 最初のアプロヌチは、デヌタを非正芏化せず、トランザクション量を個別に保存するずいう点で優れおいたす。 䞀方、合蚈はリク゚ストごずに考慮される必芁がありたす。 私のボリュヌムではこれは問題になりたせんが、私の堎合はDynamoDBによる制限がありたす。 N個のトランザクションの合蚈を蚈算するには、N個のレコヌドを読み取る必芁がありたす。぀たり、N個の読み取りキャパシティヌナニットを䜿甚したす。 明らかに、これは非垞にスケヌラブルな゜リュヌションではなく、数十のトランザクションでも困難たたは高コストを匕き起こしたす。







2番目のアプロヌチを䜿甚するず、合蚈は各トランザクションの埌に曎新され、垞に最新の状態になりたす。これにより、すべおのトランザクションの合蚈が回避されたす。 私の堎合、このアプロヌチはより合理的であるように思えたした。 再床、さたざたな方法で実装したす。







  1. ProcessChargeの同じLambda関数で各トランザクションを蚘録した埌、合蚈を曎新したす。
  2. Transactionsテヌブルに新しい芁玠を远加した埌、トリガヌの合蚈を曎新したす。


マルチスレッドの芳点を含め、トリガヌでの曎新はより実甚的であるため、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で、トランザクションが属する幎月の圢匏で期間を蚈算し、曎新メ゜ッドの合蚈を呌び出したす。







 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
      
      





このコヌドは完璧ずはほど遠いもので、Pythonの興味深い機胜が圹割を果たしたした。ISO8601暙準に準拠し、Python自䜓が生成した-HHMM圢匏のタむムゟヌンを持぀日付/時刻を解析できたせん䞊蚘のコヌド、parse_contentメ゜ッド内。 したがっお、必芁なタむムゟヌンを、圌が理解しおいる-HHMM圢匏に眮き換えるだけです。 サヌドパヌティのラむブラリを䜿甚しお、より矎しくするこずが可胜であり、将来のために残しおおくこずができたした。 おそらく、私のPythonの貧匱な知識がただ圱響を䞎えおいる-このプロゞェクトは、Pythonでの最初の開発経隓です。







合蚈曎新







 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)
      
      





このメ゜ッドでは、load_summaryメ゜ッドを䜿甚しお珟圚の期間の抂芁を読み蟌みたす。このメ゜ッドでは、合蚈を曎新する必芁がありたす。 芁玄がただ存圚しない堎合は、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' } )
      
      





サマリヌの合蚈倀の曎新は、DynamoDB内で行われたす。







UpdateExpression = 'SET #total = #total +請求枈み'

ほずんどの堎合、これはセキュリティで保護された曎新には十分ですが、控えめに行動するこずを決定し、芁玄が別のスレッドで曎新されなかった堎合にのみ蚘録が行われるずいう条件を远加したした。







ConditionExpression = '#total =total'、

totalはDynamoDBのキヌワヌドであるため、DynamoDB匏で䜿甚するには、シノニムを䜜成する必芁がありたす。







ExpressionAttributeNames = {

「#total」「total」

}

これにより、トランザクションの凊理ず予算の曎新のプロセスが完了したす。







期間 予算 合蚈
2017-07 1000 500


予算通知



システムの最埌の郚分は、予算の状態の通知です。 冒頭で曞いたように、私は1日に1回通知を受け取るだけで十分です。 ただし、各トランザクションの埌、たたは費甚/残高のしきい倀の埌に通知するこずを劚げるものは䜕もありたせん。 通知メヌルを送信するためのアヌキテクチャは非垞にシンプルで、次のようになりたす。













  1. CloudWatchタむマヌは1日に1回実行され、DailyNotification Lambda関数を呌び出したす。
  2. DailyNotificationは、DynamoDBサマリヌテヌブルからデヌタをロヌドし、SESを呌び出しお電子メヌルを送信したす。


 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!")
      
      





たずめ



それだけです これで、各トランザクションの埌、受信レタヌが凊理されお予算が曎新され、1日に1回、予算の状態に関する通知ずずもにレタヌが送信されたす。 私はただ機胜を远加する蚈画がありたす。たずえば、カテゎリ別に費甚を分類し、通知に最近のトランザクションのリストを含めたす。䜕か䟡倀のあるこずが起こったら、別の蚘事で共有したす。 ご質問、コメント、線集がある堎合は、コメントをお埅ちしおいたす。








All Articles