Esempio n. 1
0
class SaleHandler(metaclass=Singleton):
    def __init__(self) -> None:
        super().__init__()

        self._logger = Logger(self.__class__.__name__)
        self._logger.info("Initializing Sale handler...")

        self._db = TinyDB("db/sale.json")

    def create_sale(self, item: Item, quantity: int, price: int, seller: str,
                    seller_id: int) -> Sale:
        result: Sale = Sale(
            item_uid=item.uid,
            quantity=quantity,
            price=price,
            seller=seller,
            seller_discord_id=seller_id,
            from_date_timestamp=datetime.today().timestamp(),
            to_date_timestamp=(datetime.today() +
                               timedelta(days=7)).timestamp(),
        )
        self._db.insert(result.__dict__)

        return result

    def get_sale_by_sale_uid(self, sale_uid: str) -> Optional[Sale]:
        sale = Query()
        result = self._db.get(sale["_sale_uid"] == sale_uid)

        if not result:
            return None
        else:
            return Sale.from_dict(result)

    def get_all_sales(self) -> List[Sale]:
        return self._parse_entities(self._db.all())

    def get_sales_by_item_uid(self, item_uid: str) -> List[Sale]:
        sale = Query()
        return self._parse_entities(
            self._db.search(sale["_item_uid"] == item_uid))

    def get_sales_by_item_uids(self, item_uids: List[str]) -> List[Sale]:
        sale = Query()
        return self._parse_entities(
            self._db.search(sale["_item_uid"].one_of(item_uids)))

    def remove_sale_by_sale_uid(self, sale_uid: str) -> int:
        sale = Query()
        return len(self._db.remove(sale["_sale_uid"] == sale_uid))

    def remove_stale_sales(self) -> int:
        sale = Query()
        return len(
            self._db.remove(
                sale["_to_date_timestamp"] < datetime.today().timestamp()))

    @staticmethod
    def _parse_entities(entities: List[Dict]) -> List[Sale]:
        return list(map(lambda x: Sale.from_dict(x), entities))
Esempio n. 2
0
    def __init__(self) -> None:
        super().__init__()

        self._logger = Logger(self.__class__.__name__)
        self._logger.info("Initializing Item handler...")

        self._logger.info("Loading item list...")
        self._items: Set[Item] = set()

        with open("resources/items.json") as json_file:
            data = json.load(json_file)

            for i in data:
                if i["precio"] != "-":
                    item2add: Item = Item(i["nombre"], i["precio"])

                    if any(item2add.uid == item.uid for item in self._items):
                        # this scenario is _EXTREMELY_ unlikely is dataset was correct to begin with.
                        # collision(s) are a strong indicator of repeated items inside dataset.
                        raise Exception(
                            "Item UID collision detected with item {}. Aborting operation."
                            .format(item2add))

                    self._items.add(item2add)

        self._logger.info("Loaded {} sellable items...".format(
            self._items.__len__()))
Esempio n. 3
0
    def __init__(self) -> None:
        super().__init__()

        self._logger = Logger(self.__class__.__name__)
        self._logger.info("Initializing Sale handler...")

        self._db = TinyDB("db/sale.json")
Esempio n. 4
0
class Debug:
    """
    This class includes environmental settings and code that should run on non-production environments for debugging
    purposes.
    """
    def __init__(self) -> None:
        self._logger = Logger(self.__class__.__name__)
        self._logger.debug(
            "Debug mode is active. Additional development features are enabled."
        )

        import pretty_errors

        pretty_errors.configure(
            separator_character="*",
            filename_display=pretty_errors.FILENAME_EXTENDED,
            line_number_first=True,
            display_link=True,
            lines_before=5,
            lines_after=2,
            line_color=pretty_errors.RED + "> " +
            pretty_errors.default_config.line_color,
            code_color="  " + pretty_errors.default_config.line_color,
            truncate_code=True,
            display_locals=False,
        )
