From b6c5543e8b9543747fadb0fd72d462df5dda6bca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Kruli=C5=A1?= Date: Thu, 17 Jul 2025 18:35:37 +0200 Subject: [PATCH] Migrating to new .toml config. --- .gitignore | 25 +- BUILD.md | 44 ++ LICENSE | 44 +- README.rst | 1 + docs/Makefile | 462 +++++++++---------- docs/conf.py | 590 ++++++++++++------------ docs/index.rst | 48 +- main.py | 10 +- monitor/config_manager.py | 262 +++++------ monitor/install/config.yml | 26 +- monitor/install/postinst | 38 +- monitor/install/recodex-monitor.service | 46 +- monitor/main.py | 134 +++--- monitor/test/__init__.py | 6 +- monitor/test/__main__.py | 14 +- monitor/test/test_ClientConnections.py | 116 ++--- monitor/test/test_ConfigManager.py | 186 ++++---- monitor/test/test_ServerConnection.py | 112 ++--- monitor/zeromq_connection.py | 156 +++---- pyproject.toml | 48 ++ recodex-monitor.spec | 29 +- requirements.txt | 28 +- setup.py | 30 -- testing/index.html | 60 +-- testing/submitter.py | 60 +-- 25 files changed, 1326 insertions(+), 1249 deletions(-) create mode 100644 BUILD.md create mode 100644 pyproject.toml delete mode 100644 setup.py diff --git a/.gitignore b/.gitignore index db2f3d6..9061e85 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,13 @@ -*.pyc -__pycache__ -.idea/ -build/ -dist/ -recodex_monitor.egg-info/ -docs/* -!docs/index.rst -!docs/Makefile -!docs/conf.py -MANIFEST - +*.pyc +__pycache__ +.idea/ +build/ +dist/ +recodex_monitor.egg-info/ +docs/* +!docs/index.rst +!docs/Makefile +!docs/conf.py +MANIFEST + +/.vscode diff --git a/BUILD.md b/BUILD.md new file mode 100644 index 0000000..1da3f1b --- /dev/null +++ b/BUILD.md @@ -0,0 +1,44 @@ +# Build Instructions + +This project has been converted from the old `setup.py` style to the modern `pyproject.toml` configuration. + +## Building the Package + +### Using pip (recommended) +```bash +pip install build +python -m build +``` + +### Using setuptools directly +```bash +python setup.py sdist bdist_wheel +``` + +### Installing in development mode +```bash +pip install -e . +``` + +## RPM Package Building + +The `.spec` file has been updated to work with the new build system: + +```bash +rpmbuild -ba recodex-monitor.spec +``` + +## Changes Made + +1. **Created `pyproject.toml`** - Modern Python packaging configuration +2. **Updated `setup.py`** - Now defers to `pyproject.toml` for backward compatibility +3. **Created `setup.cfg`** - Additional configuration file (optional) +4. **Updated `recodex-monitor.spec`** - Modified to work with wheel-based builds +5. **Updated `requirements.txt`** - Cleaned up dependencies + +## Migration Notes + +- Version is now dynamically read from `monitor.__version__` +- All package metadata is centralized in `pyproject.toml` +- The build process now creates modern wheel packages +- RPM packaging uses wheel installation instead of direct setup.py diff --git a/LICENSE b/LICENSE index 120cf86..687844c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,22 +1,22 @@ -The MIT License (MIT) - -Copyright (c) 2016 ReCodEx Team - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - +The MIT License (MIT) + +Copyright (c) 2016 ReCodEx Team + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/README.rst b/README.rst index 64260d9..8f64644 100644 --- a/README.rst +++ b/README.rst @@ -89,6 +89,7 @@ tab of monitor GitHub repository (it is architecture independent package) - Other Linux distributions can install monitor by calling setup directly... .. code:: bash + ~$ python3 setup.py install --install-scripts /usr/bin ~# ./install/postinst diff --git a/docs/Makefile b/docs/Makefile index 4eeb22a..094df8c 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,231 +1,231 @@ -# Makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -PAPER = -BUILDDIR = _build - -# User-friendly check for sphinx-build -ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) - $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don\'t have Sphinx installed, grab it from http://sphinx-doc.org/) -endif - -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . -# the i18n builder cannot share the environment and doctrees with the others -I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . - -.PHONY: help -help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " applehelp to make an Apple Help Book" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " epub3 to make an epub3" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " texinfo to make Texinfo files" - @echo " info to make Texinfo files and run them through makeinfo" - @echo " gettext to make PO message catalogs" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " xml to make Docutils-native XML files" - @echo " pseudoxml to make pseudoxml-XML files for display purposes" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - @echo " coverage to run coverage check of the documentation (if enabled)" - @echo " dummy to check syntax errors of document sources" - -.PHONY: clean -clean: - rm -rf $(BUILDDIR)/* - -.PHONY: html -html: - sphinx-apidoc -f -o . ../monitor - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -.PHONY: dirhtml -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -.PHONY: singlehtml -singlehtml: - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -.PHONY: pickle -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -.PHONY: json -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -.PHONY: htmlhelp -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -.PHONY: qthelp -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/monitor.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/monitor.qhc" - -.PHONY: applehelp -applehelp: - $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp - @echo - @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." - @echo "N.B. You won't be able to view it unless you put it in" \ - "~/Library/Documentation/Help or install it in your application" \ - "bundle." - -.PHONY: devhelp -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/monitor" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/monitor" - @echo "# devhelp" - -.PHONY: epub -epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -.PHONY: epub3 -epub3: - $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 - @echo - @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." - -.PHONY: latex -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make' in that directory to run these through (pdf)latex" \ - "(use \`make latexpdf' here to do that automatically)." - -.PHONY: latexpdf -latexpdf: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through pdflatex..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -.PHONY: latexpdfja -latexpdfja: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through platex and dvipdfmx..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -.PHONY: text -text: - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -.PHONY: man -man: - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -.PHONY: texinfo -texinfo: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo - @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." - @echo "Run \`make' in that directory to run these through makeinfo" \ - "(use \`make info' here to do that automatically)." - -.PHONY: info -info: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo "Running Texinfo files through makeinfo..." - make -C $(BUILDDIR)/texinfo info - @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." - -.PHONY: gettext -gettext: - $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale - @echo - @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." - -.PHONY: changes -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -.PHONY: linkcheck -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." - -.PHONY: doctest -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." - -.PHONY: coverage -coverage: - $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage - @echo "Testing of coverage in the sources finished, look at the " \ - "results in $(BUILDDIR)/coverage/python.txt." - -.PHONY: xml -xml: - $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml - @echo - @echo "Build finished. The XML files are in $(BUILDDIR)/xml." - -.PHONY: pseudoxml -pseudoxml: - $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml - @echo - @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." - -.PHONY: dummy -dummy: - $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy - @echo - @echo "Build finished. Dummy builder generates no files." +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# User-friendly check for sphinx-build +ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) + $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don\'t have Sphinx installed, grab it from http://sphinx-doc.org/) +endif + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " applehelp to make an Apple Help Book" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " epub3 to make an epub3" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + @echo " coverage to run coverage check of the documentation (if enabled)" + @echo " dummy to check syntax errors of document sources" + +.PHONY: clean +clean: + rm -rf $(BUILDDIR)/* + +.PHONY: html +html: + sphinx-apidoc -f -o . ../monitor + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +.PHONY: dirhtml +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +.PHONY: singlehtml +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +.PHONY: pickle +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +.PHONY: json +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +.PHONY: htmlhelp +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +.PHONY: qthelp +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/monitor.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/monitor.qhc" + +.PHONY: applehelp +applehelp: + $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp + @echo + @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." + @echo "N.B. You won't be able to view it unless you put it in" \ + "~/Library/Documentation/Help or install it in your application" \ + "bundle." + +.PHONY: devhelp +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/monitor" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/monitor" + @echo "# devhelp" + +.PHONY: epub +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +.PHONY: epub3 +epub3: + $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 + @echo + @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." + +.PHONY: latex +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +.PHONY: latexpdf +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +.PHONY: latexpdfja +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +.PHONY: text +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +.PHONY: man +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +.PHONY: texinfo +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +.PHONY: info +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +.PHONY: gettext +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +.PHONY: changes +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +.PHONY: linkcheck +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +.PHONY: doctest +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +.PHONY: coverage +coverage: + $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage + @echo "Testing of coverage in the sources finished, look at the " \ + "results in $(BUILDDIR)/coverage/python.txt." + +.PHONY: xml +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +.PHONY: pseudoxml +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." + +.PHONY: dummy +dummy: + $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy + @echo + @echo "Build finished. Dummy builder generates no files." diff --git a/docs/conf.py b/docs/conf.py index 359ed52..0f71cad 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,295 +1,295 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# monitor documentation build configuration file, created by -# sphinx-quickstart on Fri May 13 19:29:14 2016. -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -import sys -import os - -# 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. -sys.path.insert(0, os.path.abspath('..')) -sys.path.insert(1, os.path.abspath('../monitor')) - -# -- General configuration ------------------------------------------------ - -# If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.todo', - 'sphinx.ext.coverage', - 'sphinx.ext.imgmath', - 'sphinx.ext.viewcode', - 'sphinx.ext.githubpages', -] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix(es) of source filenames. -# You can specify multiple suffix as a list of string: -# source_suffix = ['.rst', '.md'] -source_suffix = '.rst' - -# The encoding of source files. -#source_encoding = 'utf-8-sig' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = 'monitor' -copyright = '2016, ReCodEx Team, Petr Stefan' -author = 'ReCodEx Team, Petr Stefan' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = '0.1.0' -# The full version, including alpha/beta/rc tags. -release = '0.1.0' - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] - -# The reST default role (used for this markup: `text`) to use for all -# documents. -#default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -#add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] - -# If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False - -# If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = True - - -# -- Options for HTML output ---------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -html_theme = 'alabaster' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -#html_theme_options = {} - -# Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] - -# The name for this set of Sphinx documents. -# " v documentation" by default. -#html_title = 'monitor v0.1.0' - -# A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -#html_logo = None - -# The name of an image file (relative to this directory) to use as a favicon of -# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -#html_favicon = None - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - -# Add any extra paths that contain custom files (such as robots.txt or -# .htaccess) here, relative to this directory. These files are copied -# directly to the root of the documentation. -#html_extra_path = [] - -# If not None, a 'Last updated on:' timestamp is inserted at every page -# bottom, using the given strftime format. -# The empty string is equivalent to '%b %d, %Y'. -#html_last_updated_fmt = None - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -#html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} - -# If false, no module index is generated. -#html_domain_indices = True - -# If false, no index is generated. -#html_use_index = True - -# If true, the index is split into individual pages for each letter. -#html_split_index = False - -# If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None - -# Language to be used for generating the HTML full-text search index. -# Sphinx supports the following languages: -# 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' -# 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh' -#html_search_language = 'en' - -# A dictionary with options for the search language support, empty by default. -# 'ja' uses this config value. -# 'zh' user can custom change `jieba` dictionary path. -#html_search_options = {'type': 'default'} - -# The name of a javascript file (relative to the configuration directory) that -# implements a search results scorer. If empty, the default will be used. -#html_search_scorer = 'scorer.js' - -# Output file base name for HTML help builder. -htmlhelp_basename = 'monitordoc' - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', - -# Latex figure (float) alignment -#'figure_align': 'htbp', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - (master_doc, 'monitor.tex', 'monitor Documentation', - 'ReCodEx Team, Petr Stefan', 'manual'), -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -#latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -#latex_use_parts = False - -# If true, show page references after internal links. -#latex_show_pagerefs = False - -# If true, show URL addresses after external links. -#latex_show_urls = False - -# Documents to append as an appendix to all manuals. -#latex_appendices = [] - -# If false, no module index is generated. -#latex_domain_indices = True - - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'monitor', 'monitor Documentation', - [author], 1) -] - -# If true, show URL addresses after external links. -#man_show_urls = False - - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - (master_doc, 'monitor', 'monitor Documentation', - author, 'monitor', 'One line description of project.', - 'Miscellaneous'), -] - -# Documents to append as an appendix to all manuals. -#texinfo_appendices = [] - -# If false, no module index is generated. -#texinfo_domain_indices = True - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' - -# If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# monitor documentation build configuration file, created by +# sphinx-quickstart on Fri May 13 19:29:14 2016. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys +import os + +# 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. +sys.path.insert(0, os.path.abspath('..')) +sys.path.insert(1, os.path.abspath('../monitor')) + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.todo', + 'sphinx.ext.coverage', + 'sphinx.ext.imgmath', + 'sphinx.ext.viewcode', + 'sphinx.ext.githubpages', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = 'monitor' +copyright = '2016, ReCodEx Team, Petr Stefan' +author = 'ReCodEx Team, Petr Stefan' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '0.1.0' +# The full version, including alpha/beta/rc tags. +release = '0.1.0' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +#keep_warnings = False + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = True + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'alabaster' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. +# " v documentation" by default. +#html_title = 'monitor v0.1.0' + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (relative to this directory) to use as a favicon of +# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +#html_extra_path = [] + +# If not None, a 'Last updated on:' timestamp is inserted at every page +# bottom, using the given strftime format. +# The empty string is equivalent to '%b %d, %Y'. +#html_last_updated_fmt = None + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Language to be used for generating the HTML full-text search index. +# Sphinx supports the following languages: +# 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' +# 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh' +#html_search_language = 'en' + +# A dictionary with options for the search language support, empty by default. +# 'ja' uses this config value. +# 'zh' user can custom change `jieba` dictionary path. +#html_search_options = {'type': 'default'} + +# The name of a javascript file (relative to the configuration directory) that +# implements a search results scorer. If empty, the default will be used. +#html_search_scorer = 'scorer.js' + +# Output file base name for HTML help builder. +htmlhelp_basename = 'monitordoc' + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +#'papersize': 'letterpaper', + +# The font size ('10pt', '11pt' or '12pt'). +#'pointsize': '10pt', + +# Additional stuff for the LaTeX preamble. +#'preamble': '', + +# Latex figure (float) alignment +#'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'monitor.tex', 'monitor Documentation', + 'ReCodEx Team, Petr Stefan', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'monitor', 'monitor Documentation', + [author], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'monitor', 'monitor Documentation', + author, 'monitor', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +#texinfo_no_detailmenu = False diff --git a/docs/index.rst b/docs/index.rst index 32ef902..7ad491b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,24 +1,24 @@ -.. monitor documentation master file, created by - sphinx-quickstart on Fri May 13 19:29:14 2016. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -Welcome to monitor's documentation! -=================================== - -Contents: - -.. toctree:: - :maxdepth: 2 - - modules - monitor - - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` - +.. monitor documentation master file, created by + sphinx-quickstart on Fri May 13 19:29:14 2016. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to monitor's documentation! +=================================== + +Contents: + +.. toctree:: + :maxdepth: 2 + + modules + monitor + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/main.py b/main.py index 97a4c98..62def30 100644 --- a/main.py +++ b/main.py @@ -1,5 +1,5 @@ -#!/usr/bin/env python3 - -if __name__ == "__main__": - from monitor.main import main - main() +#!/usr/bin/env python3 + +if __name__ == "__main__": + from monitor.main import main + main() diff --git a/monitor/config_manager.py b/monitor/config_manager.py index 4f7c714..fac0c7c 100644 --- a/monitor/config_manager.py +++ b/monitor/config_manager.py @@ -1,131 +1,131 @@ -#!/usr/bin/env python3 - -import yaml -import logging -import logging.handlers - - -class ConfigManager: - """ - Class to handle all configuration items. - """ - def __init__(self, config_file=None): - """ - Init with default values. - - :param config_file: Path to YAML configuration file. - If not given, default values are used. - """ - - self._config = dict() - if config_file: - try: - with open(config_file, 'r') as f: - self._config = yaml.safe_load(f) - except FileNotFoundError: - # using defaults - pass - - def get_websocket_uri(self): - """ - Get address for websocket server. - - :return: List with 2 items - string hostname and int port - """ - return self._config['websocket_uri'] if 'websocket_uri' in self._config else ['127.0.0.1', 4567] - - def get_zeromq_uri(self): - """ - Get address for zeromq server. - - :return: List with 2 items - string hostname and int port - """ - return self._config['zeromq_uri'] if 'zeromq_uri' in self._config else ['127.0.0.1', 7894] - - def get_logger_settings(self): - """ - Get path to system log file. - - :return: List with 4 items - string path, logging level, integer maximum size - of logfile and integer number of rotations kept. - """ - result = ["/tmp/recodex-monitor.log", logging.INFO, 1024*1024, 3] - if 'logger' in self._config: - sect = self._config['logger'] - if 'file' in sect: - result[0] = sect['file'] - if 'level' in sect: - result[1] = self._get_loglevel_from_string(sect['level']) - if 'max-size' in sect: - try: - result[2] = int(sect['max-size']) - except: - pass - if 'rotations' in sect: - try: - result[3] = int(sect['rotations']) - except: - pass - return result - - def _get_loglevel_from_string(self, str_level): - """ - Convert logging level from string to logging module type. - - :param str_level: string representation of logging level - :return: logging level (defaults to logging.INFO) - """ - level_mapping = { - "debug": logging.DEBUG, - "info": logging.INFO, - "warning": logging.WARNING, - "error": logging.ERROR, - "critical": logging.CRITICAL - } - if str_level in level_mapping: - return level_mapping[str_level] - else: - return logging.INFO - - -def init_logger(logfile, level, max_size, rotations): - """ - Initialize new system logger for monitor. If arguments are invalid, - empty logger will be created. - - :param logfile: Path to file with log. - :param level: Log level as logging. - :param max_size: Maximum size of log file. - :param rotations: Number of log files kept. - :return: Initialized logger. - """ - try: - # create logger - logger = logging.getLogger('recodex-monitor') - logger.setLevel(level) - - # create rotating file handler - ch = logging.handlers.RotatingFileHandler(logfile, maxBytes=max_size, backupCount=rotations) - ch.setLevel(level) - - # create formatter - formatter = logging.Formatter('[%(asctime)s] [%(name)s] [%(levelname)s] %(message)s') - - # add formatter to ch - ch.setFormatter(formatter) - - # add ch to logger - logger.addHandler(ch) - except Exception as e: - # create empty logger - print("Invalid logger configuration. Creating null logger. Error: {}".format(e)) - logger = logging.getLogger('recodex-monitor-dummy') - logging.disable(logging.CRITICAL) - - # print welcome message to log file - logger.critical("-------------------------") - logger.critical(" ReCodEx Monitor started") - logger.critical("-------------------------") - - # return created logger - return logger +#!/usr/bin/env python3 + +import yaml +import logging +import logging.handlers + + +class ConfigManager: + """ + Class to handle all configuration items. + """ + def __init__(self, config_file=None): + """ + Init with default values. + + :param config_file: Path to YAML configuration file. + If not given, default values are used. + """ + + self._config = dict() + if config_file: + try: + with open(config_file, 'r') as f: + self._config = yaml.safe_load(f) + except FileNotFoundError: + # using defaults + pass + + def get_websocket_uri(self): + """ + Get address for websocket server. + + :return: List with 2 items - string hostname and int port + """ + return self._config['websocket_uri'] if 'websocket_uri' in self._config else ['127.0.0.1', 4567] + + def get_zeromq_uri(self): + """ + Get address for zeromq server. + + :return: List with 2 items - string hostname and int port + """ + return self._config['zeromq_uri'] if 'zeromq_uri' in self._config else ['127.0.0.1', 7894] + + def get_logger_settings(self): + """ + Get path to system log file. + + :return: List with 4 items - string path, logging level, integer maximum size + of logfile and integer number of rotations kept. + """ + result = ["/tmp/recodex-monitor.log", logging.INFO, 1024*1024, 3] + if 'logger' in self._config: + sect = self._config['logger'] + if 'file' in sect: + result[0] = sect['file'] + if 'level' in sect: + result[1] = self._get_loglevel_from_string(sect['level']) + if 'max-size' in sect: + try: + result[2] = int(sect['max-size']) + except: + pass + if 'rotations' in sect: + try: + result[3] = int(sect['rotations']) + except: + pass + return result + + def _get_loglevel_from_string(self, str_level): + """ + Convert logging level from string to logging module type. + + :param str_level: string representation of logging level + :return: logging level (defaults to logging.INFO) + """ + level_mapping = { + "debug": logging.DEBUG, + "info": logging.INFO, + "warning": logging.WARNING, + "error": logging.ERROR, + "critical": logging.CRITICAL + } + if str_level in level_mapping: + return level_mapping[str_level] + else: + return logging.INFO + + +def init_logger(logfile, level, max_size, rotations): + """ + Initialize new system logger for monitor. If arguments are invalid, + empty logger will be created. + + :param logfile: Path to file with log. + :param level: Log level as logging. + :param max_size: Maximum size of log file. + :param rotations: Number of log files kept. + :return: Initialized logger. + """ + try: + # create logger + logger = logging.getLogger('recodex-monitor') + logger.setLevel(level) + + # create rotating file handler + ch = logging.handlers.RotatingFileHandler(logfile, maxBytes=max_size, backupCount=rotations) + ch.setLevel(level) + + # create formatter + formatter = logging.Formatter('[%(asctime)s] [%(name)s] [%(levelname)s] %(message)s') + + # add formatter to ch + ch.setFormatter(formatter) + + # add ch to logger + logger.addHandler(ch) + except Exception as e: + # create empty logger + print("Invalid logger configuration. Creating null logger. Error: {}".format(e)) + logger = logging.getLogger('recodex-monitor-dummy') + logging.disable(logging.CRITICAL) + + # print welcome message to log file + logger.critical("-------------------------") + logger.critical(" ReCodEx Monitor started") + logger.critical("-------------------------") + + # return created logger + return logger diff --git a/monitor/install/config.yml b/monitor/install/config.yml index 4e7960f..200149e 100644 --- a/monitor/install/config.yml +++ b/monitor/install/config.yml @@ -1,13 +1,13 @@ ---- -websocket_uri: - - "127.0.0.1" - - 4567 -zeromq_uri: - - "127.0.0.1" - - 7894 -logger: - file: "/var/log/recodex/monitor.log" - level: "debug" # level of logging - one of "debug", "info", "warning", "error", "critical" - max-size: 1048576 # 1 MB; max size of file before log rotation - rotations: 3 # number of rotations kept -... +--- +websocket_uri: + - "127.0.0.1" + - 4567 +zeromq_uri: + - "127.0.0.1" + - 7894 +logger: + file: "/var/log/recodex/monitor.log" + level: "debug" # level of logging - one of "debug", "info", "warning", "error", "critical" + max-size: 1048576 # 1 MB; max size of file before log rotation + rotations: 3 # number of rotations kept +... diff --git a/monitor/install/postinst b/monitor/install/postinst index e7baa11..f39dcca 100755 --- a/monitor/install/postinst +++ b/monitor/install/postinst @@ -1,19 +1,19 @@ -#!/bin/sh - -CONF_DIR=/etc/recodex -LOG_DIR=/var/log/recodex - -# Create 'recodex' user if not exist -id -u recodex > /dev/null 2>&1 -if [ $? -eq 1 ] -then - useradd --system --shell /sbin/nologin recodex -fi - -# Create default logging directory and set proper permission -mkdir -p ${LOG_DIR} -chown -R recodex:recodex ${LOG_DIR} - -# Change owner of config files -chown -R recodex:recodex ${CONF_DIR} - +#!/bin/sh + +CONF_DIR=/etc/recodex +LOG_DIR=/var/log/recodex + +# Create 'recodex' user if not exist +id -u recodex > /dev/null 2>&1 +if [ $? -eq 1 ] +then + useradd --system --shell /sbin/nologin recodex +fi + +# Create default logging directory and set proper permission +mkdir -p ${LOG_DIR} +chown -R recodex:recodex ${LOG_DIR} + +# Change owner of config files +chown -R recodex:recodex ${CONF_DIR} + diff --git a/monitor/install/recodex-monitor.service b/monitor/install/recodex-monitor.service index ad42119..0cf9115 100644 --- a/monitor/install/recodex-monitor.service +++ b/monitor/install/recodex-monitor.service @@ -1,23 +1,23 @@ -# ReCodEx monitor systemd configuration file -# -# This file should be placed in /etc/systemd/system/ directory -# For starting monitor, following should be met: -# - config file with name 'config.yml' in conf directory -# - run 'systemctl start recodex-monitor.service - -[Unit] -Description=Recodex Monitor -Documentation=http://recodex.github.io/monitor/,https://github.com/ReCodEx/GlobalWiki/wiki -DefaultDependencies=true - -[Service] -Type=simple -StandardInput=null -StandardOutput=null -StandardError=journal -User=recodex -Group=recodex -ExecStart=/usr/bin/recodex-monitor -c /etc/recodex/monitor/config.yml - -[Install] -WantedBy=multi-user.target +# ReCodEx monitor systemd configuration file +# +# This file should be placed in /etc/systemd/system/ directory +# For starting monitor, following should be met: +# - config file with name 'config.yml' in conf directory +# - run 'systemctl start recodex-monitor.service + +[Unit] +Description=Recodex Monitor +Documentation=http://recodex.github.io/monitor/,https://github.com/ReCodEx/GlobalWiki/wiki +DefaultDependencies=true + +[Service] +Type=simple +StandardInput=null +StandardOutput=null +StandardError=journal +User=recodex +Group=recodex +ExecStart=/usr/bin/recodex-monitor -c /etc/recodex/monitor/config.yml + +[Install] +WantedBy=multi-user.target diff --git a/monitor/main.py b/monitor/main.py index bc420d9..69d5341 100644 --- a/monitor/main.py +++ b/monitor/main.py @@ -1,67 +1,67 @@ -#!/usr/bin/env python3 -""" -Script which runs the monitor - tool for resending messages from ZeroMQ to WebSockets. -""" - -from .websocket_connections import ClientConnections, WebsocketServer -from .zeromq_connection import ServerConnection -from .config_manager import ConfigManager, init_logger -import asyncio -import argparse - - -parser = argparse.ArgumentParser() -parser.add_argument('-c', '--config', help="Path to configuration file", default=None) - - -def main(): - """ - Main function of monitor program. - - :return: Nothing - """ - args = parser.parse_args() - - # get configuration - config = ConfigManager(args.config) - # create event loop for websocket server thread - loop = asyncio.new_event_loop() - # get logger - logger = init_logger(*config.get_logger_settings()) - # here we'll store all active connections - connections = ClientConnections(logger, loop) - - websock_server = None - try: - logger.info("starting websocket server ...") - # run websocket part of monitor in separate thread - websock_server = WebsocketServer(config.get_websocket_uri(), connections, loop, logger) - websock_server.start() - logger.info("websocket server started") - - # create zeromq connection - logger.info("starting zeromq server ...") - zmq_uri = config.get_zeromq_uri() - zmq_server = ServerConnection(zmq_uri[0], zmq_uri[1], logger) - - # specify callback for zeromq incoming message - def message_callback(client_id, data): - loop.call_soon_threadsafe(connections.send_message, client_id, data) - # start zeromq server with given callback - zmq_server.start(message_callback) - except KeyboardInterrupt: - logger.warning("keyboard interrupt detected") - finally: - logger.warning("quiting...") - loop.call_soon_threadsafe(connections.remove_all_clients) - logger.debug("websocket clients removed") - loop.call_soon_threadsafe(loop.stop) - logger.debug("websocket message loop stopped") - if websock_server: - websock_server.join() - logger.debug("websocket server thread exited") - loop.close() - logger.debug("main thread exited") - -if __name__ == "__main__": - main() +#!/usr/bin/env python3 +""" +Script which runs the monitor - tool for resending messages from ZeroMQ to WebSockets. +""" + +from .websocket_connections import ClientConnections, WebsocketServer +from .zeromq_connection import ServerConnection +from .config_manager import ConfigManager, init_logger +import asyncio +import argparse + + +parser = argparse.ArgumentParser() +parser.add_argument('-c', '--config', help="Path to configuration file", default=None) + + +def main(): + """ + Main function of monitor program. + + :return: Nothing + """ + args = parser.parse_args() + + # get configuration + config = ConfigManager(args.config) + # create event loop for websocket server thread + loop = asyncio.new_event_loop() + # get logger + logger = init_logger(*config.get_logger_settings()) + # here we'll store all active connections + connections = ClientConnections(logger, loop) + + websock_server = None + try: + logger.info("starting websocket server ...") + # run websocket part of monitor in separate thread + websock_server = WebsocketServer(config.get_websocket_uri(), connections, loop, logger) + websock_server.start() + logger.info("websocket server started") + + # create zeromq connection + logger.info("starting zeromq server ...") + zmq_uri = config.get_zeromq_uri() + zmq_server = ServerConnection(zmq_uri[0], zmq_uri[1], logger) + + # specify callback for zeromq incoming message + def message_callback(client_id, data): + loop.call_soon_threadsafe(connections.send_message, client_id, data) + # start zeromq server with given callback + zmq_server.start(message_callback) + except KeyboardInterrupt: + logger.warning("keyboard interrupt detected") + finally: + logger.warning("quiting...") + loop.call_soon_threadsafe(connections.remove_all_clients) + logger.debug("websocket clients removed") + loop.call_soon_threadsafe(loop.stop) + logger.debug("websocket message loop stopped") + if websock_server: + websock_server.join() + logger.debug("websocket server thread exited") + loop.close() + logger.debug("main thread exited") + +if __name__ == "__main__": + main() diff --git a/monitor/test/__init__.py b/monitor/test/__init__.py index d52fa52..2fa5ea0 100644 --- a/monitor/test/__init__.py +++ b/monitor/test/__init__.py @@ -1,3 +1,3 @@ -#!/usr/bin/env python3 - -__all__ = ['test_ClientConnections', 'test_websock_server'] +#!/usr/bin/env python3 + +__all__ = ['test_ClientConnections', 'test_websock_server'] diff --git a/monitor/test/__main__.py b/monitor/test/__main__.py index a6e50cd..6097ad6 100644 --- a/monitor/test/__main__.py +++ b/monitor/test/__main__.py @@ -1,7 +1,7 @@ -#!/usr/bin/env python3 - -import unittest - -if __name__ == "__main__": - testsuite = unittest.TestLoader().discover('.') - unittest.TextTestRunner(verbosity=1).run(testsuite) +#!/usr/bin/env python3 + +import unittest + +if __name__ == "__main__": + testsuite = unittest.TestLoader().discover('.') + unittest.TextTestRunner(verbosity=1).run(testsuite) diff --git a/monitor/test/test_ClientConnections.py b/monitor/test/test_ClientConnections.py index da37b83..b0d8b1b 100644 --- a/monitor/test/test_ClientConnections.py +++ b/monitor/test/test_ClientConnections.py @@ -1,58 +1,58 @@ -#!/usr/bin/env python3 - -import unittest -import asyncio -from unittest.mock import MagicMock -from monitor.websocket_connections import ClientConnections - - -class TestClientConnections(unittest.TestCase): - def setUp(self): - logger = MagicMock() - loop = MagicMock - self._connections = ClientConnections(logger, loop) - - def test_add_client(self): - queue = self._connections.add_client("1234") - self.assertNotEqual(queue, None, "No queue returned") - self.assertIsInstance(queue, asyncio.Queue, "Wrong type of returned value") - self.assertEqual(queue, self._connections._clients["1234"][0], - "Queue not present at requested id") - queue2 = self._connections.add_client("456777") - self.assertNotEqual(queue, queue2, "Different clients have same future") - self._connections.add_client("1234") - self.assertEqual(len(self._connections._clients["1234"]), 2, "No concurrent clients for same id") - - def test_remove_channel(self): - self._connections.add_client("1234") - self._connections.remove_channel("4567") - self.assertEqual(len(self._connections._clients), 1, "Nonexisting channel removed") - self._connections.remove_channel("1234") - self.assertEqual(len(self._connections._clients), 0, "Existing channel not removed") - - def test_remove_client(self): - queue1 = self._connections.add_client("1234") - queue2 = self._connections.add_client("1234") - self._connections.remove_client("4567", queue1) - self.assertEqual(len(self._connections._clients["1234"]), 2, "Nonexisting client removed") - self._connections.remove_client("1234", queue2) - self.assertEqual(len(self._connections._clients["1234"]), 1, "Existing client not removed") - - def test_remove_all_clients(self): - self._connections.add_client("1234") - self._connections.add_client("4567") - self._connections.remove_all_clients() - self.assertEqual(len(self._connections._clients), 0, "Some clients left in collection") - self._connections.remove_all_clients() - self.assertEqual(len(self._connections._clients), 0, "Some clients left in collection") - - def test_send_message(self): - queue = self._connections.add_client("1234") - self._connections.send_message("1234", "testing message") - self.assertEqual(queue.get_nowait(), "testing message", "Result from queue differs") - self._connections.send_message("1234", "msg two") - self.assertEqual(queue.get_nowait(), "msg two", "Queue has not second message") - - self._connections.send_message("51236", "random string") - queue2 = self._connections.add_client("51236") - self.assertEqual(queue2.get_nowait(), "random string", "Result from queue differs") +#!/usr/bin/env python3 + +import unittest +import asyncio +from unittest.mock import MagicMock +from monitor.websocket_connections import ClientConnections + + +class TestClientConnections(unittest.TestCase): + def setUp(self): + logger = MagicMock() + loop = MagicMock + self._connections = ClientConnections(logger, loop) + + def test_add_client(self): + queue = self._connections.add_client("1234") + self.assertNotEqual(queue, None, "No queue returned") + self.assertIsInstance(queue, asyncio.Queue, "Wrong type of returned value") + self.assertEqual(queue, self._connections._clients["1234"][0], + "Queue not present at requested id") + queue2 = self._connections.add_client("456777") + self.assertNotEqual(queue, queue2, "Different clients have same future") + self._connections.add_client("1234") + self.assertEqual(len(self._connections._clients["1234"]), 2, "No concurrent clients for same id") + + def test_remove_channel(self): + self._connections.add_client("1234") + self._connections.remove_channel("4567") + self.assertEqual(len(self._connections._clients), 1, "Nonexisting channel removed") + self._connections.remove_channel("1234") + self.assertEqual(len(self._connections._clients), 0, "Existing channel not removed") + + def test_remove_client(self): + queue1 = self._connections.add_client("1234") + queue2 = self._connections.add_client("1234") + self._connections.remove_client("4567", queue1) + self.assertEqual(len(self._connections._clients["1234"]), 2, "Nonexisting client removed") + self._connections.remove_client("1234", queue2) + self.assertEqual(len(self._connections._clients["1234"]), 1, "Existing client not removed") + + def test_remove_all_clients(self): + self._connections.add_client("1234") + self._connections.add_client("4567") + self._connections.remove_all_clients() + self.assertEqual(len(self._connections._clients), 0, "Some clients left in collection") + self._connections.remove_all_clients() + self.assertEqual(len(self._connections._clients), 0, "Some clients left in collection") + + def test_send_message(self): + queue = self._connections.add_client("1234") + self._connections.send_message("1234", "testing message") + self.assertEqual(queue.get_nowait(), "testing message", "Result from queue differs") + self._connections.send_message("1234", "msg two") + self.assertEqual(queue.get_nowait(), "msg two", "Queue has not second message") + + self._connections.send_message("51236", "random string") + queue2 = self._connections.add_client("51236") + self.assertEqual(queue2.get_nowait(), "random string", "Result from queue differs") diff --git a/monitor/test/test_ConfigManager.py b/monitor/test/test_ConfigManager.py index 23f9f03..c1a529e 100644 --- a/monitor/test/test_ConfigManager.py +++ b/monitor/test/test_ConfigManager.py @@ -1,93 +1,93 @@ -#!/usr/bin/env python3 - -import tempfile -import unittest -import os -import logging -from monitor.config_manager import ConfigManager, init_logger - - -class TestConfigManager(unittest.TestCase): - def test_websock_uri_default(self): - config = ConfigManager() - self.assertEqual(config.get_websocket_uri(), ["127.0.0.1", 4567]) - - # wrong filename - using defaults - config = ConfigManager('/a/b/tmp/log.bla') - self.assertEqual(config.get_websocket_uri(), ["127.0.0.1", 4567]) - - def test_websock_uri_loaded(self): - handle, path = tempfile.mkstemp() - with open(path, 'w') as f: - f.write('websocket_uri:\n - 77.75.76.3\n - 8080') - - config = ConfigManager(path) - self.assertEqual(config.get_websocket_uri(), ["77.75.76.3", 8080]) - os.remove(path) - - def test_zeromq_uri_default(self): - config = ConfigManager() - self.assertEqual(config.get_zeromq_uri(), ["127.0.0.1", 7894]) - - # wrong filename - using defaults - config = ConfigManager('/a/b/tmp/log.bla') - self.assertEqual(config.get_zeromq_uri(), ["127.0.0.1", 7894]) - - def test_zeromq_uri_loaded(self): - handle, path = tempfile.mkstemp() - with open(path, 'w') as f: - f.write('zeromq_uri:\n - 77.75.76.3\n - 8080') - - config = ConfigManager(path) - self.assertEqual(config.get_zeromq_uri(), ["77.75.76.3", 8080]) - os.remove(path) - - def test_logger_path_default(self): - config = ConfigManager() - self.assertEqual(config.get_logger_settings(), ["/tmp/recodex-monitor.log", logging.INFO, 1024*1024, 3]) - - # wrong filename - using defaults - config = ConfigManager('/a/b/tmp/log.bla') - self.assertEqual(config.get_logger_settings(), ["/tmp/recodex-monitor.log", logging.INFO, 1024*1024, 3]) - - def test_logger_path_loaded(self): - handle, path = tempfile.mkstemp() - with open(path, 'w') as f: - f.write('logger:\n file: /var/log/tmp/file.log\n level: "debug"\n max-size: 564\n rotations: 7') - - config = ConfigManager(path) - self.assertEqual(config.get_logger_settings(), ["/var/log/tmp/file.log", logging.DEBUG, 564, 7]) - os.remove(path) - - def test_loglevel_from_string(self): - config = ConfigManager() - self.assertEqual(config._get_loglevel_from_string("debug"), logging.DEBUG) - self.assertEqual(config._get_loglevel_from_string("info"), logging.INFO) - self.assertEqual(config._get_loglevel_from_string("warning"), logging.WARNING) - self.assertEqual(config._get_loglevel_from_string("error"), logging.ERROR) - self.assertEqual(config._get_loglevel_from_string("critical"), logging.CRITICAL) - self.assertEqual(config._get_loglevel_from_string("nonexisting"), logging.INFO) - - -class TestLoggerInitialization(unittest.TestCase): - def test_invalid_path(self): - logger = init_logger("/a/b/c.txt", logging.INFO, 45, 2) - self.assertFalse(logger.isEnabledFor(logging.CRITICAL)) - # restore global logging - logging.disable(logging.NOTSET) - - def test_valid_creation(self): - handle, path = tempfile.mkstemp() - logger = init_logger(path, logging.WARNING, 450, 2) - logger.debug("aaa") - logger.warning("bbb") - logger.error("ccc") - [h.flush() for h in logger.handlers] - - self.assertEqual(logger.getEffectiveLevel(), logging.WARNING) - with open(path, 'r') as f: - content = f.readlines() - # expect 5 lines - 3 of header and one with 'bbb' and one 'ccc' - self.assertEqual(len(content), 5) - - os.remove(path) +#!/usr/bin/env python3 + +import tempfile +import unittest +import os +import logging +from monitor.config_manager import ConfigManager, init_logger + + +class TestConfigManager(unittest.TestCase): + def test_websock_uri_default(self): + config = ConfigManager() + self.assertEqual(config.get_websocket_uri(), ["127.0.0.1", 4567]) + + # wrong filename - using defaults + config = ConfigManager('/a/b/tmp/log.bla') + self.assertEqual(config.get_websocket_uri(), ["127.0.0.1", 4567]) + + def test_websock_uri_loaded(self): + handle, path = tempfile.mkstemp() + with open(path, 'w') as f: + f.write('websocket_uri:\n - 77.75.76.3\n - 8080') + + config = ConfigManager(path) + self.assertEqual(config.get_websocket_uri(), ["77.75.76.3", 8080]) + os.remove(path) + + def test_zeromq_uri_default(self): + config = ConfigManager() + self.assertEqual(config.get_zeromq_uri(), ["127.0.0.1", 7894]) + + # wrong filename - using defaults + config = ConfigManager('/a/b/tmp/log.bla') + self.assertEqual(config.get_zeromq_uri(), ["127.0.0.1", 7894]) + + def test_zeromq_uri_loaded(self): + handle, path = tempfile.mkstemp() + with open(path, 'w') as f: + f.write('zeromq_uri:\n - 77.75.76.3\n - 8080') + + config = ConfigManager(path) + self.assertEqual(config.get_zeromq_uri(), ["77.75.76.3", 8080]) + os.remove(path) + + def test_logger_path_default(self): + config = ConfigManager() + self.assertEqual(config.get_logger_settings(), ["/tmp/recodex-monitor.log", logging.INFO, 1024*1024, 3]) + + # wrong filename - using defaults + config = ConfigManager('/a/b/tmp/log.bla') + self.assertEqual(config.get_logger_settings(), ["/tmp/recodex-monitor.log", logging.INFO, 1024*1024, 3]) + + def test_logger_path_loaded(self): + handle, path = tempfile.mkstemp() + with open(path, 'w') as f: + f.write('logger:\n file: /var/log/tmp/file.log\n level: "debug"\n max-size: 564\n rotations: 7') + + config = ConfigManager(path) + self.assertEqual(config.get_logger_settings(), ["/var/log/tmp/file.log", logging.DEBUG, 564, 7]) + os.remove(path) + + def test_loglevel_from_string(self): + config = ConfigManager() + self.assertEqual(config._get_loglevel_from_string("debug"), logging.DEBUG) + self.assertEqual(config._get_loglevel_from_string("info"), logging.INFO) + self.assertEqual(config._get_loglevel_from_string("warning"), logging.WARNING) + self.assertEqual(config._get_loglevel_from_string("error"), logging.ERROR) + self.assertEqual(config._get_loglevel_from_string("critical"), logging.CRITICAL) + self.assertEqual(config._get_loglevel_from_string("nonexisting"), logging.INFO) + + +class TestLoggerInitialization(unittest.TestCase): + def test_invalid_path(self): + logger = init_logger("/a/b/c.txt", logging.INFO, 45, 2) + self.assertFalse(logger.isEnabledFor(logging.CRITICAL)) + # restore global logging + logging.disable(logging.NOTSET) + + def test_valid_creation(self): + handle, path = tempfile.mkstemp() + logger = init_logger(path, logging.WARNING, 450, 2) + logger.debug("aaa") + logger.warning("bbb") + logger.error("ccc") + [h.flush() for h in logger.handlers] + + self.assertEqual(logger.getEffectiveLevel(), logging.WARNING) + with open(path, 'r') as f: + content = f.readlines() + # expect 5 lines - 3 of header and one with 'bbb' and one 'ccc' + self.assertEqual(len(content), 5) + + os.remove(path) diff --git a/monitor/test/test_ServerConnection.py b/monitor/test/test_ServerConnection.py index 53a8388..36895d4 100644 --- a/monitor/test/test_ServerConnection.py +++ b/monitor/test/test_ServerConnection.py @@ -1,56 +1,56 @@ -#!/usr/bin/env python3 - -import unittest -import zmq -from unittest.mock import * -from monitor.zeromq_connection import ServerConnection - - -class TestServerConnection(unittest.TestCase): - @patch('zmq.Context') - def test_init(self, mock_context): - mock_socket = MagicMock() - mock_receiver = MagicMock() - logger = MagicMock() - mock_context.return_value = mock_socket - mock_socket.socket.return_value = mock_receiver - - ServerConnection("ip_address", 1025, logger) - mock_context.assert_called_once_with() - mock_socket.socket.assert_called_once_with(zmq.ROUTER) - mock_receiver.setsockopt.assert_called_once_with(zmq.IDENTITY, b"recodex-monitor") - mock_receiver.bind.assert_called_once_with("tcp://ip_address:1025") - - @patch('zmq.Context') - def test_start_normal(self, mock_context): - mock_socket = MagicMock() - mock_receiver = MagicMock() - logger = MagicMock() - mock_context.return_value = mock_socket - mock_socket.socket.return_value = mock_receiver - mock_callback = MagicMock() - - server = ServerConnection("ip_address", 1025, logger) - mock_receiver.recv_multipart.side_effect = [[b"id", b"1234", b"command text", b"task id text", - b"task state text"], [b"id", b"0", b"exit"]] - ret = server.start(mock_callback) - - self.assertTrue(ret) - mock_callback.assert_called_once_with("1234", '{"command": "command text", "task_id": "task id text", ' - '"task_state": "task state text"}') - - @patch('zmq.Context') - def test_start_socket_error(self, mock_context): - mock_socket = MagicMock() - mock_receiver = MagicMock() - logger = MagicMock() - mock_context.return_value = mock_socket - mock_socket.socket.return_value = mock_receiver - mock_callback = MagicMock() - - server = ServerConnection("ip_address", 1025, logger) - mock_receiver.recv_multipart.side_effect = Exception - ret = server.start(mock_callback) - - self.assertFalse(ret) - self.assertFalse(mock_callback.called) +#!/usr/bin/env python3 + +import unittest +import zmq +from unittest.mock import * +from monitor.zeromq_connection import ServerConnection + + +class TestServerConnection(unittest.TestCase): + @patch('zmq.Context') + def test_init(self, mock_context): + mock_socket = MagicMock() + mock_receiver = MagicMock() + logger = MagicMock() + mock_context.return_value = mock_socket + mock_socket.socket.return_value = mock_receiver + + ServerConnection("ip_address", 1025, logger) + mock_context.assert_called_once_with() + mock_socket.socket.assert_called_once_with(zmq.ROUTER) + mock_receiver.setsockopt.assert_called_once_with(zmq.IDENTITY, b"recodex-monitor") + mock_receiver.bind.assert_called_once_with("tcp://ip_address:1025") + + @patch('zmq.Context') + def test_start_normal(self, mock_context): + mock_socket = MagicMock() + mock_receiver = MagicMock() + logger = MagicMock() + mock_context.return_value = mock_socket + mock_socket.socket.return_value = mock_receiver + mock_callback = MagicMock() + + server = ServerConnection("ip_address", 1025, logger) + mock_receiver.recv_multipart.side_effect = [[b"id", b"1234", b"command text", b"task id text", + b"task state text"], [b"id", b"0", b"exit"]] + ret = server.start(mock_callback) + + self.assertTrue(ret) + mock_callback.assert_called_once_with("1234", '{"command": "command text", "task_id": "task id text", ' + '"task_state": "task state text"}') + + @patch('zmq.Context') + def test_start_socket_error(self, mock_context): + mock_socket = MagicMock() + mock_receiver = MagicMock() + logger = MagicMock() + mock_context.return_value = mock_socket + mock_socket.socket.return_value = mock_receiver + mock_callback = MagicMock() + + server = ServerConnection("ip_address", 1025, logger) + mock_receiver.recv_multipart.side_effect = Exception + ret = server.start(mock_callback) + + self.assertFalse(ret) + self.assertFalse(mock_callback.called) diff --git a/monitor/zeromq_connection.py b/monitor/zeromq_connection.py index 264dfe3..896b9b0 100644 --- a/monitor/zeromq_connection.py +++ b/monitor/zeromq_connection.py @@ -1,78 +1,78 @@ -#!/usr/bin/env python3 -""" -Handle zeromq socket. -""" - -import zmq -import json - - -class ServerConnection: - """ - Class responsible for creating zeromq socket (server) and receiving - messages from connected clients. The message should be text with - format ,, where text will be sent to websocket - client subscribed to channel . - """ - def __init__(self, address, port, logger): - """ - Initialize new instance with given address and port. - - :param address: String representation of IP address - to listen to or a hostname. - :param port: String port where to listen. - :param logger: System logger - """ - self._logger = logger - context = zmq.Context() - self._receiver = context.socket(zmq.ROUTER) - self._receiver.setsockopt(zmq.IDENTITY, b"recodex-monitor") - address = "tcp://{}:{}".format(address, port) - self._receiver.bind(address) - self._logger.info("zeromq server initialized at {}".format(address)) - - def start(self, message_callback): - """ - Start receiving messages from underlying zeromq socket. - - :param message_callback: Function to be called when new messages arrived. - This function should not block for long. Required are two parameters, first - is id of stream and second is text of the message. Both are strings. - :return: True if exited normally (by "exit" message with ID 0), False if - socket error occurred. - """ - while True: - # try to receive a message - try: - message = self._receiver.recv_multipart() - self._logger.debug("zeromq server: got message '{}'".format(message)) - except Exception as e: - self._logger.error("zeromq server: socket error: {}".format(e)) - return False - # split given message - try: - """ - decode the message with following parts: - 0 - zeromq identity of sender - 1 - byte array with channel id - 2 - byte array with message command - 3 - byte array with message task_id - only for TASK command - 4 - byte array with message task_state - only for TASK command - """ - decoded_message = [item.decode() for item in message[1:]] - client_id = decoded_message[0] - keys = ["command", "task_id", "task_state"] - data = json.dumps(dict(zip(keys, decoded_message[1:])), sort_keys=True) - except ValueError: - continue - if client_id == "0" and data == '{"command": "exit"}': - self._logger.info("zeromq server: got shutdown command") - break - # call registered callback with given data - message_callback(client_id, data) - - # after last message (command FINISHED) send also poison pill - # to close listening sockets - if decoded_message[1] == "FINISHED": - message_callback(client_id, None) - return True +#!/usr/bin/env python3 +""" +Handle zeromq socket. +""" + +import zmq +import json + + +class ServerConnection: + """ + Class responsible for creating zeromq socket (server) and receiving + messages from connected clients. The message should be text with + format ,, where text will be sent to websocket + client subscribed to channel . + """ + def __init__(self, address, port, logger): + """ + Initialize new instance with given address and port. + + :param address: String representation of IP address + to listen to or a hostname. + :param port: String port where to listen. + :param logger: System logger + """ + self._logger = logger + context = zmq.Context() + self._receiver = context.socket(zmq.ROUTER) + self._receiver.setsockopt(zmq.IDENTITY, b"recodex-monitor") + address = "tcp://{}:{}".format(address, port) + self._receiver.bind(address) + self._logger.info("zeromq server initialized at {}".format(address)) + + def start(self, message_callback): + """ + Start receiving messages from underlying zeromq socket. + + :param message_callback: Function to be called when new messages arrived. + This function should not block for long. Required are two parameters, first + is id of stream and second is text of the message. Both are strings. + :return: True if exited normally (by "exit" message with ID 0), False if + socket error occurred. + """ + while True: + # try to receive a message + try: + message = self._receiver.recv_multipart() + self._logger.debug("zeromq server: got message '{}'".format(message)) + except Exception as e: + self._logger.error("zeromq server: socket error: {}".format(e)) + return False + # split given message + try: + """ + decode the message with following parts: + 0 - zeromq identity of sender + 1 - byte array with channel id + 2 - byte array with message command + 3 - byte array with message task_id - only for TASK command + 4 - byte array with message task_state - only for TASK command + """ + decoded_message = [item.decode() for item in message[1:]] + client_id = decoded_message[0] + keys = ["command", "task_id", "task_state"] + data = json.dumps(dict(zip(keys, decoded_message[1:])), sort_keys=True) + except ValueError: + continue + if client_id == "0" and data == '{"command": "exit"}': + self._logger.info("zeromq server: got shutdown command") + break + # call registered callback with given data + message_callback(client_id, data) + + # after last message (command FINISHED) send also poison pill + # to close listening sockets + if decoded_message[1] == "FINISHED": + message_callback(client_id, None) + return True diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..d376b45 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,48 @@ +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "recodex-monitor" +dynamic = ["version"] +description = "Publish ZeroMQ messages through WebSockets" +readme = "README.rst" +license = { file = "LICENSE" } +authors = [ + {name = "ReCodEx Team"} +] +keywords = ["ReCodEx", "messages", "ZeroMQ", "WebSockets"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] +dependencies = [ + "zmq", + "websockets", + "PyYAML", +] +requires-python = ">=3.9" + +[project.urls] +Homepage = "https://github.com/ReCodEx/monitor" +Repository = "https://github.com/ReCodEx/monitor" +Issues = "https://github.com/ReCodEx/monitor/issues" + +[project.scripts] +recodex-monitor = "monitor.main:main" + +[tool.setuptools.dynamic] +version = {attr = "monitor.__version__"} + +[tool.setuptools.packages.find] +include = ["monitor*"] + +[tool.setuptools.package-data] +"monitor" = ["install/*"] diff --git a/recodex-monitor.spec b/recodex-monitor.spec index 4908b46..4716e08 100644 --- a/recodex-monitor.spec +++ b/recodex-monitor.spec @@ -1,7 +1,7 @@ %define name recodex-monitor %define short_name monitor -%define version 1.1.1 -%define unmangled_version 17032a170de28fd0a45d603f179dfcee89efaba7 +%define version 1.2.0 +%define unmangled_version ca7c8e7d34a0606e9f28db3b4b4942042c85d557 %define release 1 Summary: Publish ZeroMQ messages through WebSockets @@ -17,8 +17,10 @@ Vendor: Petr Stefan Url: https://github.com/ReCodEx/monitor BuildRequires: systemd -%{?fedora:BuildRequires: python3 python3-devel python3-setuptools} -%{?rhel:BuildRequires: python3 python3-devel python3-setuptools} +%{?fedora:BuildRequires: python3 python3-devel python3-setuptools python3-pip python3-wheel} +%{?rhel:BuildRequires: python3 python3-devel python3-setuptools python3-pip python3-wheel} +BuildRequires: python3dist(build) +BuildRequires: python3dist(setuptools) >= 61.0 Requires(post): systemd Requires(preun): systemd Requires(postun): systemd @@ -34,12 +36,25 @@ Monitor is a proxying component that channels zeromq messages into websocket. It %setup -n %{short_name}-%{unmangled_version} %build -%py3_build +# Build using modern Python build system with pyproject.toml +# This replaces the legacy setup.py build process +%{python3} -m build --wheel --no-isolation + %install -%py3_install +# Install the wheel using pip (modern approach) +%{python3} -m pip install --no-deps --no-index --find-links dist/ --root=%{buildroot} recodex-monitor + +# Create log directory mkdir -p %{buildroot}/var/log/recodex +# Install system files manually (these files are no longer installed automatically with pyproject.toml) +mkdir -p %{buildroot}/lib/systemd/system +mkdir -p %{buildroot}%{_sysconfdir}/recodex/monitor +install -m 644 monitor/install/recodex-monitor.service %{buildroot}/lib/systemd/system/ +install -m 644 monitor/install/config.yml %{buildroot}%{_sysconfdir}/recodex/monitor/ + + %clean rm -rf $RPM_BUILD_ROOT @@ -66,7 +81,7 @@ exit 0 %dir %attr(-,recodex,recodex) /var/log/recodex %{python3_sitelib}/monitor/ -%{python3_sitelib}/recodex_monitor-%{version}-py?.?.egg-info/ +%{python3_sitelib}/recodex_monitor-%{version}.dist-info/ %{_bindir}/recodex-monitor %config(noreplace) %attr(0600,recodex,recodex) %{_sysconfdir}/recodex/monitor/config.yml /lib/systemd/system/recodex-monitor.service diff --git a/requirements.txt b/requirements.txt index 5bc91fc..f34eea3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,15 +1,13 @@ -# -# Required packages for ReCodEx monitor tool -# - -# ZeroMQ -zmq - -# WebSockets -websockets -asyncio - -# Argument and configuration parsing -pyyaml -argparse - +# +# Required packages for ReCodEx monitor tool +# + +# ZeroMQ +zmq + +# WebSockets +websockets + +# Configuration parsing +PyYAML + diff --git a/setup.py b/setup.py deleted file mode 100644 index 5547452..0000000 --- a/setup.py +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env python3 - -from setuptools import setup -from monitor import __version__ - - -setup(name='recodex-monitor', - version=__version__, - description='Publish ZeroMQ messages through WebSockets', - author='Petr Stefan', - author_email='', - url='https://github.com/ReCodEx/monitor', - license="MIT", - keywords=['ReCodEx', 'messages', 'ZeroMQ', 'WebSockets'], - classifiers=["Development Status :: 5 - Production/Stable", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - 'Operating System :: POSIX :: Linux', - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5"], - packages=['monitor'], - package_data={'': ['./install/*']}, - data_files=[ - ('/lib/systemd/system', ['monitor/install/recodex-monitor.service']), - ('/etc/recodex/monitor', ['monitor/install/config.yml']) - ], - entry_points={'console_scripts': ['recodex-monitor = monitor.main:main']} - ) diff --git a/testing/index.html b/testing/index.html index 8555fb8..ad39397 100644 --- a/testing/index.html +++ b/testing/index.html @@ -1,30 +1,30 @@ - - - - Monitor WebSocket demo - - - - - - - + + + + Monitor WebSocket demo + + + + + + + diff --git a/testing/submitter.py b/testing/submitter.py index 003f6b8..9b031a1 100755 --- a/testing/submitter.py +++ b/testing/submitter.py @@ -1,30 +1,30 @@ -#!/usr/bin/env python3 -""" -Testing tool for sending messages to zeromq connection with monitor. -""" - -import zmq - - -context = zmq.Context() -zmq_socket = context.socket(zmq.ROUTER) -zmq_socket.connect("tcp://127.0.0.1:7894") - -print("Write messages with format ,") -try: - while True: - message = input("> ") - id, msg = message.split(',') - """ - Message has following format: - - identity of monitor socket - - id of target channel as byte array - - text of the message as byte array - """ - zmq_socket.send_multipart([b"recodex-monitor", id.encode(), msg.encode()]) - if message == "0,exit": - break -except KeyboardInterrupt: - pass -finally: - print("Quitting...") +#!/usr/bin/env python3 +""" +Testing tool for sending messages to zeromq connection with monitor. +""" + +import zmq + + +context = zmq.Context() +zmq_socket = context.socket(zmq.ROUTER) +zmq_socket.connect("tcp://127.0.0.1:7894") + +print("Write messages with format ,") +try: + while True: + message = input("> ") + id, msg = message.split(',') + """ + Message has following format: + - identity of monitor socket + - id of target channel as byte array + - text of the message as byte array + """ + zmq_socket.send_multipart([b"recodex-monitor", id.encode(), msg.encode()]) + if message == "0,exit": + break +except KeyboardInterrupt: + pass +finally: + print("Quitting...")