diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..e0d22a1 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,28 @@ +# .coveragerc to control coverage.py +[run] +branch = True +source = meshctrl +# omit = bad_file.py + +[paths] +source = + src/ + */site-packages/ + +[report] +# Regexes for lines to exclude from consideration +exclude_lines = + # Have to re-enable the standard pragma + pragma: no cover + + # Don't complain about missing debug-only code: + def __repr__ + if self\.debug + + # Don't complain if tests don't hit defensive assertion code: + raise AssertionError + raise NotImplementedError + + # Don't complain if non-runnable code isn't run: + if 0: + if __name__ == .__main__.: diff --git a/.gitignore b/.gitignore index 82f9275..e9e1e9b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,162 +1,54 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ +# Temporary and binary files +*~ *.py[cod] -*$py.class - -# C extensions *.so +*.cfg +!.isort.cfg +!setup.cfg +*.orig +*.log +*.pot +__pycache__/* +.cache/* +.*.swp +*/.ipynb_checkpoints/* +.DS_Store -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg +# Project files +.ropeproject +.project +.pydevproject +.settings +.idea +.vscode +tags + +# Package files *.egg -MANIFEST +*.eggs/ +.installed.cfg +*.egg-info -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ +# Unittest and coverage +htmlcov/* .coverage .coverage.* -.cache -nosetests.xml +.tox +junit*.xml coverage.xml -*.cover -*.py,cover -.hypothesis/ .pytest_cache/ -cover/ -# Translations -*.mo -*.pot +# Build and docs folder/files +build/* +dist/* +sdist/* +docs/api/* +docs/_rst/* +docs/_build/* +cover/* +MANIFEST -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/latest/usage/project/#working-with-version-control -.pdm.toml -.pdm-python -.pdm-build/ - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ +# Per-project virtualenvs +.venv*/ +.conda*/ +.python-version diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 0000000..a2bcab3 --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,27 @@ +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Build documentation in the docs/ directory with Sphinx +sphinx: + configuration: docs/conf.py + +# Build documentation with MkDocs +#mkdocs: +# configuration: mkdocs.yml + +# Optionally build your docs in additional formats such as PDF +formats: + - pdf + +build: + os: ubuntu-22.04 + tools: + python: "3.11" + +python: + install: + - requirements: docs/requirements.txt + - {path: ., method: pip} diff --git a/AUTHORS.rst b/AUTHORS.rst new file mode 100644 index 0000000..67ce357 --- /dev/null +++ b/AUTHORS.rst @@ -0,0 +1,5 @@ +============ +Contributors +============ + +* Josiah Baldwin diff --git a/CHANGELOG.rst b/CHANGELOG.rst new file mode 100644 index 0000000..28e2786 --- /dev/null +++ b/CHANGELOG.rst @@ -0,0 +1,8 @@ +========= +Changelog +========= + +Version 0.1 +=========== + +Create diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 0000000..61aa917 --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,244 @@ +============ +Contributing +============ + +Welcome to ``meshctrl`` contributor's guide. + +This document focuses on getting any potential contributor familiarized +with the development processes, but `other kinds of contributions`_ are also +appreciated. + +If you are new to using git_ or have never collaborated in a project previously, +please have a look at `contribution-guide.org`_. Other resources are also +listed in the excellent `guide created by FreeCodeCamp`_. + +Please notice, all users and contributors are expected to be **open, +considerate, reasonable, and respectful**. When in doubt, `Python Software +Foundation's Code of Conduct`_ is a good reference in terms of behavior +guidelines. + + +Issue Reports +============= + +If you experience bugs or general issues with ``meshctrl``, please have a look +on the `issue tracker`_. If you don't see anything useful there, please feel +free to fire an issue report. + +.. tip:: + Please don't forget to include the closed issues in your search. + Sometimes a solution was already reported, and the problem is considered + **solved**. + +New issue reports should include information about your programming environment +(e.g., operating system, Python version) and steps to reproduce the problem. +Please try also to simplify the reproduction steps to a very minimal example +that still illustrates the problem you are facing. By removing other factors, +you help us to identify the root cause of the issue. + + +Documentation Improvements +========================== + +You can help improve ``meshctrl`` docs by making them more readable and coherent, or +by adding missing information and correcting mistakes. + +``meshctrl`` documentation uses Sphinx_ as its main documentation compiler. +This means that the docs are kept in the same repository as the project code, and +that any documentation update is done in the same way was a code contribution. + + .. tip:: + Please notice that the `GitHub web interface`_ provides a quick way of + propose changes in ``meshctrl``'s files. While this mechanism can + be tricky for normal code contributions, it works perfectly fine for + contributing to the docs, and can be quite handy. + + If you are interested in trying this method out, please navigate to + the ``docs`` folder in the source repository_, find which file you + would like to propose changes and click in the little pencil icon at the + top, to open `GitHub's code editor`_. Once you finish editing the file, + please write a message in the form at the bottom of the page describing + which changes have you made and what are the motivations behind them and + submit your proposal. + +When working on documentation changes in your local machine, you can +compile them using |tox|_:: + + tox -e docs + +and use Python's built-in web server for a preview in your web browser +(``http://localhost:8000``):: + + python3 -m http.server --directory 'docs/_build/html' + + +.. Code Contributions +.. ================== + + +.. An architecture description, design principles or at least a summary of the +.. main concepts will make it easy for potential contributors to get started +.. quickly. + +Submit an issue +--------------- + +Before you work on any non-trivial code contribution it's best to first create +a report in the `issue tracker`_ to start a discussion on the subject. +This often provides additional considerations and avoids unnecessary work. + +Create an environment +--------------------- + +Before you start coding, we recommend creating an isolated `virtual +environment`_ to avoid any problems with your installed Python packages. +This can easily be done via |virtualenv|_:: + + python -m venv + source /bin/activate + +Clone the repository +-------------------- + +#. Create an user account on |the repository service| if you do not already have one. +#. Fork the project repository_: click on the *Fork* button near the top of the + page. This creates a copy of the code under your account on |the repository service|. +#. Clone this copy to your local disk:: + + git clone git@github.com:YourLogin/meshctrl.git + cd meshctrl + +#. You should run:: + + pip install -U pip setuptools -e . + + to be able to import the package under development in the Python REPL. + +Implement your changes +---------------------- + +#. Create a branch to hold your changes:: + + git checkout -b my-feature + + and start making changes. Never work on the main branch! + +#. Start your work on this branch. Don't forget to add docstrings_ to new + functions, modules and classes, especially if they are part of public APIs. + +#. Add yourself to the list of contributors in ``AUTHORS.rst``. + +#. When you’re done editing, do:: + + git add + git commit + + to record your changes in git_. + + +Submit your contribution +------------------------ + +#. If everything works fine, push your local branch to |the repository service| with:: + + git push -u origin my-feature + +#. Go to the web page of your fork and click |contribute button| + to send your changes for review. + + Find more detailed information in `creating a PR`_. You might also want to open + the PR as a draft first and mark it as ready for review after the feedbacks + from the continuous integration (CI) system or any required fixes. + + +Troubleshooting +--------------- + +The following tips can be used when facing problems to build or test the +package: + +#. Make sure to fetch all the tags from the upstream repository_. + The command ``git describe --abbrev=0 --tags`` should return the version you + are expecting. If you are trying to run CI scripts in a fork repository, + make sure to push all the tags. + You can also try to remove all the egg files or the complete egg folder, i.e., + ``.eggs``, as well as the ``*.egg-info`` folders in the ``src`` folder or + potentially in the root of your project. + +#. Sometimes |tox|_ misses out when new dependencies are added, especially to + ``setup.cfg`` and ``docs/requirements.txt``. If you find any problems with + missing dependencies when running a command with |tox|_, try to recreate the + ``tox`` environment using the ``-r`` flag. For example, instead of:: + + tox -e docs + + Try running:: + + tox -r -e docs + +#. Make sure to have a reliable |tox|_ installation that uses the correct + Python version (e.g., 3.7+). When in doubt you can run:: + + tox --version + # OR + which tox + + If you have trouble and are seeing weird errors upon running |tox|_, you can + also try to create a dedicated `virtual environment`_ with a |tox|_ binary + freshly installed. For example:: + + virtualenv .venv + source .venv/bin/activate + .venv/bin/pip install tox + .venv/bin/tox -e all + +#. `Pytest can drop you`_ in an interactive session in the case an error occurs. + In order to do that you need to pass a ``--pdb`` option (for example by + running ``tox -- -k --pdb``). + You can also setup breakpoints manually instead of using the ``--pdb`` option. + + + + +.. <-- start --> + +.. |the repository service| replace:: GitHub +.. |contribute button| replace:: "Create pull request" + +.. _repository: https://github.com/HuFlungDu/meshctrl +.. _issue tracker: https://github.com/HuFlungDu/meshctrl/issues +.. <-- end --> + + +.. |virtualenv| replace:: ``virtualenv`` +.. |pre-commit| replace:: ``pre-commit`` +.. |tox| replace:: ``tox`` + + +.. _black: https://pypi.org/project/black/ +.. _CommonMark: https://commonmark.org/ +.. _contribution-guide.org: https://www.contribution-guide.org/ +.. _creating a PR: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request +.. _descriptive commit message: https://chris.beams.io/posts/git-commit +.. _docstrings: https://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html +.. _first-contributions tutorial: https://github.com/firstcontributions/first-contributions +.. _flake8: https://flake8.pycqa.org/en/stable/ +.. _git: https://git-scm.com +.. _GitHub's fork and pull request workflow: https://guides.github.com/activities/forking/ +.. _guide created by FreeCodeCamp: https://github.com/FreeCodeCamp/how-to-contribute-to-open-source +.. _Miniconda: https://docs.conda.io/en/latest/miniconda.html +.. _MyST: https://myst-parser.readthedocs.io/en/latest/syntax/syntax.html +.. _other kinds of contributions: https://opensource.guide/how-to-contribute +.. _pre-commit: https://pre-commit.com/ +.. _PyPI: https://pypi.org/ +.. _PyScaffold's contributor's guide: https://pyscaffold.org/en/stable/contributing.html +.. _Pytest can drop you: https://docs.pytest.org/en/stable/how-to/failures.html#using-python-library-pdb-with-pytest +.. _Python Software Foundation's Code of Conduct: https://www.python.org/psf/conduct/ +.. _reStructuredText: https://www.sphinx-doc.org/en/master/usage/restructuredtext/ +.. _Sphinx: https://www.sphinx-doc.org/en/master/ +.. _tox: https://tox.wiki/en/stable/ +.. _virtual environment: https://realpython.com/python-virtual-environments-a-primer/ +.. _virtualenv: https://docs.python.org/3/library/venv.html + +.. _GitHub web interface: https://docs.github.com/en/repositories/working-with-files/managing-files/editing-files +.. _GitHub's code editor: https://docs.github.com/en/repositories/working-with-files/managing-files/editing-files diff --git a/LICENSE b/LICENSE.txt similarity index 97% rename from LICENSE rename to LICENSE.txt index 3c466cf..25d826a 100644 --- a/LICENSE +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -MIT License +The MIT License (MIT) Copyright (c) 2024 Josiah Baldwin diff --git a/README.md b/README.md index 225d812..9acaf94 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ -# pylibmeshctrl +# meshctrl Libmeshctrl implementation in python diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..4e613d8 --- /dev/null +++ b/README.rst @@ -0,0 +1,49 @@ +.. These are examples of badges you might want to add to your README: + please update the URLs accordingly + + .. image:: https://api.cirrus-ci.com/github//pylibmeshctrl.svg?branch=main + :alt: Built Status + :target: https://cirrus-ci.com/github//pylibmeshctrl + .. image:: https://readthedocs.org/projects/pylibmeshctrl/badge/?version=latest + :alt: ReadTheDocs + :target: https://pylibmeshctrl.readthedocs.io/en/stable/ + .. image:: https://img.shields.io/coveralls/github//pylibmeshctrl/main.svg + :alt: Coveralls + :target: https://coveralls.io/r//pylibmeshctrl + .. image:: https://img.shields.io/pypi/v/pylibmeshctrl.svg + :alt: PyPI-Server + :target: https://pypi.org/project/pylibmeshctrl/ + .. image:: https://img.shields.io/conda/vn/conda-forge/pylibmeshctrl.svg + :alt: Conda-Forge + :target: https://anaconda.org/conda-forge/pylibmeshctrl + .. image:: https://pepy.tech/badge/pylibmeshctrl/month + :alt: Monthly Downloads + :target: https://pepy.tech/project/pylibmeshctrl + .. image:: https://img.shields.io/twitter/url/http/shields.io.svg?style=social&label=Twitter + :alt: Twitter + :target: https://twitter.com/pylibmeshctrl + +.. image:: https://img.shields.io/badge/-PyScaffold-005CA0?logo=pyscaffold + :alt: Project generated with PyScaffold + :target: https://pyscaffold.org/ + +| + +============= +meshctrl +============= + + + Libmeshctrl implementation in python + + +This is a library for interacting with a Mesh Central instance programatically. Written in python. + + +.. _pyscaffold-notes: + +Note +==== + +This project has been set up using PyScaffold 4.6. For details and usage +information on PyScaffold see https://pyscaffold.org/. diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..31655dd --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,29 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build +AUTODOCDIR = api + +# 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 https://sphinx-doc.org/") +endif + +.PHONY: help clean Makefile + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +clean: + rm -rf $(BUILDDIR)/* $(AUTODOCDIR) + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/_static/.gitignore b/docs/_static/.gitignore new file mode 100644 index 0000000..3c96363 --- /dev/null +++ b/docs/_static/.gitignore @@ -0,0 +1 @@ +# Empty directory diff --git a/docs/authors.rst b/docs/authors.rst new file mode 100644 index 0000000..cd8e091 --- /dev/null +++ b/docs/authors.rst @@ -0,0 +1,2 @@ +.. _authors: +.. include:: ../AUTHORS.rst diff --git a/docs/changelog.rst b/docs/changelog.rst new file mode 100644 index 0000000..871950d --- /dev/null +++ b/docs/changelog.rst @@ -0,0 +1,2 @@ +.. _changes: +.. include:: ../CHANGELOG.rst diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..7c7975d --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,287 @@ +# This file is execfile()d with the current directory set to its containing dir. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import os +import sys +import shutil + +# -- Path setup -------------------------------------------------------------- + +__location__ = os.path.dirname(__file__) + +# 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.join(__location__, "../src")) + +# -- Run sphinx-apidoc ------------------------------------------------------- +# This hack is necessary since RTD does not issue `sphinx-apidoc` before running +# `sphinx-build -b html . _build/html`. See Issue: +# https://github.com/readthedocs/readthedocs.org/issues/1139 +# DON'T FORGET: Check the box "Install your project inside a virtualenv using +# setup.py install" in the RTD Advanced Settings. +# Additionally it helps us to avoid running apidoc manually + +try: # for Sphinx >= 1.7 + from sphinx.ext import apidoc +except ImportError: + from sphinx import apidoc + +output_dir = os.path.join(__location__, "api") +module_dir = os.path.join(__location__, "../src/meshctrl") +try: + shutil.rmtree(output_dir) +except FileNotFoundError: + pass + +try: + import sphinx + + cmd_line = f"sphinx-apidoc --implicit-namespaces -f -o {output_dir} {module_dir}" + + args = cmd_line.split(" ") + if tuple(sphinx.__version__.split(".")) >= ("1", "7"): + # This is a rudimentary parse_version to avoid external dependencies + args = args[1:] + + apidoc.main(args) +except Exception as e: + print("Running `sphinx-apidoc` failed!\n{}".format(e)) + +# -- 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.intersphinx", + "sphinx.ext.todo", + "sphinx.ext.autosummary", + "sphinx.ext.viewcode", + "sphinx.ext.coverage", + "sphinx.ext.doctest", + "sphinx.ext.ifconfig", + "sphinx.ext.mathjax", + "sphinx.ext.napoleon", + "enum_tools.autoenum", +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# The suffix of source filenames. +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 = "meshctrl" +copyright = "2024, Josiah Baldwin" + +# 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. +# +# version: The short X.Y version. +# release: The full version, including alpha/beta/rc tags. +# If you don’t need the separation provided between version and release, +# just set them both to the same value. +try: + from meshctrl import __version__ as version +except ImportError: + version = "" + +if not version or version.lower() == "unknown": + version = os.getenv("READTHEDOCS_VERSION", "unknown") # automatically set by RTD + +release = version + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# 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. +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", ".venv"] + +# 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 this is True, todo emits a warning for each TODO entries. The default is False. +todo_emit_warnings = 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 = { + "sidebar_width": "300px", + "page_width": "1200px" +} + +# Add any paths that contain custom themes here, relative to this directory. +# html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +# html_title = None + +# 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 = "" + +# The name of an image file (within the static path) to use as 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"] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +# html_last_updated_fmt = '%b %d, %Y' + +# 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 + +# Output file base name for HTML help builder. +htmlhelp_basename = "meshctrl-doc" + + +# -- 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": "", +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass [howto/manual]). +latex_documents = [ + ("index", "user_guide.tex", "meshctrl Documentation", "Josiah Baldwin", "manual") +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +# latex_logo = "" + +# 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 + +# -- External mapping -------------------------------------------------------- +python_version = ".".join(map(str, sys.version_info[0:2])) +intersphinx_mapping = { + "sphinx": ("https://www.sphinx-doc.org/en/master", None), + "python": ("https://docs.python.org/" + python_version, None), + "matplotlib": ("https://matplotlib.org", None), + "numpy": ("https://numpy.org/doc/stable", None), + "sklearn": ("https://scikit-learn.org/stable", None), + "pandas": ("https://pandas.pydata.org/pandas-docs/stable", None), + "scipy": ("https://docs.scipy.org/doc/scipy/reference", None), + "setuptools": ("https://setuptools.pypa.io/en/stable/", None), + "pyscaffold": ("https://pyscaffold.org/en/stable", None), +} + +print(f"loading configurations for {project} {version} ...", file=sys.stderr) diff --git a/docs/contributing.rst b/docs/contributing.rst new file mode 100644 index 0000000..e582053 --- /dev/null +++ b/docs/contributing.rst @@ -0,0 +1 @@ +.. include:: ../CONTRIBUTING.rst diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..51bf4ae --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,26 @@ +============= +meshctrl +============= + +This is the documentation of **meshctrl**. + +Contents +======== + +.. toctree:: + :maxdepth: 2 + + Overview + Contributions & Help + License + Authors + Changelog + Module Reference + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/license.rst b/docs/license.rst new file mode 100644 index 0000000..3989c51 --- /dev/null +++ b/docs/license.rst @@ -0,0 +1,7 @@ +.. _license: + +======= +License +======= + +.. include:: ../LICENSE.txt diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..dc1312a --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/readme.rst b/docs/readme.rst new file mode 100644 index 0000000..81995ef --- /dev/null +++ b/docs/readme.rst @@ -0,0 +1,2 @@ +.. _readme: +.. include:: ../README.rst diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..c31abb7 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,12 @@ +# Requirements file for ReadTheDocs, check .readthedocs.yml. +# To build the module reference correctly, make sure every external package +# under `install_requires` in `setup.cfg` is also listed here! +sphinx>=3.2.1 +sphinx-jinja2-compat>=0.1.1 +sphinx-toolbox>=2.16.0 +# sphinx_rtd_theme +cffi==1.17.1 +cryptography==43.0.3 +pycparser==2.22 +websockets==13.1 +enum_tools \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..89a5bed --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,9 @@ +[build-system] +# AVOID CHANGING REQUIRES: IT WILL BE UPDATED BY PYSCAFFOLD! +requires = ["setuptools>=46.1.0", "setuptools_scm[toml]>=5"] +build-backend = "setuptools.build_meta" + +[tool.setuptools_scm] +# For smarter version schemes and other configuration options, +# check out https://github.com/pypa/setuptools_scm +version_scheme = "no-guess-dev" diff --git a/requirements.txt b/requirements.txt index c53dbea..5bf1b72 100644 Binary files a/requirements.txt and b/requirements.txt differ diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..a464900 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,125 @@ +# This file is used to configure your project. +# Read more about the various options under: +# https://setuptools.pypa.io/en/latest/userguide/declarative_config.html +# https://setuptools.pypa.io/en/latest/references/keywords.html + +[metadata] +name = meshctrl +description = Add a short description here! +author = Josiah Baldwin +author_email = jbaldwin8889@gmail.com +license = MIT +license_files = LICENSE.txt +long_description = file: README.rst +long_description_content_type = text/x-rst; charset=UTF-8 +url = https://github.com/pyscaffold/pyscaffold/ +# Add here related links, for example: +project_urls = + Documentation = https://pyscaffold.org/ +# Source = https://github.com/pyscaffold/pyscaffold/ +# Changelog = https://pyscaffold.org/en/latest/changelog.html +# Tracker = https://github.com/pyscaffold/pyscaffold/issues +# Conda-Forge = https://anaconda.org/conda-forge/pyscaffold +# Download = https://pypi.org/project/PyScaffold/#files +# Twitter = https://twitter.com/PyScaffold + +# Change if running only on Windows, Mac or Linux (comma-separated) +platforms = any + +# Add here all kinds of additional classifiers as defined under +# https://pypi.org/classifiers/ +classifiers = + Development Status :: 4 - Beta + Programming Language :: Python + + +[options] +zip_safe = False +packages = find_namespace: +include_package_data = True +package_dir = + =src + +# Require a min/specific Python version (comma-separated conditions) +# python_requires = >=3.8 + +# Add here dependencies of your project (line-separated), e.g. requests>=2.2,<3.0. +# Version specifiers like >=2.2,<3.0 avoid problems due to API changes in +# new major versions. This works if the required packages follow Semantic Versioning. +# For more information, check out https://semver.org/. +install_requires = + importlib-metadata; python_version<"3.8" + + +[options.packages.find] +where = src +exclude = + tests + +[options.extras_require] +# Add here additional requirements for extra features, to install with: +# `pip install meshctrl[PDF]` like: +# PDF = ReportLab; RXP + +# Add here test requirements (semicolon/line-separated) +testing = + setuptools + pytest + pytest-cov + +[options.entry_points] +# Add here console scripts like: +# console_scripts = +# script_name = meshctrl.module:function +# For example: +# console_scripts = +# fibonacci = meshctrl.skeleton:run +# And any other entry points, for example: +# pyscaffold.cli = +# awesome = pyscaffoldext.awesome.extension:AwesomeExtension + +[tool:pytest] +# Specify command line options as you would do when invoking pytest directly. +# e.g. --cov-report html (or xml) for html/xml output or --junitxml junit.xml +# in order to write a coverage file that can be read by Jenkins. +# CAUTION: --cov flags may prohibit setting breakpoints while debugging. +# Comment those flags to avoid this pytest issue. +addopts = + --cov meshctrl --cov-report term-missing + --verbose +norecursedirs = + dist + build + .tox +testpaths = tests +# Use pytest markers to select/deselect specific tests +# markers = +# slow: mark tests as slow (deselect with '-m "not slow"') +# system: mark end-to-end system tests + +[devpi:upload] +# Options for the devpi: PyPI server and packaging tool +# VCS export must be deactivated since we are using setuptools-scm +no_vcs = 1 +formats = bdist_wheel + +[flake8] +# Some sane defaults for the code style checker flake8 +max_line_length = 88 +extend_ignore = E203, W503 +# ^ Black-compatible +# E203 and W503 have edge cases handled by black +exclude = + .tox + build + dist + .eggs + docs/conf.py + +[pyscaffold] +# PyScaffold's parameters when the project was created. +# This will be used when updating. Do not change! +version = 4.6 +package = meshctrl +extensions = + no_skeleton diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..a4446a6 --- /dev/null +++ b/setup.py @@ -0,0 +1,22 @@ +""" + Setup file for meshctrl. + Use setup.cfg to configure your project. + + This file was generated with PyScaffold 4.6. + PyScaffold helps you to put up the scaffold of your new Python project. + Learn more under: https://pyscaffold.org/ +""" + +from setuptools import setup + +if __name__ == "__main__": + try: + setup(use_scm_version={"version_scheme": "no-guess-dev"}) + except: # noqa + print( + "\n\nAn error occurred while building the project, " + "please ensure you have the most updated version of setuptools, " + "setuptools_scm and wheel with:\n" + " pip install -U setuptools setuptools_scm wheel\n\n" + ) + raise diff --git a/src/meshctrl/__init__.py b/src/meshctrl/__init__.py index 3801f16..4fb81a3 100644 --- a/src/meshctrl/__init__.py +++ b/src/meshctrl/__init__.py @@ -1 +1,16 @@ -from .session import Session \ No newline at end of file +import sys + +if sys.version_info[:2] >= (3, 8): + # TODO: Import directly (no need for conditional) when `python_requires = >= 3.8` + from importlib.metadata import PackageNotFoundError, version # pragma: no cover +else: + from importlib_metadata import PackageNotFoundError, version # pragma: no cover + +try: + # Change here if project is renamed and does not equal the package name + dist_name = "meshctrl" + __version__ = version(dist_name) +except PackageNotFoundError: # pragma: no cover + __version__ = "unknown" +finally: + del version, PackageNotFoundError diff --git a/src/meshctrl/constants.py b/src/meshctrl/constants.py index 6ef136c..825ff4d 100644 --- a/src/meshctrl/constants.py +++ b/src/meshctrl/constants.py @@ -1,45 +1,87 @@ import enum +try: + from enum_tools.documentation import document_enum +except: + def document_enum(cls, *args, **kwargs): + return cls +@document_enum class UserRights(enum.IntFlag): + """ + Bitwise flags for user rights + + """ + + #: Give user no rights norights = 0 - backup = enum.auto() + #: Allow backup of mesh database + backup = enum.auto() + #: User can add or remove users manageusers = enum.auto() + #: User can restore the database from a backup restore = enum.auto() + #: User can upload files to server storage fileaccess = enum.auto() + #: User can update server version update = enum.auto() + #: User is disabled locked = enum.auto() - nonewgroups = enum.auto() - notools = enum.auto() - usergroups = enum.auto() + #: User cannot create new meshes + nonewgroups = enum.auto() # + notools = enum.auto() # + #: User can create user groups + usergroups = enum.auto() # + #: User can record desktop sessions recordings = enum.auto() locksettings = enum.auto() - fullrights = backup|manageusers|restore|fileaccess|\ - update|locked|nonewgroups|notools|usergroups|\ - recordings|locksettings + #: User has full rights + fullrights = backup|manageusers|restore|fileaccess|update|locked|nonewgroups|notools|usergroups|recordings|locksettings +@document_enum class MeshRights(enum.IntFlag): + """ + Bitwise flags for mesh rights + Pulls double duty as rights for a connected device + """ + #: Give user no rights norights = 0 + #: Edit the group editgroup = enum.auto() + #: Add/remove users manageusers = enum.auto() + #: Add/remove devices managedevices = enum.auto() + #: Remote control access remotecontrol = enum.auto() + #: Agent console access agentconsole = enum.auto() serverfiles = enum.auto() + #: Wake device from sleep wakedevices = enum.auto() + #: Add notes to the device/mesh notes = enum.auto() + #: Only view the desktop; no control desktopviewonly = enum.auto() + #: No terminal access noterminal = enum.auto() + #: No file access nofiles = enum.auto() + #: No AMT access noamt = enum.auto() limiteddesktop = enum.auto() limitedevents = enum.auto() chatnotify = enum.auto() uninstall = enum.auto() + #: Disable remote desktop noremotedesktop = enum.auto() + #: Allow to send commands to the device remotecommands = enum.auto() + #: Reset or poweroff device resetpoweroff = enum.auto() + #: All rights fullrights = 0xFFFFFFFF +@document_enum class ConsentFlags(enum.IntFlag): none = 0 desktopnotify = enum.auto() @@ -49,9 +91,9 @@ class ConsentFlags(enum.IntFlag): terminalprompt = enum.auto() filesprompt = enum.auto() desktopprivacybar = enum.auto() - all = desktopnotify|terminalnotify|filesnotify|desktopprompt|terminalprompt|\ - filesprompt|filesprompt + all = desktopnotify|terminalnotify|filesnotify|desktopprompt|terminalprompt|filesprompt|filesprompt +@document_enum class MeshFeatures(enum.IntFlag): none = 0 autoremove = enum.auto() @@ -59,15 +101,27 @@ class MeshFeatures(enum.IntFlag): recordsessions = enum.auto() all = autoremove|hostnamesync|recordsessions +@document_enum class SharingType(enum.StrEnum): + """ + String constants used to determine which type of device share to create + """ desktop = enum.auto() terminal = enum.auto() +@document_enum class SharingTypeInt(enum.IntEnum): + """ + Internal enum used to map SHARINGTYPE to the number used by MeshCentral + """ desktop = enum.auto() terminal = enum.auto() +@document_enum class Icon(enum.IntEnum): + """ + Which icon to use for a device + """ desktop = enum.auto() laptop = enum.auto() phone = enum.auto() diff --git a/src/meshctrl/exceptions.py b/src/meshctrl/exceptions.py index aa53242..ba2aa97 100644 --- a/src/meshctrl/exceptions.py +++ b/src/meshctrl/exceptions.py @@ -1,21 +1,17 @@ class MeshCtrlError(Exception): + """ + Base class for Meshctrl errors + """ pass -# /** Represents an error thrown from the server -# * @extends Error -# */ class ServerError(MeshCtrlError): + """ + Represents an error thrown from the server + """ pass -# /** Represents an error in the websocket -# * @extends Error -# */ - class SocketError(MeshCtrlError): - pass - -# /** Represents that a command timed out -# * @extends Error -# */ -class TimeoutError(MeshCtrlError): - pass + """ + Represents an error in the websocket + """ + pass \ No newline at end of file diff --git a/src/meshctrl/files.py b/src/meshctrl/files.py new file mode 100644 index 0000000..53ef6d9 --- /dev/null +++ b/src/meshctrl/files.py @@ -0,0 +1,4 @@ +from . import tunnel + +class Files(tunnel.Tunnel): + pass \ No newline at end of file diff --git a/src/meshctrl/session.py b/src/meshctrl/session.py index 24e499d..3f0fecc 100644 --- a/src/meshctrl/session.py +++ b/src/meshctrl/session.py @@ -18,7 +18,30 @@ def _check_socket(f): return wrapper class Session(object): - def __init__(self, url, user=None, domain=None, password=None, loginkey=None, proxy=None, token=None, ignoreSSL=False, auto_reconnect=False): + + ''' + Class for MeshCentral control session + + Args: + url (str): URL of meshcentral server to connect to. Should start with either "ws://" or "wss://". + user (str): Username of to use for connecting. Can also be username generated from token. + domain (str): Domain to connect to + password (str): Password with which to connect. Can also be password generated from token. + loginkey (str|bytes): Key from already handled login. Overrides username/password. + proxy (str): "url:port" to use for proxy server + token (str): Login token. This appears to be superfluous + ignore_ssl (bool): Ignore SSL errors + + Returns: + :py:class:`Session`: Session connected to url + + Attributes: + url (str): url to which the session is connected + initialized (asyncio.Event): Event marking if the Session initialization has finished. Wait on this to wait for a connection. + alive (bool): Whether the session connection is currently alive + ''' + + def __init__(self, url, user=None, domain=None, password=None, loginkey=None, proxy=None, token=None, ignore_ssl=False, auto_reconnect=False): if len(url) < 5 or ((not url.startswith('wss://')) and (not url.startsWith('ws://'))): raise ValueError("Invalid URL") @@ -51,7 +74,7 @@ class Session(object): domainid = domain if (user != None): username = user - url += '?auth=' + util.encode_cookie({ userid: 'user/' + domainid + '/' + username, domainid: domainid }, ckey) + url += '?auth=' + util._encode_cookie({ userid: 'user/' + domainid + '/' + username, domainid: domainid }, ckey) if token: token = b',' + base64.b64encode(token.encode()) @@ -187,7 +210,7 @@ class Session(object): @_check_socket async def _send_command(self, data, name, timeout=None): id = f"meshctrl_{name}_{self._get_command_id()}" - # This fixes a very theoretical bug with hash colisions in the case of an infinite number of requests. Now the bug will only happen if there are currently 2**32-1 of the same type of request going out at the same time + # This fixes a very theoretical bug with hash colisions in the case of an infinite int of requests. Now the bug will only happen if there are currently 2**32-1 of the same type of request going out at the same time while id in self._inflight: id = f"meshctrl_{name}_{self._get_command_id()}" @@ -221,653 +244,933 @@ class Session(object): raise response return response - - '''* - * Get device groups. Only returns meshes to which the logged in user has access - * @param {number?} [timeout=None] - duration in milliseconds to wait for a response before throwing an error - * @returns {Promise} List of meshes - * @throws {SocketError} Info about socket closure - * @throws {TimeoutError} Command timed out - ''' async def list_device_groups(self, timeout=None): + ''' + Get device groups. Only returns meshes to which the logged in user has access + + Args: + timeout (int): Duration in milliseconds to wait for a response before throwing an error + + Returns: + list[dict]: List of meshes + + Raises: + :py:class:`~meshctrl.exceptions.ServerError`: Error from server + :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure + asyncio.TimeoutError: Command timed out + ''' data = await self._send_command({"action": "meshes"}, "list_device_groups", timeout) return data["meshes"] - '''* - * Send an invite email for a group or mesh - * @param {string} group - Name of mesh to which to invite email - * @param {string} email - Email of user to invite - * @param {Object} [options={}] - * @param {string} [options.name=None] - User's name. For display purposes. - * @param {string} [options.message=None] - Message to send to user in invite email - * @param {string} [options.meshid=None] - ID of mesh which to invite user. Overrides "group" - * @param {number?} [timeout=None] - duration in milliseconds to wait for a response before throwing an error - * @return {Promise} true on success - * @throws {ServerError} Error text from server if there is a failure - * @throws {SocketError} Info about socket closure - * @throws {TimeoutError} Command timed out - ''' - async def send_invite_email(group, email, name=None, message=None, meshid=None, timeout=None): + + async def send_invite_email(self, group, email, name=None, message=None, meshid=None, timeout=None): + ''' + Send an invite email for a group or mesh + + Args: + group (str): Name of mesh to which to invite email + email (str): Email of user to invite + name (str): User's name. For display purposes. + message (str): Message to send to user in invite email + meshid (str): ID of mesh which to invite user. Overrides "group" + timeout (int): duration in milliseconds to wait for a response before throwing an error + + Returns: + bool: True on success, False otherwise + + Raises: + :py:class:`~meshctrl.exceptions.ServerError`: Error text from server if there is a failure + :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure + asyncio.TimeoutError: Command timed out + ''' raise NotImplementedError() - '''* - * Generate an invite link for a group or mesh - * @param {string} group - Name of group to add - * @param {number} hours - Hours until link expires - * @param {Object} [options={}] - * @param {constants.MeshRights} [options.flags=None] - Bitwise flags for constants.MeshRights - * @param {string} [options.meshid=None] - ID of mesh which to invite user. Overrides "group" - * @param {number?} [timeout=None] - duration in milliseconds to wait for a response before throwing an error - * @return {Promise} Invite link information - * @throws {ServerError} Error text from server if there is a failure - * @throws {SocketError} Info about socket closure - * @throws {TimeoutError} Command timed out - ''' - async def generate_invite_link(group, hours, flags=None, meshid=None, timeout=None): + async def generate_invite_link(self, group, hours, flags=None, meshid=None, timeout=None): + ''' + Generate an invite link for a group or mesh + + Args: + group (str): Name of group to add + hours (int): Hours until link expires + flags (~meshctrl.constants.MeshRights): Bitwise flags for constants.MeshRights + meshid (str): ID of mesh which to invite user. Overrides "group" + timeout (int): duration in milliseconds to wait for a response before throwing an error + + Returns: + dict: Invite link information + + Raises: + :py:class:`~meshctrl.exceptions.ServerError`: Error text from server if there is a failure + :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure + asyncio.TimeoutError: Command timed out + ''' raise NotImplementedError() - '''* - * List users on server. Admin Only. - * @param {number?} [timeout=None] - duration in milliseconds to wait for a response before throwing an error - * @returns {Promise} List of users - * @throws {ServerError} Error text from server if there is a failure - * @throws {SocketError} Info about socket closure - * @throws {TimeoutError} Command timed out - ''' - async def list_users(timeout=None): + async def list_users(self, timeout=None): + ''' + List users on server. Admin Only. + + Args: + timeout (int): duration in milliseconds to wait for a response before throwing an error + + Returns: + list[dict]: List of users + + Raises: + :py:class:`~meshctrl.exceptions.ServerError`: Error text from server if there is a failure + :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure + asyncio.TimeoutError: Command timed out + ''' raise NotImplementedError() - '''* - * Get list of connected users. Admin Only. - * @param {number?} [timeout=None] - duration in milliseconds to wait for a response before throwing an error - * @returns {Promise} List of user sessions - * @throws {SocketError} Info about socket closure - * @throws {TimeoutError} Command timed out - ''' - async def list_user_sessions(timeout=None): + async def list_user_sessions(self, timeout=None): + ''' + Get list of connected users. Admin Only. + + Args: + timeout (int): duration in milliseconds to wait for a response before throwing an error + + Returns: + list[dict] List of user sessions + + Raises: + :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure + asyncio.TimeoutError: Command timed out + ''' raise NotImplementedError() - '''* - * Get user groups. Admin will get all user groups, otherwise get limited user groups - * @param {number?} [timeout=None] - duration in milliseconds to wait for a response before throwing an error - * @returns {Promise} List of groups, or None if no groups are found - * @throws {SocketError} Info about socket closure - * @throws {TimeoutError} Command timed out - ''' - async def list_user_groups(timeout=None): + async def list_user_groups(self, timeout=None): + ''' + Get user groups. Admin will get all user groups, otherwise get limited user groups + + Args: + timeout (int): duration in milliseconds to wait for a response before throwing an error + + Returns: + list[dict]: List of groups + + Raises: + :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure + asyncio.TimeoutError: Command timed out + ''' raise NotImplementedError() - '''* - * Get devices to which the user has access. - * @param {Object} [options={}] - * @param {boolean} [options.details=False] - Get device details - * @param {string} [options.group=None] - Get devices from specific group by name. Overrides meshid - * @param {string} [options.meshid=None] - Get devices from specific group by id - * @param {number?} [timeout=None] - duration in milliseconds to wait for a response before throwing an error - * @returns {Promise} List of nodes - * @throws {SocketError} Info about socket closure - * @throws {TimeoutError} Command timed out - ''' - async def list_devices(details=False, group=None, meshid=None, timeout=None): + + async def list_devices(self, details=False, group=None, meshid=None, timeout=None): + ''' + Get devices to which the user has access. + + Args: + details (bool): Get device details + group (str): Get devices from specific group by name. Overrides meshid + meshid (str): Get devices from specific group by id + timeout (int): duration in milliseconds to wait for a response before throwing an error + + Returns: + list[dict]: List of nodes + + Raises: + :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure + asyncio.TimeoutError: Command timed out + ''' raise NotImplementedError() - '''* - * @callback Session~CloseCallback - * @param {SocketError} err - Error explaining the closure to the best of our ability - ''' + - '''* - * Listen for the socket to close - * @param {Session~CloseCallback} f - Function to call when the socket closes - ''' + def on_close(self, f): + ''' + Listen for the socket to close - def on_close(f): + Args: + f (function(data: dict)): Function to call when the socket closes + ''' raise NotImplementedError() - '''* - * @callback Session~EventCallback - * @param {Object} data - Raw event data from the server - ''' + def listen_to_events(self, f, filter=None): + ''' + Listen to events from the server - '''* - * Listen to events from the server - * @param {Session~EventCallback} f - Function to call when an event occurs - * @param {Object} [filter=None] - Object to filter events with. Only trigger for events that deep-match this object. Use sets for "array.contains" and arrays for equality of lists. - * @return {function} - Function used for listening. Use this to stop listening to events if you want that. - ''' - def listen_to_events(f, filter=None): + Args: + f (function(data: dict)): Function to call when an event occurs + filter (dict): dict to filter events with. Only trigger for events that deep-match this dict. Use sets for "array.contains" and arrays for equality of lists. + + Returns: + function: - Function used for listening. Use this to stop listening to events if you want that. + ''' + raise NotImplementedError() + + def stop_listening_to_events(self, f): + ''' + Stop listening to server events + + Args: + @param {function} Callback to stop listening with. + ''' raise NotImplementedError() - '''* - * Stop listening to server events - * @param {function} Callback to stop listening with. - ''' - def stop_listening_to_events(f): + async def list_events(self, userid=None, nodeid=None, limit=None, timeout=None): + ''' + List events visible to the currect user + + Args: + userid (str): Filter by user. Overrides nodeid. + nodeid (str): Filter by node + limit (int): Limit to the N most recent events + timeout (int): duration in milliseconds to wait for a response before throwing an error + + Returns: + list[dict]: List of events + + Raises: + :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure + asyncio.TimeoutError: Command timed out + ''' raise NotImplementedError() - '''* - * List events visible to the currect user - * @param {Object} [options={}] - * @param {string} [options.userid=None] - Filter by user. Overrides nodeid. - * @param {string} [options.nodeid=None] - Filter by node - * @param {number} [options.limit=None] - Limit to the N most recent events - * @param {number?} [timeout=None] - duration in milliseconds to wait for a response before throwing an error - * @return {Promise} List of events - * @throws {SocketError} Info about socket closure - * @throws {TimeoutError} Command timed out - ''' - async def list_events(userid=None, nodeid=None, limit=None, timeout=None): + async def list_login_tokens(self, timeout=None): + ''' + List login tokens for current user. WARNING: Non namespaced call. Calling this function again before it returns may cause unintended consequences. + + Args: + timeout (int): duration in milliseconds to wait for a response before throwing an error + + Returns: + list[dict]: List of tokens + + Raises: + :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure + asyncio.TimeoutError: Command timed out + ''' raise NotImplementedError() - '''* - * List login tokens for current user. WARNING: Non namespaced call. Calling this function again before it returns may cause unintended consequences. - * @param {number?} [timeout=None] - duration in milliseconds to wait for a response before throwing an error - * @return {Promise} List of tokens - * @throws {SocketError} Info about socket closure - * @throws {TimeoutError} Command timed out - ''' - async def list_login_tokens(timeout=None): + async def add_login_token(self, name, expire=None, timeout=None): + ''' + Create login token for current user. WARNING: Non namespaced call. Calling this function again before it returns may cause unintended consequences. + + Args: + name (str): Name of token + expire (int): Minutes until expiration. 0 or None for no expiration. + timeout (int): duration in milliseconds to wait for a response before throwing an error + + Returns: + dict: Created token + + Raises: + :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure + asyncio.TimeoutError: Command timed out + ''' raise NotImplementedError() - '''* - * Create login token for current user. WARNING: Non namespaced call. Calling this function again before it returns may cause unintended consequences. - * @param {string} name - Name of token - * @param {number} [expire=None] - Minutes until expiration. 0 or None for no expiration. - * @param {number?} [timeout=None] - duration in milliseconds to wait for a response before throwing an error - * @return {Promise} Created token - * @throws {SocketError} Info about socket closure - * @throws {TimeoutError} Command timed out - ''' - async def add_login_token(name, expire=None, timeout=None): + async def remove_login_token(self, names, timeout=None): + ''' + Remove login token for current user. WARNING: Non namespaced call. Calling this function again before it returns may cause unintended consequences. + + Args: + name (str): Name of token or token username + timeout (int): duration in milliseconds to wait for a response before throwing an error + + Returns: + list[dict]: List of remaining tokens + + Raises: + :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure + asyncio.TimeoutError: Command timed out + ''' raise NotImplementedError() - '''* - * Remove login token for current user. WARNING: Non namespaced call. Calling this function again before it returns may cause unintended consequences. - * @param {string} name - Name of token or token username - * @param {number?} [timeout=None] - duration in milliseconds to wait for a response before throwing an error - * @return {Promise} List of remaining tokens - * @throws {SocketError} Info about socket closure - * @throws {TimeoutError} Command timed out - ''' - async def remove_login_token(names, timeout=None): + async def add_user(self, name, password, randompass=False, domain=None, email=None, emailverified=False, resetpass=False, realname=None, phone=None, rights=None, timeout=None): + ''' + Add a new user + + Args: + name (str): username + password (str): user's starting password + randompass (bool): Generate a random password for the user. Overrides password + domain (str): Domain to which to add the user + email (str): User's email address + emailverified (bool): Pre-verify the user's email address + resetpass (bool): Force the user to reset their password on first login + realname (str): User's real name + phone (str): User's phone int + rights (~meshctrl.constants.UserRights): Bitwise mask of user's rights on the server + timeout (int): duration in milliseconds to wait for a response before throwing an error + + Returns: + bool: True on success, otherwise False + + Raises: + :py:class:`~meshctrl.exceptions.ServerError`: Error text from server if there is a failure + :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure + asyncio.TimeoutError: Command timed out + ''' raise NotImplementedError() - '''* - * Add a new user - * @param {string} name - username - * @param {string} password - user's starting password - * @param {Object} [options={}] - * @param {boolean} [options.randompass=False] - Generate a random password for the user. Overrides password - * @param {string} [options.domain=None] - Domain to which to add the user - * @param {string} [options.email=None] - User's email address - * @param {boolean} [options.emailverified=False] - Pre-verify the user's email address - * @param {boolean} [options.resetpass=False] - Force the user to reset their password on first login - * @param {string} [options.realname=None] - User's real name - * @param {string} [options.phone=None] - User's phone number - * @param {constants.UserRights} [options.rights=None] - Bitwise mask of user's rights on the server - * @param {number?} [timeout=None] - duration in milliseconds to wait for a response before throwing an error - * @return {Promise} true on success - * @throws {ServerError} Error text from server if there is a failure - * @throws {SocketError} Info about socket closure - * @throws {TimeoutError} Command timed out - ''' - async def add_user(name, password, randompass=False, domain=None, email=None, emailverified=False, resetpass=False, realname=None, phone=None, rights=None, timeout=None): + async def edit_user(self, userid, domain=None, email=None, emailverified=False, resetpass=False, realname=None, phone=None, rights=None, timeout=None): + ''' + Edit an existing user + + Args: + userid (str): Unique userid + domain (str): Domain to which to add the user + email (str): User's email address + emailverified (bool): Verify or unverify the user's email address + resetpass (bool): Force the user to reset their password on next login + realname (str): User's real name + phone (str): User's phone int + rights (~meshctrl.constants.UserRights): Bitwise mask of user's rights on the server + timeout (int): duration in milliseconds to wait for a response before throwing an error + + Returns: + bool: True on success, otherwise False + + Raises: + :py:class:`~meshctrl.exceptions.ServerError`: Error text from server if there is a failure + :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure + asyncio.TimeoutError: Command timed out + ''' raise NotImplementedError() - '''* - * Edit an existing user - * @param {string} userid - Unique userid - * @param {Object} [options={}] - * @param {string} [options.domain=None] - Domain to which to add the user - * @param {string} [options.email=None] - User's email address - * @param {boolean} [options.emailverified=False] - Verify or unverify the user's email address - * @param {boolean} [options.resetpass=False] - Force the user to reset their password on next login - * @param {string} [options.realname=None] - User's real name - * @param {string} [options.phone=None] - User's phone number - * @param {constants.UserRights} [options.rights=None] - Bitwise mask of user's rights on the server - * @param {number?} [timeout=None] - duration in milliseconds to wait for a response before throwing an error - * @return {Promise} true on success - * @throws {ServerError} Error text from server if there is a failure - * @throws {SocketError} Info about socket closure - * @throws {TimeoutError} Command timed out - ''' - async def edit_user(userid, domain=None, email=None, emailverified=False, resetpass=False, realname=None, phone=None, rights=None, timeout=None): + async def remove_user(self, userid, timeout=None): + ''' + Remove an existing user + + Args: + userid (str): Unique userid + timeout (int): duration in milliseconds to wait for a response before throwing an error + + Returns: + bool: True on success, otherwise False + + Raises: + :py:class:`~meshctrl.exceptions.ServerError`: Error text from server if there is a failure + :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure + asyncio.TimeoutError: Command timed out + ''' raise NotImplementedError() - '''* - * Remove an existing user - * @param {string} userid - Unique userid - * @param {number?} [timeout=None] - duration in milliseconds to wait for a response before throwing an error - * @return {Promise} true on success - * @throws {ServerError} Error text from server if there is a failure - * @throws {SocketError} Info about socket closure - * @throws {TimeoutError} Command timed out - ''' - async def remove_user(userid, timeout=None): + async def add_user_group(self, name, description=None, timeout=None): + ''' + Create a new user group + + Args: + name (str): Name of usergroup + description (str): Description of user group + timeout (int): duration in milliseconds to wait for a response before throwing an error + + Returns: + dict: New user group + + Raises: + :py:class:`~meshctrl.exceptions.ServerError`: Error text from server if there is a failure + :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure + asyncio.TimeoutError: Command timed out + ''' raise NotImplementedError() - '''* - * Create a new user group - * @param {string} name - Name of usergroup - * @param {string} [description=None] - Description of user group - * @param {number?} [timeout=None] - duration in milliseconds to wait for a response before throwing an error - * @return {Promise} New user group - * @throws {ServerError} Error text from server if there is a failure - * @throws {SocketError} Info about socket closure - * @throws {TimeoutError} Command timed out - ''' - async def add_user_group(name, description=None, timeout=None): + async def remove_user_group(self, groupid, timeout=None): + ''' + Remove an existing user group + + Args: + userid (str): Unique userid + timeout (int): duration in milliseconds to wait for a response before throwing an error + + Returns: + bool: True on success, otherwise False + + Raises: + :py:class:`~meshctrl.exceptions.ServerError`: Error text from server if there is a failure + :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure + asyncio.TimeoutError: Command timed out + ''' raise NotImplementedError() - '''* - * Remove an existing user group - * @param {string} userid - Unique userid - * @param {number?} [timeout=None] - duration in milliseconds to wait for a response before throwing an error - * @return {Promise} true on success - * @throws {ServerError} Error text from server if there is a failure - * @throws {SocketError} Info about socket closure - * @throws {TimeoutError} Command timed out - ''' - async def remove_user_group(groupid, timeout=None): + async def add_users_to_user_group(self, userids, groupid, timeout=None): + ''' + Add user(s) to an existing user group. WARNING: Non namespaced call. Calling this function again before it returns may cause unintended consequences. + + Args: + ids (str|list[str]): Unique user id(s) + groupid (str): Group to add the given user to + timeout (int): duration in milliseconds to wait for a response before throwing an error + + Returns: + list[str]: List of users that were successfully added + + Raises: + :py:class:`~meshctrl.exceptions.ServerError`: Error text from server if there is a failure + :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure + asyncio.TimeoutError: Command timed out + ''' raise NotImplementedError() - '''* - * Add user(s) to an existing user group. WARNING: Non namespaced call. Calling this function again before it returns may cause unintended consequences. - * @param {string|array} ids - Unique user id(s) - * @param {string} groupid - Group to add the given user to - * @param {number?} [timeout=None] - duration in milliseconds to wait for a response before throwing an error - * @return {Promise} List of users that were successfully added - * @throws {ServerError} Error text from server if there is a failure - * @throws {SocketError} Info about socket closure - * @throws {TimeoutError} Command timed out - ''' - async def add_users_to_user_group(userids, groupid, timeout=None): + async def remove_user_from_user_group(self, userid, groupid, timeout=None): + ''' + Remove user from an existing user group + + Args: + id (str): Unique user id + groupid (str): Group to remove the given user from + timeout (int): duration in milliseconds to wait for a response before throwing an error + + Returns: + bool: True on success, otherwise False + + Raises: + :py:class:`~meshctrl.exceptions.ServerError`: Error text from server if there is a failure + :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure + asyncio.TimeoutError: Command timed out + ''' raise NotImplementedError() - '''* - * Remove user from an existing user group - * @param {string} id - Unique user id - * @param {string} groupid - Group to remove the given user from - * @param {number?} [timeout=None] - duration in milliseconds to wait for a response before throwing an error - * @return {Promise} true on success - * @throws {ServerError} Error text from server if there is a failure - * @throws {SocketError} Info about socket closure - * @throws {TimeoutError} Command timed out - ''' - async def remove_user_from_user_group(userid, groupid, timeout=None): + async def add_users_to_device(self, userids, nodeid, rights=None, timeout=None): + ''' + Add a user to an existing node + + Args: + userids (str|list[str]): Unique user id(s) + nodeid (str): Node to add the given user to + rights (~meshctrl.constants.MeshRights): Bitwise mask for the rights on the given mesh + timeout (int): duration in milliseconds to wait for a response before throwing an error + + Returns: + bool: True on success, otherwise False + + Raises: + :py:class:`~meshctrl.exceptions.ServerError`: Error text from server if there is a failure + :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure + asyncio.TimeoutError: Command timed out + ''' raise NotImplementedError() - '''* - * Add a user to an existing node - * @param {string|array} userids - Unique user id(s) - * @param {string} nodeid - Node to add the given user to - * @param {constants.MeshRights} [rights=None] - Bitwise mask for the rights on the given mesh - * @param {number?} [timeout=None] - duration in milliseconds to wait for a response before throwing an error - * @return {Promise} true on success - * @throws {ServerError} Error text from server if there is a failure - * @throws {SocketError} Info about socket closure - * @throws {TimeoutError} Command timed out - ''' - async def add_users_to_device(userids, nodeid, rights=None, timeout=None): + async def remove_users_from_device(self, nodeid, userids, timeout=None): + ''' + Remove users from an existing node + + Args: + nodeid (str): Node to remove the given users from + userids (str|list[str]): Unique user id(s) + timeout (int): duration in milliseconds to wait for a response before throwing an error + + Returns: + bool: True on success, otherwise False + + Raises: + :py:class:`~meshctrl.exceptions.ServerError`: Error text from server if there is a failure + :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure + asyncio.TimeoutError: Command timed out + ''' raise NotImplementedError() - '''* - * Remove users from an existing node - * @param {string} nodeid - Node to remove the given users from - * @param {string|array} userids - Unique user id(s) - * @param {number?} [timeout=None] - duration in milliseconds to wait for a response before throwing an error - * @return {Promise} true on success - * @throws {ServerError} Error text from server if there is a failure - * @throws {SocketError} Info about socket closure - * @throws {TimeoutError} Command timed out - ''' - async def remove_users_from_device(nodeid, userids, timeout=None): + async def add_device_group(self, name, description="", amtonly=False, features=0, consent=0, timeout=None): + ''' + Create a new device group + + Args: + name (str): Name of device group + description (str): Description of device group + amtonly (bool): + features (~meshctrl.constants.MeshFeatures): Bitwise features to enable on the group + consent (~meshctrl.constants.ConsentFlags): Bitwise consent flags to use for the group + timeout (int): duration in milliseconds to wait for a response before throwing an error + + Returns: + dict: New device group + + Raises: + :py:class:`~meshctrl.exceptions.ServerError`: Error text from server if there is a failure + :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure + asyncio.TimeoutError: Command timed out + ''' raise NotImplementedError() - '''* - * Create a new device group - * @param {string} name - Name of device group - * @param {Object} [options={}] - * @param {string} [options.description=""] - Description of device group - * @param {boolean} [options.amtonly=False] - - * @param {constants.MeshFeatures} [options.features=0] - Bitwise features to enable on the group - * @param {constants.ConsentFlags} [options.consent=0] - Bitwise consent flags to use for the group - * @param {number?} [timeout=None] - duration in milliseconds to wait for a response before throwing an error - * @return {Promise} New device group - * @throws {ServerError} Error text from server if there is a failure - * @throws {SocketError} Info about socket closure - * @throws {TimeoutError} Command timed out - ''' - async def add_device_group(name, description="", amtonly=False, features=0, consent=0, timeout=None): + async def remove_device_group(self, meshid, isname=False, timeout=None): + ''' + Remove an existing device group + + Args: + meshid (str): Unique id of device group + isname (bool): treat "meshid" as a name instead of an id + timeout (int): duration in milliseconds to wait for a response before throwing an error + + Returns: + bool: True on success, otherwise False + + Raises: + :py:class:`~meshctrl.exceptions.ServerError`: Error text from server if there is a failure + :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure + asyncio.TimeoutError: Command timed out + ''' raise NotImplementedError() - '''* - * Remove an existing device group - * @param {string} meshid - Unique id of device group - * @param {boolean} [isname=False] - treat "meshid" as a name instead of an id - * @param {number?} [timeout=None] - duration in milliseconds to wait for a response before throwing an error - * @return {Promise} true on success - * @throws {ServerError} Error text from server if there is a failure - * @throws {SocketError} Info about socket closure - * @throws {TimeoutError} Command timed out - ''' - async def remove_device_group(meshid, isname=False, timeout=None): + async def edit_device_group(self, meshid, isname=False, name=None, description=None, flags=None, consent=None, invite_codes=None, backgroundonly=False, interactiveonly=False, timeout=None): + ''' + Edit an existing device group + + Args: + meshid (str): Unique id of device group + isname (bool): treat "meshid" as a name instead of an id + name (str): New name for group + description (str): New description + flags (~meshctrl.constants.MeshFeatures): Features to enable on the group + consent (~meshctrl.constants.ConsentFlags): Which consent flags to use for the group + invite_codes (list[str]): Create new invite codes + backgroundonly (bool): Flag for invite codes + interactiveonly (bool): Flag for invite codes + timeout (int): duration in milliseconds to wait for a response before throwing an error + + Returns: + bool: True on success, otherwise False + + Raises: + :py:class:`~meshctrl.exceptions.ServerError`: Error text from server if there is a failure + :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure + asyncio.TimeoutError: Command timed out + ''' raise NotImplementedError() - '''* - * Edit an existing device group - * @param {string} meshid - Unique id of device group - * @param {Object} [options={}] - * @param {boolean} [options.isname=False] - treat "meshid" as a name instead of an id - * @param {string} [options.name=None] - New name for group - * @param {boolean} [options.description=None] - New description - * @param {constants.MeshFeatures} [options.flags=None] - Features to enable on the group - * @param {constants.ConsentFlags} [options.consent=None] - Which consent flags to use for the group - * @param {string[]} [options.invite_codes=None] - Create new invite codes - * @param {boolean} [options.backgroundonly=False] - Flag for invite codes - * @param {boolean} [options.interactiveonly=False] - Flag for invite codes - * @param {number?} [timeout=None] - duration in milliseconds to wait for a response before throwing an error - * @return {Promise} true on success - * @throws {ServerError} Error text from server if there is a failure - * @throws {SocketError} Info about socket closure - * @throws {TimeoutError} Command timed out - ''' - async def edit_device_group(meshid, isname=False, name=None, description=None, flags=None, consent=None, invite_codes=None, backgroundonly=False, interactiveonly=False, timeout=None): + async def move_to_device_group(self, nodeids, meshid, isname=False, timeout=None): + ''' + Move a device from one group to another + + Args: + nodeids (str|list[str]): Unique node id(s) + meshid (str): Unique mesh id + isname (bool): treat "meshid" as a name instead of an id + timeout (int): duration in milliseconds to wait for a response before throwing an error + + Returns: + bool: True on success, otherwise False + + Raises: + :py:class:`~meshctrl.exceptions.ServerError`: Error text from server if there is a failure + :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure + asyncio.TimeoutError: Command timed out + ''' raise NotImplementedError() - '''* - * Move a device from one group to another - * @param {string|array} nodeids - Unique node id(s) - * @param {string} meshid - Unique mesh id - * @param {boolean} [isname=False] - treat "meshid" as a name instead of an id - * @param {number?} [timeout=None] - duration in milliseconds to wait for a response before throwing an error - * @return {Promise} true on success - * @throws {ServerError} Error text from server if there is a failure - * @throws {SocketError} Info about socket closure - * @throws {TimeoutError} Command timed out - ''' - async def move_to_device_group(nodeids, meshid, isname=False, timeout=None): + async def add_users_to_device_group(self, userids, meshid, isname=False, rights=0, timeout=None): + ''' + Add a user to an existing mesh + + Args: + userids (str|list[str]): Unique user id(s) + meshid (str): Mesh to add the given user to + isname (bool): Read meshid as a name rather than an id + rights (~meshctrl.constants.MeshRights): Bitwise mask for the rights on the given mesh + timeout (int): duration in milliseconds to wait for a response before throwing an error + + Returns: + dict: Object showing which were added correctly and which were not, along with their result messages + + Raises: + :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure + asyncio.TimeoutError: Command timed out + ''' raise NotImplementedError() - '''* - * Add a user to an existing mesh - * @param {string|array} userids - Unique user id(s) - * @param {string} meshid - Mesh to add the given user to - * @param {Object} [options={}] - * @param {boolean} [options.isname=False] - Read meshid as a name rather than an id - * @param {constants.MeshRights} [options.rights=0] - Bitwise mask for the rights on the given mesh - * @param {number?} [timeout=None] - duration in milliseconds to wait for a response before throwing an error - * @return {Promise} Object showing which were added correctly and which were not, along with their result messages - * @throws {SocketError} Info about socket closure - * @throws {TimeoutError} Command timed out - ''' - async def add_users_to_device_group(userids, meshid, isname=False, rights=0, timeout=None): + async def remove_users_from_device_group(self, userids, meshid, isname=False, timeout=None): + ''' + Remove users from an existing mesh + + Args: + userids (str|list[str]): Unique user id(s) + meshid (str): Mesh to add the given user to + isname (bool): Read meshid as a name rather than an id + timeout (int): duration in milliseconds to wait for a response before throwing an error + + Returns: + dict: Object showing which were removed correctly and which were not + + Raises: + :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure + asyncio.TimeoutError: Command timed out + ''' raise NotImplementedError() - '''* - * Remove users from an existing mesh - * @param {string|array} userids - Unique user id(s) - * @param {string} meshid - Mesh to add the given user to - * @param {boolean} [isname=False] - Read meshid as a name rather than an id - * @param {number?} [timeout=None] - duration in milliseconds to wait for a response before throwing an error - * @return {Promise} Object showing which were removed correctly and which were not - * @throws {SocketError} Info about socket closure - * @throws {TimeoutError} Command timed out - ''' - async def remove_users_from_device_group(userids, meshid, isname=False, timeout=None): + async def broadcast(self, message, userid=None, timeout=None): + ''' + Broadcast a message to all users or a single user + + Args: + message (str): Message to broadcast + userid (str): Optional user to which to send the message + timeout (int): duration in milliseconds to wait for a response before throwing an error + + Returns: + bool: True if successful + + Raises: + :py:class:`~meshctrl.exceptions.ServerError`: Error text from server if there is a failure + :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure + asyncio.TimeoutError: Command timed out + ''' raise NotImplementedError() - '''* - * Broadcast a message to all users or a single user - * @param {string} message - Message to broadcast - * @param {string} [userid=None] - Optional user to which to send the message - * @param {number?} [timeout=None] - duration in milliseconds to wait for a response before throwing an error - * @return {Promise} True if successful - * @throws {ServerError} Error text from server if there is a failure - * @throws {SocketError} Info about socket closure - * @throws {TimeoutError} Command timed out - ''' - async def broadcast(message, userid=None, timeout=None): + async def device_info(self, nodeid, timeout=None): + ''' + Get all info for a given device. WARNING: Non namespaced call. Calling this function again before it returns may cause unintended consequences. + + Args: + nodeid (str): Unique id of desired node + timeout (int): duration in milliseconds to wait for a response before throwing an error + + Returns: + dict: Object containing all meaningful device info + + Raises: + ValueError: `Invalid device id` if device is not found + :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure + asyncio.TimeoutError: Command timed out + ''' raise NotImplementedError() - '''* Get all info for a given device. WARNING: Non namespaced call. Calling this function again before it returns may cause unintended consequences. - * @param {string} nodeid - Unique id of desired node - * @param {number?} [timeout=None] - duration in milliseconds to wait for a response before throwing an error - * @returns {Promise} Object containing all meaningful device info - * @throws {ValueError} `Invalid device id` if device is not found - * @throws {SocketError} Info about socket closure - * @throws {TimeoutError} Command timed out - ''' - async def device_info(nodeid, timeout=None): + async def edit_device(self, nodeid, name=None, description=None, tags=None, icon=None, consent=None, timeout=None): + ''' + Edit properties of an existing device + + Args: + nodeid (str): Unique id of desired node + name (str): New name for device + description (str): New description for device + tags (str|list[str]]): New tags for device + icon (~meshctrl.constants.Icon): New icon for device + consent (~meshctrl.constants.ConsentFlags): New consent flags for device + timeout (int): duration in milliseconds to wait for a response before throwing an error + + Returns: + bool: True if successful + + Raises: + :py:class:`~meshctrl.exceptions.ServerError`: Error text from server if there is a failure + :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure + asyncio.TimeoutError: Command timed out + ''' raise NotImplementedError() + async def run_command(self, nodeids, command, powershell=False, runasuser=False, runasuseronly=False, timeout=None): + ''' + Run a command on any int of nodes. WARNING: Non namespaced call. Calling this function again before it returns may cause unintended consequences. - '''* Edit properties of an existing device - * @param {string} nodeid - Unique id of desired node - * @param {Object} [options={}] - * @param {string} [options.name=None] - New name for device - * @param {string} [options.description=None] - New description for device - * @param {string|string[]} [options.tags=None] - New tags for device - * @param {constants.Icon} [options.icon=None] - New icon for device - * @param {constants.ConsentFlags} [options.consent=None] - New consent flags for device - * @param {number?} [timeout=None] - duration in milliseconds to wait for a response before throwing an error - * @returns {Promise} True if successful - * @throws {ServerError} Error text from server if there is a failure - * @throws {SocketError} Info about socket closure - * @throws {TimeoutError} Command timed out - ''' - async def edit_device(nodeid, name=None, description=None, tags=None, icon=None, consent=None, timeout=None): + Args: + nodeids (str|list[str]): Unique ids of nodes on which to run the command + command (str): Command to run + powershell (bool): Use powershell to run command. Only available on Windows. + runasuser (bool): Attempt to run as a user instead of the root permissions given to the agent. Fall back to root if we cannot. + runasuseronly (bool): Error if we cannot run the command as the logged in user. + timeout (int): duration in milliseconds to wait for a response before throwing an error + + Returns: + dict: Object containing mapped output of the commands by device + + Raises: + :py:class:`~meshctrl.exceptions.ServerError`: Error text from server if there is a failure + :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure + asyncio.TimeoutError: Command timed out + ''' raise NotImplementedError() - '''* Run a command on any number of nodes. WARNING: Non namespaced call. Calling this function again before it returns may cause unintended consequences. - * @param {string|string[]} nodeids - Unique ids of nodes on which to run the command - * @param {string} command - Command to run - * @param {Object} [options={}] - * @param {boolean} [options.powershell=False] - Use powershell to run command. Only available on Windows. - * @param {boolean} [options.runasuser=False] - Attempt to run as a user instead of the root permissions given to the agent. Fall back to root if we cannot. - * @param {boolean} [options.runasuseronly=False] - Error if we cannot run the command as the logged in user. - * @param {number?} [timeout=None] - duration in milliseconds to wait for a response before throwing an error - * @returns {Promise} Object containing mapped output of the commands by device - * @throws {ServerError} Error text from server if there is a failure - * @throws {SocketError} Info about socket closure - * @throws {TimeoutError} Command timed out - ''' - async def run_command(nodeids, command, powershell=False, runasuser=False, runasuseronly=False, timeout=None): + async def shell(self, nodeid, unique=False): + ''' + Get a terminal shell on the given device + + Args: + nodeid (str): Unique id of node on which to open the shell + unique (bool): True: Create a unique :py:class:`~meshctrl.shell.Shell`. Caller is responsible for cleanup. False: Use a cached :py:class:`~meshctrl.shell.Shell` if available, otherwise create and cache. + + Returns: + :py:class:`~meshctrl.shell.Shell`: Newly created and initialized :py:class:`~meshctrl.shell.Shell` or cached :py:class:`~meshctrl.shell.Shell` if unique is False and a shell is currently active + ''' raise NotImplementedError() - '''* Get a terminal shell on the given device - * @param {string} nodeid - Unique id of node on which to open the shell - * @param {boolean} [unique=False] - true: Create a unique {@link _Shell}. Caller is responsible for cleanup. False: Use a cached {@link _Shell} if available, otherwise create and cache. - * @returns {Promise<_Shell>} Newly created and initialized {@link _Shell} or cached {@link _Shell} if unique is False and a shell is currently active - ''' - async def shell(nodeid, unique=False): + async def smart_shell(self, nodeid, regex, unique=False): + ''' + Get a smart terminal shell on the given device + + Args: + nodeid (str): Unique id of node on which to open the shell + regex (regex): Regex to watch for to signify that the shell is ready for new input. + unique (bool): true: Create a unique :py:class:`~meshctrl.shell.SmartShell`. Caller is responsible for cleanup. False: Use a cached :py:class:`~meshctrl.shell.SmartShell` if available, otherwise create and cache. + Returns: + :py:class:`~meshctrl.shell.SmartShell`: Newly created and initialized :py:class:`~meshctrl.shell.SmartShell` or cached :py:class:`~meshctrl.shell.SmartShell` if unique is False and a smartshell with regex is currently active + ''' raise NotImplementedError() - '''* Get a smart terminal shell on the given device - * @param {string} nodeid - Unique id of node on which to open the shell - * @param {regex} regex - Regex to watch for to signify that the shell is ready for new input. - * @param {boolean} [unique=False] - true: Create a unique {@link _SmartShell}. Caller is responsible for cleanup. False: Use a cached {@link _SmartShell} if available, otherwise create and cache. - * @returns {Promise<_SmartShell>} Newly created and initialized {@link _SmartShell} or cached {@link _SmartShell} if unique is False and a smartshell with regex is currently active - ''' - async def smart_shell(nodeid, regex, unique=False): + async def wake_devices(self, nodeids, timeout=None): + ''' + Wake up given devices + + Args: + nodeids (str|list[str]): Unique ids of nodes which to wake + timeout (int): duration in milliseconds to wait for a response before throwing an error + + Returns: + bool: True if successful + + Raises: + :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure + asyncio.TimeoutError: Command timed out + ''' raise NotImplementedError() - '''* Wake up given devices - * @param {string|string[]} nodeids - Unique ids of nodes which to wake - * @param {number?} [timeout=None] - duration in milliseconds to wait for a response before throwing an error - * @returns {Promise} True if successful - * @throws {SocketError} Info about socket closure - * @throws {TimeoutError} Command timed out - ''' - async def wake_devices(nodeids, timeout=None): + async def reset_devices(self, nodeids, timeout=None): + ''' + Reset given devices + + Args: + nodeids (str|list[str]): Unique ids of nodes which to reset + timeout (int): duration in milliseconds to wait for a response before throwing an error + + Returns: + bool: True if successful + + Raises: + :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure + asyncio.TimeoutError: Command timed out + ''' raise NotImplementedError() - '''* Reset given devices - * @param {string|string[]} nodeids - Unique ids of nodes which to reset - * @param {number?} [timeout=None] - duration in milliseconds to wait for a response before throwing an error - * @returns {Promise} True if successful - * @throws {SocketError} Info about socket closure - * @throws {TimeoutError} Command timed out - ''' - async def reset_devices(nodeids, timeout=None): + async def sleep_devices(self, nodeids, timeout=None): + ''' + Sleep given devices + + Args: + nodeids (str|list[str]): Unique ids of nodes which to sleep + timeout (int): duration in milliseconds to wait for a response before throwing an error + + Returns: + bool: True if successful + + Raises: + :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure + asyncio.TimeoutError: Command timed out + ''' raise NotImplementedError() - '''* Sleep given devices - * @param {string|string[]} nodeids - Unique ids of nodes which to sleep - * @param {number?} [timeout=None] - duration in milliseconds to wait for a response before throwing an error - * @returns {Promise} True if successful - * @throws {SocketError} Info about socket closure - * @throws {TimeoutError} Command timed out - ''' - async def sleep_devices(nodeids, timeout=None): + async def power_off_devices(self, nodeids, timeout=None): + ''' Power off given devices + + Args: + nodeids (str|list[str]): Unique ids of nodes which to power off + timeout (int): duration in milliseconds to wait for a response before throwing an error + + Returns: + bool: True if successful + + Raises: + :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure + asyncio.TimeoutError: Command timed out + ''' raise NotImplementedError() - '''* Power off given devices - * @param {string|string[]} nodeids - Unique ids of nodes which to power off - * @param {number?} [timeout=None] - duration in milliseconds to wait for a response before throwing an error - * @returns {Promise} True if successful - * @throws {SocketError} Info about socket closure - * @throws {TimeoutError} Command timed out - ''' - async def power_off_devices(nodeids, timeout=None): + async def list_device_shares(self, nodeid, timeout=None): + ''' + List device shares of given node. WARNING: Non namespaced call. Calling this function again before it returns may cause unintended consequences. + + Args: + nodeid (str): Unique id of nodes of which to list shares + timeout (int): duration in milliseconds to wait for a response before throwing an error + + Returns: + list[dict]: Array of dicts representing device shares + + Raises: + :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure + asyncio.TimeoutError: Command timed out + ''' raise NotImplementedError() - '''* List device shares of given node. WARNING: Non namespaced call. Calling this function again before it returns may cause unintended consequences. - * @param {string} nodeid - Unique id of nodes of which to list shares - * @param {number?} [timeout=None] - duration in milliseconds to wait for a response before throwing an error - * @returns {Promise} Array of objects representing device shares - * @throws {SocketError} Info about socket closure - * @throws {TimeoutError} Command timed out - ''' - async def list_device_shares(nodeid, timeout=None): + async def add_device_share(self, nodeid, name, type=constants.SharingType.desktop, consent=None, start=None, end=None, duration=60*60, timeout=None): + ''' + Add device share to given node. WARNING: Non namespaced call. Calling this function again before it returns may cause unintended consequences. + + Args: + nodeid (str): Unique id of nodes of which to list shares + name (str): Name of guest with which to share + type (~meshctrl.constants.SharingType): Type of share thise should be + consent (~meshctrl.constants.ConsentFlags): Consent flags for share. Defaults to "notify" for your given constants.SharingType + start (int|datetime.datetime): When to start the share + end (int|datetime.datetime): When to end the share. If None, use duration instead + duration (int): Duration in seconds for share to exist + timeout (int): duration in milliseconds to wait for a response before throwing an error + + Returns: + dict: Info about the newly created share + + Raises: + :py:class:`~meshctrl.exceptions.ServerError`: Error text from server if there is a failure + :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure + asyncio.TimeoutError: Command timed out + ''' raise NotImplementedError() - '''* Add device share to given node. WARNING: Non namespaced call. Calling this function again before it returns may cause unintended consequences. - * @param {string} nodeid - Unique id of nodes of which to list shares - * @param {string} name - Name of guest with which to share - * @param {Object} [options={}] - * @param {constants.SharingType} [options.type=constants.SharingType.desktop] - Type of share thise should be - * @param {constants.ConsentFlags} [options.consent=None] - Consent flags for share. Defaults to "notify" for your given constants.SharingType - * @param {number|Date} [options.start=new Date()] - When to start the share - * @param {number|Date} [options.end=None] - When to end the share. If None, use duration instead - * @param {number} [options.duration=60*60] - Duration in seconds for share to exist - * @param {number?} [timeout=None] - duration in milliseconds to wait for a response before throwing an error - * @returns {Promise} Info about the newly created share - * @throws {ServerError} Error text from server if there is a failure - * @throws {SocketError} Info about socket closure - * @throws {TimeoutError} Command timed out - ''' - async def add_device_share(nodeid, name, type=constants.SharingType.desktop, consent=None, start=None, end=None, duration=60*60, timeout=None): + async def remove_device_share(self, nodeid, shareid, timeout=None): + ''' + Remove a device share + + Args: + nodeid (str): Unique node from which to remove the share + shareid (str): Unique share id to be removed + timeout (int): duration in milliseconds to wait for a response before throwing an error + + Returns: + bool: true if successful + + Raises: + :py:class:`~meshctrl.exceptions.ServerError`: Error text from server if there is a failure + :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure + asyncio.TimeoutError: Command timed out + ''' raise NotImplementedError() - '''* Remove a device share - * @param {string} nodeid - Unique node from which to remove the share - * @param {string} shareid - Unique share id to be removed - * @param {number?} [timeout=None] - duration in milliseconds to wait for a response before throwing an error - * @returns {Promise} true if successful - * @throws {ServerError} Error text from server if there is a failure - * @throws {SocketError} Info about socket closure - * @throws {TimeoutError} Command timed out - ''' - async def remove_device_share(nodeid, shareid, timeout=None): + async def device_open_url(self, nodeid, url, timeout=None): + ''' + Open url in browser on device. WARNING: Non namespaced call. Calling this function again before it returns may cause unintended consequences. + + Args: + nodeid (str): Unique node from which to remove the share + url (str): url to open + timeout (int): duration in milliseconds to wait for a response before throwing an error + + Returns: + bool: true if successful + + Raises: + :py:class:`~meshctrl.exceptions.ServerError`: Error text from server if there is a failure + Exception: `Failed to open url` if failure occurs + :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure + asyncio.TimeoutError: Command timed out + ''' raise NotImplementedError() - '''* Open url in browser on device. WARNING: Non namespaced call. Calling this function again before it returns may cause unintended consequences. - * @param {string} nodeid - Unique node from which to remove the share - * @param {string} url - url to open - * @param {number?} [timeout=None] - duration in milliseconds to wait for a response before throwing an error - * @returns {Promise} true if successful - * @throws {ServerError} Error text from server if there is a failure - * @throws {Error} `Failed to open url` if failure occurs - * @throws {SocketError} Info about socket closure - * @throws {TimeoutError} Command timed out - ''' - async def device_open_url(nodeid, url, timeout=None): + async def device_message(self, nodeid, message, title="MeshCentral", timeout=None): + ''' + Display a message on remote device. + + Args: + nodeid (str): Unique node from which to remove the share + message (str): message to display + title (str): message title + timeout (int): duration in milliseconds to wait for a response before throwing an error + + Returns: + bool: true if successful + + Raises: + :py:class:`~meshctrl.exceptions.ServerError`: Error text from server if there is a failure + :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure + asyncio.TimeoutError: Command timed out + ''' raise NotImplementedError() - '''* Display a message on remote device. - * @param {string} nodeid - Unique node from which to remove the share - * @param {string} message - message to display - * @param {string} [title="MeshCentral"] - message title - * @param {number?} [timeout=None] - duration in milliseconds to wait for a response before throwing an error - * @returns {Promise} true if successful - * @throws {ServerError} Error text from server if there is a failure - * @throws {SocketError} Info about socket closure - * @throws {TimeoutError} Command timed out - ''' - async def device_message(nodeid, message, title="MeshCentral", timeout=None): + async def device_toast(self, nodeids, message, title="MeshCentral", timeout=None): + ''' + Popup a toast a message on remote device. + + Args: + nodeids (str|list[str]): Unique node from which to remove the share + message (str): message to display + title (str): message title + timeout (int): duration in milliseconds to wait for a response before throwing an error + + Returns: + bool: true if successful + + Raises: + :py:class:`~meshctrl.exceptions.ServerError`: Error text from server if there is a failure + :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure + asyncio.TimeoutError: Command timed out + @todo This function returns true even if it fails, because the server tells us it succeeds before it actually knows, then later tells us it failed, but it's hard to find that because it looks exactly like a success. + ''' raise NotImplementedError() - '''* Popup a toast a message on remote device. - * @param {string|string[]} nodeids - Unique node from which to remove the share - * @param {string} message - message to display - * @param {string} [title="MeshCentral"] - message title - * @param {number?} [timeout=None] - duration in milliseconds to wait for a response before throwing an error - * @returns {Promise} true if successful - * @throws {ServerError} Error text from server if there is a failure - * @throws {SocketError} Info about socket closure - * @throws {TimeoutError} Command timed out - * @todo This function returns true even if it fails, because the server tells us it succeeds before it actually knows, then later tells us it failed, but it's hard to find that because it looks exactly like a success. - ''' - async def device_toast(nodeids, message, title="MeshCentral", timeout=None): + def interuser(self, data, session=None, user=None): + ''' + Fire off an interuser message. This is a fire and forget api, we have no way of checking if the user got the message. + + Args: + data (serializable): Any sort of serializable data you want to send to the user + session (str): Direct session to send to. Use this after you have made connection with a specific user session. + user (str): Send message to all sessions of a particular user. One of these must be set. + + Raises: + ValueError: Value error if neither user nor session are given. + :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure + ''' raise NotImplementedError() - '''* Fire off an interuser message. This is a fire and forget api, we have no way of checking if the user got the message. - * @param {serializable} data - Any sort of serializable data you want to send to the user - * @param {Object} [options={}] - * @param {string} [options.session=None] - Direct session to send to. Use this after you have made connection with a specific user session. - * @param {string} [options.user=None] - Send message to all sessions of a particular user. One of these must be set. - * @throws {ValueError} Value error if neither user nor session are given. - * @throws {SocketError} Info about socket closure - ''' - def interuser(data, session=None, user=None): + async def upload(self, nodeid, source, target, unique_file_tunnel=False): + ''' + Upload a stream to a device. This creates an _File and destroys it every call. If you need to upload multiple files, use {@link Session#file_explorer} instead. + + Args: + nodeid (str): Unique id to upload stream to + source (ReadableStream): ReadableStream from which to read data + target (str): Path which to upload stream to on remote device + unique_file_tunnel (bool): True: Create a unique :py:class:`~meshctrl.files.Files` for this call, which will be cleaned up on return, else use cached or cache :py:class:`~meshctrl.files.Files` + + Returns: + Promise: {result: bool whether upload succeeded, size: number of bytes uploaded} + ''' raise NotImplementedError() - '''* Upload a stream to a device. This creates an _File and destroys it every call. If you need to upload multiple files, use {@link Session#file_explorer} instead. - * @param {string} nodeid - Unique id to upload stream to - * @param {ReadableStream} source - ReadableStream from which to read data - * @param {string} target - Path which to upload stream to on remote device - * @param {boolean} [unique_file_tunnel=False] - true: Create a unique {@link _Files} for this call, which will be cleaned up on return, else use cached or cache {@link _Files} - * @returns {Promise} - {result: bool whether upload succeeded, size: number of bytes uploaded} - ''' - async def upload(nodeid, source, target, unique_file_tunnel=False): + async def upload_file(self, nodeid, filepath, target, unique_file_tunnel=False): + ''' + Friendly wrapper around :py:class:`~meshctrl.session.Session.upload` to upload from a filepath. Creates a ReadableStream and calls upload. + + Args: + nodeid (str): Unique id to upload file to + filepath (str): Path from which to read the data + target (str): Path which to upload file to on remote device + unique_file_tunnel (bool): True: Create a unique :py:class:`~meshctrl.files.Files` for this call, which will be cleaned up on return, else use cached or cache :py:class:`~meshctrl.files.Files` + + Returns: + dict: {result: bool whether upload succeeded, size: number of bytes uploaded} + ''' raise NotImplementedError() - '''* Friendly wrapper around {@link Session#upload} to upload from a filepath. Creates a ReadableStream and calls upload. - * @param {string} nodeid - Unique id to upload file to - * @param {string} filepath - Path from which to read the data - * @param {string} target - Path which to upload file to on remote device - * @param {boolean} [unique_file_tunnel=False] - true: Create a unique {@link _Files} for this call, which will be cleaned up on return, else use cached or cache {@link _Files} - * @returns {Promise} - {result: bool whether upload succeeded, size: number of bytes uploaded} - ''' - async def upload_file(nodeid, filepath, target, unique_file_tunnel=False): + async def download(self, nodeid, source, target=None, unique_file_tunnel=False): + ''' + Download a file from a device into a writable stream. This creates an :py:class:`~meshctrl.files.Files` and destroys it every call. If you need to upload multiple files, use :py:class:`~meshctrl.session.Session.file_explorer` instead. + + Args: + nodeid (str): Unique id to download file from + source (str): Path from which to download from device + target (WritableStream): Stream to which to write data. If None, create new PassThrough stream which is both readable and writable. + unique_file_tunnel (bool): True: Create a unique :py:class:`~meshctrl.files.Files` for this call, which will be cleaned up on return, else use cached or cache :py:class:`~meshctrl.files.Files` + + Returns: + WritableStream: The stream which has been downloaded into + + Raises: + Exception: String showing the intermediate outcome and how many bytes were downloaded + ''' raise NotImplementedError() - '''* Download a file from a device into a writable stream. This creates an _File and destroys it every call. If you need to upload multiple files, use {@link Session#file_explorer} instead. - * @param {string} nodeid - Unique id to download file from - * @param {string} source - Path from which to download from device - * @param {WritableStream} [target=None] - Stream to which to write data. If None, create new PassThrough stream which is both readable and writable. - * @param {boolean} [unique_file_tunnel=False] - true: Create a unique {@link _Files} for this call, which will be cleaned up on return, else use cached or cache {@link _Files} - * @returns {Promise} The stream which has been downloaded into - * @throws {Error} String showing the intermediate outcome and how many bytes were downloaded - ''' - async def download(nodeid, source, target=None, unique_file_tunnel=False): + async def download_file(self, nodeid, source, filepath, unique_file_tunnel=False): + ''' + Friendly wrapper around :py:class:`~meshctrl.session.Session.download` to download to a filepath. Creates a WritableStream and calls download. + + Args: + nodeid (str): Unique id to download file from + source (str): Path from which to download from device + filepath (str): Path to which to download data + unique_file_tunnel (bool): True: Create a unique :py:class:`~meshctrl.files.Files` for this call, which will be cleaned up on return, else use cached or cache :py:class:`~meshctrl.files.Files` + + Returns: + WritableStream: The stream which has been downloaded into + ''' raise NotImplementedError() - '''* Friendly wrapper around {@link Session#download} to download to a filepath. Creates a WritableStream and calls download. - * @param {string} nodeid - Unique id to download file from - * @param {string} source - Path from which to download from device - * @param {string} filepath - Path to which to download data - * @param {boolean} [unique_file_tunnel=False] - true: Create a unique {@link _Files} for this call, which will be cleaned up on return, else use cached or cache {@link _Files} - * @returns {Promise} The stream which has been downloaded into - ''' - async def download_file(nodeid, source, filepath, unique_file_tunnel=False): - raise NotImplementedError() + async def file_explorer(self, nodeid, unique=False): + ''' + Create, initialize, and return an :py:class:`~meshctrl.files.Files` object for the given node - '''* Create, initialize, and return an _File object for the given node - * @param {string} nodeid - Unique id on which to open file explorer - * @param {boolean} [unique=False] - true: Create a unique {@link _Files}. Caller is responsible for cleanup. False: Use a cached {@link _Files} if available, otherwise create and cache. - * @returns {Promise<_Files>} A newly initialized file explorer. - ''' - async def file_explorer(nodeid, unique=False): + Args: + nodeid (str): Unique id on which to open file explorer + unique (bool): True: Create a unique :py:class:`~meshctrl.files.Files`. Caller is responsible for cleanup. False: Use a cached :py:class:`~meshctrl.files.Files` if available, otherwise create and cache. + + Returns: + :py:class:`~meshctrl.files.Files`: A newly initialized file explorer. + ''' raise NotImplementedError() \ No newline at end of file diff --git a/src/meshctrl/shell.py b/src/meshctrl/shell.py new file mode 100644 index 0000000..0fbd158 --- /dev/null +++ b/src/meshctrl/shell.py @@ -0,0 +1,7 @@ +from . import tunnel + +class Shell(tunnel.Tunnel): + pass + +class SmartShell(tunnel.Tunnel): + pass \ No newline at end of file diff --git a/src/meshctrl/tunnel.py b/src/meshctrl/tunnel.py new file mode 100644 index 0000000..2098af1 --- /dev/null +++ b/src/meshctrl/tunnel.py @@ -0,0 +1,2 @@ +class Tunnel(object): + pass \ No newline at end of file diff --git a/src/meshctrl/util.py b/src/meshctrl/util.py index 67df02a..4b65bda 100644 --- a/src/meshctrl/util.py +++ b/src/meshctrl/util.py @@ -4,7 +4,7 @@ from cryptography.hazmat.primitives.ciphers.aead import AESGCM import json import base64 -def encode_cookie(o, key): +def _encode_cookie(o, key): o["time"] = int(time.time()); # Add the cookie creation time iv = secrets.token_bytes(12) key = AESGCM(key) @@ -12,17 +12,41 @@ def encode_cookie(o, key): return base64.b64encode(crypted).replace("+", '@').replace("/", '$'); class Eventer(object): + """ + Eventer object to allow pub/sub interactions with a Session object + """ def __init__(self): self._ons = {} self._onces = {} def on(self, event, func): + """ + Subscribe to `event`. `func` will be called when that event is emitted. + + Args: + event (str): Event name to subscribe to + func (function(data: object)): Function to call when event is emitted. `data` could be of any type. Also used as a key to remove this subscription. + """ self._ons.setdefault(event, set()).add(func) def once(self, event, func): + """ + Subscribe to `event` once. `func` will be called when that event is emitted. The binding will then be removed. + + Args: + event (str): Event name to subscribe to + func (function(data: object)): Function to call when event is emitted. `data` could be of any type. Also used as a key to remove this subscription. + """ self._onces.setdefault(event, set()).add(func) def off(self, event, func): + """ + Unsubscribe from `event`. `func` is the object originally passed during the bind. + + Args: + event (str): Event name to unsubscribe from + func (object): Function which was originally passed when subscribing. + """ try: self._onces.setdefault(event, set()).remove(func) except KeyError: @@ -33,6 +57,13 @@ class Eventer(object): pass def emit(self, event, data): + """ + Emit `event` with `data`. All subscribed functions will be called (order is nonsensical). + + Args: + event (str): Event name emit + data (object): Data to pass to all the bound functions + """ for f in self._onces.get(event, []): f(data) try: diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..e8a5039 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,10 @@ +""" + Dummy conftest.py for meshctrl. + + If you don't know what this is for, just leave it empty. + Read more about conftest.py under: + - https://docs.pytest.org/en/stable/fixture.html + - https://docs.pytest.org/en/stable/writing_plugins.html +""" + +# import pytest diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..69f8159 --- /dev/null +++ b/tox.ini @@ -0,0 +1,93 @@ +# Tox configuration file +# Read more under https://tox.wiki/ +# THIS SCRIPT IS SUPPOSED TO BE AN EXAMPLE. MODIFY IT ACCORDING TO YOUR NEEDS! + +[tox] +minversion = 3.24 +envlist = default +isolated_build = True + + +[testenv] +description = Invoke pytest to run automated tests +setenv = + TOXINIDIR = {toxinidir} +passenv = + HOME + SETUPTOOLS_* +extras = + testing +commands = + pytest {posargs} + + +# # To run `tox -e lint` you need to make sure you have a +# # `.pre-commit-config.yaml` file. See https://pre-commit.com +# [testenv:lint] +# description = Perform static analysis and style checks +# skip_install = True +# deps = pre-commit +# passenv = +# HOMEPATH +# PROGRAMDATA +# SETUPTOOLS_* +# commands = +# pre-commit run --all-files {posargs:--show-diff-on-failure} + + +[testenv:{build,clean}] +description = + build: Build the package in isolation according to PEP517, see https://github.com/pypa/build + clean: Remove old distribution files and temporary build artifacts (./build and ./dist) +# https://setuptools.pypa.io/en/stable/build_meta.html#how-to-use-it +skip_install = True +changedir = {toxinidir} +deps = + build: build[virtualenv] +passenv = + SETUPTOOLS_* +commands = + clean: python -c 'import shutil; [shutil.rmtree(p, True) for p in ("build", "dist", "docs/_build")]' + clean: python -c 'import pathlib, shutil; [shutil.rmtree(p, True) for p in pathlib.Path("src").glob("*.egg-info")]' + build: python -m build {posargs} +# By default, both `sdist` and `wheel` are built. If your sdist is too big or you don't want +# to make it available, consider running: `tox -e build -- --wheel` + + +[testenv:{docs,doctests,linkcheck}] +description = + docs: Invoke sphinx-build to build the docs + doctests: Invoke sphinx-build to run doctests + linkcheck: Check for broken links in the documentation +passenv = + SETUPTOOLS_* +setenv = + DOCSDIR = {toxinidir}/docs + BUILDDIR = {toxinidir}/docs/_build + docs: BUILD = html + doctests: BUILD = doctest + linkcheck: BUILD = linkcheck +deps = + -r {toxinidir}/docs/requirements.txt + # ^ requirements.txt shared with Read The Docs +commands = + sphinx-build --color -b {env:BUILD} -d "{env:BUILDDIR}/doctrees" "{env:DOCSDIR}" "{env:BUILDDIR}/{env:BUILD}" {posargs} + + +[testenv:publish] +description = + Publish the package you have been developing to a package index server. + By default, it uses testpypi. If you really want to publish your package + to be publicly accessible in PyPI, use the `-- --repository pypi` option. +skip_install = True +changedir = {toxinidir} +passenv = + # See: https://twine.readthedocs.io/en/latest/ + TWINE_USERNAME + TWINE_PASSWORD + TWINE_REPOSITORY + TWINE_REPOSITORY_URL +deps = twine +commands = + python -m twine check dist/* + python -m twine upload {posargs:--repository {env:TWINE_REPOSITORY:testpypi}} dist/*