Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 画像、タイトル、本文を取得するAPIを作成 #261

Merged
merged 21 commits into from
Jan 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ name = "backend"
version = "0.1.0"
description = "Add your description here"
dependencies = [
"pytest>=8.3.3",
"boto3>=1.35.57",
"moto>=5.0.20",
"ruff>=0.7.3",
"pytest>=8.3.3",
"boto3>=1.35.57",
"moto>=5.0.20",
"ruff>=0.8.4",
]
readme = "README.md"
requires-python = ">= 3.8"
Expand Down
2 changes: 1 addition & 1 deletion requirements-dev.lock
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ requests==2.32.3
# via responses
responses==0.25.3
# via moto
ruff==0.7.3
ruff==0.8.4
# via backend
s3transfer==0.10.3
# via boto3
Expand Down
2 changes: 1 addition & 1 deletion requirements.lock
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ requests==2.32.3
# via responses
responses==0.25.3
# via moto
ruff==0.7.3
ruff==0.8.4
# via backend
s3transfer==0.10.3
# via boto3
Expand Down
278 changes: 278 additions & 0 deletions src/backend/lambda/get_diary_data/get_diary_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
import base64
import json
import logging
import os
from datetime import datetime, timezone
from typing import Any, Dict, Optional

import boto3
from botocore.exceptions import ClientError

logger = logging.getLogger(__name__)
formatter = logging.Formatter(
"[%(asctime)s - %(levelname)s - %(filename)s(func:%(funcName)s, line:%(lineno)d)] %(message)s"
)
handler = logging.StreamHandler()
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.setLevel(logging.INFO)

dynamodb = boto3.resource("dynamodb", region_name="ap-northeast-1")


