├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── docs ├── Makefile └── source │ ├── conf.py │ └── index.rst ├── setup.cfg ├── setup.py ├── syncthing ├── __init__.py └── meta.py └── tests ├── test_parse_datetime.py ├── test_scan_folder.py └── test_syncthing.py /.gitignore: -------------------------------------------------------------------------------- 1 | # local 2 | .idea 3 | syncthing/private_* 4 | syncthing/settings/local* 5 | scratch.py 6 | .pypirc 7 | docs/build 8 | 9 | 10 | # Byte-compiled / optimized / DLL files 11 | __pycache__/ 12 | *.py[cod] 13 | 14 | # C extensions 15 | *.so 16 | 17 | # Distribution / packaging 18 | .Python 19 | env/ 20 | build/ 21 | develop-eggs/ 22 | dist/ 23 | downloads/ 24 | eggs/ 25 | .eggs/ 26 | lib/ 27 | lib64/ 28 | parts/ 29 | sdist/ 30 | var/ 31 | *.egg-info/ 32 | .installed.cfg 33 | *.egg 34 | 35 | # PyInstaller 36 | # Usually these files are written by a python script from a template 37 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 38 | *.manifest 39 | *.spec 40 | 41 | # Installer logs 42 | pip-log.txt 43 | pip-delete-this-directory.txt 44 | 45 | # Unit test / coverage reports 46 | htmlcov/ 47 | .tox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *,cover 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | 62 | # Sphinx documentation 63 | docs/_build/ 64 | 65 | # PyBuilder 66 | target/ 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2017 Blake VandeMerwe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | ~ 24 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.md LICENSE VERSION requirements.txt 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | python-syncthing 2 | ================ 3 | 4 | [![pypi](https://img.shields.io/pypi/v/syncthing.svg?style=flat)](https://pypi.python.org/pypi/syncthing) 5 | [![Syncthing](https://img.shields.io/badge/syncthing-0.14.44-blue.svg?style=flat)](https://syncthing.net) 6 | [![Documentation Status](https://readthedocs.org/projects/python-syncthing/badge/?version=latest)](http://python-syncthing.readthedocs.io/en/latest/?badge=latest) 7 | [![MIT License](https://img.shields.io/github/license/blakev/python-syncthing.svg?style=flat)](https://github.com/blakev/python-syncthing/blob/master/LICENSE) 8 | 9 | 10 | Python bindings to the Syncthing REST interface. 11 | 12 | - [Python API Documentation](http://python-syncthing.readthedocs.io/en/latest/) 13 | - [Syncthing](https://syncthing.net/) 14 | - [Syncthing REST Documentation](https://docs.syncthing.net/dev/rest.html) 15 | - [Syncthing Forums](https://forum.syncthing.net/) 16 | 17 | 18 | ```bash 19 | $ pip install syncthing 20 | ``` 21 | 22 | ## Getting Started 23 | 24 | ```python 25 | from syncthing import Syncthing 26 | 27 | API_KEY = "..." 28 | 29 | s = Syncthing(API_KEY) 30 | 31 | # name spaced by API endpoints 32 | s.system.connections() 33 | 34 | # supports GET/POST semantics 35 | sync_errors = s.system.errors() 36 | s.system.clear() 37 | 38 | if sync_errors: 39 | for e in sync_errors: 40 | print(e) 41 | 42 | # supports event long-polling 43 | event_stream = s.events(limit=10) 44 | for event in event_stream: 45 | # do something with `event` 46 | if event_stream.count > 100: 47 | event_stream.stop() 48 | ``` 49 | 50 | ## Running Tests 51 | 52 | The API doctests rely on the following function to run against your instance. 53 | None of the "breaking" calls will be tested. You must set the following environment 54 | variables otherwise all tests will fail. 55 | 56 | ```python 57 | def _syncthing(): 58 | KEY = os.getenv('SYNCTHING_API_KEY') 59 | HOST = os.getenv('SYNCTHING_HOST', '127.0.0.1') 60 | PORT = os.getenv('SYNCTHING_PORT', 8384) 61 | IS_HTTPS = bool(int(os.getenv('SYNCTHING_HTTPS', '0'))) 62 | SSL_CERT_FILE = os.getenv('SYNCTHING_CERT_FILE') 63 | return Syncthing(KEY, HOST, PORT, 10.0, IS_HTTPS, SSL_CERT_FILE) 64 | ``` 65 | 66 | ## License 67 | 68 | > The MIT License (MIT) 69 | > 70 | > Copyright (c) 2015-2017 Blake VandeMerwe 71 | > 72 | > Permission is hereby granted, free of charge, to any person obtaining a copy 73 | > of this software and associated documentation files (the "Software"), to deal 74 | > in the Software without restriction, including without limitation the rights 75 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 76 | > copies of the Software, and to permit persons to whom the Software is 77 | > furnished to do so, subject to the following conditions: 78 | > The above copyright notice and this permission notice shall be included in all 79 | > copies or substantial portions of the Software. 80 | > 81 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 82 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 83 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 84 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 85 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 86 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 87 | > SOFTWARE. 88 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don\'t have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 21 | 22 | .PHONY: help 23 | help: 24 | @echo "Please use \`make ' where is one of" 25 | @echo " html to make standalone HTML files" 26 | @echo " dirhtml to make HTML files named index.html in directories" 27 | @echo " singlehtml to make a single large HTML file" 28 | @echo " pickle to make pickle files" 29 | @echo " json to make JSON files" 30 | @echo " htmlhelp to make HTML files and a HTML help project" 31 | @echo " qthelp to make HTML files and a qthelp project" 32 | @echo " applehelp to make an Apple Help Book" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " epub3 to make an epub3" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | 51 | .PHONY: clean 52 | clean: 53 | rm -rf $(BUILDDIR)/* 54 | 55 | .PHONY: html 56 | html: 57 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 58 | @echo 59 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 60 | 61 | .PHONY: dirhtml 62 | dirhtml: 63 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 64 | @echo 65 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 66 | 67 | .PHONY: singlehtml 68 | singlehtml: 69 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 70 | @echo 71 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 72 | 73 | .PHONY: pickle 74 | pickle: 75 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 76 | @echo 77 | @echo "Build finished; now you can process the pickle files." 78 | 79 | .PHONY: json 80 | json: 81 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 82 | @echo 83 | @echo "Build finished; now you can process the JSON files." 84 | 85 | .PHONY: htmlhelp 86 | htmlhelp: 87 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 88 | @echo 89 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 90 | ".hhp project file in $(BUILDDIR)/htmlhelp." 91 | 92 | .PHONY: qthelp 93 | qthelp: 94 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 95 | @echo 96 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 97 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 98 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/python-syncthing.qhcp" 99 | @echo "To view the help file:" 100 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/python-syncthing.qhc" 101 | 102 | .PHONY: applehelp 103 | applehelp: 104 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 105 | @echo 106 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 107 | @echo "N.B. You won't be able to view it unless you put it in" \ 108 | "~/Library/Documentation/Help or install it in your application" \ 109 | "bundle." 110 | 111 | .PHONY: devhelp 112 | devhelp: 113 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 114 | @echo 115 | @echo "Build finished." 116 | @echo "To view the help file:" 117 | @echo "# mkdir -p $$HOME/.local/share/devhelp/python-syncthing" 118 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/python-syncthing" 119 | @echo "# devhelp" 120 | 121 | .PHONY: epub 122 | epub: 123 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 124 | @echo 125 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 126 | 127 | .PHONY: epub3 128 | epub3: 129 | $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 130 | @echo 131 | @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." 132 | 133 | .PHONY: latex 134 | latex: 135 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 136 | @echo 137 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 138 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 139 | "(use \`make latexpdf' here to do that automatically)." 140 | 141 | .PHONY: latexpdf 142 | latexpdf: 143 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 144 | @echo "Running LaTeX files through pdflatex..." 145 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 146 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 147 | 148 | .PHONY: latexpdfja 149 | latexpdfja: 150 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 151 | @echo "Running LaTeX files through platex and dvipdfmx..." 152 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 153 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 154 | 155 | .PHONY: text 156 | text: 157 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 158 | @echo 159 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 160 | 161 | .PHONY: man 162 | man: 163 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 164 | @echo 165 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 166 | 167 | .PHONY: texinfo 168 | texinfo: 169 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 170 | @echo 171 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 172 | @echo "Run \`make' in that directory to run these through makeinfo" \ 173 | "(use \`make info' here to do that automatically)." 174 | 175 | .PHONY: info 176 | info: 177 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 178 | @echo "Running Texinfo files through makeinfo..." 179 | make -C $(BUILDDIR)/texinfo info 180 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 181 | 182 | .PHONY: gettext 183 | gettext: 184 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 185 | @echo 186 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 187 | 188 | .PHONY: changes 189 | changes: 190 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 191 | @echo 192 | @echo "The overview file is in $(BUILDDIR)/changes." 193 | 194 | .PHONY: linkcheck 195 | linkcheck: 196 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 197 | @echo 198 | @echo "Link check complete; look for any errors in the above output " \ 199 | "or in $(BUILDDIR)/linkcheck/output.txt." 200 | 201 | .PHONY: doctest 202 | doctest: 203 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 204 | @echo "Testing of doctests in the sources finished, look at the " \ 205 | "results in $(BUILDDIR)/doctest/output.txt." 206 | 207 | .PHONY: coverage 208 | coverage: 209 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 210 | @echo "Testing of coverage in the sources finished, look at the " \ 211 | "results in $(BUILDDIR)/coverage/python.txt." 212 | 213 | .PHONY: xml 214 | xml: 215 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 216 | @echo 217 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 218 | 219 | .PHONY: pseudoxml 220 | pseudoxml: 221 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 222 | @echo 223 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 224 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # python-syncthing documentation build configuration file, created by 4 | # sphinx-quickstart on Tue Jan 3 16:17:40 2017. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | 18 | # If extensions (or modules to document with autodoc) are in another directory, 19 | # add these directories to sys.path here. If the directory is relative to the 20 | # documentation root, use os.path.abspath to make it absolute, like shown here. 21 | #sys.path.insert(0, os.path.abspath('.')) 22 | sys.path.insert(0, os.path.abspath('../..')) 23 | sys.path.insert(0, os.path.abspath('_themes')) 24 | 25 | import syncthing 26 | from syncthing.meta import __version__, __author__, __title__ 27 | 28 | # -- General configuration ------------------------------------------------ 29 | 30 | import sphinx_rtd_theme 31 | 32 | html_theme = "sphinx_rtd_theme" 33 | 34 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 35 | 36 | # If your documentation needs a minimal Sphinx version, state it here. 37 | #needs_sphinx = '1.0' 38 | 39 | # Add any Sphinx extension module names here, as strings. They can be 40 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 41 | # ones. 42 | extensions = [ 43 | 'sphinx.ext.autodoc', 44 | 'sphinx.ext.doctest', 45 | 'sphinx.ext.intersphinx', 46 | 'sphinx.ext.coverage', 47 | 'sphinx.ext.viewcode', 48 | 'sphinxcontrib.napoleon', 49 | ] 50 | 51 | napoleon_google_docstring = True 52 | 53 | # Add any paths that contain templates here, relative to this directory. 54 | templates_path = ['_templates'] 55 | 56 | # The suffix(es) of source filenames. 57 | # You can specify multiple suffix as a list of string: 58 | # source_suffix = ['.rst', '.md'] 59 | source_suffix = '.rst' 60 | 61 | # The encoding of source files. 62 | #source_encoding = 'utf-8-sig' 63 | 64 | # The master toctree document. 65 | master_doc = 'index' 66 | 67 | # General information about the project. 68 | project = __title__ 69 | copyright = u'2017, Blake VandeMerwe' 70 | author = __author__ 71 | 72 | # The version info for the project you're documenting, acts as replacement for 73 | # |version| and |release|, also used in various other places throughout the 74 | # built documents. 75 | # 76 | # The short X.Y version. 77 | version = __version__ 78 | # The full version, including alpha/beta/rc tags. 79 | release = __version__ 80 | 81 | # The language for content autogenerated by Sphinx. Refer to documentation 82 | # for a list of supported languages. 83 | # 84 | # This is also used if you do content translation via gettext catalogs. 85 | # Usually you set "language" from the command line for these cases. 86 | language = None 87 | 88 | # There are two options for replacing |today|: either, you set today to some 89 | # non-false value, then it is used: 90 | #today = '' 91 | # Else, today_fmt is used as the format for a strftime call. 92 | #today_fmt = '%B %d, %Y' 93 | 94 | # List of patterns, relative to source directory, that match files and 95 | # directories to ignore when looking for source files. 96 | # This patterns also effect to html_static_path and html_extra_path 97 | exclude_patterns = [] 98 | 99 | # The reST default role (used for this markup: `text`) to use for all 100 | # documents. 101 | #default_role = None 102 | 103 | # If true, '()' will be appended to :func: etc. cross-reference text. 104 | #add_function_parentheses = True 105 | 106 | # If true, the current module name will be prepended to all description 107 | # unit titles (such as .. function::). 108 | #add_module_names = True 109 | 110 | # If true, sectionauthor and moduleauthor directives will be shown in the 111 | # output. They are ignored by default. 112 | #show_authors = False 113 | 114 | # The name of the Pygments (syntax highlighting) style to use. 115 | pygments_style = 'sphinx' 116 | 117 | # A list of ignored prefixes for module index sorting. 118 | #modindex_common_prefix = [] 119 | 120 | # If true, keep warnings as "system message" paragraphs in the built documents. 121 | #keep_warnings = False 122 | 123 | todo_include_todos = False 124 | 125 | 126 | # -- Options for HTML output ---------------------------------------------- 127 | 128 | # The theme to use for HTML and HTML Help pages. See the documentation for 129 | # a list of builtin themes. 130 | 131 | # Theme options are theme-specific and customize the look and feel of a theme 132 | # further. For a list of options available for each theme, see the 133 | # documentation. 134 | #html_theme_options = {} 135 | 136 | # Add any paths that contain custom themes here, relative to this directory. 137 | #html_theme_path = [] 138 | 139 | # The name for this set of Sphinx documents. 140 | # " v documentation" by default. 141 | #html_title = u'python-syncthing v2.0.0' 142 | 143 | # A shorter title for the navigation bar. Default is the same as html_title. 144 | #html_short_title = None 145 | 146 | # The name of an image file (relative to this directory) to place at the top 147 | # of the sidebar. 148 | #html_logo = None 149 | 150 | # The name of an image file (relative to this directory) to use as a favicon of 151 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 152 | # pixels large. 153 | #html_favicon = None 154 | 155 | # Add any paths that contain custom static files (such as style sheets) here, 156 | # relative to this directory. They are copied after the builtin static files, 157 | # so a file named "default.css" will overwrite the builtin "default.css". 158 | html_static_path = ['_static'] 159 | 160 | # Add any extra paths that contain custom files (such as robots.txt or 161 | # .htaccess) here, relative to this directory. These files are copied 162 | # directly to the root of the documentation. 163 | #html_extra_path = [] 164 | 165 | # If not None, a 'Last updated on:' timestamp is inserted at every page 166 | # bottom, using the given strftime format. 167 | # The empty string is equivalent to '%b %d, %Y'. 168 | #html_last_updated_fmt = None 169 | 170 | # If true, SmartyPants will be used to convert quotes and dashes to 171 | # typographically correct entities. 172 | #html_use_smartypants = True 173 | 174 | # Custom sidebar templates, maps document names to template names. 175 | #html_sidebars = {} 176 | 177 | # Additional templates that should be rendered to pages, maps page names to 178 | # template names. 179 | #html_additional_pages = {} 180 | 181 | # If false, no module index is generated. 182 | #html_domain_indices = True 183 | 184 | # If false, no index is generated. 185 | #html_use_index = True 186 | 187 | # If true, the index is split into individual pages for each letter. 188 | #html_split_index = False 189 | 190 | # If true, links to the reST sources are added to the pages. 191 | #html_show_sourcelink = True 192 | 193 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 194 | #html_show_sphinx = True 195 | 196 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 197 | #html_show_copyright = True 198 | 199 | # If true, an OpenSearch description file will be output, and all pages will 200 | # contain a tag referring to it. The value of this option must be the 201 | # base URL from which the finished HTML is served. 202 | #html_use_opensearch = '' 203 | 204 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 205 | #html_file_suffix = None 206 | 207 | # Language to be used for generating the HTML full-text search index. 208 | # Sphinx supports the following languages: 209 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' 210 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh' 211 | #html_search_language = 'en' 212 | 213 | # A dictionary with options for the search language support, empty by default. 214 | # 'ja' uses this config value. 215 | # 'zh' user can custom change `jieba` dictionary path. 216 | #html_search_options = {'type': 'default'} 217 | 218 | # The name of a javascript file (relative to the configuration directory) that 219 | # implements a search results scorer. If empty, the default will be used. 220 | #html_search_scorer = 'scorer.js' 221 | 222 | # Output file base name for HTML help builder. 223 | htmlhelp_basename = 'python-syncthingdoc' 224 | 225 | # -- Options for LaTeX output --------------------------------------------- 226 | 227 | latex_elements = { 228 | # The paper size ('letterpaper' or 'a4paper'). 229 | #'papersize': 'letterpaper', 230 | 231 | # The font size ('10pt', '11pt' or '12pt'). 232 | #'pointsize': '10pt', 233 | 234 | # Additional stuff for the LaTeX preamble. 235 | #'preamble': '', 236 | 237 | # Latex figure (float) alignment 238 | #'figure_align': 'htbp', 239 | } 240 | 241 | # Grouping the document tree into LaTeX files. List of tuples 242 | # (source start file, target name, title, 243 | # author, documentclass [howto, manual, or own class]). 244 | latex_documents = [ 245 | (master_doc, 'python-syncthing.tex', u'python-syncthing Documentation', 246 | u'Blake VandeMerwe', 'manual'), 247 | ] 248 | 249 | # The name of an image file (relative to this directory) to place at the top of 250 | # the title page. 251 | #latex_logo = None 252 | 253 | # For "manual" documents, if this is true, then toplevel headings are parts, 254 | # not chapters. 255 | #latex_use_parts = False 256 | 257 | # If true, show page references after internal links. 258 | #latex_show_pagerefs = False 259 | 260 | # If true, show URL addresses after external links. 261 | #latex_show_urls = False 262 | 263 | # Documents to append as an appendix to all manuals. 264 | #latex_appendices = [] 265 | 266 | # If false, no module index is generated. 267 | #latex_domain_indices = True 268 | 269 | 270 | # -- Options for manual page output --------------------------------------- 271 | 272 | # One entry per manual page. List of tuples 273 | # (source start file, name, description, authors, manual section). 274 | man_pages = [ 275 | (master_doc, 'python-syncthing', u'python-syncthing Documentation', 276 | [author], 1) 277 | ] 278 | 279 | # If true, show URL addresses after external links. 280 | #man_show_urls = False 281 | 282 | 283 | # -- Options for Texinfo output ------------------------------------------- 284 | 285 | # Grouping the document tree into Texinfo files. List of tuples 286 | # (source start file, target name, title, author, 287 | # dir menu entry, description, category) 288 | texinfo_documents = [ 289 | (master_doc, 'python-syncthing', u'python-syncthing Documentation', 290 | author, 'python-syncthing', 'Python bindings to the Syncthing REST interface.', 291 | 'Miscellaneous'), 292 | ] 293 | 294 | # Documents to append as an appendix to all manuals. 295 | #texinfo_appendices = [] 296 | 297 | # If false, no module index is generated. 298 | #texinfo_domain_indices = True 299 | 300 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 301 | #texinfo_show_urls = 'footnote' 302 | 303 | # If true, do not generate a @detailmenu in the "Top" node's menu. 304 | #texinfo_no_detailmenu = False 305 | 306 | 307 | # Example configuration for intersphinx: refer to the Python standard library. 308 | intersphinx_mapping = {'py': 'http://python.readthedocs.io/en/latest/objects.inv'} 309 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. python-syncthing documentation master file, created by 2 | sphinx-quickstart on Tue Jan 3 16:17:40 2017. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | python-syncthing 7 | ================ 8 | 9 | Python bindings to the Syncthing REST interface. 10 | 11 | - `Syncthing `_ 12 | - `Syncthing REST Documentation `_ 13 | - `Syncthing Forums `_ 14 | - `Pypi `_ (``syncthing``) 15 | 16 | Reference 17 | --------- 18 | 19 | - `Module`_ 20 | - `System Endpoints`_ 21 | - `Database Endpoints`_ 22 | - `Events Endpoints`_ 23 | - `Statistic Endpoints`_ 24 | - `Misc. Endpoints`_ 25 | - `Running Tests`_ 26 | - `License`_ 27 | 28 | 29 | Module 30 | ------ 31 | 32 | .. automodule:: syncthing 33 | :members: 34 | 35 | System Endpoints 36 | ---------------- 37 | 38 | .. autoclass:: syncthing.System() 39 | :members: 40 | :undoc-members: 41 | 42 | Database Endpoints 43 | ------------------ 44 | 45 | .. autoclass:: syncthing.Database() 46 | :members: 47 | :undoc-members: 48 | 49 | Events Endpoints 50 | ---------------- 51 | 52 | .. autoclass:: syncthing.Events() 53 | :members: 54 | :undoc-members: 55 | 56 | Statistic Endpoints 57 | ------------------- 58 | 59 | .. autoclass:: syncthing.Statistics() 60 | :members: 61 | :undoc-members: 62 | 63 | Misc. Endpoints 64 | --------------- 65 | 66 | .. autoclass:: syncthing.Misc() 67 | :members: 68 | :undoc-members: 69 | 70 | 71 | Running Tests 72 | ------------- 73 | 74 | The API doctests rely on the following function to run against your instance. 75 | None of the "breaking" calls will be tested. You must set the following environment 76 | variables otherwise all tests will fail. 77 | 78 | .. code:: 79 | 80 | def _syncthing(): 81 | KEY = os.getenv('SYNCTHING_API_KEY') 82 | HOST = os.getenv('SYNCTHING_HOST', '127.0.0.1') 83 | PORT = os.getenv('SYNCTHING_PORT', 8384) 84 | IS_HTTPS = bool(int(os.getenv('SYNCTHING_HTTPS', '0'))) 85 | SSL_CERT_FILE = os.getenv('SYNCTHING_CERT_FILE') 86 | return Syncthing(KEY, HOST, PORT, 10.0, IS_HTTPS, SSL_CERT_FILE) 87 | 88 | 89 | License 90 | ------- 91 | 92 | The MIT License (MIT) 93 | 94 | Copyright (c) 2015-2016 Blake VandeMerwe 95 | 96 | Permission is hereby granted, free of charge, to any person obtaining a copy 97 | of this software and associated documentation files (the "Software"), to deal 98 | in the Software without restriction, including without limitation the rights 99 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 100 | copies of the Software, and to permit persons to whom the Software is 101 | furnished to do so, subject to the following conditions: 102 | 103 | The above copyright notice and this permission notice shall be included in all 104 | copies or substantial portions of the Software. 105 | 106 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 107 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 108 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 109 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 110 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 111 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 112 | SOFTWARE. 113 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | from setuptools import setup 5 | 6 | setup( 7 | name = 'syncthing', 8 | version = '2.4.3', 9 | author = 'Blake VandeMerwe', 10 | author_email = 'blakev@null.net', 11 | description = 'Python bindings to the Syncthing REST interface, targeting v0.14.44', 12 | url = 'https://github.com/blakev/python-syncthing', 13 | license = 'The MIT License', 14 | install_requires = [ 15 | 'python-dateutil>=2.8.1,<=2.8.2', 16 | 'requests>=2.24.0,<=2.28.0' 17 | ], 18 | extras_require = { 19 | 'dev': [ 20 | 'sphinx', 21 | 'sphinxcontrib-napoleon', 22 | 'sphinx_rtd_theme' 23 | ] 24 | }, 25 | packages = [ 26 | 'syncthing' 27 | ], 28 | package_dir = { 29 | 'syncthing': 'syncthing' 30 | }, 31 | include_package_data = True, 32 | zip_safe = True, 33 | keywords = 'syncthing,sync,rest,backup,api', 34 | classifiers=[ 35 | 'Development Status :: 5 - Production/Stable', 36 | 'Intended Audience :: Developers', 37 | 'Natural Language :: English', 38 | 'License :: OSI Approved :: MIT License', 39 | 'Programming Language :: Python', 40 | 'Programming Language :: Python :: 2', 41 | 'Programming Language :: Python :: 2.7', 42 | 'Programming Language :: Python :: 3', 43 | 'Programming Language :: Python :: 3.7', 44 | 'Programming Language :: Python :: 3.8', 45 | 'Topic :: System :: Archiving :: Mirroring' 46 | ], 47 | ) 48 | -------------------------------------------------------------------------------- /syncthing/__init__.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # >> 4 | # Copyright (c) 2016-2017, Blake VandeMerwe 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining 7 | # a copy of this software and associated documentation files 8 | # (the "Software"), to deal in the Software without restriction, 9 | # including without limitation the rights to use, copy, modify, merge, 10 | # publish, distribute, sublicense, and/or sell copies of the Software, 11 | # and to permit persons to whom the Software is furnished to do so, subject 12 | # to the following conditions: The above copyright notice and this permission 13 | # notice shall be included in all copies or substantial portions 14 | # of the Software. 15 | # 16 | # python-syncthing, 2016 17 | # << 18 | from __future__ import unicode_literals 19 | 20 | import os 21 | import sys 22 | import json 23 | import logging 24 | import warnings 25 | from collections import namedtuple 26 | 27 | import requests 28 | from dateutil.parser import parse as dateutil_parser 29 | from requests.exceptions import Timeout 30 | from urllib3.exceptions import TimeoutError 31 | 32 | PY2 = sys.version_info[0] < 3 33 | 34 | if PY2: 35 | string_types = (basestring, str, unicode,) 36 | def reraise(msg, base): 37 | raise Syncthing(msg) 38 | 39 | else: 40 | string_types = (str,) 41 | def reraise(msg, exc): 42 | raise SyncthingError(msg) from exc 43 | 44 | logger = logging.getLogger(__name__) 45 | 46 | 47 | NoneType = type(None) 48 | DEFAULT_TIMEOUT = 10.0 49 | 50 | __all__ = ['SyncthingError', 'ErrorEvent', 'BaseAPI', 'System', 51 | 'Database', 'Statistics', 'Syncthing', 52 | # methods 53 | 'keys_to_datetime', 'parse_datetime'] 54 | 55 | ErrorEvent = namedtuple('ErrorEvent', 'when, message') 56 | """tuple[datetime.datetime,str]: used to process error lists more easily, 57 | instead of by two-key dictionaries. """ 58 | 59 | def _syncthing(): 60 | KEY = os.getenv('SYNCTHING_API_KEY') 61 | HOST = os.getenv('SYNCTHING_HOST', '127.0.0.1') 62 | PORT = os.getenv('SYNCTHING_PORT', 8384) 63 | IS_HTTPS = bool(int(os.getenv('SYNCTHING_HTTPS', '0'))) 64 | SSL_CERT_FILE = os.getenv('SYNCTHING_CERT_FILE') 65 | return Syncthing(KEY, HOST, PORT, 10.0, IS_HTTPS, SSL_CERT_FILE) 66 | 67 | 68 | def keys_to_datetime(obj, *keys): 69 | """ Converts all the keys in an object to DateTime instances. 70 | 71 | Args: 72 | obj (dict): the JSON-like ``dict`` object to modify inplace. 73 | keys (str): keys of the object being converted into DateTime 74 | instances. 75 | 76 | Returns: 77 | dict: ``obj`` inplace. 78 | 79 | >>> keys_to_datetime(None) is None 80 | True 81 | >>> keys_to_datetime({}) 82 | {} 83 | >>> a = {} 84 | >>> id(keys_to_datetime(a)) == id(a) 85 | True 86 | >>> a = {'one': '2016-06-06T19:41:43.039284', 87 | 'two': '2016-06-06T19:41:43.039284'} 88 | >>> keys_to_datetime(a) == a 89 | True 90 | >>> keys_to_datetime(a, 'one')['one'] 91 | datetime.datetime(2016, 6, 6, 19, 41, 43, 39284) 92 | >>> keys_to_datetime(a, 'one')['two'] 93 | '2016-06-06T19:41:43.039284' 94 | """ 95 | if not keys: 96 | return obj 97 | for k in keys: 98 | if k not in obj: 99 | continue 100 | v = obj[k] 101 | if not isinstance(v, string_types): 102 | continue 103 | obj[k] = parse_datetime(v) 104 | return obj 105 | 106 | 107 | def parse_datetime(s, **kwargs): 108 | """ Converts a time-string into a valid 109 | :py:class:`~datetime.datetime.DateTime` object. 110 | 111 | Args: 112 | s (str): string to be formatted. 113 | 114 | ``**kwargs`` is passed directly to :func:`.dateutil_parser`. 115 | 116 | Returns: 117 | :py:class:`~datetime.datetime.DateTime` 118 | """ 119 | if not s: 120 | return None 121 | try: 122 | ret = dateutil_parser(s, **kwargs) 123 | except (OverflowError, TypeError, ValueError) as e: 124 | reraise('datetime parsing error from %s' % s, e) 125 | return ret 126 | 127 | 128 | class SyncthingError(Exception): 129 | """Base Syncthing Exception class all non-assert errors will raise from.""" 130 | 131 | 132 | class BaseAPI(object): 133 | """ Placeholder for HTTP REST API URL prefix. """ 134 | 135 | prefix = '' 136 | 137 | def __init__(self, api_key, host='localhost', port=8384, 138 | timeout=DEFAULT_TIMEOUT, is_https=False, ssl_cert_file=None): 139 | 140 | if ssl_cert_file: 141 | if not os.path.exists(ssl_cert_file): 142 | raise SyncthingError( 143 | 'ssl_cert_file does not exist at location, %s' % 144 | ssl_cert_file) 145 | 146 | self.api_key = api_key 147 | self.host = host 148 | self.is_https = is_https 149 | self.port = port 150 | self.ssl_cert_file = ssl_cert_file 151 | self.timeout = timeout 152 | self.verify = True if ssl_cert_file or is_https else False 153 | self._headers = { 154 | 'X-API-Key': api_key 155 | } 156 | self.url = '{proto}://{host}:{port}'.format( 157 | proto='https' if is_https else 'http', host=host, port=port) 158 | self._base_url = self.url + '{endpoint}' 159 | 160 | def get(self, endpoint, data=None, headers=None, params=None, 161 | return_response=False, raw_exceptions=False): 162 | endpoint = self.prefix + endpoint 163 | return self._request('GET', endpoint, data, headers, params, 164 | return_response, raw_exceptions) 165 | 166 | def post(self, endpoint, data=None, headers=None, params=None, 167 | return_response=False, raw_exceptions=False): 168 | endpoint = self.prefix + endpoint 169 | return self._request('POST', endpoint, data, headers, params, 170 | return_response, raw_exceptions) 171 | 172 | def _request(self, method, endpoint, data=None, headers=None, params=None, 173 | return_response=False, raw_exceptions=False): 174 | method = method.upper() 175 | 176 | endpoint = self._base_url.format(endpoint=endpoint) 177 | 178 | if method not in ('GET', 'POST', 'PUT', 'DELETE'): 179 | raise SyncthingError( 180 | 'unsupported http verb requested, %s' % method) 181 | 182 | if data is None: 183 | data = {} 184 | assert isinstance(data, string_types) or isinstance(data, dict) 185 | 186 | if headers is None: 187 | headers = {} 188 | assert isinstance(headers, dict) 189 | 190 | headers.update(self._headers) 191 | 192 | try: 193 | resp = requests.request( 194 | method, 195 | endpoint, 196 | data=json.dumps(data), 197 | params=params, 198 | timeout=self.timeout, 199 | cert=self.ssl_cert_file, 200 | headers=headers 201 | ) 202 | 203 | if not return_response: 204 | resp.raise_for_status() 205 | 206 | except requests.RequestException as e: 207 | if raw_exceptions: 208 | raise e 209 | reraise('http request error', e) 210 | 211 | else: 212 | if return_response: 213 | return resp 214 | 215 | if resp.status_code != requests.codes.ok: 216 | logger.error('%d %s (%s): %s', resp.status_code, resp.reason, 217 | resp.url, resp.text) 218 | return resp 219 | 220 | if 'json' in resp.headers.get('Content-Type', 'text/plain')\ 221 | .lower(): 222 | json_data = resp.json() 223 | 224 | else: 225 | content = resp.content.decode('utf-8') 226 | if content and content[0] == '{' and content[-1] == '}': 227 | json_data = json.loads(content) 228 | 229 | else: 230 | return content 231 | 232 | if isinstance(json_data, dict) and json_data.get('error'): 233 | api_err = json_data.get('error') 234 | raise SyncthingError(api_err) 235 | return json_data 236 | 237 | 238 | class System(BaseAPI): 239 | """ HTTP REST endpoint for System calls.""" 240 | 241 | prefix = '/rest/system/' 242 | 243 | def browse(self, path=None): 244 | """ Returns a list of directories matching the path given. 245 | 246 | Args: 247 | path (str): glob pattern. 248 | 249 | Returns: 250 | List[str] 251 | """ 252 | params = None 253 | if path: 254 | assert isinstance(path, string_types) 255 | params = {'current': path} 256 | return self.get('browse', params=params) 257 | 258 | def config(self): 259 | """ Returns the current configuration. 260 | 261 | Returns: 262 | dict 263 | 264 | >>> s = _syncthing().system 265 | >>> config = s.config() 266 | >>> config 267 | ... # doctest: +ELLIPSIS 268 | {...} 269 | >>> 'version' in config and config['version'] >= 15 270 | True 271 | >>> 'folders' in config 272 | True 273 | >>> 'devices' in config 274 | True 275 | """ 276 | return self.get('config') 277 | 278 | def set_config(self, config, and_restart=False): 279 | """ Post the full contents of the configuration, in the same format as 280 | returned by :func:`.config`. The configuration will be saved to disk 281 | and the ``configInSync`` flag set to ``False``. Restart Syncthing to 282 | activate.""" 283 | assert isinstance(config, dict) 284 | self.post('config', data=config) 285 | if and_restart: 286 | self.restart() 287 | 288 | def config_insync(self): 289 | """ Returns whether the config is in sync, i.e. whether the running 290 | configuration is the same as that on disk. 291 | 292 | Returns: 293 | bool 294 | """ 295 | status = self.get('config/insync').get('configInSync', False) 296 | if status is None: 297 | status = False 298 | return status 299 | 300 | def connections(self): 301 | """ Returns the list of configured devices and some metadata 302 | associated with them. The list also contains the local device 303 | itself as not connected. 304 | 305 | Returns: 306 | dict 307 | 308 | >>> s = _syncthing().system 309 | >>> connections = s.connections() 310 | >>> sorted([k for k in connections.keys()]) 311 | ['connections', 'total'] 312 | >>> isinstance(connections['connections'], dict) 313 | True 314 | >>> isinstance(connections['total'], dict) 315 | True 316 | """ 317 | return self.get('connections') 318 | 319 | def debug(self): 320 | """ Returns the set of debug facilities and which of them are 321 | currently enabled. 322 | 323 | Returns: 324 | dict 325 | 326 | >>> s = _syncthing().system 327 | >>> debug = s.debug() 328 | >>> debug 329 | ... #doctest: +ELLIPSIS 330 | {...} 331 | >>> len(debug.keys()) 332 | 2 333 | >>> 'enabled' in debug and 'facilities' in debug 334 | True 335 | >>> isinstance(debug['enabled'], list) or debug['enabled'] is None 336 | True 337 | >>> isinstance(debug['facilities'], dict) 338 | True 339 | """ 340 | return self.get('debug') 341 | 342 | def disable_debug(self, *on): 343 | """ Disables debugging for specified facilities. 344 | 345 | Args: 346 | on (str): debugging points to apply ``disable``. 347 | 348 | Returns: 349 | None 350 | """ 351 | self.post('debug', params={'disable': ','.join(on)}) 352 | 353 | def enable_debug(self, *on): 354 | """ Enables debugging for specified facilities. 355 | 356 | Args: 357 | on (str): debugging points to apply ``enable``. 358 | 359 | Returns: 360 | None 361 | """ 362 | self.post('debug', params={'enable': ','.join(on)}) 363 | 364 | def discovery(self): 365 | """ Returns the contents of the local discovery cache. 366 | 367 | Returns: 368 | dict 369 | """ 370 | return self.get('discovery') 371 | 372 | def add_discovery(self, device, address): 373 | """ Add an entry to the discovery cache. 374 | 375 | Args: 376 | device (str): Device ID. 377 | address (str): destination address, a valid hostname or 378 | IP address that's serving a Syncthing instance. 379 | 380 | Returns: 381 | None 382 | """ 383 | self.post('discovery', params={'device': device, 384 | 'address': address}) 385 | 386 | def clear(self): 387 | """ Remove all recent errors. 388 | 389 | Returns: 390 | None 391 | """ 392 | self.post('error/clear') 393 | 394 | 395 | def clear_errors(self): 396 | """ Alias function for :meth:`.clear`. """ 397 | self.clear() 398 | 399 | def errors(self): 400 | """ Returns the list of recent errors. 401 | 402 | Returns: 403 | list: of :obj:`.ErrorEvent` tuples. 404 | """ 405 | ret_errs = list() 406 | errors = self.get('error').get('errors', None) or list() 407 | assert isinstance(errors, list) 408 | for err in errors: 409 | when = parse_datetime(err.get('when', None)) 410 | msg = err.get('message', '') 411 | e = ErrorEvent(when, msg) 412 | ret_errs.append(e) 413 | return ret_errs 414 | 415 | def show_error(self, message): 416 | """ Send an error message to the active client. The new error will be 417 | displayed on any active GUI clients. 418 | 419 | Args: 420 | message (str): Plain-text message to display. 421 | 422 | Returns: 423 | None 424 | 425 | >>> s = _syncthing() 426 | >>> s.system.show_error('my error msg') 427 | >>> s.system.errors()[0] 428 | ... # doctest: +ELLIPSIS 429 | ErrorEvent(when=datetime.datetime(...), message='"my error msg"') 430 | >>> s.system.clear_errors() 431 | >>> s.system.errors() 432 | [] 433 | """ 434 | assert isinstance(message, string_types) 435 | self.post('error', data=message) 436 | 437 | def log(self): 438 | """ Returns the list of recent log entries. 439 | 440 | Returns: 441 | dict 442 | """ 443 | return self.get('log') 444 | 445 | def pause(self, device): 446 | """ Pause the given device. 447 | 448 | Args: 449 | device (str): Device ID. 450 | 451 | Returns: 452 | dict: with keys ``success`` and ``error``. 453 | """ 454 | resp = self.post('pause', params={'device': device}, 455 | return_response=True) 456 | error = resp.text 457 | if not error: 458 | error = None 459 | return {'success': resp.status_code == requests.codes.ok, 460 | 'error': error} 461 | 462 | def ping(self, with_method='GET'): 463 | """ Pings the Syncthing server. 464 | 465 | Args: 466 | with_method (str): uses a given HTTP method, options are 467 | ``GET`` and ``POST``. 468 | 469 | Returns: 470 | dict 471 | """ 472 | assert with_method in ('GET', 'POST') 473 | if with_method == 'GET': 474 | return self.get('ping') 475 | return self.post('ping') 476 | 477 | def reset(self): 478 | """ Erase the current index database and restart Syncthing. 479 | 480 | Returns: 481 | None 482 | """ 483 | warnings.warn('This is a destructive action that cannot be undone.') 484 | self.post('reset', data={}) 485 | 486 | def reset_folder(self, folder): 487 | """ Erase the database index from a given folder and restart Syncthing. 488 | 489 | Args: 490 | folder (str): Folder ID. 491 | 492 | Returns: 493 | None 494 | """ 495 | warnings.warn('This is a destructive action that cannot be undone.') 496 | self.post('reset', data={}, params={'folder': folder}) 497 | 498 | def restart(self): 499 | """ Immediately restart Syncthing. 500 | 501 | Returns: 502 | None 503 | """ 504 | self.post('restart', data={}) 505 | 506 | def resume(self, device): 507 | """ Resume the given device. 508 | 509 | Args: 510 | device (str): Device ID. 511 | 512 | Returns: 513 | dict: with keys ``success`` and ``error``. 514 | """ 515 | resp = self.post('resume', params={'device': device}, 516 | return_response=True) 517 | error = resp.text 518 | if not error: 519 | error = None 520 | return {'success': resp.status_code == requests.codes.ok, 521 | 'error': error} 522 | 523 | def shutdown(self): 524 | """ Causes Syncthing to exit and not restart. 525 | 526 | Returns: 527 | None 528 | """ 529 | self.post('shutdown', data={}) 530 | 531 | def status(self): 532 | """ Returns information about current system status and resource usage. 533 | 534 | Returns: 535 | dict 536 | """ 537 | resp = self.get('status') 538 | resp = keys_to_datetime(resp, 'startTime') 539 | return resp 540 | 541 | def upgrade(self): 542 | """ Checks for a possible upgrade and returns an object describing 543 | the newest version and upgrade possibility. 544 | 545 | Returns: 546 | dict 547 | """ 548 | return self.get('upgrade') 549 | 550 | def can_upgrade(self): 551 | """ Returns when there's a newer version than the instance running. 552 | 553 | Returns: 554 | bool 555 | """ 556 | return (self.upgrade() or {}).get('newer', False) 557 | 558 | def do_upgrade(self): 559 | """ Perform an upgrade to the newest released version and restart. 560 | Does nothing if there is no newer version than currently running. 561 | 562 | Returns: 563 | None 564 | """ 565 | return self.post('upgrade') 566 | 567 | def version(self): 568 | """ Returns the current Syncthing version information. 569 | 570 | Returns: 571 | dict 572 | """ 573 | return self.get('version') 574 | 575 | 576 | class Database(BaseAPI): 577 | """ HTTP REST endpoint for Database calls.""" 578 | 579 | prefix = '/rest/db/' 580 | 581 | def browse(self, folder, levels=None, prefix=None): 582 | """ Returns the directory tree of the global model. 583 | 584 | Directories are always JSON objects (map/dictionary), and files are 585 | always arrays of modification time and size. The first integer is 586 | the files modification time, and the second integer is the file 587 | size. 588 | 589 | Args: 590 | folder (str): The root folder to traverse. 591 | levels (int): How deep within the tree we want to dwell down. 592 | (0 based, defaults to unlimited depth) 593 | prefix (str): Defines a prefix within the tree where to start 594 | building the structure. 595 | 596 | Returns: 597 | dict 598 | """ 599 | assert isinstance(levels, int) or levels is None 600 | assert isinstance(prefix, string_types) or prefix is None 601 | return self.get('browse', params={'folder': folder, 602 | 'levels': levels, 603 | 'prefix': prefix}) 604 | 605 | def completion(self, device, folder): 606 | """ Returns the completion percentage (0 to 100) for a given device 607 | and folder. 608 | 609 | Args: 610 | device (str): The Syncthing device the folder is syncing to. 611 | folder (str): The folder that is being synced. 612 | 613 | Returs: 614 | int 615 | """ 616 | return self.get( 617 | 'completion', 618 | params={'folder': folder, 'device': device} 619 | ).get('completion', None) 620 | 621 | def file(self, folder, file_): 622 | """ Returns most data available about a given file, including version 623 | and availability. 624 | 625 | Args: 626 | folder (str): 627 | file_ (str): 628 | 629 | Returns: 630 | dict 631 | """ 632 | return self.get('file', params={'folder': folder, 633 | 'file': file_}) 634 | 635 | def ignores(self, folder): 636 | """ Returns the content of the ``.stignore`` as the ignore field. A 637 | second field, expanded, provides a list of strings which represent 638 | globbing patterns described by gobwas/glob (based on standard 639 | wildcards) that match the patterns in ``.stignore`` and all the 640 | includes. 641 | 642 | If appropriate these globs are prepended by the following modifiers: 643 | ``!`` to negate the glob, ``(?i)`` to do case insensitive matching and 644 | ``(?d)`` to enable removing of ignored files in an otherwise empty 645 | directory. 646 | 647 | Args: 648 | folder 649 | 650 | Returns: 651 | dict 652 | """ 653 | return self.get('ignores', params={'folder': folder}) 654 | 655 | def set_ignores(self, folder, *patterns): 656 | """ Applies ``patterns`` to ``folder``'s ``.stignore`` file. 657 | 658 | Args: 659 | folder (str): 660 | patterns (str): 661 | 662 | Returns: 663 | dict 664 | """ 665 | if not patterns: 666 | return {} 667 | data = {'ignore': list(patterns)} 668 | return self.post('ignores', params={'folder': folder}, data=data) 669 | 670 | def need(self, folder, page=None, perpage=None): 671 | """ Returns lists of files which are needed by this device in order 672 | for it to become in sync. 673 | 674 | Args: 675 | folder (str): 676 | page (int): If defined applies pagination accross the 677 | collection of results. 678 | perpage (int): If defined applies pagination across the 679 | collection of results. 680 | 681 | Returns: 682 | dict 683 | """ 684 | assert isinstance(page, int) or page is None 685 | assert isinstance(perpage, int) or perpage is None 686 | return self.get('need', params={'folder': folder, 687 | 'page': page, 688 | 'perpage': perpage}) 689 | 690 | def override(self, folder): 691 | """ Request override of a send-only folder. 692 | 693 | Args: 694 | folder (str): folder ID. 695 | 696 | Returns: 697 | dict 698 | """ 699 | self.post('override', params={'folder': folder}) 700 | 701 | def prio(self, folder, file_): 702 | """ Moves the file to the top of the download queue. 703 | 704 | Args: 705 | folder (str): 706 | file_ (str): 707 | 708 | Returns: 709 | dict 710 | """ 711 | self.post('prio', params={'folder': folder, 712 | 'file': file_}) 713 | 714 | def scan(self, folder, sub=None, next_=None): 715 | """ Request immediate rescan of a folder, or a specific path within a 716 | folder. 717 | 718 | Args: 719 | folder (str): Folder ID. 720 | sub (str): Path relative to the folder root. If sub is omitted 721 | the entire folder is scanned for changes, otherwise only 722 | the given path children are scanned. 723 | next_ (int): Delays Syncthing's automated rescan interval for 724 | a given amount of seconds. 725 | 726 | Returns: 727 | str 728 | """ 729 | if not sub: 730 | sub = '' 731 | assert isinstance(sub, string_types) 732 | assert isinstance(next_, int) or next_ is None 733 | return self.post('scan', params={'folder': folder, 734 | 'sub': sub, 735 | 'next': next_}) 736 | 737 | def status(self, folder): 738 | """ Returns information about the current status of a folder. 739 | 740 | Note: 741 | This is an expensive call, increasing CPU and RAM usage on the 742 | device. Use sparingly. 743 | 744 | Args: 745 | folder (str): Folder ID. 746 | 747 | Returns: 748 | dict 749 | """ 750 | return self.get('status', params={'folder': folder}) 751 | 752 | 753 | class Events(BaseAPI): 754 | """ HTTP REST endpoints for Event based calls. 755 | 756 | Syncthing provides a simple long polling interface for exposing events 757 | from the core utility towards a GUI. 758 | 759 | .. code-block:: python 760 | 761 | syncthing = Syncthing() 762 | event_stream = syncthing.events(limit=5) 763 | 764 | for event in event_stream: 765 | print(event) 766 | if event_stream.count > 10: 767 | event_stream.stop() 768 | """ 769 | 770 | prefix = '/rest/' 771 | 772 | def __init__(self, api_key, last_seen_id=None, filters=None, limit=None, 773 | *args, **kwargs): 774 | if 'timeout' not in kwargs: 775 | # increase our timeout to account for long polling. 776 | # this will reduce the number of timed-out connections, which are 777 | # swallowed by the library anyway 778 | kwargs['timeout'] = 60.0 #seconds 779 | 780 | super(Events, self).__init__(api_key, *args, **kwargs) 781 | self._last_seen_id = last_seen_id or 0 782 | self._filters = filters 783 | self._limit = limit 784 | 785 | self._count = 0 786 | self.blocking = True 787 | 788 | @property 789 | def count(self): 790 | """ The number of events that have been processed by this event stream. 791 | 792 | Returns: 793 | int 794 | """ 795 | return self._count 796 | 797 | @property 798 | def last_seen_id(self): 799 | """ The id of the last seen event. 800 | 801 | Returns: 802 | int 803 | """ 804 | return self._last_seen_id 805 | 806 | def disk_events(self): 807 | """ Blocking generator of disk related events. Each event is 808 | represented as a ``dict`` with metadata. 809 | 810 | Returns: 811 | generator[dict] 812 | """ 813 | for event in self._events('events/disk', None, self._limit): 814 | yield event 815 | 816 | def stop(self): 817 | """ Breaks the while-loop while the generator is polling for event 818 | changes. 819 | 820 | Returns: 821 | None 822 | """ 823 | self.blocking = False 824 | 825 | def _events(self, using_url, filters=None, limit=None): 826 | """ A long-polling method that queries Syncthing for events.. 827 | 828 | Args: 829 | using_url (str): REST HTTP endpoint 830 | filters (List[str]): Creates an "event group" in Syncthing to 831 | only receive events that have been subscribed to. 832 | limit (int): The number of events to query in the history 833 | to catch up to the current state. 834 | 835 | Returns: 836 | generator[dict] 837 | """ 838 | 839 | # coerce 840 | if not isinstance(limit, (int, NoneType)): 841 | limit = None 842 | 843 | # coerce 844 | if filters is None: 845 | filters = [] 846 | 847 | # format our list into the correct expectation of string with commas 848 | if isinstance(filters, string_types): 849 | filters = filters.split(',') 850 | 851 | # reset the state if the loop was broken with `stop` 852 | if not self.blocking: 853 | self.blocking = True 854 | 855 | # block/long-poll for updates to the events api 856 | while self.blocking: 857 | params = { 858 | 'since': self._last_seen_id, 859 | 'limit': limit, 860 | } 861 | 862 | if filters: 863 | params['events'] = ','.join(map(str, filters)) 864 | 865 | try: 866 | data = self.get(using_url, params=params, raw_exceptions=True) 867 | except (Timeout, TimeoutError) as e: 868 | # swallow timeout errors for long polling 869 | data = None 870 | except Exception as e: 871 | reraise('', e) 872 | 873 | if data: 874 | for event in data: 875 | # handle potentially multiple events returned in a list 876 | self._count += 1 877 | yield event 878 | # update our last_seen_id to move our event counter forward 879 | self._last_seen_id = data[-1]['id'] 880 | 881 | def __iter__(self): 882 | """ Helper interface for :obj:`._events` """ 883 | for event in self._events('events', self._filters, self._limit): 884 | yield event 885 | 886 | 887 | class Statistics(BaseAPI): 888 | """ HTTP REST endpoint for Statistic calls.""" 889 | 890 | prefix = '/rest/stats/' 891 | 892 | def device(self): 893 | """ Returns general statistics about devices. 894 | 895 | Currently, only contains the time the device was last seen. 896 | 897 | Returns: 898 | dict 899 | """ 900 | return self.get('device') 901 | 902 | def folder(self): 903 | """ Returns general statistics about folders. 904 | 905 | Currently contains the last scan time and the last synced file. 906 | 907 | Returns: 908 | dict 909 | """ 910 | return self.get('folder') 911 | 912 | 913 | class Misc(BaseAPI): 914 | """ HTTP REST endpoint for Miscelaneous calls.""" 915 | 916 | prefix = '/rest/svc/' 917 | 918 | def device_id(self, id_): 919 | """ Verifies and formats a device ID. Accepts all currently valid 920 | formats (52 or 56 characters with or without separators, upper or lower 921 | case, with trivial substitutions). Takes one parameter, id, and returns 922 | either a valid device ID in modern format, or an error. 923 | 924 | Args: 925 | id_ (str) 926 | 927 | Raises: 928 | SyncthingError: when ``id_`` is an invalid length. 929 | 930 | Returns: 931 | str 932 | """ 933 | return self.get('deviceid', params={'id': id_}).get('id') 934 | 935 | def language(self): 936 | """ Returns a list of canonicalized localization codes, as picked up 937 | from the Accept-Language header sent by the browser. By default, this 938 | API will return a single element that's empty; however calling 939 | :func:`Misc.get` directly with `lang` you can set specific headers to 940 | get values back as intended. 941 | 942 | Returns: 943 | List[str] 944 | 945 | >>> s = _syncthing() 946 | >>> len(s.misc.language()) 947 | 1 948 | >>> s.misc.language()[0] 949 | '' 950 | >>> s.misc.get('lang', headers={'Accept-Language': 'en-us'}) 951 | ['en-us'] 952 | """ 953 | return self.get('lang') 954 | 955 | def random_string(self, length=32): 956 | """ Returns a strong random generated string (alphanumeric) of the 957 | specified length. 958 | 959 | Args: 960 | length (int): default ``32``. 961 | 962 | Returns: 963 | str 964 | 965 | >>> s = _syncthing() 966 | >>> len(s.misc.random_string()) 967 | 32 968 | >>> len(s.misc.random_string(32)) 969 | 32 970 | >>> len(s.misc.random_string(1)) 971 | 1 972 | >>> len(s.misc.random_string(0)) 973 | 32 974 | >>> len(s.misc.random_string(None)) 975 | 32 976 | >>> import string 977 | >>> all_letters = string.ascii_letters + string.digits 978 | >>> all([c in all_letters for c in s.misc.random_string(128)]) 979 | True 980 | >>> all([c in all_letters for c in s.misc.random_string(1024)]) 981 | True 982 | """ 983 | return self.get( 984 | 'random/string', 985 | params={'length': length} 986 | ).get('random', None) 987 | 988 | def report(self): 989 | """ Returns the data sent in the anonymous usage report. 990 | 991 | Returns: 992 | dict 993 | 994 | >>> s = _syncthing() 995 | >>> report = s.misc.report() 996 | >>> 'version' in report 997 | True 998 | >>> 'longVersion' in report 999 | True 1000 | >>> 'syncthing v' in report['longVersion'] 1001 | True 1002 | """ 1003 | return self.get('report') 1004 | 1005 | 1006 | class Syncthing(object): 1007 | """ Default interface for interacting with Syncthing server instance. 1008 | 1009 | Args: 1010 | api_key (str) 1011 | host (str) 1012 | port (int) 1013 | timeout (float) 1014 | is_https (bool) 1015 | ssl_cert_file (str) 1016 | 1017 | Attributes: 1018 | system: instance of :class:`.System`. 1019 | database: instance of :class:`.Database`. 1020 | stats: instance of :class:`.Statistics`. 1021 | misc: instance of :class:`.Misc`. 1022 | 1023 | Note: 1024 | - attribute :attr:`.db` is an alias of :attr:`.database` 1025 | - attribute :attr:`.sys` is an alias of :attr:`.system` 1026 | """ 1027 | 1028 | def __init__(self, api_key, host='localhost', port=8384, 1029 | timeout=DEFAULT_TIMEOUT, is_https=False, ssl_cert_file=None): 1030 | 1031 | # save this for deferred api sub instances 1032 | self.__api_key = api_key 1033 | 1034 | self.api_key = api_key 1035 | self.host = host 1036 | self.port = port 1037 | self.timeout = timeout 1038 | self.is_https = is_https 1039 | self.ssl_cert_file = ssl_cert_file 1040 | 1041 | self.__kwargs = kwargs = { 1042 | 'host': host, 1043 | 'port': port, 1044 | 'timeout': timeout, 1045 | 'is_https': is_https, 1046 | 'ssl_cert_file': ssl_cert_file 1047 | } 1048 | 1049 | self.system = self.sys = System(api_key, **kwargs) 1050 | self.database = self.db = Database(api_key, **kwargs) 1051 | self.stats = Statistics(api_key, **kwargs) 1052 | self.misc = Misc(api_key, **kwargs) 1053 | 1054 | def events(self, last_seen_id=None, filters=None, **kwargs): 1055 | kw = dict(self.__kwargs) 1056 | kw.update(kwargs) 1057 | return Events(api_key=self.__api_key, 1058 | last_seen_id=last_seen_id, 1059 | filters=filters, 1060 | **kw) 1061 | 1062 | 1063 | if __name__ == "__main__": 1064 | import doctest 1065 | doctest.testmod() 1066 | -------------------------------------------------------------------------------- /syncthing/meta.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # >> 4 | # Copyright (c) 2016, Blake VandeMerwe 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining 7 | # a copy of this software and associated documentation files 8 | # (the "Software"), to deal in the Software without restriction, 9 | # including without limitation the rights to use, copy, modify, merge, 10 | # publish, distribute, sublicense, and/or sell copies of the Software, 11 | # and to permit persons to whom the Software is furnished to do so, subject 12 | # to the following conditions: The above copyright notice and this permission 13 | # notice shall be included in all copies or substantial portions 14 | # of the Software. 15 | # 16 | # python-syncthing, 2016 17 | # << 18 | 19 | __title__ = 'python-syncthing' 20 | __author__ = 'Blake VandeMerwe' 21 | __authoremail__ = 'blakev@null.net' 22 | __version__ = '2.4.2' 23 | 24 | -------------------------------------------------------------------------------- /tests/test_parse_datetime.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # >> 4 | # Copyright (c) 2016-2017, Blake VandeMerwe 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining 7 | # a copy of this software and associated documentation files 8 | # (the "Software"), to deal in the Software without restriction, 9 | # including without limitation the rights to use, copy, modify, merge, 10 | # publish, distribute, sublicense, and/or sell copies of the Software, 11 | # and to permit persons to whom the Software is furnished to do so, subject 12 | # to the following conditions: The above copyright notice and this permission 13 | # notice shall be included in all copies or substantial portions 14 | # of the Software. 15 | # 16 | # python-syncthing, 2016 17 | # << 18 | 19 | import unittest 20 | from datetime import datetime 21 | 22 | from syncthing import SyncthingError, parse_datetime 23 | 24 | 25 | class TestParseDatetime(unittest.TestCase): 26 | def test_empties(self): 27 | assert parse_datetime(None) is None 28 | assert parse_datetime('') is None 29 | assert parse_datetime(0) is None 30 | 31 | def test_raises(self): 32 | self.assertRaises(SyncthingError, parse_datetime, 'abc') 33 | self.assertRaises(SyncthingError, parse_datetime, [1]) 34 | 35 | def test_old_docstring_tests(self): 36 | tests = [ 37 | ('2016-06-06T19:41:43.039284753+02:00', datetime(2016, 6, 6, 21, 41, 43, 39284)), 38 | ('2016-06-06T19:41:43.039284753+02:00', datetime(2016, 6, 6, 21, 41, 43, 39284)), 39 | ('2016-06-06T19:41:43.039284753-02:00', datetime(2016, 6, 6, 17, 41, 43, 39284)), 40 | ('2016-06-06T19:41:43.039284', datetime(2016, 6, 6, 17, 41, 43, 39284)), 41 | ('2016-06-06T19:41:43.039284000-02:00', datetime(2016, 6, 6, 19, 41, 43, 39284)) 42 | ] 43 | for a, b in tests: 44 | assert parse_datetime(a).toordinal() == b.toordinal() 45 | -------------------------------------------------------------------------------- /tests/test_scan_folder.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # >> 4 | # Copyright (c) 2016-2017, Blake VandeMerwe 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining 7 | # a copy of this software and associated documentation files 8 | # (the "Software"), to deal in the Software without restriction, 9 | # including without limitation the rights to use, copy, modify, merge, 10 | # publish, distribute, sublicense, and/or sell copies of the Software, 11 | # and to permit persons to whom the Software is furnished to do so, subject 12 | # to the following conditions: The above copyright notice and this permission 13 | # notice shall be included in all copies or substantial portions 14 | # of the Software. 15 | # 16 | # python-syncthing, 2016 17 | # << 18 | 19 | import os 20 | import unittest 21 | 22 | from six import string_types 23 | 24 | from syncthing import Syncthing, SyncthingError, BaseAPI 25 | 26 | KEY = os.getenv('SYNCTHING_API_KEY') 27 | HOST = os.getenv('SYNCTHING_HOST', '127.0.0.1') 28 | PORT = os.getenv('SYNCTHING_PORT', 8384) 29 | IS_HTTPS = bool(int(os.getenv('SYNCTHING_HTTPS', '0'))) 30 | SSL_CERT_FILE = os.getenv('SYNCTHING_CERT_FILE') 31 | 32 | def syncthing(): 33 | return Syncthing(KEY, HOST, PORT, 10.0, IS_HTTPS, SSL_CERT_FILE) 34 | 35 | 36 | s = syncthing() 37 | 38 | class TestScanFolder(unittest.TestCase): 39 | def test_folder_scan_root(self): 40 | folders = s.stats.folder() 41 | available = list(folders.keys()) 42 | if not available: 43 | raise EnvironmentError('there are no folders to scan') 44 | use = available[0] 45 | ret = s.database.scan(use) 46 | last_scan = folders[use]['lastScan'] 47 | assert isinstance(ret, string_types) 48 | assert s.stats.folder()[use]['lastScan'] > last_scan 49 | 50 | def test_folder_scan_sub(self): 51 | folder = '2vw2z-xwpvk' 52 | last_scan = s.stats.folder()[folder]['lastScan'] 53 | ret = s.database.scan(folder, 'docs') 54 | assert isinstance(ret, string_types) 55 | assert s.stats.folder()[folder]['lastScan'] > last_scan 56 | -------------------------------------------------------------------------------- /tests/test_syncthing.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # >> 4 | # Copyright (c) 2016-2017, Blake VandeMerwe 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining 7 | # a copy of this software and associated documentation files 8 | # (the "Software"), to deal in the Software without restriction, 9 | # including without limitation the rights to use, copy, modify, merge, 10 | # publish, distribute, sublicense, and/or sell copies of the Software, 11 | # and to permit persons to whom the Software is furnished to do so, subject 12 | # to the following conditions: The above copyright notice and this permission 13 | # notice shall be included in all copies or substantial portions 14 | # of the Software. 15 | # 16 | # python-syncthing, 2016 17 | # << 18 | from __future__ import unicode_literals 19 | 20 | import os 21 | import unittest 22 | 23 | import requests 24 | from syncthing import Syncthing, SyncthingError, BaseAPI 25 | 26 | KEY = os.getenv('SYNCTHING_API_KEY') 27 | HOST = os.getenv('SYNCTHING_HOST', '127.0.0.1') 28 | PORT = os.getenv('SYNCTHING_PORT', 8384) 29 | IS_HTTPS = bool(int(os.getenv('SYNCTHING_HTTPS', '0'))) 30 | SSL_CERT_FILE = os.getenv('SYNCTHING_CERT_FILE') 31 | 32 | def syncthing(): 33 | return Syncthing(KEY, HOST, PORT, 10.0, IS_HTTPS, SSL_CERT_FILE) 34 | 35 | 36 | class TestBaseAPI(unittest.TestCase): 37 | 38 | def test_a_imports(self): 39 | assert Syncthing 40 | assert SyncthingError 41 | assert BaseAPI 42 | 43 | def test_b_instantiation(self): 44 | Syncthing('') 45 | 46 | def test_c_attributes(self): 47 | s = Syncthing('') 48 | assert s.host is not None 49 | assert hasattr(s, 'system') 50 | assert hasattr(s, 'database') 51 | assert hasattr(s, 'stats') 52 | assert hasattr(s, 'misc') 53 | 54 | def test_c_connection(self): 55 | sync = syncthing() 56 | resp = requests.get(sync.misc.url) 57 | self.assertEqual(resp.status_code, 200, 'cannot connect to syncthing') 58 | 59 | def test_m_database(self): 60 | pass 61 | 62 | def test_m_stats(self): 63 | pass 64 | 65 | def test_m_misc_device(self): 66 | s = syncthing() 67 | 68 | self.assertEqual(s.misc.device_id(None), '') 69 | self.assertEqual(s.misc.device_id(''), '') 70 | with self.assertRaises(SyncthingError): 71 | s.misc.device_id(1234) 72 | 73 | orig = 'p56ioi7m--zjnu2iq-gdr-eydm-2mgtmgl3bxnpq6w5btbbz4tjxzwicq' 74 | valid = 'P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2' 75 | self.assertEqual(s.misc.device_id(orig), valid) 76 | 77 | def test_m_misc_lang(self): 78 | s = syncthing() 79 | langs = s.misc.language() 80 | self.assertIsInstance(langs, list) 81 | self.assertEqual(len(langs), 1) 82 | 83 | langs = s.misc.get('lang', headers={'Accept-Language': 'en_us'}) 84 | self.assertIsInstance(langs, list) 85 | self.assertEqual(len(langs), 1) 86 | self.assertEqual(langs[0], 'en_us') 87 | 88 | def test_m_misc_random_string(self): 89 | s = syncthing() 90 | 91 | self.assertEqual(len(s.misc.random_string()), 32) 92 | self.assertEqual(len(s.misc.random_string(16)), 16) 93 | self.assertEqual(len(s.misc.random_string(0)), 32) 94 | self.assertEqual(len(s.misc.random_string(1)), 1) 95 | self.assertEqual(len(s.misc.get('random/string').get('random', None)), 32) 96 | 97 | 98 | class TestSystemAPI(unittest.TestCase): 99 | def test_errors(self): 100 | s = syncthing() 101 | s.system.errors() 102 | 103 | def test_status(self): 104 | s = syncthing() 105 | status = s.system.status() 106 | self.assertIsInstance(status, dict) --------------------------------------------------------------------------------