3
\$\begingroup\$

I have been working on where I create a payload of dicts with different values as store, name etc etc which you will see very soon. The idea is that with the payload I send to this script, it should see if the values is in the payload (dict) and if it is then add it into discord embed. As simple as it sounds.

I have done something like this:

#!/usr/bin/python3
# -*- coding: utf-8 -*-
import time
from threading import Thread
from typing import Dict

import pendulum
from discord_webhook import DiscordEmbed, DiscordWebhook
from loguru import logger

mixed_filtered = {
    'test1': 'https://discordapp.com/api/webhooks/529345345345345/6h4_yshmNDKktdT-0VevOhqdXG9rDhRWclIfDD4jY8IbdCQ5-kllob-k1251252151',
    'test2': 'https://discordapp.com/api/webhooks/529674575474577/6h4_yshmNDKktdT-0VevOhqdXG9rDhRWclIfDD4jY8IbdCQ5-kllob-fhgdfdghfhdh'
}

mixed_unfiltered = {
    'test1': 'https://discordapp.com/api/webhooks/12412421412412/6h4_yshmNDKktdT-0VevOhqdXG9rDhR12412412414Q5-kllob-kI2jAxCZ5PdIn',
    'test2': 'https://discordapp.com/api/webhooks/529617352682110997/6h4_yshmNDKktdT-0VevOhqdXG912412412412IbdCQ5-kllob-kI2jAxCZ5PdIn'
}