Esempio n. 5
0
    def __init__(self) -> None:
        super().__init__()

        self._logger = Logger(self.__class__.__name__, TRACE_LEVELV_NUM)
        self._logger.info("Initializing internationalization (l18n) module...")

        self._language: str = LOCALE_EN
        self._gettext: Optional[Callable[[str], str]] = None
    def __init__(self) -> None:
        super().__init__()
        self._logger = Logger(self.__class__.__name__)

        self._logger.info("Initializing command handler...")
        self._item_handler = ItemHandler()
        self._sale_handler = SaleHandler()

        self._logger.info("Setting up background jobs...")
        self._stale_offers_cleanup_scheduler = StaleOfferCleanupJob().start()
class StaleOfferCleanupJob(metaclass=Singleton):

    _task: Task

    def __init__(self) -> None:
        super().__init__()

        self._logger = Logger(self.__class__.__name__)
        self._logger.info("Initializing stale offers cleanup job...")
        self._sale_handler = SaleHandler()

    async def _run(self) -> None:
        self._logger.info(
            "Stale offers scheduler started successfully. Will run once per hour."
        )
        while True:
            # execute task
            self._logger.info("Executing stale offers cleanup task...")

            removed_entries: int = self._sale_handler.remove_stale_sales()

            self._logger.info(
                "Removed {} stale entries.".format(removed_entries))

            # wait before next iteration
            await asyncio.sleep(3600)

    def start(self) -> StaleOfferCleanupJob:
        self._task = asyncio.get_event_loop().create_task(self._run())
        return self

    def stop(self) -> bool:
        return self._task.cancel()
Esempio n. 8
0
    def __init__(self) -> None:
        self._logger = Logger(self.__class__.__name__)
        self._logger.debug(
            "Debug mode is active. Additional development features are enabled."
        )

        import pretty_errors

        pretty_errors.configure(
            separator_character="*",
            filename_display=pretty_errors.FILENAME_EXTENDED,
            line_number_first=True,
            display_link=True,
            lines_before=5,
            lines_after=2,
            line_color=pretty_errors.RED + "> " +
            pretty_errors.default_config.line_color,
            code_color="  " + pretty_errors.default_config.line_color,
            truncate_code=True,
            display_locals=False,
        )
Esempio n. 9
0
class MercadoAO(commands.Bot):
    """
    MercadoAO's Discord Client. Soft wrapper around discord.py's bot library.
    """
    def __init__(self, **options):
        super().__init__(**options)

        self._logger = Logger(self.__class__.__name__)

        self._logger.info("=== Initializing MercadoAO Discord Bot ===")
        # bot will be truly ready when the on_ready() function gets called

    async def on_ready(self) -> None:
        self._logger.debug("Logged on as {0}!".format(self.user))
        self._logger.info("=== MercadoAO initialized ===")
