Module geode.api

Expand source code
# ------------------------------------------------------------------------------
#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 3 of the License.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software
#  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
#  MA 02110-1301, USA.
# ------------------------------------------------------------------------------

# API
from geode.db import Database
from geode.conf import Configuration

from geode.utils import get_data

from geode.lib.game import Game
from geode.lib.console import Console
from geode.lib.emulator import Emulator

# Filesystem
from os import W_OK
from os import getpid
from os import getlogin

from os.path import exists
from os.path import expanduser

from pathlib import Path

# Logging
import logging

from logging.config import fileConfig

# Regex
from re import IGNORECASE

from re import sub as re_sub
from re import compile as re_compile

# Structure
from enum import Enum

# System
import tempfile

try:
    from xdg.BaseDirectory import xdg_data_home
    from xdg.BaseDirectory import xdg_config_home

except ImportError as error:
    from os import environ

    if "XDG_DATA_HOME" in environ:
        xdg_data_home = environ["XDG_DATA_HOME"]
    else:
        xdg_data_home = expanduser("~/.local/share")

    if "XDG_CONFIG_HOME" in environ:
        xdg_config_home = environ["XDG_CONFIG_HOME"]
    else:
        xdg_config_home = expanduser("~/.config")

# ------------------------------------------------------------------------------
#   Class
# ------------------------------------------------------------------------------

