From 30064b2c9e535d3b4531ed38a95b7fb5cc683b1b Mon Sep 17 00:00:00 2001 From: Serhii Date: Mon, 23 Sep 2024 18:33:50 +0300 Subject: [PATCH 1/4] chores_lint_errors_and_path_issues --- Makefile | 1 + apprunner.yaml | 2 +- debug.py | 6 ++ linguaphoto/ai/cli.py | 9 +- linguaphoto/api/collection.py | 10 ++- linguaphoto/api/image.py | 5 +- linguaphoto/api/user.py | 6 +- linguaphoto/crud/base.py | 2 +- linguaphoto/crud/collection.py | 2 +- linguaphoto/crud/image.py | 6 +- linguaphoto/crud/user.py | 5 +- linguaphoto/main.py | 87 ------------------- linguaphoto/models.py | 2 +- linguaphoto/settings.py | 4 +- linguaphoto/utils/auth.py | 2 + linguaphoto/utils/cloudfront_url_signer.py | 2 +- .../private_key.pem => private_key.pem | 0 17 files changed, 46 insertions(+), 105 deletions(-) create mode 100644 debug.py rename linguaphoto/private_key.pem => private_key.pem (100%) diff --git a/Makefile b/Makefile index 0e0e0af..3966aec 100644 --- a/Makefile +++ b/Makefile @@ -42,6 +42,7 @@ start-db: # ------------------------ # format-backend: + @isort linguaphoto @black linguaphoto @ruff format linguaphoto .PHONY: format diff --git a/apprunner.yaml b/apprunner.yaml index 2fa80da..0fd0c55 100644 --- a/apprunner.yaml +++ b/apprunner.yaml @@ -12,7 +12,7 @@ build: # Specify the start phase command run: - command: venv/bin/python3 main.py # Adjust to the path to your application entry point + command: venv/bin/python3 linguaphoto/main.py # Adjust to the path to your application entry point env: - name: DYNAMODB_TABLE_NAME value: "linguaphoto" diff --git a/debug.py b/debug.py new file mode 100644 index 0000000..c404722 --- /dev/null +++ b/debug.py @@ -0,0 +1,6 @@ +"""it is entity for debugging""" +import uvicorn + +from linguaphoto.main import app + +uvicorn.run(app, port=8080, host="0.0.0.0") diff --git a/linguaphoto/ai/cli.py b/linguaphoto/ai/cli.py index 02a64b9..2df6bad 100644 --- a/linguaphoto/ai/cli.py +++ b/linguaphoto/ai/cli.py @@ -3,6 +3,7 @@ import argparse import asyncio import logging +from io import BytesIO from pathlib import Path from openai import AsyncOpenAI @@ -29,7 +30,13 @@ async def main() -> None: client = AsyncOpenAI( api_key="sk-svcacct-PFETCFHtqmHOmIpP_IAyQfBGz5LOpvC6Zudj7d5Wcdp9WjJT4ImAxuotGcpyT3BlbkFJRbtswQqIxYHam9TN13mCM04_OTZE-v8z-Rw1WEcwzyZqW_GcK0PNNyFp6BcA" ) - transcription_response = await transcribe_image(image, client) + # Convert the ImageFile to BytesIO + image_bytes = BytesIO() + image.save(image_bytes, format="JPEG") # Use the appropriate format for your image + image_bytes.seek(0) # Reset the stream position to the beginning + + # Now call the transcribe_image function with the BytesIO object + transcription_response = await transcribe_image(image_bytes, client) print(transcription_response.model_dump_json(indent=2)) with open(root_dir / "transcription.json", "w") as file: file.write(transcription_response.model_dump_json(indent=2)) diff --git a/linguaphoto/api/collection.py b/linguaphoto/api/collection.py index dc63ed4..b7bc7f7 100644 --- a/linguaphoto/api/collection.py +++ b/linguaphoto/api/collection.py @@ -7,7 +7,10 @@ from linguaphoto.crud.collection import CollectionCrud from linguaphoto.errors import NotAuthorizedError from linguaphoto.models import Collection -from linguaphoto.schemas.collection import CollectionCreateFragment, CollectionEditFragment +from linguaphoto.schemas.collection import ( + CollectionCreateFragment, + CollectionEditFragment, +) from linguaphoto.utils.auth import get_current_user_id router = APIRouter() @@ -33,10 +36,11 @@ async def create( @router.get("/get_collection", response_model=Collection) async def getcollection( id: str, user_id: str = Depends(get_current_user_id), collection_crud: CollectionCrud = Depends() -) -> dict | None: +) -> Collection: async with collection_crud: collection = await collection_crud.get_collection(id) - print(collection) + if collection is None: + raise ValueError if collection.user != user_id: raise NotAuthorizedError return collection diff --git a/linguaphoto/api/image.py b/linguaphoto/api/image.py index 8884289..d912370 100644 --- a/linguaphoto/api/image.py +++ b/linguaphoto/api/image.py @@ -54,8 +54,9 @@ async def delete_image( if image: async with collection_crud: collection = await collection_crud.get_collection(image.collection) - updated_images = list(filter(lambda image: image != id, collection.images)) - await collection_crud.edit_collection(image.collection, {"images": updated_images}) + if collection: + updated_images = list(filter(lambda image: image != id, collection.images)) + await collection_crud.edit_collection(image.collection, {"images": updated_images}) await image_crud.delete_image(id) return raise HTTPException(status_code=400, detail="Image is invalid") diff --git a/linguaphoto/api/user.py b/linguaphoto/api/user.py index 2d6ba55..02a03ae 100644 --- a/linguaphoto/api/user.py +++ b/linguaphoto/api/user.py @@ -10,7 +10,11 @@ UserSigninRespondFragment, UserSignupFragment, ) -from linguaphoto.utils.auth import create_access_token, decode_access_token, oauth2_schema +from linguaphoto.utils.auth import ( + create_access_token, + decode_access_token, + oauth2_schema, +) router = APIRouter() diff --git a/linguaphoto/crud/base.py b/linguaphoto/crud/base.py index d298c11..4695614 100644 --- a/linguaphoto/crud/base.py +++ b/linguaphoto/crud/base.py @@ -170,7 +170,7 @@ async def _update_item( raise ValueError(f"Invalid update: {str(e)}") raise - async def _delete_item(self, item: BaseModel | str) -> None: + async def _delete_item(self, item: LinguaBaseModel | str) -> None: table = await self.db.Table(TABLE_NAME) await table.delete_item(Key={"id": item if isinstance(item, str) else item.id}) diff --git a/linguaphoto/crud/collection.py b/linguaphoto/crud/collection.py index 631287a..32e11e2 100644 --- a/linguaphoto/crud/collection.py +++ b/linguaphoto/crud/collection.py @@ -12,7 +12,7 @@ async def create_collection(self, user_id: str, title: str, description: str) -> await self._add_item(collection) return collection - async def get_collection(self, collection_id: str) -> Collection: + async def get_collection(self, collection_id: str) -> Collection | None: collection = await self._get_item(collection_id, Collection, True) return collection diff --git a/linguaphoto/crud/image.py b/linguaphoto/crud/image.py index 61d7837..f126908 100644 --- a/linguaphoto/crud/image.py +++ b/linguaphoto/crud/image.py @@ -73,7 +73,7 @@ async def get_images(self, collection_id: str, user_id: str) -> List[Image]: images = await self._get_items_from_secondary_index("user", user_id, Image, Key("collection").eq(collection_id)) return images - async def get_image(self, image_id: str) -> Image: + async def get_image(self, image_id: str) -> Image | None: image = await self._get_item(image_id, Image, True) return image @@ -86,6 +86,8 @@ async def translate(self, images: List[str], user_id: str) -> List[Image]: for id in images: # Retrieve image metadata and download the image content image_instance = await self._get_item(id, Image, True) + if image_instance is None: + continue response = requests.get(image_instance.image_url) if response.status_code == 200: img_source = BytesIO(response.content) @@ -102,6 +104,8 @@ async def translate(self, images: List[str], user_id: str) -> List[Image]: # Set buffer position to the start audio_buffer.seek(0) audio_url = await self.create_audio(audio_buffer) + if audio_url is None: + continue # Attach the audio URL to the transcription transcription.audio_url = audio_url image_instance.transcriptions = transcription_response.transcriptions diff --git a/linguaphoto/crud/user.py b/linguaphoto/crud/user.py index b7edefb..f7a3e11 100644 --- a/linguaphoto/crud/user.py +++ b/linguaphoto/crud/user.py @@ -32,6 +32,5 @@ async def verify_user_by_email(self, user: UserSigninFragment) -> bool: else: raise ValueError - async def update_user(self, id: str, data: dict) -> User | None: - user = await self._update_item(id, User, data) - return user + async def update_user(self, id: str, data: dict) -> None: + await self._update_item(id, User, data) diff --git a/linguaphoto/main.py b/linguaphoto/main.py index 4c4f21f..b92c07a 100644 --- a/linguaphoto/main.py +++ b/linguaphoto/main.py @@ -5,7 +5,6 @@ from fastapi.middleware.cors import CORSMiddleware from linguaphoto.api.api import router -from linguaphoto.settings import settings app = FastAPI() @@ -18,94 +17,8 @@ allow_headers=["*"], ) -# Retrieve AWS configuration from environment variables -bucket_name = settings.bucket_name -dynamodb_table_name = settings.dynamodb_table_name -media_hosting_server = settings.media_hosting_server -key_pair_id = settings.key_pair_id - app.include_router(router, prefix="") - -# class ImageMetadata(BaseModel): -# filename: str -# s3_url: str - - -# @app.post("/upload/", response_model=ImageMetadata) -# async def upload_image(file: UploadFile = File(...)) -> ImageMetadata: -# if file.filename is None or not file.filename: -# raise HTTPException(status_code=400, detail="File name is missing.") - -# try: -# # Generate a unique file name -# file_extension = file.filename.split(".")[-1] if "." in file.filename else "unknown" -# unique_filename = f"{uuid.uuid4()}.{file_extension}" - -# if bucket_name is None: -# raise HTTPException(status_code=500, detail="Bucket name is not set.") - -# if dynamodb_table_name is None: -# raise HTTPException(status_code=500, detail="DynamoDB table name is not set.") - -# # Create an instance of CloudFrontUrlSigner -# private_key_path = os.path.abspath("private_key.pem") -# cfs = CloudFrontUrlSigner(str(key_pair_id), private_key_path) -# # Generate a signed URL -# url = f"{media_hosting_server}/{unique_filename}" -# custom_policy = cfs.create_custom_policy(url, expire_days=100) -# s3_url = cfs.generate_presigned_url(url, custom_policy) -# print(s3_url) -# # Create an S3 client with aioboto3 -# async with aioboto3.Session().client( -# "s3", -# region_name=settings.aws_region_name, -# aws_access_key_id=settings.aws_access_key_id, -# aws_secret_access_key=settings.aws_secret_access_key, -# ) as s3_client: -# # Upload the file to S3 -# await s3_client.upload_fileobj(file.file, bucket_name, f"uploads/{unique_filename}") - -# # Create a DynamoDB resource with aioboto3 -# async with aioboto3.Session().resource( -# "dynamodb", -# region_name=settings.aws_region_name, -# aws_access_key_id=settings.aws_access_key_id, -# aws_secret_access_key=settings.aws_secret_access_key, -# ) as dynamodb: -# table = await dynamodb.Table(dynamodb_table_name) -# # Save metadata to DynamoDB -# await table.put_item(Item={"id": unique_filename, "s3_url": s3_url}) - -# return ImageMetadata(filename=unique_filename, s3_url=s3_url) - -# except Exception as e: -# print(str(e)) -# raise HTTPException(status_code=500, detail=str(e)) - - -# @app.get("/download/{filename}") -# async def download_image(filename: str): -# try: -# async with aioboto3.Session().resource( -# "dynamodb", -# region_name=settings.aws_region_name, -# aws_access_key_id=settings.aws_access_key_id, -# aws_secret_access_key=settings.aws_secret_access_key, -# ) as dynamodb: -# table = await dynamodb.Table(dynamodb_table_name) -# # Retrieve image metadata from DynamoDB -# response = await table.get_item(Key={"id": filename}) - -# if "Item" not in response: -# raise HTTPException(status_code=404, detail="Image not found") - -# # Return the S3 URL for download -# return {"s3_url": response["Item"]["s3_url"]} - -# except Exception as e: -# raise HTTPException(status_code=500, detail=str(e)) - if __name__ == "__main__": print("Starting webserver...") uvicorn.run(app, port=8080, host="0.0.0.0") diff --git a/linguaphoto/models.py b/linguaphoto/models.py index 4905f06..15baa57 100644 --- a/linguaphoto/models.py +++ b/linguaphoto/models.py @@ -79,7 +79,7 @@ class TranscriptionResponse(BaseModel): class Image(LinguaBaseModel): is_translated: bool = False transcriptions: list[Transcription] = [] - collection: str | None = None + collection: str image_url: str user: str diff --git a/linguaphoto/settings.py b/linguaphoto/settings.py index 4aab1d2..f775b3c 100644 --- a/linguaphoto/settings.py +++ b/linguaphoto/settings.py @@ -18,7 +18,7 @@ class Settings: - bucket_name = os.getenv("S3_BUCKET_NAME") + bucket_name = os.getenv("S3_BUCKET_NAME", "linguaphoto") dynamodb_table_name = os.getenv("DYNAMODB_TABLE_NAME", "linguaphoto") media_hosting_server = os.getenv("MEDIA_HOSTING_SERVER") key_pair_id = os.getenv("KEY_PAIR_ID") @@ -27,7 +27,7 @@ class Settings: aws_secret_access_key = os.getenv("AWS_SECRET_ACCESS_KEY") openai_key = os.getenv("OPENAI_API_KEY") stripe_key = os.getenv("STRIPE_API_KEY") - stripe_price_id = os.getenv("STRIPE_PRODUCT_PRICE_ID") + stripe_price_id = os.getenv("STRIPE_PRODUCT_PRICE_ID", "price_1Q0ZaMKeTo38dsfeSWRDGCEf") settings = Settings() diff --git a/linguaphoto/utils/auth.py b/linguaphoto/utils/auth.py index 97fc31a..108f37e 100644 --- a/linguaphoto/utils/auth.py +++ b/linguaphoto/utils/auth.py @@ -61,6 +61,8 @@ async def subscription_validate(token: str = Depends(oauth2_schema), user_crud: raise HTTPException(status_code=422, detail="Could not validate credentials") async with user_crud: user = await user_crud.get_user(user_id, True) + if user is None: + raise HTTPException(status_code=422, detail="Could not validate credentials") if user.is_subscription is False: raise HTTPException(status_code=422, detail="You need to subscribe.") return True diff --git a/linguaphoto/utils/cloudfront_url_signer.py b/linguaphoto/utils/cloudfront_url_signer.py index 07ff904..3697dcc 100644 --- a/linguaphoto/utils/cloudfront_url_signer.py +++ b/linguaphoto/utils/cloudfront_url_signer.py @@ -33,7 +33,7 @@ def _rsa_signer(self, message: str) -> bytes: with open(self.private_key_path, "r") as key_file: private_key = key_file.read() return rsa.sign( - message, # Ensure message is in bytes + message.encode("utf8"), # Ensure message is in bytes rsa.PrivateKey.load_pkcs1(private_key.encode("utf8")), "SHA-1", # CloudFront requires SHA-1 hash ) diff --git a/linguaphoto/private_key.pem b/private_key.pem similarity index 100% rename from linguaphoto/private_key.pem rename to private_key.pem From f2e4e0a099ca96e9f983ca238cf286b5cdd8d729 Mon Sep 17 00:00:00 2001 From: Serhii Date: Tue, 24 Sep 2024 12:34:55 +0300 Subject: [PATCH 2/4] feature_db_py --- apprunner.yaml | 2 + debug.py | 1 + linguaphoto/crud/base.py | 116 ++++++++++++++++++++++++++++++++++++- linguaphoto/db.py | 110 +++++++++++++++++++++++++++++++++++ linguaphoto/settings.py | 1 + linguaphoto/utils/utils.py | 12 ++++ 6 files changed, 241 insertions(+), 1 deletion(-) create mode 100644 linguaphoto/db.py create mode 100644 linguaphoto/utils/utils.py diff --git a/apprunner.yaml b/apprunner.yaml index 0fd0c55..99c12f3 100644 --- a/apprunner.yaml +++ b/apprunner.yaml @@ -14,6 +14,8 @@ build: run: command: venv/bin/python3 linguaphoto/main.py # Adjust to the path to your application entry point env: + - name: HOMEPAGE_URL + value: "linguaphoto.com" - name: DYNAMODB_TABLE_NAME value: "linguaphoto" - name: S3_BUCKET_NAME diff --git a/debug.py b/debug.py index c404722..0b59d80 100644 --- a/debug.py +++ b/debug.py @@ -1,4 +1,5 @@ """it is entity for debugging""" + import uvicorn from linguaphoto.main import app diff --git a/linguaphoto/crud/base.py b/linguaphoto/crud/base.py index 4695614..c3cbdf0 100644 --- a/linguaphoto/crud/base.py +++ b/linguaphoto/crud/base.py @@ -1,8 +1,9 @@ """Defines the base CRUD interface.""" +import itertools import logging from types import TracebackType -from typing import Any, AsyncContextManager, BinaryIO, Self, TypeVar +from typing import Any, AsyncContextManager, BinaryIO, Literal, Self, TypeVar import aioboto3 from boto3.dynamodb.conditions import ComparisonCondition, Key @@ -13,6 +14,7 @@ from linguaphoto.errors import InternalError, ItemNotFoundError from linguaphoto.models import BaseModel, LinguaBaseModel from linguaphoto.settings import settings +from linguaphoto.utils.utils import get_cors_origins T = TypeVar("T", bound=BaseModel) @@ -21,6 +23,9 @@ DEFAULT_SCAN_LIMIT = 1000 ITEMS_PER_PAGE = 12 +TableKey = tuple[str, Literal["S", "N", "B"], Literal["HASH", "RANGE"]] +GlobalSecondaryIndex = tuple[str, str, Literal["S", "N", "B"], Literal["HASH", "RANGE"]] + logger = logging.getLogger(__name__) @@ -45,6 +50,10 @@ def s3(self) -> S3ServiceResource: def get_gsi_index_name(cls, colname: str) -> str: return f"{colname}-index" + @classmethod + def get_gsis(cls) -> set[str]: + return {"type"} + async def __aenter__(self) -> Self: session = aioboto3.Session() db = await session.resource( @@ -174,6 +183,66 @@ async def _delete_item(self, item: LinguaBaseModel | str) -> None: table = await self.db.Table(TABLE_NAME) await table.delete_item(Key={"id": item if isinstance(item, str) else item.id}) + async def _create_dynamodb_table( + self, + name: str, + keys: list[TableKey], + gsis: list[GlobalSecondaryIndex] | None = None, + deletion_protection: bool = False, + ) -> None: + """Creates a table in the Dynamo database if a table of that name does not already exist. + + Args: + name: Name of the table. + keys: Primary and secondary keys. Do not include non-key attributes. + gsis: Making an attribute a GSI is required in order to query + against it. Note HASH on a GSI does not actually enforce + uniqueness. Instead, the difference is: you cannot query + RANGE fields alone, but you may query HASH fields. + deletion_protection: Whether the table is protected from being + deleted. + """ + try: + await self.db.meta.client.describe_table(TableName=name) + logger.info("Found existing table %s", name) + except ClientError: + logger.info("Creating %s table", name) + + if gsis: + table = await self.db.create_table( + AttributeDefinitions=[ + {"AttributeName": n, "AttributeType": t} + for n, t in itertools.chain(((n, t) for (n, t, _) in keys), ((n, t) for _, n, t, _ in gsis)) + ], + TableName=name, + KeySchema=[{"AttributeName": n, "KeyType": t} for n, _, t in keys], + GlobalSecondaryIndexes=( + [ + { + "IndexName": i, + "KeySchema": [{"AttributeName": n, "KeyType": t}], + "Projection": {"ProjectionType": "ALL"}, + } + for i, n, _, t in gsis + ] + ), + DeletionProtectionEnabled=deletion_protection, + BillingMode="PAY_PER_REQUEST", + ) + + else: + table = await self.db.create_table( + AttributeDefinitions=[ + {"AttributeName": n, "AttributeType": t} for n, t in ((n, t) for (n, t, _) in keys) + ], + TableName=name, + KeySchema=[{"AttributeName": n, "KeyType": t} for n, _, t in keys], + DeletionProtectionEnabled=deletion_protection, + BillingMode="PAY_PER_REQUEST", + ) + + await table.wait_until_exists() + async def _list_items( self, item_class: type[T], @@ -206,3 +275,48 @@ async def _list_items( async def _upload_to_s3(self, file: BinaryIO, unique_filename: str) -> None: bucket = await self.s3.Bucket(settings.bucket_name) await bucket.upload_fileobj(file, f"uploads/{unique_filename}") + + async def _create_s3_bucket(self) -> None: + """Creates an S3 bucket if it does not already exist.""" + try: + await self.s3.meta.client.head_bucket(Bucket=settings.bucket_name) + logger.info("Found existing bucket %s", settings.bucket_name) + except ClientError: + logger.info("Creating %s bucket", settings.bucket_name) + await self.s3.create_bucket(Bucket=settings.bucket_name) + + logger.info("Updating %s CORS configuration", settings.bucket_name) + s3_cors = await self.s3.BucketCors(settings.bucket_name) + await s3_cors.put( + CORSConfiguration={ + "CORSRules": [ + { + "AllowedHeaders": ["*"], + "AllowedMethods": ["GET"], + "AllowedOrigins": get_cors_origins(), + "ExposeHeaders": ["ETag"], + } + ] + }, + ) + + async def _delete_dynamodb_table(self, name: str) -> None: + """Deletes a table in the Dynamo database. + + Args: + name: Name of the table. + """ + try: + table = await self.db.Table(name) + await table.delete() + logger.info("Deleted table %s", name) + except ClientError: + logger.info("Table %s does not exist", name) + + async def _delete_s3_bucket(self) -> None: + """Deletes an S3 bucket.""" + bucket = await self.s3.Bucket(settings.bucket_name) + logger.info("Deleting bucket %s", settings.bucket_name) + async for obj in bucket.objects.all(): + await obj.delete() + await bucket.delete() diff --git a/linguaphoto/db.py b/linguaphoto/db.py new file mode 100644 index 0000000..c354baf --- /dev/null +++ b/linguaphoto/db.py @@ -0,0 +1,110 @@ +"""Defines base tools for interacting with the database.""" + +import argparse +import asyncio +import logging +from typing import AsyncGenerator, Literal, Self + +from linguaphoto.crud.base import TABLE_NAME, BaseCrud +from linguaphoto.crud.collection import CollectionCrud +from linguaphoto.crud.image import ImageCrud +from linguaphoto.crud.user import UserCrud + + +class Crud( + CollectionCrud, + ImageCrud, + UserCrud, + BaseCrud, +): + """Composes the various CRUD classes into a single class.""" + + @classmethod + async def get(cls) -> AsyncGenerator[Self, None]: + async with cls() as crud: + yield crud + + +async def create_tables(crud: Crud | None = None, deletion_protection: bool = False) -> None: + """Initializes all of the database tables. + + Args: + crud: The top-level CRUD class. + deletion_protection: Whether to enable deletion protection on the tables. + """ + logging.basicConfig(level=logging.INFO) + + if crud is None: + async with Crud() as new_crud: + await create_tables(new_crud) + + else: + gsis_set = crud.get_gsis() + gsis: list[tuple[str, str, Literal["S", "N", "B"], Literal["HASH", "RANGE"]]] = [ + (Crud.get_gsi_index_name(g), g, "S", "HASH") for g in gsis_set + ] + + await asyncio.gather( + crud._create_dynamodb_table( + name=TABLE_NAME, + keys=[ + ("id", "S", "HASH"), + ], + gsis=gsis, + deletion_protection=deletion_protection, + ), + crud._create_s3_bucket(), + ) + + +async def delete_tables(crud: Crud | None = None) -> None: + """Deletes all of the database tables. + + Args: + crud: The top-level CRUD class. + """ + logging.basicConfig(level=logging.INFO) + + if crud is None: + async with Crud() as new_crud: + await delete_tables(new_crud) + + else: + await crud._delete_dynamodb_table(TABLE_NAME) + await crud._delete_s3_bucket() + + +async def populate_with_dummy_data(crud: Crud | None = None) -> None: + """Populates the database with dummy data. + + Args: + crud: The top-level CRUD class. + """ + if crud is None: + async with Crud() as new_crud: + await populate_with_dummy_data(new_crud) + + else: + raise NotImplementedError("This function is not yet implemented.") + + +async def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("action", choices=["create", "delete", "populate"]) + args = parser.parse_args() + + async with Crud() as crud: + match args.action: + case "create": + await create_tables(crud) + case "delete": + await delete_tables(crud) + case "populate": + await populate_with_dummy_data(crud) + case _: + raise ValueError(f"Invalid action: {args.action}") + + +if __name__ == "__main__": + # python -m store.app.db + asyncio.run(main()) diff --git a/linguaphoto/settings.py b/linguaphoto/settings.py index f775b3c..6e4fd95 100644 --- a/linguaphoto/settings.py +++ b/linguaphoto/settings.py @@ -28,6 +28,7 @@ class Settings: openai_key = os.getenv("OPENAI_API_KEY") stripe_key = os.getenv("STRIPE_API_KEY") stripe_price_id = os.getenv("STRIPE_PRODUCT_PRICE_ID", "price_1Q0ZaMKeTo38dsfeSWRDGCEf") + homepage_url = os.getenv("HOMEPAGE_URL", "") settings = Settings() diff --git a/linguaphoto/utils/utils.py b/linguaphoto/utils/utils.py new file mode 100644 index 0000000..9b9f65c --- /dev/null +++ b/linguaphoto/utils/utils.py @@ -0,0 +1,12 @@ +"""Defines package-wide utility functions.""" + +from linguaphoto.settings import settings + +LOCALHOST_URLS = [ + "http://127.0.0.1:3000", + "http://localhost:3000", +] + + +def get_cors_origins() -> list[str]: + return list({settings.homepage_url, *LOCALHOST_URLS}) From 5027d2f00940cfe0863a07a9fe7a43802fb8a794 Mon Sep 17 00:00:00 2001 From: Serhii Date: Tue, 24 Sep 2024 17:00:12 +0300 Subject: [PATCH 3/4] fix_audio_issue --- frontend/src/components/Audio.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/components/Audio.tsx b/frontend/src/components/Audio.tsx index d48e484..5dea933 100644 --- a/frontend/src/components/Audio.tsx +++ b/frontend/src/components/Audio.tsx @@ -93,6 +93,7 @@ const AudioPlayer: React.FC = ({ currentImage, index }) => { useEffect(() => { if (audioRef.current) { audioRef.current.load(); + audioRef.current.playbackRate = playbackRate; } }, [currentImage, index]); From ea27e2d21d5d8dd32134b3c042074897a0ef7c3e Mon Sep 17 00:00:00 2001 From: Serhii Date: Tue, 24 Sep 2024 17:12:31 +0300 Subject: [PATCH 4/4] chores_lint_error --- linguaphoto/settings.py | 1 - 1 file changed, 1 deletion(-) diff --git a/linguaphoto/settings.py b/linguaphoto/settings.py index e5a5b8e..6e4fd95 100644 --- a/linguaphoto/settings.py +++ b/linguaphoto/settings.py @@ -31,5 +31,4 @@ class Settings: homepage_url = os.getenv("HOMEPAGE_URL", "") - settings = Settings()