Esempio n. 10
0
class ItemHandler(metaclass=Singleton):
    def __init__(self) -> None:
        super().__init__()

        self._logger = Logger(self.__class__.__name__)
        self._logger.info("Initializing Item handler...")

        self._logger.info("Loading item list...")
        self._items: Set[Item] = set()

        with open("resources/items.json") as json_file:
            data = json.load(json_file)

            for i in data:
                if i["precio"] != "-":
                    item2add: Item = Item(i["nombre"], i["precio"])

                    if any(item2add.uid == item.uid for item in self._items):
                        # this scenario is _EXTREMELY_ unlikely is dataset was correct to begin with.
                        # collision(s) are a strong indicator of repeated items inside dataset.
                        raise Exception(
                            "Item UID collision detected with item {}. Aborting operation."
                            .format(item2add))

                    self._items.add(item2add)

        self._logger.info("Loaded {} sellable items...".format(
            self._items.__len__()))

    def is_uid(self, query: str) -> bool:
        """
        Returns whether the given query should be considered an UID or not.

        :param query: The string to search.
        :return:
        """
        return UID_PREFIX in query or self.uid_search(query) is not None

    def sanitize_uid(self, uid: str) -> Optional[str]:
        """
        Returns a sanitized UID (a UID without prefix, if applicable); or None if given parameter was not an UID.

        :param uid: The UID to sanitize.
        """
        return uid.replace(UID_PREFIX, "") if self.is_uid(uid) else None

    def uid_search(self, uid: str) -> Optional[Item]:
        """
        Returns the Item that matches the given UID, or None if there was no match.

        :param uid: the UID to search for.
        :return:
        """
        uid = uid.replace(UID_PREFIX, "")
        return next((item for item in self._items if item.uid.__eq__(uid)),
                    None)

    def search(self, search_param: str) -> Set[Item]:
        """
        Returns a list of potentially sellable Item(s) from the game that match the given query.

        It uses a mix between literal substring comparisons and difflib's SequenceMatcher. The best matches among the
        possibilities are returned in a set.

        :param search_param: word for which close matches are desired
        :return: a list of the best "good enough" matches.
        """

        # initialize search ---

        search_param = unidecode.unidecode(
            search_param.lower())  # sanitize input

        def sequence_matcher(
                items: Iterable[Item], n: int,
                cutoff: float) -> List[Item]:  # define aux fuzzy matcher
            matches: List = list()
            for item in items:
                if len(matches) > n:
                    break
                if difflib.SequenceMatcher(
                        None, search_param,
                        item.sanitized_name).ratio() >= cutoff:
                    matches.append(item)
            return matches

        # execute search algorithm ---

        full_match: Optional[Item] = next(
            (item for item in self._items
             if item.sanitized_name.__eq__(search_param)), None)
        if full_match is not None:
            return {
                full_match
            }  # if we got one full match, stop searching - save resources

        # if we didn't get one full match, we are going to do a best-effort fuzzy search
        substring_name_matches: Set[Item] = set(
            filter(
                lambda x: True
                if search_param in x.sanitized_name else False, self._items))
        fuzzy_name_matches: List[Item] = sequence_matcher(
            self._items, 100, 0.6)

        substring_name_matches.update(fuzzy_name_matches)

        return substring_name_matches
Esempio n. 11
0
class Singleton(type):
    """
    A metaclass that creates a Singleton base class when called. This is a thread-safe implementation of Singleton.

    This metaclass has been further customized to allow Singletons within Singletons, by implementing a double-lock
    check mechanism (https://stackoverflow.com/a/59476524).

    A metaclass is the class of a class; that is, a class is an instance of its metaclass. You find the metaclass of
    an object in Python with type(obj). Normal new-style classes are of type type. Logger in the code above will be
    of type class 'your_module.Singleton', just as the (only) instance of Logger will be of type class
    'your_module.Logger'. When you call logger with Logger(), Python first asks the metaclass of Logger, Singleton,
    what to do, allowing instance creation to be pre-empted. This process is the same as Python asking a class what
    to do by calling __getattr__ when you reference one of it's attributes by doing myclass.attribute.

    A metaclass essentially decides what the definition of a class means and how to implement that definition. See
    for example http://code.activestate.com/recipes/498149/, which essentially recreates C-style structs in Python
    using metaclasses. The thread What are some (concrete) use-cases for metaclasses? also provides some examples,
    they generally seem to be related to declarative programming, especially as used in ORMs.

    In general, it makes sense to use a metaclass to implement a singleton. A singleton is special because is
    created only once, and a metaclass is the way you customize the creation of a class. Using a metaclass gives you
    more control in case you need to customize the singleton class definitions in other ways.

    Your singletons won't need multiple inheritance (because the metaclass is not a base class), but for subclasses
    ofthe created class that use multiple inheritance, you need to make sure the singleton class is the
    first / leftmost one with a metaclass that redefines __call__ This is very unlikely to be an issue. The instance
    dict is not in the instance's namespace so it won't accidentally overwrite it.

    You will also hear that the singleton pattern violates the "Single Responsibility Principle" -- each class
    should do only one thing. That way you don't have to worry about messing up one thing the code does if you need
    to change another, because they are separate and encapsulated. The metaclass implementation passes this test.
    The metaclass is responsible for enforcing the pattern and the created class and subclasses need not be aware
    that they are singletons.

    https://refactoring.guru/design-patterns/singleton/python/example#example-1
    https://stackoverflow.com/a/6798042
    """

    _logger = Logger("Singleton")

    _instances: Dict = {}
    _instance_locks: Dict = {}
    _singleton_lock: Lock = threading.Lock()

    def __call__(cls, *args, **kwargs):
        # double-checked locking pattern
        cls._logger.trace("Inside __call__ of Singleton...")
        cls._logger.trace(f"Called By {cls}...")
        if cls not in cls._instances:
            cls._logger.trace("Trying to get lock...")
            with cls._singleton_lock:
                lock = cls._instance_locks.setdefault(cls, threading.Lock())
            with lock:
                cls._logger.trace("Got lock...")
                if cls not in cls._instances:
                    cls._logger.trace(f"Object of type {cls} being created...")
                    cls._instances[cls] = super(Singleton,
                                                cls).__call__(*args, **kwargs)
                    with cls._singleton_lock:
                        del cls._instance_locks[cls]
        cls._logger.trace(f"Returning created Instance {cls}")
        return cls._instances[cls]