class API(object):

    class Metadata(Enum):
        """ Represent metadata
        """

        NAME    = "geode"
        VERSION = "1.0"


    def __init__(self, **kwargs):
        """ Constructor

        Raises
        ------
        TypeError
            when filepath parameter is not a string
            when instance parameter is not a string
            when configuration parameter is not a string
            when storage parameter is not a string
        OSError
            when filepath parameter file not exists
        ValueError
            when instance name is missing

        Notes
        -----
        Two methods are available to initialize Geode API. You can use a
        configuration file which contains default values as:

            [metadata]
            instance = geode

            [path]
            configuration = ~/.config
            storage = ~/.local/share

        And use the filepath parameter as:

        >>> API(filepath="geode.conf")

        Or you can use the manual parameters instance, configuration and storage
        as:

        >>> API(instance="geode",
            configuration="~/.config", storage="~/.local/share")

        The two parameters configuration and storage are optional. They use the
        default values write in the example above if missing.
        """

        self.__lock = None

        self.__pid = getpid()

        self.__instance_name = None

        self.__storage_path = None
        self.__configuration_path = None

        self.__storage = {
            "consoles": list(),
            "emulators": list()
        }

        self.__paths = {
            "configuration": None,
            "consoles": None,
            "emulators": None,
            "home": Path.home(),
            "storage": None
        }

        # ----------------------------------------
        #   Check misc parameters
        # ----------------------------------------

        self.__debug = False

        if "debug" in kwargs:
            self.__debug = kwargs["debug"]

        # ----------------------------------------
        #   Filepath parameters
        # ----------------------------------------

        # Use a configuration file which contains default values
        if "filepath" in kwargs:
            if type(kwargs["filepath"]) is not str:
                raise TypeError("Wrong type for path, expected str")

            filepath = Path(kwargs["filepath"]).expanduser()

            if not filepath.exists():
                raise OSError(2, "Cannot found path file: %s" % filepath)

            # Generate configuration instance
            self.__configuration = Configuration(str(filepath))

            self.__instance_name = self.__configuration.get(
                "metadata", "instance", fallback=None)

            self.__configuration_path = Path(self.__configuration.get(
                "path", "configuration", fallback=xdg_config_home))

            self.__storage_path = Path(self.__configuration.get(
                "path", "storage", fallback=xdg_data_home))

        # ----------------------------------------
        #   Manual parameters
        # ----------------------------------------

        # Retrieve instance name
        if "instance" in kwargs:
            if type(kwargs["instance"]) is not str:
                raise TypeError("Wrong type for instance, expected str")

            self.__instance_name = kwargs["instance"]

        # Retrieve configuration folder path
        if "configuration" in kwargs:
            if type(kwargs["configuration"]) is not str:
                raise TypeError("Wrong type for configuration, expected str")

            self.__configuration_path = Path(kwargs["configuration"])

        # Retrieve storage folder path
        if "storage" in kwargs:
            if type(kwargs["storage"]) is not str:
                raise TypeError("Wrong type for storage, expected str")

            self.__storage_path = Path(kwargs["storage"])

        # ----------------------------------------
        #   Check instance name
        # ----------------------------------------

        if self.__instance_name is None:
            raise ValueError("Missing instance name")

        if self.__configuration_path is None:
            self.__configuration_path = Path(xdg_config_home)

        if self.__storage_path is None:
            self.__storage_path = Path(xdg_data_home)

        # ----------------------------------------
        #   Initialize API
        # ----------------------------------------

        # Check and generate default folders
        self.__init_folders()

        # Start logger module
        self.__init_logger()

        # Check lock file
        self.__init_lock()


    def __init_folders(self):
        """ Initialize default folders using to store data
        """

        # Store config folder
        self.__paths["configuration"] = \
            self.__configuration_path.expanduser().joinpath(
            self.__instance_name)

        # Store local folder
        self.__paths["storage"] = \
            self.__storage_path.expanduser().joinpath(
            self.__instance_name)

        # API main folder
        for key, path in self.__paths.items():

            if path is not None and not path.exists():
                self.__logger.info("Generate %s folder" % path)

                # Create subfolder for current instance
                path.mkdir(0o755, True)

        # API specific configuration folders
        for key in ("consoles", "emulators"):
            self.__paths[key] = self.get_configuration_path().joinpath(key)

            if not self.__paths[key].exists():
                self.__logger.info("Generate %s folder" % self.__paths[key])

                # Create subfolder for current instance
                self.__paths[key].mkdir(0o755, True)


    def __init_logger(self):
        """ Initialize logger module
        """

        # Set logging file path
        logging.log_path = self.get_storage_path().joinpath(
            "%s.log" % self.__instance_name)

        # Save previous logging file
        if logging.log_path.exists():
            logging.log_path.rename("%s.old" % str(logging.log_path))

        # Generate logger from configuration file
        fileConfig(Path(get_data("config", "logger.conf")))

        self.__logger = logging.getLogger("geode")

        if not self.__debug:
            self.__logger.setLevel(logging.INFO)

        self.__logger.info("Initialize %s instance with API v.%s" % (
            self.__instance_name, API.Metadata.VERSION.value))


    def __init_lock(self):
        """ Initialize lock file to avoid multiple instance

        Raises
        ------
        RuntimeError
            when an instance already exists
        """

        pattern = "%s.*:%s" % (self.__instance_name, getlogin())

        # Check if a temporary lock file already exists
        if len(list(Path(tempfile.gettempdir()).glob(pattern))) > 0:
            raise RuntimeError("An instance already exists")

        # Generate a new lock file
        self.__lock = tempfile.NamedTemporaryFile(
            suffix=":%s" % getlogin(), prefix="%s." % self.__instance_name)

        self.__logger.info("Register with PID %d" % self.__pid)


    def __init_objects(self, key, element):
        """ Initialize an object

        Parameters
        ----------
        key : str
            Storage key (emulators or consoles)
        element : geode.lib.console.Console or geode.lib.emulator.Emulator
            Object type

        Raises
        ------
        OSError
            when storage folder not exists
        """

        # Reset storage content
        self.__storage[key].clear()

        path = self.__paths[key]

        # Check if folder path still exists
        if not path.exists():
            raise OSError(2, "Cannot found %s folder: %s" % (key, path))

        # Retrieve every configuration file from folder
        for filepath in sorted(path.glob("*.conf")):

            try:
                self.__storage[key].append(element(self, filepath))

            except TypeError as error:
                self.__logger.error(
                    "Cannot instantiate %s: %s" % (key, str(error)))


    def __add_object(self, key, element, **kwargs):
        """ Add a new object in API storage

        Parameters
        ----------
        key : str
            Storage key (emulators or consoles)
        element : geode.lib.console.Console or geode.lib.emulator.Emulator
            Object type

        Raises
        ------
        KeyError
            when name parameter is missing
        OSError
            when folder path not exists
            when folder path is not a directory
        """

        if not "name" in kwargs:
            raise KeyError("Missing name key in parameters")

        folder = self.__paths[key]

        if not folder.exists():
            raise OSError(2, "Cannot found folder: %s" % str(folder))

        if not folder.is_dir():
            raise OSError(1, "Path is not a directory: %s" % str(folder))

        # ----------------------------------------
        #   Generate a new identifier
        # ----------------------------------------

        # Retrieve only alphanumeric element from filename
        identifier = re_sub(r"[^\w\d]+", ' ', kwargs["name"].lower())
        # Remove useless spaces and replace the others with a dash
        identifier = re_sub(r"[\s|_]+", '-', identifier.strip())

        filepath = folder.joinpath("%s.conf" % identifier)

        # Check if other files has the same identifier
        matched = sorted(list(folder.glob("%s-[0-9].conf" % identifier)))

        if len(matched) > 0 or filepath.exists():

            # Retrieve the last used number
            if len(matched) > 0:
                number = int(matched[-1].stem.split('-')[-1]) + 1

            # Start a new counter
            else:
                number = 2

            filepath = folder.joinpath("%s-%d.conf" % (identifier, number))

        # ----------------------------------------
        #   Generate configuration file
        # ----------------------------------------

        configuration = Configuration(str(filepath))

        # Retrieve default value from wanted CommonObject
        for section, option, value in element.attributes:

            # Use value from parameters
            if option in kwargs:
                value = kwargs[option]

            configuration.modify(section, option, value)

        # Save configuration file
        configuration.update()

        # ----------------------------------------
        #   Store object
        # ----------------------------------------

        try:
            self.__storage[key].append(element(self, filepath))

            return self.__storage[key][-1]

        except TypeError as error:
            self.__logger.error(
                "Cannot instantiate %s: %s" % (key, str(error)))

        return None


    @property
    def log(self):
        """ Return logger instance

        Returns
        -------
        logging.Logger
            Logger instance
        """

        return self.__logger


    @property
    def pid(self):
        """ Return application process identifier

        Returns
        -------
        int
            Process identifier
        """

        return self.__pid


    def init(self):
        """ Initialize API structure and data
        """

        # ----------------------------------------
        #   Database
        # ----------------------------------------

        self.__database = Database(
            path=self.get_storage_path().joinpath("games.db"),
            scheme=Path(get_data("config", "database.conf")))

        self.__database.connect()

        # Check if the database respect the specified scheme
        if not self.__database.check_integrity():

            # Generate a new database
            if not self.__database.path.exists():
                self.__database.generate_database()

                self.__database.insert("metadata",
                    data={ "version": API.Metadata.VERSION.value })

        # ----------------------------------------
        #   Games environment keys
        # ----------------------------------------

        self.__environment = Configuration(
            str(self.get_configuration_path().joinpath("games.conf")))

        # ----------------------------------------
        #   Objects
        # ----------------------------------------

        self.__init_objects("emulators", Emulator)

        self.__init_objects("consoles", Console)


    def get_database(self):
        """ Return database instance

        Returns
        -------
        geode.db.Database
            Database instance
        """

        return self.__database


    def get_environment(self):
        """ Return games environment configuration file

        Returns
        -------
        geode.conf.Configuration
            Environment configuration instance
        """

        return self.__environment


    def get_instance(self):
        """ Retrieve API instance name

        Returns
        -------
        str
            Current API instance name
        """

        return self.__instance_name


    def get_paths(self):
        """ Retrieve API paths

        Returns
        -------
        list
            List which contains key and path tuples
        """

        return list(self.__paths.items())


    def get_storage_path(self):
        """ Retrieve storage folder path

        Returns
        -------
        pathlib.Path
            Storage folder
        """

        return self.__paths["storage"]


    def get_configuration_path(self):
        """ Retrieve configuration folder path

        Returns
        -------
        pathlib.Path
            Configuration folder
        """

        return self.__paths["configuration"]


    def get_emulators(self):
        """ Retrieve available emulators

        Returns
        -------
        list
            Emulator objects as list
        """

        return self.__storage["emulators"]


    def get_emulators_path(self):
        """ Retrieve emulators folder path

        Returns
        -------
        pathlib.Path
            Emulators folder
        """

        return self.__paths["emulators"]


    def get_emulator(self, key):
        """ Retrieve a specific emulator

        Parameters
        ----------
        key : str
            Emulator identifier key

        Returns
        -------
        geode.lib.emulator.Emulator or None
            Emulator instance if found, None otherwise
        """

        return next((emulator for emulator in self.__storage["emulators"] \
            if emulator.id == key), None)


    def add_emulator(self, **kwargs):
        """ Append a new emulator

        Returns
        -------
        geode.lib.emulator.Emulator or None
            Emulator instance if success, None otherwise
        """

        return self.__add_object("emulators", Emulator, **kwargs)


    def delete_emulator(self, key):
        """ Delete a specific emulator

        Parameters
        ----------
        key : str or geode.lib.emulator.Emulator
            Emulator identifier key or instance

        Returns
        -------
        bool
            True if emulator has been successfully removed, False otherwise
        """

        if type(key) is Emulator:
            emulator = key

        elif type(key) is str:
            emulator = self.get_emulator(key)

        if emulator is not None:

            try:
                # Remove configuration file
                emulator.get_path().unlink()

                # Remove instance from storage
                self.get_emulators().remove(emulator)

                # Remove instance from consoles
                for console in self.get_consoles():

                    # Remove emulator entry from console instance
                    if console.emulator == emulator:
                        console.emulator = None

                        console.save()

                # Remove instance from memory
                del emulator

                return True

            except Exception as error:
                return False

        return False


    def get_consoles(self):
        """ Retrieve available consoles

        Returns
        -------
        list
            Console objects as list
        """

        return self.__storage["consoles"]


    def get_consoles_path(self):
        """ Retrieve consoles folder path

        Returns
        -------
        pathlib.Path
            Consoles folder
        """

        return self.__paths["consoles"]


    def get_console(self, key):
        """ Retrieve a specific console

        Parameters
        ----------
        key : str
            Console identifier key

        Returns
        -------
        geode.lib.console.Console or None
            Console instance if found, None otherwise
        """

        return next((console for console in self.__storage["consoles"] \
            if console.id == key), None)


    def add_console(self, **kwargs):
        """ Append a new console

        Returns
        -------
        geode.lib.console.Console or None
            Console instance if success, None otherwise
        """

        return self.__add_object("consoles", Console, **kwargs)


    def delete_console(self, key):
        """ Delete a specific console

        Parameters
        ----------
        key : str or geode.lib.console.Console
            Console identifier key or instance

        Returns
        -------
        bool
            True if console has been successfully removed, False otherwise
        """

        if type(key) is Console:
            console = key

        elif type(key) is str:
            console = self.get_console(key)

        if console is not None:

            try:
                # Remove configuration file
                console.get_path().unlink()

                # Remove instance from storage
                self.get_consoles().remove(console)

                # Remove instance from memory
                del console

                return True

            except Exception as error:
                return False

        return False


    def get_games(self, sort=False):
        """ Retrieve every games available

        Parameters
        ----------
        sort : bool, optional
            Sort list before return it (Default: False)

        Returns
        -------
        list
            Games list
        """

        games = list()

        for console in self.__storage["consoles"]:
            games.extend(console.get_games())

        if sort:
            games.sort(key=lambda game: game.name.lower().replace(' ', ''))

        return games


    def get_game(self, key):
        """ Retrieve a specific game

        Parameters
        ----------
        key : str
            Game identifier key
        sort : bool, optional
            Sort list before return it (Default: False)

        Returns
        -------
        geode.lib.game.Game or None
            Game instance if found, None otherwise
        """

        return next((game for game in self.get_games() if game.id == key), None)


    def search_game(self, key, sort=False):
        """ Search games from a specific key

        Parameters
        ----------
        key : str
            Key to search in games list (based on identifier and name)

        Returns
        -------
        generator
            Game instances
        """

        regex = re_compile(key, IGNORECASE)

        return (game for game in self.get_games(sort) \
            if regex.search(game.name) or regex.search(game.id))


    def delete_game(self, key):
        """ Delete a specific game from database

        This function only remove the game from database. If you want to remove
        the game file, you need to do it manually.

        Parameters
        ----------
        key : str or geode.lib.game.Game
            Game identifier key or instance
        """

        if type(key) is Game:
            game = key

        elif type(key) is str:
            game = self.get_game(key)

        if game is not None:
            console = game.console

            # Remove instance from storage
            return console.delete_game(game)

        return False

