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))
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 __init__(self) -> None: super().__init__() self._logger = Logger(self.__class__.__name__) self._logger.info("Initializing Sale handler...") self._db = TinyDB("db/sale.json")
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, )
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()
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, )
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 ===")
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
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]
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)
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
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()