Esempio n. 12
0
class CommandHandler:
    def __init__(self) -> None:
        super().__init__()
        self._logger = Logger(self.__class__.__name__)

        self._logger.info("Initializing command handler...")
        self._item_handler = ItemHandler()
        self._sale_handler = SaleHandler()

        self._logger.info("Setting up background jobs...")
        self._stale_offers_cleanup_scheduler = StaleOfferCleanupJob().start()

    async def sell_handler(
        self,
        ctx: Context,
        item_to_sell: str,
        quantity: int,
        price: int,
        announce: bool,
        announcement_channel_id: Optional[int],
    ) -> None:
        self._logger.debug(
            "[SELL] - [{}] command called by [{}] with arguments [{}] [{}] [{}]"
            .format(ctx.command, ctx.author, item_to_sell, quantity, price))

        item: Item
        if self._item_handler.is_uid(query=item_to_sell):
            item = get_or_else_throw(
                self._item_handler.uid_search(uid=item_to_sell))
        else:
            search: Set[Item] = self._item_handler.search(
                search_param=item_to_sell)
            if len(search) != 1:
                await self.send_partitioned_message(
                    ctx.author,
                    _("Your sale of [{}] matched multiple items. "
                      "Please make your offer again with a more specific argument (uid's are also accepted). "
                      "Potential matches: {}").format(
                          item_to_sell, set(map(lambda x: str(x), search))),
                )
                return
            item = search.pop()

        sale: Sale = self._sale_handler.create_sale(item=item,
                                                    quantity=quantity,
                                                    price=price,
                                                    seller=str(ctx.author),
                                                    seller_id=ctx.author.id)

        await ctx.author.send(
            _("Your sale of [{}] units of [{}] for [{}] has been accepted and published"
              ).format(sale.quantity, item.name, sale.price))

        if announce and announcement_channel_id:
            await self.send_announcement(ctx, announcement_channel_id,
                                         str(sale))

    async def buy_handler(self, ctx: Context, sale_uid: str) -> None:
        self._logger.debug(
            "[BUY] - [{}] command called by [{}] with argument [{}]".format(
                ctx.command, ctx.author, sale_uid))

        sale: Optional[Sale] = self._sale_handler.get_sale_by_sale_uid(
            sale_uid=sale_uid)

        if not sale:
            await ctx.author.send(
                _("Your buy request for ID [{}] did not match any ongoing sales. Maybe it has been bought already? "
                  "Check ID and try again.").format(sale_uid))
            return

        buyer: User = ctx.author
        await ctx.author.send(
            _("Congratulations! You have bought [{}] units of [{}] for [{}]! "
              "I have already DMed the seller [{}] with details of the transaction. "
              "Message him to complete delivery.").format(
                  sale.quantity, sale.item, sale.price, sale.seller))
        await self.get_user_by_id(ctx, user_id=sale.seller_discord_id).send(
            _("Congratulations! Your sale of [{}] units of [{}] for [{}] has been bought by [{}]! "
              "DM buyer to complete the transaction!").format(
                  sale.quantity, sale.item, sale.price, buyer))
        self._sale_handler.remove_sale_by_sale_uid(sale.sale_uid)

    async def list_all_handler(self, ctx: Context) -> None:
        self._logger.debug("[LIST ALL] - [{}] command called by [{}]".format(
            ctx.command, ctx.author))

        search_results: List[Sale] = self._sale_handler.get_all_sales()

        if not search_results:
            await ctx.author.send(_("No sales currently going on"))
        else:
            await ctx.author.send(
                _("The following sales are currently undergoing:"))
            await self.send_partitioned_message(
                ctx.author, "\n".join(str(sale) for sale in search_results))

    async def list_handler(self, ctx: Context, query: str) -> None:
        self._logger.debug(
            "[LIST] - [{}] command called by [{}] with argument [{}]".format(
                ctx.command, ctx.author, query))

        search_results: List[Sale]
        sanitized_uid: Optional[str] = self._item_handler.sanitize_uid(
            uid=query)
        if sanitized_uid:
            search_results = self._sale_handler.get_sales_by_item_uid(
                item_uid=sanitized_uid)
        else:
            search_results = self._sale_handler.get_sales_by_item_uids(
                list(
                    map(lambda x: x.uid,
                        self._item_handler.search(search_param=query))))

        if not search_results:
            await ctx.author.send(
                _("No sales currently going on for query [{}]").format(query))
        else:
            await ctx.author.send(
                _("The following sales are currently undergoing for query [{}]:"
                  ).format(query))
            await self.send_partitioned_message(
                ctx.author, "\n".join(str(sale) for sale in search_results))

    async def search_handler(self, ctx: Context, query: str) -> None:
        self._logger.debug(
            "[SEARCH] - [{}] command called by [{}] with argument [{}]".format(
                ctx.command, ctx.author, query))

        if self._item_handler.is_uid(
                query=query):  # if UID is detected, redirect to UID handler
            self._logger.debug(
                "UID detected. Redirecting to UID search handler...")
            await self.search_uid_handler(ctx, query)
            return

        start = time.time()
        search_results: Set[Item] = self._item_handler.search(
            search_param=query)
        end = time.time()

        if not search_results:
            await ctx.author.send(
                _("Your search for ['{}'] awarded 0 results.").format(query))
        else:
            await self.send_partitioned_message(
                ctx.author,
                _("Your search for ['{}'] awarded {} and was completed in {} seconds."
                  ).format(query, set(map(lambda x: str(x), search_results)),
                           round(end - start, 4)),
            )

    async def search_uid_handler(self, ctx: Context, item_uid: str) -> None:
        self._logger.debug(
            "[UID SEARCH] - [{}] command called by [{}] with argument [{}]".
            format(ctx.command, ctx.author, item_uid))

        start = time.time()
        search_result: Optional[Item] = self._item_handler.uid_search(
            uid=item_uid)
        end = time.time()

        if not search_result:
            await ctx.author.send(
                _("Your search for ['{}'] awarded 0 results.").format(item_uid)
            )
        else:
            await self.send_partitioned_message(
                ctx.author,
                _("Your search for ['{}'] awarded [{}] and was completed in {} seconds."
                  ).format(item_uid, str(search_result), round(end - start,
                                                               4)),
            )

    async def send_announcement(self, ctx: Context, channel_id: int,
                                msg: str) -> None:
        """
        Sends a global announcement to the given channel

        :param ctx: The message's Context.
        :param channel_id: The target Channel's ID.
        :param msg: The message to send.
        """
        channel = ctx.bot.get_channel(channel_id)
        if channel is not None:
            self._logger.info("Announcing message [{}] in channel [{}]".format(
                msg, str(channel)))
            await channel.send(msg)
        else:
            self._logger.error(
                "Should have announced message in channel [{}], but it couldn't be found"
                .format(str(channel)))

    @staticmethod
    def get_user_by_id(ctx: Context, user_id: int) -> User:
        """
        Retrieves the corresponding Discord's User by ID.

        :param ctx: The search's Context.
        :param user_id: The ID to be searched.
        """
        return ctx.bot.get_user(user_id)

    @staticmethod
    async def send_partitioned_message(target: Context,
                                       msg: str,
                                       wrap_at=2000) -> None:
        """
        Sends a message to Author/Channel, honoring Discord's maximum message length.

        :param target: The message's Context.
        :param msg: The message to partition.
        :param wrap_at: The maximum message length. Defaults to Discord's max.
        """
        for line in textwrap.wrap(text=msg,
                                  width=wrap_at,
                                  replace_whitespace=False):
            await target.send(line)