Classes

class API (**kwargs)

Constructor

Raises

TypeError
when filepath parameter is not a string when instance parameter is not a string when configuration parameter is not a string when storage parameter is not a string
OSError
when filepath parameter file not exists
ValueError
when instance name is missing

Notes

Two methods are available to initialize Geode API. You can use a configuration file which contains default values as:

[metadata]
instance = geode

[path]
configuration = ~/.config
storage = ~/.local/share

And use the filepath parameter as:

>>> API(filepath="geode.conf")

Or you can use the manual parameters instance, configuration and storage as:

>>> API(instance="geode",
    configuration="~/.config", storage="~/.local/share")

The two parameters configuration and storage are optional. They use the default values write in the example above if missing.

Expand source code
class API(object):

    class Metadata(Enum):
        """ Represent metadata
        """

        NAME    = "geode"
        VERSION = "1.0"


    def __init__(self, **kwargs):
        """ Constructor

        Raises
        ------
        TypeError
            when filepath parameter is not a string
            when instance parameter is not a string
            when configuration parameter is not a string
            when storage parameter is not a string
        OSError
            when filepath parameter file not exists
        ValueError
            when instance name is missing

        Notes
        -----
        Two methods are available to initialize Geode API. You can use a
        configuration file which contains default values as:

            [metadata]
            instance = geode

            [path]
            configuration = ~/.config
            storage = ~/.local/share

        And use the filepath parameter as:

        >>> API(filepath="geode.conf")

        Or you can use the manual parameters instance, configuration and storage
        as:

        >>> API(instance="geode",
            configuration="~/.config", storage="~/.local/share")

        The two parameters configuration and storage are optional. They use the
        default values write in the example above if missing.
        """

        self.__lock = None

        self.__pid = getpid()

        self.__instance_name = None

        self.__storage_path = None
        self.__configuration_path = None

        self.__storage = {
            "consoles": list(),
            "emulators": list()
        }

        self.__paths = {
            "configuration": None,
            "consoles": None,
            "emulators": None,
            "home": Path.home(),
            "storage": None
        }

        # ----------------------------------------
        #   Check misc parameters
        # ----------------------------------------

        self.__debug = False

        if "debug" in kwargs:
            self.__debug = kwargs["debug"]

        # ----------------------------------------
        #   Filepath parameters
        # ----------------------------------------

        # Use a configuration file which contains default values
        if "filepath" in kwargs:
            if type(kwargs["filepath"]) is not str:
                raise TypeError("Wrong type for path, expected str")

            filepath = Path(kwargs["filepath"]).expanduser()

            if not filepath.exists():
                raise OSError(2, "Cannot found path file: %s" % filepath)

            # Generate configuration instance
            self.__configuration = Configuration(str(filepath))

            self.__instance_name = self.__configuration.get(
                "metadata", "instance", fallback=None)

            self.__configuration_path = Path(self.__configuration.get(
                "path", "configuration", fallback=xdg_config_home))

            self.__storage_path = Path(self.__configuration.get(
                "path", "storage", fallback=xdg_data_home))

        # ----------------------------------------
        #   Manual parameters
        # ----------------------------------------

        # Retrieve instance name
        if "instance" in kwargs:
            if type(kwargs["instance"]) is not str:
                raise TypeError("Wrong type for instance, expected str")

            self.__instance_name = kwargs["instance"]

        # Retrieve configuration folder path
        if "configuration" in kwargs:
            if type(kwargs["configuration"]) is not str:
                raise TypeError("Wrong type for configuration, expected str")

            self.__configuration_path = Path(kwargs["configuration"])

        # Retrieve storage folder path
        if "storage" in kwargs:
            if type(kwargs["storage"]) is not str:
                raise TypeError("Wrong type for storage, expected str")

            self.__storage_path = Path(kwargs["storage"])

        # ----------------------------------------
        #   Check instance name
        # ----------------------------------------

        if self.__instance_name is None:
            raise ValueError("Missing instance name")

        if self.__configuration_path is None:
            self.__configuration_path = Path(xdg_config_home)

        if self.__storage_path is None:
            self.__storage_path = Path(xdg_data_home)

        # ----------------------------------------
        #   Initialize API
        # ----------------------------------------

        # Check and generate default folders
        self.__init_folders()

        # Start logger module
        self.__init_logger()

        # Check lock file
        self.__init_lock()


    def __init_folders(self):
        """ Initialize default folders using to store data
        """

        # Store config folder
        self.__paths["configuration"] = \
            self.__configuration_path.expanduser().joinpath(
            self.__instance_name)

        # Store local folder
        self.__paths["storage"] = \
            self.__storage_path.expanduser().joinpath(
            self.__instance_name)

        # API main folder
        for key, path in self.__paths.items():

            if path is not None and not path.exists():
                self.__logger.info("Generate %s folder" % path)

                # Create subfolder for current instance
                path.mkdir(0o755, True)

        # API specific configuration folders
        for key in ("consoles", "emulators"):
            self.__paths[key] = self.get_configuration_path().joinpath(key)

            if not self.__paths[key].exists():
                self.__logger.info("Generate %s folder" % self.__paths[key])

                # Create subfolder for current instance
                self.__paths[key].mkdir(0o755, True)


    def __init_logger(self):
        """ Initialize logger module
        """

        # Set logging file path
        logging.log_path = self.get_storage_path().joinpath(
            "%s.log" % self.__instance_name)

        # Save previous logging file
        if logging.log_path.exists():
            logging.log_path.rename("%s.old" % str(logging.log_path))

        # Generate logger from configuration file
        fileConfig(Path(get_data("config", "logger.conf")))

        self.__logger = logging.getLogger("geode")

        if not self.__debug:
            self.__logger.setLevel(logging.INFO)

        self.__logger.info("Initialize %s instance with API v.%s" % (
            self.__instance_name, API.Metadata.VERSION.value))


    def __init_lock(self):
        """ Initialize lock file to avoid multiple instance

        Raises
        ------
        RuntimeError
            when an instance already exists
        """

        pattern = "%s.*:%s" % (self.__instance_name, getlogin())

        # Check if a temporary lock file already exists
        if len(list(Path(tempfile.gettempdir()).glob(pattern))) > 0:
            raise RuntimeError("An instance already exists")

        # Generate a new lock file
        self.__lock = tempfile.NamedTemporaryFile(
            suffix=":%s" % getlogin(), prefix="%s." % self.__instance_name)

        self.__logger.info("Register with PID %d" % self.__pid)


    def __init_objects(self, key, element):
        """ Initialize an object

        Parameters
        ----------
        key : str
            Storage key (emulators or consoles)
        element : geode.lib.console.Console or geode.lib.emulator.Emulator
            Object type

        Raises
        ------
        OSError
            when storage folder not exists
        """

        # Reset storage content
        self.__storage[key].clear()

        path = self.__paths[key]

        # Check if folder path still exists
        if not path.exists():
            raise OSError(2, "Cannot found %s folder: %s" % (key, path))

        # Retrieve every configuration file from folder
        for filepath in sorted(path.glob("*.conf")):

            try:
                self.__storage[key].append(element(self, filepath))

            except TypeError as error:
                self.__logger.error(
                    "Cannot instantiate %s: %s" % (key, str(error)))


    def __add_object(self, key, element, **kwargs):
        """ Add a new object in API storage

        Parameters
        ----------
        key : str
            Storage key (emulators or consoles)
        element : geode.lib.console.Console or geode.lib.emulator.Emulator
            Object type

        Raises
        ------
        KeyError
            when name parameter is missing
        OSError
            when folder path not exists
            when folder path is not a directory
        """

        if not "name" in kwargs:
            raise KeyError("Missing name key in parameters")

        folder = self.__paths[key]

        if not folder.exists():
            raise OSError(2, "Cannot found folder: %s" % str(folder))

        if not folder.is_dir():
            raise OSError(1, "Path is not a directory: %s" % str(folder))

        # ----------------------------------------
        #   Generate a new identifier
        # ----------------------------------------

        # Retrieve only alphanumeric element from filename
        identifier = re_sub(r"[^\w\d]+", ' ', kwargs["name"].lower())
        # Remove useless spaces and replace the others with a dash
        identifier = re_sub(r"[\s|_]+", '-', identifier.strip())

        filepath = folder.joinpath("%s.conf" % identifier)

        # Check if other files has the same identifier
        matched = sorted(list(folder.glob("%s-[0-9].conf" % identifier)))

        if len(matched) > 0 or filepath.exists():

            # Retrieve the last used number
            if len(matched) > 0:
                number = int(matched[-1].stem.split('-')[-1]) + 1

            # Start a new counter
            else:
                number = 2

            filepath = folder.joinpath("%s-%d.conf" % (identifier, number))

        # ----------------------------------------
        #   Generate configuration file
        # ----------------------------------------

        configuration = Configuration(str(filepath))

        # Retrieve default value from wanted CommonObject
        for section, option, value in element.attributes:

            # Use value from parameters
            if option in kwargs:
                value = kwargs[option]

            configuration.modify(section, option, value)

        # Save configuration file
        configuration.update()

        # ----------------------------------------
        #   Store object
        # ----------------------------------------

        try:
            self.__storage[key].append(element(self, filepath))

            return self.__storage[key][-1]

        except TypeError as error:
            self.__logger.error(
                "Cannot instantiate %s: %s" % (key, str(error)))

        return None


    @property
    def log(self):
        """ Return logger instance

        Returns
        -------
        logging.Logger
            Logger instance
        """

        return self.__logger


    @property
    def pid(self):
        """ Return application process identifier

        Returns
        -------
        int
            Process identifier
        """

        return self.__pid


    def init(self):
        """ Initialize API structure and data
        """

        # ----------------------------------------
        #   Database
        # ----------------------------------------

        self.__database = Database(
            path=self.get_storage_path().joinpath("games.db"),
            scheme=Path(get_data("config", "database.conf")))

        self.__database.connect()

        # Check if the database respect the specified scheme
        if not self.__database.check_integrity():

            # Generate a new database
            if not self.__database.path.exists():
                self.__database.generate_database()

                self.__database.insert("metadata",
                    data={ "version": API.Metadata.VERSION.value })

        # ----------------------------------------
        #   Games environment keys
        # ----------------------------------------

        self.__environment = Configuration(
            str(self.get_configuration_path().joinpath("games.conf")))

        # ----------------------------------------
        #   Objects
        # ----------------------------------------

        self.__init_objects("emulators", Emulator)

        self.__init_objects("consoles", Console)


    def get_database(self):
        """ Return database instance

        Returns
        -------
        geode.db.Database
            Database instance
        """

        return self.__database


    def get_environment(self):
        """ Return games environment configuration file

        Returns
        -------
        geode.conf.Configuration
            Environment configuration instance
        """

        return self.__environment


    def get_instance(self):
        """ Retrieve API instance name

        Returns
        -------
        str
            Current API instance name
        """

        return self.__instance_name


    def get_paths(self):
        """ Retrieve API paths

        Returns
        -------
        list
            List which contains key and path tuples
        """

        return list(self.__paths.items())


    def get_storage_path(self):
        """ Retrieve storage folder path

        Returns
        -------
        pathlib.Path
            Storage folder
        """

        return self.__paths["storage"]


    def get_configuration_path(self):
        """ Retrieve configuration folder path

        Returns
        -------
        pathlib.Path
            Configuration folder
        """

        return self.__paths["configuration"]


    def get_emulators(self):
        """ Retrieve available emulators

        Returns
        -------
        list
            Emulator objects as list
        """

        return self.__storage["emulators"]


    def get_emulators_path(self):
        """ Retrieve emulators folder path

        Returns
        -------
        pathlib.Path
            Emulators folder
        """

        return self.__paths["emulators"]


    def get_emulator(self, key):
        """ Retrieve a specific emulator

        Parameters
        ----------
        key : str
            Emulator identifier key

        Returns
        -------
        geode.lib.emulator.Emulator or None
            Emulator instance if found, None otherwise
        """

        return next((emulator for emulator in self.__storage["emulators"] \
            if emulator.id == key), None)


    def add_emulator(self, **kwargs):
        """ Append a new emulator

        Returns
        -------
        geode.lib.emulator.Emulator or None
            Emulator instance if success, None otherwise
        """

        return self.__add_object("emulators", Emulator, **kwargs)


    def delete_emulator(self, key):
        """ Delete a specific emulator

        Parameters
        ----------
        key : str or geode.lib.emulator.Emulator
            Emulator identifier key or instance

        Returns
        -------
        bool
            True if emulator has been successfully removed, False otherwise
        """

        if type(key) is Emulator:
            emulator = key

        elif type(key) is str:
            emulator = self.get_emulator(key)

        if emulator is not None:

            try:
                # Remove configuration file
                emulator.get_path().unlink()

                # Remove instance from storage
                self.get_emulators().remove(emulator)

                # Remove instance from consoles
                for console in self.get_consoles():

                    # Remove emulator entry from console instance
                    if console.emulator == emulator:
                        console.emulator = None

                        console.save()

                # Remove instance from memory
                del emulator

                return True

            except Exception as error:
                return False

        return False


    def get_consoles(self):
        """ Retrieve available consoles

        Returns
        -------
        list
            Console objects as list
        """

        return self.__storage["consoles"]


    def get_consoles_path(self):
        """ Retrieve consoles folder path

        Returns
        -------
        pathlib.Path
            Consoles folder
        """

        return self.__paths["consoles"]


    def get_console(self, key):
        """ Retrieve a specific console

        Parameters
        ----------
        key : str
            Console identifier key

        Returns
        -------
        geode.lib.console.Console or None
            Console instance if found, None otherwise
        """

        return next((console for console in self.__storage["consoles"] \
            if console.id == key), None)


    def add_console(self, **kwargs):
        """ Append a new console

        Returns
        -------
        geode.lib.console.Console or None
            Console instance if success, None otherwise
        """

        return self.__add_object("consoles", Console, **kwargs)


    def delete_console(self, key):
        """ Delete a specific console

        Parameters
        ----------
        key : str or geode.lib.console.Console
            Console identifier key or instance

        Returns
        -------
        bool
            True if console has been successfully removed, False otherwise
        """

        if type(key) is Console:
            console = key

        elif type(key) is str:
            console = self.get_console(key)

        if console is not None:

            try:
                # Remove configuration file
                console.get_path().unlink()

                # Remove instance from storage
                self.get_consoles().remove(console)

                # Remove instance from memory
                del console

                return True

            except Exception as error:
                return False

        return False


    def get_games(self, sort=False):
        """ Retrieve every games available

        Parameters
        ----------
        sort : bool, optional
            Sort list before return it (Default: False)

        Returns
        -------
        list
            Games list
        """

        games = list()

        for console in self.__storage["consoles"]:
            games.extend(console.get_games())

        if sort:
            games.sort(key=lambda game: game.name.lower().replace(' ', ''))

        return games


    def get_game(self, key):
        """ Retrieve a specific game

        Parameters
        ----------
        key : str
            Game identifier key
        sort : bool, optional
            Sort list before return it (Default: False)

        Returns
        -------
        geode.lib.game.Game or None
            Game instance if found, None otherwise
        """

        return next((game for game in self.get_games() if game.id == key), None)


    def search_game(self, key, sort=False):
        """ Search games from a specific key

        Parameters
        ----------
        key : str
            Key to search in games list (based on identifier and name)

        Returns
        -------
        generator
            Game instances
        """

        regex = re_compile(key, IGNORECASE)

        return (game for game in self.get_games(sort) \
            if regex.search(game.name) or regex.search(game.id))


    def delete_game(self, key):
        """ Delete a specific game from database

        This function only remove the game from database. If you want to remove
        the game file, you need to do it manually.

        Parameters
        ----------
        key : str or geode.lib.game.Game
            Game identifier key or instance
        """

        if type(key) is Game:
            game = key

        elif type(key) is str:
            game = self.get_game(key)

        if game is not None:
            console = game.console

            # Remove instance from storage
            return console.delete_game(game)

        return False