def create_response(status_code: int, body: dict) -> dict:
"""
HTTPレスポンスを生成します。

Args:
status_code (int): HTTPステータスコード。
body (dict): レスポンスボディ。

Returns:
dict: フォーマット済みのHTTPレスポンス。
"""
return {
"statusCode": status_code,
"body": json.dumps(body),
"headers": {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
}


def validate_date(date: str) -> bool:
"""
日付文字列の形式を検証します。

Args:
date (str): 検証する日付文字列。

Returns:
bool: 日付が有効な場合はTrue。
"""
import re

return bool(re.match(r"^\d{4}-\d{2}-\d{2}$", date))


def get_flower_id(user_id: str, date: str) -> Optional[str]:
"""
DynamoDBからflower_idを取得します。

Args:
user_id (str): ユーザーID。
date (str): 日付文字列。

Returns:
Optional[str]: flower_idまたはNone。
"""
table_name = os.getenv("GENERATIVE_AI_TABLE_NAME")
if not table_name:
logger.error("GENERATIVE_AI_TABLE_NAME is not defined")
raise ValueError("GENERATIVE_AI_TABLE_NAME is not defined")

table = dynamodb.Table(table_name)

try:
response = table.get_item(Key={"user_id": user_id, "date": date})
return response.get("Item", {}).get("flower_id")
except ClientError as e:
logger.error(f"DynamoDB client error: {e.response['Error']['Message']}")
raise


def get_image(flower_id: str) -> Optional[str]:
"""
S3から画像を取得します。

Args:
flower_id (str): Flower ID。

Returns:
Optional[str]: Base64エンコードされた画像データまたはNone。
"""
s3 = boto3.client("s3")
bucket_name = os.getenv("FLOWER_BUCKET_NAME")
if not bucket_name:
logger.error("FLOWER_BUCKET_NAME is not defined")
raise ValueError("FLOWER_BUCKET_NAME is not defined")

s3_key = f"flowers/{flower_id}.png"

try:
response = s3.get_object(Bucket=bucket_name, Key=s3_key)
body = response["Body"].read()
return base64.b64encode(body).decode("utf-8")
except ClientError as e:
if e.response["Error"].get("Code") == "NoSuchKey":
logger.info(f"image not found: {s3_key}")
return ""


def get_title(user_id: str, date: str) -> Optional[str]:
"""
DynamoDBからタイトルを取得します。

Args:
user_id (str): ユーザーID。
date (str): 日付文字列。

Returns:
Optional[str]: タイトルまたはNone。
"""
generative_ai_table_name = os.getenv("GENERATIVE_AI_TABLE_NAME")
if not generative_ai_table_name:
logger.error("GENERATIVE_AI_TABLE_NAME is not defined")
raise ValueError("GENERATIVE_AI_TABLE_NAME is not defined")

generative_ai_table = dynamodb.Table(generative_ai_table_name)

try:
response = generative_ai_table.get_item(Key={"user_id": user_id, "date": date})
return response.get("Item", {}).get("title")
except ClientError as e:
logger.info(f"DynamoDB client error: {e.response['Error']['Message']}")
return ""


def get_body(user_id: str, date: str) -> Optional[str]:
"""
DynamoDBから本文を取得します。

Args:
user_id (str): ユーザーID。
date (str): 日付文字列。

Returns:
Optional[str]: 本文またはNone。
"""
diary_table_name = os.getenv("GENERATIVE_AI_TABLE_NAME")
if not diary_table_name:
logger.error("TABLE_NAME is not defined")
raise ValueError("TABLE_NAME is not defined")

diary_table = dynamodb.Table(diary_table_name)

try:
response = diary_table.get_item(Key={"user_id": user_id, "date": date})
return response.get("Item", {}).get("body")
except ClientError as e:
logger.error(f"DynamoDB client error: {e.response['Error']['Message']}")
raise


def get_current_week() -> (int, int):
"""
現在の年と週番号を取得します。

Returns:
tuple: 現在の年とISO週番号。
"""
current_date = datetime.now(timezone.utc)
current_year, current_week, _ = current_date.isocalendar()
return current_year, current_week


def check_bouquet_created(user_id: str, current_year: int, current_week: int) -> bool:
"""
現在の週にブーケが作成されたか確認します。

Args:
user_id (str): ユーザーID。
current_year (int): 現在の年。
current_week (int): 現在のISO週番号。

Returns:
bool: ブーケが作成されている場合はTrue、それ以外はFalse。
"""
bouquet_table_name = os.getenv("BOUQUET_TABLE_NAME")
bouquet_table = dynamodb.Table(bouquet_table_name)
try:
bouquet_table.get_item(
Key={"user_id": user_id, "year_week": f"{current_year}-{current_week}"}
)
return True
except ClientError:
return False


def count_flowers_in_week(user_id: str, current_year: int, current_week: int) -> int:
"""
現在の週にS3にある花の数をカウントします。

Args:
user_id (str): ユーザーID。
current_year (int): 現在の年。
current_week (int): 現在のISO週番号。

Returns:
int: 花の数。
"""
s3 = boto3.client("s3")
bucket_name = os.getenv("FLOWER_BUCKET_NAME")
prefix = f"{user_id}/{current_year}-{current_week}"

try:
response = s3.list_objects_v2(Bucket=bucket_name, Prefix=prefix)
return len(response.get("Contents", []))
except ClientError as e:
logger.error(f"S3 error: {e.response['Error']['Message']}")
raise


def lambda_handler(event: Dict[str, Any], context: Any) -> Dict[str, Any]:
"""
Lambda関数: 画像、タイトル、本文をDynamoDBから取得し、
ブーケ作成可能かどうかをチェックします。

Args:
event (Dict[str, Any]): Lambdaイベントデータ。
context (Any): Lambdaコンテキストオブジェクト。

Returns:
Dict[str, Any]: API Gateway互換のレスポンス。
"""
try:
query_params = event.get("queryStringParameters", {})
if not query_params or "date" not in query_params:
return create_response(
400, {"error": "Required parameter is missing: date"}
)

user_id = event["requestContext"]["authorizer"]["claims"]["sub"]
date = query_params["date"]
if not validate_date(date):
return create_response(400, {"error": "Invalid date format"})

flower_id = get_flower_id(user_id, date)
mt-osho-san marked this conversation as resolved.
Show resolved Hide resolved
image = get_image(flower_id) if flower_id else None
title = get_title(user_id, date)
body = get_body(user_id, date)

current_year, current_week = get_current_week()
bouquet_created = check_bouquet_created(user_id, current_year, current_week)
logger.info(f"bouquet create : {bouquet_created}")
flower_count = count_flowers_in_week(user_id, current_year, current_week)
logger.info(f"flower count: {flower_count}")
can_create_bouquet = not bouquet_created and flower_count >= 5
logger.info(f"can create bouquet: {can_create_bouquet}")

response_data = {
"image": image,
"title": title or "",
"body": body or "",
"can_create_bouquet": can_create_bouquet,
}

return create_response(200, response_data)

except Exception as e:
logger.error(f"Unexpected error: {str(e)}")
return create_response(
500,
{
"error": "Internal server error",
"details": str(e) if os.getenv("DEBUG") else "Please contact support",
},
)
1 change: 1 addition & 0 deletions src/backend/lib/backend-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export class BackendStack extends cdk.Stack {
const bouquet = new Bouquet(this, 'Bouquet', {
userPool: auth.userPool,
table: flower.table,
bouquetTable: diary.bouquetTable,
mt-osho-san marked this conversation as resolved.
Show resolved Hide resolved
api: api.api,
generativeAiTable: flower.generativeAiTable,
cognitoAuthorizer: api.cognitoAuthorizer,
Expand Down
22 changes: 4 additions & 18 deletions src/backend/lib/constructs/bouquet.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import * as cdk from 'aws-cdk-lib'
import * as apigateway from 'aws-cdk-lib/aws-apigateway'
import type * as cognito from 'aws-cdk-lib/aws-cognito'
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'
import type * as dynamodb from 'aws-cdk-lib/aws-dynamodb'
import * as lambda from 'aws-cdk-lib/aws-lambda'
import * as s3 from 'aws-cdk-lib/aws-s3'
import { Construct } from 'constructs'

export interface BouquetProps {
userPool: cognito.UserPool
table: dynamodb.Table
bouquetTable: dynamodb.Table
api: apigateway.RestApi
generativeAiTable: dynamodb.Table
cognitoAuthorizer: apigateway.CognitoUserPoolsAuthorizer
Expand Down Expand Up @@ -37,21 +38,6 @@ export class Bouquet extends Construct {
})
bouquetBucket.grantRead(bouquetGetFunction)

// 花束の作成情報を保存するDynamoDBテーブルの作成
const bouquetTable = new dynamodb.Table(this, 'BouquetTable', {
partitionKey: {
name: 'user_id',
type: dynamodb.AttributeType.STRING,
},
sortKey: {
name: 'year_week',
type: dynamodb.AttributeType.STRING,
},
removalPolicy: cdk.RemovalPolicy.DESTROY,
pointInTimeRecovery: true,
stream: dynamodb.StreamViewType.NEW_AND_OLD_IMAGES,
})

//花束作成用Lambda関数の定義
const BouquetCreate = new lambda.Function(this, 'BouquetCreate', {
runtime: lambda.Runtime.PYTHON_3_11,
Expand All @@ -65,13 +51,13 @@ export class Bouquet extends Construct {
}),
environment: {
GENERATIVE_AI_TABLE_NAME: props.generativeAiTable.tableName,
BOUQUET_TABLE_NAME: bouquetTable.tableName,
BOUQUET_TABLE_NAME: props.bouquetTable.tableName,
ORIGINAL_IMAGE_BUCKET_NAME: props.originalImageBucket.bucketName,
BOUQUET_BUCKET_NAME: bouquetBucket.bucketName,
},
})
props.generativeAiTable.grantReadData(BouquetCreate)
bouquetTable.grantWriteData(BouquetCreate)
props.bouquetTable.grantWriteData(BouquetCreate)
props.originalImageBucket.grantRead(BouquetCreate)
bouquetBucket.grantPut(BouquetCreate)

Expand Down
Loading
Loading