def create_embed(payload: dict) -> None:
    # -------------------------------------------------------------------------
    # Name of the product, URL of the product & New! or Restock! url
    # -------------------------------------------------------------------------
    embed = DiscordEmbed(
        url=payload["link"],
        description=payload["status"],
        color=8149447
    )

    # -------------------------------------------------------------------------
    # Image product
    # -------------------------------------------------------------------------
    embed.set_thumbnail(
        url=payload["image"]
    )

    # -------------------------------------------------------------------------
    # Store Name
    # -------------------------------------------------------------------------
    embed.add_embed_field(
        name="Site",
        value=f'{payload["store"]}'
    )

    # -------------------------------------------------------------------------
    # The price of the product
    # -------------------------------------------------------------------------
    if payload.get("price"):
        embed.add_embed_field(
            name="Price",
            value=payload["price"]
        )

    # -------------------------------------------------------------------------
    # If store is Nike
    # -------------------------------------------------------------------------
    if "Nike" in payload["store"]:

        if payload.get("nikeStatus"):
            embed.add_embed_field(
                name="\u200b",
                value="\u200b"
            )

            embed.add_embed_field(
                name="Status",
                value=payload["nikeStatus"]
            )

        # -------------------------------------------------------------------------
        # Nike  Sales Channel
        # -------------------------------------------------------------------------
        if payload.get("salesChannel"):
            embed.add_embed_field(
                name="Sales Channel",
                value="\n".join(payload["salesChannel"])
            )

    # -------------------------------------------------------------------------
    # Sizes available
    # Add extra spaces for sizes to make it cleaner for discord embed
    # -------------------------------------------------------------------------
    if payload.get("sizes"):
        payload["stock"] = sum(v for v in payload["sizes"].values() if v)

        payload["sizes"] = [f"{k} - ({v})" if v else k for k, v in payload["sizes"].items()]
        # If we have stock in values then sum it up

        embed.add_embed_field(
            name="\u200b",
            value="\u200b"
        )

        characterCount, i = 0, 0
        for j, item in enumerate(payload["sizes"]):

            # There is a limitation for Discord where if we reach over 1020 characters for one embed column.
            # IT will throw a error. Now I check if the characters count is less than 900 then we create a new embed.
            if len(item) + characterCount > 900:
                embed.add_embed_field(
                    name="Sizes",
                    value="\n".join(payload["sizes"][i:j])
                )
                characterCount, i = len(item), j
            else:
                characterCount += len(item)

        if characterCount:
            embed.add_embed_field(
                name="Sizes",
                value="\n".join(payload["sizes"][i:])
            )

        embed.add_embed_field(
            name="\u200b",
            value="\u200b"
        )

        embed.add_embed_field(
            name="\u200b",
            value="\u200b"
        )

    # -------------------------------------------------------------------------
    # If store is footlocker
    # -------------------------------------------------------------------------
    if "Footlocker" in payload["store"]:
        if payload.get("stockLoaded"):
            embed.add_embed_field(
                name="Stock Loaded",
                value=payload["stockLoaded"].upper()
            )

        if payload.get("styleCode"):
            embed.add_embed_field(
                name="\u200b",
                value="\u200b"
            )
            embed.add_embed_field(
                name="Style Code",
                value=payload["styleCode"]
            )

    # -------------------------------------------------------------------------
    # Release date for the product
    # -------------------------------------------------------------------------
    if payload.get("releaseDate"):
        embed.add_embed_field(
            name="Release Date",
            value=payload["releaseDate"].to_datetime_string()
        )

    # -------------------------------------------------------------------------
    # Stock keeping unit etc. 508214-660
    # -------------------------------------------------------------------------
    if payload.get("sku"):
        embed.add_embed_field(
            name="SKU",
            value=payload["sku"]
        )

    # -------------------------------------------------------------------------
    # Total stock of the product
    # -------------------------------------------------------------------------
    if payload.get("stock"):
        embed.add_embed_field(
            name="Total Stock",
            value=payload["stock"]
        )

    # -------------------------------------------------------------------------
    # Login/Cart/Checkout shortcut links
    # -------------------------------------------------------------------------
    embed.add_embed_field(
        name="Shortcuts Links",
        value=f'{" | ".join(shortcuts for shortcuts in payload["shortcut"])}'
    )

    # -------------------------------------------------------------------------
    # Quick task for bots
    # -------------------------------------------------------------------------
    if payload.get("quicktask"):
        embed.add_embed_field(
            name="Quick Tasks",
            value=f'{" | ".join(shortcuts for shortcuts in payload["quicktask"])}'
        )

    # -------------------------------------------------------------------------
    # Footer timestamp
    # -------------------------------------------------------------------------
    embed.set_footer(
        text=f'AutoSnkr | {pendulum.now("Europe/Stockholm").format("YYYY-MM-DD [[]HH:mm:ss.SSSS[]]")}'
    )

    # -------------------------------------------------------------------------
    # Set title on the embed
    # -------------------------------------------------------------------------
    if payload.get('stock') and payload.get('name'):
        embed.title = f'({payload["stock"]}) {payload["name"]}'
    elif payload.get('name'):
        embed.title = payload["name"]
    else:
        embed.title = payload.get('link')

    # -------------------------------------------------------------------------
    # Send payload/embed to Discord Notification function
    # -------------------------------------------------------------------------
    collection = mixed_filtered if payload["keyword"] else mixed_unfiltered

    for region, discord_collection in collection.items():
        webhook = DiscordWebhook(
            url=discord_collection,
            username="AutoSnkr Monitor",
        )

        webhook.add_embed(embed)

        # Adding thread so each URL can post as fast as possible without needing to wait for each other
        Thread(
            target=post_embed,
            args=(
                payload,
                region,
                webhook
            )
        ).start()


def post_embed(payload: Dict, region: str, webhook: DiscordWebhook) -> None:
    success: bool = False

    while not success:
        try:
            response = webhook.execute()
            success = response.ok

            # If we get a 429, retry after a short delay
            if response.status_code == 429:
                sleep_time = int(response.headers["retry-after"]) / 1000
                logger.debug(f"Rate limit -> Retrying in {sleep_time} seconds")
                time.sleep(sleep_time)
                continue

            # any response other than a 429 or a 200 OK is an error.
            if not response.ok:
                # FIXME Add discord notficiation and raise exception
                pass

            logger.info(f"Succesfully sent to Discord Reporter -> {region}")

        except Exception as err:
            # FIXME Add discord notficiation and raise exception
            pass