Class variables

var Metadata

Represent metadata

Expand source code
class Metadata(Enum):
    """ Represent metadata
    """

    NAME    = "geode"
    VERSION = "1.0"

Instance variables

var log

Return logger instance

Returns

logging.Logger
Logger instance
Expand source code
@property
def log(self):
    """ Return logger instance

    Returns
    -------
    logging.Logger
        Logger instance
    """

    return self.__logger
var pid

Return application process identifier

Returns

int
Process identifier
Expand source code
@property
def pid(self):
    """ Return application process identifier

    Returns
    -------
    int
        Process identifier
    """

    return self.__pid

Methods

def add_console(self, **kwargs)

Append a new console

Returns

Console or None
Console instance if success, None otherwise
Expand source code
def add_console(self, **kwargs):
    """ Append a new console

    Returns
    -------
    geode.lib.console.Console or None
        Console instance if success, None otherwise
    """

    return self.__add_object("consoles", Console, **kwargs)
def add_emulator(self, **kwargs)

Append a new emulator

Returns

Emulator or None
Emulator instance if success, None otherwise
Expand source code
def add_emulator(self, **kwargs):
    """ Append a new emulator

    Returns
    -------
    geode.lib.emulator.Emulator or None
        Emulator instance if success, None otherwise
    """

    return self.__add_object("emulators", Emulator, **kwargs)
