Ejemplo n.º 1
0
def confirm_sign_up(body: Mapping[str, str]) -> Response:
    """
    Confirm sign up using a confirmation code.

    :param body: Mapping containing email and confirmationCode keys
    :return: (Response specifics, status code)
    """
    logging.info("Starting confirm sign up flow.")
    email, confirmation_code = verify_parameters(body, "email",
                                                 "confirmationCode")

    client: CognitoClient = boto3.client("cognito-idp")
    try:
        client.confirm_sign_up(
            ClientId=CLIENT_ID,
            Username=email,
            ConfirmationCode=confirmation_code,
        )
    except client.exceptions.UserNotFoundException:
        return ResponseData(message="This email is not registered."), 404
    except (client.exceptions.CodeMismatchException, ParamValidationError):
        return ResponseData(message="Invalid confirmation code."), 400
    except client.exceptions.NotAuthorizedException:
        return ResponseData(message="User has already been verified."), 400
    except Exception:
        logging.exception(UNEXPECTED_EXCEPTION)
        return ResponseData(message=UNEXPECTED_EXCEPTION), 500

    logging.info("Confirm sign up flow successful.")
    return ResponseData(message="Your email has been verified!"), 200
Ejemplo n.º 2
0
def forgot_password(body: Mapping[str, str]) -> Response:
    """
    Send a forgot password email to a user.

    :param body: Mapping containing an email key
    :return: (Response specifics, status code)
    """
    logging.info("Starting forgot password flow.")
    (email, ) = verify_parameters(body, "email")

    client: CognitoClient = boto3.client("cognito-idp")
    try:
        client.forgot_password(ClientId=CLIENT_ID, Username=email)
    except client.exceptions.UserNotFoundException:
        return ResponseData(message="This email is not registered."), 404
    except client.exceptions.InvalidParameterException:
        return ResponseData(message="User is not verified yet."), 403
    except client.exceptions.LimitExceededException:
        return ResponseData(
            message="Too many requests, please try again later."), 429
    except Exception:
        logging.exception(UNEXPECTED_EXCEPTION)
        return ResponseData(message=UNEXPECTED_EXCEPTION), 500

    logging.info("Forgot password flow successful.")
    return ResponseData(
        message=f"A password reset code has been sent to {email}."), 200
Ejemplo n.º 3
0
def patch_categories(user_id: str,
                     body: Mapping[str, Mapping[str, Mapping[str, str]]]):
    """
    Batch update a set of category IDs.

    :param user_id: ID of the user
    :param body: Mapping of category IDs to PATCH operations. See patch_category for operation
                 details
    :return: (Response specifics, status code)
    """
    try:
        body = body.items()
    except AttributeError:
        return ResponseData(message=f"Invalid categories body: {body}"), 400

    res_data = {}
    for category_id, category in body:
        try:
            category_id = int(category_id)
        except ValueError:
            res_data.setdefault("failedUpdates", []).append(category_id)
            continue
        _, status_code = patch_category(user_id,
                                        category,
                                        category_id,
                                        batch=True)
        status_code != 200 and res_data.setdefault("failedUpdates",
                                                   []).append(category_id)

    if not res_data:
        return {}, 204
    return ResponseData(data=res_data), 200
Ejemplo n.º 4
0
def get_recipe(user_id: str, recipe_id: int) -> Response:
    """
    GET a specific recipe in the database.

    :param user_id: ID of the user
    :param recipe_id: ID of the recipe being retrieved
    :return: (Response specifics, status code)
    """
    logging.info(
        f"Getting recipe with ID {recipe_id} for user with ID {user_id}.")
    kwargs = format_query_fields([
        "recipeId",
        "name",
        "desc",
        "url",
        "adaptedFrom",
        "cookTime",
        "yield",
        "categories",
        "instructions",
        "ingredients",
        "imgSrc",
        "updateTime",
        "createTime",
    ], )

    recipes_table, _ = get_recipes_table()
    try:
        item = recipes_table.get_item(
            Key={
                "userId": user_id,
                "recipeId": recipe_id
            },
            **kwargs,
        ).get("Item", {})
    except ClientError as e:
        if e.response["Error"]["Code"] == "ValidationException":
            return (
                ResponseData(
                    message="One of the following parameters is invalid: "
                    f"{{User ID: {user_id}, Category ID: {recipe_id}}}"),
                400,
            )
        logging.exception(UNEXPECTED_EXCEPTION)
        raise
    if not item:
        return (
            ResponseData(
                message=
                f"User {user_id} does not have a recipe with ID {recipe_id}.",
            ),
            404,
        )

    return ResponseData(data=item), 200