if __name__ == '__main__':
    create_embed(
        {
            "store": "Basket4ballers",
            "link": "https://www.basket4ballers.com/en/pg/26471-nike-pg5-bred-cw3143-101.html",
            "name": "Nike PG5 Bred",
            "price": "EUR 119.9",
            "image": "https://cdn1.basket4ballers.com/114821-large_default/nike-pg5-bred-cw3143-101.jpg",
            "sizes": {
                "[EU 38.5](https://www.basket4ballers.com/?controller=cart&add=1&as=true&qty=1&id_product=26471&token=e4d64f25476dcee4b08744d382dc405b&ipa=205049)": 1,
                "[EU 39](https://www.basket4ballers.com/?controller=cart&add=1&as=true&qty=1&id_product=26471&token=e4d64f25476dcee4b08744d382dc405b&ipa=205052)": 1,
                "[EU 40](https://www.basket4ballers.com/?controller=cart&add=1&as=true&qty=1&id_product=26471&token=e4d64f25476dcee4b08744d382dc405b&ipa=205055)": 3,
                "[EU 40.5](https://www.basket4ballers.com/?controller=cart&add=1&as=true&qty=1&id_product=26471&token=e4d64f25476dcee4b08744d382dc405b&ipa=205058)": 4,
                "[EU 41](https://www.basket4ballers.com/?controller=cart&add=1&as=true&qty=1&id_product=26471&token=e4d64f25476dcee4b08744d382dc405b&ipa=205061)": 9,
                "[EU 42](https://www.basket4ballers.com/?controller=cart&add=1&as=true&qty=1&id_product=26471&token=e4d64f25476dcee4b08744d382dc405b&ipa=205064)": 11,
                "[EU 42.5](https://www.basket4ballers.com/?controller=cart&add=1&as=true&qty=1&id_product=26471&token=e4d64f25476dcee4b08744d382dc405b&ipa=205067)": 11,
                "[EU 43](https://www.basket4ballers.com/?controller=cart&add=1&as=true&qty=1&id_product=26471&token=e4d64f25476dcee4b08744d382dc405b&ipa=205070)": 16,
                "[EU 44](https://www.basket4ballers.com/?controller=cart&add=1&as=true&qty=1&id_product=26471&token=e4d64f25476dcee4b08744d382dc405b&ipa=205073)": 21,
                "[EU 44.5](https://www.basket4ballers.com/?controller=cart&add=1&as=true&qty=1&id_product=26471&token=e4d64f25476dcee4b08744d382dc405b&ipa=205076)": 15,
                "[EU 45](https://www.basket4ballers.com/?controller=cart&add=1&as=true&qty=1&id_product=26471&token=e4d64f25476dcee4b08744d382dc405b&ipa=205079)": 20,
                "[EU 45.5](https://www.basket4ballers.com/?controller=cart&add=1&as=true&qty=1&id_product=26471&token=e4d64f25476dcee4b08744d382dc405b&ipa=205082)": 7,
                "[EU 46](https://www.basket4ballers.com/?controller=cart&add=1&as=true&qty=1&id_product=26471&token=e4d64f25476dcee4b08744d382dc405b&ipa=205085)": 17,
                "[EU 47](https://www.basket4ballers.com/?controller=cart&add=1&as=true&qty=1&id_product=26471&token=e4d64f25476dcee4b08744d382dc405b&ipa=205088)": 7,
                "[EU 47.5](https://www.basket4ballers.com/?controller=cart&add=1&as=true&qty=1&id_product=26471&token=e4d64f25476dcee4b08744d382dc405b&ipa=205091)": 5,
                "[EU 48](https://www.basket4ballers.com/?controller=cart&add=1&as=true&qty=1&id_product=26471&token=e4d64f25476dcee4b08744d382dc405b&ipa=205094)": 3,
                "[EU 48.5](https://www.basket4ballers.com/?controller=cart&add=1&as=true&qty=1&id_product=26471&token=e4d64f25476dcee4b08744d382dc405b&ipa=205097)": 2,
                "[EU 49.5](https://www.basket4ballers.com/?controller=cart&add=1&as=true&qty=1&id_product=26471&token=e4d64f25476dcee4b08744d382dc405b&ipa=205100)": 1},
            "shortcut": ["[Login](https://www.basket4ballers.com/en/authentification?back=my-account)",
                         "[Cart](https://www.basket4ballers.com/en/commande)",
                         "[Checkout Delivery](https://www.basket4ballers.com/en/commande?step=1)",
                         "[Checkout Shipping Service](https://www.basket4ballers.com/en/commande)",
                         "[Checkout PAyment](https://www.basket4ballers.com/en/commande)"],
            "webhook": "mixed",
            "status": "Restock!",
            "keyword": True
        }
    )