def delete_console(self, key)

Delete a specific console

Parameters

key : str or Console
Console identifier key or instance

Returns

bool
True if console has been successfully removed, False otherwise
Expand source code
def delete_console(self, key):
    """ Delete a specific console

    Parameters
    ----------
    key : str or geode.lib.console.Console
        Console identifier key or instance

    Returns
    -------
    bool
        True if console has been successfully removed, False otherwise
    """

    if type(key) is Console:
        console = key

    elif type(key) is str:
        console = self.get_console(key)

    if console is not None:

        try:
            # Remove configuration file
            console.get_path().unlink()

            # Remove instance from storage
            self.get_consoles().remove(console)

            # Remove instance from memory
            del console

            return True

        except Exception as error:
            return False

    return False
def delete_emulator(self, key)

Delete a specific emulator

Parameters

key : str or Emulator
Emulator identifier key or instance

Returns

bool
True if emulator has been successfully removed, False otherwise
Expand source code
def delete_emulator(self, key):
    """ Delete a specific emulator

    Parameters
    ----------
    key : str or geode.lib.emulator.Emulator
        Emulator identifier key or instance

    Returns
    -------
    bool
        True if emulator has been successfully removed, False otherwise
    """

    if type(key) is Emulator:
        emulator = key

    elif type(key) is str:
        emulator = self.get_emulator(key)

    if emulator is not None:

        try:
            # Remove configuration file
            emulator.get_path().unlink()

            # Remove instance from storage
            self.get_emulators().remove(emulator)

            # Remove instance from consoles
            for console in self.get_consoles():

                # Remove emulator entry from console instance
                if console.emulator == emulator:
                    console.emulator = None

                    console.save()

            # Remove instance from memory
            del emulator

            return True

        except Exception as error:
            return False

    return False