Ejemplo n.º 5
0
def delete_recipe(user_id: str, recipe_id: int) -> Response:
    """
    DELETE the specified recipe from the database.

    :param user_id: ID of the user
    :param recipe_id: ID of the recipe being deleted
    :return: (Response specifics, status code)
    """
    recipes_table, resource = get_recipes_table()

    logging.info(
        f"Deleting recipe with ID {recipe_id} for user with ID {user_id}.")
    try:
        image_source: str = (recipes_table.delete_item(
            Key={
                "userId": user_id,
                "recipeId": recipe_id
            },
            ConditionExpression=Attr("userId").exists()
            & Attr("recipeId").exists(),
            ReturnValues="ALL_OLD",
        ).get("Attributes", {}).get("imgSrc"))
    except resource.meta.client.exceptions.ConditionalCheckFailedException:
        return (
            ResponseData(
                message=
                f"User {user_id} does not have a recipe with ID {recipe_id}.",
            ),
            404,
        )
    except ClientError as e:
        if e.response["Error"]["Code"] == "ValidationException":
            return (
                ResponseData(
                    message="One of the following parameters is invalid: "
                    f"{{User ID: {user_id}, Category ID: {recipe_id}}}"),
                400,
            )
        logging.exception(UNEXPECTED_EXCEPTION)
        raise

    if image_source and image_source.startswith(
            IMAGE_PREFIX):  # Delete self-hosted image from S3
        key = image_source[len(
            IMAGE_PREFIX):]  # Don't have remove_prefix in 3.8, sad
        logging.info(
            f"Deleting image with key {key} for user with ID {user_id}.")
        image: Object = boto3.resource("s3").Object(
            os.environ["images_bucket_name"], key)
        image.delete()

    return {}, 204
Ejemplo n.º 6
0
    def delete(self, category_id: int):
        """Delete a category by ID."""
        user_id = request.environ["USER_ID"]

        res, status_code = delete_category(user_id, category_id)
        if status_code != 204:
            return ResponseData(message=res["message"]), status_code

        res, status_code = remove_categories_from_recipes(user_id, [category_id])
        if status_code != 204:
            return ResponseData(data=res["data"]), status_code

        return {}, 204
Ejemplo n.º 7
0
def refresh_id_token(body: Mapping[str, str]) -> Response:
    """
    Refresh a user's ID token using a refresh token.

    :param body: Mapping containing a refreshToken key
    :return: (Response specifics, status code)
    """
    logging.info("Starting refresh ID token flow.")
    (refresh_token, ) = verify_parameters(body, "refreshToken")

    client: CognitoClient = boto3.client("cognito-idp")

    try:
        res = client.admin_initiate_auth(
            AuthFlow="REFRESH_TOKEN_AUTH",
            AuthParameters=({
                "REFRESH_TOKEN": refresh_token
            }),
            ClientId=CLIENT_ID,
            UserPoolId=USER_POOL_ID,
        )
    except client.exceptions.NotAuthorizedException:
        return (
            ResponseData(message="Invalid refresh token.",
                         data={"refreshTokenExpired": True}),
            401,
        )
    except client.exceptions.UserNotConfirmedException:
        return ResponseData(message="User is not verified."), 403
    except Exception:
        logging.exception(UNEXPECTED_EXCEPTION)
        return ResponseData(message=UNEXPECTED_EXCEPTION), 500

    res = res["AuthenticationResult"]
    access_token = res.get("AccessToken")
    id_token = res.get("IdToken")

    user = client.get_user(AccessToken=access_token).get("Username")

    logging.info("Refresh ID token flow successful.")
    return (
        ResponseData(message="You have been signed in.",
                     data={
                         "idToken": id_token,
                         "user": user
                     }),
        200,
    )
Ejemplo n.º 8
0
def get_recipes(user_id: str) -> Response:
    """
    GET all recipes in the database.

    :param user_id: ID of the user
    :return: (Response specifics, status code)
    """
    logging.info(f"Getting all recipes for user with ID {user_id}.")
    kwargs = format_query_fields([
        "recipeId",
        "name",
        "desc",
        "url",
        "adaptedFrom",
        "cookTime",
        "yield",
        "categories",
        "instructions",
        "ingredients",
        "imgSrc",
        "updateTime",
        "createTime",
    ], )

    # TODO: Paginate
    recipes_table, _ = get_recipes_table()
    items = recipes_table.query(
        KeyConditionExpression=Key("userId").eq(user_id),
        **kwargs).get("Items", [])

    return ResponseData(data={"recipes": items}), 200