The mock data is at the very bottom but in the future I will instead send the payload to the function.

I wonder what can I do to try to have less code but that still does the job. I feel like there should be a way more cleaner way to do this than what I did but looking forward to see what can be improved :)

Let me know if there is any missing information. The script should be runnable by copy pasting it but make sure to create your own discord webhooks to test the embed. I will unfortunately need to modify them so no one can spam me :)

\$\endgroup\$
0
3
+50
\$\begingroup\$

If I were a smart man I'd give up on recommending that you stop using a payload dictionary for internal data representation and instead use classes, but I'm not a smart man. Please. I implore you. We're not in JavaScript - objects don't have to be dictionaries. This could be well-represented by a class for product, and a class for product size.

Otherwise:

if payload.get("price") should be replaced by if 'price' in payload if it were only that statement; but since you actually use it,

if payload.get("price"):
    embed.add_embed_field(
        name="Price",
        value=payload["price"]
    )

should become

price = payload.get('price')
if price is not None:
    embed.add_embed_field(name='Price', value=price)

More broadly: your create_embed is a presentation function but mixes in logic concerns such as stock summation, and store-specific logic (i.e. Footlocker). That should be separated.

Your

# FIXME Add discord notficiation and raise exception

first of all has a typo - notficiation -> notification - and second of all, while this is waiting to be fixed it's of crucial importance that you not swallow exceptions. Part of development and debugging is seeing errors, and your code breaks that. So replace your pass with a raise in the meantime.

\$\endgroup\$
4
  • \$\begingroup\$ Hi again! :) Regarding the payload being dictionary it is something that I still have issue with and it is something we have worked on together as you remember :) My problem is that I do not know how I can work with only classes where I have previously asked for a code review regarding the part where I actually send the payload to the create_embed which can be found from here codereview.stackexchange.com/q/259867/223687 - If I could get more description over how I can actually do a comparision with just classes and throw away the dicts. That would be very cool to know how \$\endgroup\$ – ProtractorNewbie Apr 29 at 16:29
  • \$\begingroup\$ Currently I do not know how to do it as for today but am willing to learn :) Regarding the seperation is something I will work on. I did realize that I have done something more complicated and harder to read, thanks for that :) Will work on that part but now im more concern if im going to change with classes then I most likely wont need the dicts at all. Lets see what you have to say :D Excited to see! \$\endgroup\$ – ProtractorNewbie Apr 29 at 16:31
  • \$\begingroup\$ Let us continue this in chat: chat.stackexchange.com/rooms/123085/… \$\endgroup\$ – ProtractorNewbie Apr 29 at 18:37
  • \$\begingroup\$ Hey again! Unfortunately I need to go back to sleep as I was waiting for a reply. :) I hope and wish that I could get an answer back, would mean alot for me! \$\endgroup\$ – ProtractorNewbie Apr 29 at 23:48

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.