Esempio n. 13
0
class I18n(metaclass=Singleton):
    """
    Class in charge of providing internationalization support. Based on GNU's gettext.

    In order to initialize the I18n module, call this once at the topmost-layer of the application:
        i18n = I18n().with_lang(LOCALE).init()

    In order to make use of internationalized texts, import this at the top of each class where strings should be
    parsed:
        _: Callable[[str], str] = lambda s: I18n().gettext(s)

    And to use it:
        _(<text_to_translate>)

    https://phrase.com/blog/posts/translate-python-gnu-gettext/
    https://stackoverflow.com/questions/18822396/flask-babel-updating-of-existing-messages-pot-file
    """

    def __init__(self) -> None:
        super().__init__()

        self._logger = Logger(self.__class__.__name__, TRACE_LEVELV_NUM)
        self._logger.info("Initializing internationalization (l18n) module...")

        self._language: str = LOCALE_EN
        self._gettext: Optional[Callable[[str], str]] = None

    def with_lang(self, language: str) -> I18n:
        self._logger.info("Loading language {}...".format(language))
        self._language = language

        # since the i18n module is a singleton shared across all modules, language overload gets disabled upon
        # initialization
        self.with_lang = lambda lang: self._disable_i18n_re_init()  # type: ignore

        return self

    def init(self) -> I18n:
        el = gettext.translation(domain="messages", localedir="locales", languages=[self._language])
        el.install()
        self._gettext = el.gettext  # update gettext's call with installed language

        self._logger.info(
            "Internationalization module initialized successfully. Loaded language: {}.".format(self._language)
        )
        self._logger.debug(
            "*Note*: locale hot-swapping is not supported. "
            "If another language is desired, change configuration and restart application."
        )

        # since the i18n module is a singleton shared across all modules, we shall disable future 're'-inits of the
        # module
        self.init = self._disable_i18n_re_init  # type: ignore

        return self

    def _disable_i18n_re_init(self) -> I18n:
        self._logger.debug(
            "Internationalization module already initialized with language: {}. "
            "Re-initizalization is not supported.".format(self._language)
        )
        return self

    @property
    def gettext(self) -> Callable[[str], str]:
        if self._gettext is None:
            self._logger.debug(
                "Internationalization module was not initialized. A default implementation of 'gettext' will be "
                "returned. Translations will most probably not work."
            )
            self._logger.trace(
                "In order to initialize the i18n module properly, call: \n\n"
                ">>> I18n().with_lang(LOCALE_ES_AR).init().\n\n"
                "Afterwards, once initialized, gettext's function can be declared as: \n\n"
                ">>> _: Callable[[str], str] = lambda s: I18n().gettext(s)\n"
            )
            return gettext.gettext
        return self._gettext
Esempio n. 14
0
    def __init__(self, **options):
        super().__init__(**options)

        self._logger = Logger(self.__class__.__name__)

        self._logger.info("=== Initializing MercadoAO Discord Bot ===")
    def __init__(self) -> None:
        super().__init__()

        self._logger = Logger(self.__class__.__name__)
        self._logger.info("Initializing stale offers cleanup job...")
        self._sale_handler = SaleHandler()