Ejemplo n.º 9
0
def remove_categories_from_recipes(user_id: str,
                                   category_ids: Iterable[int]) -> Response:
    """
    Remove references to the specified category from all recipes.

    :param user_id: ID of the user
    :param category_ids: IDs of the categories to remove
    :return: (Response specifics, status code)
    """
    recipes_table, _ = get_recipes_table()
    kwargs = format_query_fields(["recipeId", "categories"])
    recipes_to_update = [
        int(recipe["recipeId"]) for recipe in recipes_table.query(
            KeyConditionExpression=Key("userId").eq(user_id),
            **kwargs,
        ).get("Items", [])
        # Check if it shares any categories with category_ids. Can't be done with
        # FilterExpression due to it not supporting checking for multiple items in sets
        if not recipe.get("categories", set()).isdisjoint(category_ids)
    ]
    if not recipes_to_update:
        return {}, 204

    logging.info(
        f"Removing references to categories with IDs {category_ids} from "
        f"recipes with IDs {recipes_to_update} for user with ID {user_id}.")

    items = [{
        "Update": {
            "TableName": os.environ["recipes_table_name"],
            "Key": {
                "recipeId": {
                    "N": f"{recipe_id}"
                },
                "userId": {
                    "S": user_id
                }
            },
            "UpdateExpression": "DELETE #categories :categories",
            "ExpressionAttributeNames": {
                "#categories": "categories"
            },
            "ExpressionAttributeValues": {
                ":categories": {
                    "NS": [str(c) for c in category_ids]
                }
            },
        }
    } for recipe_id in recipes_to_update]
    client: DynamoDBClient = boto3.client("dynamodb")
    for chunk in chunks(items, batch_size=25):
        client.transact_write_items(
            TransactItems=[item for item in chunk if item])  # Filter Nones

    return (
        ResponseData(data={"updatedRecipes": recipes_to_update}),
        200,
    )
Ejemplo n.º 10
0
def patch_recipes(
    user_id: str,
    body: Mapping[str, Mapping[str, MutableMapping[str,
                                                   Union[str,
                                                         Union[List[int],
                                                               List[str]]]]]],
) -> Response:
    """
    Batch update a set of recipe IDs.

    :param user_id: ID of the user
    :param body: Mapping of recipe IDs to PATCH operations. See patch_recipe for operation details
    :return: (Response specifics, status code)
    """
    try:
        body = body.items()
    except AttributeError:
        return ResponseData(message=f"Invalid recipes body: {body}"), 400

    res_data = {}
    pool = []
    for recipe_id, operations in body:
        try:
            recipe_id = int(recipe_id)
        except ValueError:
            logging.exception(f"{recipe_id} is not a valid recipe ID.")
            res_data.setdefault("failedUpdates", []).append(recipe_id)
            continue
        # Threading actually ends up being slower if table's WCU aren't high enough; gets throttled
        thread = Thread(
            target=patch_recipe,
            args=(user_id, operations, recipe_id),
            kwargs=dict(batch=True, pool_res=res_data),
        )
        pool.append(thread)
        thread.start()

    for thread in pool:
        thread.join()

    if not res_data:
        return {}, 204
    return ResponseData(data=res_data), 200
Ejemplo n.º 11
0
def get_category(user_id: str, category_id: int) -> Response:
    """
    GET a specific category in the database.

    :param user_id: ID of the user
    :param category_id: ID of the category being retrieved
    :return: (Response specifics, status code)
    """
    logging.info(
        f"Getting category with ID {category_id} for user with ID {user_id}.")
    kwargs = format_query_fields(
        ["categoryId", "name", "updateTime", "createTime"])

    categories_table, _ = get_categories_table()
    try:
        item = categories_table.get_item(
            Key={
                "userId": user_id,
                "categoryId": category_id
            },
            **kwargs,
        ).get("Item", {})
    except ClientError as e:
        if e.response["Error"]["Code"] == "ValidationException":
            return (
                ResponseData(
                    message="One of the following parameters is invalid: "
                    f"{{User ID: {user_id}, Category ID: {category_id}}}"),
                400,
            )
        logging.exception(UNEXPECTED_EXCEPTION)
        raise
    if not item:
        return (
            ResponseData(
                message=
                f"User {user_id} does not have a category with ID {category_id}."
            ),
            404,
        )

    return ResponseData(data=item), 200
Ejemplo n.º 12
0
def confirm_forgot_password(body: Mapping[str, str]) -> Response:
    """
    Reset a user's password using a confirmation code and a new password.

    :param body: Mapping containing email, password, and confirmationCode keys
    :return: (Response specifics, status code)
    """
    logging.info("Starting confirm forgot password flow.")
    email, password, confirmation_code = verify_parameters(
        body, "email", "password", "confirmationCode")

    client: CognitoClient = boto3.client("cognito-idp")
    try:
        client.confirm_forgot_password(
            ClientId=CLIENT_ID,
            Username=email,
            ConfirmationCode=confirmation_code,
            Password=password,
        )
    except ParamValidationError as e:
        report = e.kwargs["report"]
        if "Invalid type for parameter ConfirmationCode" in report:
            return ResponseData(message="Invalid confirmation code."), 400
        elif "Invalid length for parameter Password" in report:
            return ResponseData(message="Invalid password."), 400
        else:
            logging.exception(UNEXPECTED_EXCEPTION)
            return ResponseData(message=UNEXPECTED_EXCEPTION), 500
    except client.exceptions.UserNotFoundException:
        return ResponseData(message="This email is not registered."), 404
    except client.exceptions.InvalidPasswordException:
        return ResponseData(message="Invalid password."), 400
    except client.exceptions.CodeMismatchException:
        return ResponseData(message="Invalid confirmation code."), 400
    except client.exceptions.NotAuthorizedException:
        return (
            ResponseData(message="Password reset has already been confirmed."),
            400,
        )
    except Exception:
        logging.exception(UNEXPECTED_EXCEPTION)
        return ResponseData(message=UNEXPECTED_EXCEPTION), 500

    logging.info("Confirm forgot password flow successful.")
    return ResponseData(
        message="Your password has been successfully reset!"), 200