def delete_game(self, key)

Delete a specific game from database

This function only remove the game from database. If you want to remove the game file, you need to do it manually.

Parameters

key : str or Game
Game identifier key or instance
Expand source code
def delete_game(self, key):
    """ Delete a specific game from database

    This function only remove the game from database. If you want to remove
    the game file, you need to do it manually.

    Parameters
    ----------
    key : str or geode.lib.game.Game
        Game identifier key or instance
    """

    if type(key) is Game:
        game = key

    elif type(key) is str:
        game = self.get_game(key)

    if game is not None:
        console = game.console

        # Remove instance from storage
        return console.delete_game(game)

    return False
def get_configuration_path(self)

Retrieve configuration folder path

Returns

pathlib.Path
Configuration folder
Expand source code
def get_configuration_path(self):
    """ Retrieve configuration folder path

    Returns
    -------
    pathlib.Path
        Configuration folder
    """

    return self.__paths["configuration"]
def get_console(self, key)

Retrieve a specific console

Parameters

key : str
Console identifier key

Returns

Console or None
Console instance if found, None otherwise
Expand source code
def get_console(self, key):
    """ Retrieve a specific console

    Parameters
    ----------
    key : str
        Console identifier key

    Returns
    -------
    geode.lib.console.Console or None
        Console instance if found, None otherwise
    """

    return next((console for console in self.__storage["consoles"] \
        if console.id == key), None)
