From 5e8970cf3d68e340e6ca02c649c44b4e0e8bde78 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Fri, 9 Aug 2024 18:29:49 -0400 Subject: [PATCH] refactor: restructure source code --- .github/workflows/CI.yml | 12 +- .github/workflows/localize.yml | 11 +- .gitignore | 4 +- .readthedocs.yaml | 4 +- Dockerfile | 2 +- docs/source/conf.py | 32 +++- docs/source/contributing/localization.rst | 5 +- docs/source/pyra_docs/config.rst | 7 - docs/source/pyra_docs/hardware.rst | 7 - docs/source/pyra_docs/helpers.rst | 7 - docs/source/pyra_docs/locales.rst | 7 - docs/source/pyra_docs/logger.rst | 7 - docs/source/pyra_docs/threads.rst | 7 - docs/source/pyra_docs/webapp.rst | 7 - docs/source/src/common/common.rst | 7 + .../pyra.rst => src/common/config.rst} | 4 +- docs/source/src/common/definitions.rst | 7 + docs/source/src/common/hardware.rst | 7 + .../tray_icon.rst => src/common/helpers.rst} | 4 +- docs/source/src/common/locales.rst | 7 + docs/source/src/common/logger.rst | 7 + docs/source/src/common/threads.rst | 7 + .../common/tray_icon.rst} | 4 +- docs/source/src/common/webapp.rst | 7 + docs/source/{ => src}/global.rst | 0 docs/source/{main => src}/retroarcher.rst | 2 +- docs/source/toc.rst | 24 +-- requirements.txt | 4 +- scripts/_locale.py | 22 +-- scripts/build.py | 10 +- src/__init__.py | 0 {pyra => src/common}/__init__.py | 10 +- {pyra => src/common}/config.py | 145 ++++++++++++++++-- {pyra => src/common}/definitions.py | 15 +- {pyra => src/common}/hardware.py | 10 +- {pyra => src/common}/helpers.py | 0 {pyra => src/common}/locales.py | 6 +- {pyra => src/common}/logger.py | 36 +++-- {pyra => src/common}/threads.py | 6 +- {pyra => src/common}/tray_icon.py | 26 ++-- {pyra => src/common}/webapp.py | 126 +++++++++++---- retroarcher.py => src/retroarcher.py | 54 +++---- tests/conftest.py | 29 ++-- tests/functional/test_webapp.py | 2 +- tests/unit/test_config.py | 6 +- tests/unit/test_definitions.py | 7 +- tests/unit/test_hardware.py | 4 +- tests/unit/test_helpers.py | 6 +- tests/unit/test_init.py | 8 +- tests/unit/test_locales.py | 4 +- tests/unit/test_logger.py | 8 +- tests/unit/test_threads.py | 4 +- tests/unit/test_tray_icon.py | 10 +- tests/unit/test_webapp.py | 6 +- web/images/{retroarcher.ico => favicon.ico} | Bin web/js/discord.js | 47 ------ web/js/translations.js | 32 ++++ web/templates/base.html | 3 +- 58 files changed, 531 insertions(+), 321 deletions(-) delete mode 100644 docs/source/pyra_docs/config.rst delete mode 100644 docs/source/pyra_docs/hardware.rst delete mode 100644 docs/source/pyra_docs/helpers.rst delete mode 100644 docs/source/pyra_docs/locales.rst delete mode 100644 docs/source/pyra_docs/logger.rst delete mode 100644 docs/source/pyra_docs/threads.rst delete mode 100644 docs/source/pyra_docs/webapp.rst create mode 100644 docs/source/src/common/common.rst rename docs/source/{pyra_docs/pyra.rst => src/common/config.rst} (62%) create mode 100644 docs/source/src/common/definitions.rst create mode 100644 docs/source/src/common/hardware.rst rename docs/source/{pyra_docs/tray_icon.rst => src/common/helpers.rst} (61%) create mode 100644 docs/source/src/common/locales.rst create mode 100644 docs/source/src/common/logger.rst create mode 100644 docs/source/src/common/threads.rst rename docs/source/{pyra_docs/definitions.rst => src/common/tray_icon.rst} (60%) create mode 100644 docs/source/src/common/webapp.rst rename docs/source/{ => src}/global.rst (100%) rename docs/source/{main => src}/retroarcher.rst (80%) create mode 100644 src/__init__.py rename {pyra => src/common}/__init__.py (95%) rename {pyra => src/common}/config.py (78%) rename {pyra => src/common}/definitions.py (90%) rename {pyra => src/common}/hardware.py (98%) rename {pyra => src/common}/helpers.py (100%) rename {pyra => src/common}/locales.py (97%) rename {pyra => src/common}/logger.py (95%) rename {pyra => src/common}/threads.py (84%) rename {pyra => src/common}/tray_icon.py (95%) rename {pyra => src/common}/webapp.py (74%) rename retroarcher.py => src/retroarcher.py (83%) rename web/images/{retroarcher.ico => favicon.ico} (100%) delete mode 100644 web/js/discord.js create mode 100644 web/js/translations.js diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 8360a6e33..322b2ee7e 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -51,16 +51,16 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Install Python 3.9 + - name: Setup Python uses: actions/setup-python@v5 with: - python-version: '3.9' + python-version: '3.12' architecture: ${{ matrix.architecture }} - - name: Set up Python Dependencies + - name: Setup Python Dependencies run: | - python -m pip install --upgrade pip setuptools - python -m pip install -r requirements-dev.txt --no-warn-script-location + python -m pip install --upgrade pip setuptools wheel + python -m pip install -r requirements-dev.txt - name: Compile Locale Translations run: | @@ -108,7 +108,7 @@ jobs: --tb=native \ --verbose \ --color=yes \ - --cov=pyra \ + --cov=src \ tests - name: Upload coverage diff --git a/.github/workflows/localize.yml b/.github/workflows/localize.yml index 853f3baa0..67bb230df 100644 --- a/.github/workflows/localize.yml +++ b/.github/workflows/localize.yml @@ -6,9 +6,8 @@ on: branches: [master] paths: # prevents workflow from running unless these files change - '.github/workflows/localize.yml' - - 'retroarcher.py' - 'locale/retroarcher.po' - - 'pyra/**.py' + - 'src/**.py' - 'web/templates/**' workflow_dispatch: @@ -21,14 +20,14 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Install Python 3.9 + - name: Install Python uses: actions/setup-python@v5 # https://github.com/actions/setup-python with: - python-version: '3.9' + python-version: '3.12' - - name: Set up Python 3.9 Dependencies + - name: Setup Python Dependencies run: | - python -m pip install --upgrade pip setuptools + python -m pip install --upgrade pip setuptools wheel python -m pip install -r requirements.txt - name: Update Strings diff --git a/.gitignore b/.gitignore index 7d88bf59b..07e92ee33 100644 --- a/.gitignore +++ b/.gitignore @@ -155,8 +155,6 @@ cython_debug/ node_modules/ *package-lock.json -# RetroArcher directories +# project files and directories logs/ - -# RetroArcher files *config.ini diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 511b87cbd..e369c4776 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -8,9 +8,9 @@ version: 2 # Set the version of Python build: - os: ubuntu-20.04 + os: ubuntu-22.04 tools: - python: "3.9" + python: "3.12" jobs: pre_build: - python ./scripts/_locale.py --compile diff --git a/Dockerfile b/Dockerfile index 6ea14975f..4da75effa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -62,7 +62,7 @@ RUN groupadd -g 1000 retroarcher && \ RUN mkdir -p /config VOLUME /config -CMD ["python", "retroarcher.py"] +CMD ["python", "./src/retroarcher.py"] EXPOSE 9696 HEALTHCHECK --start-period=90s CMD python retroarcher.py --docker_healthcheck || exit 1 diff --git a/docs/source/conf.py b/docs/source/conf.py index 3078cc453..5576d266d 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -6,6 +6,8 @@ # standard imports from datetime import datetime +import os +import sys # -- Path setup -------------------------------------------------------------- @@ -13,17 +15,17 @@ # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -import os -import sys script_dir = os.path.dirname(os.path.abspath(__file__)) # the directory of this file source_dir = os.path.dirname(script_dir) # the source folder directory root_dir = os.path.dirname(source_dir) # the root folder directory +src_dir = os.path.join(root_dir, 'src') # the src folder directory try: - sys.path.insert(0, root_dir) - from pyra import definitions # put this in a try/except to prevent flake8 warning -except Exception: + sys.path.insert(0, src_dir) + from common import definitions # put this in a try/except to prevent flake8 warning +except Exception as e: + print(f"Unable to import definitions from {root_dir}: {e}") sys.exit(1) # -- Project information ----------------------------------------------------- @@ -42,10 +44,11 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'm2r2', # enable markdown files + 'myst_parser', # enable markdown files 'numpydoc', # this automatically loads `sphinx.ext.autosummary` as well 'sphinx.ext.autodoc', # autodocument modules 'sphinx.ext.autosectionlabel', + 'sphinx.ext.intersphinx', # link to other projects' documentation 'sphinx.ext.todo', # enable to-do sections 'sphinx.ext.viewcode' # add links to view source code ] @@ -59,13 +62,16 @@ exclude_patterns = ['toc.rst'] # Extensions to include. -source_suffix = ['.rst', '.md'] +source_suffix = { + '.rst': 'restructuredtext', + '.md': 'markdown', +} # -- Options for HTML output ------------------------------------------------- # images -html_favicon = os.path.join(definitions.Paths().ROOT_DIR, 'web', 'images', 'retroarcher.ico') +html_favicon = os.path.join(definitions.Paths().ROOT_DIR, 'web', 'images', 'favicon.ico') html_logo = os.path.join(definitions.Paths().ROOT_DIR, 'web', 'images', 'logo-circle.png') # Add any paths that contain custom static files (such as style sheets) here, @@ -102,3 +108,13 @@ # disable epub mimetype warnings # https://github.com/readthedocs/readthedocs.org/blob/eadf6ac6dc6abc760a91e1cb147cc3c5f37d1ea8/docs/conf.py#L235-L236 suppress_warnings = ["epub.unknown_project_files"] + +python_version = f'{sys.version_info.major}.{sys.version_info.minor}' + +intersphinx_mapping = { + 'python': ('https://docs.python.org/{}/'.format(python_version), None), +} + +numpydoc_show_class_members = True +numpydoc_show_inherited_class_members = False +numpydoc_xref_param_type = True diff --git a/docs/source/contributing/localization.rst b/docs/source/contributing/localization.rst index 56c5fc4a0..48bab51f1 100644 --- a/docs/source/contributing/localization.rst +++ b/docs/source/contributing/localization.rst @@ -43,7 +43,7 @@ situations. For example the system tray icon is user interfacing and therefore s - In order for strings to be extracted from python code, the following lines must be added. .. code-block:: python - from pyra import locales + from common import locales _ = locales.get_text() - Wrap the string to be extracted in a function as shown. @@ -76,8 +76,7 @@ any of the following paths are modified. .. code-block:: yaml - - 'retroarcher.py' - - 'pyra/**.py' + - 'src/**.py' - 'web/templates/**' When testing locally it may be desirable to manually extract, initialize, update, and compile strings. diff --git a/docs/source/pyra_docs/config.rst b/docs/source/pyra_docs/config.rst deleted file mode 100644 index 27d12371a..000000000 --- a/docs/source/pyra_docs/config.rst +++ /dev/null @@ -1,7 +0,0 @@ -.. include:: ../global.rst - -:modname:`pyra.config` ----------------------- -.. automodule:: pyra.config - :members: - :show-inheritance: diff --git a/docs/source/pyra_docs/hardware.rst b/docs/source/pyra_docs/hardware.rst deleted file mode 100644 index 7c5a1c8a6..000000000 --- a/docs/source/pyra_docs/hardware.rst +++ /dev/null @@ -1,7 +0,0 @@ -.. include:: ../global.rst - -:modname:`pyra.hardware` ---------------------------- -.. automodule:: pyra.hardware - :members: - :show-inheritance: diff --git a/docs/source/pyra_docs/helpers.rst b/docs/source/pyra_docs/helpers.rst deleted file mode 100644 index a5f116a8d..000000000 --- a/docs/source/pyra_docs/helpers.rst +++ /dev/null @@ -1,7 +0,0 @@ -.. include:: ../global.rst - -:modname:`pyra.helpers` ------------------------ -.. automodule:: pyra.helpers - :members: - :show-inheritance: diff --git a/docs/source/pyra_docs/locales.rst b/docs/source/pyra_docs/locales.rst deleted file mode 100644 index db2754c90..000000000 --- a/docs/source/pyra_docs/locales.rst +++ /dev/null @@ -1,7 +0,0 @@ -.. include:: ../global.rst - -:modname:`pyra.locales` ------------------------ -.. automodule:: pyra.locales - :members: - :show-inheritance: diff --git a/docs/source/pyra_docs/logger.rst b/docs/source/pyra_docs/logger.rst deleted file mode 100644 index c4c38a92b..000000000 --- a/docs/source/pyra_docs/logger.rst +++ /dev/null @@ -1,7 +0,0 @@ -.. include:: ../global.rst - -:modname:`pyra.logger` ----------------------- -.. automodule:: pyra.logger - :members: - :show-inheritance: diff --git a/docs/source/pyra_docs/threads.rst b/docs/source/pyra_docs/threads.rst deleted file mode 100644 index 0d29f9d2f..000000000 --- a/docs/source/pyra_docs/threads.rst +++ /dev/null @@ -1,7 +0,0 @@ -.. include:: ../global.rst - -:modname:`pyra.threads` ------------------------ -.. automodule:: pyra.threads - :members: - :show-inheritance: diff --git a/docs/source/pyra_docs/webapp.rst b/docs/source/pyra_docs/webapp.rst deleted file mode 100644 index c8f3dcdf2..000000000 --- a/docs/source/pyra_docs/webapp.rst +++ /dev/null @@ -1,7 +0,0 @@ -.. include:: ../global.rst - -:modname:`pyra.webapp` ----------------------- -.. automodule:: pyra.webapp - :members: - :show-inheritance: diff --git a/docs/source/src/common/common.rst b/docs/source/src/common/common.rst new file mode 100644 index 000000000..0f301a6a3 --- /dev/null +++ b/docs/source/src/common/common.rst @@ -0,0 +1,7 @@ +.. include:: ../global.rst + +:modname:`common.__init__` +-------------------------- +.. automodule:: common + :members: + :show-inheritance: diff --git a/docs/source/pyra_docs/pyra.rst b/docs/source/src/common/config.rst similarity index 62% rename from docs/source/pyra_docs/pyra.rst rename to docs/source/src/common/config.rst index 1355c9f68..30f6b6c99 100644 --- a/docs/source/pyra_docs/pyra.rst +++ b/docs/source/src/common/config.rst @@ -1,7 +1,7 @@ .. include:: ../global.rst -:modname:`pyra.__init__` +:modname:`common.config` ------------------------ -.. automodule:: pyra +.. automodule:: common.config :members: :show-inheritance: diff --git a/docs/source/src/common/definitions.rst b/docs/source/src/common/definitions.rst new file mode 100644 index 000000000..86c964650 --- /dev/null +++ b/docs/source/src/common/definitions.rst @@ -0,0 +1,7 @@ +.. include:: ../global.rst + +:modname:`common.definitions` +----------------------------- +.. automodule:: common.definitions + :members: + :show-inheritance: diff --git a/docs/source/src/common/hardware.rst b/docs/source/src/common/hardware.rst new file mode 100644 index 000000000..66e9c33a1 --- /dev/null +++ b/docs/source/src/common/hardware.rst @@ -0,0 +1,7 @@ +.. include:: ../global.rst + +:modname:`common.hardware` +-------------------------- +.. automodule:: common.hardware + :members: + :show-inheritance: diff --git a/docs/source/pyra_docs/tray_icon.rst b/docs/source/src/common/helpers.rst similarity index 61% rename from docs/source/pyra_docs/tray_icon.rst rename to docs/source/src/common/helpers.rst index e4fc34c28..9f08d062f 100644 --- a/docs/source/pyra_docs/tray_icon.rst +++ b/docs/source/src/common/helpers.rst @@ -1,7 +1,7 @@ .. include:: ../global.rst -:modname:`pyra.tray_icon` +:modname:`common.helpers` ------------------------- -.. automodule:: pyra.tray_icon +.. automodule:: common.helpers :members: :show-inheritance: diff --git a/docs/source/src/common/locales.rst b/docs/source/src/common/locales.rst new file mode 100644 index 000000000..f6a1776ce --- /dev/null +++ b/docs/source/src/common/locales.rst @@ -0,0 +1,7 @@ +.. include:: ../global.rst + +:modname:`common.locales` +------------------------- +.. automodule:: common.locales + :members: + :show-inheritance: diff --git a/docs/source/src/common/logger.rst b/docs/source/src/common/logger.rst new file mode 100644 index 000000000..5451e66a2 --- /dev/null +++ b/docs/source/src/common/logger.rst @@ -0,0 +1,7 @@ +.. include:: ../global.rst + +:modname:`common.logger` +------------------------ +.. automodule:: common.logger + :members: + :show-inheritance: diff --git a/docs/source/src/common/threads.rst b/docs/source/src/common/threads.rst new file mode 100644 index 000000000..dda872f9e --- /dev/null +++ b/docs/source/src/common/threads.rst @@ -0,0 +1,7 @@ +.. include:: ../global.rst + +:modname:`common.threads` +------------------------- +.. automodule:: common.threads + :members: + :show-inheritance: diff --git a/docs/source/pyra_docs/definitions.rst b/docs/source/src/common/tray_icon.rst similarity index 60% rename from docs/source/pyra_docs/definitions.rst rename to docs/source/src/common/tray_icon.rst index 7902f1ab2..ea83144d3 100644 --- a/docs/source/pyra_docs/definitions.rst +++ b/docs/source/src/common/tray_icon.rst @@ -1,7 +1,7 @@ .. include:: ../global.rst -:modname:`pyra.definitions` +:modname:`common.tray_icon` --------------------------- -.. automodule:: pyra.definitions +.. automodule:: common.tray_icon :members: :show-inheritance: diff --git a/docs/source/src/common/webapp.rst b/docs/source/src/common/webapp.rst new file mode 100644 index 000000000..68ce33887 --- /dev/null +++ b/docs/source/src/common/webapp.rst @@ -0,0 +1,7 @@ +.. include:: ../global.rst + +:modname:`common.webapp` +------------------------ +.. automodule:: common.webapp + :members: + :show-inheritance: diff --git a/docs/source/global.rst b/docs/source/src/global.rst similarity index 100% rename from docs/source/global.rst rename to docs/source/src/global.rst diff --git a/docs/source/main/retroarcher.rst b/docs/source/src/retroarcher.rst similarity index 80% rename from docs/source/main/retroarcher.rst rename to docs/source/src/retroarcher.rst index 805365900..35fb9b9c2 100644 --- a/docs/source/main/retroarcher.rst +++ b/docs/source/src/retroarcher.rst @@ -1,4 +1,4 @@ -.. include:: ../global.rst +.. include:: global.rst :modname:`retroarcher` ---------------------- diff --git a/docs/source/toc.rst b/docs/source/toc.rst index c03f6a46a..5e0089e6c 100644 --- a/docs/source/toc.rst +++ b/docs/source/toc.rst @@ -19,17 +19,17 @@ .. toctree:: :maxdepth: 0 - :caption: Code + :caption: Source Code :titlesonly: - main/retroarcher - pyra_docs/pyra - pyra_docs/config - pyra_docs/definitions - pyra_docs/hardware - pyra_docs/helpers - pyra_docs/locales - pyra_docs/logger - pyra_docs/threads - pyra_docs/tray_icon - pyra_docs/webapp + src/retroarcher + src/common/common + src/common/config + src/common/definitions + src/common/hardware + src/common/helpers + src/common/locales + src/common/logger + src/common/threads + src/common/tray_icon + src/common/webapp diff --git a/requirements.txt b/requirements.txt index 4c3804602..c8ba36da1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,13 +5,15 @@ Flask-Babel==4.0.0 furo==2024.7.18 GPUtil==1.4.0 IPy==1.01 -m2r2==0.3.3.post2 +myst-parser==4.0.0 numexpr==2.10.1 numpydoc==1.7.0 Pillow==9.5.0 +polib==1.2.0 psutil==6.0.0 pyadl==0.1 pyamdgpuinfo==2.1.6; sys_platform == 'Linux' pystray==0.19.5 requests==2.32.3 Sphinx==7.2.6 +werkzeug==3.0.3 diff --git a/scripts/_locale.py b/scripts/_locale.py index cc80b0f81..d4d09aaed 100644 --- a/scripts/_locale.py +++ b/scripts/_locale.py @@ -15,16 +15,21 @@ root_dir = os.path.dirname(script_dir) locale_dir = os.path.join(root_dir, 'locale') -# retroarcher target locales +# target locales target_locales = [ - 'de', # Deutsch + 'de', # German 'en', # English 'en_GB', # English (United Kingdom) 'en_US', # English (United States) - 'es', # español - 'fr', # français - 'it', # italiano - 'ru', # русский + 'es', # Spanish + 'fr', # French + 'it', # Italian + 'ja', # Japanese + 'pt', # Portuguese + 'ru', # Russian + 'sv', # Swedish + 'tr', # Turkish + 'zh', # Chinese (Simplified) ] @@ -41,9 +46,8 @@ def babel_extract(): f'--project={project_name}', '--version=v0', '--add-comments=NOTE', - './retroarcher.py', - './pyra', - './web' + './src', + './web', ] print(commands) diff --git a/scripts/build.py b/scripts/build.py index 773b010ab..3727a33c6 100644 --- a/scripts/build.py +++ b/scripts/build.py @@ -14,14 +14,14 @@ def build(): """Sets arguments for pyinstaller, creates spec, and builds binaries.""" pyinstaller_args = [ - 'retroarcher.py', + './src/retroarcher.py', '--onefile', '--noconfirm', '--paths=./', '--add-data=docs:docs', '--add-data=web:web', '--add-data=locale:locale', - '--icon=./web/images/retroarcher.ico' + '--icon=./web/images/favicon.ico' ] if sys.platform.lower() == 'win32': # windows @@ -29,10 +29,8 @@ def build(): pyinstaller_args.append('--splash=./web/images/logo-circle.png') # fix args for windows - arg_count = 0 - for arg in pyinstaller_args: - pyinstaller_args[arg_count] = arg.replace(':', ';') - arg_count += 1 + for index, arg in enumerate(pyinstaller_args): + pyinstaller_args[index] = arg.replace(':', ';') elif sys.platform.lower() == 'darwin': # macOS pyinstaller_args.append('--console') pyinstaller_args.append('--osx-bundle-identifier=dev.lizardbyte.retroarcher') diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pyra/__init__.py b/src/common/__init__.py similarity index 95% rename from pyra/__init__.py rename to src/common/__init__.py index e1473cc7b..15fde9d37 100644 --- a/pyra/__init__.py +++ b/src/common/__init__.py @@ -15,10 +15,10 @@ from typing import Union # local imports -from pyra import config -from pyra import definitions -from pyra import helpers -from pyra import logger +from common import config +from common import definitions +from common import helpers +from common import logger # get logger log = logger.get_logger(name=__name__) @@ -121,7 +121,7 @@ def stop(exit_code: Union[int, str] = 0, restart: bool = False): >>> stop(exit_code=0, restart=False) """ # stop the tray icon - from pyra.tray_icon import tray_end + from common.tray_icon import tray_end try: tray_end() except AttributeError: diff --git a/pyra/config.py b/src/common/config.py similarity index 78% rename from pyra/config.py rename to src/common/config.py index c01b67da8..bf2b9b0c4 100644 --- a/pyra/config.py +++ b/src/common/config.py @@ -1,10 +1,12 @@ """ .. - config.py +src/common/config.py Responsible for config related functions. """ # standard imports +import base64 +import copy import sys from typing import Optional, List @@ -13,9 +15,9 @@ from validate import Validator, ValidateError # local imports -from pyra import definitions -from pyra import logger -from pyra import locales +from common import definitions +from common import logger +from common import locales # get log log = logger.get_logger(name=__name__) @@ -47,14 +49,14 @@ def on_change_tray_toggle() -> bool: See Also -------- - pyra.tray_icon.tray_toggle : ``on_change_tray_toggle`` is an alias of this function. + common.tray_icon.tray_toggle : ``on_change_tray_toggle`` is an alias of this function. Examples -------- >>> on_change_tray_toggle() True """ - from pyra import tray_icon + from common import tray_icon return tray_icon.tray_toggle() @@ -108,12 +110,34 @@ def on_change_tray_toggle() -> bool: description=_('The localization setting to use.'), default='en', options=[ + 'de', 'en', + 'en_GB', + 'en_US', 'es', + 'fr', + 'it', + 'ja', + 'pt', + 'ru', + 'sv', + 'tr', + 'zh', ], option_names=[ + f'German ({_("German")})', f'English ({_("English")})', - f'Español ({_("Spanish")})', + f'English (Great Britain) ({_("English (Great Britain)")})', + f'English (United States) ({_("English (United States)")})', + f'Spanish ({_("Spanish")})', + f'French ({_("French")})', + f'Italian ({_("Italian")})', + f'Japanese ({_("Japanese")})', + f'Portuguese ({_("Portuguese")})', + f'Russian ({_("Russian")})', + f'Swedish ({_("Swedish")})', + f'Turkish ({_("Turkish")})', + f'Chinese (Simplified) ({_("Chinese (Simplified)")})', ], refresh=True, extra_class='col-lg-6', @@ -221,6 +245,103 @@ def on_change_tray_toggle() -> bool: ) +def is_masked_field(section: str, key: str) -> bool: + """ + Check if a field is masked. + + Parameters + ---------- + section : str + The section of the config. + key : str + The key of the config field. + + Returns + ------- + bool + True if the field is masked, otherwise False. + """ + return _CONFIG_SPEC_DICT.get(section, {}).get(key, {}).get('mask', False) + + +def encode_value(value: str) -> str: + """ + Encode a value using base64. + + Parameters + ---------- + value : str + The value to encode. + + Returns + ------- + str + The encoded value. + """ + return base64.b64encode(value.encode('utf-8')).decode('utf-8') + + +def decode_value(value: str) -> str: + """ + Decode a base64 encoded value. + + Parameters + ---------- + value : str + The value to decode. + + Returns + ------- + str + The decoded value. + """ + return base64.b64decode(value.encode('utf-8')).decode('utf-8') + + +def encode_config(config: ConfigObj) -> ConfigObj: + """ + Encode masked fields in the config. + + Parameters + ---------- + config : ConfigObj + The config object to encode. + + Returns + ------- + ConfigObj + The config object with encoded masked fields. + """ + for section, options in config.items(): + for key, value in options.items(): + if is_masked_field(section=section, key=key): + config[section][key] = encode_value(value) + return config + + +def decode_config(config: ConfigObj) -> dict: + """ + Decode masked fields in the config. + + Parameters + ---------- + config : ConfigObj + The config object to decode. + + Returns + ------- + dict + A decoded copy of the config object. + """ + _config = copy.deepcopy(config) # we need to do a deepcopy to avoid modifying the original config + + for section, options in _config.items(): + for key, value in options.items(): + if is_masked_field(section=section, key=key): + _config[section][key] = decode_value(value) + return _config + + def convert_config(d: dict = _CONFIG_SPEC_DICT, _config_spec: Optional[List] = None) -> List: """ Convert a config spec dictionary to a config spec list. @@ -255,6 +376,10 @@ def convert_config(d: dict = _CONFIG_SPEC_DICT, _config_spec: Optional[List] = N except TypeError: pass else: + # if a default value is not set, then set it to None + if 'default' not in v: + v['default'] = None + checks = ['min', 'max', 'options', 'default'] check_value = '' @@ -332,7 +457,7 @@ def create_config(config_file: str, config_spec: dict = _CONFIG_SPEC_DICT) -> Co encoding='UTF-8', list_values=True, stringify=True, - write_empty_values=False + write_empty_values=False, ) config_valid = validate_config(config=config) @@ -348,7 +473,7 @@ def create_config(config_file: str, config_spec: dict = _CONFIG_SPEC_DICT) -> Co encoding='UTF-8', list_values=True, stringify=True, - write_empty_values=False + write_empty_values=False, ) user_config_valid = validate_config(config=user_config) if not user_config_valid: @@ -376,7 +501,7 @@ def create_config(config_file: str, config_spec: dict = _CONFIG_SPEC_DICT) -> Co validate_config(config=config) config.filename = config_file - config.write() # write the config file + save_config(config=config) if config_spec == _CONFIG_SPEC_DICT: # set CONFIG dictionary global CONFIG diff --git a/pyra/definitions.py b/src/common/definitions.py similarity index 90% rename from pyra/definitions.py rename to src/common/definitions.py index e0fc24288..0acb332fd 100644 --- a/pyra/definitions.py +++ b/src/common/definitions.py @@ -124,8 +124,10 @@ class Paths: The purpose of this class is to ensure consistency when using these paths. - PYRA_DIR : str - The directory containing the retroarcher python files. + COMMON_DIR : str + The directory containing the common python files. + SRC_DIR : str + The directory containing the application python files ROOT_DIR : str The root directory of the application. This is where the source files exist. DATA_DIR : str @@ -139,13 +141,14 @@ class Paths: Examples -------- - >>> Paths.logs + >>> Paths.LOG_DIR '.../logs' """ - PYRA_DIR = os.path.dirname(os.path.abspath(__file__)) - ROOT_DIR = os.path.dirname(PYRA_DIR) + COMMON_DIR = os.path.dirname(os.path.abspath(__file__)) + SRC_DIR = os.path.dirname(COMMON_DIR) + ROOT_DIR = os.path.dirname(SRC_DIR) DATA_DIR = ROOT_DIR - BINARY_PATH = os.path.abspath(os.path.join(DATA_DIR, 'retroarcher.py')) + BINARY_PATH = os.path.abspath(os.path.join(SRC_DIR, 'retroarcher.py')) if Modes.FROZEN: # pyinstaller build DATA_DIR = os.path.dirname(sys.executable) diff --git a/pyra/hardware.py b/src/common/hardware.py similarity index 98% rename from pyra/hardware.py rename to src/common/hardware.py index 44b885d78..9ad556de2 100644 --- a/pyra/hardware.py +++ b/src/common/hardware.py @@ -12,10 +12,10 @@ import psutil # local imports -from pyra import definitions -from pyra import helpers -from pyra import locales -from pyra import logger +from common import definitions +from common import helpers +from common import locales +from common import logger try: cpu_name = cpuinfo.cpu.info[0]['ProcessorNameString'].strip() @@ -335,7 +335,7 @@ def chart_data() -> dict: See Also -------- - pyra.webapp.callback_dashboard : A callback called by javascript to get this data. + common.webapp.callback_dashboard : A callback called by javascript to get this data. Examples -------- diff --git a/pyra/helpers.py b/src/common/helpers.py similarity index 100% rename from pyra/helpers.py rename to src/common/helpers.py diff --git a/pyra/locales.py b/src/common/locales.py similarity index 97% rename from pyra/locales.py rename to src/common/locales.py index 0e97027e3..f88afbd06 100644 --- a/pyra/locales.py +++ b/src/common/locales.py @@ -27,9 +27,9 @@ from babel import localedata # local imports -from pyra import config -from pyra.definitions import Paths -from pyra import logger +from common import config +from common.definitions import Paths +from common import logger default_domain = 'retroarcher' default_locale = 'en' diff --git a/pyra/logger.py b/src/common/logger.py similarity index 95% rename from pyra/logger.py rename to src/common/logger.py index 6c1601b30..3b99326ee 100644 --- a/pyra/logger.py +++ b/src/common/logger.py @@ -25,12 +25,12 @@ from configobj import ConfigObj # local imports -import pyra -from pyra import definitions -from pyra import helpers +import common +from common import definitions +from common import helpers # These settings are for file logging only -py_name = 'pyra' +py_name = 'common' MAX_SIZE = 5000000 # 5 MB MAX_FILES = 5 @@ -67,7 +67,7 @@ def blacklist_config(config: ConfigObj): Examples -------- - >>> config_object = pyra.config.create_config(config_file='config.ini') + >>> config_object = common.config.create_config(config_file='config.ini') >>> blacklist_config(config=config_object) """ blacklist = set() @@ -102,7 +102,7 @@ class NoThreadFilter(logging.Filter): Examples -------- >>> NoThreadFilter('main') - + """ def __init__(self, threadName): @@ -152,7 +152,7 @@ class BlacklistFilter(logging.Filter): Examples -------- >>> BlacklistFilter() - + """ def __init__(self): @@ -223,7 +223,7 @@ class RegexFilter(logging.Filter): Examples -------- >>> RegexFilter() - + """ def __init__(self): @@ -301,7 +301,7 @@ class PublicIPFilter(RegexFilter): Examples -------- >>> PublicIPFilter() - + """ def __init__(self): @@ -358,7 +358,7 @@ class EmailFilter(RegexFilter): Examples -------- >>> EmailFilter() - + """ def __init__(self): @@ -414,7 +414,7 @@ class PlexTokenFilter(RegexFilter): Examples -------- >>> PlexTokenFilter() - + """ def __init__(self): @@ -461,6 +461,10 @@ def listener(logger: logging.Logger): logger : logging.Logger The logger object. + Yields + ------ + None + Examples -------- >>> logger = get_logger(name='retroarcher') @@ -568,7 +572,7 @@ def setup_loggers(): """ loggers_list = [py_name, 'werkzeug'] - submodules = pkgutil.iter_modules(pyra.__path__) + submodules = pkgutil.iter_modules(common.__path__) for submodule in submodules: loggers_list.append(f'{py_name}.{submodule[1]}') @@ -613,7 +617,7 @@ def init_logger(log_name: str) -> logging.Logger: # Configure the logger to accept all messages logger.propagate = False - logger.setLevel(logging.DEBUG if pyra.DEBUG else logging.INFO) + logger.setLevel(logging.DEBUG if common.DEBUG else logging.INFO) # Setup file logger file_formatter = logging.Formatter('%(asctime)s - %(levelname)-7s :: %(threadName)s : %(message)s', @@ -631,7 +635,7 @@ def init_logger(log_name: str) -> logging.Logger: logger.addHandler(file_handler) # Setup console logger - if not pyra.QUIET: + if not common.QUIET: console_formatter = logging.Formatter('%(asctime)s - %(levelname)s :: %(threadName)s : %(message)s', '%Y-%m-%d %H:%M:%S') console_handler = logging.StreamHandler() @@ -643,7 +647,7 @@ def init_logger(log_name: str) -> logging.Logger: # Add filters to log handlers # Only add filters after the config file has been initialized # Nothing prior to initialization should contain sensitive information - if not pyra.DEV and pyra.CONFIG: + if not common.DEV and common.CONFIG: log_handlers = logger.handlers for handler in log_handlers: handler.addFilter(BlacklistFilter()) @@ -652,7 +656,7 @@ def init_logger(log_name: str) -> logging.Logger: handler.addFilter(PlexTokenFilter()) # Install exception hooks - if log_name == py_name: # all tracebacks go to 'pyra.log' + if log_name == py_name: # all tracebacks go to 'common.log' _init_hooks(logger) # replace warn diff --git a/pyra/threads.py b/src/common/threads.py similarity index 84% rename from pyra/threads.py rename to src/common/threads.py index 10dd23785..2af452c9c 100644 --- a/pyra/threads.py +++ b/src/common/threads.py @@ -11,15 +11,15 @@ Examples -------- ->>> from pyra import config, threads, tray_icon +>>> from common import config, threads, tray_icon >>> config_object = config.create_config(config_file='config.ini') >>> tray_icon.icon = tray_icon.tray_initialize() >>> threads.run_in_thread(target=tray_icon.tray_run, name='pystray', daemon=True).start() ->>> from pyra import config, threads, webapp +>>> from common import config, threads, webapp >>> config_object = config.create_config(config_file='config.ini') >>> threads.run_in_thread(target=webapp.start_webapp, name='Flask', daemon=True).start() - * Serving Flask app 'pyra.webapp' (lazy loading) + * Serving Flask app 'common.webapp' (lazy loading) ... * Running on http://.../ (Press CTRL+C to quit) """ diff --git a/pyra/tray_icon.py b/src/common/tray_icon.py similarity index 95% rename from pyra/tray_icon.py rename to src/common/tray_icon.py index 6b7baee68..c4d453081 100644 --- a/pyra/tray_icon.py +++ b/src/common/tray_icon.py @@ -12,13 +12,13 @@ from PIL import Image # local imports -import pyra -from pyra import config -from pyra import definitions -from pyra import helpers -from pyra import locales -from pyra import logger -from pyra import threads +import common +from common import config +from common import definitions +from common import helpers +from common import locales +from common import logger +from common import threads # setup _ = locales.get_text() @@ -68,7 +68,7 @@ def tray_initialize() -> Union[Icon, bool]: tray_icon = Icon(name='retroarcher') tray_icon.title = definitions.Names.name - image = Image.open(os.path.join(definitions.Paths.ROOT_DIR, 'web', 'images', 'retroarcher.ico')) + image = Image.open(os.path.join(definitions.Paths.ROOT_DIR, 'web', 'images', 'favicon.ico')) tray_icon.icon = image # NOTE: Open the application. "%(app_name)s" = "RetroArcher". Do not translate "%(app_name)s". @@ -197,7 +197,7 @@ def tray_run_threaded() -> bool: -------- tray_initialize : This function first, initializes the tray icon using ``tray_initialize()``. tray_run : Then, ``tray_run`` is executed in a thread. - pyra.threads.run_in_thread : Run a method within a thread. + common.threads.run_in_thread : Run a method within a thread. Examples -------- @@ -243,26 +243,26 @@ def tray_quit(): """ Shutdown RetroArcher. - Set the 'pyra.SIGNAL' variable to 'shutdown'. + Set the 'common.SIGNAL' variable to 'shutdown'. Examples -------- >>> tray_quit() """ - pyra.SIGNAL = 'shutdown' + common.SIGNAL = 'shutdown' def tray_restart(): """ Restart RetroArcher. - Set the 'pyra.SIGNAL' variable to 'restart'. + Set the 'common.SIGNAL' variable to 'restart'. Examples -------- >>> tray_restart() """ - pyra.SIGNAL = 'restart' + common.SIGNAL = 'restart' def tray_run(): diff --git a/pyra/webapp.py b/src/common/webapp.py similarity index 74% rename from pyra/webapp.py rename to src/common/webapp.py index 713127322..f73aab170 100644 --- a/pyra/webapp.py +++ b/src/common/webapp.py @@ -1,10 +1,10 @@ """ -.. - webapp.py +src/common/webapp.py Responsible for serving the webapp. """ # standard imports +import json import os from typing import Optional @@ -12,18 +12,34 @@ from flask import Flask, Response from flask import jsonify, render_template as flask_render_template, request, send_from_directory from flask_babel import Babel +import polib +from werkzeug.utils import secure_filename # local imports -import pyra -from pyra import config -from pyra import hardware -from pyra.definitions import Paths -from pyra import locales -from pyra import logger +import common +from common import config +from common import hardware +from common.definitions import Paths +from common import locales +from common import logger # localization _ = locales.get_text() +responses = { + 500: Response(response='Internal Server Error', status=500, mimetype='text/plain') +} + +# mime type map +mime_type_map = { + 'gif': 'image/gif', + 'ico': 'image/vnd.microsoft.icon', + 'jpg': 'image/jpeg', + 'jpeg': 'image/jpeg', + 'png': 'image/png', + 'svg': 'image/svg+xml', +} + # setup flask app app = Flask( import_name=__name__, @@ -85,7 +101,8 @@ def render_template(template_name_or_list, **context): -------- >>> render_template(template_name_or_list='home.html', title=_('Home')) """ - context['ui_config'] = pyra.CONFIG['User_Interface'].copy() + print(common.CONFIG) + context['ui_config'] = common.CONFIG['User_Interface'].copy() return flask_render_template(template_name_or_list=template_name_or_list, **context) @@ -134,7 +151,7 @@ def callback_dashboard() -> Response: See Also -------- - pyra.hardware.chart_data : This function sets up the data in the proper format. + common.hardware.chart_data : This function sets up the data in the proper format. Examples -------- @@ -177,7 +194,7 @@ def settings(configuration_spec: Optional[str]) -> render_template: -------- >>> settings() """ - config_settings = pyra.CONFIG + config_settings = config.decode_config(common.CONFIG) if not configuration_spec: config_spec = config._CONFIG_SPEC_DICT @@ -221,30 +238,43 @@ def docs(filename) -> send_from_directory: return send_from_directory(directory=os.path.join(Paths.DOCS_DIR), path=filename) -@app.route('/favicon.ico') -def favicon() -> send_from_directory: +@app.route( + '/favicon.ico', + defaults={'img': 'favicon.ico'}, + methods=['GET'], +) +@app.route("/images/", methods=["GET"]) +def image(img: str) -> send_from_directory: """ - Serve the favicon.ico file. - - .. todo:: This documentation needs to be improved. + Get image from static/images directory. Returns ------- flask.send_from_directory - The ico file. + The image. Notes ----- The following routes trigger this function. - `/favicon.ico` + - `/favicon.ico` + - `/images/` Examples -------- - >>> favicon() + >>> image('favicon.ico') """ - return send_from_directory(directory=os.path.join(app.static_folder, 'images'), - path='retroarcher.ico', mimetype='image/vnd.microsoft.icon') + directory = os.path.join(app.static_folder, 'images') + filename = os.path.basename(secure_filename(filename=img)) # sanitize the input + + if os.path.isfile(os.path.join(directory, filename)): + file_extension = filename.rsplit('.', 1)[-1] + if file_extension in mime_type_map: + return send_from_directory(directory=directory, path=filename, mimetype=mime_type_map[file_extension]) + else: + return Response(response='Invalid file type', status=400, mimetype='text/plain') + else: + return Response(response='Image not found', status=404, mimetype='text/plain') @app.route('/status') @@ -273,7 +303,7 @@ def test_logger() -> str: """ Test logging functions. - Check `./logs/pyra.webapp.log` for output. + Check `./logs/common.webapp.log` for output. Returns ------- @@ -320,7 +350,7 @@ def api_settings(configuration_spec: Optional[str]) -> Response: Examples -------- - >>> callback_dashboard() + >>> api_settings() """ if not configuration_spec: @@ -342,6 +372,7 @@ def api_settings(configuration_spec: Optional[str]) -> Response: } data = request.form + _config = config.decode_config(common.CONFIG) for option, value in data.items(): split_option = option.split('|', 1) key = split_option[0] @@ -351,7 +382,7 @@ def api_settings(configuration_spec: Optional[str]) -> Response: # get the original value try: - og_value = config.CONFIG[key][setting] + og_value = _config[key][setting] except KeyError: og_value = '' finally: @@ -361,6 +392,8 @@ def api_settings(configuration_spec: Optional[str]) -> Response: value = float(value) if setting_type == 'integer': value = int(value) + if config.is_masked_field(section=key, key=setting): + value = config.encode_value(value) if og_value != value: # setting changed, get the on change command @@ -395,19 +428,56 @@ def start_webapp(): Examples -------- >>> start_webapp() - * Serving Flask app 'pyra.webapp' (lazy loading) + * Serving Flask app 'common.webapp' (lazy loading) ... * Running on http://.../ (Press CTRL+C to quit) - >>> from pyra import webapp, threads + >>> from common import webapp, threads >>> threads.run_in_thread(target=webapp.start_webapp, name='Flask', daemon=True).start() - * Serving Flask app 'pyra.webapp' (lazy loading) + * Serving Flask app 'common.webapp' (lazy loading) ... * Running on http://.../ (Press CTRL+C to quit) """ app.run( host=config.CONFIG['Network']['HTTP_HOST'], port=config.CONFIG['Network']['HTTP_PORT'], - debug=pyra.DEV, + debug=common.DEV, use_reloader=False # reloader doesn't work when running in a separate thread ) + + +@app.route("/translations", methods=["GET"]) +def translations() -> Response: + """ + Serve the translations. + + Returns + ------- + Response + The translations. + + Examples + -------- + >>> translations() + """ + locale = locales.get_locale() + + po_files = [ + f'{Paths.LOCALE_DIR}/{locale}/LC_MESSAGES/retroarcher.po', # selected locale + f'{Paths.LOCALE_DIR}/retroarcher.po', # fallback to default domain + ] + + for po_file in po_files: + if os.path.isfile(po_file): + po = polib.pofile(po_file) + + # convert the po to json + data = dict() + for entry in po: + if entry.msgid: + data[entry.msgid] = entry.msgstr + app.logger.debug(f'Translation: {entry.msgid} -> {entry.msgstr}') + + return Response(response=json.dumps(data), + status=200, + mimetype='application/json') diff --git a/retroarcher.py b/src/retroarcher.py similarity index 83% rename from retroarcher.py rename to src/retroarcher.py index 0b3edac8f..be5e2cefd 100644 --- a/retroarcher.py +++ b/src/retroarcher.py @@ -16,15 +16,15 @@ from typing import Union # local imports -import pyra -from pyra import config -from pyra import definitions -from pyra import helpers -from pyra import locales -from pyra import logger -from pyra import threads +import common +from common import config +from common import definitions +from common import helpers +from common import locales +from common import logger +from common import threads -py_name = 'pyra' +py_name = 'common' # locales _ = locales.get_text() @@ -153,16 +153,16 @@ def main(): else: config_file = os.path.join(definitions.Paths.DATA_DIR, definitions.Files.CONFIG) if args.debug: - pyra.DEBUG = True + common.DEBUG = True if args.dev: - pyra.DEV = True + common.DEV = True if args.quiet: - pyra.QUIET = True + common.QUIET = True # initialize retroarcher # logging should not occur until after initialize # any submodules that require translations need to be imported after config is initialize - pyra.initialize(config_file=config_file) + common.initialize(config_file=config_file) if args.config: log.info(msg=f"RetroArcher is using custom config file: {config_file}.") @@ -178,7 +178,7 @@ def main(): config.CONFIG.write() if config.CONFIG['General']['SYSTEM_TRAY']: - from pyra import tray_icon # submodule requires translations so importing after initialization + from common import tray_icon # submodule requires translations so importing after initialization # also do not import if not required by config options tray_icon.tray_run_threaded() @@ -188,7 +188,7 @@ def main(): pyi_splash.update_text("Starting the webapp") time.sleep(3) # show splash screen for a min of 3 seconds pyi_splash.close() # close the splash screen - from pyra import webapp # import at use due to translations + from common import webapp # import at use due to translations threads.run_in_thread(target=webapp.start_webapp, name='Flask', daemon=True).start() # this should be after starting flask app @@ -203,38 +203,38 @@ def wait(): """ Wait for signal. - Endlessly loop while `pyra.SIGNAL = None`. - If `pyra.SIGNAL` is changed to `shutdown` or `restart` `pyra.stop()` will be executed. - If KeyboardInterrupt signal is detected `pyra.stop()` will be executed. + Endlessly loop while `common.SIGNAL = None`. + If `common.SIGNAL` is changed to `shutdown` or `restart` `common.stop()` will be executed. + If KeyboardInterrupt signal is detected `common.stop()` will be executed. Examples -------- >>> wait() """ - from pyra import hardware # submodule requires translations so importing after initialization + from common import hardware # submodule requires translations so importing after initialization log.info("RetroArcher is ready!") while True: # wait endlessly for a signal - if not pyra.SIGNAL: + if not common.SIGNAL: hardware.update() # update dashboard resource values try: time.sleep(1) except KeyboardInterrupt: - pyra.SIGNAL = 'shutdown' + common.SIGNAL = 'shutdown' else: - log.info(f'Received signal: {pyra.SIGNAL}') + log.info(f'Received signal: {common.SIGNAL}') - if pyra.SIGNAL == 'shutdown': - pyra.stop() - elif pyra.SIGNAL == 'restart': - pyra.stop(restart=True) + if common.SIGNAL == 'shutdown': + common.stop() + elif common.SIGNAL == 'restart': + common.stop(restart=True) else: log.error('Unknown signal. Shutting down...') - pyra.stop() + common.stop() break -if __name__ == "__main__": +if __name__ == "__main__": # pragma: no cover main() diff --git a/tests/conftest.py b/tests/conftest.py index f65567ad7..7bb7cd6ce 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,15 +6,22 @@ """ # standard imports import os +import sys # lib imports import pytest -# local imports -import pyra -from pyra import config -from pyra import definitions -from pyra import webapp +root_dir = os.path.abspath(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +src_dir = os.path.join(root_dir, 'src') + +if os.path.isdir(src_dir): # avoid flake8 E402 warning + sys.path.insert(0, src_dir) + + # local imports + import common + from common import config + from common import definitions + from common import webapp @pytest.fixture(scope='function') @@ -36,17 +43,17 @@ def test_config_object(test_config_file): @pytest.fixture(scope='function') -def test_pyra_init(test_config_file): - test_pyra_init = pyra.initialize(config_file=test_config_file) +def test_common_init(test_config_file): + test_common_init = common.initialize(config_file=test_config_file) - yield test_pyra_init + yield test_common_init - pyra._INITIALIZED = False - pyra.SIGNAL = 'shutdown' + common._INITIALIZED = False + common.SIGNAL = 'shutdown' @pytest.fixture(scope='function') -def test_client(test_pyra_init): +def test_client(test_common_init): """Create a test client for testing webapp endpoints""" app = webapp.app app.testing = True diff --git a/tests/functional/test_webapp.py b/tests/functional/test_webapp.py index 57ea98ee5..9e411c377 100644 --- a/tests/functional/test_webapp.py +++ b/tests/functional/test_webapp.py @@ -2,7 +2,7 @@ .. test_webapp.py -Functional tests for pyra.webapp. +Functional tests for common.webapp. """ # standard imports import json diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index d8052f018..b5dbaf5f9 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -2,7 +2,7 @@ .. test_config.py -Unit tests for pyra.config. +Unit tests for common.config. """ # standard imports import time @@ -12,7 +12,7 @@ import pytest # local imports -from pyra import config +from common import config def test_create_config(test_config_file): @@ -45,7 +45,7 @@ def test_convert_config(): def test_on_change_tray_toggle(): """Tests the on_change_tray_toggle function""" - from pyra import tray_icon + from common import tray_icon if not tray_icon.icon_supported: pytest.skip("tray icon not supported") diff --git a/tests/unit/test_definitions.py b/tests/unit/test_definitions.py index d8cab3870..3aae90a4f 100644 --- a/tests/unit/test_definitions.py +++ b/tests/unit/test_definitions.py @@ -2,10 +2,10 @@ .. test_definitions.py -Unit tests for pyra.definitions.py. +Unit tests for common.definitions.py. """ # local imports -from pyra import definitions +from common import definitions def test_names(): @@ -52,7 +52,8 @@ def test_paths(): """Tests Paths class""" paths = definitions.Paths - assert paths.PYRA_DIR + assert paths.COMMON_DIR + assert paths.SRC_DIR assert paths.ROOT_DIR assert paths.DATA_DIR assert paths.BINARY_PATH diff --git a/tests/unit/test_hardware.py b/tests/unit/test_hardware.py index 83d41b382..9e397b1fb 100644 --- a/tests/unit/test_hardware.py +++ b/tests/unit/test_hardware.py @@ -2,13 +2,13 @@ .. test_hardware.py -Unit tests for pyra.hardware.py. +Unit tests for common.hardware.py. """ # lib imports import pytest # local imports -from pyra import hardware +from common import hardware def test_update(): diff --git a/tests/unit/test_helpers.py b/tests/unit/test_helpers.py index 86d2b60bc..05d45c4cf 100644 --- a/tests/unit/test_helpers.py +++ b/tests/unit/test_helpers.py @@ -2,14 +2,14 @@ .. test_helpers.py -Unit tests for pyra.helpers.py. +Unit tests for common.helpers.py. """ # standard imports import datetime import logging # local imports -from pyra import helpers +from common import helpers def test_check_folder_writeable(): @@ -22,7 +22,7 @@ def test_check_folder_writeable(): def test_get_logger(): """Test that logger object can be created""" - test_logger = helpers.get_logger(name='pyra') + test_logger = helpers.get_logger(name='common') assert isinstance(test_logger, logging.Logger) diff --git a/tests/unit/test_init.py b/tests/unit/test_init.py index 69ae5f979..7879328f4 100644 --- a/tests/unit/test_init.py +++ b/tests/unit/test_init.py @@ -2,15 +2,15 @@ .. test_init.py -Unit tests for pyra.__init__.py. +Unit tests for common.__init__.py. """ import pytest -def test_initialize(test_pyra_init): +def test_initialize(test_common_init): """Tests initializing retroarcher""" - print(test_pyra_init) - assert test_pyra_init + print(test_common_init) + assert test_common_init @pytest.mark.skip(reason="impossible to test as it has a sys.exit() event and won't actually return") diff --git a/tests/unit/test_locales.py b/tests/unit/test_locales.py index e150018f5..23d704bc7 100644 --- a/tests/unit/test_locales.py +++ b/tests/unit/test_locales.py @@ -2,13 +2,13 @@ .. test_locales.py -Unit tests for pyra.locales.py. +Unit tests for common.locales.py. """ # standard imports import inspect # local imports -from pyra import locales +from common import locales def test_get_all_locales(): diff --git a/tests/unit/test_logger.py b/tests/unit/test_logger.py index b9834448c..6d42cba87 100644 --- a/tests/unit/test_logger.py +++ b/tests/unit/test_logger.py @@ -2,13 +2,13 @@ .. test_logger.py -Unit tests for pyra.logger.py. +Unit tests for common.logger.py. """ # standard imports import logging # local imports -from pyra import logger +from common import logger def test_blacklist_config(): @@ -33,7 +33,7 @@ def test_init_multiprocessing(): def test_get_logger(): """Test that logger object can be created""" - log = logger.get_logger(name='pyra') + log = logger.get_logger(name='common') assert isinstance(log, logging.Logger) @@ -44,7 +44,7 @@ def test_setup_loggers(): def test_init_logger(): """Test that logger can be initialized""" - log = logger.init_logger(log_name='pyra') + log = logger.init_logger(log_name='common') assert isinstance(log, logging.Logger) diff --git a/tests/unit/test_threads.py b/tests/unit/test_threads.py index 6adf05afa..53fa7fcea 100644 --- a/tests/unit/test_threads.py +++ b/tests/unit/test_threads.py @@ -2,10 +2,10 @@ .. test_threads.py -Unit tests for pyra.threads. +Unit tests for common.threads. """ # local imports -from pyra import threads +from common import threads def test_run_in_thread(): diff --git a/tests/unit/test_tray_icon.py b/tests/unit/test_tray_icon.py index 22124b671..a1af84351 100644 --- a/tests/unit/test_tray_icon.py +++ b/tests/unit/test_tray_icon.py @@ -2,7 +2,7 @@ .. test_tray_icon.py -Unit tests for pyra.tray_icon. +Unit tests for common.tray_icon. """ # standard imports import time @@ -11,8 +11,8 @@ import pytest # local imports -import pyra -from pyra import tray_icon +import common +from common import tray_icon @pytest.fixture(scope='function') @@ -118,7 +118,7 @@ def test_tray_quit(): """Test tray_quit function""" tray_icon.tray_quit() - signal = pyra.SIGNAL + signal = common.SIGNAL assert signal == 'shutdown' @@ -127,7 +127,7 @@ def test_tray_restart(): """Test tray_restart function""" tray_icon.tray_restart() - signal = pyra.SIGNAL + signal = common.SIGNAL assert signal == 'restart' diff --git a/tests/unit/test_webapp.py b/tests/unit/test_webapp.py index f28952ab2..f9032f941 100644 --- a/tests/unit/test_webapp.py +++ b/tests/unit/test_webapp.py @@ -2,14 +2,14 @@ .. test_webapp.py -Unit tests for pyra.webapp. +Unit tests for common.webapp. """ # standard imports import sys # local imports -from pyra import threads -from pyra import webapp +from common import threads +from common import webapp def test_start_webapp(): diff --git a/web/images/retroarcher.ico b/web/images/favicon.ico similarity index 100% rename from web/images/retroarcher.ico rename to web/images/favicon.ico diff --git a/web/js/discord.js b/web/js/discord.js deleted file mode 100644 index 92c974ec7..000000000 --- a/web/js/discord.js +++ /dev/null @@ -1,47 +0,0 @@ -// this script requires jquery to be loaded on the source page, like so... -// -function getRandomIntInclusive(min, max) { - min = Math.ceil(min); - max = Math.floor(max); - return Math.floor(Math.random() * (max - min + 1) + min); -} - -function randomQuote(quote, crate) { - let the_quote = null - if (quote['quote_safe']) { - the_quote = quote['quote_safe']; - } else { - the_quote = quote['quote']; - } - - crate.notify(the_quote) -} - -let quote = null - -// get random video game quotes -$.ajax({ - url: `https://app.lizardbyte.dev/uno/random-quotes/games.json`, - type: "GET", - dataType: "json", - success: function (result) { - let quote_index = getRandomIntInclusive(0, result.length - 1); - quote = result[quote_index] - } -}); - -// use Jquery to load other javascript -$.getScript('https://cdn.jsdelivr.net/npm/@widgetbot/crate@3', function() -{ - const crate = new Crate({ - server: '804382334370578482', - channel: '804383092822900797', - defer: false, - }) - - let sleep = ms => { - return new Promise(resolve => setTimeout(resolve, ms)); - }; - // sleep for 1 second - sleep(420000).then(() => {randomQuote(quote, crate)}) -}); diff --git a/web/js/translations.js b/web/js/translations.js new file mode 100644 index 000000000..fe1a239bf --- /dev/null +++ b/web/js/translations.js @@ -0,0 +1,32 @@ +let translations = null + +let getTranslation = function(string) { + // download translations + if (translations === null) { + $.ajax({ + async: false, + url: "/translations/", + type: "GET", + dataType: "json", + success: function (result) { + translations = result + } + }) + } + + if (translations) { + try { + if (translations[string]) { + return translations[string] + } else { + return string + } + } catch (err) { + return string + } + } + else { + // could not download translations + return string + } +} diff --git a/web/templates/base.html b/web/templates/base.html index b51a04de2..4cfbff052 100644 --- a/web/templates/base.html +++ b/web/templates/base.html @@ -30,7 +30,6 @@ {% block head %}{% endblock head %} - @@ -54,7 +53,7 @@ - +