Ejemplo n.º 13
0
def delete_category(user_id: str, category_id: int) -> Response:
    """
    DELETE the specified category from the database.

    :param user_id: ID of the user
    :param category_id: ID of the category being deleted
    :return: (Response specifics, status code)
    """
    table, resource = get_categories_table()

    logging.info(
        f"Deleting category with ID {category_id} for user with ID {user_id}.")
    try:
        table.delete_item(
            Key={
                "userId": user_id,
                "categoryId": category_id
            },
            ConditionExpression=Attr("userId").exists()
            & Attr("categoryId").exists(),
        )
    except resource.meta.client.exceptions.ConditionalCheckFailedException:
        return (
            ResponseData(
                message=
                f"User {user_id} does not have a category with ID {category_id}.",
            ),
            404,
        )
    except ClientError as e:
        if e.response["Error"]["Code"] == "ValidationException":
            return (
                ResponseData(
                    message="One of the following parameters is invalid: "
                    f"{{User ID: {user_id}, Category ID: {category_id}}}"),
                400,
            )
        logging.exception(UNEXPECTED_EXCEPTION)
        raise

    return {}, 204
Ejemplo n.º 14
0
def sign_up(body: Mapping[str, str]) -> Response:
    """
    Sign up using email and password.

    :param body: Mapping containing email and password keys
    :return: (Response specifics, status code)
    """
    logging.info("Starting sign up flow.")
    email, password = verify_parameters(body, "email", "password")

    client: CognitoClient = boto3.client("cognito-idp")
    try:
        res = client.sign_up(ClientId=CLIENT_ID,
                             Username=email,
                             Password=password)
    except client.exceptions.UsernameExistsException:
        return ResponseData(message="This email is already registered."), 409
    except (client.exceptions.InvalidPasswordException, ParamValidationError):
        return ResponseData(message="Invalid password."), 400
    except Exception:
        logging.exception(UNEXPECTED_EXCEPTION)
        return ResponseData(message=UNEXPECTED_EXCEPTION), 500

    meta_table, _ = get_meta_table()

    meta_table.put_item(
        Item={
            "userId": res["UserSub"],
            "signUpTime": datetime.utcnow().replace(microsecond=0).isoformat(),
        })

    logging.info("Sign up flow successful.")
    return (
        ResponseData(
            message=
            f"Sign up successful! A verification email has been sent to {email}."
        ),
        200,
    )
Ejemplo n.º 15
0
def sign_in(body: Mapping[str, str]) -> Response:
    """
    Sign in using email and password.

    :param body: Mapping containing email and password keys
    :return: (Response specifics, status code)
    """
    logging.info(f"Starting sign in flow. {body}")
    email, password = verify_parameters(body, "email", "password")

    client: CognitoClient = boto3.client("cognito-idp")

    try:
        res = client.admin_initiate_auth(
            AuthFlow="ADMIN_USER_PASSWORD_AUTH",
            AuthParameters={
                "USERNAME": email,
                "PASSWORD": password
            },
            ClientId=CLIENT_ID,
            UserPoolId=USER_POOL_ID,
        )
    except client.exceptions.InvalidParameterException as e:
        message: str = e.args[0]
        return (
            ResponseData(message=message[message.rindex(": ") +
                                         2:].replace("USERNAME", "EMAIL")),
            400,
        )
    except client.exceptions.NotAuthorizedException:
        return ResponseData(message="Incorrect password."), 401
    except client.exceptions.UserNotConfirmedException:
        return ResponseData(message="User is not verified."), 403
    except client.exceptions.UserNotFoundException:
        return ResponseData(message="This email is not registered."), 404
    except Exception:
        logging.exception(UNEXPECTED_EXCEPTION)
        return ResponseData(message=UNEXPECTED_EXCEPTION), 500

    res = res["AuthenticationResult"]
    access_token = res.get("AccessToken")
    refresh_token = res.get("RefreshToken")
    id_token = res.get("IdToken")

    user = client.get_user(AccessToken=access_token).get("Username")

    logging.info("Sign in flow successful.")
    return (
        ResponseData(
            message="You have been signed in.",
            data={
                "idToken": id_token,
                "refreshToken": refresh_token,
                "user": user
            },
        ),
        200,
    )