def get_consoles(self)

Retrieve available consoles

Returns

list
Console objects as list
Expand source code
def get_consoles(self):
    """ Retrieve available consoles

    Returns
    -------
    list
        Console objects as list
    """

    return self.__storage["consoles"]
def get_consoles_path(self)

Retrieve consoles folder path

Returns

pathlib.Path
Consoles folder
Expand source code
def get_consoles_path(self):
    """ Retrieve consoles folder path

    Returns
    -------
    pathlib.Path
        Consoles folder
    """

    return self.__paths["consoles"]
def get_database(self)

Return database instance

Returns

Database
Database instance
Expand source code
def get_database(self):
    """ Return database instance

    Returns
    -------
    geode.db.Database
        Database instance
    """

    return self.__database
def get_emulator(self, key)

Retrieve a specific emulator

Parameters

key : str
Emulator identifier key

Returns

Emulator or None
Emulator instance if found, None otherwise
Expand source code
def get_emulator(self, key):
    """ Retrieve a specific emulator

    Parameters
    ----------
    key : str
        Emulator identifier key

    Returns
    -------
    geode.lib.emulator.Emulator or None
        Emulator instance if found, None otherwise
    """

    return next((emulator for emulator in self.__storage["emulators"] \
        if emulator.id == key), None)
