From 8fa5bef6b514b71a456b49fb5ca8ff103780eac3 Mon Sep 17 00:00:00 2001 From: hyeon Date: Thu, 30 Nov 2023 12:20:53 +0900 Subject: [PATCH 01/46] Add response HTTP status code --- iap/main.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/iap/main.py b/iap/main.py index 641deb07..c7b37b79 100644 --- a/iap/main.py +++ b/iap/main.py @@ -27,9 +27,14 @@ @app.middleware("http") -def log_incoming_url(request: Request, call_next): +async def log_request_response(request: Request, call_next): logger.info(f"[{request.method}] {request.url}") - return call_next(request) + response = await call_next(request) + if response.status_code == 200: + logger.info(f"Request success with {response.status_code}") + else: + logger.error(f"Request failed with {response.status_code}") + return response # Error handler From 80dbed5dfca0a59a60cf2205eff044ce97d436ee Mon Sep 17 00:00:00 2001 From: hyeon Date: Thu, 30 Nov 2023 17:46:40 +0900 Subject: [PATCH 02/46] Create IAP status monitor --- worker/worker/status_monitor.py | 171 ++++++++++++++++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 worker/worker/status_monitor.py diff --git a/worker/worker/status_monitor.py b/worker/worker/status_monitor.py new file mode 100644 index 00000000..f1dcb7af --- /dev/null +++ b/worker/worker/status_monitor.py @@ -0,0 +1,171 @@ +import os +from datetime import datetime, timezone, timedelta +from typing import Dict, List + +import requests +from sqlalchemy import create_engine, select +from sqlalchemy.orm import sessionmaker, scoped_session + +from common import logger +from common.enums import ReceiptStatus, TxStatus +from common.models.receipt import Receipt +from common.utils.aws import fetch_secrets +from common.utils.receipt import PlanetID + +DB_URI = os.environ.get("DB_URI") +db_password = fetch_secrets(os.environ.get("REGION_NAME"), os.environ.get("SECRET_ARN"))["password"] +DB_URI = DB_URI.replace("[DB_PASSWORD]", db_password) +CURRENT_PLANET = PlanetID.ODIN if os.environ.get("STAGE") == "mainnet" else PlanetID.ODIN_INTERNAL +GQL_URL = f"{os.environ.get('HEADLESS')}/graphql" +IAP_ALERT_WEBHOOK_URL = os.environ.get("IAP_ALERT_WEBHOOK_URL") +IAP_GARAGE_WEBHOOK_URL = os.environ.get("IAP_GARAGE_WEBHOOK_URL") + +FUNGIBLE_DICT = { + "3991e04dd808dc0bc24b21f5adb7bf1997312f8700daf1334bf34936e8a0813a": "Hourglass (400000)", + "00dfffe23964af9b284d121dae476571b7836b8d9e2e5f510d92a840fecc64fe": "AP Potion (500000)", + "1a755098a2bc0659a063107df62e2ff9b3cdaba34d96b79519f504b996f53820": "Silver Dust (800201)", + "f8faf92c9c0d0e8e06694361ea87bfc8b29a8ae8de93044b98470a57636ed0e0": "Golden Dust (600201)", +} + +engine = create_engine(DB_URI) + + +def send_message(url: str, title: str, blocks: List): + if not blocks: + logger.info(f"{title} :: No blocks to send.") + return + + message = { + "blocks": [{ + "type": "header", + "text": { + "type": "plain_text", + "text": title, + "emoji": True + } + }], + "attachments": [{"blocks": blocks}] + } + resp = requests.post(url, json=message) + logger.info(f"{title} :: Sent {len(blocks)} :: {resp.status_code} :: {resp.text}") + + +def create_block(text: str) -> Dict: + return { + "type": "section", + "text": { + "type": "mrkdwn", + "text": text + } + } + + +def check_invalid_receipt(sess): + """Notify all invalid receipt""" + invalid_list = sess.scalars(select(Receipt).where( + Receipt.created_at <= (datetime.now(tz=timezone.utc) - timedelta(minutes=1)), + Receipt.status.in_([ReceiptStatus.INIT, ReceiptStatus.VALIDATION_REQUEST, ReceiptStatus.INVALID]) + )).fetchall() + + msg = [] + for invalid in invalid_list: + msg.append(create_block(f"ID {invalid.id} :: {invalid.uuid} :: {invalid.status.name}")) + + send_message(IAP_ALERT_WEBHOOK_URL, "[NineChronicles.IAP] Non-Valid Receipt Report", msg) + + +def check_tx_failure(sess): + """Notify all failed Tx""" + tx_failed_receipt_list = sess.scalars(select(Receipt).where(Receipt.tx_status == TxStatus.FAILURE)).fetchall() + + msg = [] + for receipt in tx_failed_receipt_list: + msg.append(create_block( + f"ID {receipt.id} :: {receipt.uuid} :: {receipt.tx_status.name}\nTx. ID: {receipt.tx_id}" + )) + + send_message(IAP_ALERT_WEBHOOK_URL, "[NineChronicles.IAP] Tx. Failed Receipt Report", msg) + + +def check_halt_tx(sess): + """Notify when STAGED|INVALID tx over 5min.""" + tx_halt_receipt_list = sess.scalars(select(Receipt).where( + Receipt.tx_status.in_([TxStatus.INVALID, TxStatus.STAGED]), + Receipt.created_at <= (datetime.now(tz=timezone.utc) - timedelta(minutes=5)), + )).fetchall() + + msg = [] + for receipt in tx_halt_receipt_list: + msg.append(create_block(f"ID {receipt.id} :: {receipt.uuid}\n{receipt.tx_id}")) + + send_message(IAP_ALERT_WEBHOOK_URL, "[NineChronicles.IAP] Tx. Invalid Receipt Report", msg) + + +def check_no_tx(sess): + """Notify when no Tx created purchase over 3min.""" + no_tx_receipt_list = sess.scalars(select(Receipt).where( + Receipt.status == ReceiptStatus.VALID, + Receipt.tx.is_(None), + Receipt.created_at <= (datetime.now(tz=timezone.utc) - timedelta(minutes=3)) + )).fetchall() + + msg = [] + for receipt in no_tx_receipt_list: + msg.append(create_block( + f"ID {receipt.id} :: {receipt.uuid}::Product {receipt.product_id}\n{receipt.agent_addr} :: {receipt.avatar_addr}" + )) + + send_message(IAP_ALERT_WEBHOOK_URL, "[NineChronicles.IAP] No Tx. Create Receipt Report", msg) + + +def check_garage(): + """Report IAP Garage stock""" + query = """{ + stateQuery { + garages( + agentAddr: "0xCb75C84D76A6f97A2d55882Aea4436674c288673", + currencyTickers: ["CRYSTAL", "RUNE_GOLDENLEAF"] + fungibleItemIds: [ + "3991e04dd808dc0bc24b21f5adb7bf1997312f8700daf1334bf34936e8a0813a", # Hourglass (400000) + "00dfffe23964af9b284d121dae476571b7836b8d9e2e5f510d92a840fecc64fe", # AP Potion (500000) + "f8faf92c9c0d0e8e06694361ea87bfc8b29a8ae8de93044b98470a57636ed0e0" # Golden Dust (600201) + "1a755098a2bc0659a063107df62e2ff9b3cdaba34d96b79519f504b996f53820", # Silver Dust (800201) + ] + ) { + garageBalances { currency { ticker minters decimalPlaces } quantity } + fungibleItemGarages { fungibleItemId item {itemSubType} count } + } + } + }""" + + resp = requests.post(GQL_URL, json={"query": query}) + data = resp.json()["data"]["stateQuery"]["garages"] + fav_data = data["garageBalances"] + item_data = data["fungibleItemGarages"] + + msg = [] + for fav in fav_data: + msg.append(create_block(f"{fav['currency']['ticker']} : {int(fav['quantity'].split('.')[0]):,}")) + for item in item_data: + msg.append(create_block(f"{FUNGIBLE_DICT[item['fungibleItemId']]} : {item['count']:,}")) + + send_message(IAP_GARAGE_WEBHOOK_URL, "[NineChronicles.IAP] Daily Garage Report", msg) + + +def handle(event, context): + session = scoped_session(sessionmaker(bind=engine)) + if datetime.utcnow().hour == 3 and datetime.now().minute == 0: # 12:00 KST + check_garage() + + with session.begin() as sess: + check_invalid_receipt(sess) + check_halt_tx(sess) + check_tx_failure(sess) + + +if __name__ == "__main__": + sess = scoped_session(sessionmaker(bind=engine)) + check_garage() + check_invalid_receipt(sess) + check_halt_tx(sess) + check_tx_failure(sess) From e584ee63b6732d1134d0b38a3c8fd9f21a6a82a5 Mon Sep 17 00:00:00 2001 From: hyeon Date: Thu, 30 Nov 2023 17:57:30 +0900 Subject: [PATCH 03/46] Update CDK stack - Remove old, unused stack - Add status monitor - For mainnet, execute every minute - For other, execute every hour --- worker/worker_cdk_stack.py | 60 +++++++++++--------------------------- 1 file changed, 17 insertions(+), 43 deletions(-) diff --git a/worker/worker_cdk_stack.py b/worker/worker_cdk_stack.py index ce8de1ed..089de655 100644 --- a/worker/worker_cdk_stack.py +++ b/worker/worker_cdk_stack.py @@ -138,56 +138,30 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: ) minute_event_rule.add_target(_event_targets.LambdaFunction(tracker)) - # Price updater Lambda function - # NOTE: Price is directly fetched between client and google play. - # Not need to update price in IAP service. - # updater = _lambda.Function( - # self, f"{config.stage}-9c-iap-price-updater-function", - # function_name=f"{config.stage}-9c-iap-price-updater", - # runtime=_lambda.Runtime.PYTHON_3_10, - # description="9c IAP price updater from google/apple store", - # code=_lambda.AssetCode("worker/worker", exclude=exclude_list), - # handler="updater.update_prices", - # layers=[layer], - # role=role, - # vpc=shared_stack.vpc, - # timeout=cdk_core.Duration.seconds(120), - # environment=env, - # memory_size=192, - # ) - - # Every hour - # hourly_event_rule = _events.Rule( - # self, f"{config.stage}-9c-iap-price-updater-event", - # schedule=_events.Schedule.cron(minute="0") # Every hour - # ) - # - # hourly_event_rule.add_target(_event_targets.LambdaFunction(updater)) - - # IAP garage daily report - env["IAP_GARAGE_WEBHOOK_URL"] = os.environ.get("IAP_GARAGE_WEBHOOK_URL") - garage_report = _lambda.Function( - self, f"{config.stage}-9c-iap-garage-report", - function_name=f"{config.stage}_9c-iap-garage-reporter", - runtime=_lambda.Runtime.PYTHON_3_10, - description="Daily report of 9c IAP Garage item count", + # IAP Status Monitor + status_monitor = _lambda.Function( + self, f"{config.stage}-9c-iap-status-monitor-function", + function_name=f"{config.stage}-9c-iap-status-monitor", + description="Receipt and Tx. status monitor for Nine Chronicles", code=_lambda.AssetCode("worker/worker", exclude=exclude_list), - handler="garage_noti.noti", + handler="status_monitor.handle", layers=[layer], role=role, vpc=shared_stack.vpc, - timeout=cdk_core.Duration.seconds(10), - environment=env, - memory_size=192, + timeout=cdk_core.Duration.seconds(300), + memory_size=512, ) - # EveryDay 03:00 UTC == 12:00 KST - if config.stage != "internal": - everyday_event_rule = _events.Rule( - self, f"{config.stage}-9c-iap-everyday-event", - schedule=_events.Schedule.cron(hour="3", minute="0") # Every day 00:00 ETC + if config.stage == "mainnet": + minute_event_rule.add_target(_event_targets.LambdaFunction(status_monitor)) + else: + # Every hour + hourly_event_rule = _events.Rule( + self, f"{config.stage}-9c-iap-hourly-event", + schedule=_events.Schedule.cron(minute="0") # Every hour ) - everyday_event_rule.add_target(_event_targets.LambdaFunction(garage_report)) + + hourly_event_rule.add_target(_event_targets.LambdaFunction(status_monitor)) # Golden dust by NCG handler env["GOLDEN_DUST_REQUEST_SHEET_ID"] = config.golden_dust_request_sheet_id From a13e991105e44c4119e271ba21012f4c184cc88e Mon Sep 17 00:00:00 2001 From: hyeon Date: Fri, 1 Dec 2023 10:10:33 +0900 Subject: [PATCH 04/46] Add missing runtime --- worker/worker_cdk_stack.py | 1 + 1 file changed, 1 insertion(+) diff --git a/worker/worker_cdk_stack.py b/worker/worker_cdk_stack.py index 089de655..1757e53b 100644 --- a/worker/worker_cdk_stack.py +++ b/worker/worker_cdk_stack.py @@ -143,6 +143,7 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: self, f"{config.stage}-9c-iap-status-monitor-function", function_name=f"{config.stage}-9c-iap-status-monitor", description="Receipt and Tx. status monitor for Nine Chronicles", + runtime=_lambda.Runtime.PYTHON_3_10, code=_lambda.AssetCode("worker/worker", exclude=exclude_list), handler="status_monitor.handle", layers=[layer], From d4e771b4618224d034cf501e90d5f395315a62ad Mon Sep 17 00:00:00 2001 From: hyeon Date: Fri, 1 Dec 2023 14:36:33 +0900 Subject: [PATCH 05/46] TEST shop in mainnet not allowed --- iap/api/purchase.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/iap/api/purchase.py b/iap/api/purchase.py index 777f66a2..dc21b7f6 100644 --- a/iap/api/purchase.py +++ b/iap/api/purchase.py @@ -210,7 +210,11 @@ def request_product(receipt_data: ReceiptSchema, sess=Depends(session)): receipt.product_id = product.id ## Test elif receipt_data.store == Store.TEST: - success, msg = True, "This is test" + if os.environ.get("STAGE") == "mainnet": + receipt.status = ReceiptStatus.INVALID + success, msg = False, f"{receipt.store} is not validatable store." + else: + success, msg = True, "This is test" ## INVALID else: receipt.status = ReceiptStatus.UNKNOWN From cb63c8994c683338878030f9ade0b5ccd13bd534 Mon Sep 17 00:00:00 2001 From: hyeon Date: Mon, 4 Dec 2023 15:16:04 +0900 Subject: [PATCH 06/46] Add voucher_request model --- .../5bf8ab4f3b13_add_voucher_request_table.py | 46 +++++++++++++++++++ common/models/__init__.py | 1 + common/models/voucher.py | 28 +++++++++++ 3 files changed, 75 insertions(+) create mode 100644 common/alembic/versions/5bf8ab4f3b13_add_voucher_request_table.py create mode 100644 common/models/voucher.py diff --git a/common/alembic/versions/5bf8ab4f3b13_add_voucher_request_table.py b/common/alembic/versions/5bf8ab4f3b13_add_voucher_request_table.py new file mode 100644 index 00000000..3b5f64b0 --- /dev/null +++ b/common/alembic/versions/5bf8ab4f3b13_add_voucher_request_table.py @@ -0,0 +1,46 @@ +"""Add voucher_request table + +Revision ID: 5bf8ab4f3b13 +Revises: 57e65ed34bd6 +Create Date: 2023-12-01 11:22:16.116276 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = '5bf8ab4f3b13' +down_revision = '57e65ed34bd6' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('voucher_request', + sa.Column('receipt_id', sa.Integer(), nullable=False), + sa.Column('uuid', sa.UUID(), nullable=False), + sa.Column('agent_addr', sa.Text(), nullable=False), + sa.Column('avatar_addr', sa.Text(), nullable=False), + sa.Column('planet_id', sa.LargeBinary(length=12), nullable=False), + sa.Column('product_id', sa.Integer(), nullable=False), + sa.Column('product_name', sa.Text(), nullable=False), + sa.Column('status', sa.Integer(), nullable=True), + sa.Column('message', sa.Text(), nullable=True), + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['product_id'], ['product.id'], ), + sa.ForeignKeyConstraint(['receipt_id'], ['receipt.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_voucher_request_uuid'), 'voucher_request', ['uuid'], unique=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_voucher_request_uuid'), table_name='voucher_request') + op.drop_table('voucher_request') + # ### end Alembic commands ### diff --git a/common/models/__init__.py b/common/models/__init__.py index f44fb57a..6bdf13bf 100644 --- a/common/models/__init__.py +++ b/common/models/__init__.py @@ -2,4 +2,5 @@ "garage", "receipt", "product", + "voucher", ] diff --git a/common/models/voucher.py b/common/models/voucher.py new file mode 100644 index 00000000..bff755df --- /dev/null +++ b/common/models/voucher.py @@ -0,0 +1,28 @@ +import uuid + +from sqlalchemy import Column, Text, Integer, ForeignKey, UUID, LargeBinary +from sqlalchemy.orm import relationship, backref + +from common.models.base import Base, TimeStampMixin, AutoIdMixin +from common.utils.receipt import PlanetID + + +class VoucherRequest(AutoIdMixin, TimeStampMixin, Base): + __tablename__ = "voucher_request" + receipt_id = Column(Integer, ForeignKey("receipt.id"), nullable=False) + receipt = relationship("Receipt", foreign_keys=[receipt_id], uselist=False, backref=backref("voucher_request")) + + # Copy all required data to view all info solely with this table + uuid = Column(UUID(as_uuid=True), nullable=False, index=True, default=uuid.uuid4, + doc="Internal uuid for management") + agent_addr = Column(Text, nullable=False) + avatar_addr = Column(Text, nullable=False) + planet_id = Column(LargeBinary(length=12), nullable=False, default=PlanetID.ODIN.value, + doc="An identifier of planets") + product_id = Column(Integer, ForeignKey("product.id"), nullable=False) + product = relationship("Product", foreign_keys=[product_id], uselist=False) + product_name = Column(Text, nullable=False) + + # Voucher request result + status = Column(Integer, nullable=True) + message = Column(Text, nullable=True) From 96cbd467810c5af5b56280779869cc01e6e4a7d5 Mon Sep 17 00:00:00 2001 From: hyeon Date: Mon, 4 Dec 2023 15:27:48 +0900 Subject: [PATCH 07/46] Create voucher request function --- worker/worker/voucher.py | 80 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 worker/worker/voucher.py diff --git a/worker/worker/voucher.py b/worker/worker/voucher.py new file mode 100644 index 00000000..98bde73c --- /dev/null +++ b/worker/worker/voucher.py @@ -0,0 +1,80 @@ +import os +from datetime import datetime, timezone, timedelta + +import jwt +import requests +from sqlalchemy import create_engine, select +from sqlalchemy.orm import sessionmaker, scoped_session + +from common import logger +from common.models.voucher import VoucherRequest +from common.utils.aws import fetch_secrets, fetch_parameter +from schemas.aws import SQSMessage + +DB_URI = os.environ.get("DB_URI") +db_password = fetch_secrets(os.environ.get("REGION_NAME"), os.environ.get("SECRET_ARN"))["password"] +DB_URI = DB_URI.replace("[DB_PASSWORD]", db_password) +VOUCHER_URL = os.environ.get("VOUCHER_URL") +VOUCHER_JWT_SECRET = fetch_parameter( + os.environ.get("REGION_NAME"), + f"{os.environ.get('STAGE')}_9c_IAP_SEASON_PASS_JWT_SECRET", + True +)["Value"] + +engine = create_engine(DB_URI, pool_size=5, max_overflow=5) + + +def get_voucher_token() -> str: + now = datetime.now(tz=timezone.utc) + data = { + "iat": now, + "exp": now + timedelta(seconds=10) + } + return jwt.encode(data, VOUCHER_JWT_SECRET) + + +def request(sess, voucher: VoucherRequest) -> bool: + resp = requests.post( + VOUCHER_URL, + headers={"Authorization": f"Bearer {get_voucher_token()}"}, + json={ + "planetId": voucher.planet_id, + "agentAddress": voucher.agent_addr, + # "avatarAddress": voucher.avatar_addr, + "iapUuid": voucher.uuid, + "productId": voucher.product_id, + "productName": voucher.product.name, + }, + ) + success = False + voucher.status = resp.status_code + if resp.status_code == 200: + voucher.message = "Success" + success = True + else: + logger.error(f"{voucher.id} :: {voucher.uuid} :: {resp.status_code} :: {resp.text}") + voucher.message = resp.text + + sess.add(voucher) + sess.commit() + sess.refresh(voucher) + + return success + + +def handle(event, context): + message = SQSMessage(Records=event.get("Records", {})) + logger.info(f"SQS Message: {message}") + + with scoped_session(sessionmaker(bind=engine)).begin() as sess: + uuid_list = [x.body.get("uuid") for x in message.Records if x.body.get("uuid")] + voucher_list = sess.scalars(select(VoucherRequest.uuid).where(VoucherRequest.uuid.in_(uuid_list))).fetchall() + target_message_list = [x.body for x in message.Records if + x.body.get("force", False) is True or x.body.get("uuid") not in voucher_list] + + for msg in target_message_list: + voucher = VoucherRequest(**msg) + sess.add(voucher) + sess.commit() + sess.refresh(voucher) + request(sess, voucher) From 268224da4afb464cac51f2031d02c0f9dc1b57ed Mon Sep 17 00:00:00 2001 From: hyeon Date: Mon, 4 Dec 2023 15:28:21 +0900 Subject: [PATCH 08/46] Send voucher request using SQS --- common/shared_stack.py | 10 +++++++++- iap/api/purchase.py | 13 +++++++++++++ iap/iap_cdk_stack.py | 6 +++--- worker/worker_cdk_stack.py | 11 +++++++++++ 4 files changed, 36 insertions(+), 4 deletions(-) diff --git a/common/shared_stack.py b/common/shared_stack.py index 0a081c36..0e4db50d 100644 --- a/common/shared_stack.py +++ b/common/shared_stack.py @@ -53,6 +53,13 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: visibility_timeout=cdk_core.Duration.seconds(20), ) + self.voucher_dlq = _sqs.Queue(self, f"{config.stage}-9c-iap-voucher-dlq") + self.voucher_q = _sqs.Queue( + self, f"{config.stage}-9c-iap-voucher-queue", + dead_letter_queue=_sqs.DeadLetterQueue(max_receive_count=12, queue=self.voucher_dlq), + visibility_timeout=cdk_core.Duration.seconds(10) + ) + # RDS self.rds_security_group = _ec2.SecurityGroup( self, f"{config.stage}-9c-iap-rds-sg", vpc=self.vpc, allow_all_outbound=True @@ -89,7 +96,8 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: ("KMS_KEY_ID", True), ("GOOGLE_CREDENTIAL", True), ("APPLE_CREDENTIAL", True), - ("SEASON_PASS_JWT_SECRET", True) + ("SEASON_PASS_JWT_SECRET", True), + ("VOUCHER_JWT_SECRET", True), ) ssm = boto3.client("ssm", region_name=config.region_name, aws_access_key_id=os.environ.get("AWS_ACCESS_KEY_ID"), diff --git a/iap/api/purchase.py b/iap/api/purchase.py index 777f66a2..62f5072f 100644 --- a/iap/api/purchase.py +++ b/iap/api/purchase.py @@ -35,6 +35,7 @@ sqs = boto3.client("sqs", region_name=settings.REGION_NAME) SQS_URL = os.environ.get("SQS_URL") +VOUCHER_SQS_URL = os.environ.get("VOUCHER_SQS_URL") def validate_apple(tx_id: str) -> Tuple[bool, str, Optional[ApplePurchaseSchema]]: @@ -161,6 +162,18 @@ def request_product(receipt_data: ReceiptSchema, sess=Depends(session)): ) sess.add(receipt) sess.commit() + logger.info(f"Send voucher request: {receipt.uuid}") + resp = sqs.send_message(QueueUrl=VOUCHER_SQS_URL, + MessageBody=json.dumps({ + "id": receipt.id, + "uuid": receipt.uuid, + "product_id": receipt.product_id, + "product_name": receipt.product.name, + "agent_addr": receipt.agent_addr, + "avatar_addr": receipt.avatar_addr, + "planet_id": receipt_data.planetId.decode(), + })) + logger.info(f"Voucher message: {resp['MessageId']}") sess.refresh(receipt) if receipt_data.store not in (Store.APPLE, Store.APPLE_TEST) and not product: diff --git a/iap/iap_cdk_stack.py b/iap/iap_cdk_stack.py index d8903f70..cab8814a 100644 --- a/iap/iap_cdk_stack.py +++ b/iap/iap_cdk_stack.py @@ -8,7 +8,6 @@ aws_certificatemanager as _acm, aws_iam as _iam, aws_lambda as _lambda, - aws_logs as _logs, ) from constructs import Construct @@ -64,7 +63,7 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: role.add_to_policy( _iam.PolicyStatement( actions=["sqs:sendmessage"], - resources=[shared_stack.q.queue_arn] + resources=[shared_stack.q.queue_arn, shared_stack.voucher_q.queue_arn, ] ) ) ssm = boto3.client("ssm", region_name=config.region_name, @@ -92,6 +91,7 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: "LOGGING_LEVEL": "INFO", "DB_ECHO": "False", "SQS_URL": shared_stack.q.queue_url, + "VOUCHER_SQS_URL": shared_stack.voucher_q.queue_url, "GOOGLE_PACKAGE_NAME": config.google_package_name, "APPLE_BUNDLE_ID": config.apple_bundle_id, "APPLE_VALIDATION_URL": config.apple_validation_url, @@ -104,7 +104,7 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: } # Lambda Function - exclude_list = [".", "*", ".idea", ".git", ".pytest_cache", ".gitignore", ".github",] + exclude_list = [".", "*", ".idea", ".git", ".pytest_cache", ".gitignore", ".github", ] exclude_list.extend(COMMON_LAMBDA_EXCLUDE) exclude_list.extend(IAP_LAMBDA_EXCLUDE) diff --git a/worker/worker_cdk_stack.py b/worker/worker_cdk_stack.py index ce8de1ed..665720d3 100644 --- a/worker/worker_cdk_stack.py +++ b/worker/worker_cdk_stack.py @@ -189,6 +189,17 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: ) everyday_event_rule.add_target(_event_targets.LambdaFunction(garage_report)) + # IAP Voucher + voucher_handler = _lambda.Function( + self, f"{config.stage}-9c-iap-voucher-handler-function", + function_name=f"{config.stage}-9c-iap-voucher-handler", + description="IAP voucher handler between IAP and portal", + runtime=_lambda.Runtime.PYTHON_3_10, + code=_lambda.AssetCode("worker/worker", exclude=exclude_list), + handler="voucher.handle", + + ) + # Golden dust by NCG handler env["GOLDEN_DUST_REQUEST_SHEET_ID"] = config.golden_dust_request_sheet_id env["GOLDEN_DUST_WORK_SHEET_ID"] = config.golden_dust_work_sheet_id From 1440ecab119188f141e28b66549e18f462430ff1 Mon Sep 17 00:00:00 2001 From: hyeon Date: Mon, 4 Dec 2023 15:34:57 +0900 Subject: [PATCH 09/46] Add missing config --- .github/workflows/deploy.yml | 4 ++++ .github/workflows/main.yml | 3 +++ .github/workflows/synth.yml | 3 +++ common/__init__.py | 3 +++ 4 files changed, 13 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 6b203f32..97a5d4ed 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -41,6 +41,8 @@ on: required: true SEASON_PASS_JWT_SECRET: required: true + VOUCHER_JWT_SECRET: + required: true BRIDGE_DATA: required: true @@ -137,6 +139,7 @@ jobs: CDN_HOST: ${{ vars.CDN_HOST }} PLANET_URL: ${{ vars.PLANET_URL }} SEASON_PASS_JWT_SECRET: ${{ secrets.SEASON_PASS_JWT_SECRET }} + VOUCHER_JWT_SECRET: ${{ secrets.VOUCHER_JWT_SECRET }} BRIDGE_DATA: ${{ secrets.BRIDGE_DATA }} run: | source $VENV @@ -166,6 +169,7 @@ jobs: CDN_HOST: ${{ vars.CDN_HOST }} PLANET_URL: ${{ vars.PLANET_URL }} SEASON_PASS_JWT_SECRET: ${{ secrets.SEASON_PASS_JWT_SECRET }} + VOUCHER_JWT_SECRET: ${{ secrets.VOUCHER_JWT_SECRET }} BRIDGE_DATA: ${{ secrets.BRIDGE_DATA }} run: | source $VENV diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 10841ef6..7effb838 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -45,6 +45,7 @@ jobs: GOLDEN_DUST_REQUEST_SHEET_ID: ${{ secrets.GOLDEN_DUST_REQUEST_SHEET_ID }} GOLDEN_DUST_WORK_SHEET_ID: ${{ secrets.GOLDEN_DUST_WORK_SHEET_ID }} SEASON_PASS_JWT_SECRET: ${{ secrets.SEASON_PASS_JWT_SECRET }} + VOUCHER_JWT_SECRET: ${{ secrets.VOUCHER_JWT_SECRET }} BRIDGE_DATA: ${{ secrets.BRIDGE_DATA }} deploy_without_approval: @@ -68,6 +69,7 @@ jobs: GOLDEN_DUST_REQUEST_SHEET_ID: ${{ secrets.GOLDEN_DUST_REQUEST_SHEET_ID }} GOLDEN_DUST_WORK_SHEET_ID: ${{ secrets.GOLDEN_DUST_WORK_SHEET_ID }} SEASON_PASS_JWT_SECRET: ${{ secrets.SEASON_PASS_JWT_SECRET }} + VOUCHER_JWT_SECRET: ${{ secrets.VOUCHER_JWT_SECRET }} BRIDGE_DATA: ${{ secrets.BRIDGE_DATA }} approval: @@ -103,4 +105,5 @@ jobs: GOLDEN_DUST_REQUEST_SHEET_ID: ${{ secrets.GOLDEN_DUST_REQUEST_SHEET_ID }} GOLDEN_DUST_WORK_SHEET_ID: ${{ secrets.GOLDEN_DUST_WORK_SHEET_ID }} SEASON_PASS_JWT_SECRET: ${{ secrets.SEASON_PASS_JWT_SECRET }} + VOUCHER_JWT_SECRET: ${{ secrets.VOUCHER_JWT_SECRET }} BRIDGE_DATA: ${{ secrets.BRIDGE_DATA }} diff --git a/.github/workflows/synth.yml b/.github/workflows/synth.yml index 225726e9..ebd0a9a9 100644 --- a/.github/workflows/synth.yml +++ b/.github/workflows/synth.yml @@ -36,6 +36,8 @@ on: required: true SEASON_PASS_JWT_SECRET: required: true + VOUCHER_JWT_SECRET: + required: true BRIDGE_DATA: required: true @@ -124,6 +126,7 @@ jobs: CDN_HOST: ${{ vars.CDN_HOST }} PLANET_URL: ${{ vars.PLANET_URL }} SEASON_PASS_JWT_SECRET: ${{ secrets.SEASON_PASS_JWT_SECRET }} + VOUCHER_JWT_SECRET: ${{ secrets.VOUCHER_JWT_SECRET }} BRIDGE_DATA: ${{ secrets.BRIDGE_DATA }} run: | source $VENV diff --git a/common/__init__.py b/common/__init__.py index 448f822d..687c83d2 100644 --- a/common/__init__.py +++ b/common/__init__.py @@ -60,3 +60,6 @@ class Config: # SeasonPass season_pass_jwt_secret: str = None + + # Voucher + voucher_jwt_secret: str = None From 7a534f4a180d67286b886b9ab92a4121f7a3ad36 Mon Sep 17 00:00:00 2001 From: hyeon Date: Mon, 4 Dec 2023 16:50:15 +0900 Subject: [PATCH 10/46] Pass uuid as string --- iap/api/purchase.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iap/api/purchase.py b/iap/api/purchase.py index 031824ff..17ca9a7b 100644 --- a/iap/api/purchase.py +++ b/iap/api/purchase.py @@ -166,7 +166,7 @@ def request_product(receipt_data: ReceiptSchema, sess=Depends(session)): resp = sqs.send_message(QueueUrl=VOUCHER_SQS_URL, MessageBody=json.dumps({ "id": receipt.id, - "uuid": receipt.uuid, + "uuid": str(receipt.uuid), "product_id": receipt.product_id, "product_name": receipt.product.name, "agent_addr": receipt.agent_addr, From ac4fa5a66691eb021e7392f544cdd0669649eb52 Mon Sep 17 00:00:00 2001 From: hyeon Date: Mon, 4 Dec 2023 16:50:26 +0900 Subject: [PATCH 11/46] Add missing lambda settings --- worker/worker_cdk_stack.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/worker/worker_cdk_stack.py b/worker/worker_cdk_stack.py index c06f7b17..a8ed1d72 100644 --- a/worker/worker_cdk_stack.py +++ b/worker/worker_cdk_stack.py @@ -146,6 +146,7 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: runtime=_lambda.Runtime.PYTHON_3_10, code=_lambda.AssetCode("worker/worker", exclude=exclude_list), handler="status_monitor.handle", + environment=env, layers=[layer], role=role, vpc=shared_stack.vpc, @@ -172,7 +173,14 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: runtime=_lambda.Runtime.PYTHON_3_10, code=_lambda.AssetCode("worker/worker", exclude=exclude_list), handler="voucher.handle", - + layers=[layer], + role=role, + vpc=shared_stack.vpc, + timeout=cdk_core.Duration.seconds(30), + memory_size=512, + events=[ + _evt_src.SqsEventSource(shared_stack.voucher_q) + ], ) # Golden dust by NCG handler From 8c39dfeccdd519859c9bb4703e7d16c905115a56 Mon Sep 17 00:00:00 2001 From: hyeon Date: Mon, 4 Dec 2023 17:29:35 +0900 Subject: [PATCH 12/46] Set visibility timeout >= function timeout --- common/shared_stack.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/shared_stack.py b/common/shared_stack.py index 0e4db50d..e13db572 100644 --- a/common/shared_stack.py +++ b/common/shared_stack.py @@ -56,8 +56,8 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: self.voucher_dlq = _sqs.Queue(self, f"{config.stage}-9c-iap-voucher-dlq") self.voucher_q = _sqs.Queue( self, f"{config.stage}-9c-iap-voucher-queue", - dead_letter_queue=_sqs.DeadLetterQueue(max_receive_count=12, queue=self.voucher_dlq), - visibility_timeout=cdk_core.Duration.seconds(10) + dead_letter_queue=_sqs.DeadLetterQueue(max_receive_count=10, queue=self.voucher_dlq), + visibility_timeout=cdk_core.Duration.seconds(30), ) # RDS From 9c18b2f4601124aa5adf9815ee7dbcce271c60b3 Mon Sep 17 00:00:00 2001 From: hyeon Date: Mon, 4 Dec 2023 17:54:09 +0900 Subject: [PATCH 13/46] Use JWT package inside worker --- poetry.lock | 403 +++++++++++++++++++++++-------------------------- pyproject.toml | 2 +- 2 files changed, 190 insertions(+), 215 deletions(-) diff --git a/poetry.lock b/poetry.lock index aa4ab0e0..f146cadd 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,14 +1,14 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. [[package]] name = "alembic" -version = "1.12.1" +version = "1.13.0" description = "A database migration tool for SQLAlchemy." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "alembic-1.12.1-py3-none-any.whl", hash = "sha256:47d52e3dfb03666ed945becb723d6482e52190917fdb47071440cfdba05d92cb"}, - {file = "alembic-1.12.1.tar.gz", hash = "sha256:bca5877e9678b454706347bc10b97cb7d67f300320fa5c3a94423e8266e2823f"}, + {file = "alembic-1.13.0-py3-none-any.whl", hash = "sha256:a23974ea301c3ee52705db809c7413cecd165290c6679b9998dd6c74342ca23a"}, + {file = "alembic-1.13.0.tar.gz", hash = "sha256:ab4b3b94d2e1e5f81e34be8a9b7b7575fc9dd5398fccb0bef351ec9b14872623"}, ] [package.dependencies] @@ -17,7 +17,7 @@ SQLAlchemy = ">=1.3.0" typing-extensions = ">=4" [package.extras] -tz = ["python-dateutil"] +tz = ["backports.zoneinfo"] [[package]] name = "annotated-types" @@ -51,17 +51,6 @@ doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd- test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] trio = ["trio (<0.22)"] -[[package]] -name = "appnope" -version = "0.1.3" -description = "Disable App Nap on macOS >= 10.9" -optional = false -python-versions = "*" -files = [ - {file = "appnope-0.1.3-py2.py3-none-any.whl", hash = "sha256:265a455292d0bd8a72453494fa24df5a11eb18373a60c7c0430889f22548605e"}, - {file = "appnope-0.1.3.tar.gz", hash = "sha256:02bd91c4de869fbb1e1c50aafc4098827a7a54ab2f39d9dcba6c9547ed920e24"}, -] - [[package]] name = "asttokens" version = "2.4.1" @@ -190,163 +179,163 @@ files = [ [[package]] name = "bitarray" -version = "2.8.3" +version = "2.8.4" description = "efficient arrays of booleans -- C extension" optional = false python-versions = "*" files = [ - {file = "bitarray-2.8.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:be7c6343a7f24293a988e5a27c1e2f44f028476e35192e73663c4acec5c4766e"}, - {file = "bitarray-2.8.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:38233e5793e107575be656908419d2bceab359c78c28affc386c7b88b8882b8f"}, - {file = "bitarray-2.8.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:acf24bc6aedd0a490af71591b99401867d4445d64db09a7bfe0bde3e8498cc8d"}, - {file = "bitarray-2.8.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:04fcb292637012a1551e55c00796e31b5c66d1692ca25a5ac83d23779c23cd29"}, - {file = "bitarray-2.8.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:015908355354d42973ad41ba4eca697b4b55690b3ece6d9629118273e7a9e380"}, - {file = "bitarray-2.8.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48a89c2112420ebeb163a3c273c244d542cf9315c9ce5a875d305f91adcdac24"}, - {file = "bitarray-2.8.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb530a9fb7ed13a1a49bda81db2def4c73b7fef0fd1bb969b1d7605121869230"}, - {file = "bitarray-2.8.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c87146e9c2c196c012e97273f82215e2239b9bffcbb6c7802bbbedac87be2358"}, - {file = "bitarray-2.8.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:84a2628a5377971d73c95014e540a51327eb27ffdfbab81e43eac494eced3dc2"}, - {file = "bitarray-2.8.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6bcbe2ea34c88cf736f157cf3d713c1af112f0d7a9eec390d69a9e042b7d76d4"}, - {file = "bitarray-2.8.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:67ee9d71af3db621aa637f96520a8df8534fcc64e881360d3ed3a07f7e47ed1b"}, - {file = "bitarray-2.8.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ba3f27d82b45543a7d1488d151594915a6e67fb28bd4f21eb0901df2ba4ede86"}, - {file = "bitarray-2.8.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:095923f084d2271f28d7430798e698f6d0b304c58b072b4f2eb0bc132321323b"}, - {file = "bitarray-2.8.3-cp310-cp310-win32.whl", hash = "sha256:de91007504b475a93d8b0949db9dec86d39c0306de9914f7b9087daeb3d9fbaf"}, - {file = "bitarray-2.8.3-cp310-cp310-win_amd64.whl", hash = "sha256:09c140daa13d2515609d5a2dbfd289eada200e96222671194dc72eae89bc3c7b"}, - {file = "bitarray-2.8.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2bfd32ce49d23584333087262fb367b371c74cf531f6b0c16759d59f47c847d7"}, - {file = "bitarray-2.8.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:12035756896d71e82edf6a6fb46d3ca299eadbec25140c12505d4b32f561b0da"}, - {file = "bitarray-2.8.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:73fa449d9e551a063ff5c68b5d2cc0caaede5b59366d37457261ae3080f61fca"}, - {file = "bitarray-2.8.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18707458f6467072a9c3322835a299fa86df8fb3962f51afac2b50c6a4babf82"}, - {file = "bitarray-2.8.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f142476b3bb80f6887b5a3a08d69bbd526093aee5a00973c26458cc16dd5e47"}, - {file = "bitarray-2.8.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:47400fa421b8a3947f6676981f8d9b8581239831533dff374477ef2b86fda42f"}, - {file = "bitarray-2.8.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56f51107bb5406bfa4889064c01d5f9e7a545b3e2b53f159626c72c910fe8f07"}, - {file = "bitarray-2.8.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a3741359cbb1a9eb50188e8faa0ced96ca658eb85061786b7f686efa94c3604"}, - {file = "bitarray-2.8.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c65080bbba08ce07b136490b4df3d0907ec3dd76c3c5d47fda011002420f6d31"}, - {file = "bitarray-2.8.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:117a6f409dabc15320f3212d05d878cc33436c1e118e8746bf3775da2509bb7d"}, - {file = "bitarray-2.8.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:782ff781ae3c4956c15764aefc06ceb8c1c348794f09dfc8ebf62ff35166da1f"}, - {file = "bitarray-2.8.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:a7b839e5c038111fd2fbd09e83ca945da357d690e49cfa269c09aed239db9c2b"}, - {file = "bitarray-2.8.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ab7e9b1846cc62739d9d293a94f704949b588afb9ed72db00e26b7fcdb4661a3"}, - {file = "bitarray-2.8.3-cp311-cp311-win32.whl", hash = "sha256:20cc6573ac21627e0fde854d4e0450d4c97706213bac986c0d38d252452da155"}, - {file = "bitarray-2.8.3-cp311-cp311-win_amd64.whl", hash = "sha256:8011a63692e9e32cdc3fac3dfd0beceece926e8b53fb91750037fc386917f90b"}, - {file = "bitarray-2.8.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:da61c6d7b6288d29db5be77048176f41f7320316997fced28b5415e1f939448e"}, - {file = "bitarray-2.8.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:60774f73151dbcabefb5acb6d97ac09a51c999f9a903ac6f8db3d8368d338969"}, - {file = "bitarray-2.8.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c815a7ca72a5eebcd85caaeb4d32b71af1c795e38b3dff5dcb5b6b1f3ba0b4f"}, - {file = "bitarray-2.8.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a102cd1fafee8919a069fed9ea40c1ffe4d6037fd5b0a7f47326c2f75f24f70f"}, - {file = "bitarray-2.8.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b2816afe82feeb7948e58ca0be31c254e23307953e56d3313f293f79279fbe7"}, - {file = "bitarray-2.8.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98fe712a82f65de536b65fa9af7601df4e8231f14e3b0b14ef22e16e30d2fbea"}, - {file = "bitarray-2.8.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8defbf10a731b44892001daa6903b2f2f7ad8c623a7b4d9ae6bd674592b1763e"}, - {file = "bitarray-2.8.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e98a7b510aaaf0d7368b7cb983d3106aecd28abdfa4b4593b80e7f4ab5af0a97"}, - {file = "bitarray-2.8.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a5e24317b0768789c52586a31284dec8ccafa2f6c128df2f2d79656142f1e794"}, - {file = "bitarray-2.8.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:c30dbbe2f49056d4bd97a94c07a7fc0118ecc85661fdbaada36dfa9b14dc5962"}, - {file = "bitarray-2.8.3-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:2adb2ba1e7196f62587f4011b213b3609a717f92698a398904192e201ec3e29e"}, - {file = "bitarray-2.8.3-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:3aa1bd71236e07f0e7ab859a130fc57645301fd1ffd64be9a9750bce51446acb"}, - {file = "bitarray-2.8.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:63e595ca8dab2b77104e618782764bc3b172a0e9c6f97734d5fdd299063feac0"}, - {file = "bitarray-2.8.3-cp312-cp312-win32.whl", hash = "sha256:0c3de6517df7bbac18632046e722ca9000a4aeb76da68e545437fee1e61e2bbc"}, - {file = "bitarray-2.8.3-cp312-cp312-win_amd64.whl", hash = "sha256:4a6a4e83ecab1fd1fc171c57334663b24c5d286b66421efac2428b7e105c5d62"}, - {file = "bitarray-2.8.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:993438edd54350133f7569a8691074a90aa2297def69ec0e7af34de3d175cd00"}, - {file = "bitarray-2.8.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06770f6f7d238c2e2d251e9f5346358653ea8f3dbbedc83d18598f6c044f16b4"}, - {file = "bitarray-2.8.3-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44e3944ebccbc38ebdb7bd3c37a9b6ff91d87db2dad4bf3910e2b01fbd36831b"}, - {file = "bitarray-2.8.3-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a86c308018b59b999cf3d5a16889d3a347b48a2d08f34fbb4e29d5dc05fa198a"}, - {file = "bitarray-2.8.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b92c17b15bd5536c3e067051c67531adc81fcb6c1a699a760600ccd03dfcfba"}, - {file = "bitarray-2.8.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e3d80bc6722652c847e5f503c2ce94a641b016059ec45bde4e1f13454b33e904"}, - {file = "bitarray-2.8.3-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:fbc7ac38de41052599f1e27edf4f33c02d5aea6810ee299825a81863a32e26a0"}, - {file = "bitarray-2.8.3-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:bbca4c4bc9854e3166474e471f3230989fd2baf32c915e363c32f91dc6ebb704"}, - {file = "bitarray-2.8.3-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:74efd69ac9d06ce9f43a1f513cee8a82c314f85aa0bd74664abe9e608fb59ffd"}, - {file = "bitarray-2.8.3-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:c3f7a6c6b78edd81fca0035fb7a156a79f25919e1b0598afb483c26513d562f1"}, - {file = "bitarray-2.8.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b0cefac8fedb3dbbf97542dc0c6fdd8bf09a210bf6fa5799083b7309fd97b1b2"}, - {file = "bitarray-2.8.3-cp36-cp36m-win32.whl", hash = "sha256:67e366efaea6e0b5971593a83d062cb7e4e09e03d29f8d5b825effdf5f516ad3"}, - {file = "bitarray-2.8.3-cp36-cp36m-win_amd64.whl", hash = "sha256:621d5658b890b99b3f8b1a678b0afed10e096d53baa767ecbcf428fce1f48415"}, - {file = "bitarray-2.8.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ac5451951ce1e0616385e77de49afc7bd90bdf9d0aa99c0fd7b0bd23400db890"}, - {file = "bitarray-2.8.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff6b6b47da38223803aa3e7aab356f84e0636ecdbd43fa4bd11dbc00a923d474"}, - {file = "bitarray-2.8.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:154082c814e4007bf15d8dfc576ebd4e79e9ed3626017cd53810961cee7e65d8"}, - {file = "bitarray-2.8.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9f4f29c0338e5862ebc3b88091d29ff28d44ab80381f238da08aabb054777c2"}, - {file = "bitarray-2.8.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b153b846a6ac4b6eca71bb5f84d3dba51f3cd159f4322f5d67b2c41cf15973ad"}, - {file = "bitarray-2.8.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a2c8e06c3463746181255e03f07535c136f5346fb9c4a90eec2da27695102533"}, - {file = "bitarray-2.8.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f16a2247c27f4db3f8d01665ee97d46eaf0240b7a9feae16c17e906a3bb9a794"}, - {file = "bitarray-2.8.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:57f1fc3a089d9907859e940c6a4db3f5358013c75bba3b15156d93a58bca868e"}, - {file = "bitarray-2.8.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:c42fcddc955d84164667d899e8d4bbb763f4bc029fe72642a65df7382c46fe94"}, - {file = "bitarray-2.8.3-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:e60254ac626790c8c95415b095c6831056ca57a5d31839564210530c3278f170"}, - {file = "bitarray-2.8.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a0bb2e5c0c9f964bf43a09a1cf37233ff96b3318c9a50b1b7c3d74a875b32072"}, - {file = "bitarray-2.8.3-cp37-cp37m-win32.whl", hash = "sha256:edddd6d885c7195ba7734936bc1efc8a37de18ec886a8be44a484980da87947e"}, - {file = "bitarray-2.8.3-cp37-cp37m-win_amd64.whl", hash = "sha256:44ee266b71cd6bd7c99f937b30ac3b7627cad04777f2c12894cd0f820cb79ada"}, - {file = "bitarray-2.8.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a836a988ada812776af9ea6e88edf1e2eaaf38ebd545bbbcd500b2db0ced3a4f"}, - {file = "bitarray-2.8.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:089a4658706ec63293c153ffb1472cea1bbefb39ccfb214f52f0c1f5d10bf28e"}, - {file = "bitarray-2.8.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f8c492d90b41c510d799cc37c27892b149be77e225df6446854ce0b164e243a3"}, - {file = "bitarray-2.8.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b661052a4762825790a728469f897c341558392342cb68a6c54708d4e5198254"}, - {file = "bitarray-2.8.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4fd5e8a2e1b898ebc91faf6e1938bde38a4d20ee8ea49835e9adadd9b87c97c"}, - {file = "bitarray-2.8.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4d4f3e78a8c1c5bf625632488a4bdd78fe87c4603ea10443cb8f207c2a846efe"}, - {file = "bitarray-2.8.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5797552e849079ff963936a037087367f20b41d5a612b07a1ba032259a2b86c8"}, - {file = "bitarray-2.8.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:adfc210df3d85017f5d2ef82db94d46b585ecbbd7357a6ee1c3bc125cc2658e2"}, - {file = "bitarray-2.8.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:252bdf94c74192b10f7fdb42683adf1403892acdce39e3e3524e8b070793b1c7"}, - {file = "bitarray-2.8.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:879bb9f11bad60a5588f5efb4e60f42844e4787ce7d5bb0f8eb8b87a835e914f"}, - {file = "bitarray-2.8.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:7a6413b5f53d44e134276d5a3747b71d17cbc25177a50445458921424a760dcd"}, - {file = "bitarray-2.8.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:3d0daf70de198dcde459451c534333c0f59ab847649be013c9b88d24f0e49767"}, - {file = "bitarray-2.8.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:09244fa4e39ca263820dd8eca83a0175a98fb8f9bd353b4285a9ef2928b7fb41"}, - {file = "bitarray-2.8.3-cp38-cp38-win32.whl", hash = "sha256:7ad527ff1d398a703eba71ac270625087691e62efab8d0e331c53affe0628030"}, - {file = "bitarray-2.8.3-cp38-cp38-win_amd64.whl", hash = "sha256:2fcaf220e53518762dae0701082cb70d620656eaaecf5512695a6afafa885ea6"}, - {file = "bitarray-2.8.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:38e19756480bff2703155060d1849d37138a1d2242287563de112fb5bdd3217d"}, - {file = "bitarray-2.8.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:123333df4b22f12f4fc13fa4821b8ca075df59161bd41f5f189ffc791aaac10b"}, - {file = "bitarray-2.8.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ff62c1c174ceae7ef0456702f9eff1f3d76590c075b9c984c459d734f73fc766"}, - {file = "bitarray-2.8.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7554518934364b30d8da085f7a759ee3838c9ae4265b48beb82072f942b2816e"}, - {file = "bitarray-2.8.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8f0306dbc6605dd7f9e2dada33a3916c0c28f37128464de7153df7d8cf7a959"}, - {file = "bitarray-2.8.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2aeae0f2dacf546256f8720a1e8233b6735a3bf76778be701a1736d26fe4ecec"}, - {file = "bitarray-2.8.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c02d24051d7070b8f3b52fa9c8984fd8eb035115545f7c4be44c9825e8b58c8"}, - {file = "bitarray-2.8.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82fe0a774204159383d1be993191d51500cb44adbd3e9287da801e4657c0d4b2"}, - {file = "bitarray-2.8.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:aa4513a7393055faef630dcfb4d10a339c47eeb943487c0e9063ba763b66cb73"}, - {file = "bitarray-2.8.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:36f9752b654e18f99130a2bf84f54b1e6b8fad4f5f768f4390eb9b769a64a59c"}, - {file = "bitarray-2.8.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:a4212b66f9ae2e28ca1aa0307167ebfcdb2ca263a56b786cc572699e8a717f91"}, - {file = "bitarray-2.8.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:cadccf651900e3858e55dfd762d5de0786aec853f1fb26183905ddee233183b4"}, - {file = "bitarray-2.8.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9f756d159099f154a21d73932f13c8ce27f45a1c892d9b19c66a1a2c50c18474"}, - {file = "bitarray-2.8.3-cp39-cp39-win32.whl", hash = "sha256:c2ffed55994f5c73d34371474946767f936b0b83237f800be0f27a3e783baadb"}, - {file = "bitarray-2.8.3-cp39-cp39-win_amd64.whl", hash = "sha256:f69cacb3d983200114e48ec0c894e28690926f166b71202f75e976d5cd588be9"}, - {file = "bitarray-2.8.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d6a8a1da9205de97eea14aaa731c657fa8decd2d6878ee3d2d4bf33291960216"}, - {file = "bitarray-2.8.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8562dd32b4d9810a0b9c04fe3d1ed8078f27d74e3738063162c677b253216666"}, - {file = "bitarray-2.8.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed974048a4ced6e7b5d1cfcb83c046e70bf31b8a28eacfee3afa62f8690dee69"}, - {file = "bitarray-2.8.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2448d8f5ce6d8a840a5dff1b41f5124445141530724af7ba82ec7967eabd290a"}, - {file = "bitarray-2.8.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:64d867953b530b3dde93663d4c4708b533216e9dca3f3b4489698261cd80fcef"}, - {file = "bitarray-2.8.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:72bba6b388ba7c48a882bd58c86972aab73a30c3fb5b3341f28eb5bdc17365f8"}, - {file = "bitarray-2.8.3-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f62ee2eae65b72e034a24ac2bacd78d48845193168b54407e93bccd3772b247f"}, - {file = "bitarray-2.8.3-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07ed46857ed73765f2316e08f2d5108b7e694b44f4293e30fb526f3123c829d4"}, - {file = "bitarray-2.8.3-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:136bd205384a3089bc22c02a365a152e61b1e8d06ec664185c90e3ab8967260c"}, - {file = "bitarray-2.8.3-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:42d2d0123b1e68b387f4b2fd288e1a8f0dfb991cf1d2fbc56d948c3f4a113d8d"}, - {file = "bitarray-2.8.3-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5f35d5ff7334610b42632b30c27332b30db3680dd0174f86e382c3e150dfea2c"}, - {file = "bitarray-2.8.3-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7618abbac8999cd942be278130b88ac6ed364ba3446222f1db0faf4de7a052cf"}, - {file = "bitarray-2.8.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50923d862e01a546f942272193612f386ec1f90cc4528b10561854902bd8aab0"}, - {file = "bitarray-2.8.3-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c99838782dbec7f0c5cba1a6d4faa8e2da2b522423aa36a7f383a2265ac0ae3f"}, - {file = "bitarray-2.8.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e76735a285e834fc9db560de11e086453128c1177950a15c3404fe16c7d76f5e"}, - {file = "bitarray-2.8.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ffa74d8601e26570f1d0e3042fda6eb26b64ba8d8dfe9b96d0bf90a6f0d81582"}, - {file = "bitarray-2.8.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6993e46c81702d0bb39aad83ceb228cec087bc321782fbd2c6ddff7c653dcc8"}, - {file = "bitarray-2.8.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d9ec6a214563d2edd46d1a553583782379a2cb1016e8cc6c524e011905433b1"}, - {file = "bitarray-2.8.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34ceedbeed9aefde10c273d44801971db8f7505f80933fbb936969ee2343b8a3"}, - {file = "bitarray-2.8.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:cc178297951343c8d8cd8a391999abf0024ca319671418f98dea0d7e71354126"}, - {file = "bitarray-2.8.3.tar.gz", hash = "sha256:e15587b2bdf18d32eb3ba25f5f5a51bedd0dc06b3112a4c53dab5e7753bc6588"}, + {file = "bitarray-2.8.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:69be498a39ac29ea4f8e4dce36e64342d4fe813eeffa7bd9ead4ce18309fb903"}, + {file = "bitarray-2.8.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6acbfa5b85717c91bfa1bc1702c1cc6a3d1500f832f2c3c040f0d4668c75b2b5"}, + {file = "bitarray-2.8.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:305f1aa2a3aedd033ab2ab1fc930c5f0a987bf993f3ecc83a224db237a95cd18"}, + {file = "bitarray-2.8.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8f7a1a4793c4dec2dc7c6c1fac5370123a24c6dabc7312fbce8766a0d5c40c8"}, + {file = "bitarray-2.8.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87acfa228524b8564ba5d5a431ff6c708721ff7755f718992184bb9a81365f0e"}, + {file = "bitarray-2.8.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:45782033c17ea2d1c9967128bc69aee1417210b104fbda35d4da77d907afb3c5"}, + {file = "bitarray-2.8.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f343ea39f61f899bac145aac260dd456a98df59e4258ad8d395892b6b4759b20"}, + {file = "bitarray-2.8.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52c8501aa71a353dbe8dd6440bbd3449d8ffcae843bff139f87b9a84149315ce"}, + {file = "bitarray-2.8.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bb23c806f175a88db60b3894bca4956f6d557ed0571b2fcc7818c1c83f000759"}, + {file = "bitarray-2.8.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:efbe1d873f916fa31235b8acec6a686e7b7e47e3e95490cbe8b257dabaa14d3b"}, + {file = "bitarray-2.8.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0690c5483e31d7e4d7f26b045baf7f9d0aa30e91fcf1c5117095652d149b1a96"}, + {file = "bitarray-2.8.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:2cc0c09edd3fa893303513d3fb9a0d335f20b19b3f0276fe752bf88ffd5522c0"}, + {file = "bitarray-2.8.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:045b813c3567818673f2dcd9c0b41a63214c5f5a9c230ede76ac211fbcf0185a"}, + {file = "bitarray-2.8.4-cp310-cp310-win32.whl", hash = "sha256:ddfd3632e5f04619d780f60e85a5fe082a8eebce33aefb08b6783779ff04d017"}, + {file = "bitarray-2.8.4-cp310-cp310-win_amd64.whl", hash = "sha256:c19c27056cb34b352c064ac0d58ac7f7da29cd225cb3140b8ff69455e6858966"}, + {file = "bitarray-2.8.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6d83fda6e4d83742d60f522ce3bd61ce0d4690c19b73dc79ee8da2a48f2ef065"}, + {file = "bitarray-2.8.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:de1f491b329e424f7e7b2874624a604c163ea05341f709cd47c1a46f4930ca97"}, + {file = "bitarray-2.8.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c8e25c5530bd6bb5a96ad11de2dc16bebbbec8b4e2c1600bf1ce78cbf36c96e6"}, + {file = "bitarray-2.8.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:602b429cedf3631cb3b36a7e08f484972b2e13bb0fc1e240b71935aef32bb9d9"}, + {file = "bitarray-2.8.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:102b988fdbb0b221bdb71dac7d96475bfa47a767ee0fc1014a9ad5be46ebd20b"}, + {file = "bitarray-2.8.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:665f88d204d4a8fd0fe63fea66c1a420b331887e72a2b10778d97d22182e8474"}, + {file = "bitarray-2.8.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:965e52d079e8746abe6d15e8b1da7b65d9f1ccb5bceb1aa410072f09a1cdb3fd"}, + {file = "bitarray-2.8.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b619c691c94f2770373a91144bbbe42056a993fa95aba67f87a7625f71384040"}, + {file = "bitarray-2.8.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c27b57205a6946de4dedb169d42f63d8f61e51a70e3096ffce18680c8407616c"}, + {file = "bitarray-2.8.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:2f8484bea6cededfacc2921fd5715e3132467d1df50f941635b91c9920dfd66f"}, + {file = "bitarray-2.8.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e7b705a7c3bb5c7a86a2e4bf5d4607e22194d821e050b5f5605a69ded99dc5c3"}, + {file = "bitarray-2.8.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:afb4e6edfeb6797165a25e5ea221992043c46b3475f7d4d96e2c25271dfea4d8"}, + {file = "bitarray-2.8.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2bb731fe68c07d5a3aeb9df798008e41999c933ed81786c7688b190f5082e079"}, + {file = "bitarray-2.8.4-cp311-cp311-win32.whl", hash = "sha256:22a0d11bf53553e2de20eb1dbf507bba32a6c28a2b84232ff5f28289ba9ec659"}, + {file = "bitarray-2.8.4-cp311-cp311-win_amd64.whl", hash = "sha256:8ace24f1b028dee7168556e0a83c1f608abe63f4b82dc05b26ad43495d8717bf"}, + {file = "bitarray-2.8.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1009f6a4117d50c2e9e4a2d6d5a03d0cb030f649dd410fbbef4d3f3a9aca40c9"}, + {file = "bitarray-2.8.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9a53bf859e4b54ad06dda20aa42a16dd36b03f11626beacc41b570f25cfcb471"}, + {file = "bitarray-2.8.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4991d8c2b4ccccd1ea9115dae5dc51b60b562bc627784e53c31aae5699a55932"}, + {file = "bitarray-2.8.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7832ecd8adc5ef9f0af7c376ea4ab8ba66077da45e1d00da9f93366cbfb70dfe"}, + {file = "bitarray-2.8.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:644fc022c5b3973472e39930f43c113865f9ba1b4e918b52f5921d709af0e9e3"}, + {file = "bitarray-2.8.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99cd2f5dd46e886a63bc08dbb44ae63b16eeff94d714be55ce41ff86604bbc97"}, + {file = "bitarray-2.8.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e938552f8fd83ecdde6f43d7f91854fa2604cc7c7e2929fed78c3779c843ba6"}, + {file = "bitarray-2.8.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6910610f1f54e7b9e5aa5311acff812e5ae2ca5f6c041a40c9201f768c4a6893"}, + {file = "bitarray-2.8.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8becb576748328b2fdf9740a973e62f41de83702a92761e0ff441b65ebe25fce"}, + {file = "bitarray-2.8.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8b1c84803dd57f8b81397dcc24eca73bc44f1c5da36b440f358372b50c7bb7da"}, + {file = "bitarray-2.8.4-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:2e15d244cb7dab42cb1f31933da3b66d6405b1db969917460c094ba8441ea5a0"}, + {file = "bitarray-2.8.4-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:55d52dd5af45dfb09e9b107749b4fcad4a3774d5429345faa47ab459ae478de0"}, + {file = "bitarray-2.8.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ef2dbbb2924c5048bea586ddf204607c8e91fbe70b95a7dce1d5b5403f2ef06f"}, + {file = "bitarray-2.8.4-cp312-cp312-win32.whl", hash = "sha256:7ecd20dfef83d3180d9f851476e5e3d9a76973e24432721f7cc8cac52a646d3a"}, + {file = "bitarray-2.8.4-cp312-cp312-win_amd64.whl", hash = "sha256:e7ac4f3cc1cdbe5b31bce988260ac12ae0e273ec6108bf35de66384599fabc25"}, + {file = "bitarray-2.8.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ebdaea27ada24e53d673c46a8a4bba8e1904fa8589512bd3146382d877ab4be9"}, + {file = "bitarray-2.8.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf6e39e296422588c39eaa8bea17c3d5af9335c7273691615e6aa262f3a1c469"}, + {file = "bitarray-2.8.4-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6bbd70d2a0be93349ee76652992164d89dab54e55cb05d302d4375851b60d173"}, + {file = "bitarray-2.8.4-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed4e54d4425c9f5eb766ff8ee4b992fe0011575a7da5daa8bf898675c684808c"}, + {file = "bitarray-2.8.4-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f18e53a47619ef092cb28ac1f1f2b457ad68177369a5c02a1da930f5f0a43d78"}, + {file = "bitarray-2.8.4-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a1232d989dc37b2b0d760ed3cd040f848a7578417d0bda24e544e73f5d6b02a"}, + {file = "bitarray-2.8.4-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:3df0ac492e4e795e26710ee20cfd25c7bfd81c3866490078fcc7d97ccc74b01f"}, + {file = "bitarray-2.8.4-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:06d9ad81d56547b2b256e70a819eb4eefa4e7e21595b06b4102666a71eb4b961"}, + {file = "bitarray-2.8.4-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:7bc9808782e3147fb71b44129f3dfabfbe839bc35954f9f7f3dd8dd4c149413c"}, + {file = "bitarray-2.8.4-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:56cc56c382167360a94e36a54a3a14320ecbe9e8ca672574c739703136d0b5e0"}, + {file = "bitarray-2.8.4-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:de8b30056fe36947d50597680aa016f5a9a59f2621b496ca0fe8ad037ee63f76"}, + {file = "bitarray-2.8.4-cp36-cp36m-win32.whl", hash = "sha256:d80a356e6123b0910171ab7b2ce4d058146170748f11b7ec3c005da54bfbc059"}, + {file = "bitarray-2.8.4-cp36-cp36m-win_amd64.whl", hash = "sha256:00bb1de6d3c68e18fb16c6c7390e68bc656a60dfde4004d5649c792b8871a531"}, + {file = "bitarray-2.8.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6db1bc132b3ee46bb79a1d86bfadce71d581943156004e481045ce903f1979db"}, + {file = "bitarray-2.8.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27b824ae449cd38c8c77349ae7d27dc11662c9c40806729943dd175c91334a4c"}, + {file = "bitarray-2.8.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7947134bc4b28a00c07616e07013680628954bc93daa4cbab99a6d7aea402809"}, + {file = "bitarray-2.8.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c392d44cc072255e88efc4335be67ebdfb88ae4b3757bd573c49fae35e23470"}, + {file = "bitarray-2.8.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c787dbacf218cde121611706e2bb6a64e3fb566a828bab7d608c6c96cfec8a4"}, + {file = "bitarray-2.8.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c59e589f162dffb8bea47fb8108961891df5d54d3a1c59660f211a53084438cd"}, + {file = "bitarray-2.8.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1d1efcf28571909ea4c12184d51bd953370fd28ec227b1ded7cb88563c17d42a"}, + {file = "bitarray-2.8.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:afe8bedc9893a358a29430e98164a902816fd7787f92b476193a0de7aae4b985"}, + {file = "bitarray-2.8.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:64e6e316452d8018d11954698f9898a2ee69fe2f1093333c2650a4b91246c675"}, + {file = "bitarray-2.8.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:02f8002eac8ba7796e05690f317056c6ddd40ac88f73d1dd3405c5d4df15a61d"}, + {file = "bitarray-2.8.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d26fdf9d680eb9738e8b58ab7946cb35ed8b076dac823189f2614d732565e89a"}, + {file = "bitarray-2.8.4-cp37-cp37m-win32.whl", hash = "sha256:9e52a186b68b5995c3756f243e286ea701470273ba938b9f83a0ef055edeb95e"}, + {file = "bitarray-2.8.4-cp37-cp37m-win_amd64.whl", hash = "sha256:3baf866f2674241b02ab9547acaae2f705e7e9ca5a620484e8b09a25fc625def"}, + {file = "bitarray-2.8.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2c9d06891a565bdc572dc8a2d76275fc3d51e63ddff51c3e03a9a95b600ca673"}, + {file = "bitarray-2.8.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:242f9ddfed9e7b70edb2888056af1710dfbf3767342d6ef1c110fe1d3b346ad6"}, + {file = "bitarray-2.8.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9245d3181346f1f608b56cb45fb21c080558426dac566074a2c4145daa411588"}, + {file = "bitarray-2.8.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eacc7d5ad4b120131da4c6cecd8ded5e545dab3654de592cf8901a7acfd58c18"}, + {file = "bitarray-2.8.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:228e20443c841585454e95e17cf66610c9c53c3a1c66f3a9bc90a1ce31218b9d"}, + {file = "bitarray-2.8.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3449769a8f6c6a39d3d8c8760d265ff754107715c8ad3d66e90961ea463e6284"}, + {file = "bitarray-2.8.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25e882da07d5735ee089cec12dc75d55b90434e607aae5522515f23132612091"}, + {file = "bitarray-2.8.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80eca1ef96a3b379026bcf531d7cbfbfad767da37ba4e90bc529e6695f88ba09"}, + {file = "bitarray-2.8.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6c8ebc5b2cf89b4dd2d407312eeec4ed1f999863a6d29d1d1834696f6db08ac8"}, + {file = "bitarray-2.8.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9849d06f254fffd45d35ba2b39694dbc839f6c5cca8990a509b3058588f23d77"}, + {file = "bitarray-2.8.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:2ff712ba8259599135d24fcc555dbca2dc32ff5d18e8efb8d47456d2467e630f"}, + {file = "bitarray-2.8.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:e3fc442c3ae66c4f9a0d35f2c2a0e36f6a9c125b94c3db1ee8fa5af4dca51a57"}, + {file = "bitarray-2.8.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:275f4deacd4cee28311cee611cea64bf5ec197da4a95c23b2af00ecc1dee6e97"}, + {file = "bitarray-2.8.4-cp38-cp38-win32.whl", hash = "sha256:b349092caf10b6b0585e0ff0ed17e5fc8a88c3bdacb37b38778de4a1ae568827"}, + {file = "bitarray-2.8.4-cp38-cp38-win_amd64.whl", hash = "sha256:ed37c74e33c67f805e046c0e4d1af2007d4087d01748fa47a56ee3501c1bb597"}, + {file = "bitarray-2.8.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3be310edafc506a4f7c405d7d2d97274ab3ec3f2cbd2793705ccdb692559a009"}, + {file = "bitarray-2.8.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c334ab66657dc0250281d1deaaa0243bb2072da0939fc89cbce4513a79b7ebdc"}, + {file = "bitarray-2.8.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c6ab07a20fe548f4830bc3d795d4e8193616379abb8715fcf0391ca599cf4f4b"}, + {file = "bitarray-2.8.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3994b319e6f18040652b769ceb09e28b5546bffa29138019b788bafa8577478f"}, + {file = "bitarray-2.8.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:803dc8ca520db0db6e14bc61c52666a2344b5ff45c9c4524967f1920779ef64f"}, + {file = "bitarray-2.8.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a2f6cd2861922bf951451cd19c0f658d93ac313424ec705c59768626eb4b1f0"}, + {file = "bitarray-2.8.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98df463206a76ef02d8662490bafc6ca2d6dec10cfff3dda90798c0e4f330151"}, + {file = "bitarray-2.8.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da7086efb3d388078874b2fafd5042a5c6464e08ecb68bf3813c3b9d54d236b4"}, + {file = "bitarray-2.8.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:343876f1a38d9b2857f18f7d606be49b11344dc3b9c46f072760dec364a35a54"}, + {file = "bitarray-2.8.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:0488c1eaf83c993fc672115176cc6c27070d5abd5e673499ed46eeb87de52169"}, + {file = "bitarray-2.8.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:aadc4b8ac31ef4ac31f13ab416d5891ff1886b0c3115e88b4be53d3ce08e235f"}, + {file = "bitarray-2.8.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:bb05962feb649bbb5589eab89b9fa57679ce8285e647195bee76c8c0821fcf22"}, + {file = "bitarray-2.8.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:67accba68ceb3cb57bae9ed86ddd075043e373c4af6243e20c8f00153c5f374a"}, + {file = "bitarray-2.8.4-cp39-cp39-win32.whl", hash = "sha256:0adf959b63e314ea74c7d67ca6732c2a840769a7bcfe779d52d777ac6877d671"}, + {file = "bitarray-2.8.4-cp39-cp39-win_amd64.whl", hash = "sha256:d0fc43f5f5ae113ad60b502ec1efee42218c21a1e00dd1bd7c82d00b25cf72ad"}, + {file = "bitarray-2.8.4-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:63e1bb1c98d6d3004e44cb1958393c631e79c640877086a7f403c223c18687cb"}, + {file = "bitarray-2.8.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bea66a30fb0b9d3109db950b490f6aa211fb15162f097b20141b1aeb5057a670"}, + {file = "bitarray-2.8.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8aaea18c41dacf2bf1a6f81960c196f85e3991c9387c3d9bff97976be2c195a4"}, + {file = "bitarray-2.8.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad2b129e43998292f89f138dfda32ec1b9ba31e68b35a61948bc10bf53e94444"}, + {file = "bitarray-2.8.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:6647e03def035371ce0ce073912d6594ed197f799aa34641f0acce343a8f7cca"}, + {file = "bitarray-2.8.4-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d31416540af1ad2994a33cf7f2e98e1e8f50722e410afc54ae99bdd6039a4f87"}, + {file = "bitarray-2.8.4-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c936d73deca901b600fb73c9aaf3630dd358f5ce35c5d5e1ea804b33796ecb5"}, + {file = "bitarray-2.8.4-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc936c0cea105c7773e6b8cc58ed2a3b168a3da9bbdec7466cee9725198607a9"}, + {file = "bitarray-2.8.4-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9978b0968acbc2d9160758e9f63af0fbda62f121ae596ad56cb06a8afd3d5aea"}, + {file = "bitarray-2.8.4-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:dcae87cbf2058a33286ce50e627bdd1a4875579103f6b933546ffb1a34ab8c2e"}, + {file = "bitarray-2.8.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b5d1d4300706891d197cf21b39f41b3c8047d081676d82eb8dcfeb8d0073c52b"}, + {file = "bitarray-2.8.4-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7095d8f486435ffcc42014aebba27c05b2a3b38d5d3630ebe77734db7653b272"}, + {file = "bitarray-2.8.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06bcd5c171ffedb7544ad9e5b77827cd3a3ccb0dd924ef703802743b8abcf303"}, + {file = "bitarray-2.8.4-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6328f73d4e623d4fff966cbe623f3e2b3378bdbfb6937ec492aba3fd9927862f"}, + {file = "bitarray-2.8.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:2d20ee30ea7640df29013021d130bee932d701f01b2f1cbbc1ba14f3954a6b1f"}, + {file = "bitarray-2.8.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:91a570f291a4d7ea4473f37b5e1ce377d771a8567a7a6b5f7b482023bd81b3ef"}, + {file = "bitarray-2.8.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18561539cf8ca5d1970b2b78a44a1b12ae21a18183664a080525c081a44b3997"}, + {file = "bitarray-2.8.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1abea439874652c3ad6ca6a6e893cfe4f2e2c149294dbe2a5c1cf7e2e1ef200"}, + {file = "bitarray-2.8.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1a0f347672c5a8b67c36937872c75baec81e351f2209dc691608d3f76fa9e44e"}, + {file = "bitarray-2.8.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:cdd58e73a2e1bff848067a65afb77a7dcd1884050c22d18a0a7af5cf2428a3ee"}, + {file = "bitarray-2.8.4.tar.gz", hash = "sha256:2c0ba71445ee0932e510f1b0248f53b7a52926f1f78c93b868fcbe6536e61a1d"}, ] [[package]] name = "boto3" -version = "1.29.6" +version = "1.33.6" description = "The AWS SDK for Python" optional = false python-versions = ">= 3.7" files = [ - {file = "boto3-1.29.6-py3-none-any.whl", hash = "sha256:f4d19e01d176c3a5a05e4af733185ff1891b08a3c38d4a439800fa132aa6e9be"}, - {file = "boto3-1.29.6.tar.gz", hash = "sha256:d1d0d979a70bf9b0b13ae3b017f8523708ad953f62d16f39a602d67ee9b25554"}, + {file = "boto3-1.33.6-py3-none-any.whl", hash = "sha256:b88f0f305186c5fd41f168e006baa45b7002a33029aec8e5bef373237a172fca"}, + {file = "boto3-1.33.6.tar.gz", hash = "sha256:4f62fc1c7f3ea2d22917aa0aa07b86f119abd90bed3d815e4b52fb3d84773e15"}, ] [package.dependencies] -botocore = ">=1.32.6,<1.33.0" +botocore = ">=1.33.6,<1.34.0" jmespath = ">=0.7.1,<2.0.0" -s3transfer = ">=0.7.0,<0.8.0" +s3transfer = ">=0.8.2,<0.9.0" [package.extras] crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.32.6" +version = "1.33.6" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">= 3.7" files = [ - {file = "botocore-1.32.6-py3-none-any.whl", hash = "sha256:4454f967a4d1a01e3e6205c070455bc4e8fd53b5b0753221581ae679c55a9dfd"}, - {file = "botocore-1.32.6.tar.gz", hash = "sha256:ecec876103783b5efe6099762dda60c2af67e45f7c0ab4568e8265d11c6c449b"}, + {file = "botocore-1.33.6-py3-none-any.whl", hash = "sha256:14282cd432c0683770eee932c43c12bb9ad5730e23755204ad102897c996693a"}, + {file = "botocore-1.33.6.tar.gz", hash = "sha256:938056bab831829f90e09ecd70dd6b295afd52b1482f5582ee7a11d8243d9661"}, ] [package.dependencies] @@ -355,7 +344,7 @@ python-dateutil = ">=2.1,<3.0.0" urllib3 = {version = ">=1.25.4,<2.1", markers = "python_version >= \"3.10\""} [package.extras] -crt = ["awscrt (==0.19.12)"] +crt = ["awscrt (==0.19.17)"] [[package]] name = "cachetools" @@ -610,34 +599,34 @@ typeguard = ">=2.13.3,<2.14.0" [[package]] name = "cryptography" -version = "41.0.5" +version = "41.0.7" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = ">=3.7" files = [ - {file = "cryptography-41.0.5-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:da6a0ff8f1016ccc7477e6339e1d50ce5f59b88905585f77193ebd5068f1e797"}, - {file = "cryptography-41.0.5-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:b948e09fe5fb18517d99994184854ebd50b57248736fd4c720ad540560174ec5"}, - {file = "cryptography-41.0.5-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d38e6031e113b7421db1de0c1b1f7739564a88f1684c6b89234fbf6c11b75147"}, - {file = "cryptography-41.0.5-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e270c04f4d9b5671ebcc792b3ba5d4488bf7c42c3c241a3748e2599776f29696"}, - {file = "cryptography-41.0.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ec3b055ff8f1dce8e6ef28f626e0972981475173d7973d63f271b29c8a2897da"}, - {file = "cryptography-41.0.5-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:7d208c21e47940369accfc9e85f0de7693d9a5d843c2509b3846b2db170dfd20"}, - {file = "cryptography-41.0.5-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:8254962e6ba1f4d2090c44daf50a547cd5f0bf446dc658a8e5f8156cae0d8548"}, - {file = "cryptography-41.0.5-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:a48e74dad1fb349f3dc1d449ed88e0017d792997a7ad2ec9587ed17405667e6d"}, - {file = "cryptography-41.0.5-cp37-abi3-win32.whl", hash = "sha256:d3977f0e276f6f5bf245c403156673db103283266601405376f075c849a0b936"}, - {file = "cryptography-41.0.5-cp37-abi3-win_amd64.whl", hash = "sha256:73801ac9736741f220e20435f84ecec75ed70eda90f781a148f1bad546963d81"}, - {file = "cryptography-41.0.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3be3ca726e1572517d2bef99a818378bbcf7d7799d5372a46c79c29eb8d166c1"}, - {file = "cryptography-41.0.5-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e886098619d3815e0ad5790c973afeee2c0e6e04b4da90b88e6bd06e2a0b1b72"}, - {file = "cryptography-41.0.5-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:573eb7128cbca75f9157dcde974781209463ce56b5804983e11a1c462f0f4e88"}, - {file = "cryptography-41.0.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0c327cac00f082013c7c9fb6c46b7cc9fa3c288ca702c74773968173bda421bf"}, - {file = "cryptography-41.0.5-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:227ec057cd32a41c6651701abc0328135e472ed450f47c2766f23267b792a88e"}, - {file = "cryptography-41.0.5-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:22892cc830d8b2c89ea60148227631bb96a7da0c1b722f2aac8824b1b7c0b6b8"}, - {file = "cryptography-41.0.5-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:5a70187954ba7292c7876734183e810b728b4f3965fbe571421cb2434d279179"}, - {file = "cryptography-41.0.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:88417bff20162f635f24f849ab182b092697922088b477a7abd6664ddd82291d"}, - {file = "cryptography-41.0.5-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c707f7afd813478e2019ae32a7c49cd932dd60ab2d2a93e796f68236b7e1fbf1"}, - {file = "cryptography-41.0.5-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:580afc7b7216deeb87a098ef0674d6ee34ab55993140838b14c9b83312b37b86"}, - {file = "cryptography-41.0.5-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fba1e91467c65fe64a82c689dc6cf58151158993b13eb7a7f3f4b7f395636723"}, - {file = "cryptography-41.0.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:0d2a6a598847c46e3e321a7aef8af1436f11c27f1254933746304ff014664d84"}, - {file = "cryptography-41.0.5.tar.gz", hash = "sha256:392cb88b597247177172e02da6b7a63deeff1937fa6fec3bbf902ebd75d97ec7"}, + {file = "cryptography-41.0.7-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:3c78451b78313fa81607fa1b3f1ae0a5ddd8014c38a02d9db0616133987b9cdf"}, + {file = "cryptography-41.0.7-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:928258ba5d6f8ae644e764d0f996d61a8777559f72dfeb2eea7e2fe0ad6e782d"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a1b41bc97f1ad230a41657d9155113c7521953869ae57ac39ac7f1bb471469a"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:841df4caa01008bad253bce2a6f7b47f86dc9f08df4b433c404def869f590a15"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5429ec739a29df2e29e15d082f1d9ad683701f0ec7709ca479b3ff2708dae65a"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:43f2552a2378b44869fe8827aa19e69512e3245a219104438692385b0ee119d1"}, + {file = "cryptography-41.0.7-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:af03b32695b24d85a75d40e1ba39ffe7db7ffcb099fe507b39fd41a565f1b157"}, + {file = "cryptography-41.0.7-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:49f0805fc0b2ac8d4882dd52f4a3b935b210935d500b6b805f321addc8177406"}, + {file = "cryptography-41.0.7-cp37-abi3-win32.whl", hash = "sha256:f983596065a18a2183e7f79ab3fd4c475205b839e02cbc0efbbf9666c4b3083d"}, + {file = "cryptography-41.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:90452ba79b8788fa380dfb587cca692976ef4e757b194b093d845e8d99f612f2"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:079b85658ea2f59c4f43b70f8119a52414cdb7be34da5d019a77bf96d473b960"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:b640981bf64a3e978a56167594a0e97db71c89a479da8e175d8bb5be5178c003"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e3114da6d7f95d2dee7d3f4eec16dacff819740bbab931aff8648cb13c5ff5e7"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d5ec85080cce7b0513cfd233914eb8b7bbd0633f1d1703aa28d1dd5a72f678ec"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7a698cb1dac82c35fcf8fe3417a3aaba97de16a01ac914b89a0889d364d2f6be"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:37a138589b12069efb424220bf78eac59ca68b95696fc622b6ccc1c0a197204a"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:68a2dec79deebc5d26d617bfdf6e8aab065a4f34934b22d3b5010df3ba36612c"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:09616eeaef406f99046553b8a40fbf8b1e70795a91885ba4c96a70793de5504a"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:48a0476626da912a44cc078f9893f292f0b3e4c739caf289268168d8f4702a39"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c7f3201ec47d5207841402594f1d7950879ef890c0c495052fa62f58283fde1a"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c5ca78485a255e03c32b513f8c2bc39fedb7f5c5f8535545bdc223a03b24f248"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d6c391c021ab1f7a82da5d8d0b3cee2f4b2c455ec86c8aebbc84837a631ff309"}, + {file = "cryptography-41.0.7.tar.gz", hash = "sha256:13f93ce9bea8016c253b34afc6bd6a75993e5c40672ed5405a9c832f0d4a00bc"}, ] [package.dependencies] @@ -1022,13 +1011,13 @@ grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] [[package]] name = "google-api-python-client" -version = "2.108.0" +version = "2.109.0" description = "Google API Client Library for Python" optional = false python-versions = ">=3.7" files = [ - {file = "google-api-python-client-2.108.0.tar.gz", hash = "sha256:6396efca83185fb205c0abdbc1c2ee57b40475578c6af37f6d0e30a639aade99"}, - {file = "google_api_python_client-2.108.0-py2.py3-none-any.whl", hash = "sha256:9d1327213e388943ebcd7db5ce6e7f47987a7e6874e3e1f6116010eea4a0e75d"}, + {file = "google-api-python-client-2.109.0.tar.gz", hash = "sha256:d06390c25477c361d52639fe00ef912c3fab8dafc7fbf29580c1144e92523a79"}, + {file = "google_api_python_client-2.109.0-py2.py3-none-any.whl", hash = "sha256:72e7d46cc70908d808e29f16d983b441783fe56b694cec132db9af9fb991daa2"}, ] [package.dependencies] @@ -1040,13 +1029,13 @@ uritemplate = ">=3.0.1,<5" [[package]] name = "google-auth" -version = "2.23.4" +version = "2.24.0" description = "Google Authentication Library" optional = false python-versions = ">=3.7" files = [ - {file = "google-auth-2.23.4.tar.gz", hash = "sha256:79905d6b1652187def79d491d6e23d0cbb3a21d3c7ba0dbaa9c8a01906b13ff3"}, - {file = "google_auth-2.23.4-py2.py3-none-any.whl", hash = "sha256:d4bbc92fe4b8bfd2f3e8d88e5ba7085935da208ee38a134fc280e7ce682a05f2"}, + {file = "google-auth-2.24.0.tar.gz", hash = "sha256:2ec7b2a506989d7dbfdbe81cb8d0ead8876caaed14f86d29d34483cbe99c57af"}, + {file = "google_auth-2.24.0-py2.py3-none-any.whl", hash = "sha256:9b82d5c8d3479a5391ea0a46d81cca698d328459da31d4a459d4e901a5d927e0"}, ] [package.dependencies] @@ -1265,13 +1254,13 @@ pyparsing = {version = ">=2.4.2,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.0.2 || >3.0 [[package]] name = "idna" -version = "3.4" +version = "3.6" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.5" files = [ - {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, - {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, + {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, + {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, ] [[package]] @@ -1302,24 +1291,23 @@ files = [ [[package]] name = "ipython" -version = "8.17.2" +version = "8.18.1" description = "IPython: Productive Interactive Computing" optional = false python-versions = ">=3.9" files = [ - {file = "ipython-8.17.2-py3-none-any.whl", hash = "sha256:1e4d1d666a023e3c93585ba0d8e962867f7a111af322efff6b9c58062b3e5444"}, - {file = "ipython-8.17.2.tar.gz", hash = "sha256:126bb57e1895594bb0d91ea3090bbd39384f6fe87c3d57fd558d0670f50339bb"}, + {file = "ipython-8.18.1-py3-none-any.whl", hash = "sha256:e8267419d72d81955ec1177f8a29aaa90ac80ad647499201119e2f05e99aa397"}, + {file = "ipython-8.18.1.tar.gz", hash = "sha256:ca6f079bb33457c66e233e4580ebfc4128855b4cf6370dddd73842a9563e8a27"}, ] [package.dependencies] -appnope = {version = "*", markers = "sys_platform == \"darwin\""} colorama = {version = "*", markers = "sys_platform == \"win32\""} decorator = "*" exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} jedi = ">=0.16" matplotlib-inline = "*" pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} -prompt-toolkit = ">=3.0.30,<3.0.37 || >3.0.37,<3.1.0" +prompt-toolkit = ">=3.0.41,<3.1.0" pygments = ">=2.4.0" stack-data = "*" traitlets = ">=5" @@ -1447,16 +1435,6 @@ files = [ {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, @@ -1643,13 +1621,13 @@ testing = ["docopt", "pytest (<6.0.0)"] [[package]] name = "pexpect" -version = "4.8.0" +version = "4.9.0" description = "Pexpect allows easy control of interactive console applications." optional = false python-versions = "*" files = [ - {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"}, - {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"}, + {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, + {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, ] [package.dependencies] @@ -1737,7 +1715,6 @@ files = [ {file = "psycopg2_binary-2.9.9-cp311-cp311-win32.whl", hash = "sha256:dc4926288b2a3e9fd7b50dc6a1909a13bbdadfc67d93f3374d984e56f885579d"}, {file = "psycopg2_binary-2.9.9-cp311-cp311-win_amd64.whl", hash = "sha256:b76bedd166805480ab069612119ea636f5ab8f8771e640ae103e05a4aae3e417"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0605eaed3eb239e87df0d5e3c6489daae3f7388d455d0c0b4df899519c6a38d"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996"}, @@ -1746,8 +1723,6 @@ files = [ {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-win32.whl", hash = "sha256:64cf30263844fa208851ebb13b0732ce674d8ec6a0c86a4e160495d299ba3c93"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:81ff62668af011f9a48787564ab7eded4e9fb17a4a6a74af5ffa6a457400d2ab"}, {file = "psycopg2_binary-2.9.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2293b001e319ab0d869d660a704942c9e2cce19745262a8aba2115ef41a0a42a"}, {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03ef7df18daf2c4c07e2695e8cfd5ee7f748a1d54d802330985a78d2a5a6dca9"}, {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a602ea5aff39bb9fac6308e9c9d82b9a35c2bf288e184a816002c9fae930b77"}, @@ -2317,20 +2292,20 @@ pyasn1 = ">=0.1.3" [[package]] name = "s3transfer" -version = "0.7.0" +version = "0.8.2" description = "An Amazon S3 Transfer Manager" optional = false python-versions = ">= 3.7" files = [ - {file = "s3transfer-0.7.0-py3-none-any.whl", hash = "sha256:10d6923c6359175f264811ef4bf6161a3156ce8e350e705396a7557d6293c33a"}, - {file = "s3transfer-0.7.0.tar.gz", hash = "sha256:fd3889a66f5fe17299fe75b82eae6cf722554edca744ca5d5fe308b104883d2e"}, + {file = "s3transfer-0.8.2-py3-none-any.whl", hash = "sha256:c9e56cbe88b28d8e197cf841f1f0c130f246595e77ae5b5a05b69fe7cb83de76"}, + {file = "s3transfer-0.8.2.tar.gz", hash = "sha256:368ac6876a9e9ed91f6bc86581e319be08188dc60d50e0d56308ed5765446283"}, ] [package.dependencies] -botocore = ">=1.12.36,<2.0a.0" +botocore = ">=1.33.2,<2.0a.0" [package.extras] -crt = ["botocore[crt] (>=1.20.29,<2.0a.0)"] +crt = ["botocore[crt] (>=1.33.2,<2.0a.0)"] [[package]] name = "six" @@ -2413,7 +2388,7 @@ files = [ ] [package.dependencies] -greenlet = {version = "!=0.4.17", markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""} +greenlet = {version = "!=0.4.17", markers = "platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\""} typing-extensions = ">=4.2.0" [package.extras] @@ -2501,18 +2476,18 @@ files = [ [[package]] name = "traitlets" -version = "5.13.0" +version = "5.14.0" description = "Traitlets Python configuration system" optional = false python-versions = ">=3.8" files = [ - {file = "traitlets-5.13.0-py3-none-any.whl", hash = "sha256:baf991e61542da48fe8aef8b779a9ea0aa38d8a54166ee250d5af5ecf4486619"}, - {file = "traitlets-5.13.0.tar.gz", hash = "sha256:9b232b9430c8f57288c1024b34a8f0251ddcc47268927367a0dd3eeaca40deb5"}, + {file = "traitlets-5.14.0-py3-none-any.whl", hash = "sha256:f14949d23829023013c47df20b4a76ccd1a85effb786dc060f34de7948361b33"}, + {file = "traitlets-5.14.0.tar.gz", hash = "sha256:fcdaa8ac49c04dfa0ed3ee3384ef6dfdb5d6f3741502be247279407679296772"}, ] [package.extras] docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] -test = ["argcomplete (>=3.0.3)", "mypy (>=1.6.0)", "pre-commit", "pytest (>=7.0,<7.5)", "pytest-mock", "pytest-mypy-testing"] +test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<7.5)", "pytest-mock", "pytest-mypy-testing"] [[package]] name = "typeguard" @@ -2703,4 +2678,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "55b3aa802db4ae96d975c3b21fb336b7a4ed52230d867ac8d2d407f975da1cd9" +content-hash = "a689087884ad064f5e886f4e7a186b11875858b7d33eb144d19211dfd5c02913" diff --git a/pyproject.toml b/pyproject.toml index 9ff4f824..424c03cb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,10 +18,10 @@ pycryptodome = "^3.18.0" eth-account = "^0.9.0" pydantic="^2.3.0" bencodex = "^1.0.1" +pyjwt = "^2.8.0" [tool.poetry.group.iap.dependencies] # IAP API -pyjwt = "^2.8.0" cryptography = "^41.0.4" fastapi = "^0.103.1" uvicorn = "^0.23.2" From 51dc6fc95e83ede145ba502db181d51ffc2acb24 Mon Sep 17 00:00:00 2001 From: hyeon Date: Mon, 4 Dec 2023 18:09:58 +0900 Subject: [PATCH 14/46] Add missing schemas --- worker/worker/schemas/__init__.py | 0 worker/worker/schemas/aws.py | 33 +++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 worker/worker/schemas/__init__.py create mode 100644 worker/worker/schemas/aws.py diff --git a/worker/worker/schemas/__init__.py b/worker/worker/schemas/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/worker/worker/schemas/aws.py b/worker/worker/schemas/aws.py new file mode 100644 index 00000000..3cb9e2f1 --- /dev/null +++ b/worker/worker/schemas/aws.py @@ -0,0 +1,33 @@ +import json +from dataclasses import dataclass, fields +from typing import Union, List + + +@dataclass +class SQSMessageRecord: + messageId: str + receiptHandle: str + body: Union[dict, str] + attributes: dict + messageAttributes: dict + md5OfBody: str + eventSource: str + eventSourceARN: str + awsRegion: str + + # Avoid TypeError when init dataclass. https://stackoverflow.com/questions/54678337/how-does-one-ignore-extra-arguments-passed-to-a-dataclass # noqa + def __init__(self, **kwargs): + names = set([f.name for f in fields(self)]) + for k, v in kwargs.items(): + if k in names: + if k == 'body' and isinstance(v, str): + v = json.loads(v) + setattr(self, k, v) + + +@dataclass +class SQSMessage: + Records: Union[List[SQSMessageRecord], dict] + + def __post_init__(self): + self.Records = [SQSMessageRecord(**x) for x in self.Records] From 38e8b680eb9517ba9a4253ca75c56ecf83979c51 Mon Sep 17 00:00:00 2001 From: hyeon Date: Mon, 4 Dec 2023 23:42:06 +0900 Subject: [PATCH 15/46] Set env. --- worker/worker_cdk_stack.py | 1 + 1 file changed, 1 insertion(+) diff --git a/worker/worker_cdk_stack.py b/worker/worker_cdk_stack.py index a8ed1d72..802c3b95 100644 --- a/worker/worker_cdk_stack.py +++ b/worker/worker_cdk_stack.py @@ -174,6 +174,7 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: code=_lambda.AssetCode("worker/worker", exclude=exclude_list), handler="voucher.handle", layers=[layer], + environment=env, role=role, vpc=shared_stack.vpc, timeout=cdk_core.Duration.seconds(30), From 2d99388fc2a751150b1ddc89044dc6f27bef65b3 Mon Sep 17 00:00:00 2001 From: hyeon Date: Mon, 4 Dec 2023 23:49:48 +0900 Subject: [PATCH 16/46] Not begin transaction to use session as a session --- worker/worker/voucher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worker/worker/voucher.py b/worker/worker/voucher.py index 98bde73c..79eb48f3 100644 --- a/worker/worker/voucher.py +++ b/worker/worker/voucher.py @@ -66,7 +66,7 @@ def handle(event, context): message = SQSMessage(Records=event.get("Records", {})) logger.info(f"SQS Message: {message}") - with scoped_session(sessionmaker(bind=engine)).begin() as sess: + with scoped_session(sessionmaker(bind=engine)) as sess: uuid_list = [x.body.get("uuid") for x in message.Records if x.body.get("uuid")] voucher_list = sess.scalars(select(VoucherRequest.uuid).where(VoucherRequest.uuid.in_(uuid_list))).fetchall() target_message_list = [x.body for x in message.Records if From 26b50efa9c630d643f754c40a28dc8427a0f1d2e Mon Sep 17 00:00:00 2001 From: hyeon Date: Mon, 4 Dec 2023 23:57:01 +0900 Subject: [PATCH 17/46] Fix wrong permission and token --- worker/worker/voucher.py | 2 +- worker/worker_cdk_stack.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/worker/worker/voucher.py b/worker/worker/voucher.py index 98bde73c..009c4e8a 100644 --- a/worker/worker/voucher.py +++ b/worker/worker/voucher.py @@ -17,7 +17,7 @@ VOUCHER_URL = os.environ.get("VOUCHER_URL") VOUCHER_JWT_SECRET = fetch_parameter( os.environ.get("REGION_NAME"), - f"{os.environ.get('STAGE')}_9c_IAP_SEASON_PASS_JWT_SECRET", + f"{os.environ.get('STAGE')}_9c_IAP_VOUCHER_JWT_SECRET", True )["Value"] diff --git a/worker/worker_cdk_stack.py b/worker/worker_cdk_stack.py index 802c3b95..7384e41b 100644 --- a/worker/worker_cdk_stack.py +++ b/worker/worker_cdk_stack.py @@ -70,6 +70,7 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: shared_stack.google_credential_arn, shared_stack.apple_credential_arn, shared_stack.kms_key_id_arn, + shared_stack.voucher_jwt_secret_arn, ] ) ) From ec567dd64b155dc142270766486138e53ff5ae87 Mon Sep 17 00:00:00 2001 From: hyeon Date: Mon, 4 Dec 2023 23:57:01 +0900 Subject: [PATCH 18/46] Fix wrong permission and token --- worker/worker/voucher.py | 2 +- worker/worker_cdk_stack.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/worker/worker/voucher.py b/worker/worker/voucher.py index 98bde73c..009c4e8a 100644 --- a/worker/worker/voucher.py +++ b/worker/worker/voucher.py @@ -17,7 +17,7 @@ VOUCHER_URL = os.environ.get("VOUCHER_URL") VOUCHER_JWT_SECRET = fetch_parameter( os.environ.get("REGION_NAME"), - f"{os.environ.get('STAGE')}_9c_IAP_SEASON_PASS_JWT_SECRET", + f"{os.environ.get('STAGE')}_9c_IAP_VOUCHER_JWT_SECRET", True )["Value"] diff --git a/worker/worker_cdk_stack.py b/worker/worker_cdk_stack.py index 802c3b95..7384e41b 100644 --- a/worker/worker_cdk_stack.py +++ b/worker/worker_cdk_stack.py @@ -70,6 +70,7 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: shared_stack.google_credential_arn, shared_stack.apple_credential_arn, shared_stack.kms_key_id_arn, + shared_stack.voucher_jwt_secret_arn, ] ) ) From c600ddabe0e0162dd353abaed986405b22b1e98f Mon Sep 17 00:00:00 2001 From: hyeon Date: Tue, 5 Dec 2023 00:34:18 +0900 Subject: [PATCH 19/46] Add mapping type to relationship --- common/models/voucher.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/common/models/voucher.py b/common/models/voucher.py index bff755df..a3485fd2 100644 --- a/common/models/voucher.py +++ b/common/models/voucher.py @@ -1,16 +1,19 @@ import uuid from sqlalchemy import Column, Text, Integer, ForeignKey, UUID, LargeBinary -from sqlalchemy.orm import relationship, backref +from sqlalchemy.orm import relationship, backref, Mapped from common.models.base import Base, TimeStampMixin, AutoIdMixin +from common.models.product import Product +from common.models.receipt import Receipt from common.utils.receipt import PlanetID class VoucherRequest(AutoIdMixin, TimeStampMixin, Base): __tablename__ = "voucher_request" receipt_id = Column(Integer, ForeignKey("receipt.id"), nullable=False) - receipt = relationship("Receipt", foreign_keys=[receipt_id], uselist=False, backref=backref("voucher_request")) + receipt: Mapped["Receipt"] = relationship("Receipt", foreign_keys=[receipt_id], uselist=False, + backref=backref("voucher_request")) # Copy all required data to view all info solely with this table uuid = Column(UUID(as_uuid=True), nullable=False, index=True, default=uuid.uuid4, @@ -20,7 +23,7 @@ class VoucherRequest(AutoIdMixin, TimeStampMixin, Base): planet_id = Column(LargeBinary(length=12), nullable=False, default=PlanetID.ODIN.value, doc="An identifier of planets") product_id = Column(Integer, ForeignKey("product.id"), nullable=False) - product = relationship("Product", foreign_keys=[product_id], uselist=False) + product: Mapped["Product"] = relationship("Product", foreign_keys=[product_id], uselist=False) product_name = Column(Text, nullable=False) # Voucher request result From 437c6bddd7d5930dbdd9595eda14ad95a2922115 Mon Sep 17 00:00:00 2001 From: hyeon Date: Tue, 5 Dec 2023 00:34:32 +0900 Subject: [PATCH 20/46] Send receipt_id to voucher_request --- iap/api/purchase.py | 1 + 1 file changed, 1 insertion(+) diff --git a/iap/api/purchase.py b/iap/api/purchase.py index 17ca9a7b..1ddc627d 100644 --- a/iap/api/purchase.py +++ b/iap/api/purchase.py @@ -167,6 +167,7 @@ def request_product(receipt_data: ReceiptSchema, sess=Depends(session)): MessageBody=json.dumps({ "id": receipt.id, "uuid": str(receipt.uuid), + "receipt_id": receipt.id, "product_id": receipt.product_id, "product_name": receipt.product.name, "agent_addr": receipt.agent_addr, From 90e5606550d4accc497d6662c5c59ac52168b465 Mon Sep 17 00:00:00 2001 From: hyeon Date: Tue, 5 Dec 2023 00:34:51 +0900 Subject: [PATCH 21/46] Handle session in old-fashioned way --- worker/worker/voucher.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/worker/worker/voucher.py b/worker/worker/voucher.py index 009c4e8a..b0a452e7 100644 --- a/worker/worker/voucher.py +++ b/worker/worker/voucher.py @@ -66,7 +66,8 @@ def handle(event, context): message = SQSMessage(Records=event.get("Records", {})) logger.info(f"SQS Message: {message}") - with scoped_session(sessionmaker(bind=engine)).begin() as sess: + sess = scoped_session(sessionmaker(bind=engine)) + try: uuid_list = [x.body.get("uuid") for x in message.Records if x.body.get("uuid")] voucher_list = sess.scalars(select(VoucherRequest.uuid).where(VoucherRequest.uuid.in_(uuid_list))).fetchall() target_message_list = [x.body for x in message.Records if @@ -78,3 +79,6 @@ def handle(event, context): sess.commit() sess.refresh(voucher) request(sess, voucher) + finally: + if sess is not None: + sess.close() From c82c6b184c486ca5e3419842992019951e8a5856 Mon Sep 17 00:00:00 2001 From: hyeon Date: Tue, 5 Dec 2023 00:35:04 +0900 Subject: [PATCH 22/46] Type casting planet_id --- worker/worker/voucher.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/worker/worker/voucher.py b/worker/worker/voucher.py index b0a452e7..c1822028 100644 --- a/worker/worker/voucher.py +++ b/worker/worker/voucher.py @@ -9,6 +9,7 @@ from common import logger from common.models.voucher import VoucherRequest from common.utils.aws import fetch_secrets, fetch_parameter +from common.utils.receipt import PlanetID from schemas.aws import SQSMessage DB_URI = os.environ.get("DB_URI") @@ -75,6 +76,7 @@ def handle(event, context): for msg in target_message_list: voucher = VoucherRequest(**msg) + voucher.planet_id = PlanetID(voucher.planet_id.encode()) sess.add(voucher) sess.commit() sess.refresh(voucher) From 95d4c6c998802d36d1d7fda71be0d2fd132816df Mon Sep 17 00:00:00 2001 From: hyeon Date: Tue, 5 Dec 2023 01:08:23 +0900 Subject: [PATCH 23/46] Add voucher request url --- .github/workflows/deploy.yml | 4 ++++ .github/workflows/main.yml | 3 +++ .github/workflows/synth.yml | 3 +++ common/__init__.py | 1 + worker/worker_cdk_stack.py | 5 ++++- 5 files changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 97a5d4ed..ab5d158a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -41,6 +41,8 @@ on: required: true SEASON_PASS_JWT_SECRET: required: true + VOUCHER_URL: + required: true VOUCHER_JWT_SECRET: required: true BRIDGE_DATA: @@ -139,6 +141,7 @@ jobs: CDN_HOST: ${{ vars.CDN_HOST }} PLANET_URL: ${{ vars.PLANET_URL }} SEASON_PASS_JWT_SECRET: ${{ secrets.SEASON_PASS_JWT_SECRET }} + VOUCHER_URL: ${{ secrets.VOUCHER_URL }} VOUCHER_JWT_SECRET: ${{ secrets.VOUCHER_JWT_SECRET }} BRIDGE_DATA: ${{ secrets.BRIDGE_DATA }} run: | @@ -169,6 +172,7 @@ jobs: CDN_HOST: ${{ vars.CDN_HOST }} PLANET_URL: ${{ vars.PLANET_URL }} SEASON_PASS_JWT_SECRET: ${{ secrets.SEASON_PASS_JWT_SECRET }} + VOUCHER_URL: ${{ secrets.VOUCHER_URL }} VOUCHER_JWT_SECRET: ${{ secrets.VOUCHER_JWT_SECRET }} BRIDGE_DATA: ${{ secrets.BRIDGE_DATA }} run: | diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7effb838..aff10c4b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -45,6 +45,7 @@ jobs: GOLDEN_DUST_REQUEST_SHEET_ID: ${{ secrets.GOLDEN_DUST_REQUEST_SHEET_ID }} GOLDEN_DUST_WORK_SHEET_ID: ${{ secrets.GOLDEN_DUST_WORK_SHEET_ID }} SEASON_PASS_JWT_SECRET: ${{ secrets.SEASON_PASS_JWT_SECRET }} + VOUCHER_URL: ${{ secrets.VOUCHER_URL }} VOUCHER_JWT_SECRET: ${{ secrets.VOUCHER_JWT_SECRET }} BRIDGE_DATA: ${{ secrets.BRIDGE_DATA }} @@ -69,6 +70,7 @@ jobs: GOLDEN_DUST_REQUEST_SHEET_ID: ${{ secrets.GOLDEN_DUST_REQUEST_SHEET_ID }} GOLDEN_DUST_WORK_SHEET_ID: ${{ secrets.GOLDEN_DUST_WORK_SHEET_ID }} SEASON_PASS_JWT_SECRET: ${{ secrets.SEASON_PASS_JWT_SECRET }} + VOUCHER_URL: ${{ secrets.VOUCHER_URL }} VOUCHER_JWT_SECRET: ${{ secrets.VOUCHER_JWT_SECRET }} BRIDGE_DATA: ${{ secrets.BRIDGE_DATA }} @@ -105,5 +107,6 @@ jobs: GOLDEN_DUST_REQUEST_SHEET_ID: ${{ secrets.GOLDEN_DUST_REQUEST_SHEET_ID }} GOLDEN_DUST_WORK_SHEET_ID: ${{ secrets.GOLDEN_DUST_WORK_SHEET_ID }} SEASON_PASS_JWT_SECRET: ${{ secrets.SEASON_PASS_JWT_SECRET }} + VOUCHER_URL: ${{ secrets.VOUCHER_URL }} VOUCHER_JWT_SECRET: ${{ secrets.VOUCHER_JWT_SECRET }} BRIDGE_DATA: ${{ secrets.BRIDGE_DATA }} diff --git a/.github/workflows/synth.yml b/.github/workflows/synth.yml index ebd0a9a9..22285080 100644 --- a/.github/workflows/synth.yml +++ b/.github/workflows/synth.yml @@ -36,6 +36,8 @@ on: required: true SEASON_PASS_JWT_SECRET: required: true + VOUCHER_URL: + required: true VOUCHER_JWT_SECRET: required: true BRIDGE_DATA: @@ -126,6 +128,7 @@ jobs: CDN_HOST: ${{ vars.CDN_HOST }} PLANET_URL: ${{ vars.PLANET_URL }} SEASON_PASS_JWT_SECRET: ${{ secrets.SEASON_PASS_JWT_SECRET }} + VOUCHER_URL: ${{ secrets.VOUCHER_URL }} VOUCHER_JWT_SECRET: ${{ secrets.VOUCHER_JWT_SECRET }} BRIDGE_DATA: ${{ secrets.BRIDGE_DATA }} run: | diff --git a/common/__init__.py b/common/__init__.py index 687c83d2..4ff7df59 100644 --- a/common/__init__.py +++ b/common/__init__.py @@ -62,4 +62,5 @@ class Config: season_pass_jwt_secret: str = None # Voucher + voucher_url: str = None voucher_jwt_secret: str = None diff --git a/worker/worker_cdk_stack.py b/worker/worker_cdk_stack.py index 7384e41b..4531b05d 100644 --- a/worker/worker_cdk_stack.py +++ b/worker/worker_cdk_stack.py @@ -1,4 +1,5 @@ import os +from copy import deepcopy import aws_cdk as cdk_core import boto3 @@ -167,6 +168,8 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: hourly_event_rule.add_target(_event_targets.LambdaFunction(status_monitor)) # IAP Voucher + voucher_env = deepcopy(env) + voucher_env["VOUCHER_URL"] = config.voucher_url voucher_handler = _lambda.Function( self, f"{config.stage}-9c-iap-voucher-handler-function", function_name=f"{config.stage}-9c-iap-voucher-handler", @@ -175,7 +178,7 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: code=_lambda.AssetCode("worker/worker", exclude=exclude_list), handler="voucher.handle", layers=[layer], - environment=env, + environment=voucher_env, role=role, vpc=shared_stack.vpc, timeout=cdk_core.Duration.seconds(30), From 910c2523cae0dd2c51990e09bb684f96ea78c591 Mon Sep 17 00:00:00 2001 From: hyeon Date: Tue, 5 Dec 2023 01:08:56 +0900 Subject: [PATCH 24/46] Name id is for VoucherRequest.id that is auto index --- iap/api/purchase.py | 1 - 1 file changed, 1 deletion(-) diff --git a/iap/api/purchase.py b/iap/api/purchase.py index 1ddc627d..9806b3f8 100644 --- a/iap/api/purchase.py +++ b/iap/api/purchase.py @@ -165,7 +165,6 @@ def request_product(receipt_data: ReceiptSchema, sess=Depends(session)): logger.info(f"Send voucher request: {receipt.uuid}") resp = sqs.send_message(QueueUrl=VOUCHER_SQS_URL, MessageBody=json.dumps({ - "id": receipt.id, "uuid": str(receipt.uuid), "receipt_id": receipt.id, "product_id": receipt.product_id, From 110459b4c9d8898abfc9e7e9284ab1984920a5a8 Mon Sep 17 00:00:00 2001 From: hyeon Date: Tue, 5 Dec 2023 01:09:14 +0900 Subject: [PATCH 25/46] Type cast to json serialize --- worker/worker/voucher.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worker/worker/voucher.py b/worker/worker/voucher.py index c1822028..10903743 100644 --- a/worker/worker/voucher.py +++ b/worker/worker/voucher.py @@ -39,10 +39,10 @@ def request(sess, voucher: VoucherRequest) -> bool: VOUCHER_URL, headers={"Authorization": f"Bearer {get_voucher_token()}"}, json={ - "planetId": voucher.planet_id, + "planetId": voucher.planet_id.decode(), "agentAddress": voucher.agent_addr, # "avatarAddress": voucher.avatar_addr, - "iapUuid": voucher.uuid, + "iapUuid": str(voucher.uuid), "productId": voucher.product_id, "productName": voucher.product.name, }, From 5cae155aa1fc55fa8dc586dfea4f964f3b8b2d16 Mon Sep 17 00:00:00 2001 From: hyeon Date: Tue, 5 Dec 2023 01:21:02 +0900 Subject: [PATCH 26/46] Add unique constraint to VoucherRequest.receipt_id to avoid duplication --- .../2822c456abc3_add_unique_constraint.py | 28 +++++++++++++++++++ common/models/voucher.py | 2 +- worker/worker/voucher.py | 18 ++++++++---- 3 files changed, 42 insertions(+), 6 deletions(-) create mode 100644 common/alembic/versions/2822c456abc3_add_unique_constraint.py diff --git a/common/alembic/versions/2822c456abc3_add_unique_constraint.py b/common/alembic/versions/2822c456abc3_add_unique_constraint.py new file mode 100644 index 00000000..8a5f1042 --- /dev/null +++ b/common/alembic/versions/2822c456abc3_add_unique_constraint.py @@ -0,0 +1,28 @@ +"""Add unique constraint + +Revision ID: 2822c456abc3 +Revises: 5bf8ab4f3b13 +Create Date: 2023-12-05 01:11:38.405536 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = '2822c456abc3' +down_revision = '5bf8ab4f3b13' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_unique_constraint("voucher_request_receipt_id_unique", 'voucher_request', ['receipt_id']) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint("voucher_request_receipt_id_unique", 'voucher_request', type_='unique') + # ### end Alembic commands ### diff --git a/common/models/voucher.py b/common/models/voucher.py index a3485fd2..dd4641f7 100644 --- a/common/models/voucher.py +++ b/common/models/voucher.py @@ -11,7 +11,7 @@ class VoucherRequest(AutoIdMixin, TimeStampMixin, Base): __tablename__ = "voucher_request" - receipt_id = Column(Integer, ForeignKey("receipt.id"), nullable=False) + receipt_id = Column(Integer, ForeignKey("receipt.id"), nullable=False, unique=True) receipt: Mapped["Receipt"] = relationship("Receipt", foreign_keys=[receipt_id], uselist=False, backref=backref("voucher_request")) diff --git a/worker/worker/voucher.py b/worker/worker/voucher.py index 10903743..42f1909a 100644 --- a/worker/worker/voucher.py +++ b/worker/worker/voucher.py @@ -1,9 +1,11 @@ import os from datetime import datetime, timezone, timedelta +from typing import Tuple import jwt import requests from sqlalchemy import create_engine, select +from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import sessionmaker, scoped_session from common import logger @@ -34,7 +36,7 @@ def get_voucher_token() -> str: return jwt.encode(data, VOUCHER_JWT_SECRET) -def request(sess, voucher: VoucherRequest) -> bool: +def request(sess, voucher: VoucherRequest) -> Tuple[bool, VoucherRequest]: resp = requests.post( VOUCHER_URL, headers={"Authorization": f"Bearer {get_voucher_token()}"}, @@ -60,7 +62,7 @@ def request(sess, voucher: VoucherRequest) -> bool: sess.commit() sess.refresh(voucher) - return success + return success, voucher def handle(event, context): @@ -78,9 +80,15 @@ def handle(event, context): voucher = VoucherRequest(**msg) voucher.planet_id = PlanetID(voucher.planet_id.encode()) sess.add(voucher) - sess.commit() - sess.refresh(voucher) - request(sess, voucher) + try: + sess.commit() + sess.refresh(voucher) + except IntegrityError: + logger.warning(f"Receipt {voucher.uuid} is duplicated. Skip.") + sess.rollback() + else: + success, voucher = request(sess, voucher) + logger.info(f"Voucher request for {voucher.uuid} :: {success} :: {voucher.message}") finally: if sess is not None: sess.close() From a9e89937411a758cd863d77c4fea2859dedbe65b Mon Sep 17 00:00:00 2001 From: hyeon Date: Tue, 5 Dec 2023 01:21:49 +0900 Subject: [PATCH 27/46] API accepts string type for product_id --- worker/worker/voucher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worker/worker/voucher.py b/worker/worker/voucher.py index 42f1909a..a3551169 100644 --- a/worker/worker/voucher.py +++ b/worker/worker/voucher.py @@ -45,7 +45,7 @@ def request(sess, voucher: VoucherRequest) -> Tuple[bool, VoucherRequest]: "agentAddress": voucher.agent_addr, # "avatarAddress": voucher.avatar_addr, "iapUuid": str(voucher.uuid), - "productId": voucher.product_id, + "productId": str(voucher.product_id), "productName": voucher.product.name, }, ) From 9b8fcab54d5d3fd3b0ec7b36b6c6eaccdcb4cae9 Mon Sep 17 00:00:00 2001 From: hyeon Date: Tue, 5 Dec 2023 01:54:38 +0900 Subject: [PATCH 28/46] Set webhook urls to send slack message --- .github/workflows/deploy.yml | 4 ++++ .github/workflows/main.yml | 3 +++ .github/workflows/synth.yml | 3 +++ common/__init__.py | 4 ++++ worker/worker_cdk_stack.py | 6 +++++- 5 files changed, 19 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 97a5d4ed..8cd4b19f 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -35,6 +35,8 @@ on: required: true IAP_GARAGE_WEBHOOK_URL: required: true + IAP_ALERT_WEBHOOK_URL: + required: true GOLDEN_DUST_REQUEST_SHEET_ID: required: true GOLDEN_DUST_WORK_SHEET_ID: @@ -133,6 +135,7 @@ jobs: APPLE_KEY_ID: ${{ secrets.APPLE_KEY_ID }} APPLE_ISSUER_ID: ${{ secrets.APPLE_ISSUER_ID }} IAP_GARAGE_WEBHOOK_URL: ${{ secrets.IAP_GARAGE_WEBHOOK_URL }} + IAP_ALERT_WEBHOOK_URL: ${{ secrets.IAP_ALERT_WEBHOOK_URL }} GOLDEN_DUST_REQUEST_SHEET_ID: ${{ secrets.GOLDEN_DUST_REQUEST_SHEET_ID }} GOLDEN_DUST_WORK_SHEET_ID: ${{ secrets.GOLDEN_DUST_WORK_SHEET_ID }} FORM_SHEET: ${{ vars.FORM_SHEET }} @@ -163,6 +166,7 @@ jobs: APPLE_KEY_ID: ${{ secrets.APPLE_KEY_ID }} APPLE_ISSUER_ID: ${{ secrets.APPLE_ISSUER_ID }} IAP_GARAGE_WEBHOOK_URL: ${{ secrets.IAP_GARAGE_WEBHOOK_URL }} + IAP_ALERT_WEBHOOK_URL: ${{ secrets.IAP_ALERT_WEBHOOK_URL }} GOLDEN_DUST_REQUEST_SHEET_ID: ${{ secrets.GOLDEN_DUST_REQUEST_SHEET_ID }} GOLDEN_DUST_WORK_SHEET_ID: ${{ secrets.GOLDEN_DUST_WORK_SHEET_ID }} FORM_SHEET: ${{ vars.FORM_SHEET }} diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7effb838..2b879b1f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -42,6 +42,7 @@ jobs: APPLE_ISSUER_ID: ${{ secrets.APPLE_ISSUER_ID }} SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} IAP_GARAGE_WEBHOOK_URL: ${{ secrets.IAP_GARAGE_WEBHOOK_URL }} + IAP_ALERT_WEBHOOK_URL: ${{ secrets.IAP_ALERT_WEBHOOK_URL }} GOLDEN_DUST_REQUEST_SHEET_ID: ${{ secrets.GOLDEN_DUST_REQUEST_SHEET_ID }} GOLDEN_DUST_WORK_SHEET_ID: ${{ secrets.GOLDEN_DUST_WORK_SHEET_ID }} SEASON_PASS_JWT_SECRET: ${{ secrets.SEASON_PASS_JWT_SECRET }} @@ -66,6 +67,7 @@ jobs: APPLE_ISSUER_ID: ${{ secrets.APPLE_ISSUER_ID }} SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} IAP_GARAGE_WEBHOOK_URL: ${{ secrets.IAP_GARAGE_WEBHOOK_URL }} + IAP_ALERT_WEBHOOK_URL: ${{ secrets.IAP_ALERT_WEBHOOK_URL }} GOLDEN_DUST_REQUEST_SHEET_ID: ${{ secrets.GOLDEN_DUST_REQUEST_SHEET_ID }} GOLDEN_DUST_WORK_SHEET_ID: ${{ secrets.GOLDEN_DUST_WORK_SHEET_ID }} SEASON_PASS_JWT_SECRET: ${{ secrets.SEASON_PASS_JWT_SECRET }} @@ -102,6 +104,7 @@ jobs: APPLE_ISSUER_ID: ${{ secrets.APPLE_ISSUER_ID }} SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} IAP_GARAGE_WEBHOOK_URL: ${{ secrets.IAP_GARAGE_WEBHOOK_URL }} + IAP_ALERT_WEBHOOK_URL: ${{ secrets.IAP_ALERT_WEBHOOK_URL }} GOLDEN_DUST_REQUEST_SHEET_ID: ${{ secrets.GOLDEN_DUST_REQUEST_SHEET_ID }} GOLDEN_DUST_WORK_SHEET_ID: ${{ secrets.GOLDEN_DUST_WORK_SHEET_ID }} SEASON_PASS_JWT_SECRET: ${{ secrets.SEASON_PASS_JWT_SECRET }} diff --git a/.github/workflows/synth.yml b/.github/workflows/synth.yml index ebd0a9a9..94169ee2 100644 --- a/.github/workflows/synth.yml +++ b/.github/workflows/synth.yml @@ -30,6 +30,8 @@ on: required: true IAP_GARAGE_WEBHOOK_URL: required: true + IAP_ALERT_WEBHOOK_URL: + required: true GOLDEN_DUST_REQUEST_SHEET_ID: required: true GOLDEN_DUST_WORK_SHEET_ID: @@ -120,6 +122,7 @@ jobs: APPLE_KEY_ID: ${{ secrets.APPLE_KEY_ID }} APPLE_ISSUER_ID: ${{ secrets.APPLE_ISSUER_ID }} IAP_GARAGE_WEBHOOK_URL: ${{ secrets.IAP_GARAGE_WEBHOOK_URL }} + IAP_ALERT_WEBHOOK_URL: ${{ secrets.IAP_ALERT_WEBHOOK_URL }} GOLDEN_DUST_REQUEST_SHEET_ID: ${{ secrets.GOLDEN_DUST_REQUEST_SHEET_ID }} GOLDEN_DUST_WORK_SHEET_ID: ${{ secrets.GOLDEN_DUST_WORK_SHEET_ID }} FORM_SHEET: ${{ vars.FORM_SHEET }} diff --git a/common/__init__.py b/common/__init__.py index 687c83d2..45de2326 100644 --- a/common/__init__.py +++ b/common/__init__.py @@ -58,6 +58,10 @@ class Config: golden_dust_work_sheet_id: Optional[str] = None form_sheet: Optional[str] = None + # Status monitor + iap_garage_webhook_url: str = None + iap_alert_webhook_url: str = None + # SeasonPass season_pass_jwt_secret: str = None diff --git a/worker/worker_cdk_stack.py b/worker/worker_cdk_stack.py index 802c3b95..c3329b6c 100644 --- a/worker/worker_cdk_stack.py +++ b/worker/worker_cdk_stack.py @@ -1,4 +1,5 @@ import os +from copy import deepcopy import aws_cdk as cdk_core import boto3 @@ -139,6 +140,9 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: minute_event_rule.add_target(_event_targets.LambdaFunction(tracker)) # IAP Status Monitor + monitor_env = deepcopy(env) + monitor_env["IAP_GARAGE_WEBHOOK_URL"] = config.iap_garage_webhook_url + monitor_env["IAP_ALERT_WEBHOOK_URL"] = config.iap_alert_webhook_url status_monitor = _lambda.Function( self, f"{config.stage}-9c-iap-status-monitor-function", function_name=f"{config.stage}-9c-iap-status-monitor", @@ -146,7 +150,7 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: runtime=_lambda.Runtime.PYTHON_3_10, code=_lambda.AssetCode("worker/worker", exclude=exclude_list), handler="status_monitor.handle", - environment=env, + environment=monitor_env, layers=[layer], role=role, vpc=shared_stack.vpc, From f310594bbf17636091885d1125094ccf6ef5d7d6 Mon Sep 17 00:00:00 2001 From: hyeon Date: Tue, 5 Dec 2023 01:55:10 +0900 Subject: [PATCH 29/46] Handle session in well-known way --- worker/worker/status_monitor.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/worker/worker/status_monitor.py b/worker/worker/status_monitor.py index f1dcb7af..6fc70bb7 100644 --- a/worker/worker/status_monitor.py +++ b/worker/worker/status_monitor.py @@ -153,14 +153,17 @@ def check_garage(): def handle(event, context): - session = scoped_session(sessionmaker(bind=engine)) + sess = scoped_session(sessionmaker(bind=engine)) if datetime.utcnow().hour == 3 and datetime.now().minute == 0: # 12:00 KST check_garage() - with session.begin() as sess: + try: check_invalid_receipt(sess) check_halt_tx(sess) check_tx_failure(sess) + finally: + if sess is not None: + sess.close() if __name__ == "__main__": From 074859e4f96aa458b268691a8281a986a76f3164 Mon Sep 17 00:00:00 2001 From: hyeon Date: Wed, 29 Nov 2023 21:24:31 +0900 Subject: [PATCH 30/46] Split ticker with dunder to keep rune type tickers --- iap/schemas/product.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iap/schemas/product.py b/iap/schemas/product.py index a7ac1d6a..f02c3ac0 100644 --- a/iap/schemas/product.py +++ b/iap/schemas/product.py @@ -35,7 +35,7 @@ class FungibleAssetValueSchema(BaseSchema): @model_validator(mode="after") def make_ticker_to_name(self): - self.ticker = self.ticker.split("_")[-1] + self.ticker = self.ticker.split("__")[-1] return self class Config: From 1c72ff2600565ba8be5ac0d4eb3121c8bcfdf61b Mon Sep 17 00:00:00 2001 From: hyeon Date: Mon, 4 Dec 2023 17:59:26 +0900 Subject: [PATCH 31/46] Skip fixation-needed tests --- tests/test_image_link.py | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/tests/test_image_link.py b/tests/test_image_link.py index 999fbe08..61c07496 100644 --- a/tests/test_image_link.py +++ b/tests/test_image_link.py @@ -1,5 +1,3 @@ -import os - import pytest import requests from sqlalchemy import select @@ -14,8 +12,6 @@ "mainnet": "https://d3q2guojv29gp2.cloudfront.net", } -HOST = HOST_DICT[os.environ.get("STAGE")] - LANG_DICT = { "English": "EN", # "Korean": "KO", @@ -24,36 +20,48 @@ } +@pytest.mark.skip("Need to be fixed") @pytest.mark.usefixtures("session") -def test_category(session): +@pytest.mark.parametrize("stage", ["internal", "mainnet"]) +def test_category(session, stage): + host = HOST_DICT[stage] category_list = session.scalars(select(Category)).fetchall() for category in category_list: - resp = requests.get(f"{HOST}/{category.path}") + resp = requests.get(f"{host}/{category.path}") if resp.status_code != 200: print(f"Category image path {category.path} for category ID {category.id} not found.") +@pytest.mark.skip("Need to be fixed") @pytest.mark.usefixtures("session") -def test_image_list(session): +@pytest.mark.parametrize("stage", ["internal", "mainnet"]) +def test_image_list(session, stage): + host = HOST_DICT[stage] product_list = session.scalars(select(Product)).fetchall() for product in product_list: - resp = requests.get(f"{HOST}/{product.path}") + resp = requests.get(f"{host}/{product.path}") if resp.status_code != 200: print(f"Product list image path {product.path} for product ID {product.id}:{product.name} not found.") -def test_image_bg(): +@pytest.mark.skip("Need to be fixed") +@pytest.mark.parametrize("stage", ["internal", "mainnet"]) +def test_image_bg(stage): + host = HOST_DICT[stage] for rarity in ProductRarity: for size in (ProductAssetUISize.ONE_BY_ONE, ProductAssetUISize.ONE_BY_TWO): target_path = f"shop/images/product/list/bg_{rarity.value}_{size.value}.png" - resp = requests.get(f"{HOST}/{target_path}") + resp = requests.get(f"{host}/{target_path}") if resp.status_code != 200: print(f"BG Image Path {target_path} for Rarity {rarity} && Size {size} not found") -def test_image_detail(): +@pytest.mark.skip("Need to be fixed") +@pytest.mark.parametrize("stage", ["internal", "mainnet"]) +def test_image_detail(stage): + host = HOST_DICT[stage] product_csv_path = "shop/l10n/product.csv" - csv_resp = requests.get(f"{HOST}/{product_csv_path}") + csv_resp = requests.get(f"{host}/{product_csv_path}") header, *body = [x.split(",") for x in csv_resp.text.split("\n")] data = [dict(zip(header, b)) for b in body] for d in data: @@ -63,8 +71,6 @@ def test_image_detail(): for lang, code in LANG_DICT.items(): target_path = d[lang] assert code in target_path - resp = requests.get(f"{HOST}/{target_path}") + resp = requests.get(f"{host}/{target_path}") if resp.status_code != 200: print(f"Detail image path {target_path} for {d['Key']} not found.") - - From 7d4253fdbd45b52548556bfcb3e74deefba6620a Mon Sep 17 00:00:00 2001 From: hyeon Date: Tue, 5 Dec 2023 02:05:18 +0900 Subject: [PATCH 32/46] Fix broken tests related to decimal places --- tests/lib9c/test_currency.py | 4 ++-- tests/lib9c/test_fungible_asset.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/lib9c/test_currency.py b/tests/lib9c/test_currency.py index 09c6ee9f..72a192e3 100644 --- a/tests/lib9c/test_currency.py +++ b/tests/lib9c/test_currency.py @@ -17,7 +17,7 @@ def test_garage(): def test_other(): other = Currency.to_currency("other") - assert other["decimalPlaces"] == b'0' + assert other["decimalPlaces"] == b'\x00' assert other["minters"] == None assert other["ticker"] == "OTHER" @@ -27,4 +27,4 @@ def test_serialize(): garage = Currency.to_currency("garage") assert Currency.serialize(garage) == bencodex.dumps({'decimalPlaces': b'\x12', 'minters': None, 'ticker': 'GARAGE', 'totalSupplyTrackable': True}) other = Currency.to_currency("other") - assert Currency.serialize(other) == bencodex.dumps({'decimalPlaces': b'0', 'minters': None, 'ticker': 'OTHER'}) + assert Currency.serialize(other) == bencodex.dumps({'decimalPlaces': b'\x00', 'minters': None, 'ticker': 'OTHER'}) diff --git a/tests/lib9c/test_fungible_asset.py b/tests/lib9c/test_fungible_asset.py index 2d31c81a..2536966d 100644 --- a/tests/lib9c/test_fungible_asset.py +++ b/tests/lib9c/test_fungible_asset.py @@ -4,10 +4,10 @@ def test_to_fungible_asset(): assert FungibleAsset.to_fungible_asset("CRYSTAL", 100, 18) == [{"decimalPlaces": b'\x12', "minters": None, "ticker": "CRYSTAL"}, 100 * 10**18] assert FungibleAsset.to_fungible_asset("GARAGE", 1, 18) == [{"decimalPlaces": b'\x12', "minters": None, "ticker": "GARAGE", "totalSupplyTrackable": True}, 1 * 10**18] - assert FungibleAsset.to_fungible_asset("OTHER", 999, 0) == [{"decimalPlaces": b'0', "minters": None, "ticker": "OTHER"}, 999] + assert FungibleAsset.to_fungible_asset("OTHER", 999, 0) == [{"decimalPlaces": b'\x00', "minters": None, "ticker": "OTHER"}, 999] def test_serialize(): assert FungibleAsset.serialize(FungibleAsset.to_fungible_asset("CRYSTAL", 100, 18)).hex() == "6c35323a647531333a646563696d616c506c61636573313a1275373a6d696e746572736e75363a7469636b657275373a4352595354414c65693130303030303030303030303030303030303030306565" assert FungibleAsset.serialize(FungibleAsset.to_fungible_asset("GARAGE", 1, 18)).hex() == "6c37363a647531333a646563696d616c506c61636573313a1275373a6d696e746572736e75363a7469636b657275363a4741524147457532303a746f74616c537570706c79547261636b61626c65746569313030303030303030303030303030303030306565" - assert FungibleAsset.serialize(FungibleAsset.to_fungible_asset("OTHER", 999, 0)).hex() == "6c35303a647531333a646563696d616c506c61636573313a3075373a6d696e746572736e75363a7469636b657275353a4f5448455265693939396565" + assert FungibleAsset.serialize(FungibleAsset.to_fungible_asset("OTHER", 999, 0)).hex() == "6c35303a647531333a646563696d616c506c61636573313a0075373a6d696e746572736e75363a7469636b657275353a4f5448455265693939396565" From 7428688309305265fc762b960755c15460e3803c Mon Sep 17 00:00:00 2001 From: hyeon Date: Tue, 5 Dec 2023 02:06:27 +0900 Subject: [PATCH 33/46] Test to make right ticker for schema --- tests/common/schemas/test_product_schema.py | 31 +++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 tests/common/schemas/test_product_schema.py diff --git a/tests/common/schemas/test_product_schema.py b/tests/common/schemas/test_product_schema.py new file mode 100644 index 00000000..1b41c600 --- /dev/null +++ b/tests/common/schemas/test_product_schema.py @@ -0,0 +1,31 @@ +import pytest + +from iap.schemas.product import FungibleAssetValueSchema, FungibleItemSchema + + +@pytest.mark.parametrize( + "fav_set", + [("CRYSTAL", "CRYSTAL"), ("FAV__CRYSTAL", "CRYSTAL"), ("RUNE_GOLDENLEAF", "RUNE_GOLDENLEAF")] +) +def test_fav_schema(fav_set): + original, expected = fav_set + + schema = FungibleAssetValueSchema(ticker=original, amount=0) + assert schema.ticker == expected + + +@pytest.mark.parametrize( + "item_set", + [(600201, + "f8faf92c9c0d0e8e06694361ea87bfc8b29a8ae8de93044b98470a57636ed0e0", + "f8faf92c9c0d0e8e06694361ea87bfc8b29a8ae8de93044b98470a57636ed0e0"), # Golden Dust + (600201, "Item_NT_600201", "Item_NT_600201"), # Golden Dust, Wrapped + (500000, "Item_NT_500000", "Item_NT_500000"), # Ap Potion, Wrapped + (49900011, "Item_NT_49900011", "Item_NT_49900011"), # SeasonPass 1 Title, Wrapped + ] +) +def test_fungible_item_schema(item_set): + sheet_item_id, original, expected = item_set + + schema = FungibleItemSchema(sheet_item_id=sheet_item_id, fungible_item_id=original, amount=1) + assert schema.fungible_item_id == expected From 42aaf35e1b56c78b11f111ad53f6af85a72b0779 Mon Sep 17 00:00:00 2001 From: hyeon Date: Tue, 5 Dec 2023 10:52:08 +0900 Subject: [PATCH 34/46] Run all tests --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8a1b8d14..29917586 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -66,7 +66,7 @@ jobs: - name: Run test run: | - poetry run pytest tests/utils + poetry run pytest tests slack_notification: uses: ./.github/workflows/slack_message.yml From 81e3f1ceccb983b9f75eee2134d20d537dee8330 Mon Sep 17 00:00:00 2001 From: hyeon Date: Wed, 6 Dec 2023 11:56:09 +0900 Subject: [PATCH 35/46] Create google refund tracker --- worker/worker/google_refund_tracker.py | 81 ++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 worker/worker/google_refund_tracker.py diff --git a/worker/worker/google_refund_tracker.py b/worker/worker/google_refund_tracker.py new file mode 100644 index 00000000..a3590c7c --- /dev/null +++ b/worker/worker/google_refund_tracker.py @@ -0,0 +1,81 @@ +import os +from dataclasses import dataclass +from datetime import datetime, timezone +from enum import IntEnum +from typing import Optional + +from common import logger +from common.utils.aws import fetch_parameter +from common.utils.google import get_google_client, Spreadsheet + +GOOGLE_PACKAGE_NAME = os.environ.get("GOOGLE_PACKAGE_NAME") +GOOGLE_CREDENTIAL = fetch_parameter( + os.environ.get("REGION_NAME"), + f"{os.environ.get('STAGE')}_9c_IAP_GOOGLE_CREDENTIAL", True +)["Value"] +SHEET_ID = os.environ.get("REFUND_SHEET_ID") + + +class VoidReason(IntEnum): + Other = 0 + Remorse = 1 + Not_received = 2 + Defective = 3 + Accidental_purchase = 4 + Fraud = 5 + Friendly_fraud = 6 + Chargeback = 7 + + +class VoidSource(IntEnum): + User = 0 + Developer = 1 + Google = 2 + + +@dataclass +class RefundData: + orderId: str + purchaseTimeMillis: str + voidedTimeMillis: str + voidedSource: int | VoidSource + voidedReason: int | VoidReason + purchaseToken: str + kind: str + + purchaseTime: Optional[datetime] = None + voidedTime: Optional[datetime] = None + + def __post_init__(self): + self.purchaseTime = datetime.fromtimestamp(int(self.purchaseTimeMillis[:-3]), tz=timezone.utc) + self.voidedTime = datetime.fromtimestamp(int(self.voidedTimeMillis[:-3]), tz=timezone.utc) + self.voidedSource = VoidSource(self.voidedSource) + self.voidedReason = VoidReason(self.voidedReason) + + +def handle(event, context): + client = get_google_client(GOOGLE_CREDENTIAL) + sheet = Spreadsheet(GOOGLE_CREDENTIAL, SHEET_ID) + prev_data = sheet.get_values("Google!A2:B").get("values", []) + prev_order_id = set([x[1] for x in prev_data]) + last_num = int(prev_data[-1][0]) + 1 if prev_data else 1 + logger.info(f"{len(prev_data)} refunded data are present.") + voided_list = client.purchases().voidedpurchases().list(packageName=GOOGLE_PACKAGE_NAME).execute() + voided_list = sorted([RefundData(**x) for x in voided_list["voidedPurchases"]], key=lambda x: x.voidedTimeMillis) + + new_data = [] + index = last_num + for void in voided_list: + if void.orderId in prev_order_id: + continue + new_data.append( + [index, void.orderId, void.purchaseTime.isoformat(), void.voidedTime.isoformat(), void.voidedSource.name, + void.voidedReason.name]) + index += 1 + + sheet.set_values(f"Google!A{last_num + 1}:F", new_data) + logger.info(f"{len(new_data)} Refunds are added") + + +if __name__ == "__main__": + handle(None, None) From 6b8a42c7f58ac1b0a04ece77c730de2bbd9d6015 Mon Sep 17 00:00:00 2001 From: hyeon Date: Thu, 7 Dec 2023 10:49:45 +0900 Subject: [PATCH 36/46] Add CDK stack for google refund updater --- .github/workflows/deploy.yml | 4 ++++ .github/workflows/main.yml | 3 +++ .github/workflows/synth.yml | 3 +++ worker/worker_cdk_stack.py | 26 ++++++++++++++++++++++++++ 4 files changed, 36 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 97a5d4ed..7f65891b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -45,6 +45,8 @@ on: required: true BRIDGE_DATA: required: true + REFUND_SHEET_ID: + required: true jobs: deployment: @@ -141,6 +143,7 @@ jobs: SEASON_PASS_JWT_SECRET: ${{ secrets.SEASON_PASS_JWT_SECRET }} VOUCHER_JWT_SECRET: ${{ secrets.VOUCHER_JWT_SECRET }} BRIDGE_DATA: ${{ secrets.BRIDGE_DATA }} + REFUND_SHEET_ID : ${{ secrets.REFUND_SHEET_ID }} run: | source $VENV yarn cdk synth @@ -171,6 +174,7 @@ jobs: SEASON_PASS_JWT_SECRET: ${{ secrets.SEASON_PASS_JWT_SECRET }} VOUCHER_JWT_SECRET: ${{ secrets.VOUCHER_JWT_SECRET }} BRIDGE_DATA: ${{ secrets.BRIDGE_DATA }} + REFUND_SHEET_ID : ${{ secrets.REFUND_SHEET_ID }} run: | source $VENV yarn cdk deploy --all --require-approval never -O output.txt diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7effb838..bae8e3aa 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -47,6 +47,7 @@ jobs: SEASON_PASS_JWT_SECRET: ${{ secrets.SEASON_PASS_JWT_SECRET }} VOUCHER_JWT_SECRET: ${{ secrets.VOUCHER_JWT_SECRET }} BRIDGE_DATA: ${{ secrets.BRIDGE_DATA }} + REFUND_SHEET_ID : ${{ secrets.REFUND_SHEET_ID }} deploy_without_approval: # This is for development / internal deployment @@ -71,6 +72,7 @@ jobs: SEASON_PASS_JWT_SECRET: ${{ secrets.SEASON_PASS_JWT_SECRET }} VOUCHER_JWT_SECRET: ${{ secrets.VOUCHER_JWT_SECRET }} BRIDGE_DATA: ${{ secrets.BRIDGE_DATA }} + REFUND_SHEET_ID : ${{ secrets.REFUND_SHEET_ID }} approval: runs-on: ubuntu-latest @@ -107,3 +109,4 @@ jobs: SEASON_PASS_JWT_SECRET: ${{ secrets.SEASON_PASS_JWT_SECRET }} VOUCHER_JWT_SECRET: ${{ secrets.VOUCHER_JWT_SECRET }} BRIDGE_DATA: ${{ secrets.BRIDGE_DATA }} + REFUND_SHEET_ID : ${{ secrets.REFUND_SHEET_ID }} diff --git a/.github/workflows/synth.yml b/.github/workflows/synth.yml index ebd0a9a9..95b6626a 100644 --- a/.github/workflows/synth.yml +++ b/.github/workflows/synth.yml @@ -40,6 +40,8 @@ on: required: true BRIDGE_DATA: required: true + REFUND_SHEET_ID: + required: true jobs: synth: @@ -128,6 +130,7 @@ jobs: SEASON_PASS_JWT_SECRET: ${{ secrets.SEASON_PASS_JWT_SECRET }} VOUCHER_JWT_SECRET: ${{ secrets.VOUCHER_JWT_SECRET }} BRIDGE_DATA: ${{ secrets.BRIDGE_DATA }} + REFUND_SHEET_ID : ${{ secrets.REFUND_SHEET_ID }} run: | source $VENV yarn cdk synth diff --git a/worker/worker_cdk_stack.py b/worker/worker_cdk_stack.py index c06f7b17..7b4318db 100644 --- a/worker/worker_cdk_stack.py +++ b/worker/worker_cdk_stack.py @@ -1,4 +1,5 @@ import os +from copy import deepcopy import aws_cdk as cdk_core import boto3 @@ -175,6 +176,31 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: ) + # Update refund sheet + refund_env = deepcopy(env) + refund_env["REFUND_SHEET_ID"] = os.environ.get("REFUND_SHEET_ID") + google_refund_handler = _lambda.Function( + self, f"{config.stage}-9c-iap-refund-update-function", + function_name=f"{config.stage}-9c-iap-refund-update", + description="Refund google sheet update function", + runtime=_lambda.Runtime.PYTHON_3_10, + code=_lambda.AssetCode("worker/worker", exclude=exclude_list), + handler="google_refund_tracker.handle", + memory_size=256, + timeout=cdk_core.Duration.seconds(120), + role=role, + env=refund_env, + layers=[layer], + vpc=shared_stack.vpc, + ) + + # Everyday 01:00 UTC + everyday_0100_rule = _events.Rule( + self, f"{config.stage}-9c-iap-everyday-0100-event", + schedule=_events.Schedule.cron(minute="0", hour="1") # Every day 01:00 UTC + ) + everyday_0100_rule.add_target(_event_targets.LambdaFunction(google_refund_handler)) + # Golden dust by NCG handler env["GOLDEN_DUST_REQUEST_SHEET_ID"] = config.golden_dust_request_sheet_id env["GOLDEN_DUST_WORK_SHEET_ID"] = config.golden_dust_work_sheet_id From 020c36d30cc746426b154e5ba01e062d92f4c9eb Mon Sep 17 00:00:00 2001 From: hyeon Date: Thu, 7 Dec 2023 18:01:19 +0900 Subject: [PATCH 37/46] Fix typo --- worker/worker_cdk_stack.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worker/worker_cdk_stack.py b/worker/worker_cdk_stack.py index 7b4318db..7f34d254 100644 --- a/worker/worker_cdk_stack.py +++ b/worker/worker_cdk_stack.py @@ -189,7 +189,7 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: memory_size=256, timeout=cdk_core.Duration.seconds(120), role=role, - env=refund_env, + environment=refund_env, layers=[layer], vpc=shared_stack.vpc, ) From fd45cfbed557c769201e63f14de0a3fa0a41da1a Mon Sep 17 00:00:00 2001 From: hyeon Date: Fri, 8 Dec 2023 15:19:09 +0900 Subject: [PATCH 38/46] Send voucher request after validation --- iap/api/purchase.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/iap/api/purchase.py b/iap/api/purchase.py index 9806b3f8..bea68b96 100644 --- a/iap/api/purchase.py +++ b/iap/api/purchase.py @@ -162,18 +162,6 @@ def request_product(receipt_data: ReceiptSchema, sess=Depends(session)): ) sess.add(receipt) sess.commit() - logger.info(f"Send voucher request: {receipt.uuid}") - resp = sqs.send_message(QueueUrl=VOUCHER_SQS_URL, - MessageBody=json.dumps({ - "uuid": str(receipt.uuid), - "receipt_id": receipt.id, - "product_id": receipt.product_id, - "product_name": receipt.product.name, - "agent_addr": receipt.agent_addr, - "avatar_addr": receipt.avatar_addr, - "planet_id": receipt_data.planetId.decode(), - })) - logger.info(f"Voucher message: {resp['MessageId']}") sess.refresh(receipt) if receipt_data.store not in (Store.APPLE, Store.APPLE_TEST) and not product: @@ -238,6 +226,18 @@ def request_product(receipt_data: ReceiptSchema, sess=Depends(session)): raise_error(sess, receipt, ValueError(f"Receipt validation failed: {msg}")) receipt.status = ReceiptStatus.VALID + # logger.info(f"Send voucher request: {receipt.uuid}") + # resp = sqs.send_message(QueueUrl=VOUCHER_SQS_URL, + # MessageBody=json.dumps({ + # "id": receipt.id, + # "uuid": receipt.uuid, + # "product_id": receipt.product_id, + # "product_name": receipt.product.name, + # "agent_addr": receipt.agent_addr, + # "avatar_addr": receipt.avatar_addr, + # "planet_id": receipt_data.planetId.decode(), + # })) + # logger.info(f"Voucher message: {resp['MessageId']}") now = datetime.now() if ((product.open_timestamp and product.open_timestamp > now) or From d77a87e06aa6cbbca512fa9fd9fadef072ffa30d Mon Sep 17 00:00:00 2001 From: hyeon Date: Fri, 8 Dec 2023 23:35:24 +0900 Subject: [PATCH 39/46] Not begin transaction before create and run query --- worker/worker/status_monitor.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/worker/worker/status_monitor.py b/worker/worker/status_monitor.py index f1dcb7af..3fb8d674 100644 --- a/worker/worker/status_monitor.py +++ b/worker/worker/status_monitor.py @@ -157,10 +157,9 @@ def handle(event, context): if datetime.utcnow().hour == 3 and datetime.now().minute == 0: # 12:00 KST check_garage() - with session.begin() as sess: - check_invalid_receipt(sess) - check_halt_tx(sess) - check_tx_failure(sess) + check_invalid_receipt(sess) + check_halt_tx(sess) + check_tx_failure(sess) if __name__ == "__main__": From f033d2bc04d0f80e3073bddad542fddbb22c38cf Mon Sep 17 00:00:00 2001 From: hyeon Date: Tue, 12 Dec 2023 10:45:08 +0900 Subject: [PATCH 40/46] Do not check buyagle by DB saved garage status --- iap/api/product.py | 32 ++++++-------------------------- 1 file changed, 6 insertions(+), 26 deletions(-) diff --git a/iap/api/product.py b/iap/api/product.py index 15a42962..2a595d3b 100644 --- a/iap/api/product.py +++ b/iap/api/product.py @@ -6,7 +6,6 @@ from common.models.product import Product, Category from common.utils.address import format_addr -from common.utils.garage import get_iap_garage from common.utils.receipt import PlanetID from iap import settings from iap.dependencies import session @@ -38,17 +37,6 @@ def product_list(agent_addr: str, .order_by(Category.order, Product.order) ).all() - iap_garage = {x.fungible_id: x.amount for x in get_iap_garage(sess)} - garage = {} - for category in all_category_list: - if ((category.open_timestamp and category.open_timestamp > datetime.now()) or - (category.close_timestamp and category.close_timestamp <= datetime.now())): - continue - - for product in category.product_list: - for fungible_item in product.fungible_item_list: - garage[fungible_item.fungible_item_id] = iap_garage.get(fungible_item.fungible_item_id, 0) - category_schema_list = [] for category in all_category_list: cat_schema = CategorySchema.model_validate(category) @@ -59,21 +47,8 @@ def product_list(agent_addr: str, (product.close_timestamp and product.close_timestamp <= datetime.now())): continue - schema_dict[product.id] = ProductSchema.model_validate(product) - # FIXME: Pinpoint get product buyability - product_buyable = True - # Check fungible item stock in garage - for item in product.fungible_item_list: - if garage.get(item.fungible_item_id, 0) < item.amount: - schema_dict[product.id].buyable = False - product_buyable = False - break - - if not product_buyable: - continue - # Check purchase history - schema = schema_dict[product.id] + schema = ProductSchema.model_validate(product) if product.daily_limit: schema.purchase_count = get_purchase_count( sess, product.id, planet_id=planet_id, agent_addr=agent_addr, daily_limit=True @@ -89,6 +64,11 @@ def product_list(agent_addr: str, sess, product.id, planet_id=planet_id, agent_addr=agent_addr ) schema.buyable = schema.purchase_count < product.account_limit + else: # Product with no limitation + schema.buyable = True + + schema_dict[product.id] = schema + cat_schema.product_list = list(schema_dict.values()) category_schema_list.append(cat_schema) From 2879bbd4788aab57aa49af025e5e0370dfc9ea22 Mon Sep 17 00:00:00 2001 From: hyeon Date: Tue, 12 Dec 2023 11:27:34 +0900 Subject: [PATCH 41/46] Load product list with ordering --- common/models/product.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/models/product.py b/common/models/product.py index 2ef97a4c..057bbd1e 100644 --- a/common/models/product.py +++ b/common/models/product.py @@ -35,7 +35,7 @@ class Category(AutoIdMixin, TimeStampMixin, Base): # FIXME: Update to nullable=False l10n_key = Column(Text, doc="L10N Key") - product_list: Mapped[List["Product"]] = relationship("Product", secondary=category_product_table) + product_list: Mapped[List["Product"]] = relationship("Product", secondary=category_product_table, order_by="Product.order") @property def path(self): From d76d96c7586836b91b8d1bf5e1f777cf78e4efc2 Mon Sep 17 00:00:00 2001 From: hyeon Date: Tue, 12 Dec 2023 17:21:50 +0900 Subject: [PATCH 42/46] Enable IAP voucher --- iap/api/purchase.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/iap/api/purchase.py b/iap/api/purchase.py index bea68b96..79468ead 100644 --- a/iap/api/purchase.py +++ b/iap/api/purchase.py @@ -226,18 +226,18 @@ def request_product(receipt_data: ReceiptSchema, sess=Depends(session)): raise_error(sess, receipt, ValueError(f"Receipt validation failed: {msg}")) receipt.status = ReceiptStatus.VALID - # logger.info(f"Send voucher request: {receipt.uuid}") - # resp = sqs.send_message(QueueUrl=VOUCHER_SQS_URL, - # MessageBody=json.dumps({ - # "id": receipt.id, - # "uuid": receipt.uuid, - # "product_id": receipt.product_id, - # "product_name": receipt.product.name, - # "agent_addr": receipt.agent_addr, - # "avatar_addr": receipt.avatar_addr, - # "planet_id": receipt_data.planetId.decode(), - # })) - # logger.info(f"Voucher message: {resp['MessageId']}") + logger.info(f"Send voucher request: {receipt.uuid}") + resp = sqs.send_message(QueueUrl=VOUCHER_SQS_URL, + MessageBody=json.dumps({ + "id": receipt.id, + "uuid": receipt.uuid, + "product_id": receipt.product_id, + "product_name": receipt.product.name, + "agent_addr": receipt.agent_addr, + "avatar_addr": receipt.avatar_addr, + "planet_id": receipt_data.planetId.decode(), + })) + logger.info(f"Voucher message: {resp['MessageId']}") now = datetime.now() if ((product.open_timestamp and product.open_timestamp > now) or From ee2f6a9f9a8974cf1c9856bb571aa1226cd7a5f7 Mon Sep 17 00:00:00 2001 From: hyeon Date: Tue, 12 Dec 2023 17:27:57 +0900 Subject: [PATCH 43/46] [Refactor] Collect all events in one section --- worker/worker_cdk_stack.py | 49 ++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/worker/worker_cdk_stack.py b/worker/worker_cdk_stack.py index 37c830a1..665858aa 100644 --- a/worker/worker_cdk_stack.py +++ b/worker/worker_cdk_stack.py @@ -93,6 +93,28 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: "BRIDGE_DATA": config.bridge_data, } + # Cloudwatch Events + ## Every minute + minute_event_rule = _events.Rule( + self, f"{config.stage}-9c-iap-every-minute-event", + schedule=_events.Schedule.cron(minute="*") # Every minute + ) + # Every ten minute + ten_minute_event_rule = _events.Rule( + self, f"{config.stage}-9c-iap-ten-minute-event", + schedule=_events.Schedule.cron(minute="*/10") # Every ten minute + ) + ## Every hour + hourly_event_rule = _events.Rule( + self, f"{config.stage}-9c-iap-hourly-event", + schedule=_events.Schedule.cron(minute="0") # Every hour + ) + # Everyday 01:00 UTC + everyday_0100_rule = _events.Rule( + self, f"{config.stage}-9c-iap-everyday-0100-event", + schedule=_events.Schedule.cron(minute="0", hour="1") # Every day 01:00 UTC + ) + # Worker Lambda Function exclude_list = [".idea", ".gitignore", ] exclude_list.extend(COMMON_LAMBDA_EXCLUDE) @@ -132,12 +154,6 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: timeout=cdk_core.Duration.seconds(50), environment=env, ) - - # Every minute - minute_event_rule = _events.Rule( - self, f"{config.stage}-9c-iap-tracker-event", - schedule=_events.Schedule.cron(minute="*") # Every minute - ) minute_event_rule.add_target(_event_targets.LambdaFunction(tracker)) # IAP Status Monitor @@ -160,14 +176,8 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: ) if config.stage == "mainnet": - minute_event_rule.add_target(_event_targets.LambdaFunction(status_monitor)) + ten_minute_event_rule.add_target(_event_targets.LambdaFunction(status_monitor)) else: - # Every hour - hourly_event_rule = _events.Rule( - self, f"{config.stage}-9c-iap-hourly-event", - schedule=_events.Schedule.cron(minute="0") # Every hour - ) - hourly_event_rule.add_target(_event_targets.LambdaFunction(status_monitor)) # IAP Voucher @@ -208,12 +218,6 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: layers=[layer], vpc=shared_stack.vpc, ) - - # Everyday 01:00 UTC - everyday_0100_rule = _events.Rule( - self, f"{config.stage}-9c-iap-everyday-0100-event", - schedule=_events.Schedule.cron(minute="0", hour="1") # Every day 01:00 UTC - ) everyday_0100_rule.add_target(_event_targets.LambdaFunction(google_refund_handler)) # Golden dust by NCG handler @@ -235,12 +239,6 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: memory_size=512, reserved_concurrent_executions=1, ) - - # Every ten minute - ten_minute_event_rule = _events.Rule( - self, f"{config.stage}-9c-iap-gd-handler-event", - schedule=_events.Schedule.cron(minute="*/10") # Every ten minute - ) ten_minute_event_rule.add_target(_event_targets.LambdaFunction(gd_handler)) # Golden dust unload Tx. tracker @@ -258,7 +256,6 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: environment=env, memory_size=256, ) - minute_event_rule.add_target(_event_targets.LambdaFunction(gd_tracker)) # Manual unload function From e38cca04bcc0cb9a746cb27d814db809c675b183 Mon Sep 17 00:00:00 2001 From: hyeon Date: Tue, 12 Dec 2023 18:08:02 +0900 Subject: [PATCH 44/46] Change message key: id to receipt_id --- iap/api/purchase.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/iap/api/purchase.py b/iap/api/purchase.py index bea68b96..5f08a03d 100644 --- a/iap/api/purchase.py +++ b/iap/api/purchase.py @@ -226,18 +226,18 @@ def request_product(receipt_data: ReceiptSchema, sess=Depends(session)): raise_error(sess, receipt, ValueError(f"Receipt validation failed: {msg}")) receipt.status = ReceiptStatus.VALID - # logger.info(f"Send voucher request: {receipt.uuid}") - # resp = sqs.send_message(QueueUrl=VOUCHER_SQS_URL, - # MessageBody=json.dumps({ - # "id": receipt.id, - # "uuid": receipt.uuid, - # "product_id": receipt.product_id, - # "product_name": receipt.product.name, - # "agent_addr": receipt.agent_addr, - # "avatar_addr": receipt.avatar_addr, - # "planet_id": receipt_data.planetId.decode(), - # })) - # logger.info(f"Voucher message: {resp['MessageId']}") + logger.info(f"Send voucher request: {receipt.uuid}") + resp = sqs.send_message(QueueUrl=VOUCHER_SQS_URL, + MessageBody=json.dumps({ + "receipt_id": receipt.id, + "uuid": str(receipt.uuid), + "product_id": receipt.product_id, + "product_name": receipt.product.name, + "agent_addr": receipt.agent_addr, + "avatar_addr": receipt.avatar_addr, + "planet_id": receipt_data.planetId.decode(), + })) + logger.info(f"Voucher message: {resp['MessageId']}") now = datetime.now() if ((product.open_timestamp and product.open_timestamp > now) or From 31ff62b3113b6c3f812b49fd265f3ba24ff109be Mon Sep 17 00:00:00 2001 From: hyeon Date: Tue, 12 Dec 2023 18:11:44 +0900 Subject: [PATCH 45/46] Log all error as-is --- worker/worker/voucher.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worker/worker/voucher.py b/worker/worker/voucher.py index a3551169..ab9439dc 100644 --- a/worker/worker/voucher.py +++ b/worker/worker/voucher.py @@ -83,8 +83,8 @@ def handle(event, context): try: sess.commit() sess.refresh(voucher) - except IntegrityError: - logger.warning(f"Receipt {voucher.uuid} is duplicated. Skip.") + except Exception as e: + logger.error(e) sess.rollback() else: success, voucher = request(sess, voucher) From ecccca4878cdb969cb657439e4dab1efd1210616 Mon Sep 17 00:00:00 2001 From: hyeon Date: Tue, 12 Dec 2023 18:39:58 +0900 Subject: [PATCH 46/46] Track golden meat --- worker/worker/status_monitor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/worker/worker/status_monitor.py b/worker/worker/status_monitor.py index 6fc70bb7..a0b9ef7b 100644 --- a/worker/worker/status_monitor.py +++ b/worker/worker/status_monitor.py @@ -25,6 +25,7 @@ "00dfffe23964af9b284d121dae476571b7836b8d9e2e5f510d92a840fecc64fe": "AP Potion (500000)", "1a755098a2bc0659a063107df62e2ff9b3cdaba34d96b79519f504b996f53820": "Silver Dust (800201)", "f8faf92c9c0d0e8e06694361ea87bfc8b29a8ae8de93044b98470a57636ed0e0": "Golden Dust (600201)", + "48e50ecd6d1aa2689fd349c1f0611e6cc1e9c4c74ec4de9d4637ec7b78617308": "Golden Meat (800202)", } engine = create_engine(DB_URI) @@ -129,6 +130,7 @@ def check_garage(): "3991e04dd808dc0bc24b21f5adb7bf1997312f8700daf1334bf34936e8a0813a", # Hourglass (400000) "00dfffe23964af9b284d121dae476571b7836b8d9e2e5f510d92a840fecc64fe", # AP Potion (500000) "f8faf92c9c0d0e8e06694361ea87bfc8b29a8ae8de93044b98470a57636ed0e0" # Golden Dust (600201) + "48e50ecd6d1aa2689fd349c1f0611e6cc1e9c4c74ec4de9d4637ec7b78617308", # Golden Meat (800202) "1a755098a2bc0659a063107df62e2ff9b3cdaba34d96b79519f504b996f53820", # Silver Dust (800201) ] ) {