Ejemplo n.º 16
0
    def get(self):
        """Scrape a url for recipe info."""

        def _normalize_list(list_: Union[str, List[str]]) -> List[str]:
            """Normalize a list or string with possible leading markers to just a list."""
            return (
                [re.sub(r"^\d+[.:]? ?", "", entry) for entry in list_.split("\n")]
                if isinstance(list_, str)
                else list_
            )

        url = request.args.get("url")
        if not url:
            return ResponseData(message="No url provided."), 400
        logging.info(f"Scraping url: {url}")
        try:
            scraped = scrape_me(prepend_scheme_if_needed(url, "http"), wild_mode=True)
        except NoSchemaFoundInWildMode:
            return ResponseData(message=f"No recipe schema found at {url}"), 200
        except (ConnectionError, InvalidURL):
            return ResponseData(message=f"{url} is not a valid url."), 404
        except Exception:
            logging.exception(r"¯\_(ツ)_/¯")
            return ResponseData(message=r"¯\_(ツ)_/¯"), 500

        data = {
            "url": url,
            "name": scraped.title(),
            "imgSrc": scraped.image(),
            "adaptedFrom": scraped.site_name() or scraped.host(),
            "yield": scraped.yields(),
            "cookTime": scraped.total_time() or "",
            "instructions": _normalize_list(scraped.instructions()),
            "ingredients": _normalize_list(scraped.ingredients()),
        }
        logging.info(f"Found data:\n{pformat(data)}")
        return ResponseData(data=data), 200
Ejemplo n.º 17
0
def resend_code(body: Mapping[str, str]) -> Response:
    """
    Resend a confirmation code to a user's email.

    :param body: Mapping containing an email key
    :return: (Response specifics, status code)
    """
    logging.info("Starting resend code flow.")
    (email, ) = verify_parameters(body, "email")

    client: CognitoClient = boto3.client("cognito-idp")
    try:
        client.resend_confirmation_code(ClientId=CLIENT_ID, Username=email)
    except client.exceptions.UserNotFoundException:
        return ResponseData(message="This email is not registered."), 404
    except client.exceptions.InvalidParameterException:
        return ResponseData(message="User is already verified."), 400
    except Exception:
        logging.exception(UNEXPECTED_EXCEPTION)
        return ResponseData(message=UNEXPECTED_EXCEPTION), 500

    logging.info("Resend code flow successful.")
    return ResponseData(
        message=f"A verification code has been sent to {email}."), 200
Ejemplo n.º 18
0
def get_categories(user_id: str) -> Response:
    """
    GET all categories in the database.

    :param user_id: ID of the user
    :return: (Response specifics, status code)
    """
    logging.info(f"Getting all categories for user with ID {user_id}.")
    kwargs = format_query_fields(
        ["categoryId", "name", "updateTime", "createTime"])

    # TODO: Paginate
    table, _ = get_categories_table()
    items = table.query(KeyConditionExpression=Key("userId").eq(user_id),
                        **kwargs).get("Items", [])
    return ResponseData(data={"categories": items}), 200
Ejemplo n.º 19
0
def get_shopping_list(user_id: str) -> Response:
    """
    GET all shopping list items in the database.

    :param user_id: ID of the user
    :return: (Response specifics, status code)
    """
    logging.info(f"Getting shopping list for user with ID {user_id}.")
    kwargs = format_query_fields(["shoppingList"])

    meta_table, _ = get_meta_table()
    shopping_list = (
        meta_table.get_item(Key={"userId": user_id}, **kwargs)
        .get("Item", {})
        .get("shoppingList", [])
    )

    return ResponseData(data={"shoppingList": shopping_list}), 200
Ejemplo n.º 20
0
def delete_categories(user_id: str, category_ids: Iterable[int]):
    """
    Batch delete a list of category IDs.

    :param user_id: ID of the user
    :param category_ids: List of category IDs
    :return: (Response specifics, status code)
    """
    verify_parameters(category_ids, list_type=int)

    res_data = {}
    for category_id in category_ids:
        _, status_code = delete_category(user_id, category_id)
        status_code != 204 and res_data.setdefault("failedDeletions",
                                                   []).append(category_id)

    if not res_data:
        return {}, 204
    return ResponseData(data=res_data), 200
Ejemplo n.º 21
0
def delete_recipes(user_id: str, recipe_ids: Iterable[int]):
    """
    Batch delete a list of recipe IDs.

    :param user_id: ID of the user
    :param recipe_ids: List of recipe IDs
    :return: (Response specifics, status code)
    """
    verify_parameters(recipe_ids, list_type=int)

    res_data = {}
    for recipe_id in recipe_ids:
        if not isinstance(recipe_id, int):
            res_data.setdefault("failedDeletions", []).append(recipe_id)
            continue
        _, status_code = delete_recipe(user_id, recipe_id)
        status_code != 204 and res_data.setdefault("failedDeletions",
                                                   []).append(recipe_id)

    if not res_data:
        return {}, 204
    return ResponseData(data=res_data), 200