def get_emulators(self)

Retrieve available emulators

Returns

list
Emulator objects as list
Expand source code
def get_emulators(self):
    """ Retrieve available emulators

    Returns
    -------
    list
        Emulator objects as list
    """

    return self.__storage["emulators"]
def get_emulators_path(self)

Retrieve emulators folder path

Returns

pathlib.Path
Emulators folder
Expand source code
def get_emulators_path(self):
    """ Retrieve emulators folder path

    Returns
    -------
    pathlib.Path
        Emulators folder
    """

    return self.__paths["emulators"]
def get_environment(self)

Return games environment configuration file

Returns

Configuration
Environment configuration instance
Expand source code
def get_environment(self):
    """ Return games environment configuration file

    Returns
    -------
    geode.conf.Configuration
        Environment configuration instance
    """

    return self.__environment
def get_game(self, key)

Retrieve a specific game

Parameters

key : str
Game identifier key
sort : bool, optional
Sort list before return it (Default: False)

Returns

Game or None
Game instance if found, None otherwise
Expand source code
def get_game(self, key):
    """ Retrieve a specific game

    Parameters
    ----------
    key : str
        Game identifier key
    sort : bool, optional
        Sort list before return it (Default: False)

    Returns
    -------
    geode.lib.game.Game or None
        Game instance if found, None otherwise
    """

    return next((game for game in self.get_games() if game.id == key), None)
def get_games(self, sort=False)

Retrieve every games available

Parameters

sort : bool, optional
Sort list before return it (Default: False)

Returns

list
Games list
Expand source code
def get_games(self, sort=False):
    """ Retrieve every games available

    Parameters
    ----------
    sort : bool, optional
        Sort list before return it (Default: False)

    Returns
    -------
    list
        Games list
    """

    games = list()

    for console in self.__storage["consoles"]:
        games.extend(console.get_games())

    if sort:
        games.sort(key=lambda game: game.name.lower().replace(' ', ''))

    return games
def get_instance(self)

Retrieve API instance name

Returns

str
Current API instance name
Expand source code
def get_instance(self):
    """ Retrieve API instance name

    Returns
    -------
    str
        Current API instance name
    """

    return self.__instance_name
def get_paths(self)

Retrieve API paths

Returns

list
List which contains key and path tuples
Expand source code
def get_paths(self):
    """ Retrieve API paths

    Returns
    -------
    list
        List which contains key and path tuples
    """

    return list(self.__paths.items())
def get_storage_path(self)

Retrieve storage folder path

Returns

pathlib.Path
Storage folder
Expand source code
def get_storage_path(self):
    """ Retrieve storage folder path

    Returns
    -------
    pathlib.Path
        Storage folder
    """

    return self.__paths["storage"]
def init(self)

Initialize API structure and data

Expand source code
def init(self):
    """ Initialize API structure and data
    """

    # ----------------------------------------
    #   Database
    # ----------------------------------------

    self.__database = Database(
        path=self.get_storage_path().joinpath("games.db"),
        scheme=Path(get_data("config", "database.conf")))

    self.__database.connect()

    # Check if the database respect the specified scheme
    if not self.__database.check_integrity():

        # Generate a new database
        if not self.__database.path.exists():
            self.__database.generate_database()

            self.__database.insert("metadata",
                data={ "version": API.Metadata.VERSION.value })

    # ----------------------------------------
    #   Games environment keys
    # ----------------------------------------

    self.__environment = Configuration(
        str(self.get_configuration_path().joinpath("games.conf")))

    # ----------------------------------------
    #   Objects
    # ----------------------------------------

    self.__init_objects("emulators", Emulator)

    self.__init_objects("consoles", Console)
def search_game(self, key, sort=False)

Search games from a specific key

Parameters

key : str
Key to search in games list (based on identifier and name)

Returns

generator
Game instances
Expand source code
def search_game(self, key, sort=False):
    """ Search games from a specific key

    Parameters
    ----------
    key : str
        Key to search in games list (based on identifier and name)

    Returns
    -------
    generator
        Game instances
    """

    regex = re_compile(key, IGNORECASE)

    return (game for game in self.get_games(sort) \
        if regex.search(game.name) or regex.search(game.id))