From 0989cbdbb35f515bcf6d42900c608bbbba2a74f5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=82=8A=E3=81=8A=E3=81=A1=E3=82=93?=
<118029.ichikama@gmail.com>
Date: Sun, 14 Sep 2025 12:32:54 +0900
Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E3=81=8D=E3=81=A3=E3=81=A8?=
=?UTF-8?q?=E9=80=9A=E7=9F=A5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
client/app/profile/page.tsx | 6 +
client/components/PostcardDetailModal.tsx | 225 ++++++++++++++
client/components/PushNotificationManager.tsx | 199 +++++++++++++
client/hooks/usePushNotification.ts | 138 +++++++++
client/public/sw.js | 116 +++++++-
client/src/api/client.gen.ts | 1 +
infra/environments/dev/main.tf | 10 +
infra/environments/dev/outputs.tf | 11 +
infra/environments/dev/variables.tf | 8 +
infra/modules/iam/main.tf | 7 +
infra/modules/iam/variables.tf | 6 +
infra/modules/sns/main.tf | 47 +++
infra/modules/sns/outputs.tf | 19 ++
infra/modules/sns/variables.tf | 22 ++
scripts/generate-vapid-keys.js | 65 ++++
server/database/client.py | 14 +-
server/database/users.py | 50 +++-
server/main.py | 18 +-
server/models/__init__.py | 2 +
server/models/user.py | 6 +
server/pyproject.toml | 1 +
server/routers/postcards.py | 34 +++
server/routers/users.py | 71 +++++
server/services/__init__.py | 1 +
server/services/sns_service.py | 134 +++++++++
server/uv.lock | 281 ++++++++++++++++++
26 files changed, 1452 insertions(+), 40 deletions(-)
create mode 100644 client/components/PostcardDetailModal.tsx
create mode 100644 client/components/PushNotificationManager.tsx
create mode 100644 client/hooks/usePushNotification.ts
create mode 100644 infra/modules/sns/main.tf
create mode 100644 infra/modules/sns/outputs.tf
create mode 100644 infra/modules/sns/variables.tf
create mode 100644 scripts/generate-vapid-keys.js
create mode 100644 server/services/__init__.py
create mode 100644 server/services/sns_service.py
diff --git a/client/app/profile/page.tsx b/client/app/profile/page.tsx
index e57197e..fa16abc 100644
--- a/client/app/profile/page.tsx
+++ b/client/app/profile/page.tsx
@@ -20,6 +20,7 @@ import {
import { getAuthToken, getAccessToken } from "@/src/utils/auth";
import { checkUserExists } from "@/src/utils/user";
import type { UserProfile } from "@/src/api/types.gen";
+import { PushNotificationManager } from "@/components/PushNotificationManager";
Amplify.configure(outputs);
@@ -173,6 +174,11 @@ export default function ProfilePage() {
+ {/* Push Notification Settings */}
+
+
{/* Actions */}
+ : null}
+ >
+ 保存
+
+ >
+ ) : (
+ }
+ variant="light"
+ >
+ 編集
+
+ )
+ ) : (
+ // Other's postcard - show collect button
+ : null}
+ >
+ キャッチする
+
+ )}
+
+
+ )}
+
+ );
+};
+
+export default PostcardDetailModal;
diff --git a/client/components/PushNotificationManager.tsx b/client/components/PushNotificationManager.tsx
new file mode 100644
index 0000000..95570d9
--- /dev/null
+++ b/client/components/PushNotificationManager.tsx
@@ -0,0 +1,199 @@
+"use client";
+
+import { useEffect } from "react";
+import { Button, Text, Group, Alert } from "@mantine/core";
+import { notifications } from "@mantine/notifications";
+import { IconBell, IconBellOff, IconAlertCircle } from "@tabler/icons-react";
+import { usePushNotification } from "@/hooks/usePushNotification";
+import { client } from "@/src/api/client.gen";
+import { getIdToken } from "@/src/utils/auth";
+
+interface PushNotificationManagerProps {
+ isAuthenticated?: boolean;
+}
+
+export function PushNotificationManager({
+ isAuthenticated = false,
+}: PushNotificationManagerProps) {
+ const {
+ isSupported,
+ subscription,
+ isLoading,
+ error,
+ subscribeToPushNotifications,
+ unsubscribeFromPushNotifications,
+ } = usePushNotification();
+
+ const sendSubscriptionToServer = async (
+ pushSubscription: PushSubscription,
+ ) => {
+ try {
+ // Convert PushSubscription to the format expected by the server
+ const subscriptionData = {
+ fcm_token: JSON.stringify({
+ endpoint: pushSubscription.endpoint,
+ keys: {
+ auth: pushSubscription.getKey("auth")
+ ? btoa(
+ String.fromCharCode(
+ ...new Uint8Array(pushSubscription.getKey("auth")!),
+ ),
+ )
+ : null,
+ p256dh: pushSubscription.getKey("p256dh")
+ ? btoa(
+ String.fromCharCode(
+ ...new Uint8Array(pushSubscription.getKey("p256dh")!),
+ ),
+ )
+ : null,
+ },
+ }),
+ };
+
+ // Get auth token
+ const authToken = await getIdToken();
+
+ // Use fetch API directly since the endpoint is not in the generated SDK yet
+ const baseUrl =
+ process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000";
+ const response = await fetch(
+ `${baseUrl}/api/users/me/push-subscription`,
+ {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ ...(authToken && { Authorization: `Bearer ${authToken}` }),
+ },
+ body: JSON.stringify(subscriptionData),
+ },
+ );
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ notifications.show({
+ title: "成功",
+ message: "プッシュ通知が有効になりました!",
+ color: "green",
+ icon: ,
+ });
+ } catch (err) {
+ console.error("Failed to send subscription to server:", err);
+ notifications.show({
+ title: "エラー",
+ message: "プッシュ通知の設定に失敗しました",
+ color: "red",
+ icon: ,
+ });
+ }
+ };
+
+ const handleSubscribe = async () => {
+ try {
+ const pushSubscription = await subscribeToPushNotifications();
+ await sendSubscriptionToServer(pushSubscription);
+ } catch (err) {
+ notifications.show({
+ title: "エラー",
+ message:
+ err instanceof Error
+ ? err.message
+ : "プッシュ通知の有効化に失敗しました",
+ color: "red",
+ icon: ,
+ });
+ }
+ };
+
+ const handleUnsubscribe = async () => {
+ try {
+ await unsubscribeFromPushNotifications();
+ notifications.show({
+ title: "無効化",
+ message: "プッシュ通知が無効になりました",
+ color: "blue",
+ icon: ,
+ });
+ } catch (err) {
+ notifications.show({
+ title: "エラー",
+ message:
+ err instanceof Error
+ ? err.message
+ : "プッシュ通知の無効化に失敗しました",
+ color: "red",
+ icon: ,
+ });
+ }
+ };
+
+ // Auto-enable push notifications when user is authenticated and not already subscribed
+ useEffect(() => {
+ if (
+ isAuthenticated &&
+ isSupported &&
+ !subscription &&
+ !isLoading &&
+ !error
+ ) {
+ // Auto-subscribe silently on first login
+ handleSubscribe();
+ }
+ }, [isAuthenticated, isSupported, subscription, isLoading, error]);
+
+ if (!isAuthenticated) {
+ return null;
+ }
+
+ if (!isSupported) {
+ return (
+ }
+ >
+ このブラウザはプッシュ通知をサポートしていません
+
+ );
+ }
+
+ return (
+
+
+ 絵葉書が拾われた時の通知:
+
+
+ {error && (
+ }>
+ {error}
+
+ )}
+
+ {subscription ? (
+ }
+ onClick={handleUnsubscribe}
+ loading={isLoading}
+ >
+ 通知を無効にする
+
+ ) : (
+ }
+ onClick={handleSubscribe}
+ loading={isLoading}
+ >
+ 通知を有効にする
+
+ )}
+
+ );
+}
diff --git a/client/hooks/usePushNotification.ts b/client/hooks/usePushNotification.ts
new file mode 100644
index 0000000..45bd3d8
--- /dev/null
+++ b/client/hooks/usePushNotification.ts
@@ -0,0 +1,138 @@
+"use client";
+
+import { useState, useEffect, useCallback } from "react";
+
+const VAPID_PUBLIC_KEY = process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY;
+
+export function usePushNotification() {
+ const [isSupported, setIsSupported] = useState(false);
+ const [subscription, setSubscription] = useState(
+ null,
+ );
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ if ("serviceWorker" in navigator && "PushManager" in window) {
+ setIsSupported(true);
+ }
+ }, []);
+
+ const urlBase64ToUint8Array = (base64String: string) => {
+ const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
+ const base64 = (base64String + padding)
+ .replace(/-/g, "+")
+ .replace(/_/g, "/");
+
+ const rawData = window.atob(base64);
+ const outputArray = new Uint8Array(rawData.length);
+
+ for (let i = 0; i < rawData.length; ++i) {
+ outputArray[i] = rawData.charCodeAt(i);
+ }
+ return outputArray;
+ };
+
+ const registerServiceWorker = async () => {
+ if (!("serviceWorker" in navigator)) {
+ throw new Error("Service Worker is not supported");
+ }
+
+ const registration = await navigator.serviceWorker.register("/sw.js");
+ await navigator.serviceWorker.ready;
+ return registration;
+ };
+
+ const subscribeToPushNotifications = useCallback(async () => {
+ if (!isSupported) {
+ throw new Error("Push notifications are not supported");
+ }
+
+ if (!VAPID_PUBLIC_KEY) {
+ throw new Error("VAPID public key is not configured");
+ }
+
+ setIsLoading(true);
+ setError(null);
+
+ try {
+ // Request notification permission
+ const permission = await Notification.requestPermission();
+ if (permission !== "granted") {
+ throw new Error("Notification permission denied");
+ }
+
+ // Register service worker
+ const registration = await registerServiceWorker();
+
+ // Subscribe to push notifications
+ const pushSubscription = await registration.pushManager.subscribe({
+ userVisibleOnly: true,
+ applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY),
+ });
+
+ setSubscription(pushSubscription);
+ return pushSubscription;
+ } catch (err) {
+ const errorMessage =
+ err instanceof Error
+ ? err.message
+ : "Failed to subscribe to push notifications";
+ setError(errorMessage);
+ throw new Error(errorMessage);
+ } finally {
+ setIsLoading(false);
+ }
+ }, [isSupported]);
+
+ const unsubscribeFromPushNotifications = useCallback(async () => {
+ if (!subscription) {
+ return;
+ }
+
+ setIsLoading(true);
+ setError(null);
+
+ try {
+ await subscription.unsubscribe();
+ setSubscription(null);
+ } catch (err) {
+ const errorMessage =
+ err instanceof Error
+ ? err.message
+ : "Failed to unsubscribe from push notifications";
+ setError(errorMessage);
+ throw new Error(errorMessage);
+ } finally {
+ setIsLoading(false);
+ }
+ }, [subscription]);
+
+ const checkExistingSubscription = useCallback(async () => {
+ if (!isSupported) {
+ return;
+ }
+
+ try {
+ const registration = await navigator.serviceWorker.ready;
+ const existingSubscription =
+ await registration.pushManager.getSubscription();
+ setSubscription(existingSubscription);
+ } catch (err) {
+ console.error("Failed to check existing subscription:", err);
+ }
+ }, [isSupported]);
+
+ useEffect(() => {
+ checkExistingSubscription();
+ }, [checkExistingSubscription]);
+
+ return {
+ isSupported,
+ subscription,
+ isLoading,
+ error,
+ subscribeToPushNotifications,
+ unsubscribeFromPushNotifications,
+ };
+}
diff --git a/client/public/sw.js b/client/public/sw.js
index 71b4824..0e4ef37 100644
--- a/client/public/sw.js
+++ b/client/public/sw.js
@@ -1,24 +1,112 @@
self.addEventListener("push", function (event) {
+ console.log("Push event received:", event);
+
if (event.data) {
- const data = event.data.json();
- const options = {
- body: data.body,
- icon: data.icon || "/icon-512x512.png",
- badge: "/icon-512x512.png",
- vibrate: [100, 50, 100],
- data: {
- dateOfArrival: Date.now(),
- primaryKey: "2",
- },
- };
- event.waitUntil(self.registration.showNotification(data.title, options));
+ try {
+ // Try to parse as JSON first
+ let data;
+ try {
+ data = event.data.json();
+ } catch (e) {
+ // If JSON parsing fails, try to parse as text
+ const textData = event.data.text();
+ console.log("Received text data:", textData);
+
+ // Try to extract JSON from SNS format
+ try {
+ const snsData = JSON.parse(textData);
+ if (snsData.Message) {
+ // SNS sends the actual message in the Message field
+ data = JSON.parse(snsData.Message);
+ } else {
+ data = snsData;
+ }
+ } catch (e2) {
+ // Fallback: create notification from raw text
+ data = {
+ title: "絵葉書通知",
+ body: textData,
+ };
+ }
+ }
+
+ const options = {
+ body: data.body || data.message || "新しい通知があります",
+ icon: data.icon || "/icon-512x512.png",
+ badge: "/icon-512x512.png",
+ vibrate: [100, 50, 100],
+ data: {
+ dateOfArrival: Date.now(),
+ primaryKey: "postcard-notification",
+ url: data.url || "/collection", // Default to collection page
+ },
+ requireInteraction: true, // Keep notification visible until user interacts
+ actions: [
+ {
+ action: "view",
+ title: "確認する",
+ },
+ ],
+ };
+
+ const title = data.title || "Postcard";
+
+ event.waitUntil(self.registration.showNotification(title, options));
+ } catch (error) {
+ console.error("Error processing push event:", error);
+
+ // Fallback notification
+ const fallbackOptions = {
+ body: "新しい通知があります",
+ icon: "/icon-512x512.png",
+ badge: "/icon-512x512.png",
+ data: {
+ dateOfArrival: Date.now(),
+ primaryKey: "postcard-notification-fallback",
+ },
+ };
+
+ event.waitUntil(
+ self.registration.showNotification("Postcard", fallbackOptions),
+ );
+ }
}
});
self.addEventListener("notificationclick", function (event) {
- console.log("Notification click received.");
+ console.log("Notification click received:", event);
+
event.notification.close();
+
+ let urlToOpen = event.notification.data?.url || "/collection";
+
+ // Handle action buttons
+ if (event.action === "view") {
+ urlToOpen = "/collection";
+ }
+
event.waitUntil(
- clients.openWindow("https://main.d1wwi4yufsd2k4.amplifyapp.com"),
+ clients
+ .matchAll({ type: "window", includeUncontrolled: true })
+ .then(function (clientList) {
+ // Check if there is already a window/tab open with the target URL
+ for (let i = 0; i < clientList.length; i++) {
+ const client = clientList[i];
+ if (client.url.includes(urlToOpen) && "focus" in client) {
+ return client.focus();
+ }
+ }
+
+ // If no existing window/tab with target URL, open a new one
+ if (clients.openWindow) {
+ const baseUrl = self.location.origin;
+ return clients.openWindow(baseUrl + urlToOpen);
+ }
+ }),
);
});
+
+// Handle notification close event
+self.addEventListener("notificationclose", function (event) {
+ console.log("Notification closed:", event);
+});
diff --git a/client/src/api/client.gen.ts b/client/src/api/client.gen.ts
index f283a74..0b65e65 100644
--- a/client/src/api/client.gen.ts
+++ b/client/src/api/client.gen.ts
@@ -24,5 +24,6 @@ export type CreateClientConfig =
export const client = createClient(
createConfig({
baseUrl: "https://postcard-dev-alb-437445372.us-east-1.elb.amazonaws.com",
+ // baseUrl: "http://localhost:8000",
}),
);
diff --git a/infra/environments/dev/main.tf b/infra/environments/dev/main.tf
index 8f29dc6..f49c679 100644
--- a/infra/environments/dev/main.tf
+++ b/infra/environments/dev/main.tf
@@ -51,6 +51,7 @@ module "iam" {
app_name = var.app_name
environment = var.environment
dynamodb_table_arn = module.dynamodb.table_arn
+ sns_policy_arn = module.sns.sns_policy_arn
tags = local.common_tags
}
@@ -100,6 +101,15 @@ module "dynamodb" {
environment = var.environment
}
+# SNS Module for push notifications
+module "sns" {
+ source = "../../modules/sns"
+ app_name = var.app_name
+ environment = var.environment
+ firebase_server_key = var.firebase_server_key
+ tags = local.common_tags
+}
+
# S3 Bucket
module "s3" {
source = "../../modules/s3"
diff --git a/infra/environments/dev/outputs.tf b/infra/environments/dev/outputs.tf
index f4392ec..ca273c0 100644
--- a/infra/environments/dev/outputs.tf
+++ b/infra/environments/dev/outputs.tf
@@ -52,3 +52,14 @@ output "s3_bucket_arn" {
description = "ARN of the S3 bucket"
value = module.s3.bucket_arn
}
+
+# SNS outputs
+output "sns_topic_arn" {
+ description = "ARN of the SNS topic for postcard notifications"
+ value = module.sns.topic_arn
+}
+
+output "sns_platform_application_arn" {
+ description = "ARN of the SNS platform application"
+ value = module.sns.platform_application_arn
+}
diff --git a/infra/environments/dev/variables.tf b/infra/environments/dev/variables.tf
index 6abab37..16b8e44 100644
--- a/infra/environments/dev/variables.tf
+++ b/infra/environments/dev/variables.tf
@@ -100,3 +100,11 @@ variable "container_environment_variables" {
}))
default = []
}
+
+# SNS related variables
+variable "firebase_server_key" {
+ description = "Firebase Cloud Messaging server key for push notifications"
+ type = string
+ default = null
+ sensitive = true
+}
diff --git a/infra/modules/iam/main.tf b/infra/modules/iam/main.tf
index 43f5bdb..ea3dee6 100644
--- a/infra/modules/iam/main.tf
+++ b/infra/modules/iam/main.tf
@@ -125,3 +125,10 @@ resource "aws_iam_role_policy" "ecs_task_dynamodb_policy" {
]
})
}
+
+# Attach SNS policy to ECS Task Role if provided
+resource "aws_iam_role_policy_attachment" "ecs_task_sns_policy" {
+ count = var.sns_policy_arn != null ? 1 : 0
+ role = aws_iam_role.ecs_task_role.name
+ policy_arn = var.sns_policy_arn
+}
diff --git a/infra/modules/iam/variables.tf b/infra/modules/iam/variables.tf
index 3ec68d0..94ef882 100644
--- a/infra/modules/iam/variables.tf
+++ b/infra/modules/iam/variables.tf
@@ -13,6 +13,12 @@ variable "dynamodb_table_arn" {
type = string
}
+variable "sns_policy_arn" {
+ description = "ARN of the SNS policy for push notifications"
+ type = string
+ default = null
+}
+
variable "tags" {
description = "A map of tags to assign to the resource"
type = map(string)
diff --git a/infra/modules/sns/main.tf b/infra/modules/sns/main.tf
new file mode 100644
index 0000000..f2363eb
--- /dev/null
+++ b/infra/modules/sns/main.tf
@@ -0,0 +1,47 @@
+# SNS topic for postcard collect notifications
+resource "aws_sns_topic" "postcard_collect_notifications" {
+ name = "postcard_collect_notifications"
+
+ tags = var.tags
+}
+
+# SNS Platform Application for Firebase Cloud Messaging (PWA)
+resource "aws_sns_platform_application" "postcard_fcm_app" {
+ count = var.firebase_server_key != null ? 1 : 0
+
+ name = "${var.app_name}-${var.environment}-fcm-app"
+ platform = "GCM"
+ platform_credential = var.firebase_server_key
+}
+
+# IAM policy for SNS publish and endpoint creation
+resource "aws_iam_policy" "sns_publish_policy" {
+ name = "${var.app_name}-${var.environment}-sns-publish-policy"
+ path = "/"
+ description = "Policy for SNS publish and endpoint creation"
+
+ policy = jsonencode({
+ Version = "2012-10-17"
+ Statement = [
+ {
+ Effect = "Allow"
+ Action = [
+ "sns:Publish"
+ ]
+ Resource = aws_sns_topic.postcard_collect_notifications.arn
+ },
+ {
+ Effect = "Allow"
+ Action = [
+ "sns:CreatePlatformEndpoint",
+ "sns:DeleteEndpoint",
+ "sns:GetEndpointAttributes",
+ "sns:SetEndpointAttributes"
+ ]
+ Resource = var.firebase_server_key != null ? "${aws_sns_platform_application.postcard_fcm_app[0].arn}/*" : "*"
+ }
+ ]
+ })
+
+ tags = var.tags
+}
diff --git a/infra/modules/sns/outputs.tf b/infra/modules/sns/outputs.tf
new file mode 100644
index 0000000..2b2c004
--- /dev/null
+++ b/infra/modules/sns/outputs.tf
@@ -0,0 +1,19 @@
+output "topic_arn" {
+ description = "ARN of the SNS topic for postcard collect notifications"
+ value = aws_sns_topic.postcard_collect_notifications.arn
+}
+
+output "topic_name" {
+ description = "Name of the SNS topic"
+ value = aws_sns_topic.postcard_collect_notifications.name
+}
+
+output "platform_application_arn" {
+ description = "ARN of the SNS platform application for FCM"
+ value = var.firebase_server_key != null ? aws_sns_platform_application.postcard_fcm_app[0].arn : null
+}
+
+output "sns_policy_arn" {
+ description = "ARN of the SNS publish policy"
+ value = aws_iam_policy.sns_publish_policy.arn
+}
diff --git a/infra/modules/sns/variables.tf b/infra/modules/sns/variables.tf
new file mode 100644
index 0000000..79cadf9
--- /dev/null
+++ b/infra/modules/sns/variables.tf
@@ -0,0 +1,22 @@
+variable "app_name" {
+ description = "Application name"
+ type = string
+}
+
+variable "environment" {
+ description = "Environment name"
+ type = string
+}
+
+variable "firebase_server_key" {
+ description = "Firebase Cloud Messaging server key for PWA push notifications"
+ type = string
+ default = null
+ sensitive = true
+}
+
+variable "tags" {
+ description = "Common tags for resources"
+ type = map(string)
+ default = {}
+}
diff --git a/scripts/generate-vapid-keys.js b/scripts/generate-vapid-keys.js
new file mode 100644
index 0000000..2ed5b10
--- /dev/null
+++ b/scripts/generate-vapid-keys.js
@@ -0,0 +1,65 @@
+const crypto = require("crypto");
+
+// VAPID キーペアを生成する関数
+function generateVAPIDKeys() {
+ // P-256楕円曲線を使用してキーペアを生成
+ const keyPair = crypto.generateKeyPairSync("ec", {
+ namedCurve: "prime256v1",
+ publicKeyEncoding: {
+ type: "spki",
+ format: "der",
+ },
+ privateKeyEncoding: {
+ type: "pkcs8",
+ format: "der",
+ },
+ });
+
+ // キーをbase64urlエンコード
+ const publicKey = keyPair.publicKey.subarray(-65); // 最後の65バイト(未圧縮点)
+ const privateKey = keyPair.privateKey.subarray(-32); // 最後の32バイト(秘密鍵)
+
+ const publicKeyBase64url = Buffer.from(publicKey)
+ .toString("base64")
+ .replace(/\+/g, "-")
+ .replace(/\//g, "_")
+ .replace(/=/g, "");
+
+ const privateKeyBase64url = Buffer.from(privateKey)
+ .toString("base64")
+ .replace(/\+/g, "-")
+ .replace(/\//g, "_")
+ .replace(/=/g, "");
+
+ return {
+ publicKey: publicKeyBase64url,
+ privateKey: privateKeyBase64url,
+ };
+}
+
+// キーペアを生成して出力
+const vapidKeys = generateVAPIDKeys();
+
+console.log("=== VAPID Keys Generated ===");
+console.log();
+console.log("Public Key (for client-side):");
+console.log(vapidKeys.publicKey);
+console.log();
+console.log("Private Key (for server-side):");
+console.log(vapidKeys.privateKey);
+console.log();
+console.log("=== Environment Variables ===");
+console.log();
+console.log("For client (.env.local):");
+console.log(`NEXT_PUBLIC_VAPID_PUBLIC_KEY=${vapidKeys.publicKey}`);
+console.log();
+console.log("For server (.env):");
+console.log(`VAPID_PUBLIC_KEY=${vapidKeys.publicKey}`);
+console.log(`VAPID_PRIVATE_KEY=${vapidKeys.privateKey}`);
+console.log(
+ `SNS_TOPIC_ARN=arn:aws:sns:us-east-1:868865500828:postcard_collect_notifications`,
+);
+console.log("SNS_PLATFORM_APPLICATION_ARN= # Set this if using Firebase/GCM");
+console.log();
+
+module.exports = { generateVAPIDKeys };
diff --git a/server/database/client.py b/server/database/client.py
index 5c9cdd1..b14b007 100644
--- a/server/database/client.py
+++ b/server/database/client.py
@@ -15,9 +15,16 @@ def __init__(self):
# User operations
def create_user(
- self, user_id: str, username: str, email: str, profile_image_url: str
+ self,
+ user_id: str,
+ username: str,
+ email: str,
+ profile_image_url: str,
+ sns_endpoint_arn: str = None,
) -> bool:
- return self.users.create_user(user_id, username, email, profile_image_url)
+ return self.users.create_user(
+ user_id, username, email, profile_image_url, sns_endpoint_arn
+ )
def get_user(self, user_id: str):
return self.users.get_user(user_id)
@@ -28,6 +35,9 @@ def update_user(self, user_id: str, username: str, profile_image_url: str) -> bo
def delete_user(self, user_id: str) -> bool:
return self.users.delete_user(user_id)
+ def update_user_sns_endpoint(self, user_id: str, sns_endpoint_arn: str) -> bool:
+ return self.users.update_user_sns_endpoint(user_id, sns_endpoint_arn)
+
# Postcard operations
def create_postcard(
self, author_id: str, image_url: str, text: str, lat: float, lon: float
diff --git a/server/database/users.py b/server/database/users.py
index c77c021..d91e06e 100644
--- a/server/database/users.py
+++ b/server/database/users.py
@@ -10,21 +10,31 @@ def __init__(self, client):
self.client = client
def create_user(
- self, user_id: str, username: str, email: str, profile_image_url: str
+ self,
+ user_id: str,
+ username: str,
+ email: str,
+ profile_image_url: str,
+ sns_endpoint_arn: Optional[str] = None,
) -> bool:
"""Create a new user profile"""
try:
+ item = {
+ "PK": f"USER#{user_id}",
+ "SK": "PROFILE",
+ "user_id": user_id,
+ "username": username,
+ "email": email,
+ "profile_image_url": profile_image_url,
+ "created_at": self.client._get_timestamp(),
+ "updated_at": self.client._get_timestamp(),
+ }
+
+ if sns_endpoint_arn:
+ item["sns_endpoint_arn"] = sns_endpoint_arn
+
self.client.table.put_item(
- Item={
- "PK": f"USER#{user_id}",
- "SK": "PROFILE",
- "user_id": user_id,
- "username": username,
- "email": email,
- "profile_image_url": profile_image_url,
- "created_at": self.client._get_timestamp(),
- "updated_at": self.client._get_timestamp(),
- },
+ Item=item,
ConditionExpression=Attr("PK").not_exists(),
)
return True
@@ -75,3 +85,21 @@ def delete_user(self, user_id: str) -> bool:
if e.response["Error"]["Code"] == "ConditionalCheckFailedException":
return False
self.client._handle_client_error(e, "delete_user")
+
+ def update_user_sns_endpoint(self, user_id: str, sns_endpoint_arn: str) -> bool:
+ """Update user's SNS endpoint ARN"""
+ try:
+ self.client.table.update_item(
+ Key={"PK": f"USER#{user_id}", "SK": "PROFILE"},
+ UpdateExpression="SET sns_endpoint_arn = :endpoint_arn, updated_at = :updated_at",
+ ExpressionAttributeValues={
+ ":endpoint_arn": sns_endpoint_arn,
+ ":updated_at": self.client._get_timestamp(),
+ },
+ ConditionExpression=Attr("PK").exists(),
+ )
+ return True
+ except ClientError as e:
+ if e.response["Error"]["Code"] == "ConditionalCheckFailedException":
+ return False
+ self.client._handle_client_error(e, "update_user_sns_endpoint")
diff --git a/server/main.py b/server/main.py
index d06a288..deefe3a 100644
--- a/server/main.py
+++ b/server/main.py
@@ -28,21 +28,13 @@
)
# Add CORS middleware
+# Temporarily allow all origins for testing
app.add_middleware(
CORSMiddleware,
- allow_origins=[
- "http://localhost:3000", # Next.js development
- "https://localhost:3000", # Next.js development (HTTPS)
- "http://127.0.0.1:3000", # Alternative localhost
- "http://localhost:8000", # FastAPI Swagger UI
- "http://127.0.0.1:8000", # FastAPI Swagger UI alternative
- "https://main.doyow5whm2yhd.amplifyapp.com",
- "http://postcard-dev-alb-437445372.us-east-1.elb.amazonaws.com", # ←追加
- "https://postcard-dev-alb-437445372.us-east-1.elb.amazonaws.com", # ←httpsも必要なら
- ],
- allow_credentials=True,
- allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
- allow_headers=["*"],
+ allow_origins=["*"], # Allow all origins for testing
+ allow_credentials=False, # Must be False when allow_origins=["*"]
+ allow_methods=["*"], # Allow all methods
+ allow_headers=["*"], # Allow all headers
)
# Include routers
diff --git a/server/models/__init__.py b/server/models/__init__.py
index 0bb68fe..a6312af 100644
--- a/server/models/__init__.py
+++ b/server/models/__init__.py
@@ -6,6 +6,7 @@
UserUpdateRequest,
UserUpdateResponse,
UserDeleteResponse,
+ PushSubscription,
)
from .postcard import (
PostcardCreateRequest,
@@ -39,6 +40,7 @@
"UserUpdateRequest",
"UserUpdateResponse",
"UserDeleteResponse",
+ "PushSubscription",
# Postcard models
"PostcardCreateRequest",
"PostcardCreateResponse",
diff --git a/server/models/user.py b/server/models/user.py
index 154b4a4..1f20ddf 100644
--- a/server/models/user.py
+++ b/server/models/user.py
@@ -1,9 +1,15 @@
from pydantic import BaseModel
+from typing import Optional
class UserCreateRequest(BaseModel):
username: str
profile_image_url: str
+ fcm_token: Optional[str] = None
+
+
+class PushSubscription(BaseModel):
+ fcm_token: str
class UserCreateResponse(BaseModel):
diff --git a/server/pyproject.toml b/server/pyproject.toml
index 04d12b9..d56a1b6 100644
--- a/server/pyproject.toml
+++ b/server/pyproject.toml
@@ -13,4 +13,5 @@ dependencies = [
"requests",
"pyjwt[crypto]",
"python-dotenv",
+ "pywebpush",
]
diff --git a/server/routers/postcards.py b/server/routers/postcards.py
index 8f06db7..4e3ad56 100644
--- a/server/routers/postcards.py
+++ b/server/routers/postcards.py
@@ -16,6 +16,7 @@
)
from database import db
from auth import get_current_user
+from services.sns_service import SNSService
router = APIRouter(prefix="/api/postcards", tags=["postcards"])
@@ -236,6 +237,15 @@ async def collect_postcard(
postcard_id: str, current_user: dict = Depends(get_current_user)
):
user_id = current_user["user_id"]
+ collector_username = current_user.get("username")
+
+ # Get postcard details before collecting it
+ postcard = db.get_postcard(postcard_id)
+ if not postcard:
+ raise HTTPException(
+ status_code=404,
+ detail="指定した絵葉書IDが見つからない場合",
+ )
success = db.collect_postcard(user_id, postcard_id)
if not success:
@@ -244,6 +254,30 @@ async def collect_postcard(
detail="指定した絵葉書IDが見つからない、またはすでに拾われている場合",
)
+ # Send notification to the original author
+ author_id = postcard["author_id"]
+ if author_id != user_id: # Don't notify if user collected their own postcard
+ # Get author information to retrieve SNS endpoint
+ author = db.get_user(author_id)
+ if author and author.get("sns_endpoint_arn"):
+ sns_service = SNSService()
+
+ # Get collector's username for the notification
+ collector = db.get_user(user_id) if not collector_username else None
+ username = collector_username or (
+ collector.get("username") if collector else "Someone"
+ )
+
+ notification_message = f"{username}があなたの絵葉書を拾いました!"
+ notification_title = "絵葉書が拾われました"
+
+ # Send push notification
+ sns_service.send_notification(
+ endpoint_arn=author["sns_endpoint_arn"],
+ message=notification_message,
+ title=notification_title,
+ )
+
return CollectResponse(message="絵葉書をコレクションに追加しました。")
diff --git a/server/routers/users.py b/server/routers/users.py
index 7e0aaca..e875559 100644
--- a/server/routers/users.py
+++ b/server/routers/users.py
@@ -8,9 +8,11 @@
UserUpdateResponse,
UserDeleteResponse,
ErrorResponse,
+ PushSubscription,
)
from database import db
from auth import get_current_user
+from services.sns_service import SNSService
router = APIRouter(prefix="/api/users", tags=["users"])
@@ -39,12 +41,24 @@ async def create_user_profile(
user_id = current_user["user_id"]
email = current_user["email"]
+ sns_endpoint_arn = None
+
+ # Create SNS platform endpoint if FCM token is provided
+ if user_data.fcm_token:
+ sns_service = SNSService()
+ sns_endpoint_arn = sns_service.create_platform_endpoint(
+ user_id=user_id,
+ token=user_data.fcm_token,
+ user_data={"username": user_data.username},
+ )
+
# Create user profile in database
success = db.create_user(
user_id=user_id,
username=user_data.username,
email=email,
profile_image_url=user_data.profile_image_url,
+ sns_endpoint_arn=sns_endpoint_arn,
)
if not success:
@@ -176,3 +190,60 @@ async def delete_my_profile(current_user: dict = Depends(get_current_user)):
)
return UserDeleteResponse(message="ユーザーが削除されました。")
+
+
+@router.post(
+ "/me/push-subscription",
+ status_code=200,
+ summary="プッシュ通知設定",
+ description="PWAプッシュ通知用のFCMトークンを登録・更新します。",
+ responses={
+ 401: {
+ "model": ErrorResponse,
+ "description": "認証トークンがない、または無効な場合",
+ },
+ 400: {
+ "model": ErrorResponse,
+ "description": "FCMトークンが無効な場合",
+ },
+ },
+)
+async def update_push_subscription(
+ push_data: PushSubscription, current_user: dict = Depends(get_current_user)
+):
+ print(f"Received push subscription data: {push_data}")
+ print(f"FCM token: {push_data.fcm_token}")
+
+ user_id = current_user["user_id"]
+ print(f"User ID: {user_id}")
+
+ # Get user profile to check if user exists
+ user = db.get_user(user_id)
+ if not user:
+ raise HTTPException(
+ status_code=404, detail="ユーザープロフィールが見つかりません。"
+ )
+
+ sns_service = SNSService()
+
+ # Create new SNS platform endpoint
+ sns_endpoint_arn = sns_service.create_platform_endpoint(
+ user_id=user_id,
+ token=push_data.fcm_token,
+ user_data={"username": user.get("username")},
+ )
+
+ if not sns_endpoint_arn:
+ print("Failed to create SNS endpoint")
+ raise HTTPException(
+ status_code=400, detail="プッシュ通知の設定に失敗しました。"
+ )
+
+ # Update user's SNS endpoint ARN in database
+ success = db.update_user_sns_endpoint(user_id, sns_endpoint_arn)
+ if not success:
+ raise HTTPException(
+ status_code=404, detail="ユーザープロフィールが見つかりません。"
+ )
+
+ return {"message": "プッシュ通知が設定されました。"}
diff --git a/server/services/__init__.py b/server/services/__init__.py
new file mode 100644
index 0000000..d7f74b5
--- /dev/null
+++ b/server/services/__init__.py
@@ -0,0 +1 @@
+# Services package for external integrations
diff --git a/server/services/sns_service.py b/server/services/sns_service.py
new file mode 100644
index 0000000..8bfef48
--- /dev/null
+++ b/server/services/sns_service.py
@@ -0,0 +1,134 @@
+import os
+import json
+from typing import Optional
+import boto3
+from botocore.exceptions import ClientError
+from pywebpush import webpush, WebPushException
+
+
+class SNSService:
+ """Service class for AWS SNS operations and Web Push notifications"""
+
+ def __init__(self):
+ self.sns_client = boto3.client("sns")
+ self.platform_application_arn = os.getenv("SNS_PLATFORM_APPLICATION_ARN")
+ self.topic_arn = os.getenv("SNS_TOPIC_ARN")
+ self.vapid_private_key = os.getenv("VAPID_PRIVATE_KEY")
+ self.vapid_public_key = os.getenv("VAPID_PUBLIC_KEY")
+
+ # Storage for Web Push subscriptions (in production, use a database)
+ self.web_push_subscriptions = {}
+
+ def create_platform_endpoint(
+ self, user_id: str, token: str, user_data: Optional[dict] = None
+ ) -> Optional[str]:
+ """
+ Create a platform endpoint for push notifications
+ For Web Push, we'll store the subscription data and return a mock ARN
+
+ Args:
+ user_id: Unique user identifier
+ token: Web Push subscription JSON string from the client
+ user_data: Optional user data for the endpoint
+
+ Returns:
+ Mock endpoint ARN for Web Push
+ """
+ print(f"Creating Web Push endpoint for user {user_id}")
+ print(f"Token data: {token}")
+
+ try:
+ # Parse the Web Push subscription data
+ subscription_info = json.loads(token)
+
+ # Store the subscription (in production, save to database)
+ self.web_push_subscriptions[user_id] = subscription_info
+
+ # Create a mock endpoint ARN that includes the user ID
+ mock_endpoint_arn = (
+ f"arn:aws:sns:us-east-1:868865500828:endpoint/WEBPUSH/{user_id}"
+ )
+
+ print(f"Created Web Push endpoint: {mock_endpoint_arn}")
+ print(f"Stored subscription for user {user_id}")
+ return mock_endpoint_arn
+
+ except json.JSONDecodeError as e:
+ print(f"Failed to parse subscription JSON: {e}")
+ return None
+ except Exception as e:
+ print(f"Error creating Web Push endpoint: {e}")
+ return None
+
+ def send_notification(
+ self, endpoint_arn: str, message: str, title: str = "Postcard Notification"
+ ) -> bool:
+ """
+ Send a Web Push notification directly to the user
+
+ Args:
+ endpoint_arn: Mock ARN containing the user ID
+ message: Notification message
+ title: Notification title
+
+ Returns:
+ True if successful, False if failed
+ """
+ try:
+ # Extract user ID from the mock ARN
+ user_id = endpoint_arn.split("/")[-1]
+ print(f"Sending Web Push notification to user {user_id}")
+
+ # Get the stored subscription for this user
+ subscription_info = self.web_push_subscriptions.get(user_id)
+ if not subscription_info:
+ print(f"No subscription found for user {user_id}")
+ return False
+
+ # Create message payload for Web Push
+ web_push_message = {
+ "title": title,
+ "body": message,
+ "icon": "/icon-512x512.png",
+ "badge": "/icon-512x512.png",
+ "url": "/collection",
+ }
+
+ # Send Web Push notification
+ response = webpush(
+ subscription_info=subscription_info,
+ data=json.dumps(web_push_message),
+ vapid_private_key=self.vapid_private_key,
+ vapid_claims={"sub": "mailto:your-email@example.com"},
+ )
+
+ print(f"Web Push notification sent successfully: {response.status_code}")
+ return True
+
+ except WebPushException as e:
+ print(f"Web Push error: {e}")
+ return False
+ except Exception as e:
+ print(f"Error sending Web Push notification: {e}")
+ return False
+
+ def delete_endpoint(self, endpoint_arn: str) -> bool:
+ """
+ Delete a platform endpoint
+
+ Args:
+ endpoint_arn: ARN of the platform endpoint to delete
+
+ Returns:
+ True if successful, False if failed
+ """
+ try:
+ self.sns_client.delete_endpoint(EndpointArn=endpoint_arn)
+ return True
+
+ except ClientError as e:
+ error_code = e.response["Error"]["Code"]
+ print(
+ f"Error deleting endpoint: {error_code} - {e.response['Error']['Message']}"
+ )
+ return False
diff --git a/server/uv.lock b/server/uv.lock
index a2e8fca..8eb6b48 100644
--- a/server/uv.lock
+++ b/server/uv.lock
@@ -2,6 +2,61 @@ version = 1
revision = 3
requires-python = ">=3.13"
+[[package]]
+name = "aiohappyeyeballs"
+version = "2.6.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" },
+]
+
+[[package]]
+name = "aiohttp"
+version = "3.12.15"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "aiohappyeyeballs" },
+ { name = "aiosignal" },
+ { name = "attrs" },
+ { name = "frozenlist" },
+ { name = "multidict" },
+ { name = "propcache" },
+ { name = "yarl" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9b/e7/d92a237d8802ca88483906c388f7c201bbe96cd80a165ffd0ac2f6a8d59f/aiohttp-3.12.15.tar.gz", hash = "sha256:4fc61385e9c98d72fcdf47e6dd81833f47b2f77c114c29cd64a361be57a763a2", size = 7823716, upload-time = "2025-07-29T05:52:32.215Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f2/33/918091abcf102e39d15aba2476ad9e7bd35ddb190dcdd43a854000d3da0d/aiohttp-3.12.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9f922ffd05034d439dde1c77a20461cf4a1b0831e6caa26151fe7aa8aaebc315", size = 696741, upload-time = "2025-07-29T05:51:19.021Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/2a/7495a81e39a998e400f3ecdd44a62107254803d1681d9189be5c2e4530cd/aiohttp-3.12.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2ee8a8ac39ce45f3e55663891d4b1d15598c157b4d494a4613e704c8b43112cd", size = 474407, upload-time = "2025-07-29T05:51:21.165Z" },
+ { url = "https://files.pythonhosted.org/packages/49/fc/a9576ab4be2dcbd0f73ee8675d16c707cfc12d5ee80ccf4015ba543480c9/aiohttp-3.12.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3eae49032c29d356b94eee45a3f39fdf4b0814b397638c2f718e96cfadf4c4e4", size = 466703, upload-time = "2025-07-29T05:51:22.948Z" },
+ { url = "https://files.pythonhosted.org/packages/09/2f/d4bcc8448cf536b2b54eed48f19682031ad182faa3a3fee54ebe5b156387/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b97752ff12cc12f46a9b20327104448042fce5c33a624f88c18f66f9368091c7", size = 1705532, upload-time = "2025-07-29T05:51:25.211Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/f3/59406396083f8b489261e3c011aa8aee9df360a96ac8fa5c2e7e1b8f0466/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:894261472691d6fe76ebb7fcf2e5870a2ac284c7406ddc95823c8598a1390f0d", size = 1686794, upload-time = "2025-07-29T05:51:27.145Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/71/164d194993a8d114ee5656c3b7ae9c12ceee7040d076bf7b32fb98a8c5c6/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5fa5d9eb82ce98959fc1031c28198b431b4d9396894f385cb63f1e2f3f20ca6b", size = 1738865, upload-time = "2025-07-29T05:51:29.366Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/00/d198461b699188a93ead39cb458554d9f0f69879b95078dce416d3209b54/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0fa751efb11a541f57db59c1dd821bec09031e01452b2b6217319b3a1f34f3d", size = 1788238, upload-time = "2025-07-29T05:51:31.285Z" },
+ { url = "https://files.pythonhosted.org/packages/85/b8/9e7175e1fa0ac8e56baa83bf3c214823ce250d0028955dfb23f43d5e61fd/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5346b93e62ab51ee2a9d68e8f73c7cf96ffb73568a23e683f931e52450e4148d", size = 1710566, upload-time = "2025-07-29T05:51:33.219Z" },
+ { url = "https://files.pythonhosted.org/packages/59/e4/16a8eac9df39b48ae102ec030fa9f726d3570732e46ba0c592aeeb507b93/aiohttp-3.12.15-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:049ec0360f939cd164ecbfd2873eaa432613d5e77d6b04535e3d1fbae5a9e645", size = 1624270, upload-time = "2025-07-29T05:51:35.195Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/f8/cd84dee7b6ace0740908fd0af170f9fab50c2a41ccbc3806aabcb1050141/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b52dcf013b57464b6d1e51b627adfd69a8053e84b7103a7cd49c030f9ca44461", size = 1677294, upload-time = "2025-07-29T05:51:37.215Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/42/d0f1f85e50d401eccd12bf85c46ba84f947a84839c8a1c2c5f6e8ab1eb50/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9b2af240143dd2765e0fb661fd0361a1b469cab235039ea57663cda087250ea9", size = 1708958, upload-time = "2025-07-29T05:51:39.328Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/6b/f6fa6c5790fb602538483aa5a1b86fcbad66244997e5230d88f9412ef24c/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ac77f709a2cde2cc71257ab2d8c74dd157c67a0558a0d2799d5d571b4c63d44d", size = 1651553, upload-time = "2025-07-29T05:51:41.356Z" },
+ { url = "https://files.pythonhosted.org/packages/04/36/a6d36ad545fa12e61d11d1932eef273928b0495e6a576eb2af04297fdd3c/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:47f6b962246f0a774fbd3b6b7be25d59b06fdb2f164cf2513097998fc6a29693", size = 1727688, upload-time = "2025-07-29T05:51:43.452Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/c8/f195e5e06608a97a4e52c5d41c7927301bf757a8e8bb5bbf8cef6c314961/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:760fb7db442f284996e39cf9915a94492e1896baac44f06ae551974907922b64", size = 1761157, upload-time = "2025-07-29T05:51:45.643Z" },
+ { url = "https://files.pythonhosted.org/packages/05/6a/ea199e61b67f25ba688d3ce93f63b49b0a4e3b3d380f03971b4646412fc6/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad702e57dc385cae679c39d318def49aef754455f237499d5b99bea4ef582e51", size = 1710050, upload-time = "2025-07-29T05:51:48.203Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/2e/ffeb7f6256b33635c29dbed29a22a723ff2dd7401fff42ea60cf2060abfb/aiohttp-3.12.15-cp313-cp313-win32.whl", hash = "sha256:f813c3e9032331024de2eb2e32a88d86afb69291fbc37a3a3ae81cc9917fb3d0", size = 422647, upload-time = "2025-07-29T05:51:50.718Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/8e/78ee35774201f38d5e1ba079c9958f7629b1fd079459aea9467441dbfbf5/aiohttp-3.12.15-cp313-cp313-win_amd64.whl", hash = "sha256:1a649001580bdb37c6fdb1bebbd7e3bc688e8ec2b5c6f52edbb664662b17dc84", size = 449067, upload-time = "2025-07-29T05:51:52.549Z" },
+]
+
+[[package]]
+name = "aiosignal"
+version = "1.4.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "frozenlist" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" },
+]
+
[[package]]
name = "annotated-types"
version = "0.7.0"
@@ -24,6 +79,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213, upload-time = "2025-08-04T08:54:24.882Z" },
]
+[[package]]
+name = "attrs"
+version = "25.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" },
+]
+
[[package]]
name = "boto3"
version = "1.40.30"
@@ -219,6 +283,49 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e5/47/d63c60f59a59467fda0f93f46335c9d18526d7071f025cb5b89d5353ea42/fastapi-0.116.1-py3-none-any.whl", hash = "sha256:c46ac7c312df840f0c9e220f7964bada936781bc4e2e6eb71f1c4d7553786565", size = 95631, upload-time = "2025-07-11T16:22:30.485Z" },
]
+[[package]]
+name = "frozenlist"
+version = "1.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/79/b1/b64018016eeb087db503b038296fd782586432b9c077fc5c7839e9cb6ef6/frozenlist-1.7.0.tar.gz", hash = "sha256:2e310d81923c2437ea8670467121cc3e9b0f76d3043cc1d2331d56c7fb7a3a8f", size = 45078, upload-time = "2025-06-09T23:02:35.538Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/24/90/6b2cebdabdbd50367273c20ff6b57a3dfa89bd0762de02c3a1eb42cb6462/frozenlist-1.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee80eeda5e2a4e660651370ebffd1286542b67e268aa1ac8d6dbe973120ef7ee", size = 79791, upload-time = "2025-06-09T23:01:09.368Z" },
+ { url = "https://files.pythonhosted.org/packages/83/2e/5b70b6a3325363293fe5fc3ae74cdcbc3e996c2a11dde2fd9f1fb0776d19/frozenlist-1.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d1a81c85417b914139e3a9b995d4a1c84559afc839a93cf2cb7f15e6e5f6ed2d", size = 47165, upload-time = "2025-06-09T23:01:10.653Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/25/a0895c99270ca6966110f4ad98e87e5662eab416a17e7fd53c364bf8b954/frozenlist-1.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cbb65198a9132ebc334f237d7b0df163e4de83fb4f2bdfe46c1e654bdb0c5d43", size = 45881, upload-time = "2025-06-09T23:01:12.296Z" },
+ { url = "https://files.pythonhosted.org/packages/19/7c/71bb0bbe0832793c601fff68cd0cf6143753d0c667f9aec93d3c323f4b55/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dab46c723eeb2c255a64f9dc05b8dd601fde66d6b19cdb82b2e09cc6ff8d8b5d", size = 232409, upload-time = "2025-06-09T23:01:13.641Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/45/ed2798718910fe6eb3ba574082aaceff4528e6323f9a8570be0f7028d8e9/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6aeac207a759d0dedd2e40745575ae32ab30926ff4fa49b1635def65806fddee", size = 225132, upload-time = "2025-06-09T23:01:15.264Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/e2/8417ae0f8eacb1d071d4950f32f229aa6bf68ab69aab797b72a07ea68d4f/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bd8c4e58ad14b4fa7802b8be49d47993182fdd4023393899632c88fd8cd994eb", size = 237638, upload-time = "2025-06-09T23:01:16.752Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/b7/2ace5450ce85f2af05a871b8c8719b341294775a0a6c5585d5e6170f2ce7/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04fb24d104f425da3540ed83cbfc31388a586a7696142004c577fa61c6298c3f", size = 233539, upload-time = "2025-06-09T23:01:18.202Z" },
+ { url = "https://files.pythonhosted.org/packages/46/b9/6989292c5539553dba63f3c83dc4598186ab2888f67c0dc1d917e6887db6/frozenlist-1.7.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a5c505156368e4ea6b53b5ac23c92d7edc864537ff911d2fb24c140bb175e60", size = 215646, upload-time = "2025-06-09T23:01:19.649Z" },
+ { url = "https://files.pythonhosted.org/packages/72/31/bc8c5c99c7818293458fe745dab4fd5730ff49697ccc82b554eb69f16a24/frozenlist-1.7.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bd7eb96a675f18aa5c553eb7ddc24a43c8c18f22e1f9925528128c052cdbe00", size = 232233, upload-time = "2025-06-09T23:01:21.175Z" },
+ { url = "https://files.pythonhosted.org/packages/59/52/460db4d7ba0811b9ccb85af996019f5d70831f2f5f255f7cc61f86199795/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:05579bf020096fe05a764f1f84cd104a12f78eaab68842d036772dc6d4870b4b", size = 227996, upload-time = "2025-06-09T23:01:23.098Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/c9/f4b39e904c03927b7ecf891804fd3b4df3db29b9e487c6418e37988d6e9d/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:376b6222d114e97eeec13d46c486facd41d4f43bab626b7c3f6a8b4e81a5192c", size = 242280, upload-time = "2025-06-09T23:01:24.808Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/33/3f8d6ced42f162d743e3517781566b8481322be321b486d9d262adf70bfb/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0aa7e176ebe115379b5b1c95b4096fb1c17cce0847402e227e712c27bdb5a949", size = 217717, upload-time = "2025-06-09T23:01:26.28Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/e8/ad683e75da6ccef50d0ab0c2b2324b32f84fc88ceee778ed79b8e2d2fe2e/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3fbba20e662b9c2130dc771e332a99eff5da078b2b2648153a40669a6d0e36ca", size = 236644, upload-time = "2025-06-09T23:01:27.887Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/14/8d19ccdd3799310722195a72ac94ddc677541fb4bef4091d8e7775752360/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f3f4410a0a601d349dd406b5713fec59b4cee7e71678d5b17edda7f4655a940b", size = 238879, upload-time = "2025-06-09T23:01:29.524Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/13/c12bf657494c2fd1079a48b2db49fa4196325909249a52d8f09bc9123fd7/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e2cdfaaec6a2f9327bf43c933c0319a7c429058e8537c508964a133dffee412e", size = 232502, upload-time = "2025-06-09T23:01:31.287Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/8b/e7f9dfde869825489382bc0d512c15e96d3964180c9499efcec72e85db7e/frozenlist-1.7.0-cp313-cp313-win32.whl", hash = "sha256:5fc4df05a6591c7768459caba1b342d9ec23fa16195e744939ba5914596ae3e1", size = 39169, upload-time = "2025-06-09T23:01:35.503Z" },
+ { url = "https://files.pythonhosted.org/packages/35/89/a487a98d94205d85745080a37860ff5744b9820a2c9acbcdd9440bfddf98/frozenlist-1.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:52109052b9791a3e6b5d1b65f4b909703984b770694d3eb64fad124c835d7cba", size = 43219, upload-time = "2025-06-09T23:01:36.784Z" },
+ { url = "https://files.pythonhosted.org/packages/56/d5/5c4cf2319a49eddd9dd7145e66c4866bdc6f3dbc67ca3d59685149c11e0d/frozenlist-1.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a6f86e4193bb0e235ef6ce3dde5cbabed887e0b11f516ce8a0f4d3b33078ec2d", size = 84345, upload-time = "2025-06-09T23:01:38.295Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/7d/ec2c1e1dc16b85bc9d526009961953df9cec8481b6886debb36ec9107799/frozenlist-1.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:82d664628865abeb32d90ae497fb93df398a69bb3434463d172b80fc25b0dd7d", size = 48880, upload-time = "2025-06-09T23:01:39.887Z" },
+ { url = "https://files.pythonhosted.org/packages/69/86/f9596807b03de126e11e7d42ac91e3d0b19a6599c714a1989a4e85eeefc4/frozenlist-1.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:912a7e8375a1c9a68325a902f3953191b7b292aa3c3fb0d71a216221deca460b", size = 48498, upload-time = "2025-06-09T23:01:41.318Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/cb/df6de220f5036001005f2d726b789b2c0b65f2363b104bbc16f5be8084f8/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9537c2777167488d539bc5de2ad262efc44388230e5118868e172dd4a552b146", size = 292296, upload-time = "2025-06-09T23:01:42.685Z" },
+ { url = "https://files.pythonhosted.org/packages/83/1f/de84c642f17c8f851a2905cee2dae401e5e0daca9b5ef121e120e19aa825/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f34560fb1b4c3e30ba35fa9a13894ba39e5acfc5f60f57d8accde65f46cc5e74", size = 273103, upload-time = "2025-06-09T23:01:44.166Z" },
+ { url = "https://files.pythonhosted.org/packages/88/3c/c840bfa474ba3fa13c772b93070893c6e9d5c0350885760376cbe3b6c1b3/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:acd03d224b0175f5a850edc104ac19040d35419eddad04e7cf2d5986d98427f1", size = 292869, upload-time = "2025-06-09T23:01:45.681Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/1c/3efa6e7d5a39a1d5ef0abeb51c48fb657765794a46cf124e5aca2c7a592c/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2038310bc582f3d6a09b3816ab01737d60bf7b1ec70f5356b09e84fb7408ab1", size = 291467, upload-time = "2025-06-09T23:01:47.234Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/00/d5c5e09d4922c395e2f2f6b79b9a20dab4b67daaf78ab92e7729341f61f6/frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8c05e4c8e5f36e5e088caa1bf78a687528f83c043706640a92cb76cd6999384", size = 266028, upload-time = "2025-06-09T23:01:48.819Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/27/72765be905619dfde25a7f33813ac0341eb6b076abede17a2e3fbfade0cb/frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:765bb588c86e47d0b68f23c1bee323d4b703218037765dcf3f25c838c6fecceb", size = 284294, upload-time = "2025-06-09T23:01:50.394Z" },
+ { url = "https://files.pythonhosted.org/packages/88/67/c94103a23001b17808eb7dd1200c156bb69fb68e63fcf0693dde4cd6228c/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:32dc2e08c67d86d0969714dd484fd60ff08ff81d1a1e40a77dd34a387e6ebc0c", size = 281898, upload-time = "2025-06-09T23:01:52.234Z" },
+ { url = "https://files.pythonhosted.org/packages/42/34/a3e2c00c00f9e2a9db5653bca3fec306349e71aff14ae45ecc6d0951dd24/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:c0303e597eb5a5321b4de9c68e9845ac8f290d2ab3f3e2c864437d3c5a30cd65", size = 290465, upload-time = "2025-06-09T23:01:53.788Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/73/f89b7fbce8b0b0c095d82b008afd0590f71ccb3dee6eee41791cf8cd25fd/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:a47f2abb4e29b3a8d0b530f7c3598badc6b134562b1a5caee867f7c62fee51e3", size = 266385, upload-time = "2025-06-09T23:01:55.769Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/45/e365fdb554159462ca12df54bc59bfa7a9a273ecc21e99e72e597564d1ae/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:3d688126c242a6fabbd92e02633414d40f50bb6002fa4cf995a1d18051525657", size = 288771, upload-time = "2025-06-09T23:01:57.4Z" },
+ { url = "https://files.pythonhosted.org/packages/00/11/47b6117002a0e904f004d70ec5194fe9144f117c33c851e3d51c765962d0/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:4e7e9652b3d367c7bd449a727dc79d5043f48b88d0cbfd4f9f1060cf2b414104", size = 288206, upload-time = "2025-06-09T23:01:58.936Z" },
+ { url = "https://files.pythonhosted.org/packages/40/37/5f9f3c3fd7f7746082ec67bcdc204db72dad081f4f83a503d33220a92973/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1a85e345b4c43db8b842cab1feb41be5cc0b10a1830e6295b69d7310f99becaf", size = 282620, upload-time = "2025-06-09T23:02:00.493Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/31/8fbc5af2d183bff20f21aa743b4088eac4445d2bb1cdece449ae80e4e2d1/frozenlist-1.7.0-cp313-cp313t-win32.whl", hash = "sha256:3a14027124ddb70dfcee5148979998066897e79f89f64b13328595c4bdf77c81", size = 43059, upload-time = "2025-06-09T23:02:02.072Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/ed/41956f52105b8dbc26e457c5705340c67c8cc2b79f394b79bffc09d0e938/frozenlist-1.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3bf8010d71d4507775f658e9823210b7427be36625b387221642725b515dcf3e", size = 47516, upload-time = "2025-06-09T23:02:03.779Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/45/b82e3c16be2182bff01179db177fe144d58b5dc787a7d4492c6ed8b9317f/frozenlist-1.7.0-py3-none-any.whl", hash = "sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e", size = 13106, upload-time = "2025-06-09T23:02:34.204Z" },
+]
+
[[package]]
name = "h11"
version = "0.16.0"
@@ -228,6 +335,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
]
+[[package]]
+name = "http-ece"
+version = "1.2.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cryptography" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/7c/af/249d1576653b69c20b9ac30e284b63bd94af6a175d72d87813235caf2482/http_ece-1.2.1.tar.gz", hash = "sha256:8c6ab23116bbf6affda894acfd5f2ca0fb8facbcbb72121c11c75c33e7ce8cff", size = 8830, upload-time = "2024-08-08T00:10:47.301Z" }
+
[[package]]
name = "idna"
version = "3.10"
@@ -246,6 +362,104 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256, upload-time = "2022-06-17T18:00:10.251Z" },
]
+[[package]]
+name = "multidict"
+version = "6.6.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/69/7f/0652e6ed47ab288e3756ea9c0df8b14950781184d4bd7883f4d87dd41245/multidict-6.6.4.tar.gz", hash = "sha256:d2d4e4787672911b48350df02ed3fa3fffdc2f2e8ca06dd6afdf34189b76a9dd", size = 101843, upload-time = "2025-08-11T12:08:48.217Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3a/5d/e1db626f64f60008320aab00fbe4f23fc3300d75892a3381275b3d284580/multidict-6.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f46a6e8597f9bd71b31cc708195d42b634c8527fecbcf93febf1052cacc1f16e", size = 75848, upload-time = "2025-08-11T12:07:19.912Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/aa/8b6f548d839b6c13887253af4e29c939af22a18591bfb5d0ee6f1931dae8/multidict-6.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:22e38b2bc176c5eb9c0a0e379f9d188ae4cd8b28c0f53b52bce7ab0a9e534657", size = 45060, upload-time = "2025-08-11T12:07:21.163Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/c6/f5e97e5d99a729bc2aa58eb3ebfa9f1e56a9b517cc38c60537c81834a73f/multidict-6.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5df8afd26f162da59e218ac0eefaa01b01b2e6cd606cffa46608f699539246da", size = 43269, upload-time = "2025-08-11T12:07:22.392Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/31/d54eb0c62516776f36fe67f84a732f97e0b0e12f98d5685bebcc6d396910/multidict-6.6.4-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:49517449b58d043023720aa58e62b2f74ce9b28f740a0b5d33971149553d72aa", size = 237158, upload-time = "2025-08-11T12:07:23.636Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/1c/8a10c1c25b23156e63b12165a929d8eb49a6ed769fdbefb06e6f07c1e50d/multidict-6.6.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae9408439537c5afdca05edd128a63f56a62680f4b3c234301055d7a2000220f", size = 257076, upload-time = "2025-08-11T12:07:25.049Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/86/90e20b5771d6805a119e483fd3d1e8393e745a11511aebca41f0da38c3e2/multidict-6.6.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:87a32d20759dc52a9e850fe1061b6e41ab28e2998d44168a8a341b99ded1dba0", size = 240694, upload-time = "2025-08-11T12:07:26.458Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/49/484d3e6b535bc0555b52a0a26ba86e4d8d03fd5587d4936dc59ba7583221/multidict-6.6.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:52e3c8d43cdfff587ceedce9deb25e6ae77daba560b626e97a56ddcad3756879", size = 266350, upload-time = "2025-08-11T12:07:27.94Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/b4/aa4c5c379b11895083d50021e229e90c408d7d875471cb3abf721e4670d6/multidict-6.6.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ad8850921d3a8d8ff6fbef790e773cecfc260bbfa0566998980d3fa8f520bc4a", size = 267250, upload-time = "2025-08-11T12:07:29.303Z" },
+ { url = "https://files.pythonhosted.org/packages/80/e5/5e22c5bf96a64bdd43518b1834c6d95a4922cc2066b7d8e467dae9b6cee6/multidict-6.6.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:497a2954adc25c08daff36f795077f63ad33e13f19bfff7736e72c785391534f", size = 254900, upload-time = "2025-08-11T12:07:30.764Z" },
+ { url = "https://files.pythonhosted.org/packages/17/38/58b27fed927c07035abc02befacab42491e7388ca105e087e6e0215ead64/multidict-6.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:024ce601f92d780ca1617ad4be5ac15b501cc2414970ffa2bb2bbc2bd5a68fa5", size = 252355, upload-time = "2025-08-11T12:07:32.205Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/a1/dad75d23a90c29c02b5d6f3d7c10ab36c3197613be5d07ec49c7791e186c/multidict-6.6.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a693fc5ed9bdd1c9e898013e0da4dcc640de7963a371c0bd458e50e046bf6438", size = 250061, upload-time = "2025-08-11T12:07:33.623Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/1a/ac2216b61c7f116edab6dc3378cca6c70dc019c9a457ff0d754067c58b20/multidict-6.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:190766dac95aab54cae5b152a56520fd99298f32a1266d66d27fdd1b5ac00f4e", size = 249675, upload-time = "2025-08-11T12:07:34.958Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/79/1916af833b800d13883e452e8e0977c065c4ee3ab7a26941fbfdebc11895/multidict-6.6.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:34d8f2a5ffdceab9dcd97c7a016deb2308531d5f0fced2bb0c9e1df45b3363d7", size = 261247, upload-time = "2025-08-11T12:07:36.588Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/65/d1f84fe08ac44a5fc7391cbc20a7cedc433ea616b266284413fd86062f8c/multidict-6.6.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:59e8d40ab1f5a8597abcef00d04845155a5693b5da00d2c93dbe88f2050f2812", size = 257960, upload-time = "2025-08-11T12:07:39.735Z" },
+ { url = "https://files.pythonhosted.org/packages/13/b5/29ec78057d377b195ac2c5248c773703a6b602e132a763e20ec0457e7440/multidict-6.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:467fe64138cfac771f0e949b938c2e1ada2b5af22f39692aa9258715e9ea613a", size = 250078, upload-time = "2025-08-11T12:07:41.525Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/0e/7e79d38f70a872cae32e29b0d77024bef7834b0afb406ddae6558d9e2414/multidict-6.6.4-cp313-cp313-win32.whl", hash = "sha256:14616a30fe6d0a48d0a48d1a633ab3b8bec4cf293aac65f32ed116f620adfd69", size = 41708, upload-time = "2025-08-11T12:07:43.405Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/34/746696dffff742e97cd6a23da953e55d0ea51fa601fa2ff387b3edcfaa2c/multidict-6.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:40cd05eaeb39e2bc8939451f033e57feaa2ac99e07dbca8afe2be450a4a3b6cf", size = 45912, upload-time = "2025-08-11T12:07:45.082Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/87/3bac136181e271e29170d8d71929cdeddeb77f3e8b6a0c08da3a8e9da114/multidict-6.6.4-cp313-cp313-win_arm64.whl", hash = "sha256:f6eb37d511bfae9e13e82cb4d1af36b91150466f24d9b2b8a9785816deb16605", size = 43076, upload-time = "2025-08-11T12:07:46.746Z" },
+ { url = "https://files.pythonhosted.org/packages/64/94/0a8e63e36c049b571c9ae41ee301ada29c3fee9643d9c2548d7d558a1d99/multidict-6.6.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:6c84378acd4f37d1b507dfa0d459b449e2321b3ba5f2338f9b085cf7a7ba95eb", size = 82812, upload-time = "2025-08-11T12:07:48.402Z" },
+ { url = "https://files.pythonhosted.org/packages/25/1a/be8e369dfcd260d2070a67e65dd3990dd635cbd735b98da31e00ea84cd4e/multidict-6.6.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0e0558693063c75f3d952abf645c78f3c5dfdd825a41d8c4d8156fc0b0da6e7e", size = 48313, upload-time = "2025-08-11T12:07:49.679Z" },
+ { url = "https://files.pythonhosted.org/packages/26/5a/dd4ade298674b2f9a7b06a32c94ffbc0497354df8285f27317c66433ce3b/multidict-6.6.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3f8e2384cb83ebd23fd07e9eada8ba64afc4c759cd94817433ab8c81ee4b403f", size = 46777, upload-time = "2025-08-11T12:07:51.318Z" },
+ { url = "https://files.pythonhosted.org/packages/89/db/98aa28bc7e071bfba611ac2ae803c24e96dd3a452b4118c587d3d872c64c/multidict-6.6.4-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:f996b87b420995a9174b2a7c1a8daf7db4750be6848b03eb5e639674f7963773", size = 229321, upload-time = "2025-08-11T12:07:52.965Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/bc/01ddda2a73dd9d167bd85d0e8ef4293836a8f82b786c63fb1a429bc3e678/multidict-6.6.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc356250cffd6e78416cf5b40dc6a74f1edf3be8e834cf8862d9ed5265cf9b0e", size = 249954, upload-time = "2025-08-11T12:07:54.423Z" },
+ { url = "https://files.pythonhosted.org/packages/06/78/6b7c0f020f9aa0acf66d0ab4eb9f08375bac9a50ff5e3edb1c4ccd59eafc/multidict-6.6.4-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:dadf95aa862714ea468a49ad1e09fe00fcc9ec67d122f6596a8d40caf6cec7d0", size = 228612, upload-time = "2025-08-11T12:07:55.914Z" },
+ { url = "https://files.pythonhosted.org/packages/00/44/3faa416f89b2d5d76e9d447296a81521e1c832ad6e40b92f990697b43192/multidict-6.6.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7dd57515bebffd8ebd714d101d4c434063322e4fe24042e90ced41f18b6d3395", size = 257528, upload-time = "2025-08-11T12:07:57.371Z" },
+ { url = "https://files.pythonhosted.org/packages/05/5f/77c03b89af0fcb16f018f668207768191fb9dcfb5e3361a5e706a11db2c9/multidict-6.6.4-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:967af5f238ebc2eb1da4e77af5492219fbd9b4b812347da39a7b5f5c72c0fa45", size = 256329, upload-time = "2025-08-11T12:07:58.844Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/e9/ed750a2a9afb4f8dc6f13dc5b67b514832101b95714f1211cd42e0aafc26/multidict-6.6.4-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a4c6875c37aae9794308ec43e3530e4aa0d36579ce38d89979bbf89582002bb", size = 247928, upload-time = "2025-08-11T12:08:01.037Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/b5/e0571bc13cda277db7e6e8a532791d4403dacc9850006cb66d2556e649c0/multidict-6.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7f683a551e92bdb7fac545b9c6f9fa2aebdeefa61d607510b3533286fcab67f5", size = 245228, upload-time = "2025-08-11T12:08:02.96Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/a3/69a84b0eccb9824491f06368f5b86e72e4af54c3067c37c39099b6687109/multidict-6.6.4-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:3ba5aaf600edaf2a868a391779f7a85d93bed147854925f34edd24cc70a3e141", size = 235869, upload-time = "2025-08-11T12:08:04.746Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/9d/28802e8f9121a6a0804fa009debf4e753d0a59969ea9f70be5f5fdfcb18f/multidict-6.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:580b643b7fd2c295d83cad90d78419081f53fd532d1f1eb67ceb7060f61cff0d", size = 243446, upload-time = "2025-08-11T12:08:06.332Z" },
+ { url = "https://files.pythonhosted.org/packages/38/ea/6c98add069b4878c1d66428a5f5149ddb6d32b1f9836a826ac764b9940be/multidict-6.6.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:37b7187197da6af3ee0b044dbc9625afd0c885f2800815b228a0e70f9a7f473d", size = 252299, upload-time = "2025-08-11T12:08:07.931Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/09/8fe02d204473e14c0af3affd50af9078839dfca1742f025cca765435d6b4/multidict-6.6.4-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e1b93790ed0bc26feb72e2f08299691ceb6da5e9e14a0d13cc74f1869af327a0", size = 246926, upload-time = "2025-08-11T12:08:09.467Z" },
+ { url = "https://files.pythonhosted.org/packages/37/3d/7b1e10d774a6df5175ecd3c92bff069e77bed9ec2a927fdd4ff5fe182f67/multidict-6.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a506a77ddee1efcca81ecbeae27ade3e09cdf21a8ae854d766c2bb4f14053f92", size = 243383, upload-time = "2025-08-11T12:08:10.981Z" },
+ { url = "https://files.pythonhosted.org/packages/50/b0/a6fae46071b645ae98786ab738447de1ef53742eaad949f27e960864bb49/multidict-6.6.4-cp313-cp313t-win32.whl", hash = "sha256:f93b2b2279883d1d0a9e1bd01f312d6fc315c5e4c1f09e112e4736e2f650bc4e", size = 47775, upload-time = "2025-08-11T12:08:12.439Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/0a/2436550b1520091af0600dff547913cb2d66fbac27a8c33bc1b1bccd8d98/multidict-6.6.4-cp313-cp313t-win_amd64.whl", hash = "sha256:6d46a180acdf6e87cc41dc15d8f5c2986e1e8739dc25dbb7dac826731ef381a4", size = 53100, upload-time = "2025-08-11T12:08:13.823Z" },
+ { url = "https://files.pythonhosted.org/packages/97/ea/43ac51faff934086db9c072a94d327d71b7d8b40cd5dcb47311330929ef0/multidict-6.6.4-cp313-cp313t-win_arm64.whl", hash = "sha256:756989334015e3335d087a27331659820d53ba432befdef6a718398b0a8493ad", size = 45501, upload-time = "2025-08-11T12:08:15.173Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/69/b547032297c7e63ba2af494edba695d781af8a0c6e89e4d06cf848b21d80/multidict-6.6.4-py3-none-any.whl", hash = "sha256:27d8f8e125c07cb954e54d75d04905a9bba8a439c1d84aca94949d4d03d8601c", size = 12313, upload-time = "2025-08-11T12:08:46.891Z" },
+]
+
+[[package]]
+name = "propcache"
+version = "0.3.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a6/16/43264e4a779dd8588c21a70f0709665ee8f611211bdd2c87d952cfa7c776/propcache-0.3.2.tar.gz", hash = "sha256:20d7d62e4e7ef05f221e0db2856b979540686342e7dd9973b815599c7057e168", size = 44139, upload-time = "2025-06-09T22:56:06.081Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/dc/d1/8c747fafa558c603c4ca19d8e20b288aa0c7cda74e9402f50f31eb65267e/propcache-0.3.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ca592ed634a73ca002967458187109265e980422116c0a107cf93d81f95af945", size = 71286, upload-time = "2025-06-09T22:54:54.369Z" },
+ { url = "https://files.pythonhosted.org/packages/61/99/d606cb7986b60d89c36de8a85d58764323b3a5ff07770a99d8e993b3fa73/propcache-0.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9ecb0aad4020e275652ba3975740f241bd12a61f1a784df044cf7477a02bc252", size = 42425, upload-time = "2025-06-09T22:54:55.642Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/96/ef98f91bbb42b79e9bb82bdd348b255eb9d65f14dbbe3b1594644c4073f7/propcache-0.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7f08f1cc28bd2eade7a8a3d2954ccc673bb02062e3e7da09bc75d843386b342f", size = 41846, upload-time = "2025-06-09T22:54:57.246Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/ad/3f0f9a705fb630d175146cd7b1d2bf5555c9beaed54e94132b21aac098a6/propcache-0.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1a342c834734edb4be5ecb1e9fb48cb64b1e2320fccbd8c54bf8da8f2a84c33", size = 208871, upload-time = "2025-06-09T22:54:58.975Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/38/2085cda93d2c8b6ec3e92af2c89489a36a5886b712a34ab25de9fbca7992/propcache-0.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a544caaae1ac73f1fecfae70ded3e93728831affebd017d53449e3ac052ac1e", size = 215720, upload-time = "2025-06-09T22:55:00.471Z" },
+ { url = "https://files.pythonhosted.org/packages/61/c1/d72ea2dc83ac7f2c8e182786ab0fc2c7bd123a1ff9b7975bee671866fe5f/propcache-0.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:310d11aa44635298397db47a3ebce7db99a4cc4b9bbdfcf6c98a60c8d5261cf1", size = 215203, upload-time = "2025-06-09T22:55:01.834Z" },
+ { url = "https://files.pythonhosted.org/packages/af/81/b324c44ae60c56ef12007105f1460d5c304b0626ab0cc6b07c8f2a9aa0b8/propcache-0.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c1396592321ac83157ac03a2023aa6cc4a3cc3cfdecb71090054c09e5a7cce3", size = 206365, upload-time = "2025-06-09T22:55:03.199Z" },
+ { url = "https://files.pythonhosted.org/packages/09/73/88549128bb89e66d2aff242488f62869014ae092db63ccea53c1cc75a81d/propcache-0.3.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cabf5b5902272565e78197edb682017d21cf3b550ba0460ee473753f28d23c1", size = 196016, upload-time = "2025-06-09T22:55:04.518Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/3f/3bdd14e737d145114a5eb83cb172903afba7242f67c5877f9909a20d948d/propcache-0.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0a2f2235ac46a7aa25bdeb03a9e7060f6ecbd213b1f9101c43b3090ffb971ef6", size = 205596, upload-time = "2025-06-09T22:55:05.942Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/ca/2f4aa819c357d3107c3763d7ef42c03980f9ed5c48c82e01e25945d437c1/propcache-0.3.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:92b69e12e34869a6970fd2f3da91669899994b47c98f5d430b781c26f1d9f387", size = 200977, upload-time = "2025-06-09T22:55:07.792Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/4a/e65276c7477533c59085251ae88505caf6831c0e85ff8b2e31ebcbb949b1/propcache-0.3.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:54e02207c79968ebbdffc169591009f4474dde3b4679e16634d34c9363ff56b4", size = 197220, upload-time = "2025-06-09T22:55:09.173Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/54/fc7152e517cf5578278b242396ce4d4b36795423988ef39bb8cd5bf274c8/propcache-0.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4adfb44cb588001f68c5466579d3f1157ca07f7504fc91ec87862e2b8e556b88", size = 210642, upload-time = "2025-06-09T22:55:10.62Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/80/abeb4a896d2767bf5f1ea7b92eb7be6a5330645bd7fb844049c0e4045d9d/propcache-0.3.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fd3e6019dc1261cd0291ee8919dd91fbab7b169bb76aeef6c716833a3f65d206", size = 212789, upload-time = "2025-06-09T22:55:12.029Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/db/ea12a49aa7b2b6d68a5da8293dcf50068d48d088100ac016ad92a6a780e6/propcache-0.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4c181cad81158d71c41a2bce88edce078458e2dd5ffee7eddd6b05da85079f43", size = 205880, upload-time = "2025-06-09T22:55:13.45Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/e5/9076a0bbbfb65d1198007059c65639dfd56266cf8e477a9707e4b1999ff4/propcache-0.3.2-cp313-cp313-win32.whl", hash = "sha256:8a08154613f2249519e549de2330cf8e2071c2887309a7b07fb56098f5170a02", size = 37220, upload-time = "2025-06-09T22:55:15.284Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/f5/b369e026b09a26cd77aa88d8fffd69141d2ae00a2abaaf5380d2603f4b7f/propcache-0.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e41671f1594fc4ab0a6dec1351864713cb3a279910ae8b58f884a88a0a632c05", size = 40678, upload-time = "2025-06-09T22:55:16.445Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/3a/6ece377b55544941a08d03581c7bc400a3c8cd3c2865900a68d5de79e21f/propcache-0.3.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:9a3cf035bbaf035f109987d9d55dc90e4b0e36e04bbbb95af3055ef17194057b", size = 76560, upload-time = "2025-06-09T22:55:17.598Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/da/64a2bb16418740fa634b0e9c3d29edff1db07f56d3546ca2d86ddf0305e1/propcache-0.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:156c03d07dc1323d8dacaa221fbe028c5c70d16709cdd63502778e6c3ccca1b0", size = 44676, upload-time = "2025-06-09T22:55:18.922Z" },
+ { url = "https://files.pythonhosted.org/packages/36/7b/f025e06ea51cb72c52fb87e9b395cced02786610b60a3ed51da8af017170/propcache-0.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74413c0ba02ba86f55cf60d18daab219f7e531620c15f1e23d95563f505efe7e", size = 44701, upload-time = "2025-06-09T22:55:20.106Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/00/faa1b1b7c3b74fc277f8642f32a4c72ba1d7b2de36d7cdfb676db7f4303e/propcache-0.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f066b437bb3fa39c58ff97ab2ca351db465157d68ed0440abecb21715eb24b28", size = 276934, upload-time = "2025-06-09T22:55:21.5Z" },
+ { url = "https://files.pythonhosted.org/packages/74/ab/935beb6f1756e0476a4d5938ff44bf0d13a055fed880caf93859b4f1baf4/propcache-0.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1304b085c83067914721e7e9d9917d41ad87696bf70f0bc7dee450e9c71ad0a", size = 278316, upload-time = "2025-06-09T22:55:22.918Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/9d/994a5c1ce4389610838d1caec74bdf0e98b306c70314d46dbe4fcf21a3e2/propcache-0.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab50cef01b372763a13333b4e54021bdcb291fc9a8e2ccb9c2df98be51bcde6c", size = 282619, upload-time = "2025-06-09T22:55:24.651Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/00/a10afce3d1ed0287cef2e09506d3be9822513f2c1e96457ee369adb9a6cd/propcache-0.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fad3b2a085ec259ad2c2842666b2a0a49dea8463579c606426128925af1ed725", size = 265896, upload-time = "2025-06-09T22:55:26.049Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/a8/2aa6716ffa566ca57c749edb909ad27884680887d68517e4be41b02299f3/propcache-0.3.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:261fa020c1c14deafd54c76b014956e2f86991af198c51139faf41c4d5e83892", size = 252111, upload-time = "2025-06-09T22:55:27.381Z" },
+ { url = "https://files.pythonhosted.org/packages/36/4f/345ca9183b85ac29c8694b0941f7484bf419c7f0fea2d1e386b4f7893eed/propcache-0.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:46d7f8aa79c927e5f987ee3a80205c987717d3659f035c85cf0c3680526bdb44", size = 268334, upload-time = "2025-06-09T22:55:28.747Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/ca/fcd54f78b59e3f97b3b9715501e3147f5340167733d27db423aa321e7148/propcache-0.3.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:6d8f3f0eebf73e3c0ff0e7853f68be638b4043c65a70517bb575eff54edd8dbe", size = 255026, upload-time = "2025-06-09T22:55:30.184Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/95/8e6a6bbbd78ac89c30c225210a5c687790e532ba4088afb8c0445b77ef37/propcache-0.3.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:03c89c1b14a5452cf15403e291c0ccd7751d5b9736ecb2c5bab977ad6c5bcd81", size = 250724, upload-time = "2025-06-09T22:55:31.646Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/b0/0dd03616142baba28e8b2d14ce5df6631b4673850a3d4f9c0f9dd714a404/propcache-0.3.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:0cc17efde71e12bbaad086d679ce575268d70bc123a5a71ea7ad76f70ba30bba", size = 268868, upload-time = "2025-06-09T22:55:33.209Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/98/2c12407a7e4fbacd94ddd32f3b1e3d5231e77c30ef7162b12a60e2dd5ce3/propcache-0.3.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:acdf05d00696bc0447e278bb53cb04ca72354e562cf88ea6f9107df8e7fd9770", size = 271322, upload-time = "2025-06-09T22:55:35.065Z" },
+ { url = "https://files.pythonhosted.org/packages/35/91/9cb56efbb428b006bb85db28591e40b7736847b8331d43fe335acf95f6c8/propcache-0.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4445542398bd0b5d32df908031cb1b30d43ac848e20470a878b770ec2dcc6330", size = 265778, upload-time = "2025-06-09T22:55:36.45Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/4c/b0fe775a2bdd01e176b14b574be679d84fc83958335790f7c9a686c1f468/propcache-0.3.2-cp313-cp313t-win32.whl", hash = "sha256:f86e5d7cd03afb3a1db8e9f9f6eff15794e79e791350ac48a8c924e6f439f394", size = 41175, upload-time = "2025-06-09T22:55:38.436Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/ff/47f08595e3d9b5e149c150f88d9714574f1a7cbd89fe2817158a952674bf/propcache-0.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9704bedf6e7cbe3c65eca4379a9b53ee6a83749f047808cbb5044d40d7d72198", size = 44857, upload-time = "2025-06-09T22:55:39.687Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/35/cc0aaecf278bb4575b8555f2b137de5ab821595ddae9da9d3cd1da4072c7/propcache-0.3.2-py3-none-any.whl", hash = "sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f", size = 12663, upload-time = "2025-06-09T22:56:04.484Z" },
+]
+
+[[package]]
+name = "py-vapid"
+version = "1.9.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cryptography" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ff/57/5c1c61f27ce01f939443cf3f6c279a295f7ec0327b18a1cbbcfefe0b5456/py_vapid-1.9.2.tar.gz", hash = "sha256:3c8973b6cf8384ad0c9ae64d6270ccc480e0b92c702d8f5ea2cc03e6b51247f9", size = 20300, upload-time = "2024-11-19T21:55:41.859Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/73/fb/b877a221b09dabcebeb073d5e7f19244f3fa1d5aec87092c359a6049a006/py_vapid-1.9.2-py3-none-any.whl", hash = "sha256:4ccf8a00fc54f1f99f66fb543c96f2c82622508ad814b6e9225f2c26948934d7", size = 21492, upload-time = "2024-11-19T21:55:40.832Z" },
+]
+
[[package]]
name = "pyasn1"
version = "0.6.1"
@@ -370,6 +584,23 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" },
]
+[[package]]
+name = "pywebpush"
+version = "2.0.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "aiohttp" },
+ { name = "cryptography" },
+ { name = "http-ece" },
+ { name = "py-vapid" },
+ { name = "requests" },
+ { name = "six" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/41/ca/6e669bf676916d66c8c7adedc291e9a9758650f9d85ec040fda13e3c82f4/pywebpush-2.0.3.tar.gz", hash = "sha256:584878e3c243e873a22db8895505d95715bc796ef74cc1b8fe99f596174161e3", size = 25874, upload-time = "2024-11-19T21:30:20.444Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/90/8a/ecaa2a589338a038b89148b01a5db2dac53e45918342da69baca1fb058fc/pywebpush-2.0.3-py3-none-any.whl", hash = "sha256:04666441715bc547918d7668b2ac7ad5c4b5de7d0a6cf528daf61e0c4bc5431c", size = 21364, upload-time = "2024-11-19T21:30:19.312Z" },
+]
+
[[package]]
name = "requests"
version = "2.32.5"
@@ -420,6 +651,7 @@ dependencies = [
{ name = "python-dotenv" },
{ name = "python-jose", extra = ["cryptography"] },
{ name = "python-multipart" },
+ { name = "pywebpush" },
{ name = "requests" },
{ name = "uvicorn" },
]
@@ -432,6 +664,7 @@ requires-dist = [
{ name = "python-dotenv" },
{ name = "python-jose", extras = ["cryptography"] },
{ name = "python-multipart" },
+ { name = "pywebpush" },
{ name = "requests" },
{ name = "uvicorn" },
]
@@ -508,3 +741,51 @@ sdist = { url = "https://files.pythonhosted.org/packages/5e/42/e0e305207bb88c6b8
wheels = [
{ url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406, upload-time = "2025-06-28T16:15:44.816Z" },
]
+
+[[package]]
+name = "yarl"
+version = "1.20.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "idna" },
+ { name = "multidict" },
+ { name = "propcache" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/3c/fb/efaa23fa4e45537b827620f04cf8f3cd658b76642205162e072703a5b963/yarl-1.20.1.tar.gz", hash = "sha256:d017a4997ee50c91fd5466cef416231bb82177b93b029906cefc542ce14c35ac", size = 186428, upload-time = "2025-06-10T00:46:09.923Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8a/e1/2411b6d7f769a07687acee88a062af5833cf1966b7266f3d8dfb3d3dc7d3/yarl-1.20.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:0b5ff0fbb7c9f1b1b5ab53330acbfc5247893069e7716840c8e7d5bb7355038a", size = 131811, upload-time = "2025-06-10T00:44:18.933Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/27/584394e1cb76fb771371770eccad35de400e7b434ce3142c2dd27392c968/yarl-1.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:14f326acd845c2b2e2eb38fb1346c94f7f3b01a4f5c788f8144f9b630bfff9a3", size = 90078, upload-time = "2025-06-10T00:44:20.635Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/9a/3246ae92d4049099f52d9b0fe3486e3b500e29b7ea872d0f152966fc209d/yarl-1.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f60e4ad5db23f0b96e49c018596707c3ae89f5d0bd97f0ad3684bcbad899f1e7", size = 88748, upload-time = "2025-06-10T00:44:22.34Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/25/35afe384e31115a1a801fbcf84012d7a066d89035befae7c5d4284df1e03/yarl-1.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:49bdd1b8e00ce57e68ba51916e4bb04461746e794e7c4d4bbc42ba2f18297691", size = 349595, upload-time = "2025-06-10T00:44:24.314Z" },
+ { url = "https://files.pythonhosted.org/packages/28/2d/8aca6cb2cabc8f12efcb82749b9cefecbccfc7b0384e56cd71058ccee433/yarl-1.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:66252d780b45189975abfed839616e8fd2dbacbdc262105ad7742c6ae58f3e31", size = 342616, upload-time = "2025-06-10T00:44:26.167Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/e9/1312633d16b31acf0098d30440ca855e3492d66623dafb8e25b03d00c3da/yarl-1.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59174e7332f5d153d8f7452a102b103e2e74035ad085f404df2e40e663a22b28", size = 361324, upload-time = "2025-06-10T00:44:27.915Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/a0/688cc99463f12f7669eec7c8acc71ef56a1521b99eab7cd3abb75af887b0/yarl-1.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e3968ec7d92a0c0f9ac34d5ecfd03869ec0cab0697c91a45db3fbbd95fe1b653", size = 359676, upload-time = "2025-06-10T00:44:30.041Z" },
+ { url = "https://files.pythonhosted.org/packages/af/44/46407d7f7a56e9a85a4c207724c9f2c545c060380718eea9088f222ba697/yarl-1.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1a4fbb50e14396ba3d375f68bfe02215d8e7bc3ec49da8341fe3157f59d2ff5", size = 352614, upload-time = "2025-06-10T00:44:32.171Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/91/31163295e82b8d5485d31d9cf7754d973d41915cadce070491778d9c9825/yarl-1.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11a62c839c3a8eac2410e951301309426f368388ff2f33799052787035793b02", size = 336766, upload-time = "2025-06-10T00:44:34.494Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/8e/c41a5bc482121f51c083c4c2bcd16b9e01e1cf8729e380273a952513a21f/yarl-1.20.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:041eaa14f73ff5a8986b4388ac6bb43a77f2ea09bf1913df7a35d4646db69e53", size = 364615, upload-time = "2025-06-10T00:44:36.856Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/5b/61a3b054238d33d70ea06ebba7e58597891b71c699e247df35cc984ab393/yarl-1.20.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:377fae2fef158e8fd9d60b4c8751387b8d1fb121d3d0b8e9b0be07d1b41e83dc", size = 360982, upload-time = "2025-06-10T00:44:39.141Z" },
+ { url = "https://files.pythonhosted.org/packages/df/a3/6a72fb83f8d478cb201d14927bc8040af901811a88e0ff2da7842dd0ed19/yarl-1.20.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1c92f4390e407513f619d49319023664643d3339bd5e5a56a3bebe01bc67ec04", size = 369792, upload-time = "2025-06-10T00:44:40.934Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/af/4cc3c36dfc7c077f8dedb561eb21f69e1e9f2456b91b593882b0b18c19dc/yarl-1.20.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d25ddcf954df1754ab0f86bb696af765c5bfaba39b74095f27eececa049ef9a4", size = 382049, upload-time = "2025-06-10T00:44:42.854Z" },
+ { url = "https://files.pythonhosted.org/packages/19/3a/e54e2c4752160115183a66dc9ee75a153f81f3ab2ba4bf79c3c53b33de34/yarl-1.20.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:909313577e9619dcff8c31a0ea2aa0a2a828341d92673015456b3ae492e7317b", size = 384774, upload-time = "2025-06-10T00:44:45.275Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/20/200ae86dabfca89060ec6447649f219b4cbd94531e425e50d57e5f5ac330/yarl-1.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:793fd0580cb9664548c6b83c63b43c477212c0260891ddf86809e1c06c8b08f1", size = 374252, upload-time = "2025-06-10T00:44:47.31Z" },
+ { url = "https://files.pythonhosted.org/packages/83/75/11ee332f2f516b3d094e89448da73d557687f7d137d5a0f48c40ff211487/yarl-1.20.1-cp313-cp313-win32.whl", hash = "sha256:468f6e40285de5a5b3c44981ca3a319a4b208ccc07d526b20b12aeedcfa654b7", size = 81198, upload-time = "2025-06-10T00:44:49.164Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/ba/39b1ecbf51620b40ab402b0fc817f0ff750f6d92712b44689c2c215be89d/yarl-1.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:495b4ef2fea40596bfc0affe3837411d6aa3371abcf31aac0ccc4bdd64d4ef5c", size = 86346, upload-time = "2025-06-10T00:44:51.182Z" },
+ { url = "https://files.pythonhosted.org/packages/43/c7/669c52519dca4c95153c8ad96dd123c79f354a376346b198f438e56ffeb4/yarl-1.20.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f60233b98423aab21d249a30eb27c389c14929f47be8430efa7dbd91493a729d", size = 138826, upload-time = "2025-06-10T00:44:52.883Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/42/fc0053719b44f6ad04a75d7f05e0e9674d45ef62f2d9ad2c1163e5c05827/yarl-1.20.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6f3eff4cc3f03d650d8755c6eefc844edde99d641d0dcf4da3ab27141a5f8ddf", size = 93217, upload-time = "2025-06-10T00:44:54.658Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/7f/fa59c4c27e2a076bba0d959386e26eba77eb52ea4a0aac48e3515c186b4c/yarl-1.20.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:69ff8439d8ba832d6bed88af2c2b3445977eba9a4588b787b32945871c2444e3", size = 92700, upload-time = "2025-06-10T00:44:56.784Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/d4/062b2f48e7c93481e88eff97a6312dca15ea200e959f23e96d8ab898c5b8/yarl-1.20.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cf34efa60eb81dd2645a2e13e00bb98b76c35ab5061a3989c7a70f78c85006d", size = 347644, upload-time = "2025-06-10T00:44:59.071Z" },
+ { url = "https://files.pythonhosted.org/packages/89/47/78b7f40d13c8f62b499cc702fdf69e090455518ae544c00a3bf4afc9fc77/yarl-1.20.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8e0fe9364ad0fddab2688ce72cb7a8e61ea42eff3c7caeeb83874a5d479c896c", size = 323452, upload-time = "2025-06-10T00:45:01.605Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/2b/490d3b2dc66f52987d4ee0d3090a147ea67732ce6b4d61e362c1846d0d32/yarl-1.20.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f64fbf81878ba914562c672024089e3401974a39767747691c65080a67b18c1", size = 346378, upload-time = "2025-06-10T00:45:03.946Z" },
+ { url = "https://files.pythonhosted.org/packages/66/ad/775da9c8a94ce925d1537f939a4f17d782efef1f973039d821cbe4bcc211/yarl-1.20.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6342d643bf9a1de97e512e45e4b9560a043347e779a173250824f8b254bd5ce", size = 353261, upload-time = "2025-06-10T00:45:05.992Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/23/0ed0922b47a4f5c6eb9065d5ff1e459747226ddce5c6a4c111e728c9f701/yarl-1.20.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56dac5f452ed25eef0f6e3c6a066c6ab68971d96a9fb441791cad0efba6140d3", size = 335987, upload-time = "2025-06-10T00:45:08.227Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/49/bc728a7fe7d0e9336e2b78f0958a2d6b288ba89f25a1762407a222bf53c3/yarl-1.20.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7d7f497126d65e2cad8dc5f97d34c27b19199b6414a40cb36b52f41b79014be", size = 329361, upload-time = "2025-06-10T00:45:10.11Z" },
+ { url = "https://files.pythonhosted.org/packages/93/8f/b811b9d1f617c83c907e7082a76e2b92b655400e61730cd61a1f67178393/yarl-1.20.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:67e708dfb8e78d8a19169818eeb5c7a80717562de9051bf2413aca8e3696bf16", size = 346460, upload-time = "2025-06-10T00:45:12.055Z" },
+ { url = "https://files.pythonhosted.org/packages/70/fd/af94f04f275f95da2c3b8b5e1d49e3e79f1ed8b6ceb0f1664cbd902773ff/yarl-1.20.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:595c07bc79af2494365cc96ddeb772f76272364ef7c80fb892ef9d0649586513", size = 334486, upload-time = "2025-06-10T00:45:13.995Z" },
+ { url = "https://files.pythonhosted.org/packages/84/65/04c62e82704e7dd0a9b3f61dbaa8447f8507655fd16c51da0637b39b2910/yarl-1.20.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7bdd2f80f4a7df852ab9ab49484a4dee8030023aa536df41f2d922fd57bf023f", size = 342219, upload-time = "2025-06-10T00:45:16.479Z" },
+ { url = "https://files.pythonhosted.org/packages/91/95/459ca62eb958381b342d94ab9a4b6aec1ddec1f7057c487e926f03c06d30/yarl-1.20.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c03bfebc4ae8d862f853a9757199677ab74ec25424d0ebd68a0027e9c639a390", size = 350693, upload-time = "2025-06-10T00:45:18.399Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/00/d393e82dd955ad20617abc546a8f1aee40534d599ff555ea053d0ec9bf03/yarl-1.20.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:344d1103e9c1523f32a5ed704d576172d2cabed3122ea90b1d4e11fe17c66458", size = 355803, upload-time = "2025-06-10T00:45:20.677Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/ed/c5fb04869b99b717985e244fd93029c7a8e8febdfcffa06093e32d7d44e7/yarl-1.20.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:88cab98aa4e13e1ade8c141daeedd300a4603b7132819c484841bb7af3edce9e", size = 341709, upload-time = "2025-06-10T00:45:23.221Z" },
+ { url = "https://files.pythonhosted.org/packages/24/fd/725b8e73ac2a50e78a4534ac43c6addf5c1c2d65380dd48a9169cc6739a9/yarl-1.20.1-cp313-cp313t-win32.whl", hash = "sha256:b121ff6a7cbd4abc28985b6028235491941b9fe8fe226e6fdc539c977ea1739d", size = 86591, upload-time = "2025-06-10T00:45:25.793Z" },
+ { url = "https://files.pythonhosted.org/packages/94/c3/b2e9f38bc3e11191981d57ea08cab2166e74ea770024a646617c9cddd9f6/yarl-1.20.1-cp313-cp313t-win_amd64.whl", hash = "sha256:541d050a355bbbc27e55d906bc91cb6fe42f96c01413dd0f4ed5a5240513874f", size = 93003, upload-time = "2025-06-10T00:45:27.752Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/2d/2345fce04cfd4bee161bf1e7d9cdc702e3e16109021035dbb24db654a622/yarl-1.20.1-py3-none-any.whl", hash = "sha256:83b8eb083fe4683c6115795d9fc1cfaf2cbbefb19b3a1cb68f6527460f483a77", size = 46542, upload-time = "2025-06-10T00:46:07.521Z" },
+]
From 0b0219176df96630328edb6b58fee476d287f2e8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=82=8A=E3=81=8A=E3=81=A1=E3=82=93?=
<118029.ichikama@gmail.com>
Date: Sun, 14 Sep 2025 13:21:36 +0900
Subject: [PATCH 2/2] =?UTF-8?q?fix:=20terraform=E3=81=A7env=E6=B8=A1?=
=?UTF-8?q?=E3=81=99=E3=81=AE=E3=81=A8=E3=80=81readme?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 117 +++++++++++++++++++++++-
aws-architecture.md | 133 ++++++++++++++++++++++++++++
infra/environments/dev/main.tf | 9 +-
infra/environments/dev/variables.tf | 8 --
infra/modules/sns/main.tf | 11 +--
infra/modules/sns/outputs.tf | 2 +-
infra/modules/sns/variables.tf | 7 --
7 files changed, 256 insertions(+), 31 deletions(-)
create mode 100644 aws-architecture.md
diff --git a/README.md b/README.md
index 6de222a..09ee3b6 100644
--- a/README.md
+++ b/README.md
@@ -3,13 +3,128 @@
## プロジェクト概要
本プロジェクトは、Project LINKS が提供する「モーダルシフト関連データ 自動車輸送統計調査」([データセットはこちら](https://www.geospatial.jp/ckan/dataset/links-modalshift-2024))を活用し、
-日本全国を舞台に“絵葉書”が移動する様子を楽しめる SNS サービスです。
+日本全国を舞台に"絵葉書"が移動する様子を楽しめる SNS サービスです。
ユーザーは、移動アルゴリズムによって絵葉書がどのように旅をするかを可視化し、他のユーザーと共有できます。
- Progate ハッカソン powered by AWS で作成
- バックエンド: FastAPI
- フロントエンド: Next.js(PWA 対応)
+## Tech Stack
+
+### Backend
+
+
+
+### Frontend
+
+
+
+### Infrastructure
+
+
+
+```mermaid
+graph TB
+ %% Users and External Services
+ User[👤 User]
+ GitHub[📚 GitHub Repository]
+
+ %% Frontend/Client
+ subgraph "Frontend (Client)"
+ Client[📱 React Client App]
+ Amplify[🚀 AWS Amplify]
+ end
+
+ %% Core Infrastructure (Terraform)
+ subgraph "Core Infrastructure (Terraform)"
+ %% Authentication
+ Cognito[🔐 Amazon Cognito
User Authentication]
+
+ %% Compute Layer
+ subgraph "Compute Layer"
+ ALB[⚖️ Application Load Balancer
with SSL Certificate]
+ ECS[🐳 ECS Fargate Cluster
Container Service]
+ EC2[🖥️ EC2 Auto Scaling Group
with IAM Instance Profile]
+ ECR[📦 ECR Repository
Container Images]
+ end
+
+ %% Data Layer
+ subgraph "Data Storage"
+ DynamoDB[🗄️ DynamoDB
NoSQL Database]
+ S3[🪣 S3 Bucket
Static Files & Images]
+ end
+
+ %% Lambda Functions
+ Lambda[⚡ Lambda Function
update-location
Cron: Every 5 minutes]
+
+ %% Notification Services
+ SNS[📧 Amazon SNS
Push Notifications
with Firebase Integration]
+
+ %% Location Services
+ Location[🗺️ Amazon Location Service
API Key for Maps]
+
+ %% Monitoring & Logs
+ CloudWatch[📊 CloudWatch
Logs & Monitoring]
+ end
+
+ %% CI/CD
+ subgraph "CI/CD Pipeline"
+ GitHubActions[🔄 GitHub Actions]
+ OIDC[🔑 OIDC Provider
for GitHub Actions]
+ end
+
+ %% User Interactions
+ User --> Client
+ Client --> Amplify
+
+ %% Amplify Services
+ Amplify --> Cognito
+ Amplify --> S3
+ Amplify --> Location
+ Amplify -.-> DynamoDB
+
+ %% Main Application Flow
+ Client --> ALB
+ ALB --> ECS
+ ECS --> DynamoDB
+ ECS --> S3
+ ECS --> SNS
+ ECS --> CloudWatch
+
+ %% Lambda Integration
+ Lambda --> DynamoDB
+ Lambda --> CloudWatch
+ CloudWatch -.->|Cron Trigger| Lambda
+
+ %% Container Management
+ ECR --> ECS
+ EC2 --> ECS
+
+ %% CI/CD Flow
+ GitHub --> GitHubActions
+ GitHubActions --> OIDC
+ OIDC --> ECR
+ GitHubActions --> ECS
+
+ %% Styling
+ classDef user fill:#e1f5fe
+ classDef frontend fill:#f3e5f5
+ classDef compute fill:#fff3e0
+ classDef storage fill:#e8f5e8
+ classDef serverless fill:#fff8e1
+ classDef auth fill:#fce4ec
+ classDef cicd fill:#f1f8e9
+
+ class User user
+ class Client,Amplify frontend
+ class ALB,ECS,EC2,ECR compute
+ class DynamoDB,S3 storage
+ class Lambda,SNS,Location,CloudWatch serverless
+ class Cognito,OIDC auth
+ class GitHub,GitHubActions cicd
+```
+
## セットアップ手順
### 前提条件
diff --git a/aws-architecture.md b/aws-architecture.md
new file mode 100644
index 0000000..081316e
--- /dev/null
+++ b/aws-architecture.md
@@ -0,0 +1,133 @@
+# AWS Architecture Diagram
+
+```mermaid
+graph TB
+ %% Users and External Services
+ User[👤 User]
+ GitHub[📚 GitHub Repository]
+
+ %% Frontend/Client
+ subgraph "Frontend (Client)"
+ Client[📱 React Client App]
+ Amplify[🚀 AWS Amplify]
+ end
+
+ %% Core Infrastructure (Terraform)
+ subgraph "Core Infrastructure (Terraform)"
+ %% Authentication
+ Cognito[🔐 Amazon Cognito
User Authentication]
+
+ %% Compute Layer
+ subgraph "Compute Layer"
+ ALB[⚖️ Application Load Balancer
with SSL Certificate]
+ ECS[🐳 ECS Fargate Cluster
Container Service]
+ EC2[🖥️ EC2 Auto Scaling Group
with IAM Instance Profile]
+ ECR[📦 ECR Repository
Container Images]
+ end
+
+ %% Data Layer
+ subgraph "Data Storage"
+ DynamoDB[🗄️ DynamoDB
NoSQL Database]
+ S3[🪣 S3 Bucket
Static Files & Images]
+ end
+
+ %% Lambda Functions
+ Lambda[⚡ Lambda Function
update-location
Cron: Every 5 minutes]
+
+ %% Notification Services
+ SNS[📧 Amazon SNS
Push Notifications
with Firebase Integration]
+
+ %% Location Services
+ Location[🗺️ Amazon Location Service
API Key for Maps]
+
+ %% Monitoring & Logs
+ CloudWatch[📊 CloudWatch
Logs & Monitoring]
+ end
+
+ %% CI/CD
+ subgraph "CI/CD Pipeline"
+ GitHubActions[🔄 GitHub Actions]
+ OIDC[🔑 OIDC Provider
for GitHub Actions]
+ end
+
+ %% User Interactions
+ User --> Client
+ Client --> Amplify
+
+ %% Amplify Services
+ Amplify --> Cognito
+ Amplify --> S3
+ Amplify --> Location
+ Amplify -.-> DynamoDB
+
+ %% Main Application Flow
+ Client --> ALB
+ ALB --> ECS
+ ECS --> DynamoDB
+ ECS --> S3
+ ECS --> SNS
+ ECS --> CloudWatch
+
+ %% Lambda Integration
+ Lambda --> DynamoDB
+ Lambda --> CloudWatch
+ CloudWatch -.->|Cron Trigger| Lambda
+
+ %% Container Management
+ ECR --> ECS
+ EC2 --> ECS
+
+ %% CI/CD Flow
+ GitHub --> GitHubActions
+ GitHubActions --> OIDC
+ OIDC --> ECR
+ GitHubActions --> ECS
+
+ %% Styling
+ classDef user fill:#e1f5fe
+ classDef frontend fill:#f3e5f5
+ classDef compute fill:#fff3e0
+ classDef storage fill:#e8f5e8
+ classDef serverless fill:#fff8e1
+ classDef auth fill:#fce4ec
+ classDef cicd fill:#f1f8e9
+
+ class User user
+ class Client,Amplify frontend
+ class ALB,ECS,EC2,ECR compute
+ class DynamoDB,S3 storage
+ class Lambda,SNS,Location,CloudWatch serverless
+ class Cognito,OIDC auth
+ class GitHub,GitHubActions cicd
+```
+
+## Architecture Overview
+
+このプロジェクトは以下の主要コンポーネントで構成されています:
+
+### Frontend Layer
+
+- **React Client**: メインのフロントエンドアプリケーション
+- **AWS Amplify**: 認証、データ、ストレージ、位置情報サービスの統合
+
+### Core Infrastructure (Terraform)
+
+- **ECS Fargate**: コンテナ化されたアプリケーションの実行
+- **Application Load Balancer**: SSL終端とトラフィック分散
+- **EC2 Auto Scaling**: 自動スケーリング機能付きインスタンス管理
+- **DynamoDB**: NoSQLデータベース
+- **S3**: 静的ファイルと画像の保存
+- **Lambda**: 定期実行関数(5分間隔で位置情報更新)
+
+### Additional Services
+
+- **Amazon Cognito**: ユーザー認証とアクセス管理
+- **Amazon SNS**: Firebaseと連携したプッシュ通知
+- **Amazon Location Service**: 地図APIキーの管理
+- **CloudWatch**: ログとモニタリング
+
+### CI/CD Pipeline
+
+- **GitHub Actions**: 自動デプロイメント
+- **OIDC Provider**: セキュアなAWSアクセス
+- **ECR**: Dockerイメージのレジストリ
diff --git a/infra/environments/dev/main.tf b/infra/environments/dev/main.tf
index f49c679..8862de0 100644
--- a/infra/environments/dev/main.tf
+++ b/infra/environments/dev/main.tf
@@ -103,11 +103,10 @@ module "dynamodb" {
# SNS Module for push notifications
module "sns" {
- source = "../../modules/sns"
- app_name = var.app_name
- environment = var.environment
- firebase_server_key = var.firebase_server_key
- tags = local.common_tags
+ source = "../../modules/sns"
+ app_name = var.app_name
+ environment = var.environment
+ tags = local.common_tags
}
# S3 Bucket
diff --git a/infra/environments/dev/variables.tf b/infra/environments/dev/variables.tf
index 16b8e44..6abab37 100644
--- a/infra/environments/dev/variables.tf
+++ b/infra/environments/dev/variables.tf
@@ -100,11 +100,3 @@ variable "container_environment_variables" {
}))
default = []
}
-
-# SNS related variables
-variable "firebase_server_key" {
- description = "Firebase Cloud Messaging server key for push notifications"
- type = string
- default = null
- sensitive = true
-}
diff --git a/infra/modules/sns/main.tf b/infra/modules/sns/main.tf
index f2363eb..2422352 100644
--- a/infra/modules/sns/main.tf
+++ b/infra/modules/sns/main.tf
@@ -5,14 +5,7 @@ resource "aws_sns_topic" "postcard_collect_notifications" {
tags = var.tags
}
-# SNS Platform Application for Firebase Cloud Messaging (PWA)
-resource "aws_sns_platform_application" "postcard_fcm_app" {
- count = var.firebase_server_key != null ? 1 : 0
-
- name = "${var.app_name}-${var.environment}-fcm-app"
- platform = "GCM"
- platform_credential = var.firebase_server_key
-}
+# SNS Platform Application - Not used for Web Push notifications
# IAM policy for SNS publish and endpoint creation
resource "aws_iam_policy" "sns_publish_policy" {
@@ -38,7 +31,7 @@ resource "aws_iam_policy" "sns_publish_policy" {
"sns:GetEndpointAttributes",
"sns:SetEndpointAttributes"
]
- Resource = var.firebase_server_key != null ? "${aws_sns_platform_application.postcard_fcm_app[0].arn}/*" : "*"
+ Resource = "*"
}
]
})
diff --git a/infra/modules/sns/outputs.tf b/infra/modules/sns/outputs.tf
index 2b2c004..9a457a1 100644
--- a/infra/modules/sns/outputs.tf
+++ b/infra/modules/sns/outputs.tf
@@ -10,7 +10,7 @@ output "topic_name" {
output "platform_application_arn" {
description = "ARN of the SNS platform application for FCM"
- value = var.firebase_server_key != null ? aws_sns_platform_application.postcard_fcm_app[0].arn : null
+ value = null
}
output "sns_policy_arn" {
diff --git a/infra/modules/sns/variables.tf b/infra/modules/sns/variables.tf
index 79cadf9..5c32986 100644
--- a/infra/modules/sns/variables.tf
+++ b/infra/modules/sns/variables.tf
@@ -8,13 +8,6 @@ variable "environment" {
type = string
}
-variable "firebase_server_key" {
- description = "Firebase Cloud Messaging server key for PWA push notifications"
- type = string
- default = null
- sensitive = true
-}
-
variable "tags" {
description = "Common tags for resources"
type = map(string)