Ejemplo n.º 22
0
def put_recipes(user_id: str, body: Iterable[RecipeEntry]) -> Response:
    """
    PUT a list of recipes to the database.

    :param user_id: ID of the user
    :param body: Recipe data to replace with
    :return: (Response specifics, status code)
    """
    table, _ = get_recipes_table()
    recipes, failed_adds = [], []
    existing_categories, new_categories, category_failed_adds = [], [], []

    logging.info(f"Batch adding recipes from body {body}")
    with table.batch_writer() as batch:
        for recipe in body:
            res, status_code = post_recipe(user_id,
                                           recipe,
                                           recipes_table=batch)
            if status_code == 400:
                failed_adds.append(recipe)
                continue
            data = res["data"]
            for list_, name in zip(
                (existing_categories, new_categories, category_failed_adds),
                ("existingCategories", "newCategories", "categoryFailedAdds"),
            ):
                list_.extend(data.pop(name, []))

            recipes.append(data)

    res_data = {
        "recipes": recipes,
        "failedAdds": failed_adds,
        "existingCategories": existing_categories,
        "newCategories": new_categories,
        "categoryFailedAdds": category_failed_adds,
    }
    return ResponseData(data=res_data), 200
Ejemplo n.º 23
0
    def delete(self):
        """Batch delete categories."""
        category_ids = api.payload.get("categoryIds")
        if not isinstance(category_ids, list):
            raise AssertionError("categoryIds provided is not a list.")
        user_id = request.environ["USER_ID"]
        res_data = {}

        res, status_code = delete_categories(user_id, category_ids)
        if status_code != 204:
            res_data.update(res["data"])
            category_ids = [
                category_id
                for category_id in category_ids
                if category_id not in res_data["failedDeletions"]
            ]

        res, status_code = remove_categories_from_recipes(user_id, category_ids)
        if status_code != 204:
            res_data.update(res["data"])

        if not res_data:
            return {}, 204
        return ResponseData(data=res_data), 200
Ejemplo n.º 24
0
def method_not_allowed(_) -> Response:
    return ResponseData(message="Unsupported HTTP method."), 405
Ejemplo n.º 25
0
def method_not_implemented(_) -> Response:
    return ResponseData(message="Method not implemented."), 501
Ejemplo n.º 26
0
def assertion_error(e) -> Response:
    return ResponseData(message=e.args[0]), 400
Ejemplo n.º 27
0
def patch_category(user_id: str,
                   body: Mapping[str, Mapping[str, str]],
                   category_id: int,
                   batch: bool = False) -> Optional[Response]:
    """
    PATCH a category in the database, updating the specified entry.

    :param user_id: ID of the user
    :param body: Mapping of PATCH operations to category data. Currently supports 'update'
    :param category_id: ID of the category being updated
    :param batch: Whether or not this is part of a batch operation
    :return: (Response specifics, status code)
    """
    kwargs = {
        "UpdateExpression": "",
        "ExpressionAttributeNames": {},
        "ExpressionAttributeValues": {},
    }
    try:
        validation = {
            "update": dict,
        }
        verify_parameters(body,
                          valid_parameters=validation.keys(),
                          parameter_types=validation)
        (updates, ) = (body.get(parameter) for parameter in validation)

        validation = {
            "name": str,
        }
        verify_parameters(updates,
                          valid_parameters=validation.keys(),
                          parameter_types=validation)

        edit_time = datetime.utcnow().replace(microsecond=0).isoformat()
        updates["updateTime"] = edit_time
        kwargs[
            "UpdateExpression"] = f"SET {', '.join(f'#{k} = :{k}' for k in updates)}"
        kwargs["ExpressionAttributeNames"].update(
            {f"#{k}": k
             for k in updates})
        kwargs["ExpressionAttributeValues"].update(
            {f":{k}": v
             for k, v in updates.items()})
        kwargs["UpdateExpression"] = kwargs["UpdateExpression"].replace(
            "  ", " ")

    except AssertionError:
        if batch:  # Continue with the rest of the batch
            return {}, 400
        raise

    table, resource = get_categories_table()

    logging.info(
        f"Updating category with ID {category_id} for user with ID {user_id}.")
    try:
        table.update_item(
            Key={
                "userId": user_id,
                "categoryId": category_id
            },
            ConditionExpression=Attr("userId").exists()
            & Attr("categoryId").exists(),
            **kwargs,
        )
    except resource.meta.client.exceptions.ConditionalCheckFailedException:
        return (
            ResponseData(
                message=
                f"User {user_id} does not have a category with ID {category_id}.",
            ),
            404,
        )

    return {}, 204
Ejemplo n.º 28
0
def post_category(
    user_id: str,
    body: CategoryEntry,
    category_id: Optional[int] = None,
    *,
    batch: Optional[bool] = False,
    pool_res: Optional[MutableMapping[str, Union[List[str],
                                                 List[CategoryEntry]]]] = None,
) -> Response:
    """
    POST a category to the database, adding a new entry. If 'category_id' is specified, instead
    replace an existing category.

    :param user_id: ID of the user
    :param body: Category data to replace with
    :param category_id: ID of the category being replaced
    :param batch: Whether or not this is part of a batch operation
    :param pool_res: Response data used by the calling thread pool
    :return: (Response specifics, status code)
    """
    logging.info(body)
    try:
        # If not updating an existing category, name must be present
        # TODO: Verify types
        verify_parameters(
            body,
            "name" if not category_id else None,
            valid_parameters=editable_category_fields,
            parameter_types=editable_category_fields,
        )
    except AssertionError:
        if batch and pool_res:  # Continue with the rest of the batch
            pool_res["failed_adds"].append(body["name"])
            return {}, 400
        raise

    edit_time = datetime.utcnow().replace(microsecond=0).isoformat()
    body["updateTime"] = edit_time
    if category_id is None:  # POST
        status_code = 201
        body["createTime"] = edit_time
        category_id = get_next_id(user_id, "category")

        logging.info(
            f"Creating category with ID {category_id} for user with ID {user_id} and body {body}."
        )
    else:  # PUT
        status_code = 200
        kwargs = format_query_fields(["createTime"])
        categories_table, _ = get_categories_table()
        body["createTime"] = (categories_table.get_item(
            Key={
                "userId": user_id,
                "categoryId": category_id
            }, **kwargs).get("Item", {}).get("createTime", edit_time))

        logging.info(
            f"Updating category with ID {category_id} for user with ID {user_id} and body {body}."
        )

    body["categoryId"] = category_id
    categories_table, _ = get_categories_table()
    categories_table.put_item(Item={
        **body,
        "userId": user_id,
    })
    if batch and pool_res:
        pool_res["new_categories"].append(body)

    return ResponseData(data=body), status_code
Ejemplo n.º 29
0
def patch_recipe(
    user_id: str,
    body: Mapping[str, MutableMapping[str, Union[str, Union[Iterable[int],
                                                            Iterable[str]]]]],
    recipe_id: int,
    *,
    batch: Optional[bool] = False,
    pool_res: Optional[MutableMapping[str, List[int]]] = None,
) -> Response:
    """
    PATCH a recipe in the database, updating the specified entry.

    For categories: if being added or updated, specify by name. If being removed, specify by ID.

    :param user_id: ID of the user
    :param body: Mapping of PATCH operations to recipe data. Currently supports 'add', 'remove', and
                 'update'
    :param recipe_id: ID of the recipe being updated
    :param batch: Whether or not this is part of a batch operation
    :param pool_res: Response data used by the calling thread pool
    :return: (Response specifics, status code)
    """
    kwargs = {
        "UpdateExpression": "",
        "ExpressionAttributeNames": {},
        "ExpressionAttributeValues": {},
    }
    remove_kwargs = {
        "UpdateExpression": "",
        "ExpressionAttributeNames": {},
        "ExpressionAttributeValues": {},
    }
    res_data = {}

    try:
        validation = {
            "add": dict,
            "remove": dict,
            "update": dict,
        }
        verify_parameters(body,
                          valid_parameters=validation.keys(),
                          parameter_types=validation)
        adds, removes, updates = (body.get(parameter, {})
                                  for parameter in validation)
        c = "categories"
        if c in updates and (c in adds or c in removes):
            raise AssertionError(
                "Categories cannot be updated and added/removed simultaneously."
            )

        if adds:
            (categories_to_add, ) = verify_parameters(adds,
                                                      c,
                                                      valid_parameters=[c])
            verify_parameters(categories_to_add, list_type=str)

            existing_categories, new_categories, failed_adds = add_categories_from_recipe(
                user_id, categories_to_add)
            if existing_categories:
                res_data["existingCategories"] = existing_categories
            if new_categories:
                res_data["newCategories"] = new_categories
                kwargs["UpdateExpression"] += f"ADD #{c} :{c}"
                kwargs["ExpressionAttributeNames"][f"#{c}"] = c
                kwargs["ExpressionAttributeValues"][f":{c}"] = {
                    category["categoryId"]
                    for category in new_categories
                }
            if failed_adds:
                res_data["categoryFailedAdds"] = failed_adds

        if removes:
            # Can't add to and remove from a set in the same call, need separate kwargs
            (categories_to_remove, ) = verify_parameters(removes,
                                                         c,
                                                         valid_parameters=[c])
            verify_parameters(categories_to_remove, list_type=int)
            remove_kwargs["UpdateExpression"] = f"DELETE #{c} :{c}"
            remove_kwargs["ExpressionAttributeNames"][f"#{c}"] = c
            remove_kwargs["ExpressionAttributeValues"][f":{c}"] = set(
                categories_to_remove)

        if updates:
            verify_parameters(
                updates,
                valid_parameters=editable_recipe_fields.keys(),
                parameter_types=editable_recipe_fields,
            )
            verify_parameters(updates.get("ingredients", []), list_type=str)
            verify_parameters(updates.get("instructions", []), list_type=str)
            updates = add_image_from_recipe(updates)
            if "imgSrc" in updates:
                res_data["imgSrc"] = updates["imgSrc"]

            categories = updates.get(c)
            if categories:
                verify_parameters(categories, list_type=str)

                existing_categories, new_categories, failed_adds = add_categories_from_recipe(
                    user_id, categories_to_add)
                if existing_categories:
                    res_data["existingCategories"] = existing_categories
                if new_categories:
                    res_data["newCategories"] = new_categories
                    updates[c] = {
                        category["categoryId"]
                        for category in (*existing_categories, *new_categories)
                    }
                if failed_adds:
                    res_data["categoryFailedAdds"] = failed_adds
    except AssertionError:
        if batch:  # Continue with the rest of the batch
            pool_res.setdefault("failedUpdates", []).append(recipe_id)
            return {}, 400
        raise

    edit_time = datetime.utcnow().replace(microsecond=0).isoformat()
    updates["updateTime"] = edit_time

    kwargs[
        "UpdateExpression"] += f" SET {', '.join(f'#{k} = :{k}' for k in updates)}"
    kwargs["ExpressionAttributeNames"].update({f"#{k}": k for k in updates})
    kwargs["ExpressionAttributeValues"].update(
        {f":{k}": v
         for k, v in updates.items()})
    kwargs["UpdateExpression"] = kwargs["UpdateExpression"].replace("  ", " ")

    recipes_table, resource = get_recipes_table()

    logging.info(
        f"Updating recipe with ID {recipe_id} for user with ID {user_id} and body {body}."
    )
    try:
        if removes:
            recipes_table.update_item(
                Key={
                    "userId": user_id,
                    "recipeId": recipe_id
                },
                ConditionExpression=Attr("userId").exists()
                & Attr("recipeId").exists(),
                **remove_kwargs,
            )
        recipes_table.update_item(
            Key={
                "userId": user_id,
                "recipeId": recipe_id
            },
            ConditionExpression=Attr("userId").exists()
            & Attr("recipeId").exists(),
            **kwargs,
        )
    except resource.meta.client.exceptions.ConditionalCheckFailedException:
        pool_res.setdefault("failedUpdates", []).append(recipe_id)
        return (
            ResponseData(
                message=
                f"User {user_id} does not have a recipe with ID {recipe_id}.",
            ),
            404,
        )

    if not res_data:
        return {}, 204
    return ResponseData(data=res_data), 200
Ejemplo n.º 30
0
def post_recipe(
    user_id: str,
    body: RecipeEntry,
    recipe_id: Optional[int] = None,
    *,
    batch: Optional[bool] = False,
    recipes_table: Optional[Table] = None,
) -> Response:
    """
    POST a recipe to the database, adding a new entry. If 'recipe_id' is specified, instead replace
    an existing recipe.

    :param user_id: ID of the user
    :param body: Recipe data to replace with
    :param recipe_id: ID of the recipe being replaced
    :param batch: Whether or not this is part of a batch operation
    :param recipes_table: Different DynamoDB table to use. Used for batch writing
    :return: (Response specifics, status code)
    """
    # If not updating an existing recipe, name must be present
    # TODO: Verify types
    try:
        verify_parameters(
            body,
            "name" if not recipe_id else None,
            valid_parameters=editable_recipe_fields,
            parameter_types=editable_recipe_fields,
        )
        verify_parameters(body.get("ingredients", []), list_type=str)
        verify_parameters(body.get("instructions", []), list_type=str)
    except AssertionError:
        if batch:  # Continue with the rest of the batch
            return {}, 400
        raise
    body = {k: v for k, v in body.items() if v}

    res_data = {}
    categories_to_add = body.pop("categories", [])

    if categories_to_add:
        existing_categories, new_categories, failed_adds = add_categories_from_recipe(
            user_id, categories_to_add)
        body["categories"] = {
            category["categoryId"]
            for category in (*existing_categories, *new_categories)
        }
        if existing_categories:
            res_data["existingCategories"] = existing_categories
        if new_categories:
            res_data["newCategories"] = new_categories
        if failed_adds:
            res_data["categoryFailedAdds"] = failed_adds

    edit_time = datetime.utcnow().replace(microsecond=0).isoformat()
    body["updateTime"] = edit_time
    body = add_image_from_recipe(body)
    if recipe_id is None:  # POST
        status_code = 201
        body["createTime"] = edit_time
        recipe_id = get_next_id(user_id, "recipe")

        logging.info(
            f"Creating recipe with ID {recipe_id} for user with ID {user_id} and body {body}."
        )
    else:  # PUT
        status_code = 200
        recipes_table, _ = get_recipes_table()
        create_time = (recipes_table.get_item(Key={
            "userId": user_id,
            "recipeId": recipe_id
        }).get("Item", {}).get("createTime", edit_time))
        body["createTime"] = create_time
        logging.info(
            f"Updating recipe with ID {recipe_id} for user with ID {user_id} and body {body}."
        )

    body["recipeId"] = recipe_id
    if not recipes_table:
        recipes_table, _ = get_recipes_table()
    recipes_table.put_item(Item={
        **body,
        "userId": user_id,
    })

    res_data.update(body)
    return ResponseData(data=res_data), status_code