plumbum-1.7.2/0000755000232200023220000000000014161152315013547 5ustar debalancedebalanceplumbum-1.7.2/.readthedocs.yml0000644000232200023220000000060014161152302016625 0ustar debalancedebalance# .readthedocs.yml # 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 # Include PDF and ePub formats: all python: version: 3.8 install: - method: pip path: . extra_requirements: - docs plumbum-1.7.2/noxfile.py0000644000232200023220000000234014161152302015560 0ustar debalancedebalance# -*- coding: utf-8 -*- from __future__ import annotations import nox ALL_PYTHONS = ["2.7", "3.5", "3.6", "3.7", "3.8", "3.9"] nox.options.sessions = ["lint", "tests"] @nox.session(reuse_venv=True) def lint(session): """ Run the linter. """ session.install("pre-commit") session.run("pre-commit", "run", "--all-files", *session.posargs) @nox.session(python=ALL_PYTHONS, reuse_venv=True) def tests(session): """ Run the unit and regular tests. """ session.install("-e", ".[dev]") session.run("pytest", *session.posargs) @nox.session(reuse_venv=True) def docs(session): """ Build the docs. Pass "serve" to serve. """ session.install("-e", ".[docs]") session.chdir("docs") session.run("sphinx-build", "-M", "html", ".", "_build") if session.posargs: if "serve" in session.posargs: print("Launching docs at http://localhost:8000/ - use Ctrl-C to quit") session.run("python", "-m", "http.server", "8000", "-d", "_build/html") else: print("Unsupported argument to docs") @nox.session def build(session): """ Build an SDist and wheel. """ session.install("build") session.run("python", "-m", "build") plumbum-1.7.2/plumbum.egg-info/0000755000232200023220000000000014161152315016722 5ustar debalancedebalanceplumbum-1.7.2/plumbum.egg-info/requires.txt0000644000232200023220000000033014161152314021315 0ustar debalancedebalance [:platform_system == "Windows" and platform_python_implementation != "PyPy"] pywin32 [dev] paramiko psutil pytest pytest-cov pytest-mock pytest-timeout [docs] Sphinx>=3.0.0 sphinx-rtd-theme>=0.5.0 [ssh] paramiko plumbum-1.7.2/plumbum.egg-info/PKG-INFO0000644000232200023220000002001614161152314020015 0ustar debalancedebalanceMetadata-Version: 2.1 Name: plumbum Version: 1.7.2 Summary: Plumbum: shell combinators library Home-page: https://plumbum.readthedocs.io Author: Tomer Filiba Author-email: tomerfiliba@gmail.com License: MIT Keywords: path,,local,,remote,,ssh,,shell,,pipe,,popen,,process,,execution,,color,,cli Platform: POSIX Platform: Windows Classifier: Development Status :: 5 - Production/Stable Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: Microsoft :: Windows Classifier: Operating System :: POSIX Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Topic :: Software Development :: Build Tools Classifier: Topic :: System :: Systems Administration Provides: plumbum Requires-Python: !=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7 Description-Content-Type: text/x-rst Provides-Extra: dev Provides-Extra: docs Provides-Extra: ssh License-File: LICENSE .. image:: https://readthedocs.org/projects/plumbum/badge/ :target: https://plumbum.readthedocs.io/en/latest/ :alt: Documentation Status .. image:: https://github.com/tomerfiliba/plumbum/workflows/CI/badge.svg :target: https://github.com/tomerfiliba/plumbum/actions :alt: Build Status .. image:: https://coveralls.io/repos/tomerfiliba/plumbum/badge.svg?branch=master&service=github :target: https://coveralls.io/github/tomerfiliba/plumbum?branch=master :alt: Coverage Status .. image:: https://img.shields.io/pypi/v/plumbum.svg :target: https://pypi.python.org/pypi/plumbum/ :alt: PyPI Status .. image:: https://img.shields.io/pypi/pyversions/plumbum.svg :target: https://pypi.python.org/pypi/plumbum/ :alt: PyPI Versions .. image:: https://img.shields.io/conda/vn/conda-forge/plumbum.svg :target: https://github.com/conda-forge/plumbum-feedstock :alt: Conda-Forge Badge .. image:: https://img.shields.io/pypi/l/plumbum.svg :target: https://pypi.python.org/pypi/plumbum/ :alt: PyPI License .. image:: https://badges.gitter.im/plumbumpy/Lobby.svg :alt: Join the chat at https://gitter.im/plumbumpy/Lobby :target: https://gitter.im/plumbumpy/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :alt: Code styled with Black :target: https://github.com/psf/black Plumbum: Shell Combinators ========================== Ever wished the compactness of shell scripts be put into a **real** programming language? Say hello to *Plumbum Shell Combinators*. Plumbum (Latin for *lead*, which was used to create pipes back in the day) is a small yet feature-rich library for shell script-like programs in Python. The motto of the library is **"Never write shell scripts again"**, and thus it attempts to mimic the **shell syntax** ("shell combinators") where it makes sense, while keeping it all **Pythonic and cross-platform**. Apart from shell-like syntax and handy shortcuts, the library provides local and remote command execution (over SSH), local and remote file-system paths, easy working-directory and environment manipulation, and a programmatic Command-Line Interface (CLI) application toolkit. Now let's see some code! *This is only a teaser; the full documentation can be found at* `Read the Docs `_ Cheat Sheet ----------- Basics ****** .. code-block:: python >>> from plumbum import local >>> local.cmd.ls LocalCommand(/bin/ls) >>> local.cmd.ls() 'build.py\nCHANGELOG.rst\nconda.recipe\nCONTRIBUTING.rst\ndocs\nexamples\nexperiments\nLICENSE\nMANIFEST.in\nPipfile\nplumbum\nplumbum.egg-info\npytest.ini\nREADME.rst\nsetup.cfg\nsetup.py\ntests\ntranslations.py\n' >>> notepad = local["c:\\windows\\notepad.exe"] >>> notepad() # Notepad window pops up '' # Notepad window is closed by user, command returns In the example above, you can use ``local["ls"]`` if you have an unusually named executable or a full path to an executable. The ``local`` object represents your local machine. As you'll see, Plumbum also provides remote machines that use the same API! You can also use ``from plumbum.cmd import ls`` as well for accessing programs in the ``PATH``. Piping ****** .. code-block:: python >>> from plumbum.cmd import ls, grep, wc >>> chain = ls["-a"] | grep["-v", r"\.py"] | wc["-l"] >>> print(chain) /bin/ls -a | /bin/grep -v '\.py' | /usr/bin/wc -l >>> chain() '27\n' Redirection *********** .. code-block:: python >>> from plumbum.cmd import cat, head >>> ((cat < "setup.py") | head["-n", 4])() '#!/usr/bin/env python\nimport os\n\ntry:\n' >>> (ls["-a"] > "file.list")() '' >>> (cat["file.list"] | wc["-l"])() '31\n' Working-directory manipulation ****************************** .. code-block:: python >>> local.cwd >>> with local.cwd(local.cwd / "docs"): ... chain() ... '22\n' Foreground and background execution *********************************** .. code-block:: python >>> from plumbum import FG, BG >>> (ls["-a"] | grep[r"\.py"]) & FG # The output is printed to stdout directly build.py setup.py translations.py >>> (ls["-a"] | grep[r"\.py"]) & BG # The process runs "in the background" Command nesting *************** .. code-block:: python >>> from plumbum.cmd import sudo, ifconfig >>> print(sudo[ifconfig["-a"]]) /usr/bin/sudo /sbin/ifconfig -a >>> (sudo[ifconfig["-a"]] | grep["-i", "loop"]) & FG lo Link encap:Local Loopback UP LOOPBACK RUNNING MTU:16436 Metric:1 Remote commands (over SSH) ************************** Supports `openSSH `_-compatible clients, `PuTTY `_ (on Windows) and `Paramiko `_ (a pure-Python implementation of SSH2) .. code-block:: python >>> from plumbum import SshMachine >>> remote = SshMachine("somehost", user = "john", keyfile = "/path/to/idrsa") >>> r_ls = remote["ls"] >>> with remote.cwd("/lib"): ... (r_ls | grep["0.so.0"])() ... 'libusb-1.0.so.0\nlibusb-1.0.so.0.0.0\n' CLI applications **************** .. code-block:: python import logging from plumbum import cli class MyCompiler(cli.Application): verbose = cli.Flag(["-v", "--verbose"], help = "Enable verbose mode") include_dirs = cli.SwitchAttr("-I", list = True, help = "Specify include directories") @cli.switch("--loglevel", int) def set_log_level(self, level): """Sets the log-level of the logger""" logging.root.setLevel(level) def main(self, *srcfiles): print("Verbose:", self.verbose) print("Include dirs:", self.include_dirs) print("Compiling:", srcfiles) if __name__ == "__main__": MyCompiler.run() Sample output +++++++++++++ :: $ python simple_cli.py -v -I foo/bar -Ispam/eggs x.cpp y.cpp z.cpp Verbose: True Include dirs: ['foo/bar', 'spam/eggs'] Compiling: ('x.cpp', 'y.cpp', 'z.cpp') Colors and Styles ----------------- .. code-block:: python from plumbum import colors with colors.red: print("This library provides safe, flexible color access.") print(colors.bold | "(and styles in general)", "are easy!") print("The simple 16 colors or", colors.orchid & colors.underline | '256 named colors,', colors.rgb(18, 146, 64) | "or full rgb colors", 'can be used.') print("Unsafe " + colors.bg.dark_khaki + "color access" + colors.bg.reset + " is available too.") plumbum-1.7.2/plumbum.egg-info/SOURCES.txt0000644000232200023220000000646614161152315020622 0ustar debalancedebalance.editorconfig .gitattributes .gitignore .pre-commit-config.yaml .readthedocs.yml CHANGELOG.rst CONTRIBUTING.rst LICENSE MANIFEST.in README.rst noxfile.py pyproject.toml setup.cfg setup.py translations.py .github/dependabot.yml .github/workflows/ci.yml conda.recipe/.gitignore conda.recipe/README.mkd conda.recipe/bld.bat conda.recipe/build.sh conda.recipe/meta.yaml docs/.gitignore docs/Makefile docs/_cheatsheet.rst docs/_color_list.html docs/_news.rst docs/changelog.rst docs/cli.rst docs/colorlib.rst docs/colors.rst docs/conf.py docs/index.rst docs/local_commands.rst docs/local_machine.rst docs/make.bat docs/paths.rst docs/quickref.rst docs/remote.rst docs/typed_env.rst docs/utils.rst docs/_static/fish-text-black.png docs/_static/github-logo.png docs/_static/logo.png docs/_static/logo2.png docs/_static/logo3.png docs/_static/logo4.png docs/_static/logo6.png docs/_static/logo7.png docs/_static/logo8.png docs/_static/placeholder docs/_templates/placeholder docs/api/cli.rst docs/api/colors.rst docs/api/commands.rst docs/api/fs.rst docs/api/machines.rst docs/api/path.rst examples/.gitignore examples/PHSP.png examples/SimpleColorCLI.py examples/alignment.py examples/color.py examples/filecopy.py examples/fullcolor.py examples/geet.py examples/make_figures.py examples/simple_cli.py examples/testfigure.tex experiments/parallel.py experiments/test_parallel.py plumbum/__init__.py plumbum/_testtools.py plumbum/colors.py plumbum/lib.py plumbum/typed_env.py plumbum/version.py plumbum.egg-info/PKG-INFO plumbum.egg-info/SOURCES.txt plumbum.egg-info/dependency_links.txt plumbum.egg-info/requires.txt plumbum.egg-info/top_level.txt plumbum/cli/__init__.py plumbum/cli/application.py plumbum/cli/config.py plumbum/cli/i18n.py plumbum/cli/image.py plumbum/cli/progress.py plumbum/cli/switches.py plumbum/cli/terminal.py plumbum/cli/termsize.py plumbum/cli/i18n/de.po plumbum/cli/i18n/fr.po plumbum/cli/i18n/nl.po plumbum/cli/i18n/ru.po plumbum/cli/i18n/de/LC_MESSAGES/plumbum.cli.mo plumbum/cli/i18n/fr/LC_MESSAGES/plumbum.cli.mo plumbum/cli/i18n/nl/LC_MESSAGES/plumbum.cli.mo plumbum/cli/i18n/ru/LC_MESSAGES/plumbum.cli.mo plumbum/colorlib/__init__.py plumbum/colorlib/__main__.py plumbum/colorlib/_ipython_ext.py plumbum/colorlib/factories.py plumbum/colorlib/names.py plumbum/colorlib/styles.py plumbum/commands/__init__.py plumbum/commands/base.py plumbum/commands/daemons.py plumbum/commands/modifiers.py plumbum/commands/processes.py plumbum/fs/__init__.py plumbum/fs/atomic.py plumbum/fs/mounts.py plumbum/machines/__init__.py plumbum/machines/_windows.py plumbum/machines/base.py plumbum/machines/env.py plumbum/machines/local.py plumbum/machines/paramiko_machine.py plumbum/machines/remote.py plumbum/machines/session.py plumbum/machines/ssh_machine.py plumbum/path/__init__.py plumbum/path/base.py plumbum/path/local.py plumbum/path/remote.py plumbum/path/utils.py tests/_test_paramiko.py tests/conftest.py tests/env.py tests/file with space.txt tests/slow_process.bash tests/test_3_cli.py tests/test_cli.py tests/test_clicolor.py tests/test_color.py tests/test_config.py tests/test_env.py tests/test_factories.py tests/test_local.py tests/test_nohup.py tests/test_putty.py tests/test_remote.py tests/test_sudo.py tests/test_terminal.py tests/test_typed_env.py tests/test_utils.py tests/test_validate.py tests/test_visual_color.py tests/not-in-path/dummy-executableplumbum-1.7.2/plumbum.egg-info/top_level.txt0000644000232200023220000000001014161152314021442 0ustar debalancedebalanceplumbum plumbum-1.7.2/plumbum.egg-info/dependency_links.txt0000644000232200023220000000000114161152314022767 0ustar debalancedebalance plumbum-1.7.2/setup.cfg0000644000232200023220000000445114161152315015374 0ustar debalancedebalance[metadata] name = plumbum description = Plumbum: shell combinators library long_description = file: README.rst long_description_content_type = text/x-rst url = https://plumbum.readthedocs.io author = Tomer Filiba author_email = tomerfiliba@gmail.com license = MIT license_file = LICENSE platforms = POSIX, Windows classifiers = Development Status :: 5 - Production/Stable License :: OSI Approved :: MIT License Operating System :: Microsoft :: Windows Operating System :: POSIX Programming Language :: Python :: 2 Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Topic :: Software Development :: Build Tools Topic :: System :: Systems Administration keywords = path, local, remote, ssh, shell, pipe, popen, process, execution, color, cli provides = plumbum [options] packages = find: install_requires = pywin32;platform_system=='Windows' and platform_python_implementation!="PyPy" python_requires = >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.* [options.packages.find] exclude = tests [options.extras_require] dev = paramiko psutil pytest pytest-cov pytest-mock pytest-timeout docs = Sphinx>=3.0.0 sphinx-rtd-theme>=0.5.0 ssh = paramiko [options.package_data] plumbum.cli = i18n/*/LC_MESSAGES/*.mo [bdist_wheel] universal = 1 [tool:pytest] addopts = -v -ra --cov-config=setup.cfg norecursedirs = examples experiments required_plugins = pytest-timeout pytest-mock timeout = 300 optional_tests = ssh: requires self ssh access to run sudo: requires sudo access to run [coverage:run] branch = True relative_files = True source_pkgs = plumbum omit = *ipython*.py *__main__.py *_windows.py [coverage:report] exclude_lines = pragma: no cover def __repr__ raise AssertionError raise NotImplementedError if __name__ == .__main__.: [flake8] max-complexity = 50 ignore = E203, E231, E501, E722, W503, B950, B904, B003, B008, E731, B902 select = C,E,F,W,B,B9 [tool:isort] profile = black [check-manifest] ignore = .* docs/** examples/* experiments/* conda.recipe/* CONTRIBUTING.rst [egg_info] tag_build = tag_date = 0 plumbum-1.7.2/LICENSE0000644000232200023220000000207014161152302014547 0ustar debalancedebalanceCopyright (c) 2013 Tomer Filiba (tomerfiliba@gmail.com) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. plumbum-1.7.2/.editorconfig0000644000232200023220000000016614161152302016223 0ustar debalancedebalanceroot = true [*.py] charset = utf-8 indent_style = space indent_size = 4 insert_final_newline = true end_of_line = lf plumbum-1.7.2/PKG-INFO0000644000232200023220000002001614161152315014643 0ustar debalancedebalanceMetadata-Version: 2.1 Name: plumbum Version: 1.7.2 Summary: Plumbum: shell combinators library Home-page: https://plumbum.readthedocs.io Author: Tomer Filiba Author-email: tomerfiliba@gmail.com License: MIT Keywords: path,,local,,remote,,ssh,,shell,,pipe,,popen,,process,,execution,,color,,cli Platform: POSIX Platform: Windows Classifier: Development Status :: 5 - Production/Stable Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: Microsoft :: Windows Classifier: Operating System :: POSIX Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Topic :: Software Development :: Build Tools Classifier: Topic :: System :: Systems Administration Provides: plumbum Requires-Python: !=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7 Description-Content-Type: text/x-rst Provides-Extra: dev Provides-Extra: docs Provides-Extra: ssh License-File: LICENSE .. image:: https://readthedocs.org/projects/plumbum/badge/ :target: https://plumbum.readthedocs.io/en/latest/ :alt: Documentation Status .. image:: https://github.com/tomerfiliba/plumbum/workflows/CI/badge.svg :target: https://github.com/tomerfiliba/plumbum/actions :alt: Build Status .. image:: https://coveralls.io/repos/tomerfiliba/plumbum/badge.svg?branch=master&service=github :target: https://coveralls.io/github/tomerfiliba/plumbum?branch=master :alt: Coverage Status .. image:: https://img.shields.io/pypi/v/plumbum.svg :target: https://pypi.python.org/pypi/plumbum/ :alt: PyPI Status .. image:: https://img.shields.io/pypi/pyversions/plumbum.svg :target: https://pypi.python.org/pypi/plumbum/ :alt: PyPI Versions .. image:: https://img.shields.io/conda/vn/conda-forge/plumbum.svg :target: https://github.com/conda-forge/plumbum-feedstock :alt: Conda-Forge Badge .. image:: https://img.shields.io/pypi/l/plumbum.svg :target: https://pypi.python.org/pypi/plumbum/ :alt: PyPI License .. image:: https://badges.gitter.im/plumbumpy/Lobby.svg :alt: Join the chat at https://gitter.im/plumbumpy/Lobby :target: https://gitter.im/plumbumpy/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :alt: Code styled with Black :target: https://github.com/psf/black Plumbum: Shell Combinators ========================== Ever wished the compactness of shell scripts be put into a **real** programming language? Say hello to *Plumbum Shell Combinators*. Plumbum (Latin for *lead*, which was used to create pipes back in the day) is a small yet feature-rich library for shell script-like programs in Python. The motto of the library is **"Never write shell scripts again"**, and thus it attempts to mimic the **shell syntax** ("shell combinators") where it makes sense, while keeping it all **Pythonic and cross-platform**. Apart from shell-like syntax and handy shortcuts, the library provides local and remote command execution (over SSH), local and remote file-system paths, easy working-directory and environment manipulation, and a programmatic Command-Line Interface (CLI) application toolkit. Now let's see some code! *This is only a teaser; the full documentation can be found at* `Read the Docs `_ Cheat Sheet ----------- Basics ****** .. code-block:: python >>> from plumbum import local >>> local.cmd.ls LocalCommand(/bin/ls) >>> local.cmd.ls() 'build.py\nCHANGELOG.rst\nconda.recipe\nCONTRIBUTING.rst\ndocs\nexamples\nexperiments\nLICENSE\nMANIFEST.in\nPipfile\nplumbum\nplumbum.egg-info\npytest.ini\nREADME.rst\nsetup.cfg\nsetup.py\ntests\ntranslations.py\n' >>> notepad = local["c:\\windows\\notepad.exe"] >>> notepad() # Notepad window pops up '' # Notepad window is closed by user, command returns In the example above, you can use ``local["ls"]`` if you have an unusually named executable or a full path to an executable. The ``local`` object represents your local machine. As you'll see, Plumbum also provides remote machines that use the same API! You can also use ``from plumbum.cmd import ls`` as well for accessing programs in the ``PATH``. Piping ****** .. code-block:: python >>> from plumbum.cmd import ls, grep, wc >>> chain = ls["-a"] | grep["-v", r"\.py"] | wc["-l"] >>> print(chain) /bin/ls -a | /bin/grep -v '\.py' | /usr/bin/wc -l >>> chain() '27\n' Redirection *********** .. code-block:: python >>> from plumbum.cmd import cat, head >>> ((cat < "setup.py") | head["-n", 4])() '#!/usr/bin/env python\nimport os\n\ntry:\n' >>> (ls["-a"] > "file.list")() '' >>> (cat["file.list"] | wc["-l"])() '31\n' Working-directory manipulation ****************************** .. code-block:: python >>> local.cwd >>> with local.cwd(local.cwd / "docs"): ... chain() ... '22\n' Foreground and background execution *********************************** .. code-block:: python >>> from plumbum import FG, BG >>> (ls["-a"] | grep[r"\.py"]) & FG # The output is printed to stdout directly build.py setup.py translations.py >>> (ls["-a"] | grep[r"\.py"]) & BG # The process runs "in the background" Command nesting *************** .. code-block:: python >>> from plumbum.cmd import sudo, ifconfig >>> print(sudo[ifconfig["-a"]]) /usr/bin/sudo /sbin/ifconfig -a >>> (sudo[ifconfig["-a"]] | grep["-i", "loop"]) & FG lo Link encap:Local Loopback UP LOOPBACK RUNNING MTU:16436 Metric:1 Remote commands (over SSH) ************************** Supports `openSSH `_-compatible clients, `PuTTY `_ (on Windows) and `Paramiko `_ (a pure-Python implementation of SSH2) .. code-block:: python >>> from plumbum import SshMachine >>> remote = SshMachine("somehost", user = "john", keyfile = "/path/to/idrsa") >>> r_ls = remote["ls"] >>> with remote.cwd("/lib"): ... (r_ls | grep["0.so.0"])() ... 'libusb-1.0.so.0\nlibusb-1.0.so.0.0.0\n' CLI applications **************** .. code-block:: python import logging from plumbum import cli class MyCompiler(cli.Application): verbose = cli.Flag(["-v", "--verbose"], help = "Enable verbose mode") include_dirs = cli.SwitchAttr("-I", list = True, help = "Specify include directories") @cli.switch("--loglevel", int) def set_log_level(self, level): """Sets the log-level of the logger""" logging.root.setLevel(level) def main(self, *srcfiles): print("Verbose:", self.verbose) print("Include dirs:", self.include_dirs) print("Compiling:", srcfiles) if __name__ == "__main__": MyCompiler.run() Sample output +++++++++++++ :: $ python simple_cli.py -v -I foo/bar -Ispam/eggs x.cpp y.cpp z.cpp Verbose: True Include dirs: ['foo/bar', 'spam/eggs'] Compiling: ('x.cpp', 'y.cpp', 'z.cpp') Colors and Styles ----------------- .. code-block:: python from plumbum import colors with colors.red: print("This library provides safe, flexible color access.") print(colors.bold | "(and styles in general)", "are easy!") print("The simple 16 colors or", colors.orchid & colors.underline | '256 named colors,', colors.rgb(18, 146, 64) | "or full rgb colors", 'can be used.') print("Unsafe " + colors.bg.dark_khaki + "color access" + colors.bg.reset + " is available too.") plumbum-1.7.2/MANIFEST.in0000644000232200023220000000033014161152302015275 0ustar debalancedebalanceinclude LICENSE include README.rst include CHANGELOG.rst include translations.py include tests/not-in-path/dummy-executable recursive-include plumbum *.mo *.po *.py py.typed recursive-include tests *.bash *.py *.txt plumbum-1.7.2/pyproject.toml0000644000232200023220000000066614161152302016467 0ustar debalancedebalance[build-system] requires = [ "setuptools>=42", "wheel", "setuptools_scm[toml]>=3.4.3" ] build-backend = "setuptools.build_meta" [tool.setuptools_scm] write_to = "plumbum/version.py" [tool.mypy] files = ["plumbum"] python_version = "2.7" warn_unused_configs = true warn_unused_ignores = true [[tool.mypy.overrides]] module = ["IPython.*", "pywintypes.*", "win32con.*", "win32file.*", "PIL.*"] ignore_missing_imports = true plumbum-1.7.2/CHANGELOG.rst0000644000232200023220000004715514161152302015600 0ustar debalancedebalance1.7.2 ----- * Commands: avoid issue mktemp issue on some BSD variants (`#571 `_) * Better specification of dependency on pywin32 (`#568 `_) * Some DeprecationWarnings changed to FutureWarnings (`#567 `_) 1.7.1 ----- * Paths: glob with local paths no longer expands the existing path too (`#552 `_) * Paramiko: support reverse tunnels (`#562 `_) * SSHMachine: support forwarding Unix sockets in ``.tunnel()`` (`#550 `_) * CLI: Support ``COLOR_GROUP_TITLES`` (`#553 `_) * Fix a deprecated in Python 3.10 warning (`#563 `_) * Extended testing and checking on Python 3.10 and various PyPy versions. Nox is supported for easier new-user development. 1.7.0 ----- * Commands: support ``.with_cwd()`` (`#513 `_) * Commands: make ``iter_lines`` deal with decoding errors during iteration (`#525 `_) * Commands: fix handling of env-vars passed to plumbum BoundEnvCommands (`#513 `_) * Commands: fix support for win32 in ``iter_lines`` (`#500 `_) * Paths: fix incorrect ``__getitem__`` method in Path (`#506 `_) * Paths: Remote path stat had odd OSError (`#505 `_) * Paths: Fix ``RemotePath.copy()`` (`#527 `_) * Paths: missing ``__fspath__`` added (`#498 `_) * SSH: better error reporting on SshSession error (`#515 `_) * Internal: redesigned CI, major cleanup to setuptools distribution, Black formatting, style checking throughout. 1.6.9 ----- * Last version to support Python 2.6; added python_requires for future versions (`#507 `_) * Paths: Fix bug with subscription operations (`#498 `_), (`#506 `_) * Paths: Fix resolve (`#492 `_) * Commands: Fix resolve (`#491 `_) * Commands: Add context manager on popen (`#495 `_) * Several smaller fixes (`#500 `_), (`#505 `_) 1.6.8 ----- * Exceptions: Changed ProcessExecutionError's formatting to be more user-friendly (`#456 `_) * Commands: support for per-line timeout with iter_lines (`#454 `_) * Commands: support for piping stdout/stderr to a logger (`#454 `_) * Paths: support composing paths using subscription operations (`#455 `_) * CLI: Improved 'Set' validator to allow non-string types, and CSV params (`#452 `_) * TypedEnv: Facility for modeling environment-variables into python data types (`#451 `_) * Commands: execute local/remote commands via a magic `.cmd` attribute (`#450 `_) 1.6.7 ----- * Commands: Added ``run_*`` methods as an alternative to modifiers (`#386 `_) * CLI: Added support for ``ALLOW_ABREV`` (`#401 `_) * CLI: Added ``DESCRIPTION_MORE``, preserves spacing (`#378 `_) * Color: Avoid throwing error in atexit in special cases (like pytest) (`#393 `_) * Including Python 3.7 in testing matrix. * Smaller bugfixes and other testing improvements. 1.6.6 ----- * Critical Bugfix: High-speed (English) translations could break the CLI module (`#371 `_) * Small improvement to wheels packaging 1.6.5 ----- * Critical Bugfix: Syntax error in image script could break pip installs (`#366 `_) * CLI: Regression fix: English apps now load as fast as in 1.6.3 (`#364 `_) * CLI: Missing colon restored in group names * Regression fix: Restored non-setuptools installs (but really, why would you not have setuptools?) (`#367 `_) 1.6.4 ----- * CLI: Support for localization (`#339 `_), with: - Russian by Pavel Pletenev (`#339 `_) 🇷🇺 - Dutch by Roel Aaij (`#351 `_) 🇳🇱 - French by Joel Closier (`#352 `_) 🇫🇷 - German by Christoph Hasse (`#353 `_) 🇩🇪 - Pulls with more languages welcome! * CLI: Support for ``MakeDirectory`` (`#339 `_) * Commands: Fixed unicode input/output on Python 2 (`#341 `_) * Paths: More updates for pathlib compatibility (`#325 `_) * Terminal: Changed ``prompt()``'s default value for ``type`` parameter from ``int`` to ``str`` to match existing docs (`#327 `_) * Remote: Support ``~`` in PATH for a remote (`#293 `_) * Remote: Fixes for globbing with spaces in filename on a remote server (`#322 `_) * Color: Fixes to image plots, better separation (`#324 `_) * Python 3.3 has been removed from Travis and Appveyor. * Several bugs fixed 1.6.3 ----- * Python 3.6 is now supported, critical bug fixed (`#302 `_) * Commands: Better handling of return codes for pipelines (`#288 `_) * Paths: Return split support (regression) (`#286 `_) - also supports dummy args for better ``str`` compatibility * Paths: Added support for Python 3.6 path protocol * Paths: Support Python's ``in`` syntax * CLI: Added Config parser (provisional) (`#304 `_) * Color: image plots with ``python -m plumbum.cli.image`` (`#304 `_) * SSH: No longer hangs for ``timeout`` seconds on failure (`#306 `_) * Test improvements, especially on non-linux systems 1.6.2 ----- * CLI: ``Progress`` now has a clear keyword that hides the bar on completion * CLI: ``Progress`` without clear now starts on next line without having to manually add ``\n``. * Commands: modifiers now accept a timeout parameter (`#281 `_) * Commands: ``BG`` modifier now allows ``stdout``/``stderr`` redirection (to screen, for example) (`#258 `_) * Commands: Modifiers no longer crash on repr (see `#262 `_) * Remote: ``nohup`` works again, typo fixed (`#261 `_) * Added better support for SunOS and other OS's. (`#260 `_) * Colors: Context manager flushes stream now, provides more consistent results * Other smaller bugfixes, better support for Python 3.6+ 1.6.1 ----- * CLI: ``Application`` subclasses can now be run directly, instead of calling ``.run()``, to facilitate using as entry points (`#237 `_) * CLI: ``gui_open`` added to allow easy opening of paths in default gui editor (`#239 `_) * CLI: More control over help message (`#233 `_) * Remote: ``cwd`` is now stashed to reduce network usage (similar to Plumbum <1.6 behavior), and absolute paths are faster, (`#238 `_) * Bugfix: Pipelined return codes now give correct attribution (`#243 `_) * Bugfix: ``Progress`` works on Python 2.6 (`#230 `_) * Bugfix: Colors now work with more terminals (`#231 `_) * Bugfix: Getting an executable no longer returns a directory (`#234 `_) * Bugfix: Iterdir now works on Python <3.5 * Testing is now expanded and fully written in Pytest, with coverage reporting. * Added support for Conda ( as of 1.6.2, use the `-c conda-forge` channel) 1.6.0 ----- * Added support for Python 3.5, PyPy, and better Windows and Mac support, with CI testing (`#218 `_, `#217 `_, `#226 `_) * Colors: Added colors module, support for colors added to cli (`#213 `_) * Machines: Added ``.get()`` method for checking several commands. (`#205 `_) * Machines: ``local.cwd`` now is the current directory even if you change the directory with non-Plumbum methods (fixes unexpected behavior). (`#207 `_) * SSHMachine: Better error message for SSH (`#211 `_) * SSHMachine: Support for FreeBSD remote (`#220 `_) * Paths: Now a subclass of ``str``, can be opened directly (`#228 `_) * Paths: Improved pathlib compatibility with several additions and renames (`#223 `_) * Paths: Added globbing multiple patterns at once (`#221 `_) * Commands: added ``NOHUP`` modifier (`#221 `_) * CLI: added positional argument validation (`#225 `_) * CLI: added ``envname``, which allows you specify an environment variable for a ``SwitchAttr`` (`#216 `_) * CLI terminal: added ``Progress``, a command line progress bar for iterators and ranges (`#214 `_) * Continued to clean out Python 2.5 hacks 1.5.0 ----- * Removed support for Python 2.5. (Travis CI does not support it anymore) * CLI: add ``invoke``, which allows you to programmatically run applications (`#149 `_) * CLI: add ``--help-all`` and various cosmetic fixes: (`#125 `_), (`#126 `_), (`#127 `_) * CLI: add ``root_app`` property (`#141 `_) * Machines: ``getattr`` now raises ``AttributeError`` instead of `CommandNotFound` (`#135 `_) * Paramiko: ``keep_alive`` support (`#186 `_) * Paramiko: does not support piping explicitly now (`#160 `_) * Parmaiko: Added pure SFTP backend, gives STFP v4+ support (`#188 `_) * Paths: bugfix to ``cwd`` interaction with ``Path`` (`#142 `_) * Paths: read/write now accept an optional encoding parameter (`#148 `_) * Paths: Suffix support similar to the Python 3.4 standard library ``pathlib`` (`#198 `_) * Commands: renamed ``setenv`` to ``with_env`` (`#143 `_) * Commands: pipelines will now fail with ``ProcessExecutionError`` if the source process fails (`#145 `_) * Commands: added ``TF`` and ``RETCODE`` modifiers (`#202 `_) * Experimental concurrent machine support in ``experimental/parallel.py`` * Several minor bug fixes, including Windows and Python 3 fixes (`#199 `_, `#195 `_) 1.4.2 ----- * Paramiko now supports Python 3, enabled support in Plumbum * Terminal: added ``prompt()``, bugfix to ``get_terminal_size()`` (`#113 `_) * CLI: added ``cleanup()``, which is called after ``main()`` returns * CLI: bugfix to ``CountOf`` (`#118 `_) * Commands: Add a TEE modifier (`#117 `_) * Remote machines: bugfix to ``which``, bugfix to remote environment variables (`#122 `_) * Path: ``read()``/``write()`` now operate on bytes 1.4.1 ----- * Force ``/bin/sh`` to be the shell in ``SshMachine.session()`` (`#111 `_) * Added ``islink()`` and ``unlink()`` to path objects (`#100 `_, `#103 `_) * Added ``access`` to path objects * Faster ``which`` implementation (`#98 `_) * Several minor bug fixes 1.4 --- * Moved ``atomic`` and ``unixutils`` into the new ``fs`` package (file-system related utilities) * Dropped ``plumbum.utils`` legacy shortcut in favor of ``plumbum.path.utils`` * Bugfix: the left-hand-side process of a pipe wasn't waited on, leading to zombies (`#89 `_) * Added ``RelativePath`` (the result of ``Path.relative_to``) * Fixed more text alignment issues in ``cli.Application.help()`` * Introduced ``ask()`` and ``choose`` to ``cli.terminal`` * Bugfix: Path comparison operators were wrong * Added connection timeout to ``RemoteMachine`` 1.3 --- * ``Command.popen``: a new argument, ``new_session`` may be passed to ``Command.popen``, which runs the given in a new session (``setsid`` on POSIX, ``CREATE_NEW_PROCESS_GROUP`` on Windows) * ``Command.Popen``: args can now also be a list (previously, it was required to be a tuple). See * ``local.daemonize``: run commands as full daemons (double-fork and ``setsid``) on POSIX systems, or detached from their controlling console and parent (on Windows). * ``list_processes``: return a list of running process (local/remote machines) * ``SshMachine.nohup``: "daemonize" remote commands via ``nohup`` (not really a daemon, but good enough) * ``atomic``: Atomic file operations (``AtomicFile``, ``AtomicCounterFile`` and ``PidFile``) * ``copy`` and ``move``: the ``src`` argument can now be a list of files to move, e.g., ``copy(["foo", "bar"], "/usr/bin")`` * list local and remote processes * cli: better handling of text wrapping in the generated help message * cli: add a default ``main()`` method that checks for unknown subcommands * terminal: initial commit (``get_terminal_size``) * packaging: the package was split into subpackages; it grew too big for a flat namespace. imports are not expected to be broken by this change * SshMachine: added ``password`` parameter, which relies on `sshpass `_ to feed the password to ``ssh``. This is a security risk, but it's occasionally necessary. Use this with caution! * Commands now have a ``machine`` attribute that points to the machine they run on * Commands gained ``setenv``, which creates a command with a bound environment * Remote path: several fixes to ``stat`` (``StatRes``) * cli: add lazily-loaded subcommands (e.g., ``MainApp.subcommand("foo", "my.package.foo.FooApp")``), which are imported on demand * Paths: added `relative_to and split `_, which (respectively) computes the difference between two paths and splits paths into lists of nodes * cli: ``Predicate`` became a class decorator (it exists solely for pretty-printing anyway) * PuttyMachine: `bugfix `_ 1.2 --- * Path: added `chmod `_ * Path: added `link and symlink `_ * Path: ``walk()`` now applies filter recursively (`#64 `_) * Commands: added `Append redirect `_ * Commands: fix ``_subprocess`` issue (`#59 `_) * Commands: add ``__file__`` to module hack (`#66 `_) * Paramiko: add `'username' and 'password' `_ * Paramiko: add `'timeout' and 'look_for_keys' `_ * Python 3: fix `#56 `_ and `#55 `_ 1.1 --- * `Paramiko `_ integration (`#10 `_) * CLI: now with built-in support for `sub-commands `_. See also: `#43 `_ * The "import hack" has moved to the package's ``__init__.py``, to make it importable directly (`#45 `_) * Paths now support ``chmod`` (on POSIX platform) (`#49 `_) * The argument name of a ``SwitchAttr`` can now be given to it (defaults to ``VALUE``) (`#46 `_) 1.0.1 ----- * Windows: path are no longer converted to lower-case, but ``__eq__`` and ``__hash__`` operate on the lower-cased result (`#38 `_) * Properly handle empty strings in the argument list (`#41 `_) * Relaxed type-checking of ``LocalPath`` and ``RemotePath`` (`#35 `_) * Added ``PuttyMachine`` for Windows users that relies on ``plink`` and ``pscp`` (instead of ``ssh`` and ``scp``) `(#37 `_) 1.0.0 ----- * Rename ``cli.CountingAttr`` to ``cli.CountOf`` * Moved to `Travis `_ continuous integration * Added ``unixutils`` * Added ``chown`` and ``uid``/``gid`` * Lots of fixes and updates to the doc * Full list of `issues `_ 0.9.0 ----- Initial release plumbum-1.7.2/conda.recipe/0000755000232200023220000000000014161152315016101 5ustar debalancedebalanceplumbum-1.7.2/conda.recipe/meta.yaml0000644000232200023220000000173214161152302017712 0ustar debalancedebalancepackage: name: plumbum version: {{ environ.get('GIT_DESCRIBE_TAG', '').replace('v','') }} source: path: ../ requirements: build: - python - setuptools run: - python - paramiko<2.4 # [py26] - paramiko # [not py26] build: number: {{ environ.get('GIT_DESCRIBE_NUMBER', 0) }} {% if environ.get('GIT_DESCRIBE_NUMBER', '0') == '0' %}string: py{{ environ.get('PY_VER').replace('.', '') }}_0 {% else %}string: py{{ environ.get('PY_VER').replace('.', '') }}_{{ environ.get('GIT_BUILD_STR', 'GIT_STUB') }}{% endif %} test: # Python imports imports: - plumbum - plumbum.cli - plumbum.colorlib - plumbum.commands - plumbum.fs - plumbum.machines - plumbum.path requires: # Put any additional test requirements here. For example - pytest - paramiko<2.4 # [py26] - paramiko # [not py26] about: home: https://plumbum.readthedocs.io license: MIT License summary: 'Plumbum: shell combinators library' plumbum-1.7.2/conda.recipe/bld.bat0000644000232200023220000000006314161152302017325 0ustar debalancedebalance"%PYTHON%" setup.py install if errorlevel 1 exit 1 plumbum-1.7.2/conda.recipe/README.mkd0000644000232200023220000000146214161152302017532 0ustar debalancedebalance# Building instructions Change to the `conda.recipies` directory. Run ```bash $ conda install conda-build ``` to aquire the conda build tools. Then you can build with ```bash conda build --python 3.5 . ``` and pay attention to the output directory. You should see something that looks like ``` anaconda upload //anaconda/conda-bld/osx-64/plumbum-v1.6.3-py35_0.tar.bz2 ``` Now, you will need to convert to other architectures. On non-Windows systems: ``` conda convert --platform all //anaconda/conda-bld/osx-64/plumbum-v1.6.3-py35_0.tar.bz2 -o outputdir\ ``` and Windows users will need to add a `-f`. Rerun the following steps for all python versions. To upload packages, ```bash conda install anaconda-client anaconda login for f in `ls outputdir/*/*.tar.bz2`; do anaconda upload $f; done anaconda logout ``` plumbum-1.7.2/conda.recipe/build.sh0000644000232200023220000000004614161152302017530 0ustar debalancedebalance#!/bin/bash $PYTHON setup.py install plumbum-1.7.2/.gitattributes0000644000232200023220000000004314161152302016433 0ustar debalancedebalance*.py text eol=lf *.rst text eol=lf plumbum-1.7.2/setup.py0000644000232200023220000000012514161152302015253 0ustar debalancedebalance#!/usr/bin/env python # -*- coding: utf-8 -*- from setuptools import setup setup() plumbum-1.7.2/examples/0000755000232200023220000000000014161152315015365 5ustar debalancedebalanceplumbum-1.7.2/examples/fullcolor.py0000755000232200023220000000051514161152302017740 0ustar debalancedebalance#!/usr/bin/env python # -*- coding: utf-8 -*- from __future__ import print_function from plumbum import colors with colors: print("Do you believe in color, punk? DO YOU?") for i in range(0, 255, 10): for j in range(0, 255, 10): print(u"".join(colors.rgb(i, j, k)[u"\u2588"] for k in range(0, 255, 10))) plumbum-1.7.2/examples/make_figures.py0000755000232200023220000000151714161152302020403 0ustar debalancedebalance#!/usr/bin/env python # -*- coding: utf-8 -*- from __future__ import print_function, unicode_literals from plumbum import FG, cli, local from plumbum.cmd import convert, pdflatex from plumbum.path.utils import delete def image_comp(item): pdflatex["-shell-escape", item] & FG print("Converting", item) convert[item.with_suffix(".svg"), item.with_suffix(".png")] & FG delete( item.with_suffix(".log"), item.with_suffix(".aux"), ) class MyApp(cli.Application): def main(self, *srcfiles): print("Tex files should start with:") print(r"\documentclass[tikz,convert={outfile=\jobname.svg}]{standalone}") items = map(cli.ExistingFile, srcfiles) if srcfiles else local.cwd // "*.tex" for item in items: image_comp(item) if __name__ == "__main__": MyApp.run() plumbum-1.7.2/examples/alignment.py0000755000232200023220000000057514161152302017723 0ustar debalancedebalance#!/usr/bin/env python # -*- coding: utf-8 -*- from plumbum import cli class App(cli.Application): # VERSION = "1.2.3" # x = cli.SwitchAttr("--lala") y = cli.Flag("-f") def main(self, x, y): pass @App.subcommand("bar") class Bar(cli.Application): z = cli.Flag("-z") def main(self, z, w): pass if __name__ == "__main__": App.run() plumbum-1.7.2/examples/filecopy.py0000755000232200023220000000212514161152302017550 0ustar debalancedebalance#!/usr/bin/env python # -*- coding: utf-8 -*- import logging from plumbum import cli, local from plumbum.path.utils import copy, delete logger = logging.getLogger("FileCopier") class FileCopier(cli.Application): overwrite = cli.Flag("-o", help="If given, overwrite existing files") @cli.switch(["-l", "--log-to-file"], argtype=str) def log_to_file(self, filename): """logs all output to the given file""" handler = logging.FileHandler(filename) logger.addHandler(handler) @cli.switch(["--verbose"], requires=["--log-to-file"]) def set_debug(self): """Sets verbose mode""" logger.setLevel(logging.DEBUG) def main(self, src, dst): if local.path(dst).exists(): if not self.overwrite: logger.debug("Oh no! That's terrible") raise ValueError("Destination already exists") else: delete(dst) logger.debug("I'm going to copy %s to %s", src, dst) copy(src, dst) logger.debug("Great success") if __name__ == "__main__": FileCopier.run() plumbum-1.7.2/examples/simple_cli.py0000755000232200023220000000315114161152302020056 0ustar debalancedebalance#!/usr/bin/env python # -*- coding: utf-8 -*- """ $ python simple_cli.py --help simple_cli.py v1.0 Usage: simple_cli.py [SWITCHES] srcfiles... Meta-switches: -h, --help Prints this help message and quits --version Prints the program's version and quits Switches: -I VALUE:str Specify include directories; may be given multiple times --loglevel LEVEL:int Sets the log-level of the logger -v, --verbose Enable verbose mode $ python simple_cli.py x.cpp y.cpp z.cpp Verbose: False Include dirs: [] Compiling: ('x.cpp', 'y.cpp', 'z.cpp') $ python simple_cli.py -v Verbose: True Include dirs: [] Compiling: () $ python simple_cli.py -v -Ifoo/bar -Ispam/eggs Verbose: True Include dirs: ['foo/bar', 'spam/eggs'] Compiling: () $ python simple_cli.py -v -I foo/bar -Ispam/eggs x.cpp y.cpp z.cpp Verbose: True Include dirs: ['foo/bar', 'spam/eggs'] Compiling: ('x.cpp', 'y.cpp', 'z.cpp') """ from __future__ import print_function import logging from plumbum import cli class MyCompiler(cli.Application): verbose = cli.Flag(["-v", "--verbose"], help="Enable verbose mode") include_dirs = cli.SwitchAttr("-I", list=True, help="Specify include directories") @cli.switch("-loglevel", int) def set_log_level(self, level): """Sets the log-level of the logger""" logging.root.setLevel(level) def main(self, *srcfiles): print("Verbose:", self.verbose) print("Include dirs:", self.include_dirs) print("Compiling:", srcfiles) if __name__ == "__main__": MyCompiler() plumbum-1.7.2/examples/SimpleColorCLI.py0000644000232200023220000000103714161152302020514 0ustar debalancedebalance# -*- coding: utf-8 -*- import plumbum from plumbum import cli, colors # from plumbum.colorlib import HTMLStyle, StyleFactory # plumbum.colors = StyleFactory(HTMLStyle) class MyApp(cli.Application): PROGNAME = colors.green VERSION = colors.blue | "1.0.2" COLOR_GROUPS = {"Meta-switches": colors.yellow} COLOR_GROUP_TITLES = {"Meta-switches": colors.bold & colors.yellow} opts = cli.Flag("--ops", help=colors.magenta | "This is help") def main(self): print("HI") if __name__ == "__main__": MyApp.run() plumbum-1.7.2/examples/geet.py0000755000232200023220000000647714161152302016700 0ustar debalancedebalance#!/usr/bin/env python # -*- coding: utf-8 -*- """ Examples:: $ python geet.py no command given $ python geet.py leet unknown command 'leet' $ python geet.py --help geet v1.7.2 The l33t version control Usage: geet.py [SWITCHES] [SUBCOMMAND [SWITCHES]] args... Meta-switches: -h, --help Prints this help message and quits -v, --version Prints the program's version and quits Subcommands: commit creates a new commit in the current branch; see 'geet commit --help' for more info push pushes the current local branch to the remote one; see 'geet push --help' for more info $ python geet.py commit --help geet commit v1.7.2 creates a new commit in the current branch Usage: geet commit [SWITCHES] Meta-switches: -h, --help Prints this help message and quits -v, --version Prints the program's version and quits Switches: -a automatically add changed files -m VALUE:str sets the commit message; required $ python geet.py commit -m "foo" committing... """ try: import colorama colorama.init() except ImportError: pass from plumbum import cli, colors class Geet(cli.Application): SUBCOMMAND_HELPMSG = False DESCRIPTION = colors.yellow | """The l33t version control""" PROGNAME = colors.green VERSION = colors.blue | "1.7.2" COLOR_USAGE_TITLE = colors.bold | colors.magenta COLOR_USAGE = colors.magenta _group_names = ["Meta-switches", "Switches", "Sub-commands"] COLOR_GROUPS = dict( zip(_group_names, [colors.do_nothing, colors.skyblue1, colors.yellow]) ) COLOR_GROUP_TITLES = dict( zip( _group_names, [colors.bold, colors.bold | colors.skyblue1, colors.bold | colors.yellow], ) ) verbosity = cli.SwitchAttr( "--verbosity", cli.Set("low", "high", "some-very-long-name", "to-test-wrap-around"), help=colors.cyan | "sets the verbosity level of the geet tool. doesn't really do anything except for testing line-wrapping " "in help " * 3, ) verbositie = cli.SwitchAttr( "--verbositie", cli.Set("low", "high", "some-very-long-name", "to-test-wrap-around"), help=colors.hotpink | "sets the verbosity level of the geet tool. doesn't really do anything except for testing line-wrapping " "in help " * 3, ) @Geet.subcommand(colors.red | "commit") class GeetCommit(cli.Application): """creates a new commit in the current branch""" auto_add = cli.Flag("-a", help="automatically add changed files") message = cli.SwitchAttr("-m", str, mandatory=True, help="sets the commit message") def main(self): print("committing...") GeetCommit.unbind_switches("-v", "--version") @Geet.subcommand("push") class GeetPush(cli.Application): """pushes the current local branch to the remote one""" tags = cli.Flag("--tags", help="whether to push tags (default is False)") def main(self, remote, branch="master"): print("pushing to {}/{}...".format(remote, branch)) if __name__ == "__main__": Geet.run() plumbum-1.7.2/examples/testfigure.tex0000644000232200023220000000051214161152302020262 0ustar debalancedebalance\documentclass[tikz,convert={outfile=\jobname.svg}]{standalone} %\usetikzlibrary{...}% tikz package already loaded by 'tikz' option \begin{document} \begin{tikzpicture}% Example: \draw (0,0) -- (10,10); % ... \draw (10,0) -- (0,10); % ... \node at (5,5) {Lorem ipsum at domine standalonus}; \end{tikzpicture} \end{document} plumbum-1.7.2/examples/PHSP.png0000644000232200023220000003756414161152302016660 0ustar debalancedebalance‰PNG  IHDRðØ=1-êbKGDùC» IDATxœíݰ$eyèñn•’½ÄÍŠñâ5×›ÅuÙr½¥ÙÖ PæÙBñGîîUB%¦bÎA­âNÏj4ÆÈ9ZQïE´vo4¢êõGPpwz–«UV êܼùA¢ˆAùP#¢Ð÷Ç}æ=3ýμït÷LOŸï§(ê=}zzÞ™3ûœ>O?ý¼aš¦`þ=aÖƒ€5A@€š  @MР&èPt¨ :Ôj‚€5A@€š  @MР&èPt¨ :Ôj‚€5A@€š  @MР&èPt¨ :Ôj‚€5A@€š  @MР&ž4ë ØèÂ0œõæIš¦¶oÐÌÞˆ Óè_~¤\ &èPtKÇc³öI’„a˜$IQÍK–Ÿ€®*ܹ( £Èyºèõz³›Èxœ¡¨¿8Žåä:Žãáo…a†aEfìx¬ d‹~­7|ð)ã @ÍEQ$gÖÍf³Ó騾ÕëõZ­V·Û•x­’$‘}½n¯ÎÐÔ™„Ýf³™¦i’$ï½^¯ÝnË·ºÝn0µeähb`‡Yá @I5“!r2.c3¾OŽåŒþÈ‘#“=¼@œ¡¨3-=Ô-æ8IÉ­‡a8qDZœæ·Z­œSÍ€`c13'­VKÂq·Û ý@ÇN§ÙlÎür¨ å Îä²§œ‰ËÍ·Hž8Þív%šÏãðö $äaÌýuŸcN-¢Ž~Š0¥k%€™ Ãi¢á«£™ÛÎâÝ3£ß« ßÇ$IäÚ®n‘Ë…¼HßV ¿°ó:ÿëEgêømC:Þ÷ä¯èøÄÍéøñ-ýñ£w¡ã#?x®Ž{áÏéø½ÿübŸü¹þ¾ý‰s¼æ ÀÄ™¥£ÑÝû¢¨\Õ•q³Ù”.¥û’j·Ûe$’F¼†m»>VøÓùâãL¬Êí®æ‹_@—÷}DÈ–pßétr8â#°¡Ó á—r›]òÚÍ:'‡„ÚÖ=·éøÚ×ü/Ÿ¿ýF?á;§èØL­Ø¶û2ó×ßy¾Ž_ñßÑñI×ÿ”Žï½á쉟 ¨±éäÐëaô{åW¶¨az8]n6*›ÕåØÈ&¯Ch3EQî|€ +×¢¤½ :rô4MÍ»ªÊδ˜Õ,7½ùÿèxûö[2÷Ï“7wÙßÜ~þ–~îþþÀÈãoïçõG.–Á‡>{‰n|àêmc'.rôÙØL¹z¹ÈíEÚ|rö} Ì¿$IBCf`è¸RÁôoÇŽma¤,P[ʘkÚù>é„]›ÚH¿›4M‡×v€‰uèr*in¹òÊ+§61æ ˜cµZ-©”¥¡|‚õí&/ÿ,/ßb+´Üsó{t|ÝŽwêØÌw›ŠÊ•^Ïn+`åí ã§}òNíèKÝÌòÒøÐÜbÆÇ4MõGÙxå•W¾ímoKÓTw“›u½ÐÇKgÄ©‘™èò¤ú„¾(mͨ/vàU‡a8¼ºiPlz`ü¥ãòA†¶?:ÂõHÄ0 ¤\dQPéÕívuM‰o¼ñOþäOä!rzš¦Ö{½^š¦‡žþäÍ? â8–É·Ûm túû)Iíà8ð ¢¢÷EQ³Ø|tÛ—¬–¾:ÓÀº„ˆá@qþùçË@Î5ì$I²ÿ~ùr&1]I«+™‰ü2Ó™¡OÎî'x.ï€.×Èå—LfL}T^Oæ0 &òk Š¢Ç\ÎÐÛíö¦œr ç¾fмÕji”@ª§êy²Ù“”-šÁ:óY“$‘•™lI}ñØ®/§^ú:¾ïµ×ô¿QP?—œ¸KúÄOj°ßg4¢¹cÇNŸ²Üï Ln„žkg&z½ÞðYc»ÝÖôú¹çž»{÷nùrÿþýeÏv)Ú‘¨mþÙ¡/ªÙlÊ<å¬<3 ãh’ö¹#Z-êjOò—ÅèffN†Ö<DE¶€ ÁGvþÒŒNiš9rdVçææK~9Íoå©ÿöèNÇ|ú^¯gVØhªKÊm‘oÉ%9¸t›™Ò×}ùãµçïj¶™–é›$å2H1¿”eŒÆ¦ó‡Èï ýDƒûú”ËoM0[Ø ¼ëÐþ¨îš›™7ý,R€iÖlêþg}þúÛm WxMÕ…K¹©¨Úö‰ŸèŽcýÜú®c¬crë˜_ôCwWüt¶o%Çé—½^/ó‚F†ÍfÓÜ3 rò™°ÊE‚uívÛü–y·‘œw›YõV«%å;R#9-‰ÉùJ`ƒóèæåJ¯õ Í¼¹„r­1ÏÖ…vʽùÀG3VÔ2r.u)m,5ÍbÚ¾£ß+ø>cüù³.ÐñS.oè˜ö¼ÀÆáГ$q¿h öh ù.<çê£åЯºêª·¾õ­Å>=ÑŠâÑœëèÑ£aîڵ몫®*oB€ÉxW ÝtÓMo}ë[o¹å– Þô¦7íÚµë¼óÎ+xNaxêÒOŠðî}à 3÷ñm{k“'ÿžžö°ŽÃûO;‡<Ï5ñÁ¯ø»KuüÁ¿z…޼öÙÅN˜e‹î nŸ{Þyç=z4MÓ/|á G}Ñ‹^†áUW]uÓM7å›'AÄql_ |Y}™ýQ¤ÞO®ÊXèžÚ…1ó»Ž&_‚n8²Óe á¼Ð*mó¡¬w!¥}æ"sÁñòsáïºó¸(j;ô¦M›Ž=êõ¬0šÂéÛ‚Dû÷ï—~P²›Y­ç¹¶€ 'wØë+¼;ŽyË™øð]ñJ£ù™(×ÈmA#v(ðÍ ÃðÄÞ*ã‡/è÷o)5]’Ì9»$ß½8ÁcŸõOªã“®þ¾Ži€éˆ\z뢹ú‚¬<'»É@2GŽÙ½{·|W–šËìôçuÓÌÄÌeäÌ_!ÑñE˜¥­Þo&U~åØR.ÅÜúEÑÀM¡¦N§3° ä!-¸eíÉTë$Ò6â*³·¢,=:…h>̼m~8:ë¹|¦ÑßµñH¹ŒxGÌÜï `˜.¨`žäš 8p@:žïÞ½{Äq¤³ˆÙF°<ÃPC¹þ_ÏÖGô;ýÝ&¿(ZªG^}Žü·ééw˳ž€©’Àg&dA"YS^NÆÃ0”h~äÈÛAÚí¶$m¦ÐÍIÊ̵Ÿ•fùåÌX®‘Ú2ú»#ä-ÿ4u•r Ãð·oz·Œ¯ÛñNÝ^TúØ·K!ý[¦ÉwbÞu†ŽŸþ•÷éø¡¥…b'd¢Ý]Áísh©ÍdkTŠ’7 ó{*Â5 ›ÝÌGì&W¥óGùsŸôÏ“=Жm°e\R(.­߉¸¹¿ÿ}—¼XÇïí^¬ã«>òÓ6¨&²E)ÒKÆæ½LB2éä^`&ßÇvê¸uc¿íÆWo›lbØ€†Ûçš·³uwK=¼ô4MÛív³Ù”Ž3aΤ;%€ºšÓ‹˜+Ï©Ìnºæ—ò¨ žnòn‹Ù%3×o:€ªÉ\±È<}Œ¢Èìkg†Â0 µ‘úô«6tq"íç.Z­–Dyé¿( ¡²Q^ï3NÐe¢aJúe²î½0¬ÙlJ`‘…,d£$^Ò4ÕØ—¦éáÇ8 _>|8MS³|ÃÌ ÏävY0Ú\/8¾v‡yJ>?õ»¾¿„¼sèr—¿¾¡2³bß©ÌßKÞó ÛþyjÆMeäÖ3[TþÚw¶ãU«n:{íí:þáÛNÐ1µê–¹]ÒF\šŽÈÿuŸn·Ûív5”/P'Ûµ •®i½ u$  DZYUߘk×™; ÇçÂrèò÷޶sév»iš–´JÑמ+ÿ=zÏ3ä¿ÂŸ@•é’Ð+IšWW,:|øðˆƒÈ£fµ L I’n·›™©ÖTŒãÙö¾}û$×´²²âQ‡nž•Y+Òqc€¢ØV,’®X$ß²­X_St&ù]èY¯‹ÊÿådÜ6%ùsdĄ͓ôF£±¶¶¶°°ÁÒÒÒ$ýйÃ@yôdQÖ òÎ'°;«$IF,p_*YUN…%Ÿ!¡\—·ž˜F|ùÒÙå­XYYY[[Û»wïÁƒƒ h4Ul¡†áØ‹ÙÅÖö¤¨út›Â—¦Ëó¢L¾}]\›Ç·¾v–޹×_Sð¾ööŸ ó¨¼^.Ú⻌ƒÏD†rÅ8MÓ}ûö:tHߺ}ûöå]±*«N¡|Øêêªùe£Ñ˜¼l0Ckkk[æé }]*ÃØ^Æq.i“Ÿ7¼ÿd¯ƒûævŠÊ)ùú¹§ü‹Ž¿uÙ‹t¼çæ7èøØ[¶ê˜ÒFÀÅÂÂÂ@Lç j‚€ó¤×ëIíx£Ñ0·¯®®Ð`ÎHù¦V+A°²²rèÐ¡Š–-þxí¹ÃۋʉûÓ·Ú¯Œ6¼™Ê¸eßå¹ÊÈÅSÚ¸‘±;iF¦õø+++KKKú]ÎÐ`ž˜wW-..¦iº¼¼¼¼¼œ¦ésWÑ” € K²šrÑ´ƒž›ÿÚ¯ýÚØ#A YõàøJÓív»×뙫¾É‘Ë[þbà;s-:­¹]þª0o rD@P-z+'ÑzÚ»k×.Ü|óͶÇFQ$ùŠÀX€T6êŠÒ*Žãf³™¹°çd†“$ºÚµ„f9%7W¤3רÓLËÄùŸ*V …aø½/=_Æ'nŸU¨ZÕÝØùØrDÓìOGo¸ï1ÍýO½ãý:>ý}÷è˜ÒÆy1\Šg& uñ¶v»mæÐõ!ò]Kú"XŸC׌Çþýû5|—”Cx^¹S_.xšÓÖ¨¯Îœ¼ùÝã.[$ €>…cÐë„:twsY‡ðE@€š¨hÙâýþŒ NßòuÝhû«¼ì¥ælû›ló1÷ײK[m¥­.ÓÅ4K]r_.û¯kà2²óþæËu|ÍÖ‹u¼uO¿œñÞúeŽ@]q†5A@€š  @MT±Z( ÃÕ›^&ãç4?×ßn©ðsYþ-OE Kk¯\¼ËÎeßšo*{É:—皸ñÞu†ŽŸyódÀšvDÙ¢;Ê`C¨h•Kã¼l¡ƒ.ŒVÑ€ž™rŒPÑ€þõ'l–Áöípmû»¤•M¶¸™5æC Ø%éï{aÀå ©B_ß¼¹Ëkßô¬þM ÷^ôì¹ù2Ýxì-[uLŸ€JîÄ2°d„™J6{¹LС°NÈ¡¨–¹^±(°,A§½ÃF¬H§ñ}j:€*X±¨×ëI·[ír>@Ãe¯× Ö¯J*ýƒ 8räˆ>V×6ÒA!2— “Yé"D¶5çäKyH†òB¼T4å`ÚڊEút®q‘¹ô:Œß:²ÒEæŠt“--*Ð{áÏÉàBÏrf[.;OÏÛÊpæt¶pæJry.¸l·%÷Ç®‡—“ïMe?¯I·<¸F7{óY:fM»ê‹8 ë|'X±H¿Ô‹Ì´†DÕ9g>𥞰Ëév§Ó‘_NæjJ+ÒÉZtÝnW/ ¸#å Z¦¹b‘.ãYÔI’ÈBr&.óѹ <ªÕjéJu,A`¾ÉY³œÃÊEÑ z½Þ—¾ô¥]»v…a˜™o1Ïyƒ  Ã0”ˆ)53šoúk£¨™]‚.ó©V¤ÓŒÿªxÇm†¿xã_ÊØ\ôÝÆå.ó¢î¶/ª‚Peæd’§Ÿ‹2ú(”1ŸÂþ”ÃÑñCK Å>†ÍêÖ]…®Àcº,AgÖYv»Ý(Š†×œ³Ímô{UÑ:”MÖ)-ö˜5‹|Í(EÑ@PŽÑÃû¸¨D@ÃP~MÍz"6 æ'rš}½À PØÈf|†n»†ûÏÿöL÷ƒøÞõž'›§UïØ%èŠZÚÍ—ïÚ~y.B˜-mÍ[ó]~]ÁV„úÀîWè˜V˜#³?—š÷ ãûÚÛ'{R ¿ÙôÉZÌþÖ@!f†žiË­?)[|ü ý?~79ÜvïûW¶™Æ9ÑØn;¦KÖblzÇ·ìÏ7Õà2™¢Òf)¤oJÇ…K¯¯Ó< Í{âwúûÜ{ÑKt|ƧþTÇ']ý}Ó*SÀ:Ôj‚€5QÑ%ètVݾC·ûvO-£»¬išU}…ðí÷k2sî¾m\RÒ&ßë^Œ2ú ضÿÏûûK_¾§ý<“O¯jörá j‚€5A@€š¨h}xã§¿ø»/Úù±þ>žõàÓÌwÍ®úö0Ùê¾mÛ}óæE­ÕçË÷¹fraÃe’¶‰}óþªŠ ~·Ži½;§$‡þ™Ã—ßuœßxájžCOûôWþ›õŒ`zÂ04ÿI’$Š¢0 ‡›îÛ·/ Ã0 WVV*z§(lXÃY Y1¢Ùlöz=³°Ñh¬­­-,,A°´´TÑ3tؘâ8Ø"gåiš&I"¡\öYYYY[[Û»wïêêêêêêÂÂBÕÏÐ>Þ¯C¿ðÎ/ë8Oª×·k‡ËBeæþ¶•Õ4uîÒ{ÄäÒLƶ=Ïrq¾ibßý}²L]ž$»o_Óé[n×ñݯê·Þ=eù}:~hia⹡’$ét:r&®e ý²Ùlv:8ŽWWWƒ 8xð lo4œ¡@U´Z­f³i;I ]5ªŸ¡Àüú~`üNÇIÔN’ļ:ÂÚÚÚÀ:”Å¥BO‚~ǽ^¯Ûíº|aaa ¦W= ß|ÿ/éøñ~íÂ}·¯;¦åø¦uÇÌÜc=Msûæ‚]zŽ»¬Õ—§öÜv‘À6‡ó-…´ÝÁ?¶„Ñ÷/}—ºIsû&Ïã¯kck¼pßœRž%ñl\ž×ëÃPToß6.Ç¿+x£Žÿ8¾XÇÛvõ›^~Ù€¢(j·ÛN§ÓéAÐn·5ô////--éH”-@µDQ”¦©YªÇqš¦Ýn7MS³¨qqq1MÓåååååå4M«~†™ÕèA,..Ê€3t¨‰ªŸ¡Ÿvø«:~ÂEÙ)ÝÌÛëÿÔ³ËqÖ±¥ƒ‹.ŒóMÑÚrʶ}l/л‚eß’G§ç²´CÛ^Øö†ø6„°þô yz'¼#¸^ÇÍöóu¼uO¿Ýî½7œ=vØP*Ї›ýÂÎëf2˜ èÚÒ¼¾:ÔDEÏЕYuûý/¿GÇ›,w–[óÝ.iYS®vãRÒ¾} L¾…ù¶T²m>&s—Úsßšô<=­ËïeÄ¥é±íÓ•ç'îrÁÃtþöuü7øMŸz)KÙÍØ×Ÿ°yÖSX‡3t¨ :Ôj¢ê9tÓÁ>OÇ—_ÏÜÇ·d;O‰·KÖÕvL/.éZ—<µoÑ}QëóÙæcò­£÷]º/³‘ŽíM³]ÈÓÆö¢|×#<}[)»û^û[:~jçãýííícƒZâ j‚€51O)—wÜ]ê2U4 ¾ÀWEz&Ûrt¦¢ªƒ}K³McÛØJ‰méÑGï:CÇ›ž•ÝóÀdë!ì’Æu)œ/ji:ÛÜl ù¡¸ÜvŸ§ÜEžý]Þ(Ó?>ï÷u|E|©ŽÍ¥cȧ×9t¨ :Ôjbžrèf¦ï3‡ëøÂ-_ûØ<ľÆörñ]÷Î%on²Õ›Ûjº}[ïÚäYBÏÆwk‰wÖ —'òíêcSön²=öíϹVÇÏê\¬ãm»úûO÷rW:aOì’p†5A@€š  @MÌSÝtðñ:>Ë™û¸d<]Z”ø¶ó{Ì<ùh—}òÔt—±–^Q'òÔÎ{Ðvp—O‘íø¾uáûé5óø—Ù_QàŒö÷tL>}®q†5A@€š˜×”Ë5û_ìx§]ŸÚÒ&.w¨›l™±wêçi™kþÕìÒ~Ö÷ø¶í…¯¥7â¹\*ü|ÓVzL[rÌöšÛóôpáÛÚöC±ý@mo¦Yø»pà›:ÞºçÙ:¾÷–²›œ¡@MР&*šr¡:øªh@¾ŸöÉ;û_\Òú¦‰sXjΗ™7ÏÌ̺¤_m)Q[4ÏÝä¾e‹.ieÛqlŠªœ¸lÑü$ØzÿúN Ïž§AžºØÓ·Ü®ã¿ùÃý:޺瀎ɧW)¨ :Ôj¢¢9ô±l­tÏßžÝÀ%%í²Ò›o¹ôØ&«¶”¨oÉsj»àYcn¾(“må6ßn´'n6ÒÄ™{Øã›+û²%î].˜û›OäûƒËÓØ÷Î —ç5ú¶~>ý¦ß{—Ž·îyŽÉ§W gèPt¨ :ÔļæÐMoúQ¿ãçùÁøVºfæÑÌšyskݺCÞÜ«:Ø¥äÙ¥¯™º5åÉAûöö]ú.OºÙåµxߥÁŽí'îòDe¬ÏçÒÆ¥—²É6‡ç4?§ã¿9¡¿Ï¯O¿ë±Sg=…u8C€š(% Çq†a†QÙö ×±'ÀEñ)—(Šz½^³Ù ‚ ×ë…a8â>~ÙM5ñ3Þ}Ó¯ô¿Xè] ¼|«î\2c÷)êvv—J;ß¿èmÙ—,í¯~óÍ÷½CÝfârO—ÔŠ¹ïúV¾|ß§ –ÇæIû˜Ïe–3šíN½ôÝ:~ðÚ~ë]LMÁ=I’^¯×n·ã8‚ ŽãN§“$É@¼N’$‚n·Ë‰9¥””‹†i[¼–€E‘ ù|†E‘$X’$I’¤Óéö°nöÈ¥;.äTVÙb«Õ’A»Ýþ®œ˜·Ûm9Iït:©öá~èÃtÿ-·®êF—Û÷]ø6&]wçý¸ìjž|wž•É\UQçÛÎ×7µ½ncìUé{Åeò./Ä·ÞÔå"ïº}y_4™ùôµWþ‘Ž5ŸN2}šÊ èišš'é’RWf¦EÎßR휰€¯‚sèÄeE‘Äñ±‰r.@`¯ù–óÝÌ ï}ûöÉCVVVНrét:cϯÃ0l6›èõi±“€92¢æ[’ØÍfs`{£ÑX[[[XX‚`ii©ø‹¢NGÎÍ£(’­çé­VK*eZò;G~hAúÌVº¯?Ò_šîíÁµ:öMËéŸIDATOž©jß»Ø3•‘Ï“ÏS'î2‡2–—{§»ïºz.õÝÇ%ÇíÒÂåÛ>¾-\® ˜ùô»_µW[÷¼G7nÀÞîFÔ|K Õ †aÇq¯¬¬¬­­íÝ»÷àÁƒA4‚S.QµÛí^¯×jµÂ0”ùÉlÌÄK’$ÍfSöiµZæÙ:lX™5ßzÚ.šÍ¦\›\]] ‚@¢yF£ø‹¢ò«c8‹"ÛõKÙaøž#Ø€F×|gÆI 誔€>âé'Þ æÑ'Îû£ñ;­7ºæÛ´¶¶6°¥ísMXû-¿ý9ýz•Å.Ý<ò¤°Ç*ª”Øw’.û¸¼iyÞ—–Âco0rdÄÄlòÔ†ûæÁmò\ä°åèõ.³ÙËìµûÛ7½{ì>AtÍ·iaaa ¦Ó>fotÍ·ãUF:ÌžÔÚ¾Ûëõ̱\#m4æ>«««t˜=¹ ¨- Íšïn·«cóÿZ­ÁÊÊÊ¡C‡F5+Ÿ•Ñ-ÔGÛ¶ëc:¾ã/Þ2vÿ¢Êº½2§^ý»'àò\.¥ÊyZ‹Øø¦_M…§w]®|äùT¸ÇöÂó\`pY Ð…ïòuf÷¤Î9¦cý`˜/äŽc;uü’·ü¦ŽÍ[Iæ…„©³>ÿáü‡úê¿#OjÏu»Ö¤|Ëܾ²²²´´¤á *!Žã4M»Ýn·ÛMÓÔ¼ªßؾ¸¸˜¦éòòòòòrš¦u«r€¹6¢˜Ûö­ÅÅEÔ- [Û<'» Àº*7ƒïJl.úûç¸å½¨6¶.ý­+±ÇÉÓn·¨JA›Ì×^Tïßë²D\¶¾Á¦l^s6w6o¹éÊ·~ Fà j‚€5A@€š¨sºiÏÍïÑñÇ·^£ã¢RØ¥*»3jž6¶¾û¸(5U]TMw…ü¾=}{¶U_ø¿Û ?­÷W:~ø’A•P‡(jb£¤\žrù×t|ï^˜¹OQÚŽ™ÙPÔ7Ýa“ç¯f—J>ß^Á¦²+üLcˆ•M; (ªm¯©ŒÜšiâþ .µ’O9ü?´´àþD%‘0uƧ>›ÿP߸è×I¹Ö©èEôC_ è„oðUÑ€^¸-·®êØ\zÜ\+OÆÐë&iÛjdæöG¾×¯[åËab.ùñ<·×ûvU5ŸËVvYTuàØbQyáÖ彚xb&ßûާ^¯ã“>ôbÿàUggí^sœ¡@MР&èP]‚nxcIó´-M—'ƒYê¢õ¾ÊNÅÚö1åi‡ë{|›±Ut¶«&6eÿdó¼Q.¯%OÞ<ûOÌ÷‰~öÍ]—ÑbW– ;ùº[Æï:Îw_¶«¨øVÑ‹¢ü5GÊj‚€5QÑ”ËÔÖö ´õì-ª1Žyì.ÿ ¯ÇÎ/ÎР&èPt¨ rè}§þªŽïxþNoß1y»¯4nQí;lÉiöuÉÓNÄ嘶÷pì{»®°ÝàRsm»Û ¨Î?¦<½VlÛͼ¹m—«A¶Þè™ï¡ïZ}¶el\ÞÜòû:>õÒ3u\F—Ùâ j‚€5Aʥϼ'ø”åþßel½]ÇSk‡kþuÿ˜%Õà²^ÝMó,fï»4ïjpcŸËö—~ÙËÈÙ&–çy‹*7t9Ž-o6¶®Ôö¢òÔkº°³Þ-*Ї[¢ÓPF«hÊ%2ë@éâ8Ã0 Ã(Š’$ÑíI’DQ$Û²oß>yÈÊÊJEÏÐ`£‰¢¨×ë5›Í z½^«Õêv»Á[­VÍf³×ëÉbIòF£±¶¶¶°°ÁÒÒ=ÛÓ>y§Ží8KÇÛ·Œ/aœ¸Û­oõ˜-·î2ÛÄ\ÚØú¦w]î>7¹Ü,>qËßèò¤¾·õ¯«15ä©ïÌ“†ž¸qàp‘ÆöfºÔ‰úNÆe’çoïwõxåE0ñóŠýûOå<‚I¢¹ž˜‡aDZœ›FÚY¶Çq¼²²²¶¶¶wïÞƒAÐh4*šr€ EâxÇæÆ^¯ôº±Ùlv: VWWƒ hA£Ñà f/Š"ób¡Ä÷v»­ß~ˆtE@€=òês&xTÇr>pÂ>`mmm` =›­&ý>Ï6^KÓÙè’+·És§¸)OÞÜdË›Ûæã›uõª»·ÕYÛÚØøÖƒÛÞLÛmô¶c–q“Až>™Ís·„ïvßOËWéJoÛõßu\`Mú‰¼uì>fÐO’D¯šU.™b:9t¨æÝnw š î‚€•ÐjµšÍfš¦Ãs¹:ªc¹FÚh4Ì}VWW è0{š. ²±ÛíêæÿµZ1‚•••C‡Í2‡®‰—lÑ =´´ ãϾ@ÇfA«É«\Ú·ÖÛw :—Råi.‰7¶Ûêà1ãÞýÃ6ùÀF3ú¤Vî$Ò›ŒÔââââââÊÊŠŒg“rI’¤×ëµÛí$I’$‘ZË*Ÿ¤ÀÌeV£ÇÃz0Û²E\Eò§DõíýÜëtücYq“Wy¢É¥ÐÊåS[ ÅwíSž¿úMe¬Öär‡}æ<'nÒà8¶h{Q¾i™ ÇkÉÓ:xâÚÞü<¹#—çúÖoÔñ¶]¯Õñ|µÕM@×{¢ä ]¢¹í—ÀÅŒo,’¢ËÀ¸ÃU ÷CFÎL3èišš'éZ¸C°_³©-‘ëŸfŽ% C-^œ—Š—×éçýßþœkÝhMwœJúrÜííR]WFÖÒ”§™j!«ÄùæÊ]ê/mµ=ožþÆ.|ï’÷­5eÎ-Ï›ìûI0ù~*Ìã<õŸÕñ×ìÌ>~¦izÒ‡nóz–L¼úœ¢"Þ̪\4Ù(Älºœ›ëKòåè¾b€ÑfÐÛí¶¬±†¡Ô¤SåyÌ8[=œLæ'‡¾uO?}vO{ïØýÇf‹Zn­(¾sÈS¾í"OêYSöµýŠº}?¢ú@”s·Ígì>y.B”qõÈö&› %þò³kÒ%L²<Ø‘|_Þ(*â͸ʅ³r( Ý &èP,A7¹{o8[Ç'_÷~ßß|¹ŽÇ¦•­©ÛòѾ9î<=^|[ŽøÖÝû>¯n÷Mź,Ãæ[Do²ÍÇ–V¶5Ò)*Åœ§¯‹mÎÊ·ËËÄ\ êm³½(s¡ÄûwÿâØ¹UgèPt¨ :Ô9ôbœþ¾{tüù-ã—©Syj«mÊ^è˶K‰·mßeð|ŸwôÆû˜%É'œs,óIó$÷}K­‹:Nžö‰{¹üèÖ:6óÔ&ß‹¶DžO¾9ç};¯7¾ÓÞ¹R8C€š¨èúp?ô¹¸wf¨¢}î·yOð©—öËï{m™º‰3.ûÛJ˸[Ý¥ý¯÷‚ô¶íEÏß·o°-Íâ›Sò]KÏEžÒIÛmý¾?ˆ±-Ì6YžÔäÛçÀ¦¨ÜÑ;‚~ÊeÛ®3Ç>ïl‘r€š  @MР&*šCŸk^ûlÛZŒåRýVF®Ùåyó´(ê&os>ë²±>½\ØRºyê,m|û»”NæéÉì’g·í?–Ë''Ïšy.oˆm[ný;ç4ú_Ü’¹ËŒq†5A@€š  @MC/—Ùàš­Ëà²3¯·ìž-Ït¾e×¾ùqßœ¸oë“m>¶wµuysŠ*—v©%/êb‰íÌc–7ÜöX›ÌyÚ~šyîT°}J}/l˜\»y×ßëøÁå ‚>ÁëYÊÆ:Ôj‚€5A½\f—­{~RŸ¾ïu_ÑfQ—R_“o¥­Ëq\¶çIéºäÓm †¹<×ÄMS Kõz¾iEÕ°ûîï;›±mxó¼yn˜x‘Â^ròm:~—×̦…3t¨ :ÔDES.µì‡~ï ?i«ûÔÎú/z‰Ž]þ8µq¹Izš·æO\28bÿBVw²æ|r¬à“gÂ…¯4‚íÖÿ<Ç·í£YŽ<ůeôöeÎáwûšŽ«™r©h@¯Aø€)#å5A@€š¨hÊ¥Þîko×±Ù_÷¾ß~±ŽóÜîRºWÊ¢ï™[Ýž×EQ yÜ÷IÍ ¹Gî:CÇ':Ã.í|mÛmm|ç“yÌ¢Šè½ž4§YÕ¼çÇ:Ôj‚€5A½B~ø¶þrV×t.Ö±¹d]ž~&¾À2Z|ø6Jµ¶)¢MJ=‡M¾É}ß|·‹ûÓœ’Ê“ϳ¨˜m.رys_E•TçI¯{c}5úƘëúÎÇwi:¯åëŠjàî2§++–ã”Ñf:*Ð7Bø€b‘r€j Ã0IsK’$Q…aEÑÀÎûöí Ã0 Õ••Šž¡Ã–~ Úý¡™~qIq¸c™ûø*ªE©ŒL/eçò¼@ß4‹KI¢ ¯ï ´¥Y|ß[>ÊeòæøŽc;½þÖkîâ8ÞØjµ‚ h6›½^/ CÍa4µµµ……… –––8C€Jˆã8 ÃN§3°]ÎÊÓ4M’DB¹ý•••µµµ½{÷®®®®®®.,,Р¢(j·ÛÍfs`{¯×376›M ú«««A\>Ó÷!îÖÖֶР,§.Ý9vŸ‰ƒþÂÂÂ@L'‡U7PÅhC@€ªëõzæX®‘6 sŸÕÕUR.sÆ–Oÿzg³Ž/ÛÒo·ë{󴩨V½.«šå)Ó~¢eu·Ìcæ)º÷}C|»¹æ¹¿ìÕ×L¾92™oˆí*ˆm»yp—ž¾ýmÿj®ùö¹Æ#â±Ç,J·ÛmµZñqÁñ²Åƒ:t¨Ñh¬®®®¬¬:tˆ€•&åŒNGªÛí¶½,///--ió+R.P!Q¥i:P§Çqš¦Ýn7MSóVÒÅÅÅ4M——————Ó4å æCf5z‹‹‹2  Ï1[>ý®¸Ÿa|ÇS¯ÆÉSŸ^TŸ—–#7;ÍSDoãÒ5Ooáo.érÉÌöT;fë«·éØ,müÖk~_Çnï‹úãÚå±&—Rȱ\vö}¾ò¤YL¾wûÌÁenæñmUƒÊ÷“à’ûrÙǬ+}ƇéØ,ä_¥ôLq›å.zzN¦ 1ãn‹Ds( ís &J©CGýØJ·îég!õ÷¾¤ãëv¼SǾÙRÛþ¶Š:ßܺ—2ZË–±ÔœS›ßó\wÛܲ[Toaó§ÿ˜gOão}í,¯+O¬EÞÜÄ:Ôj‚€5QÅÅÞX‚®Ì¥¹^ýòèøO½^Ç3¿Å¿ ¶\¶-ïìraÀ7ŸžgnyŒ­Áwj{›ãE™oà%ÇÞ ãcoÙªcÛÕ _¦¶î¹-ÿ¡¾ý‰s*½`úèP-[nwNF«b¶šz™¹õs_ú×:þøÖkt\F\3¶¶œµÉ¥çIžŽ¯¾ÏårÌiÖËûî31³ïʉ›³{¶˜5æ¿Üëßýp_{{±“@P":ÔD“¤\6 óO×\üï:¾å´÷ëØw¥¤Le§|o—/jŸ<+"ÙÊ(}K½æã;y3µ²íŸèøŒƒý$^Q%‰.$LmÛõ±ü‡ú·\Bʰj‚€5QÅl59tdzÊå_Óqë‚/èxù„þøôm·—7²{ ø¶0•}û¾oî>ó8.;»·Ÿ+ÿoßxŽg•+·!‡(j‚€5QÅlõ¼çЙÿ”™yÌïœÓ‚àÁå3O]ºsó®¿×íïýŸ–Á…[¾¬gr;»‹MO¿ûÑ{ž‘9SÙs³Õ­mW00uDZ:þõï¿\Çßýì™:®Â²p.ÿÈ¡JD@€š  @MT1[:Ü =˜«~ès—ƒ0ïóF¾3éù¯õS·§<«_÷ýǧÑñ¾'EÇ›žõu¯iLÜkåÉO~83ä˧•‹7óÍž©ã÷<þ_eð®Ýñ‰¼UÆ'®ÿÀ{o8Ûë‰fe~sè]àbÞ L_R.™gô3Üßó/vÿ <|yÃkÿMO¿Ûkÿ'-ü­×þ¾|ïëõUƒPQâ8Ã0 Ã(Š&xx:Ô@ÇN§Ùl6›Í^¯7AL¯b¶Ô7‡Ëþì?§Ð¦íßþÄ9§Åý“ÜG·Ý¯ãÿô3ÿ¢ãó6ý£ Þµ;þ‹#KºýWü¯:>õ„~þú´Mÿ&ƒÍ/øòמ«ÛÇæ¸7=ýns“™ã~ðG?É}7Îû¨9Ÿ»Ò~Nü¦Gÿ³ŽÿþžçÈà‘WŸsêÒº}Ë­«:Îl«RµDû—‘CðÙl&I|ÎÐ`ö4ŽË—2Ð/Р¢$Ê{H«§”7 U`4KÓ´Ýn3‚f³é<«X¶HLP¾Ñ,I’Éê[)˜½Ì8îÜ è0{»5i.߀^ŲEØ€¢(êõzÝn7Š"¹Ê7>s†>!Ǻt7ßò£²¹Ì_Òy²÷ÕöY Ãp.æÓšà.ó˜p5ÿA“—#ªÐ]æ9ð#¨ÔGÈë}®àé£Ëü«üO؆”‹7Ǻd‹&4’$I«q¹b²Òô<½â¢(j·ÛÕŸ­Ë<{½ÞÀC‚ î4)‡ûû,Ó®ÚOÄeþ•ý'<ʬ£ÌŸßÛ©¥þ_6ê‰@uòŽó×?“õs_—0Öðk¬&¯yVð qìü5§QÁɧãæ_ÙÂ#TñÆ¢Šs/'êõz½^O>ÊN'I’*œ^9Î?Š"iùÖétdK5/ÊmI’´Z­ ÌDAõ%IÒétÚívž;ef«šÿ„G å2!ÇŸkš¦ò!àXò¤<Œ¿VP¥ÇOdªY¥P{qK4ïv»óõ;µÕj5›Íùšó°ÊþÎD@÷6Ù ]ò±®Â¯wÇùˉ‰l—„cPùo(aÊInš¦óuž+•^¯'5 +[|é¢:ÿ„G åâM¯M™©†ÿ± ü>¯ÎçÀqþ˜9‰ r›ÉŒ§âOO„$îæ+ýRÙ£Ì2?·$§&éóm”Ô„œOÉX.6V­×eþæ…Ъͬù½(jþä=o®7£™f=Ó¼\—Â6œ¡O"I’0 %³7t™¿Ãå ¥Óéhê9­LÍ“Ëüå¾ÓéèEÑIî[ƒ§áÓÀ¼mÎn|e›ÓX»yù'lC/—É9&+*›Ó˜÷ùÓ1GÿèPT¹@MР&èæÛˆ8‡·¬ 1vÿùB@0ßZ­V«Õ¾eIJ¹Ìíqk7ºV«5PÖe–~e’} ›w èê`àä:ó\»ÓédÞªªÝrF‰H¡K•ov% ¨ƒá‚ýÚáš{ºDs!•é“ϲdÜX`îÉmúšQ ‚ Óé Ü»/]qh‹æ™yy9¾´]¬fY:@M \ðÔF•åæ ið Ý­QÞd®†Çq5/Ÿ’rPív[ÏLJÃwf@Ñë?MÓv»­ýŽÒ4Õ^å>ºtu`.¦(ù–±‘Æ[ò›ÀŒéf#Òá3ñj&[@M4›MÍ}»Ô¢ ü˜¯Ø‰€ &d™-[5Ë’WÈ›Ûò*ÕÌž :€šP®«€šÌ,J&)€Ñóúßqë–ÌãW@}H¨ηŒ èq7›MM¼ôz= ñòÀ;’*{oíslRBž3âròÐlaæ\£5 Ãv»Í:̘Ü4ñUM¹á¨Ê1“€`©ì]û…  @MPå5A@€š  @MР&èPt¨ :Ôj‚€5A@€š  @Mü#ײè‘n˜IEND®B`‚plumbum-1.7.2/examples/color.py0000755000232200023220000000137414161152302017061 0ustar debalancedebalance#!/usr/bin/env python # -*- coding: utf-8 -*- from __future__ import print_function from plumbum import colors with colors.fg.red: print("This is in red") print("This is completly restored, even if an exception is thrown!") print("The library will restore color on exiting automatially.") print(colors.bold["This is bold and exciting!"]) print(colors.bg.cyan | "This is on a cyan background.") print(colors.fg[42] | "If your terminal supports 256 colors, this is colorful!") print() for c in colors: print(c + u"\u2588", end="") colors.reset() print() print("Colors can be reset " + colors.underline["Too!"]) for c in colors[:16]: print(c["This is in color!"]) colors.red() print("This should clean up the color automatically on program exit...") plumbum-1.7.2/experiments/0000755000232200023220000000000014161152315016112 5ustar debalancedebalanceplumbum-1.7.2/experiments/parallel.py0000644000232200023220000001303714161152302020260 0ustar debalancedebalance# -*- coding: utf-8 -*- from plumbum.commands.base import BaseCommand from plumbum.commands.processes import CommandNotFound, ProcessExecutionError, run_proc def make_concurrent(self, rhs): if not isinstance(rhs, BaseCommand): raise TypeError("rhs must be an instance of BaseCommand") if isinstance(self, ConcurrentCommand): if isinstance(rhs, ConcurrentCommand): self.commands.extend(rhs.commands) else: self.commands.append(rhs) return self elif isinstance(rhs, ConcurrentCommand): rhs.commands.insert(0, self) return rhs else: return ConcurrentCommand(self, rhs) BaseCommand.__and__ = make_concurrent class ConcurrentPopen(object): def __init__(self, procs): self.procs = procs self.stdin = None self.stdout = None self.stderr = None self.custom_encoding = None self.returncode = None @property def argv(self): return [getattr(proc, "argv", []) for proc in self.procs] def poll(self): if self.returncode is not None: return self.returncode rcs = [proc.poll() for proc in self.procs] if any(rc is None for rc in rcs): return None self.returncode = 0 for rc in rcs: if rc != 0: self.returncode = rc break return self.returncode def wait(self): for proc in self.procs: proc.wait() return self.poll() def communicate(self, input=None): if input: raise ValueError("Cannot pass input to ConcurrentPopen.communicate") out_err_tuples = [proc.communicate() for proc in self.procs] self.wait() return tuple(zip(*out_err_tuples)) class ConcurrentCommand(BaseCommand): def __init__(self, *commands): self.commands = list(commands) def formulate(self, level=0, args=()): form = ["("] for cmd in self.commands: form.extend(cmd.formulate(level, args)) form.append("&") return form + [")"] def popen(self, *args, **kwargs): return ConcurrentPopen([cmd[args].popen(**kwargs) for cmd in self.commands]) def __getitem__(self, args): """Creates a bound-command with the given arguments""" if not isinstance(args, (tuple, list)): args = [ args, ] if not args: return self else: return ConcurrentCommand(*(cmd[args] for cmd in self.commands)) class Cluster(object): def __init__(self, *machines): self.machines = list(machines) def __enter__(self): return self def __exit__(self, t, v, tb): self.close() def close(self): for mach in self.machines: mach.close() del self.machines[:] def add_machine(self, machine): self.machines.append(machine) def __iter__(self): return iter(self.machines) def filter(self, pred): return self.__class__(filter(pred, self)) def which(self, progname): return [mach.which(progname) for mach in self] def list_processes(self): return [mach.list_processes() for mach in self] def pgrep(self, pattern): return [mach.pgrep(pattern) for mach in self] def path(self, *parts): return [mach.path(*parts) for mach in self] def __getitem__(self, progname): if not isinstance(progname, str): raise TypeError( "progname must be a string, not %r" % ( type( progname, ) ) ) return ConcurrentCommand(*(mach[progname] for mach in self)) def __contains__(self, cmd): try: self[cmd] except CommandNotFound: return False else: return True @property def python(self): return ConcurrentCommand(*(mach.python for mach in self)) def session(self): return ClusterSession(*(mach.session() for mach in self)) class ClusterSession(object): def __init__(self, *sessions): self.sessions = sessions def __iter__(self): return iter(self.sessions) def __enter__(self): return self def __exit__(self, t, v, tb): self.close() def __del__(self): try: self.close() except Exception: pass def alive(self): """Returns ``True`` if the underlying shells are all alive, ``False`` otherwise""" return all(session.alive for session in self) def close(self): """Closes (terminates) all underlying shell sessions""" for session in self.sessions: session.close() del self.sessions[:] def popen(self, cmd): return ConcurrentPopen([session.popen(cmd) for session in self]) def run(self, cmd, retcode=None): return run_proc(self.popen(cmd), retcode) if __name__ == "__main__": from plumbum import local from plumbum.cmd import date, ls, sleep c = ls & date & sleep[1] print(c()) c = ls & date & sleep[1] & sleep["-z"] try: c() except ProcessExecutionError as ex: print(ex) else: assert False clst = Cluster(local, local, local) print(clst["ls"]()) # This works fine print(local.session().run("echo $$")) # this does not ret, stdout, stderr = clst.session().run("echo $$") print(ret) ret = [int(pid) for pid in stdout] assert len(set(ret)) == 3 plumbum-1.7.2/experiments/test_parallel.py0000644000232200023220000000240114161152302021310 0ustar debalancedebalance# -*- coding: utf-8 -*- import unittest from parallel import Cluster from plumbum import SshMachine, local TEST_HOST = "127.0.0.1" class TestParallel(unittest.TestCase): def setUp(self): self.remotes = [] def connect(self): m = SshMachine(TEST_HOST) self.remotes.append(m) return m def tearDown(self): for m in self.remotes: m.close() def test_parallel(self): m = Cluster(local, local) import time t = time.time() ret = m["sleep"]("2") assert len(ret) == 2 assert 2 <= time.time() - t < 4 def test_locals(self): m = Cluster(local, local, local) # we should get 3 different proc ids ret = m["bash"]["-c"]["echo $$"]() ret = list(map(int, ret)) assert len(set(ret)) == 3 def test_sessions(self): m = Cluster(local, self.connect(), local, self.connect()) # we should get 4 different proc ids ret, stdout, stderr = m.session().run("echo $$") ret = [int(pid) for pid in stdout] assert len(set(ret)) == 4 def test_commands(self): cmds = local["echo"]["1"] & local["echo"]["2"] ret = cmds() a, b = map(int, ret) assert (a, b) == (1, 2) plumbum-1.7.2/.pre-commit-config.yaml0000644000232200023220000000265314161152302020032 0ustar debalancedebalanceci: autoupdate_commit_msg: "chore: update pre-commit hooks" autofix_commit_msg: "style: pre-commit fixes" repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.0.1 hooks: - id: check-added-large-files - id: check-case-conflict - id: check-merge-conflict - id: check-symlinks - id: check-yaml exclude: ^conda.recipe/meta.yaml$ - id: debug-statements - id: end-of-file-fixer - id: mixed-line-ending - id: requirements-txt-fixer - id: trailing-whitespace - id: fix-encoding-pragma - repo: https://github.com/psf/black rev: "21.12b0" hooks: - id: black - repo: https://github.com/PyCQA/isort rev: "5.10.1" hooks: - id: isort - repo: https://github.com/asottile/pyupgrade rev: "v2.29.1" hooks: - id: pyupgrade - repo: https://github.com/asottile/setup-cfg-fmt rev: "v1.20.0" hooks: - id: setup-cfg-fmt args: [--min-py3-version=3.5] - repo: https://github.com/pycqa/flake8 rev: "4.0.1" hooks: - id: flake8 exclude: docs/conf.py additional_dependencies: [flake8-bugbear, flake8-print, flake8-2020] stages: [manual] - repo: https://github.com/pre-commit/mirrors-mypy rev: "v0.930" hooks: - id: mypy files: plumbum stages: [manual] additional_dependencies: [typed-ast, types-paramiko] # This wants the .mo files removed - repo: https://github.com/mgedmin/check-manifest rev: "0.47" hooks: - id: check-manifest stages: [manual] plumbum-1.7.2/README.rst0000644000232200023220000001540614161152302015240 0ustar debalancedebalance.. image:: https://readthedocs.org/projects/plumbum/badge/ :target: https://plumbum.readthedocs.io/en/latest/ :alt: Documentation Status .. image:: https://github.com/tomerfiliba/plumbum/workflows/CI/badge.svg :target: https://github.com/tomerfiliba/plumbum/actions :alt: Build Status .. image:: https://coveralls.io/repos/tomerfiliba/plumbum/badge.svg?branch=master&service=github :target: https://coveralls.io/github/tomerfiliba/plumbum?branch=master :alt: Coverage Status .. image:: https://img.shields.io/pypi/v/plumbum.svg :target: https://pypi.python.org/pypi/plumbum/ :alt: PyPI Status .. image:: https://img.shields.io/pypi/pyversions/plumbum.svg :target: https://pypi.python.org/pypi/plumbum/ :alt: PyPI Versions .. image:: https://img.shields.io/conda/vn/conda-forge/plumbum.svg :target: https://github.com/conda-forge/plumbum-feedstock :alt: Conda-Forge Badge .. image:: https://img.shields.io/pypi/l/plumbum.svg :target: https://pypi.python.org/pypi/plumbum/ :alt: PyPI License .. image:: https://badges.gitter.im/plumbumpy/Lobby.svg :alt: Join the chat at https://gitter.im/plumbumpy/Lobby :target: https://gitter.im/plumbumpy/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :alt: Code styled with Black :target: https://github.com/psf/black Plumbum: Shell Combinators ========================== Ever wished the compactness of shell scripts be put into a **real** programming language? Say hello to *Plumbum Shell Combinators*. Plumbum (Latin for *lead*, which was used to create pipes back in the day) is a small yet feature-rich library for shell script-like programs in Python. The motto of the library is **"Never write shell scripts again"**, and thus it attempts to mimic the **shell syntax** ("shell combinators") where it makes sense, while keeping it all **Pythonic and cross-platform**. Apart from shell-like syntax and handy shortcuts, the library provides local and remote command execution (over SSH), local and remote file-system paths, easy working-directory and environment manipulation, and a programmatic Command-Line Interface (CLI) application toolkit. Now let's see some code! *This is only a teaser; the full documentation can be found at* `Read the Docs `_ Cheat Sheet ----------- Basics ****** .. code-block:: python >>> from plumbum import local >>> local.cmd.ls LocalCommand(/bin/ls) >>> local.cmd.ls() 'build.py\nCHANGELOG.rst\nconda.recipe\nCONTRIBUTING.rst\ndocs\nexamples\nexperiments\nLICENSE\nMANIFEST.in\nPipfile\nplumbum\nplumbum.egg-info\npytest.ini\nREADME.rst\nsetup.cfg\nsetup.py\ntests\ntranslations.py\n' >>> notepad = local["c:\\windows\\notepad.exe"] >>> notepad() # Notepad window pops up '' # Notepad window is closed by user, command returns In the example above, you can use ``local["ls"]`` if you have an unusually named executable or a full path to an executable. The ``local`` object represents your local machine. As you'll see, Plumbum also provides remote machines that use the same API! You can also use ``from plumbum.cmd import ls`` as well for accessing programs in the ``PATH``. Piping ****** .. code-block:: python >>> from plumbum.cmd import ls, grep, wc >>> chain = ls["-a"] | grep["-v", r"\.py"] | wc["-l"] >>> print(chain) /bin/ls -a | /bin/grep -v '\.py' | /usr/bin/wc -l >>> chain() '27\n' Redirection *********** .. code-block:: python >>> from plumbum.cmd import cat, head >>> ((cat < "setup.py") | head["-n", 4])() '#!/usr/bin/env python\nimport os\n\ntry:\n' >>> (ls["-a"] > "file.list")() '' >>> (cat["file.list"] | wc["-l"])() '31\n' Working-directory manipulation ****************************** .. code-block:: python >>> local.cwd >>> with local.cwd(local.cwd / "docs"): ... chain() ... '22\n' Foreground and background execution *********************************** .. code-block:: python >>> from plumbum import FG, BG >>> (ls["-a"] | grep[r"\.py"]) & FG # The output is printed to stdout directly build.py setup.py translations.py >>> (ls["-a"] | grep[r"\.py"]) & BG # The process runs "in the background" Command nesting *************** .. code-block:: python >>> from plumbum.cmd import sudo, ifconfig >>> print(sudo[ifconfig["-a"]]) /usr/bin/sudo /sbin/ifconfig -a >>> (sudo[ifconfig["-a"]] | grep["-i", "loop"]) & FG lo Link encap:Local Loopback UP LOOPBACK RUNNING MTU:16436 Metric:1 Remote commands (over SSH) ************************** Supports `openSSH `_-compatible clients, `PuTTY `_ (on Windows) and `Paramiko `_ (a pure-Python implementation of SSH2) .. code-block:: python >>> from plumbum import SshMachine >>> remote = SshMachine("somehost", user = "john", keyfile = "/path/to/idrsa") >>> r_ls = remote["ls"] >>> with remote.cwd("/lib"): ... (r_ls | grep["0.so.0"])() ... 'libusb-1.0.so.0\nlibusb-1.0.so.0.0.0\n' CLI applications **************** .. code-block:: python import logging from plumbum import cli class MyCompiler(cli.Application): verbose = cli.Flag(["-v", "--verbose"], help = "Enable verbose mode") include_dirs = cli.SwitchAttr("-I", list = True, help = "Specify include directories") @cli.switch("--loglevel", int) def set_log_level(self, level): """Sets the log-level of the logger""" logging.root.setLevel(level) def main(self, *srcfiles): print("Verbose:", self.verbose) print("Include dirs:", self.include_dirs) print("Compiling:", srcfiles) if __name__ == "__main__": MyCompiler.run() Sample output +++++++++++++ :: $ python simple_cli.py -v -I foo/bar -Ispam/eggs x.cpp y.cpp z.cpp Verbose: True Include dirs: ['foo/bar', 'spam/eggs'] Compiling: ('x.cpp', 'y.cpp', 'z.cpp') Colors and Styles ----------------- .. code-block:: python from plumbum import colors with colors.red: print("This library provides safe, flexible color access.") print(colors.bold | "(and styles in general)", "are easy!") print("The simple 16 colors or", colors.orchid & colors.underline | '256 named colors,', colors.rgb(18, 146, 64) | "or full rgb colors", 'can be used.') print("Unsafe " + colors.bg.dark_khaki + "color access" + colors.bg.reset + " is available too.") plumbum-1.7.2/plumbum/0000755000232200023220000000000014161152315015230 5ustar debalancedebalanceplumbum-1.7.2/plumbum/fs/0000755000232200023220000000000014161152315015640 5ustar debalancedebalanceplumbum-1.7.2/plumbum/fs/atomic.py0000644000232200023220000002322214161152302017463 0ustar debalancedebalance# -*- coding: utf-8 -*- """ Atomic file operations """ import atexit import os import sys import threading from contextlib import contextmanager from plumbum.lib import six from plumbum.machines.local import local if not hasattr(threading, "get_ident"): try: import thread except ImportError: import _thread as thread threading.get_ident = thread.get_ident del thread try: import fcntl except ImportError: import msvcrt try: from pywintypes import error as WinError from win32con import LOCKFILE_EXCLUSIVE_LOCK, LOCKFILE_FAIL_IMMEDIATELY from win32file import OVERLAPPED, LockFileEx, UnlockFile except ImportError: raise ImportError( "On Windows, we require Python for Windows Extensions (pywin32)" ) @contextmanager def locked_file(fileno, blocking=True): hndl = msvcrt.get_osfhandle(fileno) try: LockFileEx( hndl, LOCKFILE_EXCLUSIVE_LOCK | (0 if blocking else LOCKFILE_FAIL_IMMEDIATELY), 0xFFFFFFFF, 0xFFFFFFFF, OVERLAPPED(), ) except WinError: _, ex, _ = sys.exc_info() raise WindowsError(*ex.args) try: yield finally: UnlockFile(hndl, 0, 0, 0xFFFFFFFF, 0xFFFFFFFF) else: if hasattr(fcntl, "lockf"): @contextmanager def locked_file(fileno, blocking=True): fcntl.lockf(fileno, fcntl.LOCK_EX | (0 if blocking else fcntl.LOCK_NB)) try: yield finally: fcntl.lockf(fileno, fcntl.LOCK_UN) else: @contextmanager def locked_file(fileno, blocking=True): fcntl.flock(fileno, fcntl.LOCK_EX | (0 if blocking else fcntl.LOCK_NB)) try: yield finally: fcntl.flock(fileno, fcntl.LOCK_UN) class AtomicFile(object): """ Atomic file operations implemented using file-system advisory locks (``flock`` on POSIX, ``LockFile`` on Windows). .. note:: On Linux, the manpage says ``flock`` might have issues with NFS mounts. You should take this into account. .. versionadded:: 1.3 """ CHUNK_SIZE = 32 * 1024 def __init__(self, filename, ignore_deletion=False): self.path = local.path(filename) self._ignore_deletion = ignore_deletion self._thdlock = threading.Lock() self._owned_by = None self._fileobj = None self.reopen() def __repr__(self): return ( "".format(self.path) if self._fileobj else "" ) def __del__(self): self.close() def __enter__(self): return self def __exit__(self, t, v, tb): self.close() def close(self): if self._fileobj is not None: self._fileobj.close() self._fileobj = None def reopen(self): """ Close and reopen the file; useful when the file was deleted from the file system by a different process """ self.close() self._fileobj = os.fdopen( os.open(str(self.path), os.O_CREAT | os.O_RDWR, 384), "r+b", 0 ) @contextmanager def locked(self, blocking=True): """ A context manager that locks the file; this function is reentrant by the thread currently holding the lock. :param blocking: if ``True``, the call will block until we can grab the file system lock. if ``False``, the call may fail immediately with the underlying exception (``IOError`` or ``WindowsError``) """ if self._owned_by == threading.get_ident(): yield return with self._thdlock: with locked_file(self._fileobj.fileno(), blocking): if not self.path.exists() and not self._ignore_deletion: raise ValueError("Atomic file removed from filesystem") self._owned_by = threading.get_ident() try: yield finally: self._owned_by = None def delete(self): """ Atomically delete the file (holds the lock while doing it) """ with self.locked(): self.path.delete() def _read_all(self): self._fileobj.seek(0) data = [] while True: buf = self._fileobj.read(self.CHUNK_SIZE) data.append(buf) if len(buf) < self.CHUNK_SIZE: break return six.b("").join(data) def read_atomic(self): """Atomically read the entire file""" with self.locked(): return self._read_all() def read_shared(self): """Read the file **without** holding the lock""" return self._read_all() def write_atomic(self, data): """Writes the given data atomically to the file. Note that it overwrites the entire file; ``write_atomic("foo")`` followed by ``write_atomic("bar")`` will result in only ``"bar"``. """ with self.locked(): self._fileobj.seek(0) while data: chunk = data[: self.CHUNK_SIZE] self._fileobj.write(chunk) data = data[len(chunk) :] self._fileobj.flush() self._fileobj.truncate() class AtomicCounterFile(object): """ An atomic counter based on AtomicFile. Each time you call ``next()``, it will atomically read and increment the counter's value, returning its previous value Example:: acf = AtomicCounterFile.open("/some/file") print acf.next() # e.g., 7 print acf.next() # 8 print acf.next() # 9 .. versionadded:: 1.3 """ def __init__(self, atomicfile, initial=0): """ :param atomicfile: an :class:`AtomicFile ` instance :param initial: the initial value (used when the first time the file is created) """ self.atomicfile = atomicfile self.initial = initial def __enter__(self): return self def __exit__(self, t, v, tb): self.close() def close(self): self.atomicfile.close() @classmethod def open(cls, filename): """ Shortcut for ``AtomicCounterFile(AtomicFile(filename))`` """ return cls(AtomicFile(filename)) def reset(self, value=None): """ Reset the counter's value to the one given. If ``None``, it will default to the initial value provided to the constructor """ if value is None: value = self.initial if not isinstance(value, six.integer_types): raise TypeError("value must be an integer, not {!r}".format(type(value))) self.atomicfile.write_atomic(str(value).encode("utf8")) def next(self): """ Read and increment the counter, returning its previous value """ with self.atomicfile.locked(): curr = self.atomicfile.read_atomic().decode("utf8") if not curr: curr = self.initial else: curr = int(curr) self.atomicfile.write_atomic(str(curr + 1).encode("utf8")) return curr class PidFileTaken(SystemExit): """ This exception is raised when PidFile.acquire fails to lock the pid file. Note that it derives from ``SystemExit``, so unless explicitly handled, it will terminate the process cleanly """ def __init__(self, msg, pid): SystemExit.__init__(self, msg) self.pid = pid class PidFile(object): """ A PID file is a file that's locked by some process from the moment it starts until it dies (the OS will clear the lock when the process exits). It is used to prevent two instances of the same process (normally a daemon) from running concurrently. The PID file holds its process' PID, so you know who's holding it. .. versionadded:: 1.3 """ def __init__(self, filename): self.atomicfile = AtomicFile(filename) self._ctx = None def __enter__(self): self.acquire() def __exit__(self, t, v, tb): self.release() def __del__(self): try: self.release() except Exception: pass def close(self): self.atomicfile.close() def acquire(self): """ Attempt to acquire the PID file. If it's already locked, raises :class:`PidFileTaken `. You should normally acquire the file as early as possible when the program starts """ if self._ctx is not None: return self._ctx = self.atomicfile.locked(blocking=False) try: self._ctx.__enter__() except (IOError, OSError): self._ctx = None try: pid = self.atomicfile.read_shared().strip().decode("utf8") except (IOError, OSError): pid = "Unknown" raise PidFileTaken( "PID file {!r} taken by process {}".format(self.atomicfile.path, pid), pid, ) else: self.atomicfile.write_atomic(str(os.getpid()).encode("utf8")) atexit.register(self.release) def release(self): """ Release the PID file (should only happen when the program terminates) """ if self._ctx is None: return self.atomicfile.delete() try: self._ctx.__exit__(None, None, None) finally: self._ctx = None plumbum-1.7.2/plumbum/fs/__init__.py0000644000232200023220000000007714161152302017751 0ustar debalancedebalance# -*- coding: utf-8 -*- """ file-system related operations """ plumbum-1.7.2/plumbum/fs/mounts.py0000644000232200023220000000223614161152302017536 0ustar debalancedebalance# -*- coding: utf-8 -*- import re class MountEntry(object): """ Represents a mount entry (device file, mount point and file system type) """ def __init__(self, dev, point, fstype, options): self.dev = dev self.point = point self.fstype = fstype self.options = options.split(",") def __str__(self): return "{} on {} type {} ({})".format( self.dev, self.point, self.fstype, ",".join(self.options), ) MOUNT_PATTERN = re.compile(r"(.+?)\s+on\s+(.+?)\s+type\s+(\S+)(?:\s+\((.+?)\))?") def mount_table(): """returns the system's current mount table (a list of :class:`MountEntry ` objects)""" from plumbum.cmd import mount table = [] for line in mount().splitlines(): m = MOUNT_PATTERN.match(line) if not m: continue table.append(MountEntry(*m.groups())) return table def mounted(fs): """ Indicates if a the given filesystem (device file or mount point) is currently mounted """ return any(fs == entry.dev or fs == entry.point for entry in mount_table()) plumbum-1.7.2/plumbum/commands/0000755000232200023220000000000014161152315017031 5ustar debalancedebalanceplumbum-1.7.2/plumbum/commands/__init__.py0000644000232200023220000000143614161152302021142 0ustar debalancedebalance# -*- coding: utf-8 -*- from plumbum.commands.base import ( ERROUT, BaseCommand, ConcreteCommand, shquote, shquote_list, ) from plumbum.commands.modifiers import ( BG, FG, NOHUP, RETCODE, TEE, TF, ExecutionModifier, Future, ) from plumbum.commands.processes import ( CommandNotFound, ProcessExecutionError, ProcessLineTimedOut, ProcessTimedOut, run_proc, ) __all__ = ( "BaseCommand", "ConcreteCommand", "shquote", "shquote_list", "ERROUT", "BG", "FG", "NOHUP", "RETCODE", "TEE", "TF", "ExecutionModifier", "Future", "CommandNotFound", "ProcessExecutionError", "ProcessLineTimedOut", "ProcessTimedOut", "run_proc", ) def __dir__(): return __all__ plumbum-1.7.2/plumbum/commands/processes.py0000644000232200023220000003241414161152302021411 0ustar debalancedebalance# -*- coding: utf-8 -*- import atexit import heapq import sys import time from threading import Thread from plumbum.lib import IS_WIN32, six if sys.version_info >= (3,): from io import StringIO from queue import Empty as QueueEmpty from queue import Queue else: from cStringIO import StringIO from Queue import Empty as QueueEmpty from Queue import Queue # =================================================================================================== # utility functions # =================================================================================================== def _check_process(proc, retcode, timeout, stdout, stderr): proc.verify(retcode, timeout, stdout, stderr) return proc.returncode, stdout, stderr def _iter_lines_posix(proc, decode, linesize, line_timeout=None): try: from selectors import EVENT_READ, DefaultSelector except ImportError: # Pre Python 3.4 implementation from select import select def selector(): while True: rlist, _, _ = select([proc.stdout, proc.stderr], [], [], line_timeout) if not rlist and line_timeout: raise ProcessLineTimedOut( "popen line timeout expired", getattr(proc, "argv", None), getattr(proc, "machine", None), ) for stream in rlist: yield (stream is proc.stderr), decode(stream.readline(linesize)) else: # Python 3.4 implementation def selector(): sel = DefaultSelector() sel.register(proc.stdout, EVENT_READ, 0) sel.register(proc.stderr, EVENT_READ, 1) while True: ready = sel.select(line_timeout) if not ready and line_timeout: raise ProcessLineTimedOut( "popen line timeout expired", getattr(proc, "argv", None), getattr(proc, "machine", None), ) for key, mask in ready: yield key.data, decode(key.fileobj.readline(linesize)) for ret in selector(): yield ret if proc.poll() is not None: break for line in proc.stdout: yield 0, decode(line) for line in proc.stderr: yield 1, decode(line) def _iter_lines_win32(proc, decode, linesize, line_timeout=None): class Piper(Thread): def __init__(self, fd, pipe): super().__init__(name="PlumbumPiper%sThread" % fd) self.pipe = pipe self.fd = fd self.empty = False self.daemon = True super().start() def read_from_pipe(self): return self.pipe.readline(linesize) def run(self): for line in iter(self.read_from_pipe, b""): queue.put((self.fd, decode(line))) # self.pipe.close() if line_timeout is None: line_timeout = float("inf") queue = Queue() pipers = [Piper(0, proc.stdout), Piper(1, proc.stderr)] last_line_ts = time.time() empty = True while True: try: yield queue.get_nowait() last_line_ts = time.time() empty = False except QueueEmpty: empty = True if time.time() - last_line_ts > line_timeout: raise ProcessLineTimedOut( "popen line timeout expired", getattr(proc, "argv", None), getattr(proc, "machine", None), ) if proc.poll() is not None: break if empty: time.sleep(0.1) for piper in pipers: piper.join() while True: try: yield queue.get_nowait() except QueueEmpty: break if IS_WIN32: _iter_lines = _iter_lines_win32 else: _iter_lines = _iter_lines_posix # =================================================================================================== # Exceptions # =================================================================================================== class ProcessExecutionError(EnvironmentError): """Represents the failure of a process. When the exit code of a terminated process does not match the expected result, this exception is raised by :func:`run_proc `. It contains the process' return code, stdout, and stderr, as well as the command line used to create the process (``argv``) """ def __init__(self, argv, retcode, stdout, stderr, message=None): Exception.__init__(self, argv, retcode, stdout, stderr) self.message = message self.argv = argv self.retcode = retcode if six.PY3 and isinstance(stdout, six.bytes): stdout = six.ascii(stdout) if six.PY3 and isinstance(stderr, six.bytes): stderr = six.ascii(stderr) self.stdout = stdout self.stderr = stderr def __str__(self): # avoid an import cycle from plumbum.commands.base import shquote_list stdout = "\n | ".join(str(self.stdout).splitlines()) stderr = "\n | ".join(str(self.stderr).splitlines()) cmd = " ".join(shquote_list(self.argv)) lines = [] if self.message: lines = [self.message, "\nReturn code: | ", str(self.retcode)] else: lines = ["Unexpected exit code: ", str(self.retcode)] cmd = "\n | ".join(cmd.splitlines()) lines += ["\nCommand line: | ", cmd] if stdout: lines += ["\nStdout: | ", stdout] if stderr: lines += ["\nStderr: | ", stderr] return "".join(lines) class ProcessTimedOut(Exception): """Raises by :func:`run_proc ` when a ``timeout`` has been specified and it has elapsed before the process terminated""" def __init__(self, msg, argv): Exception.__init__(self, msg, argv) self.argv = argv class ProcessLineTimedOut(Exception): """Raises by :func:`iter_lines ` when a ``line_timeout`` has been specified and it has elapsed before the process yielded another line""" def __init__(self, msg, argv, machine): Exception.__init__(self, msg, argv, machine) self.argv = argv self.machine = machine class CommandNotFound(AttributeError): """Raised by :func:`local.which ` and :func:`RemoteMachine.which ` when a command was not found in the system's ``PATH``""" def __init__(self, program, path): Exception.__init__(self, program, path) self.program = program self.path = path # =================================================================================================== # Timeout thread # =================================================================================================== class MinHeap(object): def __init__(self, items=()): self._items = list(items) heapq.heapify(self._items) def __len__(self): return len(self._items) def push(self, item): heapq.heappush(self._items, item) def pop(self): heapq.heappop(self._items) def peek(self): return self._items[0] _timeout_queue = Queue() _shutting_down = False def _timeout_thread_func(): waiting = MinHeap() try: while not _shutting_down: if waiting: ttk, _ = waiting.peek() timeout = max(0, ttk - time.time()) else: timeout = None try: proc, time_to_kill = _timeout_queue.get(timeout=timeout) if proc is SystemExit: # terminate return waiting.push((time_to_kill, proc)) except QueueEmpty: pass now = time.time() while waiting: ttk, proc = waiting.peek() if ttk > now: break waiting.pop() try: if proc.poll() is None: proc.kill() proc._timed_out = True except EnvironmentError: pass except Exception: if _shutting_down: # to prevent all sorts of exceptions during interpreter shutdown pass else: raise bgthd = Thread(target=_timeout_thread_func, name="PlumbumTimeoutThread") bgthd.daemon = True bgthd.start() def _register_proc_timeout(proc, timeout): if timeout is not None: _timeout_queue.put((proc, time.time() + timeout)) def _shutdown_bg_threads(): global _shutting_down _shutting_down = True # Make sure this still exists (don't throw error in atexit!) if _timeout_queue: _timeout_queue.put((SystemExit, 0)) # grace period bgthd.join(0.1) atexit.register(_shutdown_bg_threads) # =================================================================================================== # run_proc # =================================================================================================== def run_proc(proc, retcode, timeout=None): """Waits for the given process to terminate, with the expected exit code :param proc: a running Popen-like object, with all the expected methods. :param retcode: the expected return (exit) code of the process. It defaults to 0 (the convention for success). If ``None``, the return code is ignored. It may also be a tuple (or any object that supports ``__contains__``) of expected return codes. :param timeout: the number of seconds (a ``float``) to allow the process to run, before forcefully terminating it. If ``None``, not timeout is imposed; otherwise the process is expected to terminate within that timeout value, or it will be killed and :class:`ProcessTimedOut ` will be raised :returns: A tuple of (return code, stdout, stderr) """ _register_proc_timeout(proc, timeout) stdout, stderr = proc.communicate() proc._end_time = time.time() if not stdout: stdout = six.b("") if not stderr: stderr = six.b("") if getattr(proc, "custom_encoding", None): stdout = stdout.decode(proc.custom_encoding, "ignore") stderr = stderr.decode(proc.custom_encoding, "ignore") return _check_process(proc, retcode, timeout, stdout, stderr) # =================================================================================================== # iter_lines # =================================================================================================== BY_POSITION = object() BY_TYPE = object() DEFAULT_ITER_LINES_MODE = BY_POSITION def iter_lines( proc, retcode=0, timeout=None, linesize=-1, line_timeout=None, mode=None, _iter_lines=_iter_lines, ): """Runs the given process (equivalent to run_proc()) and yields a tuples of (out, err) line pairs. If the exit code of the process does not match the expected one, :class:`ProcessExecutionError ` is raised. :param retcode: The expected return code of this process (defaults to 0). In order to disable exit-code validation, pass ``None``. It may also be a tuple (or any iterable) of expected exit codes. :param timeout: The maximal amount of time (in seconds) to allow the process to run. ``None`` means no timeout is imposed; otherwise, if the process hasn't terminated after that many seconds, the process will be forcefully terminated an exception will be raised :param linesize: Maximum number of characters to read from stdout/stderr at each iteration. ``-1`` (default) reads until a b'\\n' is encountered. :param line_timeout: The maximal amount of time (in seconds) to allow between consecutive lines in either stream. Raise an :class:`ProcessLineTimedOut ` if the timeout has been reached. ``None`` means no timeout is imposed. :returns: An iterator of (out, err) line tuples. """ if mode is None: mode = DEFAULT_ITER_LINES_MODE assert mode in (BY_POSITION, BY_TYPE) encoding = getattr(proc, "custom_encoding", None) or "utf-8" decode = lambda s: s.decode(encoding, errors="replace").rstrip() _register_proc_timeout(proc, timeout) buffers = [StringIO(), StringIO()] for t, line in _iter_lines(proc, decode, linesize, line_timeout): # verify that the proc hasn't timed out yet proc.verify(timeout=timeout, retcode=None, stdout=None, stderr=None) buffers[t].write(line + "\n") if mode is BY_POSITION: ret = [None, None] ret[t] = line yield tuple(ret) elif mode is BY_TYPE: yield (t + 1), line # 1=stdout, 2=stderr # this will take care of checking return code and timeouts _check_process(proc, retcode, timeout, *(s.getvalue() for s in buffers)) plumbum-1.7.2/plumbum/commands/base.py0000644000232200023220000004573114161152302020323 0ustar debalancedebalance# -*- coding: utf-8 -*- import functools import shlex import subprocess import sys from contextlib import contextmanager from subprocess import PIPE, Popen from tempfile import TemporaryFile from types import MethodType import plumbum.commands.modifiers from plumbum.commands.processes import iter_lines, run_proc from plumbum.lib import six class RedirectionError(Exception): """Raised when an attempt is made to redirect an process' standard handle, which was already redirected to/from a file""" # =================================================================================================== # Utilities # =================================================================================================== # modified from the stdlib pipes module for windows _safechars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@%_-+=:,./" _funnychars = '"`$\\' def shquote(text): """Quotes the given text with shell escaping (assumes as syntax similar to ``sh``)""" text = six.str(text) if sys.version_info >= (3, 3): return shlex.quote(text) else: import pipes return pipes.quote(text) def shquote_list(seq): return [shquote(item) for item in seq] # =================================================================================================== # Commands # =================================================================================================== class BaseCommand(object): """Base of all command objects""" __slots__ = ("cwd", "env", "custom_encoding", "__weakref__") def __str__(self): return " ".join(self.formulate()) def __or__(self, other): """Creates a pipe with the other command""" return Pipeline(self, other) def __gt__(self, file): """Redirects the process' stdout to the given file""" return StdoutRedirection(self, file) def __rshift__(self, file): """Redirects the process' stdout to the given file (appending)""" return AppendingStdoutRedirection(self, file) def __ge__(self, file): """Redirects the process' stderr to the given file""" return StderrRedirection(self, file) def __lt__(self, file): """Redirects the given file into the process' stdin""" return StdinRedirection(self, file) def __lshift__(self, data): """Redirects the given data into the process' stdin""" return StdinDataRedirection(self, data) def __getitem__(self, args): """Creates a bound-command with the given arguments. Shortcut for bound_command.""" if not isinstance(args, (tuple, list)): args = [ args, ] return self.bound_command(*args) def bound_command(self, *args): """Creates a bound-command with the given arguments""" if not args: return self if isinstance(self, BoundCommand): return BoundCommand(self.cmd, self.args + list(args)) else: return BoundCommand(self, args) def __call__(self, *args, **kwargs): """A shortcut for `run(args)`, returning only the process' stdout""" return self.run(args, **kwargs)[1] def _get_encoding(self): raise NotImplementedError() def with_env(self, **env): """Returns a BoundEnvCommand with the given environment variables""" if not env: return self return BoundEnvCommand(self, env=env) def with_cwd(self, path): """ Returns a BoundEnvCommand with the specified working directory. This overrides a cwd specified in a wrapping `machine.cwd()` context manager. """ if not path: return self return BoundEnvCommand(self, cwd=path) setenv = with_env @property def machine(self): raise NotImplementedError() def formulate(self, level=0, args=()): """Formulates the command into a command-line, i.e., a list of shell-quoted strings that can be executed by ``Popen`` or shells. :param level: The nesting level of the formulation; it dictates how much shell-quoting (if any) should be performed :param args: The arguments passed to this command (a tuple) :returns: A list of strings """ raise NotImplementedError() def popen(self, args=(), **kwargs): """Spawns the given command, returning a ``Popen``-like object. .. note:: When processes run in the **background** (either via ``popen`` or :class:`& BG `), their stdout/stderr pipes might fill up, causing them to hang. If you know a process produces output, be sure to consume it every once in a while, using a monitoring thread/reactor in the background. For more info, see `#48 `_ :param args: Any arguments to be passed to the process (a tuple) :param kwargs: Any keyword-arguments to be passed to the ``Popen`` constructor :returns: A ``Popen``-like object """ raise NotImplementedError() def nohup(self, cwd=".", stdout="nohup.out", stderr=None, append=True): """Runs a command detached.""" return self.machine.daemonic_popen(self, cwd, stdout, stderr, append) @contextmanager def bgrun(self, args=(), **kwargs): """Runs the given command as a context manager, allowing you to create a `pipeline `_ (not in the UNIX sense) of programs, parallelizing their work. In other words, instead of running programs one after the other, you can start all of them at the same time and wait for them to finish. For a more thorough review, see `Lightweight Asynchronism `_. Example:: from plumbum.cmd import mkfs with mkfs["-t", "ext3", "/dev/sda1"] as p1: with mkfs["-t", "ext3", "/dev/sdb1"] as p2: pass .. note:: When processes run in the **background** (either via ``popen`` or :class:`& BG `), their stdout/stderr pipes might fill up, causing them to hang. If you know a process produces output, be sure to consume it every once in a while, using a monitoring thread/reactor in the background. For more info, see `#48 `_ For the arguments, see :func:`run `. :returns: A Popen object, augmented with a ``.run()`` method, which returns a tuple of (return code, stdout, stderr) """ retcode = kwargs.pop("retcode", 0) timeout = kwargs.pop("timeout", None) p = self.popen(args, **kwargs) was_run = [False] def runner(): if was_run[0]: return # already done was_run[0] = True try: return run_proc(p, retcode, timeout) finally: del p.run # to break cyclic reference p -> cell -> p for f in [p.stdin, p.stdout, p.stderr]: try: f.close() except Exception: pass p.run = runner yield p runner() def run(self, args=(), **kwargs): """Runs the given command (equivalent to popen() followed by :func:`run_proc `). If the exit code of the process does not match the expected one, :class:`ProcessExecutionError ` is raised. :param args: Any arguments to be passed to the process (a tuple) :param retcode: The expected return code of this process (defaults to 0). In order to disable exit-code validation, pass ``None``. It may also be a tuple (or any iterable) of expected exit codes. .. note:: this argument must be passed as a keyword argument. :param timeout: The maximal amount of time (in seconds) to allow the process to run. ``None`` means no timeout is imposed; otherwise, if the process hasn't terminated after that many seconds, the process will be forcefully terminated an exception will be raised .. note:: this argument must be passed as a keyword argument. :param kwargs: Any keyword-arguments to be passed to the ``Popen`` constructor :returns: A tuple of (return code, stdout, stderr) """ with self.bgrun(args, **kwargs) as p: return p.run() def _use_modifier(self, modifier, args): """ Applies a modifier to the current object (e.g. FG, NOHUP) :param modifier: The modifier class to apply (e.g. FG) :param args: A dictionary of arguments to pass to this modifier :return: """ modifier_instance = modifier(**args) return self & modifier_instance def run_bg(self, **kwargs): """ Run this command in the background. Uses all arguments from the BG construct :py:class: `plumbum.commands.modifiers.BG` """ return self._use_modifier(plumbum.commands.modifiers.BG, kwargs) def run_fg(self, **kwargs): """ Run this command in the foreground. Uses all arguments from the FG construct :py:class: `plumbum.commands.modifiers.FG` """ return self._use_modifier(plumbum.commands.modifiers.FG, kwargs) def run_tee(self, **kwargs): """ Run this command using the TEE construct. Inherits all arguments from TEE :py:class: `plumbum.commands.modifiers.TEE` """ return self._use_modifier(plumbum.commands.modifiers.TEE, kwargs) def run_tf(self, **kwargs): """ Run this command using the TF construct. Inherits all arguments from TF :py:class: `plumbum.commands.modifiers.TF` """ return self._use_modifier(plumbum.commands.modifiers.TF, kwargs) def run_retcode(self, **kwargs): """ Run this command using the RETCODE construct. Inherits all arguments from RETCODE :py:class: `plumbum.commands.modifiers.RETCODE` """ return self._use_modifier(plumbum.commands.modifiers.RETCODE, kwargs) def run_nohup(self, **kwargs): """ Run this command using the NOHUP construct. Inherits all arguments from NOHUP :py:class: `plumbum.commands.modifiers.NOHUP` """ return self._use_modifier(plumbum.commands.modifiers.NOHUP, kwargs) class BoundCommand(BaseCommand): __slots__ = ("cmd", "args") def __init__(self, cmd, args): self.cmd = cmd self.args = list(args) def __repr__(self): return "BoundCommand({!r}, {!r})".format(self.cmd, self.args) def _get_encoding(self): return self.cmd._get_encoding() def formulate(self, level=0, args=()): return self.cmd.formulate(level + 1, self.args + list(args)) @property def machine(self): return self.cmd.machine def popen(self, args=(), **kwargs): if isinstance(args, six.string_types): args = [ args, ] return self.cmd.popen(self.args + list(args), **kwargs) class BoundEnvCommand(BaseCommand): __slots__ = ("cmd", "env", "cwd") def __init__(self, cmd, env=None, cwd=None): self.cmd = cmd self.env = env or {} self.cwd = cwd def __repr__(self): return "BoundEnvCommand({!r}, {!r})".format(self.cmd, self.env) def _get_encoding(self): return self.cmd._get_encoding() def formulate(self, level=0, args=()): return self.cmd.formulate(level, args) @property def machine(self): return self.cmd.machine def popen(self, args=(), cwd=None, env=None, **kwargs): env = env or {} return self.cmd.popen( args, cwd=self.cwd if cwd is None else cwd, env=dict(self.env, **env), **kwargs ) class Pipeline(BaseCommand): __slots__ = ("srccmd", "dstcmd") def __init__(self, srccmd, dstcmd): self.srccmd = srccmd self.dstcmd = dstcmd def __repr__(self): return "Pipeline({!r}, {!r})".format(self.srccmd, self.dstcmd) def _get_encoding(self): return self.srccmd._get_encoding() or self.dstcmd._get_encoding() def formulate(self, level=0, args=()): return ( self.srccmd.formulate(level + 1) + ["|"] + self.dstcmd.formulate(level + 1, args) ) @property def machine(self): return self.srccmd.machine def popen(self, args=(), **kwargs): src_kwargs = kwargs.copy() src_kwargs["stdout"] = PIPE if "stdin" in kwargs: src_kwargs["stdin"] = kwargs["stdin"] srcproc = self.srccmd.popen(args, **src_kwargs) kwargs["stdin"] = srcproc.stdout dstproc = self.dstcmd.popen(**kwargs) # allow p1 to receive a SIGPIPE if p2 exits srcproc.stdout.close() if srcproc.stderr is not None: dstproc.stderr = srcproc.stderr if srcproc.stdin and src_kwargs.get("stdin") != PIPE: srcproc.stdin.close() dstproc.srcproc = srcproc # monkey-patch .wait() to wait on srcproc as well (it's expected to die when dstproc dies) dstproc_wait = dstproc.wait @functools.wraps(Popen.wait) def wait2(*args, **kwargs): rc_dst = dstproc_wait(*args, **kwargs) rc_src = srcproc.wait(*args, **kwargs) dstproc.returncode = rc_dst or rc_src return dstproc.returncode dstproc._proc.wait = wait2 dstproc_verify = dstproc.verify def verify(proc, retcode, timeout, stdout, stderr): # TODO: right now it's impossible to specify different expected # return codes for different stages of the pipeline try: or_retcode = [0] + list(retcode) except TypeError: if retcode is None: or_retcode = None # no-retcode-verification acts "greedily" else: or_retcode = [0, retcode] proc.srcproc.verify(or_retcode, timeout, stdout, stderr) dstproc_verify(retcode, timeout, stdout, stderr) dstproc.verify = MethodType(verify, dstproc) dstproc.stdin = srcproc.stdin return dstproc class BaseRedirection(BaseCommand): __slots__ = ("cmd", "file") SYM = None # type: str KWARG = None # type: str MODE = None # type: str def __init__(self, cmd, file): self.cmd = cmd self.file = file def _get_encoding(self): return self.cmd._get_encoding() def __repr__(self): return "{}({!r}, {!r})".format(self.__class__.__name__, self.cmd, self.file) def formulate(self, level=0, args=()): return self.cmd.formulate(level + 1, args) + [ self.SYM, shquote(getattr(self.file, "name", self.file)), ] @property def machine(self): return self.cmd.machine def popen(self, args=(), **kwargs): from plumbum.machines.local import LocalPath from plumbum.machines.remote import RemotePath if self.KWARG in kwargs and kwargs[self.KWARG] not in (PIPE, None): raise RedirectionError("{} is already redirected".format(self.KWARG)) if isinstance(self.file, RemotePath): raise TypeError("Cannot redirect to/from remote paths") if isinstance(self.file, six.string_types + (LocalPath,)): f = kwargs[self.KWARG] = open(str(self.file), self.MODE) else: kwargs[self.KWARG] = self.file f = None try: return self.cmd.popen(args, **kwargs) finally: if f: f.close() class StdinRedirection(BaseRedirection): __slots__ = () SYM = "<" KWARG = "stdin" MODE = "r" class StdoutRedirection(BaseRedirection): __slots__ = () SYM = ">" KWARG = "stdout" MODE = "w" class AppendingStdoutRedirection(BaseRedirection): __slots__ = () SYM = ">>" KWARG = "stdout" MODE = "a" class StderrRedirection(BaseRedirection): __slots__ = () SYM = "2>" KWARG = "stderr" MODE = "w" class _ERROUT(int): def __repr__(self): return "ERROUT" def __str__(self): return "&1" ERROUT = _ERROUT(subprocess.STDOUT) class StdinDataRedirection(BaseCommand): __slots__ = ("cmd", "data") CHUNK_SIZE = 16000 def __init__(self, cmd, data): self.cmd = cmd self.data = data def _get_encoding(self): return self.cmd._get_encoding() def formulate(self, level=0, args=()): return [ "echo {}".format(shquote(self.data)), "|", self.cmd.formulate(level + 1, args), ] @property def machine(self): return self.cmd.machine def popen(self, args=(), **kwargs): if "stdin" in kwargs and kwargs["stdin"] != PIPE: raise RedirectionError("stdin is already redirected") data = self.data if isinstance(data, six.unicode_type) and self._get_encoding() is not None: data = data.encode(self._get_encoding()) f = TemporaryFile() while data: chunk = data[: self.CHUNK_SIZE] f.write(chunk) data = data[self.CHUNK_SIZE :] f.seek(0) # try: return self.cmd.popen(args, stdin=f, **kwargs) # finally: # f.close() class ConcreteCommand(BaseCommand): QUOTE_LEVEL = None # type: int __slots__ = ("executable", "custom_encoding") def __init__(self, executable, encoding): self.executable = executable self.custom_encoding = encoding self.cwd = None self.env = None def __str__(self): return str(self.executable) def __repr__(self): return "{}({})".format(type(self).__name__, self.executable) def _get_encoding(self): return self.custom_encoding def formulate(self, level=0, args=()): argv = [six.str(self.executable)] for a in args: if a is None: continue if isinstance(a, BaseCommand): if level >= self.QUOTE_LEVEL: argv.extend(shquote_list(a.formulate(level + 1))) else: argv.extend(a.formulate(level + 1)) elif isinstance(a, (list, tuple)): argv.extend( shquote(b) if level >= self.QUOTE_LEVEL else six.str(b) for b in a ) else: argv.append(shquote(a) if level >= self.QUOTE_LEVEL else six.str(a)) # if self.custom_encoding: # argv = [a.encode(self.custom_encoding) for a in argv if isinstance(a, six.string_types)] return argv plumbum-1.7.2/plumbum/commands/modifiers.py0000644000232200023220000004061514161152302021366 0ustar debalancedebalance# -*- coding: utf-8 -*- import os import sys import time from itertools import chain from select import select from subprocess import PIPE import plumbum.commands.base from plumbum.commands.processes import BY_TYPE, ProcessExecutionError, run_proc from plumbum.lib import read_fd_decode_safely class Future(object): """Represents a "future result" of a running process. It basically wraps a ``Popen`` object and the expected exit code, and provides poll(), wait(), returncode, stdout, and stderr. """ def __init__(self, proc, expected_retcode, timeout=None): self.proc = proc self._expected_retcode = expected_retcode self._timeout = timeout self._returncode = None self._stdout = None self._stderr = None def __repr__(self): return "".format( self.proc.argv, self._returncode if self.ready() else "running", ) def poll(self): """Polls the underlying process for termination; returns ``False`` if still running, or ``True`` if terminated""" if self.proc.poll() is not None: self.wait() return self._returncode is not None ready = poll def wait(self): """Waits for the process to terminate; will raise a :class:`plumbum.commands.ProcessExecutionError` in case of failure""" if self._returncode is not None: return self._returncode, self._stdout, self._stderr = run_proc( self.proc, self._expected_retcode, self._timeout ) @property def stdout(self): """The process' stdout; accessing this property will wait for the process to finish""" self.wait() return self._stdout @property def stderr(self): """The process' stderr; accessing this property will wait for the process to finish""" self.wait() return self._stderr @property def returncode(self): """The process' returncode; accessing this property will wait for the process to finish""" self.wait() return self._returncode # =================================================================================================== # execution modifiers # =================================================================================================== class ExecutionModifier(object): __slots__ = ("__weakref__",) def __repr__(self): """Automatically creates a representation for given subclass with slots. Ignore hidden properties.""" slots = {} for cls in self.__class__.__mro__: slots_list = getattr(cls, "__slots__", ()) if isinstance(slots_list, str): slots_list = (slots_list,) for prop in slots_list: if prop[0] != "_": slots[prop] = getattr(self, prop) mystrs = ("{} = {}".format(name, slots[name]) for name in slots) return "{}({})".format(self.__class__.__name__, ", ".join(mystrs)) @classmethod def __call__(cls, *args, **kwargs): return cls(*args, **kwargs) class _BG(ExecutionModifier): """ An execution modifier that runs the given command in the background, returning a :class:`Future ` object. In order to mimic shell syntax, it applies when you right-and it with a command. If you wish to expect a different return code (other than the normal success indicate by 0), use ``BG(retcode)``. Example:: future = sleep[5] & BG # a future expecting an exit code of 0 future = sleep[5] & BG(7) # a future expecting an exit code of 7 .. note:: When processes run in the **background** (either via ``popen`` or :class:`& BG `), their stdout/stderr pipes might fill up, causing them to hang. If you know a process produces output, be sure to consume it every once in a while, using a monitoring thread/reactor in the background. For more info, see `#48 `_ """ __slots__ = ("retcode", "kargs", "timeout") def __init__(self, retcode=0, timeout=None, **kargs): self.retcode = retcode self.kargs = kargs self.timeout = timeout def __rand__(self, cmd): return Future(cmd.popen(**self.kargs), self.retcode, timeout=self.timeout) BG = _BG() class _FG(ExecutionModifier): """ An execution modifier that runs the given command in the foreground, passing it the current process' stdin, stdout and stderr. Useful for interactive programs that require a TTY. There is no return value. In order to mimic shell syntax, it applies when you right-and it with a command. If you wish to expect a different return code (other than the normal success indicate by 0), use ``FG(retcode)``. Example:: vim & FG # run vim in the foreground, expecting an exit code of 0 vim & FG(7) # run vim in the foreground, expecting an exit code of 7 """ __slots__ = ("retcode", "timeout") def __init__(self, retcode=0, timeout=None): self.retcode = retcode self.timeout = timeout def __rand__(self, cmd): cmd( retcode=self.retcode, stdin=None, stdout=None, stderr=None, timeout=self.timeout, ) FG = _FG() class _TEE(ExecutionModifier): """Run a command, dumping its stdout/stderr to the current process's stdout and stderr, but ALSO return them. Useful for interactive programs that expect a TTY but also have valuable output. Use as: ls["-l"] & TEE Returns a tuple of (return code, stdout, stderr), just like ``run()``. """ __slots__ = ("retcode", "buffered", "timeout") def __init__(self, retcode=0, buffered=True, timeout=None): """`retcode` is the return code to expect to mean "success". Set `buffered` to False to disable line-buffering the output, which may cause stdout and stderr to become more entangled than usual. """ self.retcode = retcode self.buffered = buffered self.timeout = timeout def __rand__(self, cmd): with cmd.bgrun( retcode=self.retcode, stdin=None, stdout=PIPE, stderr=PIPE, timeout=self.timeout, ) as p: outbuf = [] errbuf = [] out = p.stdout err = p.stderr buffers = {out: outbuf, err: errbuf} tee_to = {out: sys.stdout, err: sys.stderr} done = False while not done: # After the process exits, we have to do one more # round of reading in order to drain any data in the # pipe buffer. Thus, we check poll() here, # unconditionally enter the read loop, and only then # break out of the outer loop if the process has # exited. done = p.poll() is not None # We continue this loop until we've done a full # `select()` call without collecting any input. This # ensures that our final pass -- after process exit -- # actually drains the pipe buffers, even if it takes # multiple calls to read(). progress = True while progress: progress = False ready, _, _ = select((out, err), (), ()) for fd in ready: buf = buffers[fd] data, text = read_fd_decode_safely(fd, 4096) if not data: # eof continue progress = True # Python conveniently line-buffers stdout and stderr for # us, so all we need to do is write to them # This will automatically add up to three bytes if it cannot be decoded tee_to[fd].write(text) # And then "unbuffered" is just flushing after each write if not self.buffered: tee_to[fd].flush() buf.append(data) stdout = "".join([x.decode("utf-8") for x in outbuf]) stderr = "".join([x.decode("utf-8") for x in errbuf]) return p.returncode, stdout, stderr TEE = _TEE() class _TF(ExecutionModifier): """ An execution modifier that runs the given command, but returns True/False depending on the retcode. This returns True if the expected exit code is returned, and false if it is not. This is useful for checking true/false bash commands. If you wish to expect a different return code (other than the normal success indicate by 0), use ``TF(retcode)``. If you want to run the process in the forground, then use ``TF(FG=True)``. Example:: local['touch']['/root/test'] & TF * Returns False, since this cannot be touched local['touch']['/root/test'] & TF(1) # Returns True local['touch']['/root/test'] & TF(FG=True) * Returns False, will show error message """ __slots__ = ("retcode", "FG", "timeout") def __init__(self, retcode=0, FG=False, timeout=None): """`retcode` is the return code to expect to mean "success". Set `FG` to True to run in the foreground. """ self.retcode = retcode self.FG = FG self.timeout = timeout @classmethod def __call__(cls, *args, **kwargs): return cls(*args, **kwargs) def __rand__(self, cmd): try: if self.FG: cmd( retcode=self.retcode, stdin=None, stdout=None, stderr=None, timeout=self.timeout, ) else: cmd(retcode=self.retcode, timeout=self.timeout) return True except ProcessExecutionError: return False TF = _TF() class _RETCODE(ExecutionModifier): """ An execution modifier that runs the given command, causing it to run and return the retcode. This is useful for working with bash commands that have important retcodes but not very useful output. If you want to run the process in the forground, then use ``RETCODE(FG=True)``. Example:: local['touch']['/root/test'] & RETCODE # Returns 1, since this cannot be touched local['touch']['/root/test'] & RETCODE(FG=True) * Returns 1, will show error message """ __slots__ = ("foreground", "timeout") def __init__(self, FG=False, timeout=None): """`FG` to True to run in the foreground.""" self.foreground = FG self.timeout = timeout @classmethod def __call__(cls, *args, **kwargs): return cls(*args, **kwargs) def __rand__(self, cmd): if self.foreground: return cmd.run( retcode=None, stdin=None, stdout=None, stderr=None, timeout=self.timeout )[0] else: return cmd.run(retcode=None, timeout=self.timeout)[0] RETCODE = _RETCODE() class _NOHUP(ExecutionModifier): """ An execution modifier that runs the given command in the background, disconnected from the current process, returning a standard popen object. It will keep running even if you close the current process. In order to slightly mimic shell syntax, it applies when you right-and it with a command. If you wish to use a diffent working directory or different stdout, stderr, you can use named arguments. The default is ``NOHUP( cwd=local.cwd, stdout='nohup.out', stderr=None)``. If stderr is None, stderr will be sent to stdout. Use ``os.devnull`` for null output. Will respect redirected output. Example:: sleep[5] & NOHUP # Outputs to nohup.out sleep[5] & NOHUP(stdout=os.devnull) # No output The equivelent bash command would be .. code-block:: bash nohup sleep 5 & """ __slots__ = ("cwd", "stdout", "stderr", "append") def __init__(self, cwd=".", stdout="nohup.out", stderr=None, append=True): """Set ``cwd``, ``stdout``, or ``stderr``. Runs as a forked process. You can set ``append=False``, too. """ self.cwd = cwd self.stdout = stdout self.stderr = stderr self.append = append def __rand__(self, cmd): if isinstance(cmd, plumbum.commands.base.StdoutRedirection): stdout = cmd.file append = False cmd = cmd.cmd elif isinstance(cmd, plumbum.commands.base.AppendingStdoutRedirection): stdout = cmd.file append = True cmd = cmd.cmd else: stdout = self.stdout append = self.append return cmd.nohup(self.cwd, stdout, self.stderr, append) NOHUP = _NOHUP() class PipeToLoggerMixin: """ This mixin allows piping plumbum commands' output into a logger. The logger must implement a ``log(level, msg)`` method, as in ``logging.Logger`` Example:: class MyLogger(logging.Logger, PipeToLoggerMixin): pass logger = MyLogger("example.app") Here we send the output of an install.sh script into our log:: local['./install.sh'] & logger We can choose the log-level for each stream:: local['./install.sh'] & logger.pipe(out_level=logging.DEBUG, err_level=logging.DEBUG) Or use a convenience method for it:: local['./install.sh'] & logger.pipe_debug() A prefix can be added to each line:: local['./install.sh'] & logger.pipe(prefix="install.sh: ") If the command fails, an exception is raised as usual. This can be modified:: local['install.sh'] & logger.pipe_debug(retcode=None) An exception is also raised if too much time (``DEFAULT_LINE_TIMEOUT``) passed between lines in the stream, This can also be modified:: local['install.sh'] & logger.pipe(line_timeout=10) If we happen to use logbook:: class MyLogger(logbook.Logger, PipeToLoggerMixin): from logbook import DEBUG, INFO # hook up with logbook's levels """ from logging import DEBUG, INFO DEFAULT_LINE_TIMEOUT = 10 * 60 DEFAULT_STDOUT = "INFO" DEFAULT_STDERR = "DEBUG" def pipe( self, out_level=None, err_level=None, prefix=None, line_timeout=None, **kw ): """ Pipe a command's stdout and stderr lines into this logger. :param out_level: the log level for lines coming from stdout :param err_level: the log level for lines coming from stderr Optionally use `prefix` for each line. """ class LogPipe(object): def __rand__(_, cmd): popen = cmd if hasattr(cmd, "iter_lines") else cmd.popen() for typ, lines in popen.iter_lines( line_timeout=line_timeout, mode=BY_TYPE, **kw ): if not lines: continue level = levels[typ] for line in lines.splitlines(): if prefix: line = "{}: {}".format(prefix, line) self.log(level, line) return popen.returncode levels = { 1: getattr(self, self.DEFAULT_STDOUT), 2: getattr(self, self.DEFAULT_STDERR), } if line_timeout is None: line_timeout = self.DEFAULT_LINE_TIMEOUT if out_level is not None: levels[1] = out_level if err_level is not None: levels[2] = err_level return LogPipe() def pipe_info(self, prefix=None, **kw): """ Pipe a command's stdout and stderr lines into this logger (both at level INFO) """ return self.pipe(self.INFO, self.INFO, prefix=prefix, **kw) def pipe_debug(self, prefix=None, **kw): """ Pipe a command's stdout and stderr lines into this logger (both at level DEBUG) """ return self.pipe(self.DEBUG, self.DEBUG, prefix=prefix, **kw) def __rand__(self, cmd): """ Pipe a command's stdout and stderr lines into this logger. Log levels for each stream are determined by ``DEFAULT_STDOUT`` and ``DEFAULT_STDERR``. """ return cmd & self.pipe( getattr(self, self.DEFAULT_STDOUT), getattr(self, self.DEFAULT_STDERR) ) plumbum-1.7.2/plumbum/commands/daemons.py0000644000232200023220000000712114161152302021026 0ustar debalancedebalance# -*- coding: utf-8 -*- import errno import os import signal import subprocess import sys import time import traceback from plumbum.commands.processes import ProcessExecutionError class _fake_lock(object): """Needed to allow normal os.exit() to work without error""" def acquire(self, val): return True def release(self): pass def posix_daemonize(command, cwd, stdout=None, stderr=None, append=True): if stdout is None: stdout = os.devnull if stderr is None: stderr = stdout MAX_SIZE = 16384 rfd, wfd = os.pipe() argv = command.formulate() firstpid = os.fork() if firstpid == 0: # first child: become session leader, os.close(rfd) rc = 0 try: os.setsid() os.umask(0) stdin = open(os.devnull, "r") stdout = open(stdout, "a" if append else "w") stderr = open(stderr, "a" if append else "w") signal.signal(signal.SIGHUP, signal.SIG_IGN) proc = command.popen( cwd=cwd, close_fds=True, stdin=stdin.fileno(), stdout=stdout.fileno(), stderr=stderr.fileno(), ) os.write(wfd, str(proc.pid).encode("utf8")) except Exception: rc = 1 tbtext = "".join(traceback.format_exception(*sys.exc_info()))[-MAX_SIZE:] os.write(wfd, tbtext.encode("utf8")) finally: os.close(wfd) os._exit(rc) else: # wait for first child to die os.close(wfd) _, rc = os.waitpid(firstpid, 0) output = os.read(rfd, MAX_SIZE) os.close(rfd) try: output = output.decode("utf8") except UnicodeError: pass if rc == 0 and output.isdigit(): secondpid = int(output) else: raise ProcessExecutionError(argv, rc, "", output) proc = subprocess.Popen.__new__(subprocess.Popen) proc._child_created = True proc.returncode = None proc.stdout = None proc.stdin = None proc.stderr = None proc.pid = secondpid proc.universal_newlines = False proc._input = None proc._waitpid_lock = _fake_lock() proc._communication_started = False proc.args = argv proc.argv = argv def poll(self=proc): if self.returncode is None: try: os.kill(self.pid, 0) except OSError: ex = sys.exc_info()[1] if ex.errno == errno.ESRCH: # process does not exist self.returncode = 0 else: raise return self.returncode def wait(self=proc): while self.returncode is None: if self.poll() is None: time.sleep(0.5) return proc.returncode proc.poll = poll proc.wait = wait return proc def win32_daemonize(command, cwd, stdout=None, stderr=None, append=True): if stdout is None: stdout = os.devnull if stderr is None: stderr = stdout DETACHED_PROCESS = 0x00000008 stdin = open(os.devnull, "r") stdout = open(stdout, "a" if append else "w") stderr = open(stderr, "a" if append else "w") return command.popen( cwd=cwd, stdin=stdin.fileno(), stdout=stdout.fileno(), stderr=stderr.fileno(), creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS, ) plumbum-1.7.2/plumbum/colorlib/0000755000232200023220000000000014161152315017035 5ustar debalancedebalanceplumbum-1.7.2/plumbum/colorlib/factories.py0000644000232200023220000001547414161152302021375 0ustar debalancedebalance# -*- coding: utf-8 -*- """ Color-related factories. They produce Styles. """ from __future__ import absolute_import, print_function import sys from functools import reduce from .names import color_names, default_styles from .styles import ColorNotFound __all__ = ["ColorFactory", "StyleFactory"] class ColorFactory(object): """This creates color names given fg = True/False. It usually will be called as part of a StyleFactory.""" def __init__(self, fg, style): self._fg = fg self._style = style self.reset = style.from_color(style.color_class(fg=fg)) # Adding the color name shortcuts for foreground colors for item in color_names[:16]: setattr( self, item, style.from_color(style.color_class.from_simple(item, fg=fg)) ) def __getattr__(self, item): """Full color names work, but do not populate __dir__.""" try: return self._style.from_color(self._style.color_class(item, fg=self._fg)) except ColorNotFound: raise AttributeError(item) def full(self, name): """Gets the style for a color, using standard name procedure: either full color name, html code, or number.""" return self._style.from_color( self._style.color_class.from_full(name, fg=self._fg) ) def simple(self, name): """Return the extended color scheme color for a value or name.""" return self._style.from_color( self._style.color_class.from_simple(name, fg=self._fg) ) def rgb(self, r, g=None, b=None): """Return the extended color scheme color for a value.""" if g is None and b is None: return self.hex(r) else: return self._style.from_color(self._style.color_class(r, g, b, fg=self._fg)) def hex(self, hexcode): """Return the extended color scheme color for a value.""" return self._style.from_color( self._style.color_class.from_hex(hexcode, fg=self._fg) ) def ansi(self, ansiseq): """Make a style from an ansi text sequence""" return self._style.from_ansi(ansiseq) def __getitem__(self, val): """\ Shortcut to provide way to access colors numerically or by slice. If end <= 16, will stay to simple ANSI version.""" if isinstance(val, slice): (start, stop, stride) = val.indices(256) if stop <= 16: return [self.simple(v) for v in range(start, stop, stride)] else: return [self.full(v) for v in range(start, stop, stride)] elif isinstance(val, tuple): return self.rgb(*val) try: return self.full(val) except ColorNotFound: return self.hex(val) def __call__(self, val_or_r=None, g=None, b=None): """Shortcut to provide way to access colors.""" if val_or_r is None or (isinstance(val_or_r, str) and val_or_r == ""): return self._style() if isinstance(val_or_r, self._style): return self._style(val_or_r) if isinstance(val_or_r, str) and "\033" in val_or_r: return self.ansi(val_or_r) return self._style.from_color( self._style.color_class(val_or_r, g, b, fg=self._fg) ) def __iter__(self): """Iterates through all colors in extended colorset.""" return (self.full(i) for i in range(256)) def __invert__(self): """Allows clearing a color with ~""" return self.reset def __enter__(self): """This will reset the color on leaving the with statement.""" return self def __exit__(self, type, value, traceback): """This resets a FG/BG color or all styles, due to different definition of RESET for the factories.""" self.reset.now() return False def __repr__(self): """Simple representation of the class by name.""" return "<{}>".format(self.__class__.__name__) class StyleFactory(ColorFactory): """Factory for styles. Holds font styles, FG and BG objects representing colors, and imitates the FG ColorFactory to a large degree.""" def __init__(self, style): super(StyleFactory, self).__init__(True, style) self.fg = ColorFactory(True, style) self.bg = ColorFactory(False, style) self.do_nothing = style() self.reset = style(reset=True) for item in style.attribute_names: setattr(self, item, style(attributes={item: True})) self.load_stylesheet(default_styles) @property def use_color(self): """Shortcut for setting color usage on Style""" return self._style.use_color @use_color.setter def use_color(self, val): self._style.use_color = val def from_ansi(self, ansi_sequence): """Calling this is a shortcut for creating a style from an ANSI sequence.""" return self._style.from_ansi(ansi_sequence) @property def stdout(self): """This is a shortcut for getting stdout from a class without an instance.""" return self._style._stdout if self._style._stdout is not None else sys.stdout @stdout.setter def stdout(self, newout): self._style._stdout = newout def get_colors_from_string(self, color=""): """ Sets color based on string, use `.` or space for separator, and numbers, fg/bg, htmlcodes, etc all accepted (as strings). """ names = color.replace(".", " ").split() prev = self styleslist = [] for name in names: try: prev = getattr(prev, name) except AttributeError: try: prev = prev(int(name)) except (ColorNotFound, ValueError): prev = prev(name) if isinstance(prev, self._style): styleslist.append(prev) prev = self if styleslist: prev = reduce(lambda a, b: a & b, styleslist) return prev if isinstance(prev, self._style) else prev.reset def filter(self, colored_string): """Filters out colors in a string, returning only the name.""" if isinstance(colored_string, self._style): return colored_string return self._style.string_filter_ansi(colored_string) def contains_colors(self, colored_string): """Checks to see if a string contains colors.""" return self._style.string_contains_colors(colored_string) def extract(self, colored_string): """Gets colors from an ansi string, returns those colors""" return self._style.from_ansi(colored_string, True) def load_stylesheet(self, stylesheet=default_styles): for item in stylesheet: setattr(self, item, self.get_colors_from_string(stylesheet[item])) plumbum-1.7.2/plumbum/colorlib/names.py0000644000232200023220000002004014161152302020502 0ustar debalancedebalance# -*- coding: utf-8 -*- """ Names for the standard and extended color set. Extended set is similar to `vim wiki `_, `colored `_, etc. Colors based on `wikipedia `_. You can access the index of the colors with names.index(name). You can access the rgb values with ``r=int(html[n][1:3],16)``, etc. """ from __future__ import division, print_function color_names = """\ black red green yellow blue magenta cyan light_gray dark_gray light_red light_green light_yellow light_blue light_magenta light_cyan white grey_0 navy_blue dark_blue blue_3 blue_3a blue_1 dark_green deep_sky_blue_4 deep_sky_blue_4a deep_sky_blue_4b dodger_blue_3 dodger_blue_2 green_4 spring_green_4 turquoise_4 deep_sky_blue_3 deep_sky_blue_3a dodger_blue_1 green_3 spring_green_3 dark_cyan light_sea_green deep_sky_blue_2 deep_sky_blue_1 green_3a spring_green_3a spring_green_2 cyan_3 dark_turquoise turquoise_2 green_1 spring_green_2a spring_green_1 medium_spring_green cyan_2 cyan_1 dark_red deep_pink_4 purple_4 purple_4a purple_3 blue_violet orange_4 grey_37 medium_purple_4 slate_blue_3 slate_blue_3a royal_blue_1 chartreuse_4 dark_sea_green_4 pale_turquoise_4 steel_blue steel_blue_3 cornflower_blue chartreuse_3 dark_sea_green_4a cadet_blue cadet_blue_a sky_blue_3 steel_blue_1 chartreuse_3a pale_green_3 sea_green_3 aquamarine_3 medium_turquoise steel_blue_1a chartreuse_2a sea_green_2 sea_green_1 sea_green_1a aquamarine_1 dark_slate_gray_2 dark_red_a deep_pink_4a dark_magenta dark_magenta_a dark_violet purple orange_4a light_pink_4 plum_4 medium_purple_3 medium_purple_3a slate_blue_1 yellow_4 wheat_4 grey_53 light_slate_grey medium_purple light_slate_blue yellow_4_a dark_olive_green_3 dark_sea_green light_sky_blue_3 light_sky_blue_3a sky_blue_2 chartreuse_2 dark_olive_green_3a pale_green_3a dark_sea_green_3 dark_slate_gray_3 sky_blue_1 chartreuse_1 light_green_a light_green_b pale_green_1 aquamarine_1a dark_slate_gray_1 red_3 deep_pink_4b medium_violet_red magenta_3 dark_violet_a purple_a dark_orange_3 indian_red hot_pink_3 medium_orchid_3 medium_orchid medium_purple_2 dark_goldenrod light_salmon_3 rosy_brown grey_63 medium_purple_2a medium_purple_1 gold_3 dark_khaki navajo_white_3 grey_69 light_steel_blue_3 light_steel_blue yellow_3 dark_olive_green_3b dark_sea_green_3a dark_sea_green_2 light_cyan_3 light_sky_blue_1 green_yellow dark_olive_green_2 pale_green_1a dark_sea_green_2a dark_sea_green_1 pale_turquoise_1 red_3a deep_pink_3 deep_pink_3a magenta_3a magenta_3b magenta_2 dark_orange_3a indian_red_a hot_pink_3a hot_pink_2 orchid medium_orchid_1 orange_3 light_salmon_3a light_pink_3 pink_3 plum_3 violet gold_3a light_goldenrod_3 tan misty_rose_3 thistle_3 plum_2 yellow_3a khaki_3 light_goldenrod_2 light_yellow_3 grey_84 light_steel_blue_1 yellow_2 dark_olive_green_1 dark_olive_green_1a dark_sea_green_1a honeydew_2 light_cyan_1 red_1 deep_pink_2 deep_pink_1 deep_pink_1a magenta_2a magenta_1 orange_red_1 indian_red_1 indian_red_1a hot_pink hot_pink_a medium_orchid_1a dark_orange salmon_1 light_coral pale_violet_red_1 orchid_2 orchid_1 orange_1 sandy_brown light_salmon_1 light_pink_1 pink_1 plum_1 gold_1 light_goldenrod_2a light_goldenrod_2b navajo_white_1 misty_rose_1 thistle_1 yellow_1 light_goldenrod_1 khaki_1 wheat_1 cornsilk_1 grey_10_0 grey_3 grey_7 grey_11 grey_15 grey_19 grey_23 grey_27 grey_30 grey_35 grey_39 grey_42 grey_46 grey_50 grey_54 grey_58 grey_62 grey_66 grey_70 grey_74 grey_78 grey_82 grey_85 grey_89 grey_93""".split() _greys = ( 3.4, 7.4, 11, 15, 19, 23, 26.7, 30.49, 34.6, 38.6, 42.4, 46.4, 50, 54, 58, 62, 66, 69.8, 73.8, 77.7, 81.6, 85.3, 89.3, 93, ) _grey_vals = [int(x / 100.0 * 16 * 16) for x in _greys] _grey_html = ["#" + format(x, "02x") * 3 for x in _grey_vals] _normals = [int(x, 16) for x in "0 5f 87 af d7 ff".split()] _normal_html = [ "#" + format(_normals[n // 36], "02x") + format(_normals[n // 6 % 6], "02x") + format(_normals[n % 6], "02x") for n in range(16 - 16, 232 - 16) ] _base_pattern = [(n // 4, n // 2 % 2, n % 2) for n in range(8)] _base_html = ( [ "#{2:02x}{1:02x}{0:02x}".format(x[0] * 192, x[1] * 192, x[2] * 192) for x in _base_pattern ] + ["#808080"] + [ "#{2:02x}{1:02x}{0:02x}".format(x[0] * 255, x[1] * 255, x[2] * 255) for x in _base_pattern ][1:] ) color_html = _base_html + _normal_html + _grey_html color_codes_simple = list(range(8)) + list(range(60, 68)) """Simple colors, remember that reset is #9, second half is non as common.""" # Attributes attributes_ansi = dict( bold=1, dim=2, italics=3, underline=4, reverse=7, hidden=8, strikeout=9, ) # Stylesheet default_styles = dict( warn="fg red", title="fg cyan underline bold", fatal="fg red bold", highlight="bg yellow", info="fg blue", success="fg green", ) # Functions to be used for color name operations class FindNearest(object): """This is a class for finding the nearest color given rgb values. Different find methods are available.""" def __init__(self, r, g, b): self.r = r self.b = b self.g = g def only_basic(self): """This will only return the first 8 colors! Breaks the colorspace into cubes, returns color""" midlevel = 0x40 # Since bright is not included # The colors are organised so that it is a # 3D cube, black at 0,0,0, white at 1,1,1 # Compressed to linear_integers r,g,b # [[[0,1],[2,3]],[[4,5],[6,7]]] # r*1 + g*2 + b*4 return ( (self.r >= midlevel) * 1 + (self.g >= midlevel) * 2 + (self.b >= midlevel) * 4 ) def all_slow(self, color_slice=slice(None, None, None)): """This is a slow way to find the nearest color.""" distances = [ self._distance_to_color(color) for color in color_html[color_slice] ] return min(range(len(distances)), key=distances.__getitem__) def _distance_to_color(self, color): """This computes the distance to a color, should be minimized.""" rgb = (int(color[1:3], 16), int(color[3:5], 16), int(color[5:7], 16)) return (self.r - rgb[0]) ** 2 + (self.g - rgb[1]) ** 2 + (self.b - rgb[2]) ** 2 def _distance_to_color_number(self, n): color = color_html[n] return self._distance_to_color(color) def only_colorblock(self): """This finds the nearest color based on block system, only works for 17-232 color values.""" rint = min( range(len(_normals)), key=[abs(x - self.r) for x in _normals].__getitem__ ) bint = min( range(len(_normals)), key=[abs(x - self.b) for x in _normals].__getitem__ ) gint = min( range(len(_normals)), key=[abs(x - self.g) for x in _normals].__getitem__ ) return 16 + 36 * rint + 6 * gint + bint def only_simple(self): """Finds the simple color-block color.""" return self.all_slow(slice(0, 16, None)) def only_grey(self): """Finds the greyscale color.""" rawval = (self.r + self.b + self.g) / 3 n = min( range(len(_grey_vals)), key=[abs(x - rawval) for x in _grey_vals].__getitem__, ) return n + 232 def all_fast(self): """Runs roughly 8 times faster than the slow version.""" colors = [self.only_simple(), self.only_colorblock(), self.only_grey()] distances = [self._distance_to_color_number(n) for n in colors] return colors[min(range(len(distances)), key=distances.__getitem__)] def from_html(color): """Convert html hex code to rgb.""" if len(color) != 7 or color[0] != "#": raise ValueError("Invalid length of html code") return (int(color[1:3], 16), int(color[3:5], 16), int(color[5:7], 16)) def to_html(r, g, b): """Convert rgb to html hex code.""" return "#{:02x}{:02x}{:02x}".format(r, g, b) plumbum-1.7.2/plumbum/colorlib/__init__.py0000644000232200023220000000204114161152302021137 0ustar debalancedebalance# -*- coding: utf-8 -*- """\ The ``ansicolor`` object provides ``bg`` and ``fg`` to access colors, and attributes like bold and underlined text. It also provides ``reset`` to recover the normal font. """ from __future__ import absolute_import, print_function from .factories import StyleFactory from .styles import ANSIStyle, ColorNotFound, HTMLStyle, Style ansicolors = StyleFactory(ANSIStyle) htmlcolors = StyleFactory(HTMLStyle) def load_ipython_extension(ipython): # pragma: no cover try: from ._ipython_ext import OutputMagics except ImportError: print("IPython required for the IPython extension to be loaded.") raise ipython.push({"colors": htmlcolors}) ipython.register_magics(OutputMagics) def main(): # pragma: no cover """Color changing script entry. Call using python -m plumbum.colors, will reset if no arguments given.""" import sys color = " ".join(sys.argv[1:]) if len(sys.argv) > 1 else "" ansicolors.use_color = True ansicolors.get_colors_from_string(color).now() plumbum-1.7.2/plumbum/colorlib/__main__.py0000644000232200023220000000034314161152302021123 0ustar debalancedebalance# -*- coding: utf-8 -*- """ This is provided as a quick way to recover your terminal. Simply run ``python -m plumbum.colorlib`` to recover terminal color. """ from __future__ import absolute_import from . import main main() plumbum-1.7.2/plumbum/colorlib/_ipython_ext.py0000644000232200023220000000220114161152302022107 0ustar debalancedebalance# -*- coding: utf-8 -*- import sys import IPython.display from IPython.core.magic import Magics, cell_magic, magics_class, needs_local_scope if sys.version_info >= (3,): from io import StringIO else: try: from cStringIO import StringIO except ImportError: from StringIO import StringIO # type: ignore valid_choices = [x[8:] for x in dir(IPython.display) if "display_" == x[:8]] @magics_class class OutputMagics(Magics): # pragma: no cover @needs_local_scope @cell_magic def to(self, line, cell, local_ns=None): choice = line.strip() assert choice in valid_choices, "Valid choices for '%%to' are: " + str( valid_choices ) display_fn = getattr(IPython.display, "display_" + choice) "Captures stdout and renders it in the notebook with some ." with StringIO() as out: old_out = sys.stdout try: sys.stdout = out exec(cell, self.shell.user_ns, local_ns) out.seek(0) display_fn(out.getvalue(), raw=True) finally: sys.stdout = old_out plumbum-1.7.2/plumbum/colorlib/styles.py0000644000232200023220000006171214161152302020735 0ustar debalancedebalance# -*- coding: utf-8 -*- """ This file provides two classes, `Color` and `Style`. ``Color`` is rarely used directly, but merely provides the workhorse for finding and manipulating colors. With the ``Style`` class, any color can be directly called or given to a with statement. """ from __future__ import absolute_import, print_function import os import platform import re import sys from abc import ABCMeta, abstractmethod from copy import copy from .names import ( FindNearest, attributes_ansi, color_codes_simple, color_html, color_names, from_html, ) if sys.version_info >= (3,): from abc import ABC else: from abc import ABCMeta ABC = ABCMeta( "ABC", (object,), {"__module__": __name__, "__slots__": ("__weakref__")} ) try: from typing import IO, Dict, Union except ImportError: pass __all__ = [ "Color", "Style", "ANSIStyle", "HTMLStyle", "ColorNotFound", "AttributeNotFound", ] _lower_camel_names = [n.replace("_", "") for n in color_names] def get_color_repr(): """Gets best colors for current system.""" if not sys.stdout.isatty(): return False term = os.environ.get("TERM", "") # Some terminals set TERM=xterm for compatibility if term.endswith("256color") or term == "xterm": return 3 if platform.system() == "Darwin" else 4 elif term.endswith("16color"): return 2 elif term == "screen": return 1 elif os.name == "nt": return 0 else: return 3 class ColorNotFound(Exception): """Thrown when a color is not valid for a particular method.""" pass class AttributeNotFound(Exception): """Similar to color not found, only for attributes.""" pass class ResetNotSupported(Exception): """An exception indicating that Reset is not available for this Style.""" pass class Color(ABC): """\ Loaded with ``(r, g, b, fg)`` or ``(color, fg=fg)``. The second signature is a short cut and will try full and hex loading. This class stores the idea of a color, rather than a specific implementation. It provides as many different tools for representations as possible, and can be subclassed to add more representations, though that should not be needed for most situations. ``.from_`` class methods provide quick ways to create colors given different representations. You will not usually interact with this class. Possible colors:: reset = Color() # The reset color by default background_reset = Color(fg=False) # Can be a background color blue = Color(0,0,255) # Red, Green, Blue green = Color.from_full("green") # Case insensitive name, from large colorset red = Color.from_full(1) # Color number white = Color.from_html("#FFFFFF") # HTML supported yellow = Color.from_simple("red") # Simple colorset The attributes are: .. data:: reset True it this is a reset color (following attributes don't matter if True) .. data:: rgb The red/green/blue tuple for this color .. data:: simple If true will stay to 16 color mode. .. data:: number The color number given the mode, closest to rgb if not rgb not exact, gives position of closest name. .. data:: fg This is a foreground color if True. Background color if False. """ __slots__ = ("fg", "isreset", "rgb", "number", "representation", "exact") def __init__(self, r_or_color=None, g=None, b=None, fg=True): """This works from color values, or tries to load non-simple ones.""" if isinstance(r_or_color, type(self)): for item in ("fg", "isreset", "rgb", "number", "representation", "exact"): setattr(self, item, getattr(r_or_color, item)) return self.fg = fg self.isreset = True # Starts as reset color self.rgb = (0, 0, 0) self.number = None "Number of the original color, or closest color" self.representation = 4 "0 for off, 1 for 8 colors, 2 for 16 colors, 3 for 256 colors, 4 for true color" self.exact = True "This is false if the named color does not match the real color" if None in (g, b): if not r_or_color: return try: self._from_simple(r_or_color) except ColorNotFound: try: self._from_full(r_or_color) except ColorNotFound: self._from_hex(r_or_color) elif None not in (r_or_color, g, b): self.rgb = (r_or_color, g, b) self._init_number() else: raise ColorNotFound("Invalid parameters for a color!") def _init_number(self): """Should always be called after filling in r, g, b, and representation. Color will not be a reset color anymore.""" if self.representation in (0, 1): number = FindNearest(*self.rgb).only_basic() elif self.representation == 2: number = FindNearest(*self.rgb).only_simple() elif self.representation in (3, 4): number = FindNearest(*self.rgb).all_fast() if self.number is None: self.number = number self.isreset = False self.exact = self.rgb == from_html(color_html[self.number]) if not self.exact: self.number = number @classmethod def from_simple(cls, color, fg=True): """Creates a color from simple name or color number""" self = cls(fg=fg) self._from_simple(color) return self def _from_simple(self, color): try: color = color.lower() color = color.replace(" ", "") color = color.replace("_", "") except AttributeError: pass if color == "reset": return elif color in _lower_camel_names[:16]: self.number = _lower_camel_names.index(color) self.rgb = from_html(color_html[self.number]) elif isinstance(color, int) and 0 <= color < 16: self.number = color self.rgb = from_html(color_html[color]) else: raise ColorNotFound("Did not find color: " + repr(color)) self.representation = 2 self._init_number() @classmethod def from_full(cls, color, fg=True): """Creates a color from full name or color number""" self = cls(fg=fg) self._from_full(color) return self def _from_full(self, color): try: color = color.lower() color = color.replace(" ", "") color = color.replace("_", "") except AttributeError: pass if color == "reset": return elif color in _lower_camel_names: self.number = _lower_camel_names.index(color) self.rgb = from_html(color_html[self.number]) elif isinstance(color, int) and 0 <= color <= 255: self.number = color self.rgb = from_html(color_html[color]) else: raise ColorNotFound("Did not find color: " + repr(color)) self.representation = 3 self._init_number() @classmethod def from_hex(cls, color, fg=True): """Converts #123456 values to colors.""" self = cls(fg=fg) self._from_hex(color) return self def _from_hex(self, color): try: self.rgb = from_html(color) except (TypeError, ValueError): raise ColorNotFound("Did not find htmlcode: " + repr(color)) self.representation = 4 self._init_number() @property def name(self): """The (closest) name of the current color""" if self.isreset: return "reset" else: return color_names[self.number] @property def name_camelcase(self): """The camelcase name of the color""" return self.name.replace("_", " ").title().replace(" ", "") def __repr__(self): """This class has a smart representation that shows name and color (if not unique).""" name = ["Deactivated:", " Basic:", "", " Full:", " True:"][self.representation] name += "" if self.fg else " Background" name += " " + self.name_camelcase name += "" if self.exact else " " + self.hex_code return name[1:] def __eq__(self, other): """Reset colors are equal, otherwise rgb have to match.""" if self.isreset: return other.isreset else: return self.rgb == other.rgb @property def ansi_sequence(self): """This is the ansi sequence as a string, ready to use.""" return "\033[" + ";".join(map(str, self.ansi_codes)) + "m" @property def ansi_codes(self): """This is the full ANSI code, can be reset, simple, 256, or full color.""" ansi_addition = 30 if self.fg else 40 if self.isreset: return (ansi_addition + 9,) elif self.representation < 3: return (color_codes_simple[self.number] + ansi_addition,) elif self.representation == 3: return (ansi_addition + 8, 5, self.number) else: return (ansi_addition + 8, 2, self.rgb[0], self.rgb[1], self.rgb[2]) @property def hex_code(self): """This is the hex code of the current color, html style notation.""" if self.isreset: return "#000000" else: return "#" + "{0[0]:02X}{0[1]:02X}{0[2]:02X}".format(self.rgb) def __str__(self): """This just prints it's simple name""" return self.name def to_representation(self, val): """Converts a color to any representation""" other = copy(self) other.representation = val if self.isreset: return other other.number = None other._init_number() return other def limit_representation(self, val): """Only converts if val is lower than representation""" if self.representation <= val: return self else: return self.to_representation(val) class Style(object): """This class allows the color changes to be called directly to write them to stdout, ``[]`` calls to wrap colors (or the ``.wrap`` method) and can be called in a with statement. """ __slots__ = ("attributes", "fg", "bg", "isreset", "__weakref__") color_class = Color """The class of color to use. Never hardcode ``Color`` call when writing a Style method.""" attribute_names = None # type: Union[Dict[str,str], Dict[str,int]] _stdout = None # type: IO end = "\n" """The endline character. Override if needed in subclasses.""" ANSI_REG = re.compile("\033" + r"\[([\d;]+)m") """The regular expression that finds ansi codes in a string.""" @property def stdout(self): """\ This property will allow custom, class level control of stdout. It will use current sys.stdout if set to None (default). Unfortunately, it only works on an instance.. """ # Import sys repeated here to make calling this stable in atexit function import sys return ( self.__class__._stdout if self.__class__._stdout is not None else sys.stdout ) @stdout.setter def stdout(self, newout): self.__class__._stdout = newout def __init__(self, attributes=None, fgcolor=None, bgcolor=None, reset=False): """This is usually initialized from a factory.""" if isinstance(attributes, type(self)): for item in ("attributes", "fg", "bg", "isreset"): setattr(self, item, copy(getattr(attributes, item))) return self.attributes = attributes if attributes is not None else dict() self.fg = fgcolor self.bg = bgcolor self.isreset = reset invalid_attributes = set(self.attributes) - set(self.attribute_names) if len(invalid_attributes) > 0: raise AttributeNotFound( "Attribute(s) not valid: " + ", ".join(invalid_attributes) ) @classmethod def from_color(cls, color): if color.fg: self = cls(fgcolor=color) else: self = cls(bgcolor=color) return self def invert(self): """This resets current color(s) and flips the value of all attributes present""" other = self.__class__() # Opposite of reset is reset if self.isreset: other.isreset = True return other # Flip all attributes for attribute in self.attributes: other.attributes[attribute] = not self.attributes[attribute] # Reset only if color present if self.fg: other.fg = self.fg.__class__() if self.bg: other.bg = self.bg.__class__() return other @property def reset(self): """Shortcut to access reset as a property.""" return self.invert() def __copy__(self): """Copy is supported, will make dictionary and colors unique.""" result = self.__class__() result.isreset = self.isreset result.fg = copy(self.fg) result.bg = copy(self.bg) result.attributes = copy(self.attributes) return result def __invert__(self): """This allows ~color.""" return self.invert() def __add__(self, other): """Adding two matching Styles results in a new style with the combination of both. Adding with a string results in the string concatenation of a style. Addition is non-commutative, with the rightmost Style property being taken if both have the same property. (Not safe)""" if type(self) == type(other): result = copy(other) result.isreset = self.isreset or other.isreset for attribute in self.attributes: if attribute not in result.attributes: result.attributes[attribute] = self.attributes[attribute] if not result.fg: result.fg = self.fg if not result.bg: result.bg = self.bg return result else: return other.__class__(self) + other def __radd__(self, other): """This only gets called if the string is on the left side. (Not safe)""" return other + other.__class__(self) def wrap(self, wrap_this): """Wrap a sting in this style and its inverse.""" return self + wrap_this + ~self def __and__(self, other): """This class supports ``color & color2`` syntax, and ``color & "String" syntax too.``""" if type(self) == type(other): return self + other else: return self.wrap(other) def __rand__(self, other): """This class supports ``"String:" & color`` syntax, excpet in Python 2.6 due to bug with that Python.""" return self.wrap(other) def __ror__(self, other): """Support for "String" | color syntax""" return self.wrap(other) def __or__(self, other): """This class supports ``color | color2`` syntax. It also supports ``"color | "String"`` syntax too.""" return self.__and__(other) def __call__(self): """\ This is a shortcut to print color immediately to the stdout. (Not safe) """ self.now() def now(self): """Immediately writes color to stdout. (Not safe)""" self.stdout.write(str(self)) def print(self, *printables, **kargs): """\ This acts like print; will print that argument to stdout wrapped in Style with the same syntax as the print function in 3.4.""" end = kargs.get("end", self.end) sep = kargs.get("sep", " ") file = kargs.get("file", self.stdout) flush = kargs.get("flush", False) file.write(self.wrap(sep.join(map(str, printables))) + end) if flush: file.flush() print_ = print """Shortcut just in case user not using __future__""" def __getitem__(self, wrapped): """The [] syntax is supported for wrapping""" return self.wrap(wrapped) def __enter__(self): """Context manager support""" self.stdout.write(str(self)) self.stdout.flush() def __exit__(self, type, value, traceback): """Runs even if exception occurred, does not catch it.""" self.stdout.write(str(~self)) self.stdout.flush() return False @property def ansi_codes(self): """Generates the full ANSI code sequence for a Style""" if self.isreset: return [0] codes = [] for attribute in self.attributes: if self.attributes[attribute]: codes.append(attributes_ansi[attribute]) else: # Fixing bold inverse being 22 instead of 21 on some terminals: codes.append( attributes_ansi[attribute] + 20 if attributes_ansi[attribute] != 1 else 22 ) if self.fg: codes.extend(self.fg.ansi_codes) if self.bg: self.bg.fg = False codes.extend(self.bg.ansi_codes) return codes @property def ansi_sequence(self): """This is the string ANSI sequence.""" codes = self.ansi_codes if codes: return "\033[" + ";".join(map(str, self.ansi_codes)) + "m" else: return "" def __repr__(self): name = self.__class__.__name__ attributes = ", ".join(a for a in self.attributes if self.attributes[a]) neg_attributes = ", ".join( "-" + a for a in self.attributes if not self.attributes[a] ) colors = ", ".join(repr(c) for c in [self.fg, self.bg] if c) string = "; ".join(s for s in [attributes, neg_attributes, colors] if s) if self.isreset: string = "reset" return "<{}: {}>".format(name, string if string else "empty") def __eq__(self, other): """Equality is true only if reset, or if attributes, fg, and bg match.""" if type(self) == type(other): if self.isreset: return other.isreset else: return ( self.attributes == other.attributes and self.fg == other.fg and self.bg == other.bg ) else: return str(self) == other @abstractmethod def __str__(self): """Base Style does not implement a __str__ representation. This is the one required method of a subclass.""" @classmethod def from_ansi(cls, ansi_string, filter_resets=False): """This generated a style from an ansi string. Will ignore resets if filter_resets is True.""" result = cls() res = cls.ANSI_REG.search(ansi_string) for group in res.groups(): sequence = map(int, group.split(";")) result.add_ansi(sequence, filter_resets) return result def add_ansi(self, sequence, filter_resets=False): """Adds a sequence of ansi numbers to the class. Will ignore resets if filter_resets is True.""" values = iter(sequence) try: while True: value = next(values) if value == 38 or value == 48: fg = value == 38 value = next(values) if value == 5: value = next(values) if fg: self.fg = self.color_class.from_full(value) else: self.bg = self.color_class.from_full(value, fg=False) elif value == 2: r = next(values) g = next(values) b = next(values) if fg: self.fg = self.color_class(r, g, b) else: self.bg = self.color_class(r, g, b, fg=False) else: raise ColorNotFound("the value 5 or 2 should follow a 38 or 48") elif value == 0: if filter_resets is False: self.isreset = True elif value in attributes_ansi.values(): for name in attributes_ansi: if value == attributes_ansi[name]: self.attributes[name] = True elif value in (20 + n for n in attributes_ansi.values()): if filter_resets is False: for name in attributes_ansi: if value == attributes_ansi[name] + 20: self.attributes[name] = False elif 30 <= value <= 37: self.fg = self.color_class.from_simple(value - 30) elif 40 <= value <= 47: self.bg = self.color_class.from_simple(value - 40, fg=False) elif 90 <= value <= 97: self.fg = self.color_class.from_simple(value - 90 + 8) elif 100 <= value <= 107: self.bg = self.color_class.from_simple(value - 100 + 8, fg=False) elif value == 39: if filter_resets is False: self.fg = self.color_class() elif value == 49: if filter_resets is False: self.bg = self.color_class(fg=False) else: raise ColorNotFound("The code {} is not recognised".format(value)) except StopIteration: return @classmethod def string_filter_ansi(cls, colored_string): """Filters out colors in a string, returning only the name.""" return cls.ANSI_REG.sub("", colored_string) @classmethod def string_contains_colors(cls, colored_string): """Checks to see if a string contains colors.""" return len(cls.ANSI_REG.findall(colored_string)) > 0 def to_representation(self, rep): """This converts both colors to a specific representation""" other = copy(self) if other.fg: other.fg = other.fg.to_representation(rep) if other.bg: other.bg = other.bg.to_representation(rep) return other def limit_representation(self, rep): """This only converts if true representation is higher""" if rep is True or rep is False: return self other = copy(self) if other.fg: other.fg = other.fg.limit_representation(rep) if other.bg: other.bg = other.bg.limit_representation(rep) return other @property def basic(self): """The color in the 8 color representation.""" return self.to_representation(1) @property def simple(self): """The color in the 16 color representation.""" return self.to_representation(2) @property def full(self): """The color in the 256 color representation.""" return self.to_representation(3) @property def true(self): """The color in the true color representation.""" return self.to_representation(4) class ANSIStyle(Style): """This is a subclass for ANSI styles. Use it to get color on sys.stdout tty terminals on posix systems. Set ``use_color = True/False`` if you want to control color for anything using this Style.""" __slots__ = () use_color = get_color_repr() attribute_names = attributes_ansi def __str__(self): if not self.use_color: return "" else: return self.limit_representation(self.use_color).ansi_sequence class HTMLStyle(Style): """This was meant to be a demo of subclassing Style, but actually can be a handy way to quickly color html text.""" __slots__ = () attribute_names = dict( bold="b", em="em", italics="i", li="li", underline='span style="text-decoration: underline;"', code="code", ol="ol start=0", strikeout="s", ) end = "
\n" def __str__(self): if self.isreset: raise ResetNotSupported("HTML does not support global resets!") result = "" if self.bg and not self.bg.isreset: result += ''.format(self.bg.hex_code) if self.fg and not self.fg.isreset: result += ''.format(self.fg.hex_code) for attr in sorted(self.attributes): if self.attributes[attr]: result += "<" + self.attribute_names[attr] + ">" for attr in reversed(sorted(self.attributes)): if not self.attributes[attr]: result += "" if self.fg and self.fg.isreset: result += "" if self.bg and self.bg.isreset: result += "" return result plumbum-1.7.2/plumbum/__init__.py0000644000232200023220000000612714161152302017343 0ustar debalancedebalance# -*- coding: utf-8 -*- r""" Plumbum Shell Combinators ------------------------- A wrist-handy library for writing shell-like scripts in Python, that can serve as a ``Popen`` replacement, and much more:: >>> from plumbum.cmd import ls, grep, wc, cat >>> ls() u'build.py\ndist\ndocs\nLICENSE\nplumbum\nREADME.rst\nsetup.py\ntests\ntodo.txt\n' >>> chain = ls["-a"] | grep["-v", "py"] | wc["-l"] >>> print chain /bin/ls -a | /bin/grep -v py | /usr/bin/wc -l >>> chain() u'12\n' >>> ((ls["-a"] | grep["-v", "py"]) > "/tmp/foo.txt")() u'' >>> ((cat < "/tmp/foo.txt") | wc["-l"])() u'12\n' >>> from plumbum import local, FG, BG >>> with local.cwd("/tmp"): ... (ls | wc["-l"]) & FG ... 13 # printed directly to the interpreter's stdout >>> (ls | wc["-l"]) & BG >>> f=_ >>> f.stdout # will wait for the process to terminate u'9\n' Plumbum includes local/remote path abstraction, working directory and environment manipulation, process execution, remote process execution over SSH, tunneling, SCP-based upload/download, and a {arg|opt}parse replacement for the easy creation of command-line interface (CLI) programs. See https://plumbum.readthedocs.io for full details """ # Avoids a circular import error later import plumbum.path # noqa: F401 from plumbum.commands import ( BG, ERROUT, FG, NOHUP, RETCODE, TEE, TF, CommandNotFound, ProcessExecutionError, ProcessLineTimedOut, ProcessTimedOut, ) from plumbum.machines import BaseRemoteMachine, PuttyMachine, SshMachine, local from plumbum.path import LocalPath, Path, RemotePath from plumbum.version import version __author__ = "Tomer Filiba (tomerfiliba@gmail.com)" __version__ = version __all__ = ( "BG", "ERROUT", "FG", "NOHUP", "RETCODE", "TEE", "TF", "CommandNotFound", "ProcessExecutionError", "ProcessLineTimedOut", "ProcessTimedOut", "BaseRemoteMachine", "PuttyMachine", "SshMachine", "local", "LocalPath", "Path", "RemotePath", "__author__", "__version__", "cmd", ) # =================================================================================================== # Module hack: ``from plumbum.cmd import ls`` # =================================================================================================== import sys from types import ModuleType try: from typing import List except ImportError: pass class LocalModule(ModuleType): """The module-hack that allows us to use ``from plumbum.cmd import some_program``""" __all__ = () # to make help() happy __package__ = __name__ def __getattr__(self, name): try: return local[name] except CommandNotFound: raise AttributeError(name) __path__ = [] # type: List[str] __file__ = __file__ cmd = LocalModule(__name__ + ".cmd", LocalModule.__doc__) sys.modules[cmd.__name__] = cmd del sys del ModuleType del LocalModule def __dir__(): "Support nice tab completion" return __all__ plumbum-1.7.2/plumbum/machines/0000755000232200023220000000000014161152315017017 5ustar debalancedebalanceplumbum-1.7.2/plumbum/machines/remote.py0000644000232200023220000003770314161152302020672 0ustar debalancedebalance# -*- coding: utf-8 -*- import re from contextlib import contextmanager from tempfile import NamedTemporaryFile from plumbum.commands import CommandNotFound, ConcreteCommand, shquote from plumbum.lib import ProcInfo, _setdoc, six from plumbum.machines.base import BaseMachine from plumbum.machines.env import BaseEnv from plumbum.machines.local import LocalPath from plumbum.path.remote import RemotePath, RemoteWorkdir, StatRes class RemoteEnv(BaseEnv): """The remote machine's environment; exposes a dict-like interface""" __slots__ = ["_orig", "remote"] def __init__(self, remote): self.remote = remote session = remote._session # GNU env has a -0 argument; use it if present. Otherwise, # fall back to calling printenv on each (possible) variable # from plain env. env0 = session.run("env -0; echo") if env0[0] == 0 and not env0[2].rstrip(): self._curr = dict( line.split("=", 1) for line in env0[1].split("\x00") if "=" in line ) else: lines = session.run("env; echo")[1].splitlines() split = (line.split("=", 1) for line in lines) keys = (line[0] for line in split if len(line) > 1) runs = ((key, session.run('printenv "%s"; echo' % key)) for key in keys) self._curr = { key: run[1].rstrip("\n") for (key, run) in runs if run[0] == 0 and run[1].rstrip("\n") and not run[2] } self._orig = self._curr.copy() BaseEnv.__init__(self, self.remote.path, ":") @_setdoc(BaseEnv) def __delitem__(self, name): BaseEnv.__delitem__(self, name) self.remote._session.run("unset {}".format(name)) @_setdoc(BaseEnv) def __setitem__(self, name, value): BaseEnv.__setitem__(self, name, value) self.remote._session.run("export {}={}".format(name, shquote(value))) @_setdoc(BaseEnv) def pop(self, name, *default): BaseEnv.pop(self, name, *default) self.remote._session.run("unset {}".format(name)) @_setdoc(BaseEnv) def update(self, *args, **kwargs): BaseEnv.update(self, *args, **kwargs) self.remote._session.run( "export " + " ".join("{}={}".format(k, shquote(v)) for k, v in self.getdict().items()) ) def expand(self, expr): """Expands any environment variables and home shortcuts found in ``expr`` (like ``os.path.expanduser`` combined with ``os.path.expandvars``) :param expr: An expression containing environment variables (as ``$FOO``) or home shortcuts (as ``~/.bashrc``) :returns: The expanded string""" return self.remote.expand(expr) def expanduser(self, expr): """Expand home shortcuts (e.g., ``~/foo/bar`` or ``~john/foo/bar``) :param expr: An expression containing home shortcuts :returns: The expanded string""" return self.remote.expanduser(expr) # def clear(self): # BaseEnv.clear(self, *args, **kwargs) # self.remote._session.run("export %s" % " ".join("%s=%s" % (k, v) for k, v in self.getdict())) def getdelta(self): """Returns the difference between the this environment and the original environment of the remote machine""" self._curr["PATH"] = self.path.join() delta = {} for k, v in self._curr.items(): if k not in self._orig: delta[k] = str(v) for k, v in self._orig.items(): if k not in self._curr: delta[k] = "" else: if v != self._curr[k]: delta[k] = self._curr[k] return delta class RemoteCommand(ConcreteCommand): __slots__ = ["remote", "executable"] QUOTE_LEVEL = 1 def __init__(self, remote, executable, encoding="auto"): self.remote = remote ConcreteCommand.__init__( self, executable, remote.custom_encoding if encoding == "auto" else encoding ) @property def machine(self): return self.remote def __repr__(self): return "RemoteCommand({!r}, {!r})".format(self.remote, self.executable) def popen(self, args=(), **kwargs): return self.remote.popen(self[args], **kwargs) def nohup(self, cwd=".", stdout="nohup.out", stderr=None, append=True): """Runs a command detached.""" return self.machine.daemonic_popen(self, cwd, stdout, stderr, append) class ClosedRemoteMachine(Exception): pass class ClosedRemote(object): __slots__ = ["_obj", "__weakref__"] def __init__(self, obj): self._obj = obj def close(self): pass def __getattr__(self, name): raise ClosedRemoteMachine("{!r} has been closed".format(self._obj)) class BaseRemoteMachine(BaseMachine): """Represents a *remote machine*; serves as an entry point to everything related to that remote machine, such as working directory and environment manipulation, command creation, etc. Attributes: * ``cwd`` - the remote working directory * ``env`` - the remote environment * ``custom_encoding`` - the remote machine's default encoding (assumed to be UTF8) * ``connect_timeout`` - the connection timeout There also is a _cwd attribute that exists if the cwd is not current (del if cwd is changed). """ # allow inheritors to override the RemoteCommand class RemoteCommand = RemoteCommand @property def cwd(self): if not hasattr(self, "_cwd"): self._cwd = RemoteWorkdir(self) return self._cwd def __init__(self, encoding="utf8", connect_timeout=10, new_session=False): self.custom_encoding = encoding self.connect_timeout = connect_timeout self._session = self.session(new_session=new_session) self.uname = self._get_uname() self.env = RemoteEnv(self) self._python = None def _get_uname(self): rc, out, _ = self._session.run("uname", retcode=None) if rc == 0: return out.strip() else: rc, out, _ = self._session.run( "python -c 'import platform;print(platform.uname()[0])'", retcode=None ) if rc == 0: return out.strip() else: # all POSIX systems should have uname. make an educated guess it's Windows return "Windows" def __repr__(self): return "<{} {}>".format(self.__class__.__name__, self) def __enter__(self): return self def __exit__(self, t, v, tb): self.close() def close(self): """closes the connection to the remote machine; all paths and programs will become defunct""" self._session.close() self._session = ClosedRemote(self) def path(self, *parts): """A factory for :class:`RemotePaths `. Usage: ``p = rem.path("/usr", "lib", "python2.7")`` """ parts2 = [str(self.cwd)] for p in parts: if isinstance(p, LocalPath): raise TypeError("Cannot construct RemotePath from {!r}".format(p)) parts2.append(self.expanduser(str(p))) return RemotePath(self, *parts2) def which(self, progname): """Looks up a program in the ``PATH``. If the program is not found, raises :class:`CommandNotFound ` :param progname: The program's name. Note that if underscores (``_``) are present in the name, and the exact name is not found, they will be replaced in turn by hyphens (``-``) then periods (``.``), and the name will be looked up again for each alternative :returns: A :class:`RemotePath ` """ alternatives = [progname] if "_" in progname: alternatives.append(progname.replace("_", "-")) alternatives.append(progname.replace("_", ".")) for name in alternatives: for p in self.env.path: fn = p / name if fn.access("x") and not fn.is_dir(): return fn raise CommandNotFound(progname, self.env.path) def __getitem__(self, cmd): """Returns a `Command` object representing the given program. ``cmd`` can be a string or a :class:`RemotePath `; if it is a path, a command representing this path will be returned; otherwise, the program name will be looked up in the system's ``PATH`` (using ``which``). Usage:: r_ls = rem["ls"] """ if isinstance(cmd, RemotePath): if cmd.remote is self: return self.RemoteCommand(self, cmd) else: raise TypeError( "Given path does not belong to this remote machine: {!r}".format( cmd ) ) elif not isinstance(cmd, LocalPath): if "/" in cmd or "\\" in cmd: return self.RemoteCommand(self, self.path(cmd)) else: return self.RemoteCommand(self, self.which(cmd)) else: raise TypeError("cmd must not be a LocalPath: {!r}".format(cmd)) @property def python(self): """A command that represents the default remote python interpreter""" if not self._python: self._python = self["python"] return self._python def session(self, isatty=False, new_session=False): """Creates a new :class:`ShellSession ` object; this invokes the user's shell on the remote machine and executes commands on it over stdin/stdout/stderr""" raise NotImplementedError() def download(self, src, dst): """Downloads a remote file/directory (``src``) to a local destination (``dst``). ``src`` must be a string or a :class:`RemotePath ` pointing to this remote machine, and ``dst`` must be a string or a :class:`LocalPath `""" raise NotImplementedError() def upload(self, src, dst): """Uploads a local file/directory (``src``) to a remote destination (``dst``). ``src`` must be a string or a :class:`LocalPath `, and ``dst`` must be a string or a :class:`RemotePath ` pointing to this remote machine""" raise NotImplementedError() def popen(self, args, **kwargs): """Spawns the given command on the remote machine, returning a ``Popen``-like object; do not use this method directly, unless you need "low-level" control on the remote process""" raise NotImplementedError() def list_processes(self): """ Returns information about all running processes (on POSIX systems: using ``ps``) .. versionadded:: 1.3 """ ps = self["ps"] lines = ps("-e", "-o", "pid,uid,stat,args").splitlines() lines.pop(0) # header for line in lines: parts = line.strip().split() yield ProcInfo(int(parts[0]), int(parts[1]), parts[2], " ".join(parts[3:])) def pgrep(self, pattern): """ Process grep: return information about all processes whose command-line args match the given regex pattern """ pat = re.compile(pattern) for procinfo in self.list_processes(): if pat.search(procinfo.args): yield procinfo @contextmanager def tempdir(self): """A context manager that creates a remote temporary directory, which is removed when the context exits""" _, out, _ = self._session.run( "mktemp -d 2>/dev/null || mktemp -d tmp.XXXXXXXXXX" ) dir = self.path(out.strip()) # @ReservedAssignment try: yield dir finally: dir.delete() # # Path implementation # def _path_listdir(self, fn): files = self._session.run("ls -a {}".format(shquote(fn)))[1].splitlines() files.remove(".") files.remove("..") return files def _path_glob(self, fn, pattern): # shquote does not work here due to the way bash loops use space as a seperator pattern = pattern.replace(" ", r"\ ") fn = fn.replace(" ", r"\ ") matches = self._session.run( r"for fn in {}/{}; do echo $fn; done".format(fn, pattern) )[1].splitlines() if len(matches) == 1 and not self._path_stat(matches[0]): return [] # pattern expansion failed return matches def _path_getuid(self, fn): stat_cmd = ( "stat -c '%u,%U' " if self.uname not in ("Darwin", "FreeBSD") else "stat -f '%u,%Su' " ) return self._session.run(stat_cmd + shquote(fn))[1].strip().split(",") def _path_getgid(self, fn): stat_cmd = ( "stat -c '%g,%G' " if self.uname not in ("Darwin", "FreeBSD") else "stat -f '%g,%Sg' " ) return self._session.run(stat_cmd + shquote(fn))[1].strip().split(",") def _path_stat(self, fn): if self.uname not in ("Darwin", "FreeBSD"): stat_cmd = "stat -c '%F,%f,%i,%d,%h,%u,%g,%s,%X,%Y,%Z' " else: stat_cmd = "stat -f '%HT,%Xp,%i,%d,%l,%u,%g,%z,%a,%m,%c' " rc, out, _ = self._session.run(stat_cmd + shquote(fn), retcode=None) if rc != 0: return None statres = out.strip().split(",") text_mode = statres.pop(0).lower() res = StatRes((int(statres[0], 16),) + tuple(int(sr) for sr in statres[1:])) res.text_mode = text_mode return res def _path_delete(self, fn): self._session.run("rm -rf {}".format(shquote(fn))) def _path_move(self, src, dst): self._session.run("mv {} {}".format(shquote(src), shquote(dst))) def _path_copy(self, src, dst): self._session.run("cp -r {} {}".format(shquote(src), shquote(dst))) def _path_mkdir(self, fn, mode=None, minus_p=True): p_str = "-p " if minus_p else "" cmd = "mkdir {}{}".format(p_str, shquote(fn)) self._session.run(cmd) def _path_chmod(self, mode, fn): self._session.run("chmod {:o} {}".format(mode, shquote(fn))) def _path_touch(self, path): self._session.run("touch {path}".format(path=path)) def _path_chown(self, fn, owner, group, recursive): args = ["chown"] if recursive: args.append("-R") if owner is not None and group is not None: args.append("{}:{}".format(owner, group)) elif owner is not None: args.append(str(owner)) elif group is not None: args.append(":{}".format(group)) args.append(shquote(fn)) self._session.run(" ".join(args)) def _path_read(self, fn): data = self["cat"](fn) if self.custom_encoding and isinstance(data, six.unicode_type): data = data.encode(self.custom_encoding) return data def _path_write(self, fn, data): if self.custom_encoding and isinstance(data, six.unicode_type): data = data.encode(self.custom_encoding) with NamedTemporaryFile() as f: f.write(data) f.flush() f.seek(0) self.upload(f.name, fn) def _path_link(self, src, dst, symlink): self._session.run( "ln {} {} {}".format("-s" if symlink else "", shquote(src), shquote(dst)) ) @_setdoc(BaseEnv) def expand(self, expr): return self._session.run("echo {}".format(expr))[1].strip() @_setdoc(BaseEnv) def expanduser(self, expr): if not any(part.startswith("~") for part in expr.split("/")): return expr # we escape all $ signs to avoid expanding env-vars return self._session.run("echo {}".format(expr.replace("$", "\\$")))[1].strip() plumbum-1.7.2/plumbum/machines/__init__.py0000644000232200023220000000057414161152302021132 0ustar debalancedebalance# -*- coding: utf-8 -*- from plumbum.machines.local import LocalCommand, LocalMachine, local from plumbum.machines.remote import BaseRemoteMachine, RemoteCommand from plumbum.machines.ssh_machine import PuttyMachine, SshMachine __all__ = ( "LocalCommand", "LocalMachine", "local", "BaseRemoteMachine", "RemoteCommand", "PuttyMachine", "SshMachine", ) plumbum-1.7.2/plumbum/machines/_windows.py0000644000232200023220000000146614161152302021225 0ustar debalancedebalance# -*- coding: utf-8 -*- import struct from plumbum.lib import six LFANEW_OFFSET = 30 * 2 FILE_HEADER_SIZE = 5 * 4 SUBSYSTEM_OFFSET = 17 * 4 IMAGE_SUBSYSTEM_WINDOWS_GUI = 2 IMAGE_SUBSYSTEM_WINDOWS_CUI = 3 def get_pe_subsystem(filename): with open(filename, "rb") as f: if f.read(2) != six.b("MZ"): return None f.seek(LFANEW_OFFSET) lfanew = struct.unpack("L", f.read(4))[0] f.seek(lfanew) if f.read(4) != six.b("PE\x00\x00"): return None f.seek(FILE_HEADER_SIZE + SUBSYSTEM_OFFSET, 1) subsystem = struct.unpack("H", f.read(2))[0] return subsystem # print(get_pe_subsystem("c:\\windows\\notepad.exe")) == 2 # print(get_pe_subsystem("c:\\python32\\python.exe")) == 3 # print(get_pe_subsystem("c:\\python32\\pythonw.exe")) == 2 plumbum-1.7.2/plumbum/machines/env.py0000644000232200023220000001425514161152302020164 0ustar debalancedebalance# -*- coding: utf-8 -*- import os from contextlib import contextmanager class EnvPathList(list): __slots__ = ["_path_factory", "_pathsep", "__weakref__"] def __init__(self, path_factory, pathsep): self._path_factory = path_factory self._pathsep = pathsep def append(self, path): list.append(self, self._path_factory(path)) def extend(self, paths): list.extend(self, (self._path_factory(p) for p in paths)) def insert(self, index, path): list.insert(self, index, self._path_factory(path)) def index(self, path): list.index(self, self._path_factory(path)) def __contains__(self, path): return list.__contains__(self, self._path_factory(path)) def remove(self, path): list.remove(self, self._path_factory(path)) def update(self, text): self[:] = [self._path_factory(p) for p in text.split(self._pathsep)] def join(self): return self._pathsep.join(str(p) for p in self) class BaseEnv(object): """The base class of LocalEnv and RemoteEnv""" __slots__ = ["_curr", "_path", "_path_factory", "__weakref__"] CASE_SENSITIVE = True def __init__(self, path_factory, pathsep): self._path_factory = path_factory self._path = EnvPathList(path_factory, pathsep) self._update_path() def _update_path(self): self._path.update(self.get("PATH", "")) @contextmanager def __call__(self, *args, **kwargs): """A context manager that can be used for temporal modifications of the environment. Any time you enter the context, a copy of the old environment is stored, and then restored, when the context exits. :param args: Any positional arguments for ``update()`` :param kwargs: Any keyword arguments for ``update()`` """ prev = self._curr.copy() self.update(**kwargs) try: yield finally: self._curr = prev self._update_path() def __iter__(self): """Returns an iterator over the items ``(key, value)`` of current environment (like dict.items)""" return iter(self._curr.items()) def __hash__(self): raise TypeError("unhashable type") def __len__(self): """Returns the number of elements of the current environment""" return len(self._curr) def __contains__(self, name): """Tests whether an environment variable exists in the current environment""" return (name if self.CASE_SENSITIVE else name.upper()) in self._curr def __getitem__(self, name): """Returns the value of the given environment variable from current environment, raising a ``KeyError`` if it does not exist""" return self._curr[name if self.CASE_SENSITIVE else name.upper()] def keys(self): """Returns the keys of the current environment (like dict.keys)""" return self._curr.keys() def items(self): """Returns the items of the current environment (like dict.items)""" return self._curr.items() def values(self): """Returns the values of the current environment (like dict.values)""" return self._curr.values() def get(self, name, *default): """Returns the keys of the current environment (like dict.keys)""" return self._curr.get((name if self.CASE_SENSITIVE else name.upper()), *default) def __delitem__(self, name): """Deletes an environment variable from the current environment""" name = name if self.CASE_SENSITIVE else name.upper() del self._curr[name] if name == "PATH": self._update_path() def __setitem__(self, name, value): """Sets/replaces an environment variable's value in the current environment""" name = name if self.CASE_SENSITIVE else name.upper() self._curr[name] = value if name == "PATH": self._update_path() def pop(self, name, *default): """Pops an element from the current environment (like dict.pop)""" name = name if self.CASE_SENSITIVE else name.upper() res = self._curr.pop(name, *default) if name == "PATH": self._update_path() return res def clear(self): """Clears the current environment (like dict.clear)""" self._curr.clear() self._update_path() def update(self, *args, **kwargs): """Updates the current environment (like dict.update)""" self._curr.update(*args, **kwargs) if not self.CASE_SENSITIVE: for k, v in list(self._curr.items()): self._curr[k.upper()] = v self._update_path() def getdict(self): """Returns the environment as a real dictionary""" self._curr["PATH"] = self.path.join() return {k: str(v) for k, v in self._curr.items()} @property def path(self): """The system's ``PATH`` (as an easy-to-manipulate list)""" return self._path def _get_home(self): if "HOME" in self: return self._path_factory(self["HOME"]) elif "USERPROFILE" in self: # pragma: no cover return self._path_factory(self["USERPROFILE"]) elif "HOMEPATH" in self: # pragma: no cover return self._path_factory(self.get("HOMEDRIVE", ""), self["HOMEPATH"]) return None def _set_home(self, p): if "HOME" in self: self["HOME"] = str(p) elif "USERPROFILE" in self: # pragma: no cover self["USERPROFILE"] = str(p) elif "HOMEPATH" in self: # pragma: no cover self["HOMEPATH"] = str(p) else: # pragma: no cover self["HOME"] = str(p) home = property(_get_home, _set_home) """Get or set the home path""" @property def user(self): """Return the user name, or ``None`` if it is not set""" # adapted from getpass.getuser() for name in ("LOGNAME", "USER", "LNAME", "USERNAME"): # pragma: no branch if name in self: return self[name] try: # POSIX only import pwd except ImportError: return None else: return pwd.getpwuid(os.getuid())[0] # @UndefinedVariable plumbum-1.7.2/plumbum/machines/ssh_machine.py0000644000232200023220000003360314161152302021653 0ustar debalancedebalance# -*- coding: utf-8 -*- import warnings from plumbum.commands import ProcessExecutionError, shquote from plumbum.lib import IS_WIN32, _setdoc from plumbum.machines.local import local from plumbum.machines.remote import BaseRemoteMachine from plumbum.machines.session import ShellSession from plumbum.path.local import LocalPath from plumbum.path.remote import RemotePath class SshTunnel(object): """An object representing an SSH tunnel (created by :func:`SshMachine.tunnel `)""" __slots__ = ["_session", "__weakref__"] def __init__(self, session): self._session = session def __repr__(self): if self._session.alive(): return "".format(self._session.proc) else: return "" def __enter__(self): return self def __exit__(self, t, v, tb): self.close() def close(self): """Closes(terminates) the tunnel""" self._session.close() class SshMachine(BaseRemoteMachine): """ An implementation of :class:`remote machine ` over SSH. Invoking a remote command translates to invoking it over SSH :: with SshMachine("yourhostname") as rem: r_ls = rem["ls"] # r_ls is the remote `ls` # executing r_ls() translates to `ssh yourhostname ls` :param host: the host name to connect to (SSH server) :param user: the user to connect as (if ``None``, the default will be used) :param port: the server's port (if ``None``, the default will be used) :param keyfile: the path to the identity file (if ``None``, the default will be used) :param ssh_command: the ``ssh`` command to use; this has to be a ``Command`` object; if ``None``, the default ssh client will be used. :param scp_command: the ``scp`` command to use; this has to be a ``Command`` object; if ``None``, the default scp program will be used. :param ssh_opts: any additional options for ``ssh`` (a list of strings) :param scp_opts: any additional options for ``scp`` (a list of strings) :param password: the password to use; requires ``sshpass`` be installed. Cannot be used in conjunction with ``ssh_command`` or ``scp_command`` (will be ignored). NOTE: THIS IS A SECURITY RISK! :param encoding: the remote machine's encoding (defaults to UTF8) :param connect_timeout: specify a connection timeout (the time until shell prompt is seen). The default is 10 seconds. Set to ``None`` to disable :param new_session: whether or not to start the background session as a new session leader (setsid). This will prevent it from being killed on Ctrl+C (SIGINT) """ def __init__( self, host, user=None, port=None, keyfile=None, ssh_command=None, scp_command=None, ssh_opts=(), scp_opts=(), password=None, encoding="utf8", connect_timeout=10, new_session=False, ): if ssh_command is None: if password is not None: ssh_command = local["sshpass"]["-p", password, "ssh"] else: ssh_command = local["ssh"] if scp_command is None: if password is not None: scp_command = local["sshpass"]["-p", password, "scp"] else: scp_command = local["scp"] scp_args = [] ssh_args = [] if user: self._fqhost = "{}@{}".format(user, host) else: self._fqhost = host if port: ssh_args.extend(["-p", str(port)]) scp_args.extend(["-P", str(port)]) if keyfile: ssh_args.extend(["-i", str(keyfile)]) scp_args.extend(["-i", str(keyfile)]) scp_args.append("-r") ssh_args.extend(ssh_opts) scp_args.extend(scp_opts) self._ssh_command = ssh_command[tuple(ssh_args)] self._scp_command = scp_command[tuple(scp_args)] BaseRemoteMachine.__init__( self, encoding=encoding, connect_timeout=connect_timeout, new_session=new_session, ) def __str__(self): return "ssh://{}".format(self._fqhost) @_setdoc(BaseRemoteMachine) def popen(self, args, ssh_opts=(), env=None, cwd=None, **kwargs): cmdline = [] cmdline.extend(ssh_opts) cmdline.append(self._fqhost) if args: envdelta = {} if hasattr(self, "env"): envdelta.update(self.env.getdelta()) if env: envdelta.update(env) if cwd is None: cwd = getattr(self, "cwd", None) if cwd: cmdline.extend(["cd", str(cwd), "&&"]) if envdelta: cmdline.append("env") cmdline.extend( "{}={}".format(k, shquote(v)) for k, v in envdelta.items() ) if isinstance(args, (tuple, list)): cmdline.extend(args) else: cmdline.append(args) return self._ssh_command[tuple(cmdline)].popen(**kwargs) def nohup(self, command): """ Runs the given command using ``nohup`` and redirects std handles, allowing the command to run "detached" from its controlling TTY or parent. Does not return anything. Depreciated (use command.nohup or daemonic_popen). """ warnings.warn("Use .nohup on the command or use daemonic_popen)", FutureWarning) self.daemonic_popen(command, cwd=".", stdout=None, stderr=None, append=False) def daemonic_popen(self, command, cwd=".", stdout=None, stderr=None, append=True): """ Runs the given command using ``nohup`` and redirects std handles, allowing the command to run "detached" from its controlling TTY or parent. Does not return anything. .. versionadded:: 1.6.0 """ if stdout is None: stdout = "/dev/null" if stderr is None: stderr = "&1" if str(cwd) == ".": args = [] else: args = ["cd", str(cwd), "&&"] args.append("nohup") args.extend(command.formulate()) args.extend( [ (">>" if append else ">") + str(stdout), "2" + (">>" if (append and stderr != "&1") else ">") + str(stderr), "` object can be used as a *context-manager*. The more conventional use case is the following:: +---------+ +---------+ | Your | | Remote | | Machine | | Machine | +----o----+ +---- ----+ | ^ | | lport dport | | \______SSH TUNNEL____/ (secure) Here, you wish to communicate safely between port ``lport`` of your machine and port ``dport`` of the remote machine. Communication is tunneled over SSH, so the connection is authenticated and encrypted. The more general case is shown below (where ``dport != "localhost"``):: +---------+ +-------------+ +-------------+ | Your | | Remote | | Destination | | Machine | | Machine | | Machine | +----o----+ +---- ----o---+ +---- --------+ | ^ | ^ | | | | lhost:lport | | dhost:dport | | | | \_____SSH TUNNEL_____/ \_____SOCKET____/ (secure) (not secure) Usage:: rem = SshMachine("megazord") with rem.tunnel(1234, "/var/lib/mysql/mysql.sock", dhost=None): sock = socket.socket() sock.connect(("localhost", 1234)) # sock is now tunneled to the MySQL socket on megazord """ formatted_lhost = "" if lhost is None else "[{}]:".format(lhost) formatted_dhost = "" if dhost is None else "[{}]:".format(dhost) ssh_opts = ( [ "-L", "{}{}:{}{}".format(formatted_lhost, lport, formatted_dhost, dport), ] if not reverse else [ "-R", "{}{}:{}{}".format(formatted_dhost, dport, formatted_lhost, lport), ] ) proc = self.popen((), ssh_opts=ssh_opts, new_session=True) return SshTunnel( ShellSession( proc, self.custom_encoding, connect_timeout=self.connect_timeout ) ) def _translate_drive_letter(self, path): # replace c:\some\path with /c/some/path path = str(path) if ":" in path: path = "/" + path.replace(":", "").replace("\\", "/") return path @_setdoc(BaseRemoteMachine) def download(self, src, dst): if isinstance(src, LocalPath): raise TypeError("src of download cannot be {!r}".format(src)) if isinstance(src, RemotePath) and src.remote != self: raise TypeError("src {!r} points to a different remote machine".format(src)) if isinstance(dst, RemotePath): raise TypeError("dst of download cannot be {!r}".format(dst)) if IS_WIN32: src = self._translate_drive_letter(src) dst = self._translate_drive_letter(dst) self._scp_command("{}:{}".format(self._fqhost, shquote(src)), dst) @_setdoc(BaseRemoteMachine) def upload(self, src, dst): if isinstance(src, RemotePath): raise TypeError("src of upload cannot be {!r}".format(src)) if isinstance(dst, LocalPath): raise TypeError("dst of upload cannot be {!r}".format(dst)) if isinstance(dst, RemotePath) and dst.remote != self: raise TypeError("dst {!r} points to a different remote machine".format(dst)) if IS_WIN32: src = self._translate_drive_letter(src) dst = self._translate_drive_letter(dst) self._scp_command(src, "{}:{}".format(self._fqhost, shquote(dst))) class PuttyMachine(SshMachine): """ PuTTY-flavored SSH connection. The programs ``plink`` and ``pscp`` are expected to be in the path (or you may provide your own ``ssh_command`` and ``scp_command``) Arguments are the same as for :class:`plumbum.machines.remote.SshMachine` """ def __init__( self, host, user=None, port=None, keyfile=None, ssh_command=None, scp_command=None, ssh_opts=(), scp_opts=(), encoding="utf8", connect_timeout=10, new_session=False, ): if ssh_command is None: ssh_command = local["plink"] if scp_command is None: scp_command = local["pscp"] if not ssh_opts: ssh_opts = ["-ssh"] if user is None: user = local.env.user if port is not None: ssh_opts.extend(["-P", str(port)]) scp_opts = list(scp_opts) + ["-P", str(port)] port = None SshMachine.__init__( self, host, user, port, keyfile=keyfile, ssh_command=ssh_command, scp_command=scp_command, ssh_opts=ssh_opts, scp_opts=scp_opts, encoding=encoding, connect_timeout=connect_timeout, new_session=new_session, ) def __str__(self): return "putty-ssh://{}".format(self._fqhost) def _translate_drive_letter(self, path): # pscp takes care of windows paths automatically return path @_setdoc(BaseRemoteMachine) def session(self, isatty=False, new_session=False): return ShellSession( self.popen((), (["-t"] if isatty else ["-T"]), new_session=new_session), self.custom_encoding, isatty, self.connect_timeout, ) plumbum-1.7.2/plumbum/machines/session.py0000644000232200023220000002671314161152302021061 0ustar debalancedebalance# -*- coding: utf-8 -*- import logging import random import threading import time from plumbum.commands import BaseCommand, run_proc from plumbum.commands.processes import ProcessExecutionError from plumbum.lib import six from plumbum.machines.base import PopenAddons class ShellSessionError(Exception): """Raises when something goes wrong when calling :func:`ShellSession.popen `""" pass class SSHCommsError(ProcessExecutionError, EOFError): """Raises when the communication channel can't be created on the remote host or it times out.""" class SSHCommsChannel2Error(SSHCommsError): """Raises when channel 2 (stderr) is not available""" class IncorrectLogin(SSHCommsError): """Raises when incorrect login credentials are provided""" class HostPublicKeyUnknown(SSHCommsError): """Raises when the host public key isn't known""" shell_logger = logging.getLogger("plumbum.shell") # =================================================================================================== # Shell Session Popen # =================================================================================================== class MarkedPipe(object): """A pipe-like object from which you can read lines; the pipe will return report EOF (the empty string) when a special marker is detected""" __slots__ = ["pipe", "marker", "__weakref__"] def __init__(self, pipe, marker): self.pipe = pipe self.marker = marker if six.PY3: self.marker = six.bytes(self.marker, "ascii") def close(self): """'Closes' the marked pipe; following calls to ``readline`` will return """ "" # consume everything while self.readline(): pass self.pipe = None def readline(self): """Reads the next line from the pipe; returns "" when the special marker is reached. Raises ``EOFError`` if the underlying pipe has closed""" if self.pipe is None: return six.b("") line = self.pipe.readline() if not line: raise EOFError() if line.strip() == self.marker: self.pipe = None line = six.b("") return line class SessionPopen(PopenAddons): """A shell-session-based ``Popen``-like object (has the following attributes: ``stdin``, ``stdout``, ``stderr``, ``returncode``)""" def __init__(self, proc, argv, isatty, stdin, stdout, stderr, encoding): self.proc = proc self.argv = argv self.isatty = isatty self.stdin = stdin self.stdout = stdout self.stderr = stderr self.custom_encoding = encoding self.returncode = None self._done = False def poll(self): """Returns the process' exit code or ``None`` if it's still running""" if self._done: return self.returncode else: return None def wait(self): """Waits for the process to terminate and returns its exit code""" self.communicate() return self.returncode def communicate(self, input=None): """Consumes the process' stdout and stderr until the it terminates. :param input: An optional bytes/buffer object to send to the process over stdin :returns: A tuple of (stdout, stderr) """ stdout = [] stderr = [] sources = [("1", stdout, self.stdout)] if not self.isatty: # in tty mode, stdout and stderr are unified sources.append(("2", stderr, self.stderr)) i = 0 while sources: if input: chunk = input[:1000] self.stdin.write(chunk) self.stdin.flush() input = input[1000:] i = (i + 1) % len(sources) name, coll, pipe = sources[i] try: line = pipe.readline() shell_logger.debug("%s> %r", name, line) except EOFError: shell_logger.debug("%s> Nothing returned.", name) self.proc.poll() returncode = self.proc.returncode stdout = six.b("").join(stdout).decode(self.custom_encoding, "ignore") stderr = six.b("").join(stderr).decode(self.custom_encoding, "ignore") argv = self.argv.decode(self.custom_encoding, "ignore").split(";")[:1] if returncode == 5: raise IncorrectLogin( argv, returncode, stdout, stderr, message="Incorrect username or password provided", ) elif returncode == 6: raise HostPublicKeyUnknown( argv, returncode, stdout, stderr, message="The authenticity of the host can't be established", ) elif returncode != 0: raise SSHCommsError( argv, returncode, stdout, stderr, message="SSH communication failed", ) elif name == "2": raise SSHCommsChannel2Error( argv, returncode, stdout, stderr, message="No stderr result detected. Does the remote have Bash as the default shell?", ) else: raise SSHCommsError( argv, returncode, stdout, stderr, message="No communication channel detected. Does the remote exist?", ) if not line: del sources[i] else: coll.append(line) if self.isatty: stdout.pop(0) # discard first line of prompt try: self.returncode = int(stdout.pop(-1)) except (IndexError, ValueError): self.returncode = "Unknown" self._done = True stdout = six.b("").join(stdout) stderr = six.b("").join(stderr) return stdout, stderr class ShellSession(object): """An abstraction layer over *shell sessions*. A shell session is the execution of an interactive shell (``/bin/sh`` or something compatible), over which you may run commands (sent over stdin). The output of is then read from stdout and stderr. Shell sessions are less "robust" than executing a process on its own, and they are susseptible to all sorts of malformatted-strings attacks, and there is little benefit from using them locally. However, they can greatly speed up remote connections, and are required for the implementation of :class:`SshMachine `, as they allow us to send multiple commands over a single SSH connection (setting up separate SSH connections incurs a high overhead). Try to avoid using shell sessions, unless you know what you're doing. Instances of this class may be used as *context-managers*. :param proc: The underlying shell process (with open stdin, stdout and stderr) :param encoding: The encoding to use for the shell session. If ``"auto"``, the underlying process' encoding is used. :param isatty: If true, assume the shell has a TTY and that stdout and stderr are unified :param connect_timeout: The timeout to connect to the shell, after which, if no prompt is seen, the shell process is killed """ def __init__(self, proc, encoding="auto", isatty=False, connect_timeout=5): self.proc = proc self.custom_encoding = proc.custom_encoding if encoding == "auto" else encoding self.isatty = isatty self._lock = threading.RLock() self._current = None if connect_timeout: def closer(): shell_logger.error( "Connection to %s timed out (%d sec)", proc, connect_timeout ) self.close() timer = threading.Timer(connect_timeout, self.close) timer.start() try: self.run("") finally: if connect_timeout: timer.cancel() def __enter__(self): return self def __exit__(self, t, v, tb): self.close() def __del__(self): try: self.close() except Exception: pass def alive(self): """Returns ``True`` if the underlying shell process is alive, ``False`` otherwise""" return self.proc and self.proc.poll() is None def close(self): """Closes (terminates) the shell session""" if not self.alive(): return try: self.proc.stdin.write(six.b("\nexit\n\n\nexit\n\n")) self.proc.stdin.flush() time.sleep(0.05) except (ValueError, EnvironmentError): pass for p in [self.proc.stdin, self.proc.stdout, self.proc.stderr]: try: p.close() except Exception: pass try: self.proc.kill() except EnvironmentError: pass self.proc = None def popen(self, cmd): """Runs the given command in the shell, adding some decoration around it. Only a single command can be executed at any given time. :param cmd: The command (string or :class:`Command ` object) to run :returns: A :class:`SessionPopen ` instance """ if self.proc is None: raise ShellSessionError("Shell session has already been closed") if self._current and not self._current._done: raise ShellSessionError("Each shell may start only one process at a time") if isinstance(cmd, BaseCommand): full_cmd = cmd.formulate(1) else: full_cmd = cmd marker = "--.END{}.--".format(time.time() * random.random()) if full_cmd.strip(): full_cmd += " ; " else: full_cmd = "true ; " full_cmd += "echo $? ; echo '{}'".format(marker) if not self.isatty: full_cmd += " ; echo '{}' 1>&2".format(marker) if self.custom_encoding: full_cmd = full_cmd.encode(self.custom_encoding) shell_logger.debug("Running %r", full_cmd) self.proc.stdin.write(full_cmd + six.b("\n")) self.proc.stdin.flush() self._current = SessionPopen( self.proc, full_cmd, self.isatty, self.proc.stdin, MarkedPipe(self.proc.stdout, marker), MarkedPipe(self.proc.stderr, marker), self.custom_encoding, ) return self._current def run(self, cmd, retcode=0): """Runs the given command :param cmd: The command (string or :class:`Command ` object) to run :param retcode: The expected return code (0 by default). Set to ``None`` in order to ignore erroneous return codes :returns: A tuple of (return code, stdout, stderr) """ with self._lock: return run_proc(self.popen(cmd), retcode) plumbum-1.7.2/plumbum/machines/base.py0000644000232200023220000000616014161152302020302 0ustar debalancedebalance# -*- coding: utf-8 -*- from plumbum.commands.processes import ( CommandNotFound, ProcessExecutionError, ProcessTimedOut, ) class PopenAddons(object): """This adds a verify to popen objects to that the correct command is attributed when an error is thrown.""" def verify(self, retcode, timeout, stdout, stderr): """This verifies that the correct command is attributed.""" if getattr(self, "_timed_out", False): raise ProcessTimedOut( "Process did not terminate within {} seconds".format(timeout), getattr(self, "argv", None), ) if retcode is not None: if hasattr(retcode, "__contains__"): if self.returncode not in retcode: raise ProcessExecutionError( getattr(self, "argv", None), self.returncode, stdout, stderr ) elif self.returncode != retcode: raise ProcessExecutionError( getattr(self, "argv", None), self.returncode, stdout, stderr ) class BaseMachine(object): """This is a base class for other machines. It contains common code to all machines in Plumbum.""" def get(self, cmd, *othercommands): """This works a little like the ``.get`` method with dict's, only it supports an unlimited number of arguments, since later arguments are tried as commands and could also fail. It will try to call the first command, and if that is not found, it will call the next, etc. Will raise if no file named for the executable if a path is given, unlike ``[]`` access. Usage:: best_zip = local.get('pigz','gzip') """ try: command = self[cmd] if not command.executable.exists(): raise CommandNotFound(cmd, command.executable) else: return command except CommandNotFound: if othercommands: return self.get(othercommands[0], *othercommands[1:]) else: raise def __contains__(self, cmd): """Tests for the existance of the command, e.g., ``"ls" in plumbum.local``. ``cmd`` can be anything acceptable by ``__getitem__``. """ try: self[cmd] except CommandNotFound: return False else: return True @property def encoding(self): "This is a wrapper for custom_encoding" return self.custom_encoding @encoding.setter def encoding(self, value): self.custom_encoding = value def daemonic_popen(self, command, cwd="/", stdout=None, stderr=None, append=True): raise NotImplementedError("This is not implemented on this machine!") class Cmd(object): def __init__(self, machine): self._machine = machine def __getattr__(self, name): try: return self._machine[name] except CommandNotFound: raise AttributeError(name) @property def cmd(self): return self.Cmd(self) plumbum-1.7.2/plumbum/machines/local.py0000644000232200023220000004043414161152302020464 0ustar debalancedebalance# -*- coding: utf-8 -*- import logging import os import platform import re import subprocess import sys import time from contextlib import contextmanager from tempfile import mkdtemp from plumbum.commands import CommandNotFound, ConcreteCommand from plumbum.commands.daemons import posix_daemonize, win32_daemonize from plumbum.commands.processes import iter_lines from plumbum.lib import IS_WIN32, ProcInfo, StaticProperty, six from plumbum.machines.base import BaseMachine, PopenAddons from plumbum.machines.env import BaseEnv from plumbum.machines.session import ShellSession from plumbum.path.local import LocalPath, LocalWorkdir from plumbum.path.remote import RemotePath if sys.version_info[0] >= 3: # python 3 has the new-and-improved subprocess module from subprocess import PIPE, Popen has_new_subprocess = True else: # otherwise, see if we have subprocess32 try: from subprocess32 import PIPE, Popen has_new_subprocess = True except ImportError: from subprocess import PIPE, Popen has_new_subprocess = False class PlumbumLocalPopen(PopenAddons): iter_lines = iter_lines def __init__(self, *args, **kwargs): self._proc = Popen(*args, **kwargs) def __iter__(self): return self.iter_lines() def __enter__(self): return self._proc.__enter__() def __exit__(self, *args, **kwargs): return self._proc.__exit__(*args, **kwargs) def __getattr__(self, name): return getattr(self._proc, name) if IS_WIN32: from plumbum.machines._windows import IMAGE_SUBSYSTEM_WINDOWS_CUI, get_pe_subsystem logger = logging.getLogger("plumbum.local") # =================================================================================================== # Environment # =================================================================================================== class LocalEnv(BaseEnv): """The local machine's environment; exposes a dict-like interface""" __slots__ = () CASE_SENSITIVE = not IS_WIN32 def __init__(self): # os.environ already takes care of upper'ing on windows self._curr = os.environ.copy() BaseEnv.__init__(self, LocalPath, os.path.pathsep) if IS_WIN32 and "HOME" not in self and self.home is not None: self["HOME"] = self.home def expand(self, expr): """Expands any environment variables and home shortcuts found in ``expr`` (like ``os.path.expanduser`` combined with ``os.path.expandvars``) :param expr: An expression containing environment variables (as ``$FOO``) or home shortcuts (as ``~/.bashrc``) :returns: The expanded string""" prev = os.environ os.environ = self.getdict() try: output = os.path.expanduser(os.path.expandvars(expr)) finally: os.environ = prev return output def expanduser(self, expr): """Expand home shortcuts (e.g., ``~/foo/bar`` or ``~john/foo/bar``) :param expr: An expression containing home shortcuts :returns: The expanded string""" prev = os.environ os.environ = self.getdict() try: output = os.path.expanduser(expr) finally: os.environ = prev return output # =================================================================================================== # Local Commands # =================================================================================================== class LocalCommand(ConcreteCommand): __slots__ = () QUOTE_LEVEL = 2 def __init__(self, executable, encoding="auto"): ConcreteCommand.__init__( self, executable, local.custom_encoding if encoding == "auto" else encoding ) @property def machine(self): return local def popen(self, args=(), cwd=None, env=None, **kwargs): if isinstance(args, six.string_types): args = (args,) return self.machine._popen( self.executable, self.formulate(0, args), cwd=self.cwd if cwd is None else cwd, env=self.env if env is None else env, **kwargs ) # =================================================================================================== # Local Machine # =================================================================================================== class LocalMachine(BaseMachine): """The *local machine* (a singleton object). It serves as an entry point to everything related to the local machine, such as working directory and environment manipulation, command creation, etc. Attributes: * ``cwd`` - the local working directory * ``env`` - the local environment * ``custom_encoding`` - the local machine's default encoding (``sys.getfilesystemencoding()``) """ cwd = StaticProperty(LocalWorkdir) env = LocalEnv() custom_encoding = sys.getfilesystemencoding() uname = platform.uname()[0] def __init__(self): self._as_user_stack = [] if IS_WIN32: _EXTENSIONS = [""] + env.get("PATHEXT", ":.exe:.bat").lower().split( os.path.pathsep ) @classmethod def _which(cls, progname): progname = progname.lower() for p in cls.env.path: for ext in cls._EXTENSIONS: fn = p / (progname + ext) if fn.access("x") and not fn.is_dir(): return fn return None else: @classmethod def _which(cls, progname): for p in cls.env.path: fn = p / progname if fn.access("x") and not fn.is_dir(): return fn return None @classmethod def which(cls, progname): """Looks up a program in the ``PATH``. If the program is not found, raises :class:`CommandNotFound ` :param progname: The program's name. Note that if underscores (``_``) are present in the name, and the exact name is not found, they will be replaced in turn by hyphens (``-``) then periods (``.``), and the name will be looked up again for each alternative :returns: A :class:`LocalPath ` """ alternatives = [progname] if "_" in progname: alternatives.append(progname.replace("_", "-")) alternatives.append(progname.replace("_", ".")) for pn in alternatives: path = cls._which(pn) if path: return path raise CommandNotFound(progname, list(cls.env.path)) def path(self, *parts): """A factory for :class:`LocalPaths `. Usage: ``p = local.path("/usr", "lib", "python2.7")`` """ parts2 = [str(self.cwd)] for p in parts: if isinstance(p, RemotePath): raise TypeError("Cannot construct LocalPath from {!r}".format(p)) parts2.append(self.env.expanduser(str(p))) return LocalPath(os.path.join(*parts2)) def __contains__(self, cmd): try: self[cmd] except CommandNotFound: return False else: return True def __getitem__(self, cmd): """Returns a `Command` object representing the given program. ``cmd`` can be a string or a :class:`LocalPath `; if it is a path, a command representing this path will be returned; otherwise, the program name will be looked up in the system's ``PATH`` (using ``which``). Usage:: ls = local["ls"] """ if isinstance(cmd, LocalPath): return LocalCommand(cmd) elif not isinstance(cmd, RemotePath): if "/" in cmd or "\\" in cmd: # assume path return LocalCommand(local.path(cmd)) else: # search for command return LocalCommand(self.which(cmd)) else: raise TypeError("cmd must not be a RemotePath: {!r}".format(cmd)) def _popen( self, executable, argv, stdin=PIPE, stdout=PIPE, stderr=PIPE, cwd=None, env=None, new_session=False, **kwargs ): if new_session: if has_new_subprocess: kwargs["start_new_session"] = True elif IS_WIN32: kwargs["creationflags"] = ( kwargs.get("creationflags", 0) | subprocess.CREATE_NEW_PROCESS_GROUP ) else: def preexec_fn(prev_fn=kwargs.get("preexec_fn", lambda: None)): os.setsid() prev_fn() kwargs["preexec_fn"] = preexec_fn if IS_WIN32 and "startupinfo" not in kwargs and stdin not in (sys.stdin, None): subsystem = get_pe_subsystem(str(executable)) if subsystem == IMAGE_SUBSYSTEM_WINDOWS_CUI: # don't open a new console sui = subprocess.STARTUPINFO() kwargs["startupinfo"] = sui if hasattr(subprocess, "_subprocess"): sui.dwFlags |= ( subprocess._subprocess.STARTF_USESHOWWINDOW ) # @UndefinedVariable sui.wShowWindow = ( subprocess._subprocess.SW_HIDE ) # @UndefinedVariable else: sui.dwFlags |= subprocess.STARTF_USESHOWWINDOW # @UndefinedVariable sui.wShowWindow = subprocess.SW_HIDE # @UndefinedVariable if not has_new_subprocess and "close_fds" not in kwargs: if IS_WIN32 and ( stdin is not None or stdout is not None or stderr is not None ): # we can't close fds if we're on windows and we want to redirect any std handle kwargs["close_fds"] = False else: kwargs["close_fds"] = True if cwd is None: cwd = self.cwd envs = [self.env, env] env = {} for _env in envs: if not _env: continue if isinstance(_env, BaseEnv): _env = _env.getdict() env.update(_env) if self._as_user_stack: argv, executable = self._as_user_stack[-1](argv) logger.debug("Running %r", argv) proc = PlumbumLocalPopen( argv, executable=str(executable), stdin=stdin, stdout=stdout, stderr=stderr, cwd=str(cwd), env=env, **kwargs ) # bufsize = 4096 proc._start_time = time.time() proc.custom_encoding = self.custom_encoding proc.argv = argv return proc def daemonic_popen(self, command, cwd="/", stdout=None, stderr=None, append=True): """ On POSIX systems: Run ``command`` as a UNIX daemon: fork a child process to setpid, redirect std handles to /dev/null, umask, close all fds, chdir to ``cwd``, then fork and exec ``command``. Returns a ``Popen`` process that can be used to poll/wait for the executed command (but keep in mind that you cannot access std handles) On Windows: Run ``command`` as a "Windows daemon": detach from controlling console and create a new process group. This means that the command will not receive console events and would survive its parent's termination. Returns a ``Popen`` object. .. note:: this does not run ``command`` as a system service, only detaches it from its parent. .. versionadded:: 1.3 """ if IS_WIN32: return win32_daemonize(command, cwd, stdout, stderr, append) else: return posix_daemonize(command, cwd, stdout, stderr, append) if IS_WIN32: def list_processes(self): """ Returns information about all running processes (on Windows: using ``tasklist``) .. versionadded:: 1.3 """ import csv tasklist = local["tasklist"] output = tasklist("/V", "/FO", "CSV") if not six.PY3: # The Py2 csv reader does not support non-ascii values output = output.encode("ascii", "ignore") lines = output.splitlines() rows = csv.reader(lines) header = next(rows) imgidx = header.index("Image Name") pididx = header.index("PID") statidx = header.index("Status") useridx = header.index("User Name") for row in rows: yield ProcInfo( int(row[pididx]), row[useridx], row[statidx], row[imgidx] ) else: def list_processes(self): """ Returns information about all running processes (on POSIX systems: using ``ps``) .. versionadded:: 1.3 """ ps = self["ps"] lines = ps("-e", "-o", "pid,uid,stat,args").splitlines() lines.pop(0) # header for line in lines: parts = line.strip().split() yield ProcInfo( int(parts[0]), int(parts[1]), parts[2], " ".join(parts[3:]) ) def pgrep(self, pattern): """ Process grep: return information about all processes whose command-line args match the given regex pattern """ pat = re.compile(pattern) for procinfo in self.list_processes(): if pat.search(procinfo.args): yield procinfo def session(self, new_session=False): """Creates a new :class:`ShellSession ` object; this invokes ``/bin/sh`` and executes commands on it over stdin/stdout/stderr""" return ShellSession(self["sh"].popen(new_session=new_session)) @contextmanager def tempdir(self): """A context manager that creates a temporary directory, which is removed when the context exits""" dir = self.path(mkdtemp()) # @ReservedAssignment try: yield dir finally: dir.delete() @contextmanager def as_user(self, username=None): """Run nested commands as the given user. For example:: head = local["head"] head("-n1", "/dev/sda1") # this will fail... with local.as_user(): head("-n1", "/dev/sda1") :param username: The user to run commands as. If not given, root (or Administrator) is assumed """ if IS_WIN32: if username is None: username = "Administrator" self._as_user_stack.append( lambda argv: ( [ "runas", "/savecred", "/user:{}".format(username), '"' + " ".join(str(a) for a in argv) + '"', ], self.which("runas"), ) ) else: if username is None: self._as_user_stack.append( lambda argv: (["sudo"] + list(argv), self.which("sudo")) ) else: self._as_user_stack.append( lambda argv: ( ["sudo", "-u", username] + list(argv), self.which("sudo"), ) ) try: yield finally: self._as_user_stack.pop(-1) def as_root(self): """A shorthand for :func:`as_user("root") `""" return self.as_user() python = LocalCommand(sys.executable, custom_encoding) """A command that represents the current python interpreter (``sys.executable``)""" local = LocalMachine() """The *local machine* (a singleton object). It serves as an entry point to everything related to the local machine, such as working directory and environment manipulation, command creation, etc. Attributes: * ``cwd`` - the local working directory * ``env`` - the local environment * ``custom_encoding`` - the local machine's default encoding (``sys.getfilesystemencoding()``) """ plumbum-1.7.2/plumbum/machines/paramiko_machine.py0000644000232200023220000004355314161152302022666 0ustar debalancedebalance# -*- coding: utf-8 -*- import errno import logging import os import socket import stat from plumbum.commands.base import shquote from plumbum.commands.processes import ProcessLineTimedOut, iter_lines from plumbum.lib import _setdoc, six from plumbum.machines.base import PopenAddons from plumbum.machines.remote import BaseRemoteMachine from plumbum.machines.session import ShellSession from plumbum.path.local import LocalPath from plumbum.path.remote import RemotePath, StatRes try: # Sigh... we need to gracefully-import paramiko for Sphinx builds, etc import paramiko except ImportError: class paramiko(object): # type: ignore def __nonzero__(self): return False __bool__ = __nonzero__ def __getattr__(self, name): raise ImportError("No module named paramiko") paramiko = paramiko() # type: ignore logger = logging.getLogger("plumbum.paramiko") class ParamikoPopen(PopenAddons): def __init__( self, argv, stdin, stdout, stderr, encoding, stdin_file=None, stdout_file=None, stderr_file=None, ): self.argv = argv self.channel = stdout.channel self.stdin = stdin self.stdout = stdout self.stderr = stderr self.custom_encoding = encoding self.returncode = None self.pid = None self.stdin_file = stdin_file self.stdout_file = stdout_file self.stderr_file = stderr_file def poll(self): if self.returncode is None: if self.channel.exit_status_ready(): return self.wait() return self.returncode def wait(self): if self.returncode is None: self.channel.recv_exit_status() self.returncode = self.channel.exit_status self.close() return self.returncode def close(self): self.channel.shutdown_read() self.channel.shutdown_write() self.channel.close() def kill(self): # possible way to obtain pid: # "(cmd ; echo $?) & echo ?!" # and then client.exec_command("kill -9 %s" % (pid,)) raise EnvironmentError("Cannot kill remote processes, we don't have their PIDs") terminate = kill def send_signal(self, sig): raise NotImplementedError() def communicate(self): stdout = [] stderr = [] infile = self.stdin_file sources = [ ("1", stdout, self.stdout, self.stdout_file), ("2", stderr, self.stderr, self.stderr_file), ] i = 0 while sources: if infile: try: line = infile.readline() except (ValueError, IOError): line = None logger.debug("communicate: %r", line) if not line: infile.close() infile = None self.stdin.close() else: self.stdin.write(line) self.stdin.flush() i = (i + 1) % len(sources) name, coll, pipe, outfile = sources[i] line = pipe.readline() # logger.debug("%s> %r", name, line) if not line: del sources[i] elif outfile: outfile.write(line) outfile.flush() else: coll.append(line) self.wait() stdout = "".join(s for s in stdout).encode(self.custom_encoding) stderr = "".join(s for s in stderr).encode(self.custom_encoding) return stdout, stderr def iter_lines(self, timeout=None, **kwargs): if timeout is not None: raise NotImplementedError( "The 'timeout' parameter is not supported with ParamikoMachine" ) return iter_lines(self, _iter_lines=_iter_lines, **kwargs) __iter__ = iter_lines class ParamikoMachine(BaseRemoteMachine): """ An implementation of :class:`remote machine ` over Paramiko (a Python implementation of openSSH2 client/server). Invoking a remote command translates to invoking it over SSH :: with ParamikoMachine("yourhostname") as rem: r_ls = rem["ls"] # r_ls is the remote `ls` # executing r_ls() is equivalent to `ssh yourhostname ls`, only without # spawning a new ssh client :param host: the host name to connect to (SSH server) :param user: the user to connect as (if ``None``, the default will be used) :param port: the server's port (if ``None``, the default will be used) :param password: the user's password (if a password-based authentication is to be performed) (if ``None``, key-based authentication will be used) :param keyfile: the path to the identity file (if ``None``, the default will be used) :param load_system_host_keys: whether or not to load the system's host keys (from ``/etc/ssh`` and ``~/.ssh``). The default is ``True``, which means Paramiko behaves much like the ``ssh`` command-line client :param missing_host_policy: the value passed to the underlying ``set_missing_host_key_policy`` of the client. The default is ``None``, which means ``set_missing_host_key_policy`` is not invoked and paramiko's default behavior (reject) is employed :param encoding: the remote machine's encoding (defaults to UTF8) :param look_for_keys: set to False to disable searching for discoverable private key files in ``~/.ssh`` :param connect_timeout: timeout for TCP connection .. note:: If Paramiko 1.15 or above is installed, can use GSS_API authentication :param bool gss_auth: ``True`` if you want to use GSS-API authentication :param bool gss_kex: Perform GSS-API Key Exchange and user authentication :param bool gss_deleg_creds: Delegate GSS-API client credentials or not :param str gss_host: The targets name in the kerberos database. default: hostname :param bool get_pty: Execute remote commands with allocated pseudo-tty. default: False :param bool load_system_ssh_config: read system SSH config for ProxyCommand configuration. default: False """ class RemoteCommand(BaseRemoteMachine.RemoteCommand): def __or__(self, *_): raise NotImplementedError("Not supported with ParamikoMachine") def __gt__(self, *_): raise NotImplementedError("Not supported with ParamikoMachine") def __rshift__(self, *_): raise NotImplementedError("Not supported with ParamikoMachine") def __ge__(self, *_): raise NotImplementedError("Not supported with ParamikoMachine") def __lt__(self, *_): raise NotImplementedError("Not supported with ParamikoMachine") def __lshift__(self, *_): raise NotImplementedError("Not supported with ParamikoMachine") def __init__( self, host, user=None, port=None, password=None, keyfile=None, load_system_host_keys=True, missing_host_policy=None, encoding="utf8", look_for_keys=None, connect_timeout=None, keep_alive=0, gss_auth=False, gss_kex=None, gss_deleg_creds=None, gss_host=None, get_pty=False, load_system_ssh_config=False, ): self.host = host kwargs = {} if user: self._fqhost = "{}@{}".format(user, host) kwargs["username"] = user else: self._fqhost = host self._client = paramiko.SSHClient() if load_system_host_keys: self._client.load_system_host_keys() if port is not None: kwargs["port"] = port if keyfile is not None: kwargs["key_filename"] = keyfile if password is not None: kwargs["password"] = password if missing_host_policy is not None: self._client.set_missing_host_key_policy(missing_host_policy) if look_for_keys is not None: kwargs["look_for_keys"] = look_for_keys if connect_timeout is not None: kwargs["timeout"] = connect_timeout if gss_auth: kwargs["gss_auth"] = gss_auth kwargs["gss_kex"] = gss_kex kwargs["gss_deleg_creds"] = gss_deleg_creds if not gss_host: gss_host = host kwargs["gss_host"] = gss_host if load_system_ssh_config: ssh_config = paramiko.SSHConfig() with open(os.path.expanduser("~/.ssh/config")) as f: ssh_config.parse(f) try: hostConfig = ssh_config.lookup(host) kwargs["sock"] = paramiko.ProxyCommand(hostConfig["proxycommand"]) except KeyError: pass self._client.connect(host, **kwargs) self._keep_alive = keep_alive self._sftp = None self._get_pty = get_pty BaseRemoteMachine.__init__(self, encoding, connect_timeout) def __str__(self): return "paramiko://{}".format(self._fqhost) def close(self): BaseRemoteMachine.close(self) self._client.close() @property def sftp(self): """ Returns an SFTP client on top of the current SSH connection; it can be used to manipulate files directly, much like an interactive FTP/SFTP session """ if not self._sftp: self._sftp = self._client.open_sftp() return self._sftp @_setdoc(BaseRemoteMachine) def session( self, isatty=False, term="vt100", width=80, height=24, new_session=False ): # new_session is ignored for ParamikoMachine trans = self._client.get_transport() trans.set_keepalive(self._keep_alive) chan = trans.open_session() if isatty: chan.get_pty(term, width, height) chan.set_combine_stderr(True) chan.invoke_shell() stdin = chan.makefile("wb", -1) stdout = chan.makefile("rb", -1) stderr = chan.makefile_stderr("rb", -1) proc = ParamikoPopen([""], stdin, stdout, stderr, self.custom_encoding) return ShellSession(proc, self.custom_encoding, isatty) @_setdoc(BaseRemoteMachine) def popen( self, args, stdin=None, stdout=None, stderr=None, new_session=False, env=None, cwd=None, ): # new_session is ignored for ParamikoMachine argv = [] envdelta = self.env.getdelta() if env: envdelta.update(env) argv.extend(["cd", str(cwd or self.cwd), "&&"]) if envdelta: argv.append("env") argv.extend("{}={}".format(k, shquote(v)) for k, v in envdelta.items()) argv.extend(args.formulate()) cmdline = " ".join(argv) logger.debug(cmdline) si, so, se = self._client.exec_command(cmdline, 1, get_pty=self._get_pty) return ParamikoPopen( argv, si, so, se, self.custom_encoding, stdin_file=stdin, stdout_file=stdout, stderr_file=stderr, ) @_setdoc(BaseRemoteMachine) def download(self, src, dst): if isinstance(src, LocalPath): raise TypeError("src of download cannot be {!r}".format(src)) if isinstance(src, RemotePath) and src.remote != self: raise TypeError("src {!r} points to a different remote machine".format(src)) if isinstance(dst, RemotePath): raise TypeError("dst of download cannot be {!r}".format(dst)) return self._download( src if isinstance(src, RemotePath) else self.path(src), dst if isinstance(dst, LocalPath) else LocalPath(dst), ) def _download(self, src, dst): if src.is_dir(): if not dst.exists(): self.sftp.mkdir(str(dst)) for fn in src: self._download(fn, dst / fn.name) elif dst.is_dir(): self.sftp.get(str(src), str(dst / src.name)) else: self.sftp.get(str(src), str(dst)) @_setdoc(BaseRemoteMachine) def upload(self, src, dst): if isinstance(src, RemotePath): raise TypeError("src of upload cannot be {!r}".format(src)) if isinstance(dst, LocalPath): raise TypeError("dst of upload cannot be {!r}".format(dst)) if isinstance(dst, RemotePath) and dst.remote != self: raise TypeError("dst {!r} points to a different remote machine".format(dst)) return self._upload( src if isinstance(src, LocalPath) else LocalPath(src), dst if isinstance(dst, RemotePath) else self.path(dst), ) def _upload(self, src, dst): if src.is_dir(): if not dst.exists(): self.sftp.mkdir(str(dst)) for fn in src: self._upload(fn, dst / fn.name) elif dst.is_dir(): self.sftp.put(str(src), str(dst / src.name)) else: self.sftp.put(str(src), str(dst)) def connect_sock(self, dport, dhost="localhost", ipv6=False): """Returns a Paramiko ``Channel``, connected to dhost:dport on the remote machine. The ``Channel`` behaves like a regular socket; you can ``send`` and ``recv`` on it and the data will pass encrypted over SSH. Usage:: mach = ParamikoMachine("myhost") sock = mach.connect_sock(12345) data = sock.recv(100) sock.send("foobar") sock.close() """ if ipv6 and dhost == "localhost": dhost = "::1" srcaddr = ("::1", 0, 0, 0) if ipv6 else ("127.0.0.1", 0) trans = self._client.get_transport() trans.set_keepalive(self._keep_alive) chan = trans.open_channel("direct-tcpip", (dhost, dport), srcaddr) return SocketCompatibleChannel(chan) # # Path implementation # def _path_listdir(self, fn): return self.sftp.listdir(str(fn)) def _path_read(self, fn): f = self.sftp.open(str(fn), "rb") data = f.read() f.close() return data def _path_write(self, fn, data): if self.custom_encoding and isinstance(data, six.unicode_type): data = data.encode(self.custom_encoding) f = self.sftp.open(str(fn), "wb") f.write(data) f.close() def _path_stat(self, fn): try: st = self.sftp.stat(str(fn)) except IOError as e: if e.errno == errno.ENOENT: return None raise OSError(e.errno) res = StatRes( ( st.st_mode, 0, 0, 0, st.st_uid, st.st_gid, st.st_size, st.st_atime, st.st_mtime, 0, ) ) if stat.S_ISDIR(st.st_mode): res.text_mode = "directory" if stat.S_ISREG(st.st_mode): res.text_mode = "regular file" return res ################################################################################################### # Make paramiko.Channel adhere to the socket protocol, namely, send and recv should fail # when the socket has been closed ################################################################################################### class SocketCompatibleChannel(object): def __init__(self, chan): self._chan = chan def __getattr__(self, name): return getattr(self._chan, name) def send(self, s): if self._chan.closed: raise socket.error(errno.EBADF, "Bad file descriptor") return self._chan.send(s) def recv(self, count): if self._chan.closed: raise socket.error(errno.EBADF, "Bad file descriptor") return self._chan.recv(count) ################################################################################################### # Custom iter_lines for paramiko.Channel ################################################################################################### def _iter_lines(proc, decode, linesize, line_timeout=None): try: from selectors import EVENT_READ, DefaultSelector except ImportError: # Pre Python 3.4 implementation from select import select def selector(): while True: rlist, _, _ = select([proc.stdout.channel], [], [], line_timeout) if not rlist and line_timeout: raise ProcessLineTimedOut( "popen line timeout expired", getattr(proc, "argv", None), getattr(proc, "machine", None), ) for _ in rlist: yield else: # Python 3.4 implementation def selector(): sel = DefaultSelector() sel.register(proc.stdout.channel, EVENT_READ) while True: ready = sel.select(line_timeout) if not ready and line_timeout: raise ProcessLineTimedOut( "popen line timeout expired", getattr(proc, "argv", None), getattr(proc, "machine", None), ) for key, mask in ready: yield for _ in selector(): if proc.stdout.channel.recv_ready(): yield 0, proc.stdout.readline(linesize) if proc.stdout.channel.recv_stderr_ready(): yield 1, proc.stderr.readline(linesize) if proc.poll() is not None: break for line in proc.stdout: yield 0, line for line in proc.stderr: yield 1, line plumbum-1.7.2/plumbum/version.py0000644000232200023220000000021614161152314017265 0ustar debalancedebalance# coding: utf-8 # file generated by setuptools_scm # don't change, don't track in version control version = '1.7.2' version_tuple = (1, 7, 2) plumbum-1.7.2/plumbum/path/0000755000232200023220000000000014161152315016164 5ustar debalancedebalanceplumbum-1.7.2/plumbum/path/utils.py0000644000232200023220000000716514161152302017703 0ustar debalancedebalance# -*- coding: utf-8 -*- import os from plumbum.lib import six from plumbum.machines.local import LocalPath, local from plumbum.path.base import Path def delete(*paths): """Deletes the given paths. The arguments can be either strings, :class:`local paths `, :class:`remote paths `, or iterables of such. No error is raised if any of the paths does not exist (it is silently ignored) """ for p in paths: if isinstance(p, Path): p.delete() elif isinstance(p, six.string_types): local.path(p).delete() elif hasattr(p, "__iter__"): delete(*p) else: raise TypeError("Cannot delete {!r}".format(p)) def _move(src, dst): ret = copy(src, dst) delete(src) return ret def move(src, dst): """Moves the source path onto the destination path; ``src`` and ``dst`` can be either strings, :class:`LocalPaths ` or :class:`RemotePath `; any combination of the three will work. .. versionadded:: 1.3 ``src`` can also be a list of strings/paths, in which case ``dst`` must not exist or be a directory. """ if not isinstance(dst, Path): dst = local.path(dst) if isinstance(src, (tuple, list)): if not dst.exists(): dst.mkdir() elif not dst.is_dir(): raise ValueError( "When using multiple sources, dst {!r} must be a directory".format(dst) ) for src2 in src: move(src2, dst) return dst elif not isinstance(src, Path): src = local.path(src) if isinstance(src, LocalPath): if isinstance(dst, LocalPath): return src.move(dst) else: return _move(src, dst) elif isinstance(dst, LocalPath): return _move(src, dst) elif src.remote == dst.remote: return src.move(dst) else: return _move(src, dst) def copy(src, dst): """ Copy (recursively) the source path onto the destination path; ``src`` and ``dst`` can be either strings, :class:`LocalPaths ` or :class:`RemotePath `; any combination of the three will work. .. versionadded:: 1.3 ``src`` can also be a list of strings/paths, in which case ``dst`` must not exist or be a directory. """ if not isinstance(dst, Path): dst = local.path(dst) if isinstance(src, (tuple, list)): if not dst.exists(): dst.mkdir() elif not dst.is_dir(): raise ValueError( "When using multiple sources, dst {!r} must be a directory".format(dst) ) for src2 in src: copy(src2, dst) return dst elif not isinstance(src, Path): src = local.path(src) if isinstance(src, LocalPath): if isinstance(dst, LocalPath): return src.copy(dst) else: dst.remote.upload(src, dst) return dst elif isinstance(dst, LocalPath): src.remote.download(src, dst) return dst elif src.remote == dst.remote: return src.copy(dst) else: with local.tempdir() as tmp: copy(src, tmp) copy(tmp / src.name, dst) return dst def gui_open(filename): """This selects the proper gui open function. This can also be achieved with webbrowser, but that is not supported.""" if hasattr(os, "startfile"): os.startfile(filename) else: local.get("xdg-open", "open")(filename) plumbum-1.7.2/plumbum/path/remote.py0000644000232200023220000003020714161152302020027 0ustar debalancedebalance# -*- coding: utf-8 -*- import errno import os import sys from contextlib import contextmanager from plumbum.commands import ProcessExecutionError, shquote from plumbum.lib import _setdoc, six from plumbum.path.base import FSUser, Path try: # Py3 import urllib.request as urllib except ImportError: import urllib # type: ignore class StatRes(object): """POSIX-like stat result""" def __init__(self, tup): self._tup = tuple(tup) def __getitem__(self, index): return self._tup[index] st_mode = mode = property(lambda self: self[0]) st_ino = ino = property(lambda self: self[1]) st_dev = dev = property(lambda self: self[2]) st_nlink = nlink = property(lambda self: self[3]) st_uid = uid = property(lambda self: self[4]) st_gid = gid = property(lambda self: self[5]) st_size = size = property(lambda self: self[6]) st_atime = atime = property(lambda self: self[7]) st_mtime = mtime = property(lambda self: self[8]) st_ctime = ctime = property(lambda self: self[9]) class RemotePath(Path): """The class implementing remote-machine paths""" def __new__(cls, remote, *parts): if not parts: raise TypeError("At least one path part is required (none given)") windows = remote.uname.lower() == "windows" normed = [] parts = tuple( map(str, parts) ) # force the paths into string, so subscription works properly # Simple skip if path is absolute if parts[0] and parts[0][0] not in ("/", "\\"): cwd = ( remote._cwd if hasattr(remote, "_cwd") else remote._session.run("pwd")[1].strip() ) parts = (cwd,) + parts for p in parts: if windows: plist = str(p).replace("\\", "/").split("/") else: plist = str(p).split("/") if not plist[0]: plist.pop(0) del normed[:] for item in plist: if item == "" or item == ".": continue if item == "..": if normed: normed.pop(-1) else: normed.append(item) if windows: self = super(RemotePath, cls).__new__(cls, "\\".join(normed)) self.CASE_SENSITIVE = False # On this object only else: self = super(RemotePath, cls).__new__(cls, "/" + "/".join(normed)) self.CASE_SENSITIVE = True self.remote = remote return self def _form(self, *parts): return RemotePath(self.remote, *parts) @property def _path(self): return str(self) @property # type: ignore @_setdoc(Path) def name(self): if not "/" in str(self): return str(self) return str(self).rsplit("/", 1)[1] @property # type: ignore @_setdoc(Path) def dirname(self): if not "/" in str(self): return str(self) return self.__class__(self.remote, str(self).rsplit("/", 1)[0]) @property # type: ignore @_setdoc(Path) def suffix(self): return "." + self.name.rsplit(".", 1)[1] @property # type: ignore @_setdoc(Path) def suffixes(self): name = self.name exts = [] while "." in name: name, ext = name.rsplit(".", 1) exts.append("." + ext) return list(reversed(exts)) @property # type: ignore @_setdoc(Path) def uid(self): uid, name = self.remote._path_getuid(self) return FSUser(int(uid), name) @property # type: ignore @_setdoc(Path) def gid(self): gid, name = self.remote._path_getgid(self) return FSUser(int(gid), name) def _get_info(self): return (self.remote, self._path) @_setdoc(Path) def join(self, *parts): return RemotePath(self.remote, self, *parts) @_setdoc(Path) def list(self): if not self.is_dir(): return [] return [self.join(fn) for fn in self.remote._path_listdir(self)] @_setdoc(Path) def iterdir(self): if not self.is_dir(): return () return (self.join(fn) for fn in self.remote._path_listdir(self)) @_setdoc(Path) def is_dir(self): res = self.remote._path_stat(self) if not res: return False return res.text_mode == "directory" @_setdoc(Path) def is_file(self): res = self.remote._path_stat(self) if not res: return False return res.text_mode in ("regular file", "regular empty file") @_setdoc(Path) def is_symlink(self): res = self.remote._path_stat(self) if not res: return False return res.text_mode == "symbolic link" @_setdoc(Path) def exists(self): return self.remote._path_stat(self) is not None @_setdoc(Path) def stat(self): res = self.remote._path_stat(self) if res is None: raise OSError(errno.ENOENT, os.strerror(errno.ENOENT), "") return res @_setdoc(Path) def with_name(self, name): return self.__class__(self.remote, self.dirname) / name @_setdoc(Path) def with_suffix(self, suffix, depth=1): if suffix and not suffix.startswith(".") or suffix == ".": raise ValueError("Invalid suffix %r" % (suffix)) name = self.name depth = len(self.suffixes) if depth is None else min(depth, len(self.suffixes)) for i in range(depth): name, ext = name.rsplit(".", 1) return self.__class__(self.remote, self.dirname) / (name + suffix) @_setdoc(Path) def glob(self, pattern): fn = lambda pat: [ RemotePath(self.remote, m) for m in self.remote._path_glob(self, pat) ] return self._glob(pattern, fn) @_setdoc(Path) def delete(self): if not self.exists(): return self.remote._path_delete(self) unlink = delete @_setdoc(Path) def move(self, dst): if isinstance(dst, RemotePath): if dst.remote is not self.remote: raise TypeError("dst points to a different remote machine") elif not isinstance(dst, six.string_types): raise TypeError( "dst must be a string or a RemotePath (to the same remote machine), " "got %r" % (dst,) ) self.remote._path_move(self, dst) @_setdoc(Path) def copy(self, dst, override=False): if isinstance(dst, RemotePath): if dst.remote is not self.remote: raise TypeError("dst points to a different remote machine") elif not isinstance(dst, six.string_types): raise TypeError( "dst must be a string or a RemotePath (to the same remote machine), " "got %r" % (dst,) ) if override: if isinstance(dst, six.string_types): dst = RemotePath(self.remote, dst) dst.delete() else: if isinstance(dst, six.string_types): dst = RemotePath(self.remote, dst) if dst.exists(): raise TypeError("Override not specified and dst exists") self.remote._path_copy(self, dst) @_setdoc(Path) def mkdir(self, mode=None, parents=True, exist_ok=True): if parents and exist_ok: self.remote._path_mkdir(self, mode=mode, minus_p=True) else: if parents and len(self.parts) > 1: self.remote._path_mkdir(self.parent, mode=mode, minus_p=True) try: self.remote._path_mkdir(self, mode=mode, minus_p=False) except ProcessExecutionError: _, ex, _ = sys.exc_info() if "File exists" in ex.stderr: if not exist_ok: raise OSError( errno.EEXIST, "File exists (on remote end)", str(self) ) else: raise @_setdoc(Path) def read(self, encoding=None): data = self.remote._path_read(self) if encoding: data = data.decode(encoding) return data @_setdoc(Path) def write(self, data, encoding=None): if encoding: data = data.encode(encoding) self.remote._path_write(self, data) @_setdoc(Path) def touch(self): self.remote._path_touch(str(self)) @_setdoc(Path) def chown(self, owner=None, group=None, recursive=None): self.remote._path_chown( self, owner, group, self.is_dir() if recursive is None else recursive ) @_setdoc(Path) def chmod(self, mode): self.remote._path_chmod(mode, self) @_setdoc(Path) def access(self, mode=0): mode = self._access_mode_to_flags(mode) res = self.remote._path_stat(self) if res is None: return False mask = res.st_mode & 0x1FF return ((mask >> 6) & mode) or ((mask >> 3) & mode) @_setdoc(Path) def link(self, dst): if isinstance(dst, RemotePath): if dst.remote is not self.remote: raise TypeError("dst points to a different remote machine") elif not isinstance(dst, six.string_types): raise TypeError( "dst must be a string or a RemotePath (to the same remote machine), " "got %r" % (dst,) ) self.remote._path_link(self, dst, False) @_setdoc(Path) def symlink(self, dst): if isinstance(dst, RemotePath): if dst.remote is not self.remote: raise TypeError("dst points to a different remote machine") elif not isinstance(dst, six.string_types): raise TypeError( "dst must be a string or a RemotePath (to the same remote machine), " "got %r" % (dst,) ) self.remote._path_link(self, dst, True) def open(self, mode="r", bufsize=-1): """ Opens this path as a file. Only works for ParamikoMachine-associated paths for now. """ if hasattr(self.remote, "sftp") and hasattr(self.remote.sftp, "open"): return self.remote.sftp.open(self, mode, bufsize) else: raise NotImplementedError( "RemotePath.open only works for ParamikoMachine-associated " "paths for now" ) @_setdoc(Path) def as_uri(self, scheme="ssh"): return "{}://{}{}".format( scheme, self.remote._fqhost, urllib.pathname2url(str(self)) ) @property # type: ignore @_setdoc(Path) def stem(self): return self.name.rsplit(".")[0] @property # type: ignore @_setdoc(Path) def root(self): return "/" @property # type: ignore @_setdoc(Path) def drive(self): return "" class RemoteWorkdir(RemotePath): """Remote working directory manipulator""" def __new__(cls, remote): self = super(RemoteWorkdir, cls).__new__( cls, remote, remote._session.run("pwd")[1].strip() ) return self def __hash__(self): raise TypeError("unhashable type") def chdir(self, newdir): """Changes the current working directory to the given one""" self.remote._session.run("cd {}".format(shquote(newdir))) if hasattr(self.remote, "_cwd"): del self.remote._cwd return self.__class__(self.remote) def getpath(self): """Returns the current working directory as a `remote path ` object""" return RemotePath(self.remote, self) @contextmanager def __call__(self, newdir): """A context manager used to ``chdir`` into a directory and then ``chdir`` back to the previous location; much like ``pushd``/``popd``. :param newdir: The destination director (a string or a :class:`RemotePath `) """ prev = self._path changed_dir = self.chdir(newdir) try: yield changed_dir finally: self.chdir(prev) plumbum-1.7.2/plumbum/path/__init__.py0000644000232200023220000000036414161152302020274 0ustar debalancedebalance# -*- coding: utf-8 -*- from plumbum.path.base import FSUser, Path, RelativePath from plumbum.path.local import LocalPath, LocalWorkdir from plumbum.path.remote import RemotePath, RemoteWorkdir from plumbum.path.utils import copy, delete, move plumbum-1.7.2/plumbum/path/base.py0000644000232200023220000004044614161152302017454 0ustar debalancedebalance# -*- coding: utf-8 -*- from __future__ import absolute_import import itertools import operator import os import warnings from abc import abstractmethod, abstractproperty from functools import reduce from plumbum.lib import six class FSUser(int): """A special object that represents a file-system user. It derives from ``int``, so it behaves just like a number (``uid``/``gid``), but also have a ``.name`` attribute that holds the string-name of the user, if given (otherwise ``None``) """ def __new__(cls, val, name=None): self = int.__new__(cls, val) self.name = name return self class Path(str, six.ABC): """An abstraction over file system paths. This class is abstract, and the two implementations are :class:`LocalPath ` and :class:`RemotePath `. """ CASE_SENSITIVE = True def __repr__(self): return "<{} {}>".format(self.__class__.__name__, str(self)) def __div__(self, other): """Joins two paths""" return self.join(other) __truediv__ = __div__ def __getitem__(self, key): if type(key) == str or isinstance(key, Path): return self / key return str(self)[key] def __floordiv__(self, expr): """Returns a (possibly empty) list of paths that matched the glob-pattern under this path""" return self.glob(expr) def __iter__(self): """Iterate over the files in this directory""" return iter(self.list()) def __eq__(self, other): if isinstance(other, Path): return self._get_info() == other._get_info() elif isinstance(other, str): if self.CASE_SENSITIVE: return str(self) == other else: return str(self).lower() == other.lower() else: return NotImplemented def __ne__(self, other): return not (self == other) def __gt__(self, other): return str(self) > str(other) def __ge__(self, other): return str(self) >= str(other) def __lt__(self, other): return str(self) < str(other) def __le__(self, other): return str(self) <= str(other) def __hash__(self): if self.CASE_SENSITIVE: return hash(str(self)) else: return hash(str(self).lower()) def __nonzero__(self): return bool(str(self)) __bool__ = __nonzero__ def __fspath__(self): """Added for Python 3.6 support""" return str(self) def __contains__(self, item): """Paths should support checking to see if an file or folder is in them.""" try: return (self / item.name).exists() except AttributeError: return (self / item).exists() @abstractmethod def _form(self, *parts): pass def up(self, count=1): """Go up in ``count`` directories (the default is 1)""" return self.join("../" * count) def walk( self, filter=lambda p: True, dir_filter=lambda p: True ): # @ReservedAssignment """traverse all (recursive) sub-elements under this directory, that match the given filter. By default, the filter accepts everything; you can provide a custom filter function that takes a path as an argument and returns a boolean :param filter: the filter (predicate function) for matching results. Only paths matching this predicate are returned. Defaults to everything. :param dir_filter: the filter (predicate function) for matching directories. Only directories matching this predicate are recursed into. Defaults to everything. """ for p in self.list(): if filter(p): yield p if p.is_dir() and dir_filter(p): for p2 in p.walk(filter, dir_filter): yield p2 @abstractproperty def name(self): """The basename component of this path""" @property def basename(self): """Included for compatibility with older Plumbum code""" warnings.warn("Use .name instead", FutureWarning) return self.name @abstractproperty def stem(self): """The name without an extension, or the last component of the path""" @abstractproperty def dirname(self): """The dirname component of this path""" @abstractproperty def root(self): """The root of the file tree (`/` on Unix)""" @abstractproperty def drive(self): """The drive letter (on Windows)""" @abstractproperty def suffix(self): """The suffix of this file""" @abstractproperty def suffixes(self): """This is a list of all suffixes""" @abstractproperty def uid(self): """The user that owns this path. The returned value is a :class:`FSUser ` object which behaves like an ``int`` (as expected from ``uid``), but it also has a ``.name`` attribute that holds the string-name of the user""" @abstractproperty def gid(self): """The group that owns this path. The returned value is a :class:`FSUser ` object which behaves like an ``int`` (as expected from ``gid``), but it also has a ``.name`` attribute that holds the string-name of the group""" @abstractmethod def as_uri(self, scheme=None): """Returns a universal resource identifier. Use ``scheme`` to force a scheme.""" @abstractmethod def _get_info(self): pass @abstractmethod def join(self, *parts): """Joins this path with any number of paths""" @abstractmethod def list(self): """Returns the files in this directory""" @abstractmethod def iterdir(self): """Returns an iterator over the directory. Might be slightly faster on Python 3.5 than .list()""" @abstractmethod def is_dir(self): """Returns ``True`` if this path is a directory, ``False`` otherwise""" def isdir(self): """Included for compatibility with older Plumbum code""" warnings.warn("Use .is_dir() instead", FutureWarning) return self.is_dir() @abstractmethod def is_file(self): """Returns ``True`` if this path is a regular file, ``False`` otherwise""" def isfile(self): """Included for compatibility with older Plumbum code""" warnings.warn("Use .is_file() instead", FutureWarning) return self.is_file() def islink(self): """Included for compatibility with older Plumbum code""" warnings.warn("Use is_symlink instead", FutureWarning) return self.is_symlink() @abstractmethod def is_symlink(self): """Returns ``True`` if this path is a symbolic link, ``False`` otherwise""" @abstractmethod def exists(self): """Returns ``True`` if this path exists, ``False`` otherwise""" @abstractmethod def stat(self): """Returns the os.stats for a file""" pass @abstractmethod def with_name(self, name): """Returns a path with the name replaced""" @abstractmethod def with_suffix(self, suffix, depth=1): """Returns a path with the suffix replaced. Up to last ``depth`` suffixes will be replaced. None will replace all suffixes. If there are less than ``depth`` suffixes, this will replace all suffixes. ``.tar.gz`` is an example where ``depth=2`` or ``depth=None`` is useful""" def preferred_suffix(self, suffix): """Adds a suffix if one does not currently exist (otherwise, no change). Useful for loading files with a default suffix""" if len(self.suffixes) > 0: return self else: return self.with_suffix(suffix) @abstractmethod def glob(self, pattern): """Returns a (possibly empty) list of paths that matched the glob-pattern under this path""" @abstractmethod def delete(self): """Deletes this path (recursively, if a directory)""" @abstractmethod def move(self, dst): """Moves this path to a different location""" def rename(self, newname): """Renames this path to the ``new name`` (only the basename is changed)""" return self.move(self.up() / newname) @abstractmethod def copy(self, dst, override=None): """Copies this path (recursively, if a directory) to the destination path "dst". Raises TypeError if dst exists and override is False. Will overwrite if override is True. Will silently fail to copy if override is None (the default).""" @abstractmethod def mkdir(self, mode=0o777, parents=True, exist_ok=True): """ Creates a directory at this path. :param mode: **Currently only implemented for local paths!** Numeric mode to use for directory creation, which may be ignored on some systems. The current implementation reproduces the behavior of ``os.mkdir`` (i.e., the current umask is first masked out), but this may change for remote paths. As with ``os.mkdir``, it is recommended to call :func:`chmod` explicitly if you need to be sure. :param parents: If this is true (the default), the directory's parents will also be created if necessary. :param exist_ok: If this is true (the default), no exception will be raised if the directory already exists (otherwise ``OSError``). Note that the defaults for ``parents`` and ``exist_ok`` are the opposite of what they are in Python's own ``pathlib`` - this is to maintain backwards-compatibility with Plumbum's behaviour from before they were implemented. """ @abstractmethod def open(self, mode="r"): """opens this path as a file""" @abstractmethod def read(self, encoding=None): """returns the contents of this file as a ``str``. By default the data is read as text, but you can specify the encoding, e.g., ``'latin1'`` or ``'utf8'``""" @abstractmethod def write(self, data, encoding=None): """writes the given data to this file. By default the data is written as-is (either text or binary), but you can specify the encoding, e.g., ``'latin1'`` or ``'utf8'``""" @abstractmethod def touch(self): """Update the access time. Creates an empty file if none exists.""" @abstractmethod def chown(self, owner=None, group=None, recursive=None): """Change ownership of this path. :param owner: The owner to set (either ``uid`` or ``username``), optional :param group: The group to set (either ``gid`` or ``groupname``), optional :param recursive: whether to change ownership of all contained files and subdirectories. Only meaningful when ``self`` is a directory. If ``None``, the value will default to ``True`` if ``self`` is a directory, ``False`` otherwise. """ @abstractmethod def chmod(self, mode): """Change the mode of path to the numeric mode. :param mode: file mode as for os.chmod """ @staticmethod def _access_mode_to_flags( mode, flags={"f": os.F_OK, "w": os.W_OK, "r": os.R_OK, "x": os.X_OK} ): if isinstance(mode, str): mode = reduce(operator.or_, [flags[m] for m in mode.lower()], 0) return mode @abstractmethod def access(self, mode=0): """Test file existence or permission bits :param mode: a bitwise-or of access bits, or a string-representation thereof: ``'f'``, ``'x'``, ``'r'``, ``'w'`` for ``os.F_OK``, ``os.X_OK``, ``os.R_OK``, ``os.W_OK`` """ @abstractmethod def link(self, dst): """Creates a hard link from ``self`` to ``dst`` :param dst: the destination path """ @abstractmethod def symlink(self, dst): """Creates a symbolic link from ``self`` to ``dst`` :param dst: the destination path """ @abstractmethod def unlink(self): """Deletes a symbolic link""" def split(self, *dummy_args, **dummy_kargs): """Splits the path on directory separators, yielding a list of directories, e.g, ``"/var/log/messages"`` will yield ``['var', 'log', 'messages']``. """ parts = [] path = self while path != path.dirname: parts.append(path.name) path = path.dirname return parts[::-1] @property def parts(self): """Splits the directory into parts, including the base directroy, returns a tuple""" return tuple([self.drive + self.root] + self.split()) def relative_to(self, source): """Computes the "relative path" require to get from ``source`` to ``self``. They satisfy the invariant ``source_path + (target_path - source_path) == target_path``. For example:: /var/log/messages - /var/log/messages = [] /var/log/messages - /var = [log, messages] /var/log/messages - / = [var, log, messages] /var/log/messages - /var/tmp = [.., log, messages] /var/log/messages - /opt = [.., var, log, messages] /var/log/messages - /opt/lib = [.., .., var, log, messages] """ if isinstance(source, str): source = self._form(source) parts = self.split() baseparts = source.split() ancestors = len( list(itertools.takewhile(lambda p: p[0] == p[1], zip(parts, baseparts))) ) return RelativePath([".."] * (len(baseparts) - ancestors) + parts[ancestors:]) def __sub__(self, other): """Same as ``self.relative_to(other)``""" return self.relative_to(other) def _glob(self, pattern, fn): """Applies a glob string or list/tuple/iterable to the current path, using ``fn``""" if isinstance(pattern, str): return fn(pattern) else: results = [] for single_pattern in pattern: results.extend(fn(single_pattern)) return sorted(list(set(results))) def resolve(self, strict=False): """Added to allow pathlib like syntax. Does nothing since Plumbum paths are always absolute. Does not (currently) resolve symlinks.""" # TODO: Resolve symlinks here return self @property def parents(self): """Pathlib like sequence of ancestors""" join = lambda x, y: self._form(x) / y as_list = ( reduce(join, self.parts[:i], self.parts[0]) for i in range(len(self.parts) - 1, 0, -1) ) return tuple(as_list) @property def parent(self): """Pathlib like parent of the path.""" return self.parents[0] class RelativePath(object): """ Relative paths are the "delta" required to get from one path to another. Note that relative path do not point at anything, and thus are not paths. Therefore they are system agnostic (but closed under addition) Paths are always absolute and point at "something", whether existent or not. Relative paths are created by subtracting paths (``Path.relative_to``) """ def __init__(self, parts): self.parts = parts def __str__(self): return "/".join(self.parts) def __iter__(self): return iter(self.parts) def __len__(self): return len(self.parts) def __getitem__(self, index): return self.parts[index] def __repr__(self): return "RelativePath({!r})".format(self.parts) def __eq__(self, other): return str(self) == str(other) def __ne__(self, other): return not (self == other) def __gt__(self, other): return str(self) > str(other) def __ge__(self, other): return str(self) >= str(other) def __lt__(self, other): return str(self) < str(other) def __le__(self, other): return str(self) <= str(other) def __hash__(self): return hash(str(self)) def __nonzero__(self): return bool(str(self)) __bool__ = __nonzero__ def up(self, count=1): return RelativePath(self.parts[:-count]) def __radd__(self, path): return path.join(*self.parts) plumbum-1.7.2/plumbum/path/local.py0000644000232200023220000002660014161152302017630 0ustar debalancedebalance# -*- coding: utf-8 -*- import errno import glob import logging import os import shutil import sys from contextlib import contextmanager from plumbum.lib import IS_WIN32, _setdoc, glob_escape, six from plumbum.path.base import FSUser, Path from plumbum.path.remote import RemotePath try: from grp import getgrgid, getgrnam from pwd import getpwnam, getpwuid except ImportError: def getpwuid(x): # type: ignore return (None,) def getgrgid(x): # type: ignore return (None,) def getpwnam(x): # type: ignore raise OSError("`getpwnam` not supported") def getgrnam(x): # type: ignore raise OSError("`getgrnam` not supported") try: # Py3 import urllib.parse as urlparse import urllib.request as urllib except ImportError: import urllib # type: ignore import urlparse # type: ignore logger = logging.getLogger("plumbum.local") # =================================================================================================== # Local Paths # =================================================================================================== class LocalPath(Path): """The class implementing local-machine paths""" CASE_SENSITIVE = not IS_WIN32 def __new__(cls, *parts): if ( len(parts) == 1 and isinstance(parts[0], cls) and not isinstance(parts[0], LocalWorkdir) ): return parts[0] if not parts: raise TypeError("At least one path part is required (none given)") if any(isinstance(path, RemotePath) for path in parts): raise TypeError("LocalPath cannot be constructed from {!r}".format(parts)) self = super(LocalPath, cls).__new__( cls, os.path.normpath(os.path.join(*(str(p) for p in parts))) ) return self @property def _path(self): return str(self) def _get_info(self): return self._path def _form(self, *parts): return LocalPath(*parts) @property # type: ignore @_setdoc(Path) def name(self): return os.path.basename(str(self)) @property # type: ignore @_setdoc(Path) def dirname(self): return LocalPath(os.path.dirname(str(self))) @property # type: ignore @_setdoc(Path) def suffix(self): return os.path.splitext(str(self))[1] @property def suffixes(self): exts = [] base = str(self) while True: base, ext = os.path.splitext(base) if ext: exts.append(ext) else: return list(reversed(exts)) @property # type: ignore @_setdoc(Path) def uid(self): uid = self.stat().st_uid name = getpwuid(uid)[0] return FSUser(uid, name) @property # type: ignore @_setdoc(Path) def gid(self): gid = self.stat().st_gid name = getgrgid(gid)[0] return FSUser(gid, name) @_setdoc(Path) def join(self, *others): return LocalPath(self, *others) @_setdoc(Path) def list(self): return [self / fn for fn in os.listdir(str(self))] @_setdoc(Path) def iterdir(self): try: return (self / fn.name for fn in os.scandir(str(self))) except AttributeError: return (self / fn for fn in os.listdir(str(self))) @_setdoc(Path) def is_dir(self): return os.path.isdir(str(self)) @_setdoc(Path) def is_file(self): return os.path.isfile(str(self)) @_setdoc(Path) def is_symlink(self): return os.path.islink(str(self)) @_setdoc(Path) def exists(self): return os.path.exists(str(self)) @_setdoc(Path) def stat(self): return os.stat(str(self)) @_setdoc(Path) def with_name(self, name): return LocalPath(self.dirname) / name @property # type: ignore @_setdoc(Path) def stem(self): return self.name.rsplit(os.path.extsep)[0] @_setdoc(Path) def with_suffix(self, suffix, depth=1): if suffix and not suffix.startswith(os.path.extsep) or suffix == os.path.extsep: raise ValueError("Invalid suffix %r" % (suffix)) name = self.name depth = len(self.suffixes) if depth is None else min(depth, len(self.suffixes)) for _ in range(depth): name, _ = os.path.splitext(name) return LocalPath(self.dirname) / (name + suffix) @_setdoc(Path) def glob(self, pattern): fn = lambda pat: [ LocalPath(m) for m in glob.glob(os.path.join(glob_escape(str(self)), pat)) ] return self._glob(pattern, fn) @_setdoc(Path) def delete(self): if not self.exists(): return if self.is_dir(): shutil.rmtree(str(self)) else: try: os.remove(str(self)) except OSError: # pragma: no cover # file might already been removed (a race with other threads/processes) _, ex, _ = sys.exc_info() if ex.errno != errno.ENOENT: raise @_setdoc(Path) def move(self, dst): if isinstance(dst, RemotePath): raise TypeError("Cannot move local path {} to {!r}".format(self, dst)) shutil.move(str(self), str(dst)) return LocalPath(dst) @_setdoc(Path) def copy(self, dst, override=None): if isinstance(dst, RemotePath): raise TypeError("Cannot copy local path {} to {!r}".format(self, dst)) dst = LocalPath(dst) if override is False and dst.exists(): raise TypeError("File exists and override was not specified") if override: dst.delete() if self.is_dir(): shutil.copytree(str(self), str(dst)) else: dst_dir = LocalPath(dst).dirname if not dst_dir.exists(): dst_dir.mkdir() shutil.copy2(str(self), str(dst)) return dst @_setdoc(Path) def mkdir(self, mode=0o777, parents=True, exist_ok=True): if not self.exists() or not exist_ok: try: if parents: os.makedirs(str(self), mode) else: os.mkdir(str(self), mode) except OSError: # pragma: no cover # directory might already exist (a race with other threads/processes) _, ex, _ = sys.exc_info() if ex.errno != errno.EEXIST or not exist_ok: raise @_setdoc(Path) def open(self, mode="r"): return open(str(self), mode) @_setdoc(Path) def read(self, encoding=None, mode="r"): if encoding and "b" not in mode: mode = mode + "b" with self.open(mode) as f: data = f.read() if encoding: data = data.decode(encoding) return data @_setdoc(Path) def write(self, data, encoding=None, mode=None): if encoding: data = data.encode(encoding) if mode is None: if isinstance(data, six.unicode_type): mode = "w" else: mode = "wb" with self.open(mode) as f: f.write(data) @_setdoc(Path) def touch(self): with open(str(self), "a"): os.utime(str(self), None) @_setdoc(Path) def chown(self, owner=None, group=None, recursive=None): if not hasattr(os, "chown"): raise OSError("os.chown() not supported") uid = ( self.uid if owner is None else (owner if isinstance(owner, int) else getpwnam(owner)[2]) ) gid = ( self.gid if group is None else (group if isinstance(group, int) else getgrnam(group)[2]) ) os.chown(str(self), uid, gid) if recursive or (recursive is None and self.is_dir()): for subpath in self.walk(): os.chown(str(subpath), uid, gid) @_setdoc(Path) def chmod(self, mode): if not hasattr(os, "chmod"): raise OSError("os.chmod() not supported") os.chmod(str(self), mode) @_setdoc(Path) def access(self, mode=0): return os.access(str(self), self._access_mode_to_flags(mode)) @_setdoc(Path) def link(self, dst): if isinstance(dst, RemotePath): raise TypeError( "Cannot create a hardlink from local path {} to {!r}".format(self, dst) ) if hasattr(os, "link"): os.link(str(self), str(dst)) else: from plumbum.machines.local import local # windows: use mklink if self.is_dir(): local["cmd"]("/C", "mklink", "/D", "/H", str(dst), str(self)) else: local["cmd"]("/C", "mklink", "/H", str(dst), str(self)) @_setdoc(Path) def symlink(self, dst): if isinstance(dst, RemotePath): raise TypeError( "Cannot create a symlink from local path {} to {!r}".format(self, dst) ) if hasattr(os, "symlink"): os.symlink(str(self), str(dst)) else: from plumbum.machines.local import local # windows: use mklink if self.is_dir(): local["cmd"]("/C", "mklink", "/D", str(dst), str(self)) else: local["cmd"]("/C", "mklink", str(dst), str(self)) @_setdoc(Path) def unlink(self): try: if hasattr(os, "symlink") or not self.is_dir(): os.unlink(str(self)) else: # windows: use rmdir for directories and directory symlinks os.rmdir(str(self)) except OSError: # pragma: no cover # file might already been removed (a race with other threads/processes) _, ex, _ = sys.exc_info() if ex.errno != errno.ENOENT: raise @_setdoc(Path) def as_uri(self, scheme="file"): return urlparse.urljoin(str(scheme) + ":", urllib.pathname2url(str(self))) @property # type: ignore @_setdoc(Path) def drive(self): return os.path.splitdrive(str(self))[0] @property # type: ignore @_setdoc(Path) def root(self): return os.path.sep class LocalWorkdir(LocalPath): """Working directory manipulator""" def __hash__(self): raise TypeError("unhashable type") def __new__(cls): return super(LocalWorkdir, cls).__new__(cls, os.getcwd()) def chdir(self, newdir): """Changes the current working directory to the given one :param newdir: The destination director (a string or a ``LocalPath``) """ if isinstance(newdir, RemotePath): raise TypeError("newdir cannot be {!r}".format(newdir)) logger.debug("Chdir to %s", newdir) os.chdir(str(newdir)) return self.__class__() def getpath(self): """Returns the current working directory as a ``LocalPath`` object""" return LocalPath(self._path) @contextmanager def __call__(self, newdir): """A context manager used to ``chdir`` into a directory and then ``chdir`` back to the previous location; much like ``pushd``/``popd``. :param newdir: The destination directory (a string or a ``LocalPath``) """ prev = self._path newdir = self.chdir(newdir) try: yield newdir finally: self.chdir(prev) plumbum-1.7.2/plumbum/lib.py0000644000232200023220000001114714161152302016350 0ustar debalancedebalance# -*- coding: utf-8 -*- import inspect import os import re import sys from contextlib import contextmanager IS_WIN32 = sys.platform == "win32" def _setdoc(super): # @ReservedAssignment """This inherits the docs on the current class. Not really needed for Python 3.5, due to new behavoir of inspect.getdoc, but still doesn't hurt.""" def deco(func): func.__doc__ = getattr(getattr(super, func.__name__, None), "__doc__", None) return func return deco class ProcInfo(object): def __init__(self, pid, uid, stat, args): self.pid = pid self.uid = uid self.stat = stat self.args = args def __repr__(self): return "ProcInfo({!r}, {!r}, {!r}, {!r})".format( self.pid, self.uid, self.stat, self.args ) class six(object): """ A light-weight version of six (which works on IronPython) """ PY3 = sys.version_info[0] >= 3 if sys.version_info >= (3, 4): from abc import ABC else: from abc import ABCMeta ABC = ABCMeta("ABC", (object,), {"__module__": __name__, "__slots__": ()}) # Be sure to use named-tuple access, so that usage is not affected try: getfullargspec = staticmethod(inspect.getfullargspec) except AttributeError: getfullargspec = staticmethod( inspect.getargspec ) # extra fields will not be available if PY3: integer_types = (int,) string_types = (str,) MAXSIZE = sys.maxsize ascii = ascii # @UndefinedVariable bytes = bytes # @ReservedAssignment unicode_type = str @staticmethod def b(s): return s.encode("latin-1", "replace") @staticmethod def u(s): return s @staticmethod def get_method_function(m): return m.__func__ else: integer_types = (int, long) string_types = (str, unicode) MAXSIZE = getattr(sys, "maxsize", sys.maxint) ascii = repr # @ReservedAssignment bytes = str # @ReservedAssignment unicode_type = unicode @staticmethod def b(st): return st @staticmethod def u(s): return s.decode("unicode-escape") @staticmethod def get_method_function(m): return m.im_func str = unicode_type # Try/except fails because io has the wrong StringIO in Python2 # You'll get str/unicode errors if sys.version_info >= (3, 0): from io import StringIO else: from StringIO import StringIO if sys.version_info >= (3,): from glob import escape as glob_escape else: _magic_check = re.compile(u"([*?[])") _magic_check_bytes = re.compile(b"([*?[])") def glob_escape(pathname): drive, pathname = os.path.splitdrive(pathname) if isinstance(pathname, str): pathname = _magic_check_bytes.sub(r"[\1]", pathname) else: pathname = _magic_check.sub(u"[\\1]", pathname) return drive + pathname @contextmanager def captured_stdout(stdin=""): """ Captures stdout (similar to the redirect_stdout in Python 3.4+, but with slightly different arguments) """ prevstdin = sys.stdin prevstdout = sys.stdout sys.stdin = StringIO(six.u(stdin)) sys.stdout = StringIO() try: yield sys.stdout finally: sys.stdin = prevstdin sys.stdout = prevstdout class StaticProperty(object): """This acts like a static property, allowing access via class or object. This is a non-data descriptor.""" def __init__(self, function): self._function = function self.__doc__ = function.__doc__ def __get__(self, obj, klass=None): return self._function() def getdoc(object): """ This gets a docstring if available, and cleans it, but does not look up docs in inheritance tree (Pre 3.5 behavior of ``inspect.getdoc``). """ try: doc = object.__doc__ except AttributeError: return None if not isinstance(doc, str): return None return inspect.cleandoc(doc) def read_fd_decode_safely(fd, size=4096): """ This reads a utf-8 file descriptor and returns a chunck, growing up to three bytes if needed to decode the character at the end. Returns the data and the decoded text. """ data = os.read(fd.fileno(), size) for i in range(4): try: return data, data.decode("utf-8") except UnicodeDecodeError as e: if e.reason != "unexpected end of data": raise if i == 3: raise data += os.read(fd.fileno(), 1) plumbum-1.7.2/plumbum/cli/0000755000232200023220000000000014161152315015777 5ustar debalancedebalanceplumbum-1.7.2/plumbum/cli/terminal.py0000644000232200023220000001631714161152302020170 0ustar debalancedebalance# -*- coding: utf-8 -*- """ Terminal-related utilities -------------------------- """ from __future__ import absolute_import, division, print_function import os import sys from plumbum import local from .progress import Progress from .termsize import get_terminal_size __all__ = ( "readline", "ask", "choose", "prompt", "get_terminal_size", "Progress", "get_terminal_size", ) def __dir__(): return __all__ def readline(message=""): """Gets a line of input from the user (stdin)""" sys.stdout.write(message) sys.stdout.flush() return sys.stdin.readline() def ask(question, default=None): """ Presents the user with a yes/no question. :param question: The question to ask :param default: If ``None``, the user must answer. If ``True`` or ``False``, lack of response is interpreted as the default option :returns: the user's choice """ question = question.rstrip().rstrip("?").rstrip() + "?" if default is None: question += " (y/n) " elif default: question += " [Y/n] " else: question += " [y/N] " while True: try: answer = readline(question).strip().lower() except EOFError: answer = None if answer in ("y", "yes"): return True elif answer in ("n", "no"): return False elif not answer and default is not None: return default else: sys.stdout.write("Invalid response, please try again\n") def choose(question, options, default=None): """Prompts the user with a question and a set of options, from which the user needs to choose. :param question: The question to ask :param options: A set of options. It can be a list (of strings or two-tuples, mapping text to returned-object) or a dict (mapping text to returned-object).`` :param default: If ``None``, the user must answer. Otherwise, lack of response is interpreted as this answer :returns: The user's choice Example:: ans = choose("What is your favorite color?", ["blue", "yellow", "green"], default = "yellow") # `ans` will be one of "blue", "yellow" or "green" ans = choose("What is your favorite color?", {"blue" : 0x0000ff, "yellow" : 0xffff00 , "green" : 0x00ff00}, default = 0x00ff00) # this will display "blue", "yellow" and "green" but return a numerical value """ if hasattr(options, "items"): options = options.items() sys.stdout.write(question.rstrip() + "\n") choices = {} defindex = None for i, item in enumerate(options): i = i + 1 # python2.5 if isinstance(item, (tuple, list)) and len(item) == 2: text = item[0] val = item[1] else: text = item val = item choices[i] = val if default is not None and default == val: defindex = i sys.stdout.write("(%d) %s\n" % (i, text)) if default is not None: if defindex is None: msg = "Choice [{}]: ".format(default) else: msg = "Choice [%d]: " % (defindex,) else: msg = "Choice: " while True: try: choice = readline(msg).strip() except EOFError: choice = "" if not choice and default: return default try: choice = int(choice) if choice not in choices: raise ValueError() except ValueError: sys.stdout.write("Invalid choice, please try again\n") continue return choices[choice] def prompt(question, type=str, default=NotImplemented, validator=lambda val: True): """ Presents the user with a validated question, keeps asking if validation does not pass. :param question: The question to ask :param type: The type of the answer, defaults to str :param default: The default choice :param validator: An extra validator called after type conversion, can raise ValueError or return False to trigger a retry. :returns: the user's choice """ question = question.rstrip(" \t:") if default is not NotImplemented: question += " [{}]".format(default) question += ": " while True: try: ans = readline(question).strip() except EOFError: ans = "" if not ans: if default is not NotImplemented: # sys.stdout.write("\b%s\n" % (default,)) return default else: continue try: ans = type(ans) except (TypeError, ValueError) as ex: sys.stdout.write("Invalid value ({}), please try again\n".format(ex)) continue try: valid = validator(ans) except ValueError as ex: sys.stdout.write("{}, please try again\n".format(ex)) continue if not valid: sys.stdout.write("Value not in specified range, please try again\n") continue return ans def hexdump(data_or_stream, bytes_per_line=16, aggregate=True): """Convert the given bytes (or a stream with a buffering ``read()`` method) to hexdump-formatted lines, with possible aggregation of identical lines. Returns a generator of formatted lines. """ if hasattr(data_or_stream, "read"): def read_chunk(): while True: buf = data_or_stream.read(bytes_per_line) if not buf: break yield buf else: def read_chunk(): for i in range(0, len(data_or_stream), bytes_per_line): yield data_or_stream[i : i + bytes_per_line] prev = None skipped = False for i, chunk in enumerate(read_chunk()): hexd = " ".join("{:02x}".format(ord(ch)) for ch in chunk) text = "".join(ch if 32 <= ord(ch) < 127 else "." for ch in chunk) if aggregate and prev == chunk: skipped = True continue prev = chunk if skipped: yield "*" yield "{:06x} | {}| {}".format( i * bytes_per_line, hexd.ljust(bytes_per_line * 3, " "), text, ) skipped = False def pager(rows, pagercmd=None): # pragma: no cover """Opens a pager (e.g., ``less``) to display the given text. Requires a terminal. :param rows: a ``bytes`` or a list/iterator of "rows" (``bytes``) :param pagercmd: the pager program to run. Defaults to ``less -RSin`` """ if not pagercmd: pagercmd = local["less"]["-RSin"] if hasattr(rows, "splitlines"): rows = rows.splitlines() pg = pagercmd.popen(stdout=None, stderr=None) try: for row in rows: line = "{}\n".format(row) try: pg.stdin.write(line) pg.stdin.flush() except IOError: break pg.stdin.close() pg.wait() finally: try: rows.close() except Exception: pass if pg and pg.poll() is None: try: pg.terminate() except Exception: pass os.system("reset") plumbum-1.7.2/plumbum/cli/application.py0000644000232200023220000011242114161152302020651 0ustar debalancedebalance# -*- coding: utf-8 -*- from __future__ import absolute_import, division, print_function import functools import os import sys from collections import defaultdict from textwrap import TextWrapper from plumbum import colors, local from plumbum.cli.i18n import get_translation_for from plumbum.lib import getdoc, six from .switches import ( CountOf, Flag, MissingArgument, MissingMandatorySwitch, PositionalArgumentsError, SubcommandError, SwitchCombinationError, SwitchError, UnknownSwitch, WrongArgumentType, switch, ) from .terminal import get_terminal_size _translation = get_translation_for(__name__) T_, ngettext = _translation.gettext, _translation.ngettext class ShowHelp(SwitchError): pass class ShowHelpAll(SwitchError): pass class ShowVersion(SwitchError): pass class SwitchParseInfo(object): __slots__ = ["swname", "val", "index", "__weakref__"] def __init__(self, swname, val, index): self.swname = swname self.val = val self.index = index class Subcommand(object): def __init__(self, name, subapplication): self.name = name self.subapplication = subapplication def get(self): if isinstance(self.subapplication, str): modname, clsname = self.subapplication.rsplit(".", 1) mod = __import__(modname, None, None, "*") try: cls = getattr(mod, clsname) except AttributeError: raise ImportError("cannot import name {}".format(clsname)) self.subapplication = cls return self.subapplication def __repr__(self): return T_("Subcommand({self.name}, {self.subapplication})").format(self=self) _switch_groups = ["Switches", "Meta-switches"] _switch_groups_l10n = [T_("Switches"), T_("Meta-switches")] # =================================================================================================== # CLI Application base class # =================================================================================================== class Application(object): """The base class for CLI applications; your "entry point" class should derive from it, define the relevant switch functions and attributes, and the ``main()`` function. The class defines two overridable "meta switches" for version (``-v``, ``--version``) help (``-h``, ``--help``), and help-all (``--help-all``). The signature of the main function matters: any positional arguments (e.g., non-switch arguments) given on the command line are passed to the ``main()`` function; if you wish to allow unlimited number of positional arguments, use varargs (``*args``). The names of the arguments will be shown in the help message. The classmethod ``run`` serves as the entry point of the class. It parses the command-line arguments, invokes switch functions and enters ``main``. You should **not override** this method. Usage:: class FileCopier(Application): stat = Flag("p", "copy stat info as well") def main(self, src, dst): if self.stat: shutil.copy2(src, dst) else: shutil.copy(src, dst) if __name__ == "__main__": FileCopier.run() There are several class-level attributes you may set: * ``PROGNAME`` - the name of the program; if ``None`` (the default), it is set to the name of the executable (``argv[0]``); can be in color. If only a color, will be applied to the name. * ``VERSION`` - the program's version (defaults to ``1.0``, can be in color) * ``DESCRIPTION`` - a short description of your program (shown in help). If not set, the class' ``__doc__`` will be used. Can be in color. * ``DESCRIPTION_MORE`` - a detailed description of your program (shown in help). The text will be printed by paragraphs (specified by empty lines between them). The indentation of each paragraph will be the indentation of its first line. List items are identified by their first non-whitespace character being one of '-', '*', and '/'; so that they are not combined with preceding paragraphs. Bullet '/' is "invisible", meaning that the bullet itself will not be printed to the output. * ``USAGE`` - the usage line (shown in help). * ``COLOR_USAGE_TITLE`` - The color of the usage line's header. * ``COLOR_USAGE`` - The color of the usage line. * ``COLOR_GROUPS`` - A dictionary that sets colors for the groups, like Meta-switches, Switches, and Subcommands. * ``COLOR_GROUP_TITLES`` - A dictionary that sets colors for the group titles. If the dictionary is empty, it defaults to ``COLOR_GROUPS``. * ``SUBCOMMAND_HELPMSG`` - Controls the printing of extra "see subcommand -h" help message. Default is a message, set to False to remove. * ``ALLOW_ABBREV`` - Controls whether partial switch names are supported, for example '--ver' will match '--verbose'. Default is False for backward consistency with previous plumbum releases. Note that ambiguous abbreviations will not match, for example if --foothis and --foothat are defined, then --foo will not match. A note on sub-commands: when an application is the root, its ``parent`` attribute is set to ``None``. When it is used as a nested-command, ``parent`` will point to its direct ancestor. Likewise, when an application is invoked with a sub-command, its ``nested_command`` attribute will hold the chosen sub-application and its command-line arguments (a tuple); otherwise, it will be set to ``None`` """ PROGNAME = None DESCRIPTION = None DESCRIPTION_MORE = None VERSION = None USAGE = None COLOR_USAGE = None COLOR_USAGE_TITLE = None COLOR_GROUPS = None COLOR_GROUP_TITLES = None CALL_MAIN_IF_NESTED_COMMAND = True SUBCOMMAND_HELPMSG = T_("see '{parent} {sub} --help' for more info") ALLOW_ABBREV = False parent = None nested_command = None _unbound_switches = () def __new__(cls, executable=None): """Allows running the class directly as a shortcut for main. This is necessary for some setup scripts that want a single function, instead of an expression with a dot in it.""" if executable is None: return cls.run() # This return value was not a class instance, so __init__ is never called else: return super(Application, cls).__new__(cls) def __init__(self, executable): # Filter colors if self.PROGNAME is None: self.PROGNAME = os.path.basename(executable) elif isinstance(self.PROGNAME, colors._style): self.PROGNAME = self.PROGNAME | os.path.basename(executable) elif colors.filter(self.PROGNAME) == "": self.PROGNAME = colors.extract(self.PROGNAME) | os.path.basename(executable) if self.DESCRIPTION is None: self.DESCRIPTION = getdoc(self) # Allow None for the colors self.COLOR_GROUPS = defaultdict( lambda: colors.do_nothing, dict() if type(self).COLOR_GROUPS is None else type(self).COLOR_GROUPS, ) self.COLOR_GROUP_TITLES = defaultdict( lambda: colors.do_nothing, self.COLOR_GROUPS if type(self).COLOR_GROUP_TITLES is None else type(self).COLOR_GROUP_TITLES, ) if type(self).COLOR_USAGE is None: self.COLOR_USAGE = colors.do_nothing self.executable = executable self._switches_by_name = {} self._switches_by_func = {} self._switches_by_envar = {} self._subcommands = {} for cls in reversed(type(self).mro()): for obj in cls.__dict__.values(): if isinstance(obj, Subcommand): name = colors.filter(obj.name) if name.startswith("-"): raise SubcommandError( T_("Sub-command names cannot start with '-'") ) # it's okay for child classes to override sub-commands set by their parents self._subcommands[name] = obj continue swinfo = getattr(obj, "_switch_info", None) if not swinfo: continue for name in swinfo.names: if name in self._unbound_switches: continue if ( name in self._switches_by_name and not self._switches_by_name[name].overridable ): raise SwitchError( T_( "Switch {name} already defined and is not overridable" ).format(name=name) ) self._switches_by_name[name] = swinfo self._switches_by_func[swinfo.func] = swinfo if swinfo.envname: self._switches_by_envar[swinfo.envname] = swinfo @property def root_app(self): return self.parent.root_app if self.parent else self @classmethod def unbind_switches(cls, *switch_names): """Unbinds the given switch names from this application. For example :: class MyApp(cli.Application): pass MyApp.unbind_switches("--version") """ cls._unbound_switches += tuple( name.lstrip("-") for name in switch_names if name ) @classmethod def subcommand(cls, name, subapp=None): """Registers the given sub-application as a sub-command of this one. This method can be used both as a decorator and as a normal ``classmethod``:: @MyApp.subcommand("foo") class FooApp(cli.Application): pass Or :: MyApp.subcommand("foo", FooApp) .. versionadded:: 1.1 .. versionadded:: 1.3 The sub-command can also be a string, in which case it is treated as a fully-qualified class name and is imported on demand. For example, MyApp.subcommand("foo", "fully.qualified.package.FooApp") """ def wrapper(subapp): attrname = "_subcommand_{}".format( subapp if isinstance(subapp, str) else subapp.__name__ ) setattr(cls, attrname, Subcommand(name, subapp)) return subapp return wrapper(subapp) if subapp else wrapper def _get_partial_matches(self, partialname): matches = [] for switch in self._switches_by_name: if switch.startswith(partialname): matches += [ switch, ] return matches def _parse_args(self, argv): tailargs = [] swfuncs = {} index = 0 while argv: index += 1 a = argv.pop(0) val = None if a == "--": # end of options, treat the rest as tailargs tailargs.extend(argv) break if a in self._subcommands: subcmd = self._subcommands[a].get() self.nested_command = ( subcmd, [self.PROGNAME + " " + self._subcommands[a].name] + argv, ) break elif a.startswith("--") and len(a) >= 3: # [--name], [--name=XXX], [--name, XXX], [--name, ==, XXX], # [--name=, XXX], [--name, =XXX] eqsign = a.find("=") if eqsign >= 0: name = a[2:eqsign] argv.insert(0, a[eqsign:]) else: name = a[2:] if self.ALLOW_ABBREV: partials = self._get_partial_matches(name) if len(partials) == 1: name = partials[0] elif len(partials) > 1: raise UnknownSwitch( T_("Ambiguous partial switch {0}").format("--" + name) ) swname = "--" + name if name not in self._switches_by_name: raise UnknownSwitch(T_("Unknown switch {0}").format(swname)) swinfo = self._switches_by_name[name] if swinfo.argtype: if not argv: raise MissingArgument( T_("Switch {0} requires an argument").format(swname) ) a = argv.pop(0) if a and a[0] == "=": if len(a) >= 2: val = a[1:] else: if not argv: raise MissingArgument( T_("Switch {0} requires an argument").format(swname) ) val = argv.pop(0) else: val = a elif a.startswith("-") and len(a) >= 2: # [-a], [-a, XXX], [-aXXX], [-abc] name = a[1] swname = "-" + name if name not in self._switches_by_name: raise UnknownSwitch(T_("Unknown switch {0}").format(swname)) swinfo = self._switches_by_name[name] if swinfo.argtype: if len(a) >= 3: val = a[2:] else: if not argv: raise MissingArgument( T_("Switch {0} requires an argument").format(swname) ) val = argv.pop(0) elif len(a) >= 3: argv.insert(0, "-" + a[2:]) else: if a.startswith("-"): raise UnknownSwitch(T_("Unknown switch {0}").format(a)) tailargs.append(a) continue # handle argument val = self._handle_argument(val, swinfo.argtype, name) if swinfo.func in swfuncs: if swinfo.list: swfuncs[swinfo.func].val[0].append(val) else: if swfuncs[swinfo.func].swname == swname: raise SwitchError(T_("Switch {0} already given").format(swname)) else: raise SwitchError( T_("Switch {0} already given ({1} is equivalent)").format( swfuncs[swinfo.func].swname, swname ) ) else: if swinfo.list: swfuncs[swinfo.func] = SwitchParseInfo(swname, ([val],), index) elif val is NotImplemented: swfuncs[swinfo.func] = SwitchParseInfo(swname, (), index) else: swfuncs[swinfo.func] = SwitchParseInfo(swname, (val,), index) # Extracting arguments from environment variables envindex = 0 for env, swinfo in self._switches_by_envar.items(): envindex -= 1 envval = local.env.get(env) if envval is None: continue if swinfo.func in swfuncs: continue # skip if overridden by command line arguments val = self._handle_argument(envval, swinfo.argtype, env) envname = "${}".format(env) if swinfo.list: # multiple values over environment variables are not supported, # this will require some sort of escaping and separator convention swfuncs[swinfo.func] = SwitchParseInfo(envname, ([val],), envindex) elif val is NotImplemented: swfuncs[swinfo.func] = SwitchParseInfo(envname, (), envindex) else: swfuncs[swinfo.func] = SwitchParseInfo(envname, (val,), envindex) return swfuncs, tailargs @classmethod def autocomplete(cls, argv): """This is supplied to make subclassing and testing argument completion methods easier""" pass @staticmethod def _handle_argument(val, argtype, name): if argtype: try: return argtype(val) except (TypeError, ValueError): ex = sys.exc_info()[1] # compat raise WrongArgumentType( T_( "Argument of {name} expected to be {argtype}, not {val!r}:\n {ex!r}" ).format(name=name, argtype=argtype, val=val, ex=ex) ) else: return NotImplemented def _validate_args(self, swfuncs, tailargs): if six.get_method_function(self.help) in swfuncs: raise ShowHelp() if six.get_method_function(self.helpall) in swfuncs: raise ShowHelpAll() if six.get_method_function(self.version) in swfuncs: raise ShowVersion() requirements = {} exclusions = {} for swinfo in self._switches_by_func.values(): if swinfo.mandatory and not swinfo.func in swfuncs: raise MissingMandatorySwitch( T_("Switch {0} is mandatory").format( "/".join( ("-" if len(n) == 1 else "--") + n for n in swinfo.names ) ) ) requirements[swinfo.func] = { self._switches_by_name[req] for req in swinfo.requires } exclusions[swinfo.func] = { self._switches_by_name[exc] for exc in swinfo.excludes } # TODO: compute topological order gotten = set(swfuncs.keys()) for func in gotten: missing = {f.func for f in requirements[func]} - gotten if missing: raise SwitchCombinationError( T_("Given {0}, the following are missing {1}").format( swfuncs[func].swname, [self._switches_by_func[f].names[0] for f in missing], ) ) invalid = {f.func for f in exclusions[func]} & gotten if invalid: raise SwitchCombinationError( T_("Given {0}, the following are invalid {1}").format( swfuncs[func].swname, [swfuncs[f].swname for f in invalid] ) ) m = six.getfullargspec(self.main) max_args = six.MAXSIZE if m.varargs else len(m.args) - 1 min_args = len(m.args) - 1 - (len(m.defaults) if m.defaults else 0) if len(tailargs) < min_args: raise PositionalArgumentsError( ngettext( "Expected at least {0} positional argument, got {1}", "Expected at least {0} positional arguments, got {1}", min_args, ).format(min_args, tailargs) ) elif len(tailargs) > max_args: raise PositionalArgumentsError( ngettext( "Expected at most {0} positional argument, got {1}", "Expected at most {0} positional arguments, got {1}", max_args, ).format(max_args, tailargs) ) # Positional arguement validataion if hasattr(self.main, "positional"): tailargs = self._positional_validate( tailargs, self.main.positional, self.main.positional_varargs, m.args[1:], m.varargs, ) elif hasattr(m, "annotations"): args_names = list(m.args[1:]) positional = [None] * len(args_names) varargs = None # All args are positional, so convert kargs to positional for item in m.annotations: if item == m.varargs: varargs = m.annotations[item] elif item != "return": positional[args_names.index(item)] = m.annotations[item] tailargs = self._positional_validate( tailargs, positional, varargs, m.args[1:], m.varargs ) ordered = [ (f, a) for _, f, a in sorted((sf.index, f, sf.val) for f, sf in swfuncs.items()) ] return ordered, tailargs def _positional_validate(self, args, validator_list, varargs, argnames, varargname): """Makes sure args follows the validation given input""" out_args = list(args) for i in range(min(len(args), len(validator_list))): if validator_list[i] is not None: out_args[i] = self._handle_argument( args[i], validator_list[i], argnames[i] ) if len(args) > len(validator_list): if varargs is not None: out_args[len(validator_list) :] = [ self._handle_argument(a, varargs, varargname) for a in args[len(validator_list) :] ] else: out_args[len(validator_list) :] = args[len(validator_list) :] return out_args @classmethod def run(cls, argv=None, exit=True): # @ReservedAssignment """ Runs the application, taking the arguments from ``sys.argv`` by default if nothing is passed. If ``exit`` is ``True`` (the default), the function will exit with the appropriate return code; otherwise it will return a tuple of ``(inst, retcode)``, where ``inst`` is the application instance created internally by this function and ``retcode`` is the exit code of the application. .. note:: Setting ``exit`` to ``False`` is intendend for testing/debugging purposes only -- do not override it in other situations. """ if argv is None: argv = sys.argv cls.autocomplete(argv) argv = list(argv) inst = cls(argv.pop(0)) retcode = 0 try: swfuncs, tailargs = inst._parse_args(argv) ordered, tailargs = inst._validate_args(swfuncs, tailargs) except ShowHelp: inst.help() except ShowHelpAll: inst.helpall() except ShowVersion: inst.version() except SwitchError: ex = sys.exc_info()[1] # compatibility with python 2.5 print(T_("Error: {0}").format(ex)) print(T_("------")) inst.help() retcode = 2 else: for f, a in ordered: f(inst, *a) cleanup = None if not inst.nested_command or inst.CALL_MAIN_IF_NESTED_COMMAND: retcode = inst.main(*tailargs) cleanup = functools.partial(inst.cleanup, retcode) if not retcode and inst.nested_command: subapp, argv = inst.nested_command subapp.parent = inst inst, retcode = subapp.run(argv, exit=False) if cleanup: cleanup() if retcode is None: retcode = 0 if exit: sys.exit(retcode) else: return inst, retcode @classmethod def invoke(cls, *args, **switches): """Invoke this application programmatically (as a function), in the same way ``run()`` would. There are two key differences: the return value of ``main()`` is not converted to an integer (returned as-is), and exceptions are not swallowed either. :param args: any positional arguments for ``main()`` :param switches: command-line switches are passed as keyword arguments, e.g., ``foo=5`` for ``--foo=5`` """ inst = cls("") swfuncs = inst._parse_kwd_args(switches) ordered, tailargs = inst._validate_args(swfuncs, args) for f, a in ordered: f(inst, *a) cleanup = None if not inst.nested_command or inst.CALL_MAIN_IF_NESTED_COMMAND: retcode = inst.main(*tailargs) cleanup = functools.partial(inst.cleanup, retcode) if not retcode and inst.nested_command: subapp, argv = inst.nested_command subapp.parent = inst inst, retcode = subapp.run(argv, exit=False) if cleanup: cleanup() return inst, retcode def _parse_kwd_args(self, switches): """Parses keywords (positional arguments), used by invoke.""" swfuncs = {} for index, (swname, val) in enumerate(switches.items(), 1): switch = getattr(type(self), swname) swinfo = self._switches_by_func[switch._switch_info.func] if isinstance(switch, CountOf): p = (range(val),) elif swinfo.list and not hasattr(val, "__iter__"): raise SwitchError( T_("Switch {0} must be a sequence (iterable)").format(swname) ) elif not swinfo.argtype: # a flag if val not in (True, False, None, Flag): raise SwitchError(T_("Switch {0} is a boolean flag").format(swname)) p = () else: p = (val,) swfuncs[swinfo.func] = SwitchParseInfo(swname, p, index) return swfuncs def main(self, *args): """Implement me (no need to call super)""" if self._subcommands: if args: print(T_("Unknown sub-command '{0}'").format(args[0])) print(T_("------")) self.help() return 1 if not self.nested_command: print(T_("No sub-command given")) print(T_("------")) self.help() return 1 else: print(T_("main() not implemented")) return 1 def cleanup(self, retcode): """Called after ``main()`` and all sub-applications have executed, to perform any necessary cleanup. :param retcode: the return code of ``main()`` """ @switch( ["--help-all"], overridable=True, group="Meta-switches", help=T_("""Prints help messages of all sub-commands and quits"""), ) def helpall(self): """Prints help messages of all sub-commands and quits""" self.help() print("") if self._subcommands: for name, subcls in sorted(self._subcommands.items()): subapp = (subcls.get())("{} {}".format(self.PROGNAME, name)) subapp.parent = self for si in subapp._switches_by_func.values(): if si.group == "Meta-switches": si.group = "Hidden-switches" subapp.helpall() @switch( ["-h", "--help"], overridable=True, group="Meta-switches", help=T_("""Prints this help message and quits"""), ) def help(self): # @ReservedAssignment """Prints this help message and quits""" if self._get_prog_version(): self.version() print("") if self.DESCRIPTION: print(self.DESCRIPTION.strip() + "\n") def split_indentation(s): """Identifies the initial indentation (all spaces) of the string and returns the indentation as well as the remainder of the line. """ i = 0 while i < len(s) and s[i] == " ": i += 1 return s[:i], s[i:] def paragraphs(text): """Yields each paragraph of text along with its initial and subsequent indentations to be used by textwrap.TextWrapper. Identifies list items from their first non-space character being one of bullets '-', '*', and '/'. However, bullet '/' is invisible and is removed from the list item. :param text: The text to separate into paragraphs """ paragraph = None initial_indent = "" subsequent_indent = "" def current(): """Yields the current result if present.""" if paragraph: yield paragraph, initial_indent, subsequent_indent for part in text.lstrip("\n").split("\n"): indent, line = split_indentation(part) if len(line) == 0: # Starting a new paragraph for item in current(): yield item yield "", "", "" paragraph = None initial_indent = "" subsequent_indent = "" else: # Adding to current paragraph def is_list_item(line): """Returns true if the first element of 'line' is a bullet character.""" bullets = ["-", "*", "/"] return line[0] in bullets def has_invisible_bullet(line): """Returns true if the first element of 'line' is the invisible bullet ('/').""" return line[0] == "/" if is_list_item(line): # Done with current paragraph for item in current(): yield item if has_invisible_bullet(line): line = line[1:] paragraph = line initial_indent = indent # Calculate extra indentation for subsequent lines of this list item i = 1 while i < len(line) and line[i] == " ": i += 1 subsequent_indent = indent + " " * i else: if not paragraph: # Start a new paragraph paragraph = line initial_indent = indent subsequent_indent = indent else: # Add to current paragraph paragraph = paragraph + " " + line for item in current(): yield item def wrapped_paragraphs(text, width): """Yields each line of each paragraph of text after wrapping them on 'width' number of columns. :param text: The text to yield wrapped lines of :param width: The width of the wrapped output """ if not text: return width = max(width, 1) for paragraph, initial_indent, subsequent_indent in paragraphs(text): wrapper = TextWrapper( width, initial_indent=initial_indent, subsequent_indent=subsequent_indent, ) w = wrapper.wrap(paragraph) for line in w: yield line if len(w) == 0: yield "" cols, _ = get_terminal_size() for line in wrapped_paragraphs(self.DESCRIPTION_MORE, cols): print(line) m = six.getfullargspec(self.main) tailargs = m.args[1:] # skip self if m.defaults: for i, d in enumerate(reversed(m.defaults)): tailargs[-i - 1] = "[{}={}]".format(tailargs[-i - 1], d) if m.varargs: tailargs.append( "{}...".format( m.varargs, ) ) tailargs = " ".join(tailargs) utc = self.COLOR_USAGE_TITLE if self.COLOR_USAGE_TITLE else self.COLOR_USAGE print(utc | T_("Usage:")) with self.COLOR_USAGE: if not self.USAGE: if self._subcommands: self.USAGE = T_( " {progname} [SWITCHES] [SUBCOMMAND [SWITCHES]] {tailargs}\n" ) else: self.USAGE = T_(" {progname} [SWITCHES] {tailargs}\n") print( self.USAGE.format( progname=colors.filter(self.PROGNAME), tailargs=tailargs ) ) by_groups = {} for si in self._switches_by_func.values(): if si.group not in by_groups: by_groups[si.group] = [] by_groups[si.group].append(si) def switchs(by_groups, show_groups): for grp, swinfos in sorted(by_groups.items(), key=lambda item: item[0]): if show_groups: lgrp = T_(grp) if grp in _switch_groups else grp print(self.COLOR_GROUP_TITLES[grp] | lgrp + ":") for si in sorted(swinfos, key=lambda si: si.names): swnames = ", ".join( ("-" if len(n) == 1 else "--") + n for n in si.names if n in self._switches_by_name and self._switches_by_name[n] == si ) if si.argtype: if hasattr(si.argtype, "__name__"): typename = si.argtype.__name__ else: typename = str(si.argtype) argtype = " {}:{}".format(si.argname.upper(), typename) else: argtype = "" prefix = swnames + argtype yield si, prefix, self.COLOR_GROUPS[grp] if show_groups: print("") sw_width = ( max(len(prefix) for si, prefix, color in switchs(by_groups, False)) + 4 ) description_indent = " {0}{1}{2}" wrapper = TextWrapper(width=max(cols - min(sw_width, 60), 50) - 6) indentation = "\n" + " " * (cols - wrapper.width) for switch_info, prefix, color in switchs(by_groups, True): help = switch_info.help # @ReservedAssignment if switch_info.list: help += T_("; may be given multiple times") if switch_info.mandatory: help += T_("; required") if switch_info.requires: help += T_("; requires {0}").format( ", ".join( (("-" if len(switch) == 1 else "--") + switch) for switch in switch_info.requires ) ) if switch_info.excludes: help += T_("; excludes {0}").format( ", ".join( (("-" if len(switch) == 1 else "--") + switch) for switch in switch_info.excludes ) ) msg = indentation.join( wrapper.wrap(" ".join(l.strip() for l in help.splitlines())) ) if len(prefix) + wrapper.width >= cols: padding = indentation else: padding = " " * max(cols - wrapper.width - len(prefix) - 4, 1) print(description_indent.format(color | prefix, padding, color | msg)) if self._subcommands: gc = self.COLOR_GROUP_TITLES["Sub-commands"] print(gc | T_("Sub-commands:")) for name, subcls in sorted(self._subcommands.items()): with gc: subapp = subcls.get() doc = subapp.DESCRIPTION if subapp.DESCRIPTION else getdoc(subapp) if self.SUBCOMMAND_HELPMSG: help = doc + "; " if doc else "" # @ReservedAssignment help += self.SUBCOMMAND_HELPMSG.format( parent=self.PROGNAME, sub=name ) else: help = doc if doc else "" # @ReservedAssignment msg = indentation.join( wrapper.wrap(" ".join(l.strip() for l in help.splitlines())) ) if len(name) + wrapper.width >= cols: padding = indentation else: padding = " " * max(cols - wrapper.width - len(name) - 4, 1) if colors.contains_colors(subcls.name): bodycolor = colors.extract(subcls.name) else: bodycolor = gc print( description_indent.format( subcls.name, padding, bodycolor | colors.filter(msg) ) ) def _get_prog_version(self): ver = None curr = self while curr is not None: ver = getattr(curr, "VERSION", None) if ver is not None: return ver curr = curr.parent return ver @switch( ["-v", "--version"], overridable=True, group="Meta-switches", help=T_("""Prints the program's version and quits"""), ) def version(self): """Prints the program's version and quits""" ver = self._get_prog_version() ver_name = ver if ver is not None else T_("(version not set)") print("{} {}".format(self.PROGNAME, ver_name)) plumbum-1.7.2/plumbum/cli/progress.py0000644000232200023220000002052614161152302020216 0ustar debalancedebalance# -*- coding: utf-8 -*- """ Progress bar ------------ """ from __future__ import division, print_function import datetime import sys import warnings from abc import abstractmethod from plumbum.cli.termsize import get_terminal_size from plumbum.lib import six class ProgressBase(six.ABC): """Base class for progress bars. Customize for types of progress bars. :param iterator: The iterator to wrap with a progress bar :param length: The length of the iterator (will use ``__len__`` if None) :param timer: Try to time the completion status of the iterator :param body: True if the slow portion occurs outside the iterator (in a loop, for example) :param has_output: True if the iteration body produces output to the screen (forces rewrite off) :param clear: Clear the progress bar afterwards, if applicable. """ def __init__( self, iterator=None, length=None, timer=True, body=False, has_output=False, clear=True, ): if length is None: length = len(iterator) elif iterator is None: iterator = range(length) elif length is None and iterator is None: raise TypeError("Expected either an iterator or a length") self.length = length self.iterator = iterator self.timer = timer self.body = body self.has_output = has_output self.clear = clear def __len__(self): return self.length def __iter__(self): self.start() return self @abstractmethod def start(self): """This should initialize the progress bar and the iterator""" self.iter = iter(self.iterator) self.value = -1 if self.body else 0 self._start_time = datetime.datetime.now() def __next__(self): try: rval = next(self.iter) self.increment() except StopIteration: self.done() raise return rval def next(self): return self.__next__() @property def value(self): """This is the current value, as a property so setting it can be customized""" return self._value @value.setter def value(self, val): self._value = val @abstractmethod def display(self): """Called to update the progress bar""" pass def increment(self): """Sets next value and displays the bar""" self.value += 1 self.display() def time_remaining(self): """Get the time remaining for the progress bar, guesses""" if self.value < 1: return None, None elapsed_time = datetime.datetime.now() - self._start_time time_each = ( elapsed_time.days * 24 * 60 * 60 + elapsed_time.seconds + elapsed_time.microseconds / 1000000.0 ) / self.value time_remaining = time_each * (self.length - self.value) return elapsed_time, datetime.timedelta(0, time_remaining, 0) def str_time_remaining(self): """Returns a string version of time remaining""" if self.value < 1: return "Starting... " else: elapsed_time, time_remaining = list(map(str, self.time_remaining())) return "{} completed, {} remaining".format( elapsed_time.split(".")[0], time_remaining.split(".")[0] ) @abstractmethod def done(self): """Is called when the iterator is done.""" pass @classmethod def range(cls, *value, **kargs): """Fast shortcut to create a range based progress bar, assumes work done in body""" return cls(range(*value), body=True, **kargs) @classmethod def wrap(cls, iterator, length=None, **kargs): """Shortcut to wrap an iterator that does not do all the work internally""" return cls(iterator, length, body=True, **kargs) class Progress(ProgressBase): def start(self): super(Progress, self).start() self.display() def done(self): self.value = self.length self.display() if self.clear and not self.has_output: print("\r", len(str(self)) * " ", "\r", end="", sep="") else: print() def __str__(self): width = get_terminal_size(default=(0, 0))[0] if self.length == 0: self.width = 0 return "0/0 complete" percent = max(self.value, 0) / self.length ending = " " + ( self.str_time_remaining() if self.timer else "{} of {} complete".format(self.value, self.length) ) if width - len(ending) < 10 or self.has_output: self.width = 0 if self.timer: return "{:.0%} complete: {}".format(percent, self.str_time_remaining()) else: return "{:.0%} complete".format(percent) else: self.width = width - len(ending) - 2 - 1 nstars = int(percent * self.width) pbar = "[" + "*" * nstars + " " * (self.width - nstars) + "]" + ending str_percent = " {:.0%} ".format(percent) return ( pbar[: self.width // 2 - 2] + str_percent + pbar[self.width // 2 + len(str_percent) - 2 :] ) def display(self): disptxt = str(self) if self.width == 0 or self.has_output: print(disptxt) else: print("\r", end="") print(disptxt, end="") sys.stdout.flush() class ProgressIPy(ProgressBase): # pragma: no cover HTMLBOX = '
{0}
' def __init__(self, *args, **kargs): # Ipython gives warnings when using widgets about the API potentially changing with warnings.catch_warnings(): warnings.simplefilter("ignore") try: from ipywidgets import HTML, HBox, IntProgress except ImportError: # Support IPython < 4.0 from IPython.html.widgets import HTML, HBox, IntProgress super(ProgressIPy, self).__init__(*args, **kargs) self.prog = IntProgress(max=self.length) self._label = HTML() self._box = HBox((self.prog, self._label)) def start(self): from IPython.display import display display(self._box) super(ProgressIPy, self).start() @property def value(self): """This is the current value, -1 allowed (automatically fixed for display)""" return self._value @value.setter def value(self, val): self._value = val self.prog.value = max(val, 0) self.prog.description = "{:.2%}".format(self.value / self.length) if self.timer and val > 0: self._label.value = self.HTMLBOX.format(self.str_time_remaining()) def display(self): pass def done(self): if self.clear: self._box.close() class ProgressAuto(ProgressBase): """Automatically selects the best progress bar (IPython HTML or text). Does not work with qtconsole (as that is correctly identified as identical to notebook, since the kernel is the same); it will still iterate, but no graphical indication will be diplayed. :param iterator: The iterator to wrap with a progress bar :param length: The length of the iterator (will use ``__len__`` if None) :param timer: Try to time the completion status of the iterator :param body: True if the slow portion occurs outside the iterator (in a loop, for example) """ def __new__(cls, *args, **kargs): """Uses the generator trick that if a cls instance is returned, the __init__ method is not called.""" try: # pragma: no cover __IPYTHON__ try: from traitlets import TraitError except ImportError: # Support for IPython < 4.0 from IPython.utils.traitlets import TraitError try: return ProgressIPy(*args, **kargs) except TraitError: raise NameError() except (NameError, ImportError): return Progress(*args, **kargs) ProgressAuto.register(ProgressIPy) ProgressAuto.register(Progress) def main(): import time tst = Progress.range(20) for i in tst: time.sleep(1) if __name__ == "__main__": main() plumbum-1.7.2/plumbum/cli/termsize.py0000644000232200023220000000553214161152302020214 0ustar debalancedebalance# -*- coding: utf-8 -*- """ Terminal size utility --------------------- """ from __future__ import absolute_import, division, print_function import os import platform import warnings from struct import Struct def get_terminal_size(default=(80, 25)): """ Get width and height of console; works on linux, os x, windows and cygwin Adapted from https://gist.github.com/jtriley/1108174 Originally from: http://stackoverflow.com/questions/566746/how-to-get-console-window-width-in-python """ current_os = platform.system() if current_os == "Windows": # pragma: no cover size = _get_terminal_size_windows() if not size: # needed for window's python in cygwin's xterm! size = _get_terminal_size_tput() elif current_os in ("Linux", "Darwin", "FreeBSD", "SunOS") or current_os.startswith( "CYGWIN" ): size = _get_terminal_size_linux() else: # pragma: no cover warnings.warn( "Plumbum does not know the type of the current OS for term size, defaulting to UNIX" ) size = _get_terminal_size_linux() if ( size is None ): # we'll assume the standard 80x25 if for any reason we don't know the terminal size size = default return size def _get_terminal_size_windows(): # pragma: no cover try: from ctypes import create_string_buffer, windll STDERR_HANDLE = -12 h = windll.kernel32.GetStdHandle(STDERR_HANDLE) csbi_struct = Struct("hhhhHhhhhhh") csbi = create_string_buffer(csbi_struct.size) res = windll.kernel32.GetConsoleScreenBufferInfo(h, csbi) if res: _, _, _, _, _, left, top, right, bottom, _, _ = csbi_struct.unpack(csbi.raw) return right - left + 1, bottom - top + 1 return None except Exception: return None def _get_terminal_size_tput(): # pragma: no cover # get terminal width # src: http://stackoverflow.com/questions/263890/how-do-i-find-the-width-height-of-a-terminal-window try: tput = local["tput"] cols = int(tput("cols")) rows = int(tput("lines")) return (cols, rows) except Exception: return None def _ioctl_GWINSZ(fd): yx = Struct("hh") try: import fcntl import termios return yx.unpack(fcntl.ioctl(fd, termios.TIOCGWINSZ, "1234")) except Exception: return None def _get_terminal_size_linux(): cr = _ioctl_GWINSZ(0) or _ioctl_GWINSZ(1) or _ioctl_GWINSZ(2) if not cr: try: fd = os.open(os.ctermid(), os.O_RDONLY) cr = _ioctl_GWINSZ(fd) os.close(fd) except Exception: pass if not cr: try: cr = (int(os.environ["LINES"]), int(os.environ["COLUMNS"])) except Exception: return None return cr[1], cr[0] plumbum-1.7.2/plumbum/cli/__init__.py0000644000232200023220000000123614161152302020106 0ustar debalancedebalance# -*- coding: utf-8 -*- from __future__ import absolute_import from .application import Application from .config import Config, ConfigINI from .switches import ( CSV, CountOf, ExistingDirectory, ExistingFile, Flag, NonexistentPath, Predicate, Range, Set, SwitchAttr, SwitchError, autoswitch, positional, switch, ) __all__ = ( "Application", "Config", "ConfigINI", "CSV", "CountOf", "ExistingDirectory", "ExistingFile", "Flag", "NonexistentPath", "Predicate", "Range", "Set", "SwitchAttr", "SwitchError", "autoswitch", "positional", "switch", ) plumbum-1.7.2/plumbum/cli/switches.py0000644000232200023220000004737214161152302020213 0ustar debalancedebalance# -*- coding: utf-8 -*- from abc import abstractmethod from plumbum import local from plumbum.cli.i18n import get_translation_for from plumbum.lib import getdoc, six _translation = get_translation_for(__name__) _, ngettext = _translation.gettext, _translation.ngettext class SwitchError(Exception): """A general switch related-error (base class of all other switch errors)""" pass class PositionalArgumentsError(SwitchError): """Raised when an invalid number of positional arguments has been given""" pass class SwitchCombinationError(SwitchError): """Raised when an invalid combination of switches has been given""" pass class UnknownSwitch(SwitchError): """Raised when an unrecognized switch has been given""" pass class MissingArgument(SwitchError): """Raised when a switch requires an argument, but one was not provided""" pass class MissingMandatorySwitch(SwitchError): """Raised when a mandatory switch has not been given""" pass class WrongArgumentType(SwitchError): """Raised when a switch expected an argument of some type, but an argument of a wrong type has been given""" pass class SubcommandError(SwitchError): """Raised when there's something wrong with sub-commands""" pass # =================================================================================================== # The switch decorator # =================================================================================================== class SwitchInfo(object): def __init__(self, **kwargs): for k, v in kwargs.items(): setattr(self, k, v) def switch( names, argtype=None, argname=None, list=False, mandatory=False, requires=(), excludes=(), help=None, overridable=False, group="Switches", envname=None, ): """ A decorator that exposes functions as command-line switches. Usage:: class MyApp(Application): @switch(["-l", "--log-to-file"], argtype = str) def log_to_file(self, filename): handler = logging.FileHandler(filename) logger.addHandler(handler) @switch(["--verbose"], excludes=["--terse"], requires=["--log-to-file"]) def set_debug(self): logger.setLevel(logging.DEBUG) @switch(["--terse"], excludes=["--verbose"], requires=["--log-to-file"]) def set_terse(self): logger.setLevel(logging.WARNING) :param names: The name(s) under which the function is reachable; it can be a string or a list of string, but at least one name is required. There's no need to prefix the name with ``-`` or ``--`` (this is added automatically), but it can be used for clarity. Single-letter names are prefixed by ``-``, while longer names are prefixed by ``--`` :param envname: Name of environment variable to extract value from, as alternative to argv :param argtype: If this function takes an argument, you need to specify its type. The default is ``None``, which means the function takes no argument. The type is more of a "validator" than a real type; it can be any callable object that raises a ``TypeError`` if the argument is invalid, or returns an appropriate value on success. If the user provides an invalid value, :func:`plumbum.cli.WrongArgumentType` :param argname: The name of the argument; if ``None``, the name will be inferred from the function's signature :param list: Whether or not this switch can be repeated (e.g. ``gcc -I/lib -I/usr/lib``). If ``False``, only a single occurrence of the switch is allowed; if ``True``, it may be repeated indefinitely. The occurrences are collected into a list, so the function is only called once with the collections. For instance, for ``gcc -I/lib -I/usr/lib``, the function will be called with ``["/lib", "/usr/lib"]``. :param mandatory: Whether or not this switch is mandatory; if a mandatory switch is not given, :class:`MissingMandatorySwitch ` is raised. The default is ``False``. :param requires: A list of switches that this switch depends on ("requires"). This means that it's invalid to invoke this switch without also invoking the required ones. In the example above, it's illegal to pass ``--verbose`` or ``--terse`` without also passing ``--log-to-file``. By default, this list is empty, which means the switch has no prerequisites. If an invalid combination is given, :class:`SwitchCombinationError ` is raised. Note that this list is made of the switch *names*; if a switch has more than a single name, any of its names will do. .. note:: There is no guarantee on the (topological) order in which the actual switch functions will be invoked, as the dependency graph might contain cycles. :param excludes: A list of switches that this switch forbids ("excludes"). This means that it's invalid to invoke this switch if any of the excluded ones are given. In the example above, it's illegal to pass ``--verbose`` along with ``--terse``, as it will result in a contradiction. By default, this list is empty, which means the switch has no prerequisites. If an invalid combination is given, :class:`SwitchCombinationError ` is raised. Note that this list is made of the switch *names*; if a switch has more than a single name, any of its names will do. :param help: The help message (description) for this switch; this description is used when ``--help`` is given. If ``None``, the function's docstring will be used. :param overridable: Whether or not the names of this switch are overridable by other switches. If ``False`` (the default), having another switch function with the same name(s) will cause an exception. If ``True``, this is silently ignored. :param group: The switch's *group*; this is a string that is used to group related switches together when ``--help`` is given. The default group is ``Switches``. :returns: The decorated function (with a ``_switch_info`` attribute) """ if isinstance(names, six.string_types): names = [names] names = [n.lstrip("-") for n in names] requires = [n.lstrip("-") for n in requires] excludes = [n.lstrip("-") for n in excludes] def deco(func): if argname is None: argspec = six.getfullargspec(func).args if len(argspec) == 2: argname2 = argspec[1] else: argname2 = _("VALUE") else: argname2 = argname help2 = getdoc(func) if help is None else help if not help2: help2 = str(func) func._switch_info = SwitchInfo( names=names, envname=envname, argtype=argtype, list=list, func=func, mandatory=mandatory, overridable=overridable, group=group, requires=requires, excludes=excludes, argname=argname2, help=help2, ) return func return deco def autoswitch(*args, **kwargs): """A decorator that exposes a function as a switch, "inferring" the name of the switch from the function's name (converting to lower-case, and replacing underscores with hyphens). The arguments are the same as for :func:`switch `.""" def deco(func): return switch(func.__name__.replace("_", "-"), *args, **kwargs)(func) return deco # =================================================================================================== # Switch Attributes # =================================================================================================== class SwitchAttr(object): """ A switch that stores its result in an attribute (descriptor). Usage:: class MyApp(Application): logfile = SwitchAttr(["-f", "--log-file"], str) def main(self): if self.logfile: open(self.logfile, "w") :param names: The switch names :param argtype: The switch argument's (and attribute's) type :param default: The attribute's default value (``None``) :param argname: The switch argument's name (default is ``"VALUE"``) :param kwargs: Any of the keyword arguments accepted by :func:`switch ` """ ATTR_NAME = "__plumbum_switchattr_dict__" def __init__( self, names, argtype=str, default=None, list=False, argname=_("VALUE"), **kwargs ): self.__doc__ = "Sets an attribute" # to prevent the help message from showing SwitchAttr's docstring if default and argtype is not None: defaultmsg = _("; the default is {0}").format(default) if "help" in kwargs: kwargs["help"] += defaultmsg else: kwargs["help"] = defaultmsg.lstrip("; ") switch(names, argtype=argtype, argname=argname, list=list, **kwargs)(self) listtype = type([]) if list: if default is None: self._default_value = [] elif isinstance(default, (tuple, listtype)): self._default_value = listtype(default) else: self._default_value = [default] else: self._default_value = default def __call__(self, inst, val): self.__set__(inst, val) def __get__(self, inst, cls): if inst is None: return self else: return getattr(inst, self.ATTR_NAME, {}).get(self, self._default_value) def __set__(self, inst, val): if inst is None: raise AttributeError("cannot set an unbound SwitchAttr") else: if not hasattr(inst, self.ATTR_NAME): setattr(inst, self.ATTR_NAME, {self: val}) else: getattr(inst, self.ATTR_NAME)[self] = val class Flag(SwitchAttr): """A specialized :class:`SwitchAttr ` for boolean flags. If the flag is not given, the value of this attribute is ``default``; if it is given, the value changes to ``not default``. Usage:: class MyApp(Application): verbose = Flag(["-v", "--verbose"], help = "If given, I'll be very talkative") :param names: The switch names :param default: The attribute's initial value (``False`` by default) :param kwargs: Any of the keyword arguments accepted by :func:`switch `, except for ``list`` and ``argtype``. """ def __init__(self, names, default=False, **kwargs): SwitchAttr.__init__( self, names, argtype=None, default=default, list=False, **kwargs ) def __call__(self, inst): self.__set__(inst, not self._default_value) class CountOf(SwitchAttr): """A specialized :class:`SwitchAttr ` that counts the number of occurrences of the switch in the command line. Usage:: class MyApp(Application): verbosity = CountOf(["-v", "--verbose"], help = "The more, the merrier") If ``-v -v -vv`` is given in the command-line, it will result in ``verbosity = 4``. :param names: The switch names :param default: The default value (0) :param kwargs: Any of the keyword arguments accepted by :func:`switch `, except for ``list`` and ``argtype``. """ def __init__(self, names, default=0, **kwargs): SwitchAttr.__init__( self, names, argtype=None, default=default, list=True, **kwargs ) self._default_value = default # issue #118 def __call__(self, inst, v): self.__set__(inst, len(v)) # =================================================================================================== # Decorator for function that adds argument checking # =================================================================================================== class positional(object): """ Runs a validator on the main function for a class. This should be used like this:: class MyApp(cli.Application): @cli.positional(cli.Range(1,10), cli.ExistingFile) def main(self, x, *f): # x is a range, f's are all ExistingFile's) Or, Python 3 only:: class MyApp(cli.Application): def main(self, x : cli.Range(1,10), *f : cli.ExistingFile): # x is a range, f's are all ExistingFile's) If you do not want to validate on the annotations, use this decorator ( even if empty) to override annotation validation. Validators should be callable, and should have a ``.choices()`` function with possible choices. (For future argument completion, for example) Default arguments do not go through the validator. #TODO: Check with MyPy """ def __init__(self, *args, **kargs): self.args = args self.kargs = kargs def __call__(self, function): m = six.getfullargspec(function) args_names = list(m.args[1:]) positional = [None] * len(args_names) varargs = None for i in range(min(len(positional), len(self.args))): positional[i] = self.args[i] if len(args_names) + 1 == len(self.args): varargs = self.args[-1] # All args are positional, so convert kargs to positional for item in self.kargs: if item == m.varargs: varargs = self.kargs[item] else: positional[args_names.index(item)] = self.kargs[item] function.positional = positional function.positional_varargs = varargs return function class Validator(six.ABC): __slots__ = () @abstractmethod def __call__(self, obj): "Must be implemented for a Validator to work" def choices(self, partial=""): """Should return set of valid choices, can be given optional partial info""" return set() def __repr__(self): """If not overridden, will print the slots as args""" slots = {} for cls in self.__mro__: for prop in getattr(cls, "__slots__", ()): if prop[0] != "_": slots[prop] = getattr(self, prop) mystrs = ("{} = {}".format(name, slots[name]) for name in slots) return "{}({})".format(self.__class__.__name__, ", ".join(mystrs)) # =================================================================================================== # Switch type validators # =================================================================================================== class Range(Validator): """ A switch-type validator that checks for the inclusion of a value in a certain range. Usage:: class MyApp(Application): age = SwitchAttr(["--age"], Range(18, 120)) :param start: The minimal value :param end: The maximal value """ __slots__ = ("start", "end") def __init__(self, start, end): self.start = start self.end = end def __repr__(self): return "[{:d}..{:d}]".format(self.start, self.end) def __call__(self, obj): obj = int(obj) if obj < self.start or obj > self.end: raise ValueError( _("Not in range [{0:d}..{1:d}]").format(self.start, self.end) ) return obj def choices(self, partial=""): # TODO: Add partial handling return set(range(self.start, self.end + 1)) class Set(Validator): """ A switch-type validator that checks that the value is contained in a defined set of values. Usage:: class MyApp(Application): mode = SwitchAttr(["--mode"], Set("TCP", "UDP", case_sensitive = False)) num = SwitchAttr(["--num"], Set("MIN", "MAX", int, csv = True)) :param values: The set of values (strings), or other callable validators, or types, or any other object that can be compared to a string. :param case_sensitive: A keyword argument that indicates whether to use case-sensitive comparison or not. The default is ``False`` :param csv: splits the input as a comma-separated-value before validating and returning a list. Accepts ``True``, ``False``, or a string for the separator """ def __init__(self, *values, **kwargs): self.case_sensitive = kwargs.pop("case_sensitive", False) self.csv = kwargs.pop("csv", False) if self.csv is True: self.csv = "," if kwargs: raise TypeError( _("got unexpected keyword argument(s): {0}").format(kwargs.keys()) ) self.values = values def __repr__(self): return "{{{0}}}".format( ", ".join(v if isinstance(v, str) else v.__name__ for v in self.values) ) def __call__(self, value, check_csv=True): if self.csv and check_csv: return [self(v.strip(), check_csv=False) for v in value.split(",")] if not self.case_sensitive: value = value.lower() for opt in self.values: if isinstance(opt, str): if not self.case_sensitive: opt = opt.lower() if opt == value: return opt # always return original value continue try: return opt(value) except ValueError: pass raise ValueError( "Invalid value: {} (Expected one of {})".format(value, self.values) ) def choices(self, partial=""): choices = { opt if isinstance(opt, str) else "({})".format(opt) for opt in self.values } if partial: choices = {opt for opt in choices if opt.lower().startswith(partial)} return choices CSV = Set(str, csv=True) class Predicate(object): """A wrapper for a single-argument function with pretty printing""" def __init__(self, func): self.func = func def __str__(self): return self.func.__name__ def __call__(self, val): return self.func(val) def choices(self, partial=""): return set() @Predicate def ExistingDirectory(val): """A switch-type validator that ensures that the given argument is an existing directory""" p = local.path(val) if not p.is_dir(): raise ValueError(_("{0} is not a directory").format(val)) return p @Predicate def MakeDirectory(val): p = local.path(val) if p.is_file(): raise ValueError( "{} is a file, should be nonexistent, or a directory".format(val) ) elif not p.exists(): p.mkdir() return p @Predicate def ExistingFile(val): """A switch-type validator that ensures that the given argument is an existing file""" p = local.path(val) if not p.is_file(): raise ValueError(_("{0} is not a file").format(val)) return p @Predicate def NonexistentPath(val): """A switch-type validator that ensures that the given argument is a nonexistent path""" p = local.path(val) if p.exists(): raise ValueError(_("{0} already exists").format(val)) return p plumbum-1.7.2/plumbum/cli/image.py0000644000232200023220000000653414161152302017437 0ustar debalancedebalance# -*- coding: utf-8 -*- from __future__ import division, print_function import sys from plumbum import colors from .. import cli from .termsize import get_terminal_size class Image(object): __slots__ = "size char_ratio".split() def __init__(self, size=None, char_ratio=2.45): self.size = size self.char_ratio = char_ratio def best_aspect(self, orig, term): """Select a best possible size matching the original aspect ratio. Size is width, height. The char_ratio option gives the height of each char with respect to its width, zero for no effect.""" if not self.char_ratio: # Don't use if char ratio is 0 return term orig_ratio = orig[0] / orig[1] / self.char_ratio if int(term[1] / orig_ratio) <= term[0]: new_size = int(term[1] / orig_ratio), term[1] else: new_size = term[0], int(term[0] * orig_ratio) return new_size def show(self, filename, double=False): """Display an image on the command line. Can select a size or show in double resolution.""" import PIL.Image if double: return self.show_pil_double(PIL.Image.open(filename)) else: return self.show_pil(PIL.Image.open(filename)) def _init_size(self, im): """Return the expected image size""" if self.size is None: term_size = get_terminal_size() return self.best_aspect(im.size, term_size) else: return self.size def show_pil(self, im): "Standard show routine" size = self._init_size(im) new_im = im.resize(size).convert("RGB") for y in range(size[1]): for x in range(size[0] - 1): pix = new_im.getpixel((x, y)) print(colors.bg.rgb(*pix), " ", sep="", end="") # u'\u2588' print(colors.reset, " ", sep="") print(colors.reset) def show_pil_double(self, im): "Show double resolution on some fonts" size = self._init_size(im) size = (size[0], size[1] * 2) new_im = im.resize(size).convert("RGB") for y in range(size[1] // 2): for x in range(size[0] - 1): pix = new_im.getpixel((x, y * 2)) pixl = new_im.getpixel((x, y * 2 + 1)) print( colors.bg.rgb(*pixl) & colors.fg.rgb(*pix), u"\u2580", sep="", end="", ) print(colors.reset, " ", sep="") print(colors.reset) class ShowImageApp(cli.Application): "Display an image on the terminal" double = cli.Flag( ["-d", "--double"], help="Double resolution (looks good only with some fonts)" ) @cli.switch(["-c", "--colors"], cli.Range(1, 4), help="Level of color, 1-4") def colors_set(self, n): colors.use_color = n size = cli.SwitchAttr(["-s", "--size"], help="Size, should be in the form 100x150") ratio = cli.SwitchAttr( ["--ratio"], float, default=2.45, help="Aspect ratio of the font" ) @cli.positional(cli.ExistingFile) def main(self, filename): size = None if self.size: size = map(int, self.size.split("x")) Image(size, self.ratio).show(filename, self.double) if __name__ == "__main__": ShowImageApp() plumbum-1.7.2/plumbum/cli/i18n/0000755000232200023220000000000014161152315016556 5ustar debalancedebalanceplumbum-1.7.2/plumbum/cli/i18n/ru.po0000644000232200023220000001763514161152302017554 0ustar debalancedebalance# Russian translations for PACKAGE package # ÐнглийÑкие переводы Ð´Ð»Ñ Ð¿Ð°ÐºÐµÑ‚Ð° PACKAGE. # Copyright (C) 2017 THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # , 2017. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-01-26 00:34-0500\n" "PO-Revision-Date: 2017-08-14 10:21+0200\n" "Last-Translator: \n" "Language-Team: Russian \n" "Language: ru\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" #: plumbum/cli/application.py:71 #, python-brace-format msgid "Subcommand({self.name}, {self.subapplication})" msgstr "Подкоманда({self.name}, {self.subapplication})" #: plumbum/cli/application.py:75 msgid "Switches" msgstr "Опции" #: plumbum/cli/application.py:75 msgid "Meta-switches" msgstr "Мета-опции" #: plumbum/cli/application.py:157 #, python-brace-format msgid "see '{parent} {sub} --help' for more info" msgstr "вызовите '{parent} {sub} --help' Ð´Ð»Ñ Ð±Ð¾Ð»ÐµÐµ полной Ñправки" #: plumbum/cli/application.py:207 #, fuzzy msgid "Sub-command names cannot start with '-'" msgstr "Имена подкомманд не могут начинатьÑÑ Ñ '-'" #: plumbum/cli/application.py:225 #, python-brace-format msgid "Switch {name} already defined and is not overridable" msgstr "ÐžÐ¿Ñ†Ð¸Ñ {name} уже определена и не может быть переопределена" #: plumbum/cli/application.py:331 #, python-brace-format msgid "Ambiguous partial switch {0}" msgstr "" #: plumbum/cli/application.py:336 plumbum/cli/application.py:361 #: plumbum/cli/application.py:377 #, python-brace-format msgid "Unknown switch {0}" msgstr "ÐеизеÑÑ‚Ð½Ð°Ñ Ð¾Ð¿Ñ†Ð¸Ñ {0}" #: plumbum/cli/application.py:341 plumbum/cli/application.py:350 #: plumbum/cli/application.py:369 #, python-brace-format msgid "Switch {0} requires an argument" msgstr "Ð”Ð»Ñ Ð¾Ð¿Ñ†Ð¸Ð¸ {0} необходим аргумент" #: plumbum/cli/application.py:389 #, python-brace-format msgid "Switch {0} already given" msgstr "ÐžÐ¿Ñ†Ð¸Ñ {0} уже была передана" #: plumbum/cli/application.py:392 #, python-brace-format msgid "Switch {0} already given ({1} is equivalent)" msgstr "ÐžÐ¿Ñ†Ð¸Ñ {0} уже была передана (Ñквивалентна {1})" #: plumbum/cli/application.py:442 msgid "" "Argument of {name} expected to be {argtype}, not {val!r}:\n" " {ex!r}" msgstr "" "Ðргумент опции {name} должен быть типа {argtype}, но не {val!r}:\n" " {ex!r}" #: plumbum/cli/application.py:461 #, python-brace-format msgid "Switch {0} is mandatory" msgstr "ÐžÐ¿Ñ†Ð¸Ñ {0} обÑзательна" #: plumbum/cli/application.py:481 #, python-brace-format msgid "Given {0}, the following are missing {1}" msgstr "При передаче {0}, необходимо также указать {1}" #: plumbum/cli/application.py:489 #, python-brace-format msgid "Given {0}, the following are invalid {1}" msgstr "При передаче {0}, Ð½ÐµÐ»ÑŒÐ·Ñ ÑƒÐºÐ°Ð·Ð°Ñ‚ÑŒ {1}" #: plumbum/cli/application.py:500 #, python-brace-format msgid "Expected at least {0} positional argument, got {1}" msgid_plural "Expected at least {0} positional arguments, got {1}" msgstr[0] "ÐžÐ¶Ð¸Ð´Ð°Ñ ÐºÐ°Ðº минимум {0} позиционный аргумент, получено {1}" msgstr[1] "ÐžÐ¶Ð¸Ð´Ð°Ñ ÐºÐ°Ðº минимум {0} позиционных аргумента, получено {1}" msgstr[2] "ÐžÐ¶Ð¸Ð´Ð°Ñ ÐºÐ°Ðº минимум {0} позиционных аргументов, получено {1}" #: plumbum/cli/application.py:508 #, python-brace-format msgid "Expected at most {0} positional argument, got {1}" msgid_plural "Expected at most {0} positional arguments, got {1}" msgstr[0] "ÐžÐ¶Ð¸Ð´Ð°Ñ ÐºÐ°Ðº макÑимум {0} позиционный аргумент, получено {1}" msgstr[1] "ÐžÐ¶Ð¸Ð´Ð°Ñ ÐºÐ°Ðº макÑимум {0} позиционных аргумента, получено {1}" msgstr[2] "ÐžÐ¶Ð¸Ð´Ð°Ñ ÐºÐ°Ðº макÑимум {0} позиционных аргументов, получено {1}" #: plumbum/cli/application.py:599 #, python-brace-format msgid "Error: {0}" msgstr "Ошибка: {0}" #: plumbum/cli/application.py:600 plumbum/cli/application.py:686 #: plumbum/cli/application.py:691 msgid "------" msgstr "-------" #: plumbum/cli/application.py:669 #, python-brace-format msgid "Switch {0} must be a sequence (iterable)" msgstr "ÐžÐ¿Ñ†Ð¸Ñ {0} должна быть поÑледовательноÑтью (перечиÑлением)" #: plumbum/cli/application.py:674 #, python-brace-format msgid "Switch {0} is a boolean flag" msgstr "ÐžÐ¿Ñ†Ð¸Ñ {0} - Ñто булев флаг" #: plumbum/cli/application.py:685 #, python-brace-format msgid "Unknown sub-command '{0}'" msgstr "ÐеизеÑÑ‚Ð½Ð°Ñ Ð¿Ð¾Ð´ÐºÐ¾Ð¼Ð°Ð½Ð´Ð° '{0}'" #: plumbum/cli/application.py:690 msgid "No sub-command given" msgstr "Подкоманда не задана" #: plumbum/cli/application.py:695 msgid "main() not implemented" msgstr "Ð¤ÑƒÐ½ÐºÑ†Ð¸Ñ main() не реализована" #: plumbum/cli/application.py:708 #, fuzzy msgid "Prints help messages of all sub-commands and quits" msgstr "Печатает помощь по вÑем подкомандам и выходит" #: plumbum/cli/application.py:728 msgid "Prints this help message and quits" msgstr "Печатает Ñто Ñообщение и выходит" #: plumbum/cli/application.py:859 msgid "Usage:" msgstr "ИÑпользование:" #: plumbum/cli/application.py:863 #, python-brace-format msgid " {progname} [SWITCHES] [SUBCOMMAND [SWITCHES]] {tailargs}\n" msgstr " {progname} [ОПЦИИ] [ПОДКОМÐÐДР[ОПЦИИ]] {tailargs}\n" #: plumbum/cli/application.py:866 #, python-brace-format msgid " {progname} [SWITCHES] {tailargs}\n" msgstr " {progname} [ОПЦИИ] {tailargs}\n" #: plumbum/cli/application.py:916 msgid "; may be given multiple times" msgstr "; может быть передана неÑколько раз" #: plumbum/cli/application.py:918 msgid "; required" msgstr "; обÑзательнаÑ" #: plumbum/cli/application.py:920 #, python-brace-format msgid "; requires {0}" msgstr "; запрашивает {0}" #: plumbum/cli/application.py:927 #, python-brace-format msgid "; excludes {0}" msgstr "; иÑключает {0}" #: plumbum/cli/application.py:946 #, fuzzy msgid "Sub-commands:" msgstr "Подкоманды:" #: plumbum/cli/application.py:992 msgid "Prints the program's version and quits" msgstr "Печатает верÑию Ñтой программы и выходит" #: plumbum/cli/application.py:997 msgid "(version not set)" msgstr "(верÑÐ¸Ñ Ð½Ðµ задана)" #: plumbum/cli/switches.py:180 plumbum/cli/switches.py:240 msgid "VALUE" msgstr "ЗÐÐЧЕÐИЕ" #: plumbum/cli/switches.py:244 #, python-brace-format msgid "; the default is {0}" msgstr "; по умолчанию - {0}" #: plumbum/cli/switches.py:443 #, python-brace-format msgid "Not in range [{0:d}..{1:d}]" msgstr "Ðе в промежутке [{0:d}..{1:d}]" #: plumbum/cli/switches.py:476 #, python-brace-format msgid "got unexpected keyword argument(s): {0}" msgstr "получен(Ñ‹) неожиданный(е) аргумент(Ñ‹) ключ-значение: {0}" #: plumbum/cli/switches.py:538 #, python-brace-format msgid "{0} is not a directory" msgstr "{0} - Ñто не папка" #: plumbum/cli/switches.py:559 #, python-brace-format msgid "{0} is not a file" msgstr "{0} - Ñто не файл" #: plumbum/cli/switches.py:568 #, python-brace-format msgid "{0} already exists" msgstr "{0} уже была передана" #, python-brace-format #~ msgid "Expected one of {0}" #~ msgstr "ОжидалÑÑ Ð¾Ð´Ð¸Ð½ из {0}" plumbum-1.7.2/plumbum/cli/i18n/de.po0000644000232200023220000001517114161152302017507 0ustar debalancedebalance# German Translations for PACKAGE package. # Deutsche Übersetzung für PACKAGE paket. # Copyright (C) 2017 THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Christoph Hasse , 2017. #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-01-26 00:34-0500\n" "PO-Revision-Date: 2017-11-02 15:04+0200\n" "Last-Translator: Christoph Hasse \n" "Language: de_DE\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: plumbum/cli/application.py:71 #, python-brace-format msgid "Subcommand({self.name}, {self.subapplication})" msgstr "Unterbefehl({self.name}, {self.subapplication})" #: plumbum/cli/application.py:75 msgid "Switches" msgstr "Optionen" #: plumbum/cli/application.py:75 msgid "Meta-switches" msgstr "Meta-optionen" #: plumbum/cli/application.py:157 #, python-brace-format msgid "see '{parent} {sub} --help' for more info" msgstr "siehe '{parent} {sub} --help' für mehr Informationen" #: plumbum/cli/application.py:207 #, fuzzy msgid "Sub-command names cannot start with '-'" msgstr "Unterbefehle können nicht mit '-' beginnen" #: plumbum/cli/application.py:225 #, fuzzy, python-brace-format msgid "Switch {name} already defined and is not overridable" msgstr "Option {name} ist bereits definiert und nicht überschreibbar" #: plumbum/cli/application.py:331 #, python-brace-format msgid "Ambiguous partial switch {0}" msgstr "" #: plumbum/cli/application.py:336 plumbum/cli/application.py:361 #: plumbum/cli/application.py:377 #, python-brace-format msgid "Unknown switch {0}" msgstr "Unbekannte Option {0}" #: plumbum/cli/application.py:341 plumbum/cli/application.py:350 #: plumbum/cli/application.py:369 #, python-brace-format msgid "Switch {0} requires an argument" msgstr "Option {0} benötigt ein Argument" #: plumbum/cli/application.py:389 #, python-brace-format msgid "Switch {0} already given" msgstr "Option {0} bereits gegeben" #: plumbum/cli/application.py:392 #, python-brace-format msgid "Switch {0} already given ({1} is equivalent)" msgstr "Option {0} bereits gegeben({1} ist äquivalent)" #: plumbum/cli/application.py:442 msgid "" "Argument of {name} expected to be {argtype}, not {val!r}:\n" " {ex!r}" msgstr "" "Argument von {name} sollte {argtype} sein, nicht {val|1}:\n" " {ex!r}" #: plumbum/cli/application.py:461 #, python-brace-format msgid "Switch {0} is mandatory" msgstr "Option {0} ist notwendig" #: plumbum/cli/application.py:481 #, python-brace-format msgid "Given {0}, the following are missing {1}" msgstr "Gegeben {0}, werden die folgenden vermisst {1}" #: plumbum/cli/application.py:489 #, python-brace-format msgid "Given {0}, the following are invalid {1}" msgstr "Gegeben {0}, sind die folgenden ungültig {1}" #: plumbum/cli/application.py:500 #, python-brace-format msgid "Expected at least {0} positional argument, got {1}" msgid_plural "Expected at least {0} positional arguments, got {1}" msgstr[0] "Erwarte mindestens {0} positionelles Argument, erhalte {1}" msgstr[1] "Erwarte mindestens {0} positionelle Argumente, erhalte {1}" #: plumbum/cli/application.py:508 #, python-brace-format msgid "Expected at most {0} positional argument, got {1}" msgid_plural "Expected at most {0} positional arguments, got {1}" msgstr[0] "Erwarte höchstens {0} positionelles Argument, erhalte {0}" msgstr[1] "Erwarte höchstens {0} positionelle Argumente, erhalte {0}" #: plumbum/cli/application.py:599 #, python-brace-format msgid "Error: {0}" msgstr "Fehler: {0}" #: plumbum/cli/application.py:600 plumbum/cli/application.py:686 #: plumbum/cli/application.py:691 msgid "------" msgstr "------" #: plumbum/cli/application.py:669 #, python-brace-format msgid "Switch {0} must be a sequence (iterable)" msgstr "Option {0} muss eine Sequenz sein (iterierbar)" #: plumbum/cli/application.py:674 #, python-brace-format msgid "Switch {0} is a boolean flag" msgstr "Option {0} ist ein boolescher Wert" #: plumbum/cli/application.py:685 #, python-brace-format msgid "Unknown sub-command '{0}'" msgstr "Unbekannter Unterbefehl '{0}'" #: plumbum/cli/application.py:690 msgid "No sub-command given" msgstr "Kein Unterbefehl gegeben" #: plumbum/cli/application.py:695 msgid "main() not implemented" msgstr "main() nicht implementiert" #: plumbum/cli/application.py:708 #, fuzzy msgid "Prints help messages of all sub-commands and quits" msgstr "Druckt die Hilfetexte aller Unterbefehle und terminiert" #: plumbum/cli/application.py:728 msgid "Prints this help message and quits" msgstr "Druckt den Hilfetext und terminiert" #: plumbum/cli/application.py:859 msgid "Usage:" msgstr "Gebrauch:" #: plumbum/cli/application.py:863 #, python-brace-format msgid " {progname} [SWITCHES] [SUBCOMMAND [SWITCHES]] {tailargs}\n" msgstr " {progname} [OPTIONEN] [UNTERBEFEHL [OPTIONEN]] {tailargs}\n" #: plumbum/cli/application.py:866 #, python-brace-format msgid " {progname} [SWITCHES] {tailargs}\n" msgstr " {progname} [OPTIONEN] {tailargs}\n" #: plumbum/cli/application.py:916 msgid "; may be given multiple times" msgstr "; kann mehrmals angegeben werden" #: plumbum/cli/application.py:918 msgid "; required" msgstr "; benötigt" #: plumbum/cli/application.py:920 #, python-brace-format msgid "; requires {0}" msgstr "; benötigt {0}" #: plumbum/cli/application.py:927 #, python-brace-format msgid "; excludes {0}" msgstr "; schließt {0} aus" #: plumbum/cli/application.py:946 #, fuzzy msgid "Sub-commands:" msgstr "Unterbefehle:" #: plumbum/cli/application.py:992 msgid "Prints the program's version and quits" msgstr "Druckt die Programmversion und terminiert" #: plumbum/cli/application.py:997 msgid "(version not set)" msgstr "(Version nicht gesetzt)" #: plumbum/cli/switches.py:180 plumbum/cli/switches.py:240 msgid "VALUE" msgstr "WERT" #: plumbum/cli/switches.py:244 #, python-brace-format msgid "; the default is {0}" msgstr "; der Standard ist {0}" #: plumbum/cli/switches.py:443 #, python-brace-format msgid "Not in range [{0:d}..{1:d}]" msgstr "Nicht im Wertebereich [{0:d}..{1:d}]" #: plumbum/cli/switches.py:476 #, python-brace-format msgid "got unexpected keyword argument(s): {0}" msgstr "Unerwartete(s) Argument(e) erhalten: {0}" #: plumbum/cli/switches.py:538 #, python-brace-format msgid "{0} is not a directory" msgstr "{0} ist kein Ordner" #: plumbum/cli/switches.py:559 #, python-brace-format msgid "{0} is not a file" msgstr "{0} ist keine Datei" #: plumbum/cli/switches.py:568 #, python-brace-format msgid "{0} already exists" msgstr "{0} existiert bereits" #, python-brace-format #~ msgid "Expected one of {0}" #~ msgstr "Erwartet einen von {0}" plumbum-1.7.2/plumbum/cli/i18n/de/0000755000232200023220000000000014161152315017146 5ustar debalancedebalanceplumbum-1.7.2/plumbum/cli/i18n/de/LC_MESSAGES/0000755000232200023220000000000014161152315020733 5ustar debalancedebalanceplumbum-1.7.2/plumbum/cli/i18n/de/LC_MESSAGES/plumbum.cli.mo0000644000232200023220000000710214161152302023513 0ustar debalancedebalanceÞ•'T5Œ`=a%ŸÅ×Þí %D: fŠdñ(V( ¨¶Ë&ç".1`,y¦Ã(Û$-GZa'g)¦Ðãú  > %U { “ š ® Ï Û ë D G uS uÉ -? .m œ ª $à )è # /6 f / "± Ô .í ! > G e { … (Š ³ 5Î ."%'$#   & ! {progname} [SWITCHES] [SUBCOMMAND [SWITCHES]] {tailargs} {progname} [SWITCHES] {tailargs} (version not set)------; excludes {0}; may be given multiple times; required; requires {0}; the default is {0}Argument of {name} expected to be {argtype}, not {val!r}: {ex!r}Error: {0}Expected at least {0} positional argument, got {1}Expected at least {0} positional arguments, got {1}Expected at most {0} positional argument, got {1}Expected at most {0} positional arguments, got {1}Given {0}, the following are invalid {1}Given {0}, the following are missing {1}Meta-switchesNo sub-command givenNot in range [{0:d}..{1:d}]Prints the program's version and quitsPrints this help message and quitsSubcommand({self.name}, {self.subapplication})Switch {0} already givenSwitch {0} already given ({1} is equivalent)Switch {0} is a boolean flagSwitch {0} is mandatorySwitch {0} must be a sequence (iterable)Switch {0} requires an argumentSwitchesUnknown sub-command '{0}'Unknown switch {0}Usage:VALUEgot unexpected keyword argument(s): {0}main() not implementedsee '{parent} {sub} --help' for more info{0} already exists{0} is not a directory{0} is not a fileProject-Id-Version: PACKAGE VERSION Report-Msgid-Bugs-To: PO-Revision-Date: 2017-11-02 15:04+0200 Last-Translator: Christoph Hasse Language: de_DE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit {progname} [OPTIONEN] [UNTERBEFEHL [OPTIONEN]] {tailargs} {progname} [OPTIONEN] {tailargs} (Version nicht gesetzt)------; schließt {0} aus; kann mehrmals angegeben werden; benötigt; benötigt {0}; der Standard ist {0}Argument von {name} sollte {argtype} sein, nicht {val|1}: {ex!r}Fehler: {0}Erwarte mindestens {0} positionelles Argument, erhalte {1}Erwarte mindestens {0} positionelle Argumente, erhalte {1}Erwarte höchstens {0} positionelles Argument, erhalte {0}Erwarte höchstens {0} positionelle Argumente, erhalte {0}Gegeben {0}, sind die folgenden ungültig {1}Gegeben {0}, werden die folgenden vermisst {1}Meta-optionenKein Unterbefehl gegebenNicht im Wertebereich [{0:d}..{1:d}]Druckt die Programmversion und terminiertDruckt den Hilfetext und terminiertUnterbefehl({self.name}, {self.subapplication})Option {0} bereits gegebenOption {0} bereits gegeben({1} ist äquivalent)Option {0} ist ein boolescher WertOption {0} ist notwendigOption {0} muss eine Sequenz sein (iterierbar)Option {0} benötigt ein ArgumentOptionenUnbekannter Unterbefehl '{0}'Unbekannte Option {0}Gebrauch:WERTUnerwartete(s) Argument(e) erhalten: {0}main() nicht implementiertsiehe '{parent} {sub} --help' für mehr Informationen{0} existiert bereits{0} ist kein Ordner{0} ist keine Dateiplumbum-1.7.2/plumbum/cli/i18n/nl/0000755000232200023220000000000014161152315017167 5ustar debalancedebalanceplumbum-1.7.2/plumbum/cli/i18n/nl/LC_MESSAGES/0000755000232200023220000000000014161152315020754 5ustar debalancedebalanceplumbum-1.7.2/plumbum/cli/i18n/nl/LC_MESSAGES/plumbum.cli.mo0000644000232200023220000000704014161152302023535 0ustar debalancedebalanceÞ•'T5Œ`=a%ŸÅ×Þí %D: fŠdñ(V( ¨¶Ë&ç".1`,y¦Ã(Û$-GZa'g)¦Ðãúþ : #F j ‚ ‰ #™ ½ Ç Ö Eì 2 u< s² +& 'R z † !Ÿ 2Á %ô / J +b (Ž · 2Î % ' . I ] f 1m Ÿ 0¼ í ü  "%'$#   & ! {progname} [SWITCHES] [SUBCOMMAND [SWITCHES]] {tailargs} {progname} [SWITCHES] {tailargs} (version not set)------; excludes {0}; may be given multiple times; required; requires {0}; the default is {0}Argument of {name} expected to be {argtype}, not {val!r}: {ex!r}Error: {0}Expected at least {0} positional argument, got {1}Expected at least {0} positional arguments, got {1}Expected at most {0} positional argument, got {1}Expected at most {0} positional arguments, got {1}Given {0}, the following are invalid {1}Given {0}, the following are missing {1}Meta-switchesNo sub-command givenNot in range [{0:d}..{1:d}]Prints the program's version and quitsPrints this help message and quitsSubcommand({self.name}, {self.subapplication})Switch {0} already givenSwitch {0} already given ({1} is equivalent)Switch {0} is a boolean flagSwitch {0} is mandatorySwitch {0} must be a sequence (iterable)Switch {0} requires an argumentSwitchesUnknown sub-command '{0}'Unknown switch {0}Usage:VALUEgot unexpected keyword argument(s): {0}main() not implementedsee '{parent} {sub} --help' for more info{0} already exists{0} is not a directory{0} is not a fileProject-Id-Version: PACKAGE VERSION Report-Msgid-Bugs-To: PO-Revision-Date: 2017-10-14 15:04+0200 Last-Translator: Roel Aaij Language: nl_NL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit {progname} [OPTIES] [SUBCOMMANDO [OPTIES]] {tailargs} {progname} [OPTIES] {tailargs} (versie niet opgegeven)------; sluit {0} uit; kan meerdere keren gegeven worden; vereist; verseist {0}; de standaard is {0}Argement van {name} hoort {argtype} te zijn, niet {val|1}: {ex!r}Fout: {0}Verwachtte ten minste {0} positioneel argument, kreeg {1}Verwachtte ten minste {0} positionele argumenten, kreeg {1}Verwachtte hoogstens {0} positioneel argument, kreeg {0}Verwachtte hoogstens {0} positionele argumenten, kreeg {0}Gegeven {0}, zijn de volgenden ongeldig {1}Gegeven {0}, ontbreken de volgenden {1}Meta-optiesGeen subcommando gegevenNiet binnen bereik [{0:d}..{1:d}]Drukt de versie van het programma af en beëindigtDrukt dit hulpbericht af en beëindigSubopdracht({self.name}, {self.subapplication})Optie {0} is al gegevenOptie {0} is al gegeven ({1} is equivalent)Optie {0} geeft een waarheidswaarde weerOptie {0} is verplichtOptie {0} moet een reeks zijn (itereerbaar object)Een argument is vereist bij optie {0}OptiesOnbekend subcommando '{0}'Onbekende optie {0}Gebruik:WAARDEOnverwacht(e) trefwoord argument(en) gegeven: {0}main() niet geïmplementeerdzie '{parent} {sub} --help' voor meer informatie{0} bestaat al{0} is geen map{0} is geen bestandplumbum-1.7.2/plumbum/cli/i18n/fr/0000755000232200023220000000000014161152315017165 5ustar debalancedebalanceplumbum-1.7.2/plumbum/cli/i18n/fr/LC_MESSAGES/0000755000232200023220000000000014161152315020752 5ustar debalancedebalanceplumbum-1.7.2/plumbum/cli/i18n/fr/LC_MESSAGES/plumbum.cli.mo0000644000232200023220000000703614161152302023540 0ustar debalancedebalanceÞ•'T5Œ`=a%ŸÅ×Þí %D: fŠdñ(V( ¨¶Ë&ç".1`,y¦Ã(Û$-GZa'g)¦Ðãú > $N s Š ‘ "ž Á Ï à D E bQ `´ . .D s € " 'À !è 1 < /V † ¡ /¸ !è   / C P +W ƒ 4¡ Ö è "%'$#   & ! {progname} [SWITCHES] [SUBCOMMAND [SWITCHES]] {tailargs} {progname} [SWITCHES] {tailargs} (version not set)------; excludes {0}; may be given multiple times; required; requires {0}; the default is {0}Argument of {name} expected to be {argtype}, not {val!r}: {ex!r}Error: {0}Expected at least {0} positional argument, got {1}Expected at least {0} positional arguments, got {1}Expected at most {0} positional argument, got {1}Expected at most {0} positional arguments, got {1}Given {0}, the following are invalid {1}Given {0}, the following are missing {1}Meta-switchesNo sub-command givenNot in range [{0:d}..{1:d}]Prints the program's version and quitsPrints this help message and quitsSubcommand({self.name}, {self.subapplication})Switch {0} already givenSwitch {0} already given ({1} is equivalent)Switch {0} is a boolean flagSwitch {0} is mandatorySwitch {0} must be a sequence (iterable)Switch {0} requires an argumentSwitchesUnknown sub-command '{0}'Unknown switch {0}Usage:VALUEgot unexpected keyword argument(s): {0}main() not implementedsee '{parent} {sub} --help' for more info{0} already exists{0} is not a directory{0} is not a fileProject-Id-Version: PACKAGE VERSION Report-Msgid-Bugs-To: PO-Revision-Date: 2017-10-14 15:04+0200 Last-Translator: Joel Closier Language: fr_FR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit {progname} [OPTIONS] [SOUS_COMMANDE [OPTIONS]] {tailargs} {progname} [OPTIONS] {tailargs} (version non définie)------; exclut {0}; peut être fourni plusieurs fois; nécessaire; nécessite {0}; la valeur par défaut est {0}Argument de {name} doit être {argtype} , et non {val|1}: {ex!r}Erreur: {0}Au moins {0} argument de position attendu, reçu {0}Au moins {0} arguments de position, reçu {0}Au plus {0} argument de position attendu, reçu {0}Au plus {0} arguments de position, reçu {0}Etant donné {0}, ce qui suit est invalide {1}Etant donné {0}, ce qui suit est manquant {1}Meta-optionsPas de sous-commande donnéePas dans la chaîne [{0:d}..{1:d}]Imprime la version du programme et sortImprime ce message d'aide et sortSous-commande({self.name}, {self.subapplication})Option {0} déjà donnéeOption {0} déjà donnée ({1} est équivalent)Option {0} est un booléenOption {0} obligatoireOption {0} doit être une séquence (itérable)Option {0} nécessite un argumentOptionsSous-commande inconnue '{0}'Option inconnue {0}Utilisation:VALEURmot-clé inconnu donné comme argument: {0}main() n'est pas implémentévoir '{parent} {sub} --help' pour plus d'information{0} existe déjà{0} n'est pas un répertoire{0} n'est pas un fichierplumbum-1.7.2/plumbum/cli/i18n/ru/0000755000232200023220000000000014161152315017204 5ustar debalancedebalanceplumbum-1.7.2/plumbum/cli/i18n/ru/LC_MESSAGES/0000755000232200023220000000000014161152315020771 5ustar debalancedebalanceplumbum-1.7.2/plumbum/cli/i18n/ru/LC_MESSAGES/plumbum.cli.mo0000644000232200023220000001157614161152302023563 0ustar debalancedebalanceÞ•(\5œp=q%¯Õçîý &5DJ fšd(f( ¸ÆÛ&÷".Ap,‰¶Ó(ë44irŒŸ¦'¬Ô)ë(?‹QKÝ ') Q r z @“ Ô ï  j, — 4© :Þ <OV¦&º+áK <Y8–/ÏNÿ+N%zh 9 fC ª/µ#å %a61˜VÊ$!Fc#&%$   (' ! " {progname} [SWITCHES] [SUBCOMMAND [SWITCHES]] {tailargs} {progname} [SWITCHES] {tailargs} (version not set)------; excludes {0}; may be given multiple times; required; requires {0}; the default is {0}Argument of {name} expected to be {argtype}, not {val!r}: {ex!r}Error: {0}Expected at least {0} positional argument, got {1}Expected at least {0} positional arguments, got {1}Expected at most {0} positional argument, got {1}Expected at most {0} positional arguments, got {1}Given {0}, the following are invalid {1}Given {0}, the following are missing {1}Meta-switchesNo sub-command givenNot in range [{0:d}..{1:d}]Prints the program's version and quitsPrints this help message and quitsSubcommand({self.name}, {self.subapplication})Switch {0} already givenSwitch {0} already given ({1} is equivalent)Switch {0} is a boolean flagSwitch {0} is mandatorySwitch {0} must be a sequence (iterable)Switch {0} requires an argumentSwitch {name} already defined and is not overridableSwitchesUnknown sub-command '{0}'Unknown switch {0}Usage:VALUEgot unexpected keyword argument(s): {0}main() not implementedsee '{parent} {sub} --help' for more info{0} already exists{0} is not a directory{0} is not a fileProject-Id-Version: PACKAGE VERSION Report-Msgid-Bugs-To: PO-Revision-Date: 2017-08-14 10:21+0200 Last-Translator: Language-Team: Russian Language: ru MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2); {progname} [ОПЦИИ] [ПОДКОМÐÐДР[ОПЦИИ]] {tailargs} {progname} [ОПЦИИ] {tailargs} (верÑÐ¸Ñ Ð½Ðµ задана)-------; иÑключает {0}; может быть передана неÑколько раз; обÑзательнаÑ; запрашивает {0}; по умолчанию - {0}Ðргумент опции {name} должен быть типа {argtype}, но не {val!r}: {ex!r}Ошибка: {0}ÐžÐ¶Ð¸Ð´Ð°Ñ ÐºÐ°Ðº минимум {0} позиционный аргумент, получено {1}ÐžÐ¶Ð¸Ð´Ð°Ñ ÐºÐ°Ðº минимум {0} позиционных аргумента, получено {1}ÐžÐ¶Ð¸Ð´Ð°Ñ ÐºÐ°Ðº минимум {0} позиционных аргументов, получено {1}ÐžÐ¶Ð¸Ð´Ð°Ñ ÐºÐ°Ðº макÑимум {0} позиционный аргумент, получено {1}ÐžÐ¶Ð¸Ð´Ð°Ñ ÐºÐ°Ðº макÑимум {0} позиционных аргумента, получено {1}ÐžÐ¶Ð¸Ð´Ð°Ñ ÐºÐ°Ðº макÑимум {0} позиционных аргументов, получено {1}При передаче {0}, Ð½ÐµÐ»ÑŒÐ·Ñ ÑƒÐºÐ°Ð·Ð°Ñ‚ÑŒ {1}При передаче {0}, необходимо также указать {1}Мета-опцииПодкоманда не заданаÐе в промежутке [{0:d}..{1:d}]Печатает верÑию Ñтой программы и выходитПечатает Ñто Ñообщение и выходитПодкоманда({self.name}, {self.subapplication})ÐžÐ¿Ñ†Ð¸Ñ {0} уже была Ð¿ÐµÑ€ÐµÐ´Ð°Ð½Ð°ÐžÐ¿Ñ†Ð¸Ñ {0} уже была передана (Ñквивалентна {1})ÐžÐ¿Ñ†Ð¸Ñ {0} - Ñто булев Ñ„Ð»Ð°Ð³ÐžÐ¿Ñ†Ð¸Ñ {0} обÑÐ·Ð°Ñ‚ÐµÐ»ÑŒÐ½Ð°ÐžÐ¿Ñ†Ð¸Ñ {0} должна быть поÑледовательноÑтью (перечиÑлением)Ð”Ð»Ñ Ð¾Ð¿Ñ†Ð¸Ð¸ {0} необходим Ð°Ñ€Ð³ÑƒÐ¼ÐµÐ½Ñ‚ÐžÐ¿Ñ†Ð¸Ñ {name} уже определена и не может быть переопределенаОпцииÐеизеÑÑ‚Ð½Ð°Ñ Ð¿Ð¾Ð´ÐºÐ¾Ð¼Ð°Ð½Ð´Ð° '{0}'ÐеизеÑÑ‚Ð½Ð°Ñ Ð¾Ð¿Ñ†Ð¸Ñ {0}ИÑпользование:ЗÐÐЧЕÐИЕполучен(Ñ‹) неожиданный(е) аргумент(Ñ‹) ключ-значение: {0}Ð¤ÑƒÐ½ÐºÑ†Ð¸Ñ main() не реализованавызовите '{parent} {sub} --help' Ð´Ð»Ñ Ð±Ð¾Ð»ÐµÐµ полной Ñправки{0} уже была передана{0} - Ñто не папка{0} - Ñто не файлplumbum-1.7.2/plumbum/cli/i18n/nl.po0000644000232200023220000001513614161152302017531 0ustar debalancedebalance# Dutch Translations for PACKAGE package. # Nederlandse vertaling voor het PACKAGE pakket. # Copyright (C) 2017 THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Roel Aaij , 2017. #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-01-26 00:34-0500\n" "PO-Revision-Date: 2017-10-14 15:04+0200\n" "Last-Translator: Roel Aaij \n" "Language: nl_NL\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: plumbum/cli/application.py:71 #, python-brace-format msgid "Subcommand({self.name}, {self.subapplication})" msgstr "Subopdracht({self.name}, {self.subapplication})" #: plumbum/cli/application.py:75 msgid "Switches" msgstr "Opties" #: plumbum/cli/application.py:75 msgid "Meta-switches" msgstr "Meta-opties" #: plumbum/cli/application.py:157 #, python-brace-format msgid "see '{parent} {sub} --help' for more info" msgstr "zie '{parent} {sub} --help' voor meer informatie" #: plumbum/cli/application.py:207 #, fuzzy msgid "Sub-command names cannot start with '-'" msgstr "Namen van subopdrachten mogen niet met '-' beginnen" #: plumbum/cli/application.py:225 #, fuzzy, python-brace-format msgid "Switch {name} already defined and is not overridable" msgstr "Optie {name} is al gedefiniëerd en kan niet worden overschreven" #: plumbum/cli/application.py:331 #, python-brace-format msgid "Ambiguous partial switch {0}" msgstr "" #: plumbum/cli/application.py:336 plumbum/cli/application.py:361 #: plumbum/cli/application.py:377 #, python-brace-format msgid "Unknown switch {0}" msgstr "Onbekende optie {0}" #: plumbum/cli/application.py:341 plumbum/cli/application.py:350 #: plumbum/cli/application.py:369 #, python-brace-format msgid "Switch {0} requires an argument" msgstr "Een argument is vereist bij optie {0}" #: plumbum/cli/application.py:389 #, python-brace-format msgid "Switch {0} already given" msgstr "Optie {0} is al gegeven" #: plumbum/cli/application.py:392 #, python-brace-format msgid "Switch {0} already given ({1} is equivalent)" msgstr "Optie {0} is al gegeven ({1} is equivalent)" #: plumbum/cli/application.py:442 msgid "" "Argument of {name} expected to be {argtype}, not {val!r}:\n" " {ex!r}" msgstr "" "Argement van {name} hoort {argtype} te zijn, niet {val|1}:\n" " {ex!r}" #: plumbum/cli/application.py:461 #, python-brace-format msgid "Switch {0} is mandatory" msgstr "Optie {0} is verplicht" #: plumbum/cli/application.py:481 #, python-brace-format msgid "Given {0}, the following are missing {1}" msgstr "Gegeven {0}, ontbreken de volgenden {1}" #: plumbum/cli/application.py:489 #, python-brace-format msgid "Given {0}, the following are invalid {1}" msgstr "Gegeven {0}, zijn de volgenden ongeldig {1}" #: plumbum/cli/application.py:500 #, python-brace-format msgid "Expected at least {0} positional argument, got {1}" msgid_plural "Expected at least {0} positional arguments, got {1}" msgstr[0] "Verwachtte ten minste {0} positioneel argument, kreeg {1}" msgstr[1] "Verwachtte ten minste {0} positionele argumenten, kreeg {1}" #: plumbum/cli/application.py:508 #, python-brace-format msgid "Expected at most {0} positional argument, got {1}" msgid_plural "Expected at most {0} positional arguments, got {1}" msgstr[0] "Verwachtte hoogstens {0} positioneel argument, kreeg {0}" msgstr[1] "Verwachtte hoogstens {0} positionele argumenten, kreeg {0}" #: plumbum/cli/application.py:599 #, python-brace-format msgid "Error: {0}" msgstr "Fout: {0}" #: plumbum/cli/application.py:600 plumbum/cli/application.py:686 #: plumbum/cli/application.py:691 msgid "------" msgstr "------" #: plumbum/cli/application.py:669 #, python-brace-format msgid "Switch {0} must be a sequence (iterable)" msgstr "Optie {0} moet een reeks zijn (itereerbaar object)" #: plumbum/cli/application.py:674 #, python-brace-format msgid "Switch {0} is a boolean flag" msgstr "Optie {0} geeft een waarheidswaarde weer" #: plumbum/cli/application.py:685 #, python-brace-format msgid "Unknown sub-command '{0}'" msgstr "Onbekend subcommando '{0}'" #: plumbum/cli/application.py:690 msgid "No sub-command given" msgstr "Geen subcommando gegeven" #: plumbum/cli/application.py:695 msgid "main() not implemented" msgstr "main() niet geïmplementeerd" #: plumbum/cli/application.py:708 #, fuzzy msgid "Prints help messages of all sub-commands and quits" msgstr "Druk hulpberichten van alle subcommando's af en beëindig" #: plumbum/cli/application.py:728 msgid "Prints this help message and quits" msgstr "Drukt dit hulpbericht af en beëindig" #: plumbum/cli/application.py:859 msgid "Usage:" msgstr "Gebruik:" #: plumbum/cli/application.py:863 #, python-brace-format msgid " {progname} [SWITCHES] [SUBCOMMAND [SWITCHES]] {tailargs}\n" msgstr " {progname} [OPTIES] [SUBCOMMANDO [OPTIES]] {tailargs}\n" #: plumbum/cli/application.py:866 #, python-brace-format msgid " {progname} [SWITCHES] {tailargs}\n" msgstr " {progname} [OPTIES] {tailargs}\n" #: plumbum/cli/application.py:916 msgid "; may be given multiple times" msgstr "; kan meerdere keren gegeven worden" #: plumbum/cli/application.py:918 msgid "; required" msgstr "; vereist" #: plumbum/cli/application.py:920 #, python-brace-format msgid "; requires {0}" msgstr "; verseist {0}" #: plumbum/cli/application.py:927 #, python-brace-format msgid "; excludes {0}" msgstr "; sluit {0} uit" #: plumbum/cli/application.py:946 #, fuzzy msgid "Sub-commands:" msgstr "Subcommando's" #: plumbum/cli/application.py:992 msgid "Prints the program's version and quits" msgstr "Drukt de versie van het programma af en beëindigt" #: plumbum/cli/application.py:997 msgid "(version not set)" msgstr "(versie niet opgegeven)" #: plumbum/cli/switches.py:180 plumbum/cli/switches.py:240 msgid "VALUE" msgstr "WAARDE" #: plumbum/cli/switches.py:244 #, python-brace-format msgid "; the default is {0}" msgstr "; de standaard is {0}" #: plumbum/cli/switches.py:443 #, python-brace-format msgid "Not in range [{0:d}..{1:d}]" msgstr "Niet binnen bereik [{0:d}..{1:d}]" #: plumbum/cli/switches.py:476 #, python-brace-format msgid "got unexpected keyword argument(s): {0}" msgstr "Onverwacht(e) trefwoord argument(en) gegeven: {0}" #: plumbum/cli/switches.py:538 #, python-brace-format msgid "{0} is not a directory" msgstr "{0} is geen map" #: plumbum/cli/switches.py:559 #, python-brace-format msgid "{0} is not a file" msgstr "{0} is geen bestand" #: plumbum/cli/switches.py:568 #, python-brace-format msgid "{0} already exists" msgstr "{0} bestaat al" #, python-brace-format #~ msgid "Expected one of {0}" #~ msgstr "Verwachtte één van {0}" plumbum-1.7.2/plumbum/cli/i18n/fr.po0000644000232200023220000001514514161152302017527 0ustar debalancedebalance# French Translations for PACKAGE package. # Traduction francaise du paquet PACKAGE. # Copyright (C) 2017 THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Joel Closier , 2017. #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-01-26 00:34-0500\n" "PO-Revision-Date: 2017-10-14 15:04+0200\n" "Last-Translator: Joel Closier \n" "Language: fr_FR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: plumbum/cli/application.py:71 #, python-brace-format msgid "Subcommand({self.name}, {self.subapplication})" msgstr "Sous-commande({self.name}, {self.subapplication})" #: plumbum/cli/application.py:75 msgid "Switches" msgstr "Options" #: plumbum/cli/application.py:75 msgid "Meta-switches" msgstr "Meta-options" #: plumbum/cli/application.py:157 #, python-brace-format msgid "see '{parent} {sub} --help' for more info" msgstr "voir '{parent} {sub} --help' pour plus d'information" #: plumbum/cli/application.py:207 #, fuzzy msgid "Sub-command names cannot start with '-'" msgstr "le nom des Sous-commandes ne peut pas commencer avec '-' " #: plumbum/cli/application.py:225 #, fuzzy, python-brace-format msgid "Switch {name} already defined and is not overridable" msgstr "Option {name} est déjà définie et ne peut pas être sur-écrite" #: plumbum/cli/application.py:331 #, python-brace-format msgid "Ambiguous partial switch {0}" msgstr "" #: plumbum/cli/application.py:336 plumbum/cli/application.py:361 #: plumbum/cli/application.py:377 #, python-brace-format msgid "Unknown switch {0}" msgstr "Option inconnue {0}" #: plumbum/cli/application.py:341 plumbum/cli/application.py:350 #: plumbum/cli/application.py:369 #, python-brace-format msgid "Switch {0} requires an argument" msgstr "Option {0} nécessite un argument" #: plumbum/cli/application.py:389 #, python-brace-format msgid "Switch {0} already given" msgstr "Option {0} déjà donnée" #: plumbum/cli/application.py:392 #, python-brace-format msgid "Switch {0} already given ({1} is equivalent)" msgstr "Option {0} déjà donnée ({1} est équivalent)" #: plumbum/cli/application.py:442 msgid "" "Argument of {name} expected to be {argtype}, not {val!r}:\n" " {ex!r}" msgstr "" "Argument de {name} doit être {argtype} , et non {val|1}:\n" " {ex!r}" #: plumbum/cli/application.py:461 #, python-brace-format msgid "Switch {0} is mandatory" msgstr "Option {0} obligatoire" #: plumbum/cli/application.py:481 #, python-brace-format msgid "Given {0}, the following are missing {1}" msgstr "Etant donné {0}, ce qui suit est manquant {1}" #: plumbum/cli/application.py:489 #, python-brace-format msgid "Given {0}, the following are invalid {1}" msgstr "Etant donné {0}, ce qui suit est invalide {1}" #: plumbum/cli/application.py:500 #, python-brace-format msgid "Expected at least {0} positional argument, got {1}" msgid_plural "Expected at least {0} positional arguments, got {1}" msgstr[0] "Au moins {0} argument de position attendu, reçu {0}" msgstr[1] "Au moins {0} arguments de position, reçu {0}" #: plumbum/cli/application.py:508 #, python-brace-format msgid "Expected at most {0} positional argument, got {1}" msgid_plural "Expected at most {0} positional arguments, got {1}" msgstr[0] "Au plus {0} argument de position attendu, reçu {0}" msgstr[1] "Au plus {0} arguments de position, reçu {0}" #: plumbum/cli/application.py:599 #, python-brace-format msgid "Error: {0}" msgstr "Erreur: {0}" #: plumbum/cli/application.py:600 plumbum/cli/application.py:686 #: plumbum/cli/application.py:691 msgid "------" msgstr "------" #: plumbum/cli/application.py:669 #, python-brace-format msgid "Switch {0} must be a sequence (iterable)" msgstr "Option {0} doit être une séquence (itérable)" #: plumbum/cli/application.py:674 #, python-brace-format msgid "Switch {0} is a boolean flag" msgstr "Option {0} est un booléen" #: plumbum/cli/application.py:685 #, python-brace-format msgid "Unknown sub-command '{0}'" msgstr "Sous-commande inconnue '{0}'" #: plumbum/cli/application.py:690 msgid "No sub-command given" msgstr "Pas de sous-commande donnée" #: plumbum/cli/application.py:695 msgid "main() not implemented" msgstr "main() n'est pas implémenté" #: plumbum/cli/application.py:708 #, fuzzy msgid "Prints help messages of all sub-commands and quits" msgstr "Imprime les messages d'aide de toutes les sous-commandes et sort" #: plumbum/cli/application.py:728 msgid "Prints this help message and quits" msgstr "Imprime ce message d'aide et sort" #: plumbum/cli/application.py:859 msgid "Usage:" msgstr "Utilisation:" #: plumbum/cli/application.py:863 #, python-brace-format msgid " {progname} [SWITCHES] [SUBCOMMAND [SWITCHES]] {tailargs}\n" msgstr " {progname} [OPTIONS] [SOUS_COMMANDE [OPTIONS]] {tailargs}\n" #: plumbum/cli/application.py:866 #, python-brace-format msgid " {progname} [SWITCHES] {tailargs}\n" msgstr " {progname} [OPTIONS] {tailargs}\n" #: plumbum/cli/application.py:916 msgid "; may be given multiple times" msgstr "; peut être fourni plusieurs fois" #: plumbum/cli/application.py:918 msgid "; required" msgstr "; nécessaire" #: plumbum/cli/application.py:920 #, python-brace-format msgid "; requires {0}" msgstr "; nécessite {0}" #: plumbum/cli/application.py:927 #, python-brace-format msgid "; excludes {0}" msgstr "; exclut {0}" #: plumbum/cli/application.py:946 #, fuzzy msgid "Sub-commands:" msgstr "Sous-Commandes:" #: plumbum/cli/application.py:992 msgid "Prints the program's version and quits" msgstr "Imprime la version du programme et sort" #: plumbum/cli/application.py:997 msgid "(version not set)" msgstr "(version non définie)" #: plumbum/cli/switches.py:180 plumbum/cli/switches.py:240 msgid "VALUE" msgstr "VALEUR" #: plumbum/cli/switches.py:244 #, python-brace-format msgid "; the default is {0}" msgstr "; la valeur par défaut est {0}" #: plumbum/cli/switches.py:443 #, python-brace-format msgid "Not in range [{0:d}..{1:d}]" msgstr "Pas dans la chaîne [{0:d}..{1:d}]" #: plumbum/cli/switches.py:476 #, python-brace-format msgid "got unexpected keyword argument(s): {0}" msgstr "mot-clé inconnu donné comme argument: {0}" #: plumbum/cli/switches.py:538 #, python-brace-format msgid "{0} is not a directory" msgstr "{0} n'est pas un répertoire" #: plumbum/cli/switches.py:559 #, python-brace-format msgid "{0} is not a file" msgstr "{0} n'est pas un fichier" #: plumbum/cli/switches.py:568 #, python-brace-format msgid "{0} already exists" msgstr "{0} existe déjà" #, python-brace-format #~ msgid "Expected one of {0}" #~ msgstr "un des {0} attendu" plumbum-1.7.2/plumbum/cli/config.py0000644000232200023220000000703414161152302017616 0ustar debalancedebalance# -*- coding: utf-8 -*- from __future__ import division, print_function import sys from abc import abstractmethod from plumbum import local from plumbum.lib import _setdoc, six if sys.version_info >= (3,): from configparser import ConfigParser, NoOptionError, NoSectionError else: from ConfigParser import ConfigParser, NoOptionError, NoSectionError class ConfigBase(six.ABC): """Base class for Config parsers. :param filename: The file to use The ``with`` statement can be used to automatically try to read on entering and write if changed on exiting. Otherwise, use ``.read`` and ``.write`` as needed. Set and get the options using ``[]`` syntax. Usage: with Config("~/.myprog_rc") as conf: value = conf.get("option", "default") value2 = conf["option"] # shortcut for default=None """ __slots__ = "filename changed".split() def __init__(self, filename): self.filename = local.path(filename) self.changed = False def __enter__(self): try: self.read() except FileNotFoundError: pass return self def __exit__(self, exc_type, exc_val, exc_tb): if self.changed: self.write() @abstractmethod def read(self): """Read in the linked file""" pass @abstractmethod def write(self): """Write out the linked file""" self.changed = False @abstractmethod def _get(self, option): """Internal get function for subclasses""" pass @abstractmethod def _set(self, option, value): """Internal set function for subclasses. Must return the value that was set.""" pass def get(self, option, default=None): "Get an item from the store, returns default if fails" try: return self._get(option) except KeyError: self.changed = True return self._set(option, default) def set(self, option, value): """Set an item, mark this object as changed""" self.changed = True self._set(option, value) def __getitem__(self, option): return self._get(option) def __setitem__(self, option, value): return self.set(option, value) class ConfigINI(ConfigBase): DEFAULT_SECTION = "DEFAULT" slots = "parser".split() def __init__(self, filename): super(ConfigINI, self).__init__(filename) self.parser = ConfigParser() @_setdoc(ConfigBase) def read(self): self.parser.read(self.filename) super(ConfigINI, self).read() @_setdoc(ConfigBase) def write(self): with open(self.filename, "w") as f: self.parser.write(f) super(ConfigINI, self).write() @classmethod def _sec_opt(cls, option): if "." not in option: sec = cls.DEFAULT_SECTION else: sec, option = option.split(".", 1) return sec, option @_setdoc(ConfigBase) def _get(self, option): sec, option = self._sec_opt(option) try: return self.parser.get(sec, option) except (NoSectionError, NoOptionError): raise KeyError("{sec}:{option}".format(sec=sec, option=option)) @_setdoc(ConfigBase) def _set(self, option, value): sec, option = self._sec_opt(option) try: self.parser.set(sec, option, str(value)) except NoSectionError: self.parser.add_section(sec) self.parser.set(sec, option, str(value)) return str(value) Config = ConfigINI plumbum-1.7.2/plumbum/cli/i18n.py0000644000232200023220000000313114161152302017122 0ustar debalancedebalance# -*- coding: utf-8 -*- import locale # High performance method for English (no translation needed) loc = locale.getlocale()[0] if loc is None or loc.startswith("en"): class NullTranslation(object): def gettext(self, str): return str def ngettext(self, str1, strN, n): if n == 1: return str1.replace("{0}", str(n)) else: return strN.replace("{0}", str(n)) def get_translation_for(package_name): return NullTranslation() else: import gettext import os # If not installed with setuptools, this might not be available try: import pkg_resources except ImportError: pkg_resources = None try: from typing import Callable, List, Tuple except ImportError: pass local_dir = os.path.basename(__file__) def get_translation_for(package_name): # type: (str) -> gettext.NullTranslations """Find and return gettext translation for package (Try to find folder manually if setuptools does not exist) """ if "." in package_name: package_name = ".".join(package_name.split(".")[:-1]) localedir = None if pkg_resources is None: mydir = os.path.join(local_dir, "i18n") else: mydir = pkg_resources.resource_filename(package_name, "i18n") for localedir in mydir, None: localefile = gettext.find(package_name, localedir) if localefile: break return gettext.translation(package_name, localedir=localedir, fallback=True) plumbum-1.7.2/plumbum/typed_env.py0000644000232200023220000001152014161152302017572 0ustar debalancedebalance# -*- coding: utf-8 -*- import inspect import os try: from collections.abc import MutableMapping except ImportError: from collections import MutableMapping NO_DEFAULT = object() # must not inherit from AttributeError, so not to mess with python's attribute-lookup flow class EnvironmentVariableError(KeyError): pass class TypedEnv(MutableMapping): """ This object can be used in 'exploratory' mode: nv = TypedEnv() print(nv.HOME) It can also be used as a parser and validator of environment variables: class MyEnv(TypedEnv): username = TypedEnv.Str("USER", default='anonymous') path = TypedEnv.CSV("PATH", separator=":") tmp = TypedEnv.Str("TMP TEMP".split()) # support 'fallback' var-names nv = MyEnv() print(nv.username) for p in nv.path: print(p) try: print(p.tmp) except EnvironmentVariableError: print("TMP/TEMP is not defined") else: assert False """ __slots__ = ["_env", "_defined_keys"] class _BaseVar(object): def __init__(self, name, default=NO_DEFAULT): self.names = tuple(name) if isinstance(name, (tuple, list)) else (name,) self.name = self.names[0] self.default = default def convert(self, value): return value def __get__(self, instance, owner): if not instance: return self try: return self.convert(instance._raw_get(*self.names)) except EnvironmentVariableError: if self.default is NO_DEFAULT: raise return self.default def __set__(self, instance, value): instance[self.name] = value class Str(_BaseVar): pass class Bool(_BaseVar): """ Converts 'yes|true|1|no|false|0' to the appropriate boolean value. Case-insensitive. Throws a ``ValueError`` for any other value. """ def convert(self, s): s = s.lower() if s not in ("yes", "no", "true", "false", "1", "0"): raise ValueError("Unrecognized boolean value: {!r}".format(s)) return s in ("yes", "true", "1") def __set__(self, instance, value): instance[self.name] = "yes" if value else "no" class Int(_BaseVar): convert = staticmethod(int) class Float(_BaseVar): convert = staticmethod(float) class CSV(_BaseVar): """ Comma-separated-strings get split using the ``separator`` (',' by default) into a list of objects of type ``type`` (``str`` by default). """ def __init__(self, name, default=NO_DEFAULT, type=str, separator=","): super(TypedEnv.CSV, self).__init__(name, default=default) self.type = type self.separator = separator def __set__(self, instance, value): instance[self.name] = self.separator.join(map(str, value)) def convert(self, value): return [self.type(v.strip()) for v in value.split(self.separator)] # ========= def __init__(self, env=os.environ): self._env = env self._defined_keys = { k for (k, v) in inspect.getmembers(self.__class__) if isinstance(v, self._BaseVar) } def __iter__(self): return iter(dir(self)) def __len__(self): return len(self._env) def __delitem__(self, name): del self._env[name] def __setitem__(self, name, value): self._env[name] = str(value) def _raw_get(self, *key_names): for key in key_names: value = self._env.get(key, NO_DEFAULT) if value is not NO_DEFAULT: return value else: raise EnvironmentVariableError(key_names[0]) def __contains__(self, key): try: self._raw_get(key) except EnvironmentVariableError: return False else: return True def __getattr__(self, name): # if we're here then there was no descriptor defined try: return self._raw_get(name) except EnvironmentVariableError: raise AttributeError( "{} has no attribute {!r}".format(self.__class__, name) ) def __getitem__(self, key): return getattr(self, key) # delegate through the descriptors def get(self, key, default=None): try: return self[key] except EnvironmentVariableError: return default def __dir__(self): if self._defined_keys: # return only defined return sorted(self._defined_keys) # return whatever is in the environemnt (for convenience) members = set(self._env.keys()) members.update(dir(self.__class__)) return sorted(members) plumbum-1.7.2/plumbum/colors.py0000644000232200023220000000124014161152302017074 0ustar debalancedebalance# -*- coding: utf-8 -*- """ This module imitates a real module, providing standard syntax like from `plumbum.colors` and from `plumbum.colors.bg` to work alongside all the standard syntax for colors. """ from __future__ import print_function import atexit import os import sys from plumbum.colorlib import ansicolors, main _reset = ansicolors.reset.now if __name__ == "__main__": main() else: # Don't register an exit if this is called using -m! atexit.register(_reset) # Oddly, the order here matters for Python2, but not Python3 sys.modules[__name__ + ".fg"] = ansicolors.fg sys.modules[__name__ + ".bg"] = ansicolors.bg sys.modules[__name__] = ansicolors plumbum-1.7.2/plumbum/_testtools.py0000644000232200023220000000121314161152302017772 0ustar debalancedebalance# -*- coding: utf-8 -*- import os import platform import sys import pytest skip_without_chown = pytest.mark.skipif( not hasattr(os, "chown"), reason="os.chown not supported" ) skip_without_tty = pytest.mark.skipif(not sys.stdin.isatty(), reason="Not a TTY") skip_on_windows = pytest.mark.skipif( sys.platform == "win32", reason="Windows not supported for this test (yet)" ) xfail_on_windows = pytest.mark.xfail( sys.platform == "win32", reason="Windows not supported for this test (yet)" ) xfail_on_pypy = pytest.mark.xfail( platform.python_implementation() == "PyPy", reason="PyPy is currently not working on this test!", ) plumbum-1.7.2/CONTRIBUTING.rst0000644000232200023220000000150314161152302016203 0ustar debalancedebalanceContributing to Plumbum ======================= General comments ---------------- Pull requests welcome! Please make sure you add tests (in an easy ``pytest`` format) to the tests folder for your fix or features. Make sure you add documentation covering a new feature. Adding a language ----------------- Plumbum.cli prints various messages for the user. These can be localized into your local language; pull requests adding languages are welcome. To add a language, run ``./translations.py`` from the main github directory, and then copy the file ``plumbum/cli/i18n/messages.pot`` to ``plumbum/cli/i18n/.po``, and add your language. Run ``./translations.py`` again to update the file you made (save first) and also create the needed files binary file. See `gettext: PMOTW3 `_ for more info. plumbum-1.7.2/tests/0000755000232200023220000000000014161152315014711 5ustar debalancedebalanceplumbum-1.7.2/tests/test_typed_env.py0000644000232200023220000000275414161152302020323 0ustar debalancedebalance# -*- coding: utf-8 -*- import pytest from plumbum.typed_env import TypedEnv class TestTypedEnv: def test_env(self): class E(TypedEnv): terminal = TypedEnv.Str("TERM") B = TypedEnv.Bool("BOOL", default=True) I = TypedEnv.Int("INT INTEGER".split()) INTS = TypedEnv.CSV("CS_INTS", type=int) raw_env = dict(TERM="xterm", CS_INTS="1,2,3,4") e = E(raw_env) assert e.terminal == "xterm" e.terminal = "foo" assert e.terminal == "foo" assert raw_env["TERM"] == "foo" assert "terminal" not in raw_env # check default assert e.B is True raw_env["BOOL"] = "no" assert e.B is False raw_env["BOOL"] = "0" assert e.B is False e.B = True assert raw_env["BOOL"] == "yes" e.B = False assert raw_env["BOOL"] == "no" assert e.INTS == [1, 2, 3, 4] e.INTS = [1, 2] assert e.INTS == [1, 2] e.INTS = [1, 2, 3, 4] with pytest.raises(KeyError): e.I raw_env["INTEGER"] = "4" assert e.I == 4 assert e["I"] == 4 e.I = "5" assert raw_env["INT"] == "5" assert e.I == 5 assert e["I"] == 5 assert "{I} {B} {terminal}".format(**e) == "5 False foo" assert dict(e) == dict(I=5, B=False, terminal="foo", INTS=[1, 2, 3, 4]) r = TypedEnv(raw_env) assert "{INT} {BOOL} {TERM}".format(**r) == "5 no foo" plumbum-1.7.2/tests/test_local.py0000644000232200023220000010502714161152302017415 0ustar debalancedebalance# -*- coding: utf-8 -*- import os import pickle import signal import sys import time import pytest import plumbum from plumbum import ( BG, ERROUT, FG, RETCODE, TEE, TF, CommandNotFound, LocalPath, ProcessExecutionError, ProcessLineTimedOut, ProcessTimedOut, local, ) from plumbum._testtools import ( skip_on_windows, skip_without_chown, skip_without_tty, xfail_on_pypy, ) from plumbum.fs.atomic import AtomicCounterFile, AtomicFile, PidFile from plumbum.lib import IS_WIN32, six from plumbum.machines.local import LocalCommand, PlumbumLocalPopen from plumbum.path import RelativePath # This is a string since we are testing local paths SDIR = os.path.dirname(os.path.abspath(__file__)) class TestLocalPopen: @pytest.mark.skipif( sys.version_info < (3, 2), reason="Context Manager was introduced in Python 3.2" ) def test_contextmanager(self): if IS_WIN32: command = ["dir"] else: command = ["ls"] with PlumbumLocalPopen(command): pass class TestLocalPath: longpath = local.path("/some/long/path/to/file.txt") def test_name(self): name = self.longpath.name assert isinstance(name, six.string_types) assert "file.txt" == str(name) def test_dirname(self): name = self.longpath.dirname assert isinstance(name, LocalPath) assert "/some/long/path/to" == str(name).replace("\\", "/").lstrip("C:").lstrip( "D:" ) def test_uri(self): if IS_WIN32: pth = self.longpath.as_uri() assert pth.startswith("file:///") assert pth.endswith(":/some/long/path/to/file.txt") else: assert "file:///some/long/path/to/file.txt" == self.longpath.as_uri() def test_pickle(self): path1 = local.path(".") path2 = local.path("~") assert pickle.loads(pickle.dumps(self.longpath)) == self.longpath assert pickle.loads(pickle.dumps(path1)) == path1 assert pickle.loads(pickle.dumps(path2)) == path2 def test_empty(self): with pytest.raises(TypeError): LocalPath() assert local.path() == local.path(".") @skip_without_chown def test_chown(self): with local.tempdir() as dir: p = dir / "foo.txt" p.write(six.b("hello")) assert p.uid == os.getuid() assert p.gid == os.getgid() p.chown(p.uid.name) assert p.uid == os.getuid() def test_split(self): p = local.path("/var/log/messages") p.split() == ["var", "log", "messages"] @pytest.mark.xfail( sys.platform == "win32" and (sys.version_info[0] == 2), reason="Caseless comparison (at least in pytest) fails on Windows 2.7", ) def test_suffix(self): # This picks up the drive letter differently if not constructed here p1 = local.path("/some/long/path/to/file.txt") p2 = local.path("file.tar.gz") assert p1.suffix == ".txt" assert p1.suffixes == [".txt"] assert p2.suffix == ".gz" assert p2.suffixes == [".tar", ".gz"] assert p1.with_suffix(".tar.gz") == local.path("/some/long/path/to/file.tar.gz") assert p2.with_suffix(".other") == local.path("file.tar.other") assert p2.with_suffix(".other", 2) == local.path("file.other") assert p2.with_suffix(".other", 0) == local.path("file.tar.gz.other") assert p2.with_suffix(".other", None) == local.path("file.other") with pytest.raises(ValueError): p1.with_suffix("nodot") @pytest.mark.xfail( sys.platform == "win32" and (sys.version_info[0] == 2), reason="Caseless comparison (at least in pytest) fails on Windows 2.7", ) def test_newname(self): # This picks up the drive letter differently if not constructed here p1 = local.path("/some/long/path/to/file.txt") p2 = local.path("file.tar.gz") assert p1.with_name("something.tar") == local.path( "/some/long/path/to/something.tar" ) assert p2.with_name("something.tar") == local.path("something.tar") def test_relative_to(self): p = local.path("/var/log/messages") assert p.relative_to("/var/log/messages") == RelativePath([]) assert p.relative_to("/var/") == RelativePath(["log", "messages"]) assert p.relative_to("/") == RelativePath(["var", "log", "messages"]) assert p.relative_to("/var/tmp") == RelativePath(["..", "log", "messages"]) assert p.relative_to("/opt") == RelativePath(["..", "var", "log", "messages"]) assert p.relative_to("/opt/lib") == RelativePath( ["..", "..", "var", "log", "messages"] ) for src in [ local.path("/var/log/messages"), local.path("/var"), local.path("/opt/lib"), ]: delta = p.relative_to(src) assert src + delta == p def test_read_write(self): with local.tempdir() as dir: f = dir / "test.txt" text = six.b("hello world\xd7\xa9\xd7\x9c\xd7\x95\xd7\x9d").decode("utf8") f.write(text, "utf8") text2 = f.read("utf8") assert text == text2 def test_parts(self): parts = self.longpath.parts if IS_WIN32: assert parts[1:] == ("some", "long", "path", "to", "file.txt") assert ":" in parts[0] else: assert parts == ("/", "some", "long", "path", "to", "file.txt") @pytest.mark.usefixtures("testdir") def test_iterdir(self): cwd = local.path(".") files = list(cwd.iterdir()) assert cwd / "test_local.py" in files assert cwd / "test_remote.py" in files assert cwd["test_local.py"] in files assert cwd["test_remote.py"] in files def test_stem(self): assert self.longpath.stem == "file" p = local.path("/some/directory") assert p.stem == "directory" def test_root_drive(self): pathlib = pytest.importorskip("pathlib") pl_path = pathlib.Path("/some/long/path/to/file.txt").absolute() assert self.longpath.root == pl_path.root assert self.longpath.drive == pl_path.drive p_path = local.cwd / "somefile.txt" pl_path = pathlib.Path("somefile.txt").absolute() assert p_path.root == pl_path.root assert p_path.drive == pl_path.drive def test_compare_pathlib(self): pathlib = pytest.importorskip("pathlib") def filename_compare(name): p = local.path(str(name)) pl = pathlib.Path(str(name)).absolute() assert str(p) == str(pl) assert p.parts == pl.parts assert p.exists() == pl.exists() assert p.is_symlink() == pl.is_symlink() assert p.as_uri() == pl.as_uri() assert str(p.with_suffix(".this")) == str(pl.with_suffix(".this")) assert p.name == pl.name assert str(p.parent) == str(pl.parent) assert list(map(str, p.parents)) == list(map(str, pl.parents)) filename_compare("/some/long/path/to/file.txt") filename_compare(local.cwd / "somefile.txt") filename_compare("/some/long/path/") filename_compare("/some/long/path") filename_compare(__file__) def test_suffix_expected(self): assert self.longpath.preferred_suffix(".tar") == self.longpath assert (local.cwd / "this").preferred_suffix(".txt") == local.cwd / "this.txt" def test_touch(self): with local.tempdir() as tmp: one = tmp / "one" assert not one.is_file() one.touch() assert one.is_file() one.delete() assert not one.is_file() def test_copy_override(self): """Edit this when override behavior is added""" with local.tempdir() as tmp: one = tmp / "one" one.touch() two = tmp / "two" assert one.is_file() assert not two.is_file() one.copy(two) assert one.is_file() assert two.is_file() def test_copy_nonexistant_dir(self): with local.tempdir() as tmp: one = tmp / "one" one.write(b"lala") two = tmp / "two" / "one" three = tmp / "three" / "two" / "one" one.copy(two) assert one.read() == two.read() one.copy(three) assert one.read() == three.read() def test_unlink(self): with local.tempdir() as tmp: one = tmp / "one" one.touch() assert one.exists() one.unlink() assert not one.exists() def test_unhashable(self): with pytest.raises(TypeError): hash(local.cwd) def test_getpath(self): assert local.cwd.getpath() == local.path(".") def test_path_dir(self): assert local.path(__file__).dirname == SDIR def test_mkdir(self): # (identical to test_remote.TestRemotePath.test_mkdir) with local.tempdir() as tmp: tmp["a"].mkdir(exist_ok=False, parents=False) assert tmp["a"].exists() assert tmp["a"].is_dir() tmp["a"].mkdir(exist_ok=True, parents=False) tmp["a"].mkdir(exist_ok=True, parents=True) with pytest.raises(OSError): tmp["a"].mkdir(exist_ok=False, parents=False) with pytest.raises(OSError): tmp["a"].mkdir(exist_ok=False, parents=True) tmp["b"]["bb"].mkdir(exist_ok=False, parents=True) assert tmp["b"]["bb"].exists() assert tmp["b"]["bb"].is_dir() assert not tmp.exists() def test_mkdir_mode(self): # (identical to test_remote.TestRemotePath.test_mkdir_mode) with local.tempdir() as tmp: # just verify that mode argument works the same way it does for # Python's own os.mkdir, which takes into account the umask # (different from shell mkdir mode argument!); umask on my # system is 022 by default, so 033 is ok for testing this try: (tmp / "pb_333").mkdir(exist_ok=False, parents=False, mode=0o333) local.python( "-c", "import os; os.mkdir({}, 0o333)".format(repr(str(tmp / "py_333"))), ) pb_final_mode = oct((tmp / "pb_333").stat().st_mode) py_final_mode = oct((tmp / "py_333").stat().st_mode) assert pb_final_mode == py_final_mode finally: # we have to revert this so the tempdir deletion works if (tmp / "pb_333").exists(): (tmp / "pb_333").chmod(0o777) if (tmp / "py_333").exists(): (tmp / "py_333").chmod(0o777) assert not tmp.exists() def test_str_getitem(self): with local.tempdir() as tmp: assert str(tmp) == str(tmp[:]) assert str(tmp)[0] == str(tmp[0]) def test_fspath(self): with local.tempdir() as tmp: assert tmp.__fspath__() == str(tmp) @pytest.mark.usefixtures("testdir") class TestLocalMachine: def test_getattr(self): pb = plumbum assert getattr(pb.cmd, "does_not_exist", 1) == 1 ls_cmd1 = pb.cmd.non_exist1N9 if hasattr(pb.cmd, "non_exist1N9") else pb.cmd.ls ls_cmd2 = getattr(pb.cmd, "non_exist1N9", pb.cmd.ls) assert str(ls_cmd1) == str(local["ls"]) assert str(ls_cmd2) == str(local["ls"]) # TODO: This probably fails because of odd ls behavior @skip_on_windows def test_imports(self): from plumbum.cmd import ls assert "test_local.py" in local["ls"]().splitlines() assert "test_local.py" in ls().splitlines() with pytest.raises(CommandNotFound): local["non_exist1N9"]() with pytest.raises(ImportError): from plumbum.cmd import non_exist1N9 # @UnresolvedImport @UnusedImport def test_get(self): assert str(local["ls"]) == str(local.get("ls")) assert str(local["ls"]) == str(local.get("non_exist1N9", "ls")) with pytest.raises(CommandNotFound): local.get("non_exist1N9") with pytest.raises(CommandNotFound): local.get("non_exist1N9", "non_exist1N8") with pytest.raises(CommandNotFound): local.get("non_exist1N9", "/tmp/non_exist1N8") def test_shadowed_by_dir(self): real_ls = local["ls"] with local.tempdir() as tdir: with local.cwd(tdir): ls_dir = tdir / "ls" ls_dir.mkdir() fake_ls = local["ls"] assert fake_ls.executable == real_ls.executable local.env.path.insert(0, tdir) fake_ls = local["ls"] del local.env.path[0] assert fake_ls.executable == real_ls.executable def test_repr_command(self): assert "BG" in repr(BG) assert "FG" in repr(FG) @skip_on_windows def test_cwd(self): from plumbum.cmd import ls assert local.cwd == os.getcwd() assert "machines" not in ls().splitlines() with local.cwd("../plumbum"): assert "machines" in ls().splitlines() assert "machines" not in ls().splitlines() assert "machines" in ls.with_cwd("../plumbum")().splitlines() path = local.cmd.pwd.with_cwd("../plumbum")().strip() with local.cwd("/"): assert "machines" not in ls().splitlines() assert "machines" in ls.with_cwd(path)().splitlines() with pytest.raises(OSError): local.cwd.chdir("../non_exist1N9") @skip_on_windows def test_mixing_chdir(self): assert local.cwd == os.getcwd() os.chdir("../plumbum") assert local.cwd == os.getcwd() os.chdir("../tests") assert local.cwd == os.getcwd() def test_contains(self): assert "plumbum" in local.cwd / ".." assert "non_exist1N91" not in local.cwd / ".." @skip_on_windows def test_path(self): assert not (local.cwd / "../non_exist1N9").exists() assert (local.cwd / ".." / "plumbum").is_dir() # traversal found = False for fn in local.cwd / ".." / "plumbum": if fn.name == "__init__.py": assert fn.is_file() found = True assert found # glob'ing found = False for fn in local.cwd / ".." // "*/*.rst": if fn.name == "index.rst": found = True assert found for fn in local.cwd / ".." // ("*/*.rst", "*./*.html"): if fn.name == "index.rst": found = True assert found @skip_on_windows def test_glob_spaces(self): fileloc = local.cwd / "file with space.txt" assert fileloc.exists() assert local.cwd // "*space.txt" assert local.cwd // "file with*" @skip_on_windows def test_env(self): assert "PATH" in local.env assert "FOOBAR72" not in local.env with pytest.raises(ProcessExecutionError): local.python("-c", "import os;os.environ['FOOBAR72']") local.env["FOOBAR72"] = "spAm" assert local.python( "-c", "import os;print (os.environ['FOOBAR72'])" ).splitlines() == ["spAm"] with local.env(FOOBAR73=1889): assert local.python( "-c", "import os;print (os.environ['FOOBAR73'])" ).splitlines() == ["1889"] with local.env(FOOBAR73=1778): assert local.python( "-c", "import os;print (os.environ['FOOBAR73'])" ).splitlines() == ["1778"] assert local.python( "-c", "import os;print (os.environ['FOOBAR73'])" ).splitlines() == ["1889"] with pytest.raises(ProcessExecutionError): local.python("-c", "import os;os.environ['FOOBAR73']") # path manipulation with pytest.raises(CommandNotFound): local.which("dummy-executable") with local.env(): local.env.path.insert(0, local.cwd / "not-in-path") p = local.which("dummy-executable") assert p == local.cwd / "not-in-path" / "dummy-executable" def test_local(self): from plumbum.cmd import cat, head assert "plumbum" in str(local.cwd) assert "PATH" in local.env.getdict() assert local.path("foo") == os.path.join(os.getcwd(), "foo") local.which("ls") local["ls"] assert local.python("-c", "print ('hi there')").splitlines() == ["hi there"] @skip_on_windows def test_piping(self): from plumbum.cmd import grep, ls chain = ls | grep["\\.py"] assert "test_local.py" in chain().splitlines() chain = ls["-a"] | grep["test"] | grep["local"] assert "test_local.py" in chain().splitlines() @skip_on_windows def test_redirection(self): from plumbum.cmd import cat, grep, ls, rm chain = (ls | grep["\\.py"]) > "tmp.txt" chain() chain2 = (cat < "tmp.txt") | grep["local"] assert "test_local.py" in chain2().splitlines() rm("tmp.txt") chain3 = ( cat << "this is the\nworld of helloness and\nspam bar and eggs" ) | grep["hello"] assert "world of helloness and" in chain3().splitlines() rc, _, err = (grep["-Zq5"] >= "tmp2.txt").run(["-Zq5"], retcode=None) assert rc == 2 assert not err assert "usage" in (cat < "tmp2.txt")().lower() rm("tmp2.txt") rc, out, _ = (grep["-Zq5"] >= ERROUT).run(["-Zq5"], retcode=None) assert rc == 2 assert "usage" in out.lower() @skip_on_windows def test_popen(self): from plumbum.cmd import ls p = ls.popen(["-a"]) out, _ = p.communicate() assert p.returncode == 0 assert "test_local.py" in out.decode(local.encoding).splitlines() def test_run(self): from plumbum.cmd import grep, ls rc, out, err = (ls | grep["non_exist1N9"]).run(retcode=1) assert rc == 1 def test_timeout(self): from plumbum.cmd import sleep with pytest.raises(ProcessTimedOut): sleep(3, timeout=1) @skip_on_windows def test_pipe_stderr(self, capfd): from plumbum.cmd import cat, head cat["/dev/urndom"] & FG(1) assert "urndom" in capfd.readouterr()[1] assert "" == capfd.readouterr()[1] (cat["/dev/urndom"] | head["-c", "10"]) & FG(retcode=1) assert "urndom" in capfd.readouterr()[1] @skip_on_windows def test_fair_error_attribution(self): # use LocalCommand directly for predictable argv false = LocalCommand("false") true = LocalCommand("true") with pytest.raises(ProcessExecutionError) as e: (false | true) & FG assert e.value.argv == ["false"] @skip_on_windows def test_iter_lines_timeout(self): from plumbum.cmd import bash cmd = bash["-ce", "for ((i=0;1==1;i++)); do echo $i; sleep .3; done"] with pytest.raises(ProcessTimedOut): for i, (out, err) in enumerate(cmd.popen().iter_lines(timeout=1)): assert not err assert out print(i, "out:", out) assert i in (2, 3) # Mac is a bit flakey @skip_on_windows def test_iter_lines_timeout_by_type(self): from plumbum.cmd import bash from plumbum.commands.processes import BY_TYPE cmd = bash[ "-ce", "for ((i=0;1==1;i++)); do echo $i; sleep .3; echo $i 1>&2; done" ] types = {1: "out:", 2: "err:"} counts = {1: 0, 2: 0} with pytest.raises(ProcessTimedOut): # Order is important on mac for typ, line in cmd.popen().iter_lines(timeout=1, mode=BY_TYPE): counts[typ] += 1 print(types[typ], line) assert counts[1] in (3, 4) # Mac is a bit flakey assert counts[2] in (2, 3) # Mac is a bit flakey @skip_on_windows def test_iter_lines_error(self): from plumbum.cmd import ls with pytest.raises(ProcessExecutionError) as err: for i, lines in enumerate(ls["--bla"].popen()): pass assert i == 1 assert ( "/bin/ls: unrecognized option '--bla'" in err.value.stderr or "bin/ls: illegal option -- -" in err.value.stderr ) @skip_on_windows def test_iter_lines_line_timeout(self): from plumbum.cmd import bash cmd = bash["-ce", "for ((i=0;1==1;i++)); do echo $i; sleep $i; done"] with pytest.raises(ProcessLineTimedOut): # Order is important on mac for i, (out, err) in enumerate(cmd.popen().iter_lines(line_timeout=0.2)): print(i, "out:", out) print(i, "err:", err) assert i == 1 @skip_on_windows def test_modifiers(self): from plumbum.cmd import grep, ls f = (ls["-a"] | grep["\\.py"]) & BG f.wait() assert "test_local.py" in f.stdout.splitlines() command = ls["-a"] | grep["local"] command_false = ls["-a"] | grep["not_a_file_here"] command & FG assert command & TF assert not (command_false & TF) assert command & RETCODE == 0 assert command_false & RETCODE == 1 @skip_on_windows def test_tee_modifier(self, capfd): from plumbum.cmd import echo result = echo["This is fun"] & TEE assert result[1] == "This is fun\n" assert "This is fun\n" == capfd.readouterr()[0] @skip_on_windows def test_tee_race(self, capfd): from plumbum.cmd import seq EXPECT = "".join("{}\n".format(i) for i in range(1, 5001)) for _ in range(5): result = seq["1", "5000"] & TEE assert result[1] == EXPECT assert EXPECT == capfd.readouterr()[0] @skip_on_windows def test_logger_pipe(self): from plumbum.cmd import bash from plumbum.commands.modifiers import PipeToLoggerMixin logs = [] class Logger(PipeToLoggerMixin): def log(self, level, line): print(level, line) logs.append((level, line)) logger = Logger() ret = bash["-ce", "echo aaa"] & logger assert logs[-1] == (PipeToLoggerMixin.INFO, "aaa") assert ret == 0 bash["-ce", "echo bbb 1>&2"] & logger assert logs[-1] == (PipeToLoggerMixin.DEBUG, "bbb") ret = bash["-ce", "echo ccc 1>&2; false"] & logger.pipe( prefix="echo", retcode=1, err_level=0 ) assert logs[-1] == (0, "echo: ccc") assert ret == 1 @skip_on_windows def test_logger_pipe_line_timeout(self): from plumbum.cmd import bash from plumbum.commands.modifiers import PipeToLoggerMixin cmd = bash["-ce", "for ((i=0;i<10;i++)); do echo .$i; sleep .$i; done"] class Logger(PipeToLoggerMixin): def log(self, level, line): print(level, line) assert level == 20 assert float(line) <= 0.6 logger = Logger() with pytest.raises(ProcessLineTimedOut): # Order is important on mac cmd & logger.pipe(line_timeout=0.45) def test_arg_expansion(self): from plumbum.cmd import ls args = ["-l", "-F"] ls(*args) ls[args] @skip_on_windows def test_session(self): sh = local.session() for _ in range(4): _, out, _ = sh.run("ls -a") assert "test_local.py" in out.splitlines() sh.run("cd ..") sh.run("export FOO=17") out = sh.run("echo $FOO")[1] assert out.splitlines() == ["17"] def test_quoting(self): ssh = local["ssh"] pwd = local["pwd"] cmd = ssh[ "localhost", "cd", "/usr", "&&", ssh[ "localhost", "cd", "/", "&&", ssh["localhost", "cd", "/bin", "&&", pwd] ], ] assert "\"'&&'\"" in " ".join(cmd.formulate(0)) ls = local["ls"] with pytest.raises(ProcessExecutionError) as execinfo: ls("-a", "") # check that empty strings are rendered correctly assert execinfo.value.argv[-2:] == ["-a", ""] def test_tempdir(self): from plumbum.cmd import cat with local.tempdir() as dir: assert dir.is_dir() data = six.b("hello world") with open(str(dir / "test.txt"), "wb") as f: f.write(data) with open(str(dir / "test.txt"), "rb") as f: assert f.read() == data assert not dir.exists() def test_direct_open_tmpdir(self): from plumbum.cmd import cat with local.tempdir() as dir: assert dir.is_dir() data = six.b("hello world") with open(dir / "test.txt", "wb") as f: f.write(data) with open(dir / "test.txt", "rb") as f: assert f.read() == data assert not dir.exists() def test_read_write_str(self): with local.tempdir() as tmp: data = "hello world" (tmp / "foo.txt").write(data) assert (tmp / "foo.txt").read() == data def test_read_write_unicode(self): with local.tempdir() as tmp: data = six.u("hello world") (tmp / "foo.txt").write(data) assert (tmp / "foo.txt").read() == data def test_read_write_bin(self): with local.tempdir() as tmp: data = six.b("hello world") (tmp / "foo.txt").write(data) assert (tmp / "foo.txt").read(mode="rb") == data def test_links(self): with local.tempdir() as tmp: src = tmp / "foo.txt" dst1 = tmp / "bar.txt" dst2 = tmp / "spam.txt" data = "hello world" src.write(data) src.link(dst1) assert data == dst1.read() src.symlink(dst2) assert data == dst2.read() def test_list_processes(self): assert list(local.list_processes()) def test_pgrep(self): assert list(local.pgrep("python")) def _generate_sigint(self): with pytest.raises(KeyboardInterrupt): if sys.platform == "win32": from win32api import GenerateConsoleCtrlEvent GenerateConsoleCtrlEvent(0, 0) # send Ctrl+C to current TTY else: os.kill(0, signal.SIGINT) time.sleep(1) @skip_without_tty @skip_on_windows def test_same_sesion(self): from plumbum.cmd import sleep p = sleep.popen([1000]) assert p.poll() is None self._generate_sigint() time.sleep(1) assert p.poll() is not None @skip_without_tty def test_new_session(self): from plumbum.cmd import sleep p = sleep.popen([1000], new_session=True) assert p.poll() is None self._generate_sigint() time.sleep(1) assert p.poll() is None p.terminate() # Hangs sometimes on Windows @skip_on_windows @pytest.mark.timeout(20) def test_local_daemon(self): from plumbum.cmd import sleep proc = local.daemonic_popen(sleep[5]) with pytest.raises(OSError): os.waitpid(proc.pid, 0) proc.wait() @skip_on_windows def test_atomic_file(self): af1 = AtomicFile("tmp.txt") af2 = AtomicFile("tmp.txt") af1.write_atomic(six.b("foo")) af2.write_atomic(six.b("bar")) assert af1.read_atomic() == six.b("bar") assert af2.read_atomic() == six.b("bar") local.path("tmp.txt").delete() @skip_on_windows def test_atomic_file2(self): af = AtomicFile("tmp.txt") code = """from __future__ import with_statement from plumbum.fs.atomic import AtomicFile af = AtomicFile("tmp.txt") try: with af.locked(blocking = False): raise ValueError("this should have failed") except (OSError, IOError): print("already locked") """ with af.locked(): output = local.python("-c", code) assert output.strip() == "already locked" local.path("tmp.txt").delete() @skip_on_windows def test_pid_file(self): code = """from __future__ import with_statement from plumbum.fs.atomic import PidFile, PidFileTaken try: with PidFile("mypid"): raise ValueError("this should have failed") except PidFileTaken: print("already locked") """ with PidFile("mypid"): output = local.python("-c", code) assert output.strip() == "already locked" local.path("mypid").delete() @skip_on_windows def test_atomic_counter(self): local.path("counter").delete() num_of_procs = 20 num_of_increments = 20 code = """from plumbum.fs.atomic import AtomicCounterFile import time time.sleep(0.2) afc = AtomicCounterFile.open("counter") for _ in range({}): print(afc.next()) time.sleep(0.1) """.format( num_of_increments, ) procs = [] for _ in range(num_of_procs): procs.append(local.python["-c", code].popen()) results = [] for p in procs: out, _ = p.communicate() assert p.returncode == 0 results.extend(int(num) for num in out.splitlines()) assert len(results) == num_of_procs * num_of_increments assert len(set(results)) == len(results) assert min(results) == 0 assert max(results) == num_of_procs * num_of_increments - 1 local.path("counter").delete() @skip_on_windows def test_atomic_counter2(self): local.path("counter").delete() afc = AtomicCounterFile.open("counter") assert afc.next() == 0 assert afc.next() == 1 assert afc.next() == 2 with pytest.raises(TypeError): afc.reset("hello") afc.reset(70) assert afc.next() == 70 assert afc.next() == 71 assert afc.next() == 72 local.path("counter").delete() @skip_on_windows @pytest.mark.skipif("printenv" not in local, reason="printenv is missing") def test_bound_env(self): from plumbum.cmd import printenv with local.env(FOO="hello"): assert printenv.with_env(BAR="world")("FOO") == "hello\n" assert printenv.with_env(BAR="world")("BAR") == "world\n" assert printenv.with_env(FOO="sea", BAR="world")("FOO") == "sea\n" assert printenv("FOO") == "hello\n" assert local.cmd.pwd.with_cwd("/")() == "/\n" assert local.cmd.pwd["-L"].with_env(A="X").with_cwd("/")() == "/\n" def test_nesting_lists_as_argv(self): from plumbum.cmd import ls c = ls["-l", ["-a", "*.py"]] assert c.formulate()[1:] == ["-l", "-a", "*.py"] def test_contains(self): assert "ls" in local def test_issue_139(self): LocalPath(local.cwd) def test_pipeline_failure(self): from plumbum.cmd import head, ls with pytest.raises(ProcessExecutionError): (ls["--no-such-option"] | head)() def test_cmd(self): local.cmd.ls("/tmp") def test_pipeline_retcode(self): "From PR #288" from plumbum.cmd import echo, grep print((echo["one two three four"] | grep["two"] | grep["three"])(retcode=None)) print((echo["one two three four"] | grep["five"] | grep["three"])(retcode=None)) print((echo["one two three four"] | grep["two"] | grep["five"])(retcode=None)) print((echo["one two three four"] | grep["six"] | grep["five"])(retcode=None)) def test_pipeline_stdin(self): from subprocess import PIPE from plumbum.cmd import cat with (cat | cat).bgrun(stdin=PIPE) as future: future.stdin.write(b"foobar") future.stdin.close() def test_run_bg(self): from plumbum.cmd import ls f = ls["-a"].run_bg() f.wait() assert "test_local.py" in f.stdout def test_run_fg(self, capfd): from plumbum.cmd import ls ls["-l"].run_fg() stdout = capfd.readouterr()[0] assert "test_local.py" in stdout @skip_on_windows def test_run_tee(self, capfd): from plumbum.cmd import echo result = echo["This is fun"].run_tee() assert result[1] == "This is fun\n" assert "This is fun\n" == capfd.readouterr()[0] def test_run_tf(self): from plumbum.cmd import ls f = ls["-l"].run_tf() assert f == True def test_run_retcode(self): from plumbum.cmd import ls f = ls["-l"].run_retcode() assert f == 0 def test_run_nohup(self): from plumbum.cmd import ls f = ls["-l"].run_nohup() f.wait() assert os.path.exists("nohup.out") os.unlink("nohup.out") class TestLocalEncoding: try: richstr = unichr(40960) except NameError: richstr = chr(40960) @pytest.mark.xfail( IS_WIN32, reason="Unicode output on Windows does not work (Python 3.6+ was supposed to work)", ) def test_inout_rich(self): from plumbum.cmd import echo out = echo(self.richstr) assert self.richstr in out @pytest.mark.xfail( IS_WIN32 and sys.version_info < (3, 6), reason="Unicode output on Windows requires Python 3.6+", ) @pytest.mark.usefixtures("cleandir") def test_out_rich(self): import io from plumbum.cmd import cat with io.open("temp.txt", "w", encoding="utf8") as f: f.write(self.richstr) out = cat("temp.txt") assert self.richstr in out @pytest.mark.xfail(IS_WIN32, reason="Unicode path not supported on Windows for now") @pytest.mark.skipif(not six.PY3, reason="Unicode paths only supported on Python 3") @pytest.mark.usefixtures("cleandir") def test_runfile_rich(self): import os import stat name = self.richstr + six.str("_program") with open(name, "w") as f: f.write("#!{}\nprint('yes')".format(sys.executable)) st = os.stat(name) os.chmod(name, st.st_mode | stat.S_IEXEC) assert "yes" in local[local.cwd / name]() @pytest.mark.skipif( IS_WIN32, reason="Windows does not support these weird paths, so unambiguous there" ) def test_local_glob_path(tmpdir): p = tmpdir.mkdir("a*b?c") p2 = tmpdir.mkdir("aanythingbxc") p2.join("something.txt").write("content") p.join("hello.txt").write("content") p.join("other.txt").write("content") pp = LocalPath(str(p)) assert len(pp // "*.txt") == 2 plumbum-1.7.2/tests/test_3_cli.py0000644000232200023220000000136714161152302017316 0ustar debalancedebalance# -*- coding: utf-8 -*- from plumbum import cli class Main3Validator(cli.Application): def main(self, myint: int, myint2: int, *mylist: int): print(myint, myint2, mylist) class TestProg3: def test_prog(self, capsys): _, rc = Main3Validator.run(["prog", "1", "2", "3", "4", "5"], exit=False) assert rc == 0 assert "1 2 (3, 4, 5)" in capsys.readouterr()[0] class Main4Validator(cli.Application): def main(self, myint: int, myint2: int, *mylist: int) -> None: print(myint, myint2, mylist) class TestProg4: def test_prog(self, capsys): _, rc = Main4Validator.run(["prog", "1", "2", "3", "4", "5"], exit=False) assert rc == 0 assert "1 2 (3, 4, 5)" in capsys.readouterr()[0] plumbum-1.7.2/tests/test_validate.py0000644000232200023220000000526714161152302020121 0ustar debalancedebalance# -*- coding: utf-8 -*- from __future__ import division, print_function from plumbum import cli class TestValidator: def test_named(self): class Try(object): @cli.positional(x=abs, y=str) def main(selfy, x, y): pass assert Try.main.positional == [abs, str] assert Try.main.positional_varargs is None def test_position(self): class Try(object): @cli.positional(abs, str) def main(selfy, x, y): pass assert Try.main.positional == [abs, str] assert Try.main.positional_varargs is None def test_mix(self): class Try(object): @cli.positional(abs, str, d=bool) def main(selfy, x, y, z, d): pass assert Try.main.positional == [abs, str, None, bool] assert Try.main.positional_varargs is None def test_var(self): class Try(object): @cli.positional(abs, str, int) def main(selfy, x, y, *g): pass assert Try.main.positional == [abs, str] assert Try.main.positional_varargs is int def test_defaults(self): class Try(object): @cli.positional(abs, str) def main(selfy, x, y="hello"): pass assert Try.main.positional == [abs, str] class TestProg: def test_prog(self, capsys): class MainValidator(cli.Application): @cli.positional(int, int, int) def main(self, myint, myint2, *mylist): print(repr(myint), myint2, mylist) _, rc = MainValidator.run(["prog", "1", "2", "3", "4", "5"], exit=False) assert rc == 0 assert "1 2 (3, 4, 5)" == capsys.readouterr()[0].strip() def test_failure(self, capsys): class MainValidator(cli.Application): @cli.positional(int, int, int) def main(self, myint, myint2, *mylist): print(myint, myint2, mylist) _, rc = MainValidator.run(["prog", "1.2", "2", "3", "4", "5"], exit=False) assert rc == 2 value = capsys.readouterr()[0].strip() assert "int" in value assert "not" in value assert "1.2" in value def test_defaults(self, capsys): class MainValidator(cli.Application): @cli.positional(int, int) def main(self, myint, myint2=2): print(repr(myint), repr(myint2)) _, rc = MainValidator.run(["prog", "1"], exit=False) assert rc == 0 assert "1 2" == capsys.readouterr()[0].strip() _, rc = MainValidator.run(["prog", "1", "3"], exit=False) assert rc == 0 assert "1 3" == capsys.readouterr()[0].strip() plumbum-1.7.2/tests/env.py0000644000232200023220000000070214161152302016046 0ustar debalancedebalance# -*- coding: utf-8 -*- import os import platform import sys LINUX = sys.platform.startswith("linux") MACOS = sys.platform.startswith("darwin") WIN = sys.platform.startswith("win32") or sys.platform.startswith("cygwin") CPYTHON = platform.python_implementation() == "CPython" PYPY = platform.python_implementation() == "PyPy" PY2 = sys.version_info.major == 2 PY = sys.version_info IS_A_TTY = sys.stdin.isatty() HAS_CHOWN = hasattr(os, "chown") plumbum-1.7.2/tests/test_clicolor.py0000644000232200023220000000537414161152302020135 0ustar debalancedebalance# -*- coding: utf-8 -*- from plumbum import cli, colors colors.use_color = 3 def make_app(): class SimpleApp(cli.Application): PROGNAME = colors.green VERSION = colors.red | "1.0.3" @cli.switch(["a"]) def spam(self): print("!!a") def main(self, *args): print("lalala") return SimpleApp class TestSimpleApp: def test_runs(self): SimpleApp = make_app() _, rc = SimpleApp.run(["SimpleApp"], exit=False) assert rc == 0 def test_colorless_run(self, capsys): colors.use_color = 0 SimpleApp = make_app() _, rc = SimpleApp.run(["SimpleApp"], exit=False) assert capsys.readouterr()[0] == "lalala\n" def test_colorful_run(self, capsys): colors.use_color = 4 SimpleApp = make_app() _, rc = SimpleApp.run(["SimpleApp"], exit=False) assert capsys.readouterr()[0] == "lalala\n" def test_colorless_output(self, capsys): colors.use_color = 0 SimpleApp = make_app() _, rc = SimpleApp.run(["SimpleApp", "-h"], exit=False) output = capsys.readouterr()[0] assert "SimpleApp 1.0.3" in output assert "SimpleApp [SWITCHES] args..." in output def test_colorful_help(self, capsys): colors.use_color = 4 SimpleApp = make_app() _, rc = SimpleApp.run(["SimpleApp", "-h"], exit=False) output = capsys.readouterr()[0] assert "SimpleApp 1.0.3" not in output assert SimpleApp.PROGNAME | "SimpleApp" in output class TestNSApp: def test_colorful_output(self, capsys): colors.use_color = 4 class NotSoSimpleApp(cli.Application): PROGNAME = colors.blue | "NSApp" VERSION = "1.2.3" COLOR_GROUPS = {"Switches": colors.cyan} COLOR_GROUP_TITLES = {"Switches": colors.bold & colors.cyan} COLOR_USAGE_TITLE = colors.bold & colors.cyan @cli.switch(["b"], help="this is a bacon switch") def bacon(self): print("Oooooh, I love BACON!") @cli.switch(["c"], help=colors.red | "crunchy") def crunchy(self): print("Crunchy...") def main(self): print("Eating!") _, rc = NotSoSimpleApp.run(["NotSoSimpleApp", "-h"], exit=False) output = capsys.readouterr()[0] assert rc == 0 expected = str((colors.blue | "NSApp") + " 1.2.3") assert str(colors.bold & colors.cyan | "Switches:") in output assert str(colors.bold & colors.cyan | "Usage:") in output assert "-b" in output assert str(colors.red | "crunchy") in output assert str(colors.cyan | "this is a bacon switch") in output assert expected in output plumbum-1.7.2/tests/test_putty.py0000644000232200023220000000402614161152302017505 0ustar debalancedebalance# -*- coding: utf-8 -*- """Test that PuttyMachine initializes its SshMachine correctly""" import env import pytest from plumbum import PuttyMachine, SshMachine @pytest.fixture(params=["default", "322"]) def ssh_port(request): return request.param class TestPuttyMachine: @pytest.mark.skipif(env.PYPY & env.PY2, reason="PyPy2 doesn't support mocker.spy") def test_putty_command(self, mocker, ssh_port): local = mocker.patch("plumbum.machines.ssh_machine.local") init = mocker.spy(SshMachine, "__init__") mocker.patch("plumbum.machines.ssh_machine.BaseRemoteMachine") host = mocker.MagicMock() user = local.env.user port = keyfile = None ssh_command = local["plink"] scp_command = local["pscp"] ssh_opts = ["-ssh"] if ssh_port == "default": putty_port = None scp_opts = () else: putty_port = int(ssh_port) ssh_opts.extend(["-P", ssh_port]) scp_opts = ["-P", ssh_port] encoding = mocker.MagicMock() connect_timeout = 20 new_session = True PuttyMachine( host, port=putty_port, connect_timeout=connect_timeout, new_session=new_session, encoding=encoding, ) init.assert_called_with( mocker.ANY, host, user, port, keyfile=keyfile, ssh_command=ssh_command, scp_command=scp_command, ssh_opts=ssh_opts, scp_opts=scp_opts, encoding=encoding, connect_timeout=connect_timeout, new_session=new_session, ) def test_putty_str(self, mocker): local = mocker.patch("plumbum.machines.ssh_machine.local") mocker.patch("plumbum.machines.ssh_machine.BaseRemoteMachine") host = mocker.MagicMock() user = local.env.user machine = PuttyMachine(host) assert str(machine) == "putty-ssh://{}@{}".format(user, host) plumbum-1.7.2/tests/test_config.py0000644000232200023220000000404314161152302017564 0ustar debalancedebalance# -*- coding: utf-8 -*- from __future__ import print_function import pytest from plumbum import local from plumbum.cli import Config, ConfigINI fname = "test_config.ini" @pytest.mark.usefixtures("cleandir") class TestConfig: def test_makefile(self): with ConfigINI(fname) as conf: conf["value"] = 12 conf["string"] = "ho" with open(fname) as f: contents = f.read() assert "value = 12" in contents assert "string = ho" in contents def test_readfile(self): with open(fname, "w") as f: print( """ [DEFAULT] one = 1 two = hello""", file=f, ) with ConfigINI(fname) as conf: assert conf["one"] == "1" assert conf["two"] == "hello" def test_complex_ini(self): with Config(fname) as conf: conf["value"] = "normal" conf["newer.value"] = "other" with Config(fname) as conf: assert conf["value"] == "normal" assert conf["DEFAULT.value"] == "normal" assert conf["newer.value"] == "other" def test_nowith(self): conf = ConfigINI(fname) conf["something"] = "nothing" conf.write() with open(fname) as f: contents = f.read() assert "something = nothing" in contents def test_home(self): mypath = local.env.home / "some_simple_home_rc.ini" assert not mypath.exists() try: with Config("~/some_simple_home_rc.ini") as conf: conf["a"] = "b" assert mypath.exists() mypath.unlink() with Config(mypath) as conf: conf["a"] = "b" assert mypath.exists() mypath.unlink() finally: mypath.unlink() def test_notouch(self): conf = ConfigINI(fname) assert not local.path(fname).exists() def test_only_string(self): conf = ConfigINI(fname) value = conf.get("value", 2) assert value == "2" plumbum-1.7.2/tests/not-in-path/0000755000232200023220000000000014161152315017047 5ustar debalancedebalanceplumbum-1.7.2/tests/not-in-path/dummy-executable0000755000232200023220000000001214161152302022234 0ustar debalancedebalance#!/bin/sh plumbum-1.7.2/tests/conftest.py0000644000232200023220000001022614161152302017105 0ustar debalancedebalance# -*- coding: utf-8 -*- import os import sys import tempfile import pytest if sys.version_info[0] < 3: collect_ignore = ["test_3_cli.py"] SDIR = os.path.dirname(os.path.abspath(__file__)) @pytest.fixture() def testdir(): os.chdir(SDIR) @pytest.fixture() def cleandir(): newpath = tempfile.mkdtemp() os.chdir(newpath) # Pulled from https://github.com/reece/pytest-optional-tests """implements declaration of optional tests using pytest markers The MIT License (MIT) Copyright (c) 2019 Reece Hart Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ import itertools import logging import re import pytest _logger = logging.getLogger(__name__) marker_re = re.compile(r"^(?P\w+)(:\s*(?P.*))?") def pytest_addoption(parser): group = parser.getgroup("collect") group.addoption( "--run-optional-tests", action="append", dest="run_optional_tests", default=None, help="Optional test markers to run, multiple and/or comma separated okay", ) parser.addini( "optional_tests", "list of optional markers", type="linelist", default="" ) def pytest_configure(config): # register all optional tests declared in ini file as markers # https://docs.pytest.org/en/latest/writing_plugins.html#registering-custom-markers ot_ini = config.inicfg.get("optional_tests").splitlines() for ot in ot_ini: # ot should be a line like "optmarker: this is an opt marker", as with markers section config.addinivalue_line("markers", ot) ot_markers = {marker_re.match(l).group(1) for l in ot_ini} # collect requested optional tests ot_run = config.getoption("run_optional_tests") if ot_run: ot_run = list(itertools.chain.from_iterable(a.split(",") for a in ot_run)) else: ot_run = config.inicfg.get("run_optional_tests", []) if ot_run: ot_run = list(re.split(r"[,\s]+", ot_run)) ot_run = set(ot_run) _logger.info("optional tests to run:", ot_run) if ot_run: unknown_tests = ot_run - ot_markers if unknown_tests: raise ValueError( "Requested execution of undeclared optional tests: {}".format( ", ".join(unknown_tests) ) ) config._ot_markers = set(ot_markers) config._ot_run = set(ot_run) def pytest_collection_modifyitems(config, items): # https://stackoverflow.com/a/50114028/342839 ot_markers = config._ot_markers ot_run = config._ot_run skips = {} for item in items: marker_names = {m.name for m in item.iter_markers()} if not marker_names: continue test_otms = marker_names & ot_markers if not test_otms: # test is not marked with any optional marker continue if test_otms & ot_run: # test is marked with an enabled optional test; don't skip continue mns = str(marker_names) if mns not in skips: skips[mns] = pytest.mark.skip( reason="Skipping; marked with disabled optional tests ({})".format( ", ".join(marker_names) ) ) item.add_marker(skips[mns]) plumbum-1.7.2/tests/test_remote.py0000644000232200023220000005550114161152302017617 0ustar debalancedebalance# -*- coding: utf-8 -*- import logging import os import socket import sys import time from copy import deepcopy from multiprocessing import Queue from threading import Thread import env import pytest import plumbum from plumbum import ( NOHUP, CommandNotFound, ProcessExecutionError, ProcessTimedOut, RemotePath, SshMachine, local, ) from plumbum._testtools import skip_on_windows, skip_without_chown from plumbum.lib import six from plumbum.machines.session import HostPublicKeyUnknown, IncorrectLogin try: import paramiko except ImportError: paramiko = None else: from plumbum.machines.paramiko_machine import ParamikoMachine pytestmark = pytest.mark.ssh def strassert(one, two): assert str(one) == str(two) # TEST_HOST = "192.168.1.143" TEST_HOST = "127.0.0.1" if TEST_HOST not in ("::1", "127.0.0.1", "localhost"): plumbum.local.env.path.append("c:\\Program Files\\Git\\bin") @pytest.fixture(scope="session") def sshpass(): try: return plumbum.local["sshpass"] except CommandNotFound: pytest.skip("Test requires sshpass") @skip_on_windows def test_connection(): SshMachine(TEST_HOST) @pytest.mark.skip( env.LINUX and env.PY[:2] == (3, 5), reason="Doesn't work on 3.5 on Linux on GHA" ) def test_incorrect_login(sshpass): with pytest.raises(IncorrectLogin): SshMachine( TEST_HOST, password="swordfish", ssh_opts=[ "-o", "PubkeyAuthentication=no", "-o", "PreferredAuthentications=password", ], ) @pytest.mark.xfail(env.LINUX, reason="TODO: no idea why this fails on linux") def test_hostpubkey_unknown(sshpass): with pytest.raises(HostPublicKeyUnknown): SshMachine( TEST_HOST, password="swordfish", ssh_opts=["-o", "UserKnownHostsFile=/dev/null", "-o", "UpdateHostKeys=no"], ) @skip_on_windows class TestRemotePath: def _connect(self): return SshMachine(TEST_HOST) def test_name(self): name = RemotePath(self._connect(), "/some/long/path/to/file.txt").name assert isinstance(name, six.string_types) assert "file.txt" == str(name) def test_dirname(self): name = RemotePath(self._connect(), "/some/long/path/to/file.txt").dirname assert isinstance(name, RemotePath) assert "/some/long/path/to" == str(name) def test_uri(self): p1 = RemotePath(self._connect(), "/some/long/path/to/file.txt") assert "ftp://" == p1.as_uri("ftp")[:6] assert "ssh://" == p1.as_uri("ssh")[:6] assert "/some/long/path/to/file.txt" == p1.as_uri()[-27:] def test_stem(self): p = RemotePath(self._connect(), "/some/long/path/to/file.txt") assert p.stem == "file" p = RemotePath(self._connect(), "/some/long/path/") assert p.stem == "path" def test_suffix(self): p1 = RemotePath(self._connect(), "/some/long/path/to/file.txt") p2 = RemotePath(self._connect(), "file.tar.gz") assert p1.suffix == ".txt" assert p1.suffixes == [".txt"] assert p2.suffix == ".gz" assert p2.suffixes == [".tar", ".gz"] strassert( p1.with_suffix(".tar.gz"), RemotePath(self._connect(), "/some/long/path/to/file.tar.gz"), ) strassert( p2.with_suffix(".other"), RemotePath(self._connect(), "file.tar.other") ) strassert( p2.with_suffix(".other", 2), RemotePath(self._connect(), "file.other") ) strassert( p2.with_suffix(".other", 0), RemotePath(self._connect(), "file.tar.gz.other"), ) strassert( p2.with_suffix(".other", None), RemotePath(self._connect(), "file.other") ) def test_newname(self): p1 = RemotePath(self._connect(), "/some/long/path/to/file.txt") p2 = RemotePath(self._connect(), "file.tar.gz") strassert( p1.with_name("something.tar"), RemotePath(self._connect(), "/some/long/path/to/something.tar"), ) strassert( p2.with_name("something.tar"), RemotePath(self._connect(), "something.tar") ) @skip_without_chown def test_chown(self): with self._connect() as rem: with rem.tempdir() as dir: p = dir / "foo.txt" p.write(six.b("hello")) # because we're connected to localhost, we expect UID and GID to be the same assert p.uid == os.getuid() assert p.gid == os.getgid() p.chown(p.uid.name) assert p.uid == os.getuid() def test_parent(self): p1 = RemotePath(self._connect(), "/some/long/path/to/file.txt") p2 = p1.parent assert str(p2) == "/some/long/path/to" def test_mkdir(self): # (identical to test_local.TestLocalPath.test_mkdir) with self._connect() as rem: with rem.tempdir() as tmp: (tmp / "a").mkdir(exist_ok=False, parents=False) assert (tmp / "a").exists() assert (tmp / "a").is_dir() (tmp / "a").mkdir(exist_ok=True, parents=False) (tmp / "a").mkdir(exist_ok=True, parents=True) with pytest.raises(OSError): (tmp / "a").mkdir(exist_ok=False, parents=False) with pytest.raises(OSError): (tmp / "a").mkdir(exist_ok=False, parents=True) (tmp / "b" / "bb").mkdir(exist_ok=False, parents=True) assert (tmp / "b" / "bb").exists() assert (tmp / "b" / "bb").is_dir() assert not tmp.exists() @pytest.mark.xfail( reason="mkdir's mode argument is not yet implemented " "for remote paths", strict=True, ) def test_mkdir_mode(self): # (identical to test_local.TestLocalPath.test_mkdir_mode) with self._connect() as rem: with rem.tempdir() as tmp: # just verify that mode argument works the same way it does for # Python's own os.mkdir, which takes into account the umask # (different from shell mkdir mode argument!); umask on my # system is 022 by default, so 033 is ok for testing this try: (tmp / "pb_333").mkdir(exist_ok=False, parents=False, mode=0o333) rem.python( "-c", "import os; os.mkdir({}, 0o333)".format( repr(str(tmp / "py_333")) ), ) pb_final_mode = oct((tmp / "pb_333").stat().st_mode) py_final_mode = oct((tmp / "py_333").stat().st_mode) assert pb_final_mode == py_final_mode finally: # we have to revert this so the tempdir deletion works if (tmp / "pb_333").exists(): (tmp / "pb_333").chmod(0o777) if (tmp / "py_333").exists(): (tmp / "py_333").chmod(0o777) assert not tmp.exists() def test_copy(self): """ tests `RemotePath.copy` for the following scenarios: * copying a simple file from `file_a` to `copy_of_a` succeeds * copying file `file_a` into a directory `a_dir/copy_of_a` succeeds * copying a directory `a_dir` over an existing directory path with `override=False` fails * copying a directory `a_dir` over an existing directory path with `override=True` succeeds """ with self._connect() as rem: with rem.tempdir() as tmp: # setup a file and make sure it exists... (tmp / "file_a").touch() assert (tmp / "file_a").exists() assert (tmp / "file_a").is_file() # setup a directory for copying into... (tmp / "a_dir").mkdir(exist_ok=False, parents=False) assert (tmp / "a_dir").exists() assert (tmp / "a_dir").is_dir() # setup a 2nd directory for testing `override=False` (tmp / "b_dir").mkdir(exist_ok=False, parents=False) assert (tmp / "b_dir").exists() assert (tmp / "b_dir").is_dir() # copying a simple file (tmp / "file_a").copy(tmp / "copy_of_a") assert (tmp / "copy_of_a").exists() assert (tmp / "copy_of_a").is_file() # copying into a directory (tmp / "file_a").copy(tmp / "a_dir/copy_of_a") assert (tmp / "a_dir/copy_of_a").exists() assert (tmp / "a_dir/copy_of_a").is_file() # copying a directory on top of an existing directory using # `override=False` (should fail with TypeError) with pytest.raises(TypeError): (tmp / "a_dir").copy(tmp / "b_dir", override=False) # copying a directory on top of an existing directory using # `override=True` (should copy transparently) (tmp / "a_dir").copy(tmp / "b_dir", override=True) assert "copy_of_a" in (tmp / "b_dir") assert not tmp.exists() class BaseRemoteMachineTest(object): TUNNEL_PROG_AF_INET = r"""import sys, socket s = socket.socket() s.bind(("", 0)) s.listen(1) sys.stdout.write("{0}\n".format(s.getsockname()[1])) sys.stdout.flush() s2, _ = s.accept() data = s2.recv(100) s2.send(b"hello " + data) s2.close() s.close() """ TUNNEL_PROG_AF_UNIX = r"""import sys, socket, tempfile s = socket.socket(family=socket.AF_UNIX) socket_location = tempfile.NamedTemporaryFile() socket_location.close() s.bind(socket_location.name) s.listen(1) sys.stdout.write("{0}\n".format(s.getsockname())) sys.stdout.flush() s2, _ = s.accept() data = s2.recv(100) s2.send(b"hello " + data) s2.close() s.close() """ def test_basic(self): with self._connect() as rem: r_ssh = rem["ssh"] r_ls = rem["ls"] r_grep = rem["grep"] lines = r_ls("-a").splitlines() assert ".bashrc" in lines or ".bash_profile" in lines with rem.cwd(os.path.dirname(os.path.abspath(__file__))): cmd = r_ssh[ "localhost", "cd", rem.cwd, "&&", r_ls, "|", r_grep["\\.py"] ] assert "'|'" in str(cmd) assert "test_remote.py" in cmd() assert "test_remote.py" in [f.name for f in rem.cwd // "*.py"] # Testing for #271 def test_double_chdir(self): with self._connect() as rem: with rem.cwd(os.path.dirname(os.path.abspath(__file__))): rem["ls"]() with rem.cwd("/tmp"): rem["pwd"]() def test_glob(self): with self._connect() as rem: with rem.cwd(os.path.dirname(os.path.abspath(__file__))): filenames = [f.name for f in rem.cwd // ("*.py", "*.bash")] assert "test_remote.py" in filenames assert "slow_process.bash" in filenames def test_glob_spaces(self): with self._connect() as rem: with rem.cwd(os.path.dirname(os.path.abspath(__file__))): filenames = [f.name for f in rem.cwd // ("*space.txt")] assert "file with space.txt" in filenames filenames = [f.name for f in rem.cwd // ("*with space.txt")] assert "file with space.txt" in filenames def test_cmd(self): with self._connect() as rem: rem.cmd.ls("/tmp") @pytest.mark.usefixtures("testdir") def test_download_upload(self): with self._connect() as rem: rem.upload("test_remote.py", "/tmp") r_ls = rem["ls"] r_rm = rem["rm"] assert "test_remote.py" in r_ls("/tmp").splitlines() rem.download("/tmp/test_remote.py", "/tmp/test_download.txt") r_rm("/tmp/test_remote.py") r_rm("/tmp/test_download.txt") def test_session(self): with self._connect() as rem: sh = rem.session() for _ in range(4): _, out, _ = sh.run("ls -a") assert ".bashrc" in out or ".bash_profile" in out def test_env(self): with self._connect() as rem: with pytest.raises(ProcessExecutionError): rem.python("-c", "import os;os.environ['FOOBAR72']") with rem.env(FOOBAR72="lala"): with rem.env(FOOBAR72="baba"): out = rem.python("-c", "import os;print(os.environ['FOOBAR72'])") assert out.strip() == "baba" out = rem.python("-c", "import os;print(os.environ['FOOBAR72'])") assert out.strip() == "lala" # path manipulation with pytest.raises(CommandNotFound): rem.which("dummy-executable") with rem.cwd(os.path.dirname(os.path.abspath(__file__))): rem.env.path.insert(0, rem.cwd / "not-in-path") p = rem.which("dummy-executable") assert p == rem.cwd / "not-in-path" / "dummy-executable" @pytest.mark.xfail(env.PYPY, reason="PyPy sometimes fails here", strict=False) @pytest.mark.parametrize( "env", [ "lala", "-Wl,-O2 -Wl,--sort-common", "{{}}", "''", "!@%_-+=:", "'", "`", "$", "\\", ], ) def test_env_special_characters(self, env): with self._connect() as rem: with pytest.raises(ProcessExecutionError): rem.python("-c", "import os;print(os.environ['FOOBAR72'])") rem.env["FOOBAR72"] = env out = rem.python("-c", "import os;print(os.environ['FOOBAR72'])") assert out.strip() == env def test_read_write(self): with self._connect() as rem: with rem.tempdir() as dir: assert dir.is_dir() data = six.b("hello world") (dir / "foo.txt").write(data) assert (dir / "foo.txt").read() == data assert not dir.exists() def test_contains(self): with self._connect() as rem: assert "ls" in rem def test_iter_lines_timeout(self): with self._connect() as rem: try: for i, (out, err) in enumerate( rem["ping"]["-i", 0.5, "127.0.0.1"].popen().iter_lines(timeout=4) ): print("out:", out) print("err:", err) except NotImplementedError: try: pytest.skip(str(sys.exc_info()[1])) except AttributeError: return except ProcessTimedOut: assert i > 3 else: pytest.fail("Expected a timeout") def test_iter_lines_error(self): with self._connect() as rem: with pytest.raises(ProcessExecutionError) as ex: for i, lines in enumerate(rem["ls"]["--bla"].popen()): pass assert i == 1 assert "/bin/ls: " in ex.value.stderr def test_touch(self): with self._connect() as rem: rfile = rem.cwd / "sillyfile" assert not rfile.exists() rfile.touch() assert rfile.exists() rfile.delete() def serve_reverse_tunnel(queue): s = socket.socket() s.bind(("", 12223)) s.listen(1) s2, _ = s.accept() data = s2.recv(100).decode("ascii").strip() queue.put(data) s2.close() s.close() @skip_on_windows class TestRemoteMachine(BaseRemoteMachineTest): def _connect(self): return SshMachine(TEST_HOST) def test_tunnel(self): for tunnel_prog in (self.TUNNEL_PROG_AF_INET, self.TUNNEL_PROG_AF_UNIX): with self._connect() as rem: p = (rem.python["-u"] << tunnel_prog).popen() port_or_socket = p.stdout.readline().decode("ascii").strip() try: port_or_socket = int(port_or_socket) dhost = "localhost" except ValueError: dhost = None with rem.tunnel(12222, port_or_socket, dhost=dhost) as tun: s = socket.socket() s.connect(("localhost", 12222)) s.send(six.b("world")) data = s.recv(100) s.close() print(p.communicate()) assert data == b"hello world" def test_reverse_tunnel(self): with self._connect() as rem: get_unbound_socket_remote = """import sys, socket s = socket.socket() s.bind(("", 0)) s.listen(1) sys.stdout.write(str(s.getsockname()[1])) sys.stdout.flush() s.close() """ p = (rem.python["-u"] << get_unbound_socket_remote).popen() remote_socket = p.stdout.readline().decode("ascii").strip() queue = Queue() tunnel_server = Thread(target=serve_reverse_tunnel, args=(queue,)) tunnel_server.start() message = str(time.time()) with rem.tunnel(12223, remote_socket, dhost="localhost", reverse=True): remote_send_af_inet = """import socket s = socket.socket() s.connect(("localhost", {})) s.send("{}".encode("ascii")) s.close() """.format( remote_socket, message ) (rem.python["-u"] << remote_send_af_inet).popen() tunnel_server.join(timeout=1) assert queue.get() == message def test_get(self): with self._connect() as rem: assert str(rem["ls"]) == str(rem.get("ls")) assert str(rem["ls"]) == str(rem.get("not_a_valid_process_234", "ls")) assert "ls" in rem assert "not_a_valid_process_234" not in rem def test_list_processes(self): with self._connect() as rem: assert list(rem.list_processes()) def test_pgrep(self): with self._connect() as rem: assert list(rem.pgrep("ssh")) def test_nohup(self): with self._connect() as rem: sleep = rem["sleep"] sleep["5.793817"] & NOHUP(stdout=None, append=False) time.sleep(0.5) print(rem["ps"]("aux")) assert list(rem.pgrep("5.793817")) time.sleep(6) assert not list(rem.pgrep("5.793817")) def test_bound_env(self): with self._connect() as rem: printenv = rem["printenv"] with rem.env(FOO="hello"): assert printenv.with_env(BAR="world")("FOO") == "hello\n" assert printenv.with_env(BAR="world")("BAR") == "world\n" assert printenv.with_env(FOO="sea", BAR="world")("FOO") == "sea\n" assert printenv.with_env(FOO="sea", BAR="world")("BAR") == "world\n" assert rem.cmd.pwd.with_cwd("/")() == "/\n" assert rem.cmd.pwd["-L"].with_env(A="X").with_cwd("/")() == "/\n" @pytest.mark.skipif( "useradd" not in local, reason="System does not have useradd (Mac?)" ) def test_sshpass(self): with local.as_root(): local["useradd"]("-m", "-b", "/tmp", "testuser") try: with local.as_root(): try: (local["passwd"] << "123456")("--stdin", "testuser") except ProcessExecutionError: # some versions of passwd don't support --stdin, nothing to do in this case logging.warning("passwd failed") return with SshMachine("localhost", user="testuser", password="123456") as rem: assert rem["pwd"]().strip() == "/tmp/testuser" finally: with local.as_root(): local["userdel"]("-r", "testuser") @skip_on_windows class TestParamikoMachine(BaseRemoteMachineTest): def _connect(self): if paramiko is None: pytest.skip("System does not have paramiko installed") return ParamikoMachine(TEST_HOST, missing_host_policy=paramiko.AutoAddPolicy()) def test_tunnel(self): with self._connect() as rem: p = rem.python["-c", self.TUNNEL_PROG_AF_INET].popen() try: port = int(p.stdout.readline().strip()) except ValueError: print(p.communicate()) raise s = rem.connect_sock(port) s.send(b"world") data = s.recv(100) s.close() print(p.communicate()) assert data == b"hello world" def test_piping(self): with self._connect() as rem: try: cmd = rem["ls"] | rem["cat"] except NotImplementedError: pass else: pytest.fail("Should not pipe") @pytest.mark.xfail(message="Not working yet") def test_encoding(self): with self._connect() as rem: unicode_half = b"\xc2\xbd".decode("utf8") ret = rem["bash"]("-c", 'echo -e "\xC2\xBD"') assert ret == "%s\n" % unicode_half ret = list(rem["bash"]["-c", 'echo -e "\xC2\xBD"'].popen()) assert ret == [["%s\n" % unicode_half, None]] def test_path_open_remote_write_local_read(self): with self._connect() as rem: # TODO: once Python 2.6 support is dropped, the nested # with-statements below can be combined using "with x as a, y as b" with rem.tempdir() as remote_tmpdir: with local.tempdir() as tmpdir: assert remote_tmpdir.is_dir() assert tmpdir.is_dir() data = six.b("hello world") with (remote_tmpdir / "bar.txt").open("wb") as f: f.write(data) rem.download((remote_tmpdir / "bar.txt"), (tmpdir / "bar.txt")) assert (tmpdir / "bar.txt").open("rb").read() == data assert not remote_tmpdir.exists() assert not tmpdir.exists() def test_path_open_local_write_remote_read(self): with self._connect() as rem: # TODO: cf. note on Python 2.6 support above with rem.tempdir() as remote_tmpdir: with local.tempdir() as tmpdir: assert remote_tmpdir.is_dir() assert tmpdir.is_dir() data = six.b("hello world") with (tmpdir / "bar.txt").open("wb") as f: f.write(data) rem.upload((tmpdir / "bar.txt"), (remote_tmpdir / "bar.txt")) assert (remote_tmpdir / "bar.txt").open("rb").read() == data assert not remote_tmpdir.exists() assert not tmpdir.exists() plumbum-1.7.2/tests/slow_process.bash0000755000232200023220000000022114161152302020264 0ustar debalancedebalance#!/usr/bin/env bash echo "Starting test" > slow_process.out for i in $(seq 1 3) do echo $i echo $i >> slow_process.out sleep 1 done plumbum-1.7.2/tests/_test_paramiko.py0000644000232200023220000000051314161152302020257 0ustar debalancedebalance# -*- coding: utf-8 -*- from plumbum import local from plumbum.paramiko_machine import ParamikoMachine as PM local.env.path.append("c:\\progra~1\\git\\bin") from plumbum.cmd import grep, ls # noqa: E402 m = PM("192.168.1.143") mls = m["ls"] mgrep = m["grep"] # (mls | mgrep["b"])() (mls | grep["\\."])() (ls | mgrep["\\."])() plumbum-1.7.2/tests/test_env.py0000644000232200023220000000411514161152302017107 0ustar debalancedebalance# -*- coding: utf-8 -*- import pytest import plumbum from plumbum import local from plumbum._testtools import skip_on_windows try: from plumbum.cmd import printenv except ImportError: pass @skip_on_windows class TestEnv: def test_change_env(self): with local.env(silly=12): assert 12 == local.env["silly"] actual = {x.split("=")[0] for x in printenv().splitlines() if "=" in x} localenv = {x[0] for x in local.env} print(actual, localenv) assert localenv == actual assert len(local.env) == len(actual) def test_dictlike(self): keys = {x.split("=")[0] for x in printenv().splitlines() if "=" in x} values = { x.split("=", 1)[1].strip() for x in printenv().splitlines() if "=" in x } assert keys == set(local.env.keys()) assert len(values) == len(set(local.env.values())) def test_custom_env(self): with local.env(): items = {"one": "OnE", "tww": "TWOO"} local.env.update(items) assert "tww" in local.env local.env.clear() assert "tww" not in local.env def test_item(self): with local.env(): local.env["simple_plum"] = "thing" assert "simple_plum" in local.env del local.env["simple_plum"] assert "simple_plum" not in local.env local.env["simple_plum"] = "thing" assert "simple_plum" in local.env assert "thing" == local.env.pop("simple_plum") assert "simple_plum" not in local.env local.env["simple_plum"] = "thing" assert "simple_plum" not in local.env @skip_on_windows def test_home(self): assert local.env.home == local.env["HOME"] old_home = local.env.home with local.env(): local.env.home = "Nobody" assert local.env.home == local.env["HOME"] assert local.env.home == "Nobody" assert local.env.home == old_home @skip_on_windows def test_user(self): assert local.env.user plumbum-1.7.2/tests/test_sudo.py0000644000232200023220000000064214161152302017272 0ustar debalancedebalance# -*- coding: utf-8 -*- import pytest from plumbum import local from plumbum._testtools import skip_on_windows pytestmark = pytest.mark.sudo # This is a separate file to make separating (ugly) sudo command easier # For example, you can now run test_local directly without typing a password class TestSudo: @skip_on_windows def test_as_user(self): with local.as_root(): local["date"]() plumbum-1.7.2/tests/test_factories.py0000644000232200023220000001443314161152302020302 0ustar debalancedebalance#!/usr/bin/env python # -*- coding: utf-8 -*- from __future__ import print_function import sys import pytest from plumbum import colors from plumbum.colorlib import htmlcolors from plumbum.colorlib.styles import ANSIStyle as Style from plumbum.colorlib.styles import ColorNotFound class TestImportColors: def testDifferentImports(self): import plumbum.colors from plumbum.colors import bold from plumbum.colors.fg import red assert str(red) == str(colors.red) assert str(bold) == str(colors.bold) class TestANSIColor: def setup_method(self, method): colors.use_color = True def testColorSlice(self): vals = colors[:8] assert len(vals) == 8 assert vals[1] == colors.red vals = colors[40:50] assert len(vals) == 10 assert vals[1] == colors.full(41) def testLoadNumericalColor(self): assert colors.full(2) == colors[2] assert colors.simple(2) == colors(2) assert colors(54) == colors[54] assert colors(1, 30, 77) == colors.rgb(1, 30, 77) assert colors[1, 30, 77] == colors.rgb(1, 30, 77) def testColorStrings(self): assert "\033[0m" == colors.reset assert "\033[1m" == colors.bold assert "\033[39m" == colors.fg.reset def testNegateIsReset(self): assert colors.reset == ~colors assert colors.fg.reset == ~colors.fg assert colors.bg.reset == ~colors.bg def testFromPreviousColor(self): assert colors(colors.red) == colors.red assert colors(colors.bg.red) == colors.bg.red assert colors(colors.bold) == colors.bold def testFromCode(self): assert colors("\033[31m") == colors.red def testEmptyStyle(self): assert str(colors()) == "" assert str(colors("")) == "" assert str(colors(None)) == "" def testLoadColorByName(self): assert colors["LightBlue"] == colors.fg["LightBlue"] assert colors.bg["light_green"] == colors.bg["LightGreen"] assert colors["DeepSkyBlue1"] == colors["#00afff"] assert colors["DeepSkyBlue1"] == colors.hex("#00afff") assert colors["DeepSkyBlue1"] == colors[39] assert colors.DeepSkyBlue1 == colors[39] assert colors.deepskyblue1 == colors[39] assert colors.Deep_Sky_Blue1 == colors[39] assert colors.RED == colors.red with pytest.raises(AttributeError): colors.Notacolorsatall def testMultiColor(self): sumcolors = colors.bold & colors.blue assert colors.bold.reset & colors.fg.reset == ~sumcolors def testSums(self): # Sums should not be communitave, last one is used assert colors.red == colors.blue & colors.red assert colors.bg.green == colors.bg.red & colors.bg.green def testRepresentations(self): colors1 = colors.full(87) assert colors1 == colors.DarkSlateGray2 assert colors1.basic == colors.DarkSlateGray2 assert str(colors1.basic) == str(colors.LightGray) colors2 = colors.rgb(1, 45, 214) assert str(colors2.full) == str(colors.Blue3A) def testFromAnsi(self): for c in colors[1:7]: assert c == colors.from_ansi(str(c)) for c in colors.bg[1:7]: assert c == colors.from_ansi(str(c)) for c in colors: assert c == colors.from_ansi(str(c)) for c in colors.bg: assert c == colors.from_ansi(str(c)) for c in colors[:16]: assert c == colors.from_ansi(str(c)) for c in colors.bg[:16]: assert c == colors.from_ansi(str(c)) for c in (colors.bold, colors.underline, colors.italics): assert c == colors.from_ansi(str(c)) col = colors.bold & colors.fg.green & colors.bg.blue & colors.underline assert col == colors.from_ansi(str(col)) col = colors.reset assert col == colors.from_ansi(str(col)) def testWrappedColor(self): string = "This is a string" wrapped = "\033[31mThis is a string\033[39m" assert colors.red.wrap(string) == wrapped assert colors.red | string == wrapped assert colors.red[string] == wrapped newcolors = colors.blue & colors.underline assert newcolors[string] == string | newcolors assert newcolors.wrap(string) == string | colors.blue & colors.underline def testUndoColor(self): assert "\033[39m" == ~colors.fg assert "\033[49m" == ~colors.bg assert "\033[22m" == ~colors.bold assert "\033[22m" == ~colors.dim for i in range(7): assert "\033[39m" == ~colors(i) assert "\033[49m" == ~colors.bg(i) assert "\033[39m" == ~colors.fg(i) assert "\033[49m" == ~colors.bg(i) for i in range(256): assert "\033[39m" == ~colors.fg[i] assert "\033[49m" == ~colors.bg[i] assert "\033[0m" == ~colors.reset assert colors.do_nothing == ~colors.do_nothing assert colors.bold.reset == ~colors.bold def testLackOfColor(self): Style.use_color = False assert "" == colors.fg.red assert "" == ~colors.fg assert "" == colors.fg["LightBlue"] def testFromHex(self): with pytest.raises(ColorNotFound): colors.hex("asdf") with pytest.raises(ColorNotFound): colors.hex("#1234Z2") with pytest.raises(ColorNotFound): colors.hex(12) def testDirectCall(self, capsys): colors.blue() assert capsys.readouterr()[0] == str(colors.blue) def testPrint(self, capsys): colors.yellow.print("This is printed to stdout", end="") assert capsys.readouterr()[0] == str( colors.yellow.wrap("This is printed to stdout") ) class TestHTMLColor: def test_html(self): red_tagged = 'This is tagged' assert htmlcolors.red["This is tagged"] == red_tagged assert "This is tagged" | htmlcolors.red == red_tagged twin_tagged = 'This is tagged' assert "This is tagged" | htmlcolors.red & htmlcolors.em == twin_tagged assert "This is tagged" | htmlcolors.em & htmlcolors.red == twin_tagged assert htmlcolors.em & htmlcolors.red | "This is tagged" == twin_tagged plumbum-1.7.2/tests/test_visual_color.py0000644000232200023220000000445414161152302021026 0ustar debalancedebalance#!/usr/bin/env python # -*- coding: utf-8 -*- from __future__ import print_function import os import unittest from plumbum import colors # This is really intended to be run manually, so the output can be observed, rather than with py.test class TestVisualColor(unittest.TestCase): def setUp(self): if os.name == "nt": try: import colorama colorama.init() self.colorama = colorama colors.use_color = 1 print() print("Colorama initialized") except ImportError: self.colorama = None else: self.colorama = None def tearDown(self): if self.colorama: self.colorama.deinit() def testVisualColors(self): print() for c in colors.fg[:16]: with c: print("Cycle color test", end=" ") print(" - > back to normal") with colors: print( colors.fg.green + "Green " + colors.bold + "Bold " + ~colors.bold + "Normal" ) print("Reset all") def testToggleColors(self): print() print(colors.fg.red["This is in red"], "but this is not") print( colors.fg.green + "Hi, " + colors.bg[23] + "This is on a BG" + ~colors.bg + " and this is not but is still green." ) colors.yellow.print("This is printed from color.") colors.reset() for attr in colors._style.attribute_names: print("This is", attr | getattr(colors, attr), "and this is not.") colors.reset() def testLimits(self): print() cval = colors.use_color colors.use_color = 4 c = colors.rgb(123, 40, 200) print("True", repr(str(c)), repr(c)) colors.use_color = 3 print("Full", repr(str(c)), repr(c)) colors.use_color = 2 print("Simple", repr(str(c)), repr(c)) colors.use_color = 1 print("Basic", repr(str(c)), repr(c)) colors.use_color = 0 print("None", repr(str(c)), repr(c)) colors.use_color = cval if __name__ == "__main__": unittest.main() plumbum-1.7.2/tests/test_nohup.py0000644000232200023220000000425714161152302017457 0ustar debalancedebalance# -*- coding: utf-8 -*- import os import time import psutil import pytest from plumbum import NOHUP, local try: from plumbum.cmd import bash, echo except ImportError: bash = None echo = None from plumbum._testtools import skip_on_windows from plumbum.path.utils import delete @skip_on_windows class TestNohupLocal: def read_file(self, filename): assert filename in os.listdir(".") with open(filename) as f: return f.read() @pytest.mark.usefixtures("testdir") def test_slow(self): delete("nohup.out") sp = bash["slow_process.bash"] sp & NOHUP time.sleep(0.5) assert self.read_file("slow_process.out") == "Starting test\n1\n" assert self.read_file("nohup.out") == "1\n" time.sleep(1) assert self.read_file("slow_process.out") == "Starting test\n1\n2\n" assert self.read_file("nohup.out") == "1\n2\n" time.sleep(2) delete("nohup.out", "slow_process.out") def test_append(self): delete("nohup.out") output = echo["This is output"] output & NOHUP time.sleep(0.2) assert self.read_file("nohup.out") == "This is output\n" output & NOHUP time.sleep(0.2) assert self.read_file("nohup.out") == "This is output\n" * 2 delete("nohup.out") def test_redir(self): delete("nohup_new.out") output = echo["This is output"] output & NOHUP(stdout="nohup_new.out") time.sleep(0.2) assert self.read_file("nohup_new.out") == "This is output\n" delete("nohup_new.out") (output > "nohup_new.out") & NOHUP time.sleep(0.2) assert self.read_file("nohup_new.out") == "This is output\n" delete("nohup_new.out") output & NOHUP time.sleep(0.2) assert self.read_file("nohup.out") == "This is output\n" delete("nohup.out") def test_closed_filehandles(self): proc = psutil.Process() file_handles_prior = proc.num_fds() sleep_proc = local["sleep"]["1"] & NOHUP sleep_proc.wait() file_handles_after = proc.num_fds() assert file_handles_prior >= file_handles_after plumbum-1.7.2/tests/test_color.py0000644000232200023220000000545414161152302017444 0ustar debalancedebalance# -*- coding: utf-8 -*- import pytest # Just check to see if this file is importable from plumbum.cli.image import Image from plumbum.colorlib.names import FindNearest, color_html from plumbum.colorlib.styles import ANSIStyle, AttributeNotFound, Color, ColorNotFound class TestNearestColor: def test_exact(self): assert FindNearest(0, 0, 0).all_fast() == 0 for n, color in enumerate(color_html): # Ignoring duplicates if n not in (16, 21, 46, 51, 196, 201, 226, 231, 244): rgb = (int(color[1:3], 16), int(color[3:5], 16), int(color[5:7], 16)) assert FindNearest(*rgb).all_fast() == n def test_nearby(self): assert FindNearest(1, 2, 2).all_fast() == 0 assert FindNearest(7, 7, 9).all_fast() == 232 def test_simplecolor(self): assert FindNearest(1, 2, 4).only_basic() == 0 assert FindNearest(0, 255, 0).only_basic() == 2 assert FindNearest(100, 100, 0).only_basic() == 3 assert FindNearest(140, 140, 140).only_basic() == 7 class TestColorLoad: def test_rgb(self): blue = Color(0, 0, 255) # Red, Green, Blue assert blue.rgb == (0, 0, 255) def test_simple_name(self): green = Color.from_simple("green") assert green.number == 2 def test_different_names(self): assert Color("Dark Blue") == Color("Dark_Blue") assert Color("Dark_blue") == Color("Dark_Blue") assert Color("DARKBLUE") == Color("Dark_Blue") assert Color("DarkBlue") == Color("Dark_Blue") assert Color("Dark Green") == Color("Dark_Green") def test_loading_methods(self): assert Color("Yellow") == Color.from_full("Yellow") assert ( Color.from_full("yellow").representation != Color.from_simple("yellow").representation ) class TestANSIColor: @classmethod def setup_class(cls): ANSIStyle.use_color = True def test_ansi(self): assert str(ANSIStyle(fgcolor=Color("reset"))) == "\033[39m" assert str(ANSIStyle(fgcolor=Color.from_full("green"))) == "\033[38;5;2m" assert str(ANSIStyle(fgcolor=Color.from_simple("red"))) == "\033[31m" class TestNearestColor: def test_allcolors(self): myrange = ( 0, 1, 2, 5, 17, 39, 48, 73, 82, 140, 193, 210, 240, 244, 250, 254, 255, ) for r in myrange: for g in myrange: for b in myrange: near = FindNearest(r, g, b) assert ( near.all_slow() == near.all_fast() ), "Tested: {}, {}, {}".format(r, g, b) plumbum-1.7.2/tests/file with space.txt0000644000232200023220000000000014161152302020363 0ustar debalancedebalanceplumbum-1.7.2/tests/test_terminal.py0000644000232200023220000001273014161152302020134 0ustar debalancedebalance# -*- coding: utf-8 -*- import sys import time from contextlib import contextmanager import pytest from plumbum.cli.terminal import Progress, ask, choose, hexdump, prompt from plumbum.lib import StringIO try: from collections import OrderedDict except ImportError: try: from ordereddict import OrderedDict except ImportError: OrderedDict = None needs_od = pytest.mark.skipif( OrderedDict is None, reason="Ordered dict not available (Py 2.6)" ) @contextmanager def send_stdin(stdin="\n"): prevstdin = sys.stdin sys.stdin = StringIO(stdin) try: yield sys.stdin finally: sys.stdin = prevstdin class TestPrompt: def test_simple(self, capsys): with send_stdin("12"): assert prompt("Enter a random int:", type=int) == 12 assert capsys.readouterr()[0] == "Enter a random int: " def test_try_twice(self, capsys): with send_stdin("\n13"): assert prompt("Enter a random int:", type=int) == 13 assert capsys.readouterr()[0] == "Enter a random int: Enter a random int: " def test_str(self): with send_stdin("1234"): assert prompt("Enter a string", type=str) == "1234" def test_default(self, capsys): with send_stdin(""): assert prompt("Enter nothing", default="hi") == "hi" assert capsys.readouterr()[0] == "Enter nothing [hi]: " def test_typefail(self, capsys): with send_stdin("1.2\n13"): assert prompt("Enter int", type=int) == 13 assert "try again" in capsys.readouterr()[0] def test_validator(self, capsys): with send_stdin("12\n9"): assert ( prompt("Enter in range < 10", type=int, validator=lambda x: x < 10) == 9 ) assert "try again" in capsys.readouterr()[0] class TestTerminal: def test_ask(self, capsys): with send_stdin("\n"): assert ask("Do you like cats?", default=True) assert capsys.readouterr()[0] == "Do you like cats? [Y/n] " with send_stdin("\nyes"): assert ask("Do you like cats?") assert ( capsys.readouterr()[0] == "Do you like cats? (y/n) Invalid response, please try again\nDo you like cats? (y/n) " ) def test_choose(self, capsys): with send_stdin("foo\n2\n"): assert ( choose("What is your favorite color?", ["blue", "yellow", "green"]) == "yellow" ) assert ( capsys.readouterr()[0] == "What is your favorite color?\n(1) blue\n(2) yellow\n(3) green\nChoice: Invalid choice, please try again\nChoice: " ) with send_stdin("foo\n2\n"): assert ( choose( "What is your favorite color?", [("blue", 10), ("yellow", 11), ("green", 12)], ) == 11 ) assert ( capsys.readouterr()[0] == "What is your favorite color?\n(1) blue\n(2) yellow\n(3) green\nChoice: Invalid choice, please try again\nChoice: " ) with send_stdin("foo\n\n"): assert ( choose( "What is your favorite color?", ["blue", "yellow", "green"], default="yellow", ) == "yellow" ) assert ( capsys.readouterr()[0] == "What is your favorite color?\n(1) blue\n(2) yellow\n(3) green\nChoice [2]: Invalid choice, please try again\nChoice [2]: " ) def test_choose_dict(self): with send_stdin("23\n1"): value = choose("Pick", dict(one="a", two="b")) assert value in ("a", "b") @needs_od def test_ordered_dict(self): dic = OrderedDict() dic["one"] = "a" dic["two"] = "b" with send_stdin("1"): value = choose("Pick", dic) assert value == "a" with send_stdin("2"): value = choose("Pick", dic) assert value == "b" @needs_od def test_choose_dict_default(self, capsys): dic = OrderedDict() dic["one"] = "a" dic["two"] = "b" with send_stdin(): assert choose("Pick", dic, default="a") == "a" assert "[1]" in capsys.readouterr()[0] def test_hexdump(self): data = "hello world my name is queen marry" + "A" * 66 + "foo bar" output = """\ 000000 | 68 65 6c 6c 6f 20 77 6f 72 6c 64 20 6d 79 20 6e | hello world my n 000010 | 61 6d 65 20 69 73 20 71 75 65 65 6e 20 6d 61 72 | ame is queen mar 000020 | 72 79 41 41 41 41 41 41 41 41 41 41 41 41 41 41 | ryAAAAAAAAAAAAAA 000030 | 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 | AAAAAAAAAAAAAAAA * 000060 | 41 41 41 41 66 6f 6f 20 62 61 72 | AAAAfoo bar""" assert "\n".join(hexdump(data)) == output assert "\n".join(hexdump(StringIO(data))) == output def test_progress(self, capsys): for i in Progress.range(4, has_output=True, timer=False): print("hi") stdout, stderr = capsys.readouterr() output = """\ 0% complete 0% complete hi 25% complete hi 50% complete hi 75% complete hi 100% complete """ assert stdout == output def test_progress_empty(self, capsys): for i in Progress.range(0, has_output=True, timer=False): print("hi") stdout, stderr = capsys.readouterr() output = "0/0 complete" assert output in stdout plumbum-1.7.2/tests/test_cli.py0000644000232200023220000002604114161152302017070 0ustar debalancedebalance# -*- coding: utf-8 -*- import sys import pytest from plumbum import cli, local from plumbum.cli.terminal import get_terminal_size class SimpleApp(cli.Application): @cli.switch(["a"]) def spam(self): print("!!a") @cli.switch( ["b", "bacon"], argtype=int, mandatory=True, envname="PLUMBUM_TEST_BACON" ) def bacon(self, param): """give me some bacon""" print("!!b", param) eggs = cli.SwitchAttr( ["e"], str, help="sets the eggs attribute", envname="PLUMBUM_TEST_EGGS" ) cheese = cli.Flag(["--cheese"], help="cheese, please") chives = cli.Flag(["--chives"], help="chives, instead") verbose = cli.CountOf(["v"], help="increases the verbosity level") benedict = cli.CountOf( ["--benedict"], help="""a very long help message with lots of useless information that nobody would ever want to read, but heck, we need to test text wrapping in help messages as well""", ) csv = cli.SwitchAttr(["--csv"], cli.Set("MIN", "MAX", int, csv=True)) num = cli.SwitchAttr(["--num"], cli.Set("MIN", "MAX", int)) def main(self, *args): old = self.eggs self.eggs = "lalala" self.eggs = old self.tailargs = args class PositionalApp(cli.Application): def main(self, one): print("Got", one) class Geet(cli.Application): debug = cli.Flag("--debug") cleanups = [] def main(self): del self.cleanups[:] print("hi this is geet main") def cleanup(self, retcode): self.cleanups.append(1) print("geet cleaning up with rc = {}".format(retcode)) @Geet.subcommand("add") class GeetAdd(cli.Application): def main(self, *files): return "adding", files @Geet.subcommand("commit") class GeetCommit(cli.Application): message = cli.Flag("-m", str) def main(self): if self.parent.debug: return "committing in debug" else: return "committing" def cleanup(self, retcode): self.parent.cleanups.append(2) print("geet commit cleaning up with rc = {}".format(retcode)) class Sample(cli.Application): DESCRIPTION = "A sample cli application" DESCRIPTION_MORE = """ ABC This is just a sample help text typed with a Dvorak keyboard. Although this paragraph is not left or right justified in source, we expect it to appear formatted nicely on the output, maintaining the indentation of the first line. DEF this one has a different indentation. Let's test that list items are not combined as paragraphs. - Item 1 GHI more text for item 1, which may be very very very very very very long and even more long and long and long to prove that we can actually wrap list items as well. - Item 2 and this is some text for item 2 - Item 3 List items with invisible bullets should be printed without the bullet. /XYZ Invisible 1 /Invisible 2 * Star 1 * Star 2 Last paragraph can fill more than one line on the output as well. So many features is bound to cause lots of bugs. Oh well... """ foo = cli.SwitchAttr("--foo") Sample.unbind_switches("--version") class Mumble(cli.Application): pass Sample.subcommand("mumble", Mumble) class LazyLoaded(cli.Application): def main(self): print("hello world") class AppA(cli.Application): @cli.switch(["--one"]) def one(self): pass two = cli.SwitchAttr(["--two"]) class AppB(AppA): @cli.switch(["--three"]) def three(self): pass four = cli.SwitchAttr(["--four"]) def main(self): pass # Testing #363 class TestInheritedApp: def test_help(self, capsys): _, rc = AppB.run(["AppB", "-h"], exit=False) assert rc == 0 stdout, stderr = capsys.readouterr() assert "--one" in stdout assert "--two" in stdout assert "--three" in stdout assert "--four" in stdout class TestCLI: def test_meta_switches(self): _, rc = SimpleApp.run(["foo", "-h"], exit=False) assert rc == 0 _, rc = SimpleApp.run(["foo", "--version"], exit=False) assert rc == 0 def test_okay(self): _, rc = SimpleApp.run(["foo", "--bacon=81"], exit=False) assert rc == 0 inst, rc = SimpleApp.run( [ "foo", "--bacon=81", "-a", "-v", "-e", "7", "-vv", "--", "lala", "-e", "7", ], exit=False, ) assert rc == 0 assert inst.eggs == "7" _, rc = SimpleApp.run(["foo", "--bacon=81", "--csv=100"], exit=False) assert rc == 0 _, rc = SimpleApp.run(["foo", "--bacon=81", "--csv=MAX,MIN,100"], exit=False) assert rc == 0 _, rc = SimpleApp.run(["foo", "--bacon=81", "--num=100"], exit=False) assert rc == 0 _, rc = SimpleApp.run(["foo", "--bacon=81", "--num=MAX"], exit=False) assert rc == 0 _, rc = SimpleApp.run(["foo", "--bacon=81", "--num=MIN"], exit=False) assert rc == 0 def test_failures(self): _, rc = SimpleApp.run(["foo"], exit=False) assert rc == 2 _, rc = SimpleApp.run(["foo", "--bacon=81", "--csv=xx"], exit=False) assert rc == 2 _, rc = SimpleApp.run(["foo", "--bacon=81", "--csv=xx"], exit=False) assert rc == 2 _, rc = SimpleApp.run(["foo", "--bacon=81", "--num=MOO"], exit=False) assert rc == 2 _, rc = SimpleApp.run(["foo", "--bacon=81", "--num=MIN,MAX"], exit=False) assert rc == 2 _, rc = SimpleApp.run(["foo", "--bacon=81", "--num=10.5"], exit=False) assert rc == 2 _, rc = SimpleApp.run(["foo", "--bacon=hello"], exit=False) assert rc == 2 # Testing #371 def test_extra_args(self, capsys): _, rc = PositionalApp.run(["positionalapp"], exit=False) assert rc != 0 stdout, stderr = capsys.readouterr() assert "Expected at least" in stdout _, rc = PositionalApp.run(["positionalapp", "one"], exit=False) assert rc == 0 stdout, stderr = capsys.readouterr() _, rc = PositionalApp.run(["positionalapp", "one", "two"], exit=False) assert rc != 0 stdout, stderr = capsys.readouterr() assert "Expected at most" in stdout def test_subcommands(self): _, rc = Geet.run(["geet", "--debug"], exit=False) assert rc == 0 assert Geet.cleanups == [1] _, rc = Geet.run(["geet", "--debug", "add", "foo.txt", "bar.txt"], exit=False) assert rc == ("adding", ("foo.txt", "bar.txt")) assert Geet.cleanups == [1] _, rc = Geet.run(["geet", "--debug", "commit"], exit=False) assert rc == "committing in debug" assert Geet.cleanups == [2, 1] _, rc = Geet.run(["geet", "--help"], exit=False) assert rc == 0 _, rc = Geet.run(["geet", "commit", "--help"], exit=False) assert rc == 0 assert Geet.cleanups == [1] def test_help_all(self, capsys): _, rc = Geet.run(["geet", "--help-all"], exit=False) assert rc == 0 stdout, stderr = capsys.readouterr() assert "--help-all" in stdout assert "geet add" in stdout assert "geet commit" in stdout def test_unbind(self, capsys): _, rc = Sample.run(["sample", "--help"], exit=False) assert rc == 0 stdout, stderr = capsys.readouterr() assert "--foo" in stdout assert "--version" not in stdout def test_description(self, capsys): _, rc = Sample.run(["sample", "--help"], exit=False) assert rc == 0 stdout, stderr = capsys.readouterr() cols, _ = get_terminal_size() if cols < 9: # Terminal is too narrow to test pass else: # Paragraph indentation should be preserved assert " ABC" in stdout assert " DEF" in stdout assert " - Item" in stdout # List items should not be combined into paragraphs assert " * Star 2" # Lines of the same list item should be combined. (The right-hand expression of the 'or' operator # below is for when the terminal is too narrow, causing "GHI" to be wrapped to the next line.) assert " GHI" not in stdout or " GHI" in stdout # List item with invisible bullet should be indented without the bullet assert " XYZ" in stdout def test_default_main(self, capsys): _, rc = Sample.run(["sample"], exit=False) assert rc == 1 stdout, stderr = capsys.readouterr() assert "No sub-command given" in stdout _, rc = Sample.run(["sample", "pimple"], exit=False) assert rc == 1 stdout, stderr = capsys.readouterr() assert "Unknown sub-command 'pimple'" in stdout _, rc = Sample.run(["sample", "mumble"], exit=False) assert rc == 1 stdout, stderr = capsys.readouterr() assert "main() not implemented" in stdout def test_lazy_subcommand(self, capsys): class Foo(cli.Application): pass Foo.subcommand("lazy", "test_cli.LazyLoaded") _, rc = Foo.run(["foo", "lazy"], exit=False) assert rc == 0 stdout, stderr = capsys.readouterr() assert "hello world" in stdout def test_reset_switchattr(self): inst, rc = SimpleApp.run(["foo", "--bacon=81", "-e", "bar"], exit=False) assert rc == 0 assert inst.eggs == "bar" inst, rc = SimpleApp.run(["foo", "--bacon=81"], exit=False) assert rc == 0 assert inst.eggs is None def test_invoke(self): inst, rc = SimpleApp.invoke("arg1", "arg2", eggs="sunny", bacon=10, verbose=2) assert (inst.eggs, inst.verbose, inst.tailargs) == ( "sunny", 2, ("arg1", "arg2"), ) def test_env_var(self, capsys): _, rc = SimpleApp.run(["arg", "--bacon=10"], exit=False) assert rc == 0 stdout, stderr = capsys.readouterr() assert "10" in stdout with local.env( PLUMBUM_TEST_BACON="20", PLUMBUM_TEST_EGGS="raw", ): inst, rc = SimpleApp.run(["arg"], exit=False) assert rc == 0 stdout, stderr = capsys.readouterr() assert "20" in stdout assert inst.eggs == "raw" def test_mandatory_env_var(self, capsys): _, rc = SimpleApp.run(["arg"], exit=False) assert rc == 2 stdout, stderr = capsys.readouterr() assert "bacon is mandatory" in stdout def test_partial_switches(self, capsys): app = SimpleApp app.ALLOW_ABBREV = True inst, rc = app.run(["foo", "--bacon=2", "--ch"], exit=False) stdout, stderr = capsys.readouterr() assert "Ambiguous partial switch" in stdout assert rc == 2 inst, rc = app.run(["foo", "--bacon=2", "--chee"], exit=False) assert rc == 0 assert inst.cheese is True assert inst.chives is False plumbum-1.7.2/tests/test_utils.py0000644000232200023220000000353214161152302017461 0ustar debalancedebalance# -*- coding: utf-8 -*- import pytest from plumbum import SshMachine, local from plumbum._testtools import skip_on_windows from plumbum.path.utils import copy, delete, move @skip_on_windows @pytest.mark.ssh def test_copy_move_delete(): from plumbum.cmd import touch with local.tempdir() as dir: (dir / "orog").mkdir() (dir / "orog" / "rec").mkdir() for i in range(20): touch(dir / "orog" / ("f%d.txt" % (i,))) for i in range(20, 40): touch(dir / "orog" / "rec" / ("f%d.txt" % (i,))) move(dir / "orog", dir / "orig") s1 = sorted(f.name for f in (dir / "orig").walk()) copy(dir / "orig", dir / "dup") s2 = sorted(f.name for f in (dir / "dup").walk()) assert s1 == s2 with SshMachine("localhost") as rem: with rem.tempdir() as dir2: copy(dir / "orig", dir2) s3 = sorted(f.name for f in (dir2 / "orig").walk()) assert s1 == s3 copy(dir2 / "orig", dir2 / "dup") s4 = sorted(f.name for f in (dir2 / "dup").walk()) assert s1 == s4 copy(dir2 / "dup", dir / "dup2") s5 = sorted(f.name for f in (dir / "dup2").walk()) assert s1 == s5 with SshMachine("localhost") as rem2: with rem2.tempdir() as dir3: copy(dir2 / "dup", dir3) s6 = sorted(f.name for f in (dir3 / "dup").walk()) assert s1 == s6 move(dir3 / "dup", dir / "superdup") assert not (dir3 / "dup").exists() s7 = sorted(f.name for f in (dir / "superdup").walk()) assert s1 == s7 # test rm delete(dir) plumbum-1.7.2/docs/0000755000232200023220000000000014161152315014477 5ustar debalancedebalanceplumbum-1.7.2/docs/utils.rst0000644000232200023220000000327214161152302016371 0ustar debalancedebalance.. _guide-utils: Utilities ========= The ``utils`` module contains a collection of useful utility functions. Note that they are not imported into the namespace of ``plumbum`` directly, and you have to explicitly import them, e.g. ``from plumbum.path.utils import copy``. * :func:`copy(src, dst) ` - Copies ``src`` to ``dst`` (recursively, if ``src`` is a directory). The arguments can be either local or remote paths -- the function will sort out all the necessary details. * If both paths are local, the files are copied locally * If one path is local and the other is remote, the function uploads/downloads the files * If both paths refer to the same remote machine, the function copies the files locally on the remote machine * If both paths refer to different remote machines, the function downloads the files to a temporary location and then uploads them to the destination * :func:`move(src, dst) ` - Moves ``src`` onto ``dst``. The arguments can be either local or remote -- the function will sort our all the necessary details (as in ``copy``) * :func:`delete(*paths) ` - Deletes the given sequence of paths; each path may be a string, a local/remote path object, or an iterable of paths. If any of the paths does not exist, the function silently ignores the error and continues. For example :: from plumbum.path.utils import delete delete(local.cwd // "*/*.pyc", local.cwd // "*/__pycache__") * :func:`gui_open(path) ` - Opens a file in the default editor on Windows, Mac, or Linux. Uses `os.startfile` if available (Windows), `xdg_open` (GNU), or `open` (Mac). plumbum-1.7.2/docs/local_commands.rst0000644000232200023220000003146514161152302020211 0ustar debalancedebalance.. _guide-local-commands: Local Commands ============== Plumbum exposes a special singleton object named ``local``, which represents your local machine and serves as a factory for command objects:: >>> from plumbum import local >>> >>> ls = local["ls"] >>> ls >>> notepad = local["c:\\windows\\notepad.exe"] >>> notepad If you don't specify a full path, the program is searched for in your system's ``PATH`` (and if no match is found, a ``CommandNotFound`` exception is raised). Otherwise, the full path is used as given. Once you have a ``Command`` object, you can execute it like a normal function:: >>> ls() 'README.rst\nplumbum\nsetup.py\ntests\ntodo.txt\n' >>> ls("-a") '.\n..\n.git\n.gitignore\n.project\n.pydevproject\nREADME.rst\n[...]' For convenience with the common case, you can use the ``.cmd`` magic property instead of the subscription syntax: >>> ls = local.cmd.ls >>> ls .. versionadded:: 1.7 The ``.cmd`` commands provider object .. _fallbacks: If you use the ``.get()`` method instead of ``[]``, you can include fallbacks to try if the first command does not exist on the machine. This can be used to get one of several equivalent commands, or it can be used to check for common locations of a command if not in the path. For example:: pandoc = local.get('pandoc', '~/AppData/Local/Pandoc/pandoc.exe', '/Program Files/Pandoc/pandoc.exe', '/Program Files (x86)/Pandoc/pandoc.exe') An exception is still raised if none of the commands are found. Unlike ``[]`` access, an exception will be raised if the executable does not exist. .. versionadded:: 1.6 The ``.get`` method .. _import-hack: With just a touch of magic, you can *import* commands from the mock module ``cmd``, like so:: >>> from plumbum.cmd import grep, cat >>> cat .. note:: There's no real module named ``plumbum.cmd``; it's a dynamically-created "module", injected into ``sys.modules`` to enable the use of ``from plumbum.cmd import foo``. As of version 1.1, you can actually ``import plumbum.cmd``, for consistency, but it's not recommended. It is important to stress that ``from plumbum.cmd import foo`` translates to ``local["foo"]`` behind the scenes. If underscores (``_``) appear in the name, and the name cannot be found in the path as-is, the underscores will be replaced by hyphens (``-``) and the name will be looked up again. This allows you to import ``apt_get`` for ``apt-get``. .. _guide-local-commands-pipelining: Pipelining ---------- In order to form pipelines and other chains, we must first learn to *bind arguments* to commands. As you've seen, *invoking* a command runs the program; by using square brackets (``__getitem__``), we can create bound commands:: >>> ls["-l"] BoundCommand(, ('-l',)) >>> grep["-v", ".py"] BoundCommand(, ('-v', '.py')) You can think of bound commands as commands that "remember" their arguments. Creating a bound command does not run the program; in order to run it, you'll need to call (invoke) it, like so: ``ls["-l"]()`` (in fact, ``ls["-l"]()`` is equivalent to ``ls("-l")``). Now that we can bind arguments to commands, forming pipelines is easy and straight-forwards, using ``|`` (bitwise-or):: >>> chain = ls["-l"] | grep[".py"] >>> print chain C:\Program Files\Git\bin\ls.exe -l | C:\Program Files\Git\bin\grep.exe .py >>> >>> chain() '-rw-r--r-- 1 sebulba Administ 0 Apr 27 11:54 setup.py\n' .. _guide-local-commands-redir: Input/Output Redirection ------------------------ We can also use redirection into files (or any object that exposes a real ``fileno()``). If a string is given, it is assumed to be a file name, and a file with that name is opened for you. In this example, we're reading from ``stdin`` into ``grep world``, and redirecting the output to a file named ``tmp.txt``:: >>> import sys >>> ((grep["world"] < sys.stdin) > "tmp.txt")() hello hello world what has the world become? foo # Ctrl+D pressed '' .. note:: Parentheses are required here! ``grep["world"] < sys.stdin > "tmp.txt"`` would be evaluated according to the `rules for chained comparison operators `_ and result in ``False`` (Python 2) or raise an exception (Python 3). Right after ``foo``, Ctrl+D was pressed, which caused ``grep`` to finish. The empty string at the end is the command's ``stdout`` (and it's empty because it actually went to a file). Lo and behold, the file was created:: >>> cat("tmp.txt") 'hello world\nwhat has the world become?\n' If you need to send input into a program (through its ``stdin``), instead of writing the data to a file and redirecting this file into ``stdin``, you can use the shortcut ``<<`` (shift-left):: >>> (cat << "hello world\nfoo\nbar\spam" | grep["oo"]) () 'foo\n' Exit Codes ---------- If the command we're running fails (returns a non-zero exit code), we'll get an exception:: >>> cat("non/existing.file") Traceback (most recent call last): [...] ProcessExecutionError: Unexpected exit code: 1 Command line: | /bin/cat non/existing.file Stderr: | /bin/cat: non/existing.file: No such file or directory In order to avoid such exceptions, or when a different exit code is expected, just pass ``retcode = xxx`` as a keyword argument. If ``retcode`` is ``None``, no exception checking is performed (any exit code is accepted); otherwise, the exit code is expected to match the one you passed:: >>> cat("non/existing.file", retcode = None) '' >>> cat("non/existing.file", retcode = 17) Traceback (most recent call last): [...] ProcessExecutionError: Unexpected exit code: 1 Command line: | /bin/cat non/existing.file Stderr: | /bin/cat: non/existing.file: No such file or directory .. note:: If you wish to accept several valid exit codes, ``retcode`` may be a tuple or a list. For instance, ``grep("foo", "myfile.txt", retcode = (0, 2))`` If you need to have both the output/error and the exit code (using exceptions would provide either but not both), you can use the `run` method, which will provide all of them >>> cat["non/existing.file"].run(retcode=None) (1, u'', u'/bin/cat: non/existing.file: No such file or directory\n') If you need the value of the exit code, there are two ways to do it. You can call ``.run(retcode=None)`` (or any other valid retcode value) on a command, you will get a tuple ``(retcode, stdout, stderr)`` (see `Run and Popen`_. If you just need the retcode, or want to check the retcode, there are two special objects that can be applied to your command to run it and get or test the retcode. For example:: >>> cat["non/existing.file"] & RETCODE 1 >>> cat["non/existing.file"] & TF False >>> cat["non/existing.file"] & TF(1) True .. note:: If you want to run these commands in the foreground (see `Background and Foreground`_), you can give ``FG=True`` to ``TF`` or ``RETCODE``. For instance, ``cat["non/existing.file"] & TF(1,FG=True)`` .. versionadded:: 1.5 The ``TF`` and ``RETCODE`` modifiers Run and Popen ------------- Notice that calling commands (or chained-commands) only returns their ``stdout``. In order to get hold of the exit code or ``stderr``, you'll need to use the :func:`run ` method, which returns a 3-tuple of the exit code, ``stdout``, and ``stderr``:: >>> ls.run("-a") (0, '.\n..\n.git\n.gitignore\n.project\n.pydevproject\nREADME.rst\nplumbum\[...]', '') You can also pass ``retcode`` as a keyword argument to ``run`` in the same way discussed above. And, if you want to want to execute commands "in the background" (i.e., not wait for them to finish), you can use the :func:`popen ` method, which returns a normal ``subprocess.Popen`` object:: >>> p = ls.popen("-a") >>> p.communicate() ('.\n..\n.git\n.gitignore\n.project\n.pydevproject\nREADME.rst\nplumbum\n[...]', '') You can read from its ``stdout``, ``wait()`` for it, ``terminate()`` it, etc. .. _guide-local-commands-bgfg: Background and Foreground ------------------------- In order to make programming easier, there are two special objects called ``FG`` and ``BG``, which are there to help you. ``FG`` runs programs in the foreground (they receive the parent's ``stdin``, ``stdout`` and ``stderr``), and ``BG`` runs programs in the background (much like ``popen`` above, but it returns a :class:`Future ` object, instead of a ``subprocess.Popen`` one). ``FG`` is especially useful for interactive programs like editors, etc., that require a ``TTY`` or input from the user. :: >>> from plumbum import FG, BG >>> ls["-l"] & FG total 5 -rw-r--r-- 1 sebulba Administ 4478 Apr 29 15:02 README.rst drwxr-xr-x 2 sebulba Administ 4096 Apr 27 12:18 plumbum -rw-r--r-- 1 sebulba Administ 0 Apr 27 11:54 setup.py drwxr-xr-x 2 sebulba Administ 0 Apr 27 11:54 tests -rw-r--r-- 1 sebulba Administ 18 Apr 27 11:54 todo.txt .. note:: The output of ``ls`` went straight to the screen :: >>> ls["-a"] & BG >>> f = _ >>> f.ready() False >>> f.wait() >>> f.stdout '.\n..\n.git\n.gitignore\n.project\n.pydevproject\nREADME.rst\nplumbum\n[...]' If you want to redirect the output, you can pass those arguments to the BG modifier. So the command ``ls & BG(stdout=sys.stdout, stderr=sys.stderr)`` has exactly the same effect as ``ls &`` in a terminal. You can also start a long running process and detach it in ``nohup`` mode using the ``NOHUP`` modifier:: >>> ls["-a"] & NOHUP If you want to redirect the input or output to something other than ``nohup.out``, you can add parameters to the modifier:: >>> ls["-a"] & NOHUP(stdout='/dev/null') # Or None .. versionadded:: 1.6 The ``NOHUP`` modifier You can also use the ``TEE`` modifier, which causes output to be redirected to the screen (like ``FG``), but also provides access to the output (like ``BG``). .. _guide-local-commands-nesting: Command Nesting --------------- The arguments of commands can be strings (or any object that can meaningfully-convert to a string), as we've seen above, but they can also be other **commands**! This allows nesting commands into one another, forming complex command objects. The classic example is ``sudo``:: >>> from plumbum.cmd import sudo >>> print sudo[ls["-l", "-a"]] /usr/bin/sudo /bin/ls -l -a >>> sudo[ls["-l", "-a"]]() u'total 22\ndrwxr-xr-x 8 sebulba Administ 4096 May 9 20:46 .\n[...]' In fact, you can nest even command-chains (i.e., pipes and redirections), e.g., ``sudo[ls | grep["\\.py"]]``; however, that would require that the top-level program be able to handle these shell operators, and this is not the case for ``sudo``. ``sudo`` expects its argument to be an executable program, and it would complain about ``|`` not being one. So, there's a inherent differnce between between ``sudo[ls | grep["\\.py"]]`` and ``sudo[ls] | grep["\\.py"]`` (where the pipe is unnested) -- the first would fail, the latter would work as expected. Some programs (mostly shells) will be able to handle pipes and redirections -- an example of such a program is ``ssh``. For instance, you could run ``ssh["somehost", ls | grep["\\.py"]]()``; here, both ``ls`` and ``grep`` would run on ``somehost``, and only the filtered output would be sent (over SSH) to our machine. On the other hand, an invocation such as ``(ssh["somehost", ls] | grep["\\.py"])()`` would run ``ls`` on ``somehost``, send its entire output to our machine, and ``grep`` would filter it locally. We'll learn more about remote command execution :ref:`later `. In the meanwhile, we should learn that command nesting works by *shell-quoting* (or *shell-escaping*) the nested command. Quoting normally takes place from the second level of nesting:: >>> print ssh["somehost", ssh["anotherhost", ls | grep["\\.py"]]] /bin/ssh somehost /bin/ssh anotherhost /bin/ls '|' /bin/grep "'\\.py'" In this example, we first ssh to ``somehost``, from it we ssh to ``anotherhost``, and on that host we run the command chain. As you can see, ``|`` and the backslashes have been quoted, to prevent them from executing on the first-level shell; this way, they would safey get to the second-level shell. For further information, see the :ref:`api docs `. plumbum-1.7.2/docs/_templates/0000755000232200023220000000000014161152315016634 5ustar debalancedebalanceplumbum-1.7.2/docs/_templates/placeholder0000644000232200023220000000000014161152302021023 0ustar debalancedebalanceplumbum-1.7.2/docs/_static/0000755000232200023220000000000014161152315016125 5ustar debalancedebalanceplumbum-1.7.2/docs/_static/logo2.png0000644000232200023220000000652514161152302017661 0ustar debalancedebalance‰PNG  IHDR¢e8OåsRGB®ÎégAMA± üaPLTEHHHbbbtttƒƒƒœœœ§§§±±±ºººÃÃÃËËËÒÒÒÚÚÚàààƒ}çÄtRNSÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿS÷% pHYsÃÃÇo¨dtEXtSoftwarePaint.NET v3.5.100ôr¡¬IDATx^íׂ£0 EI›šäÿÿ6 ¡èªX8`ƒ3ëyÚÀèêXrÅÓ<êOáh ·¯š÷¨ˆŠ¯QET¼Š7°FQET¼Š7°FÑFôÓðŸsñZßÔÀåQtˆ–—”×uÒÌáÿy_š²ôåŽUÒSš•°¬ÿѹFQŠäµ<Š®Áhƒ_~ËE¯îpÀËÎÛæ+Ú8š{¹nóòoYEýÛA sr•YùݺžkÝ(Œr¹7I¹åG{PæZDŠ(IÊÒ] ­ˆ*¢4¨‰®ó@_f*¥"ªˆ2U­¶ØÕu¿¶Eùàô%WD¹=¼ºüŠhµ sPåöðêòßQ·|x¸™bŸÓ_³~Ø»G·BÁ; '™> _ƒïç¦E÷E´Jèö}9ŸÏ§Óéòm×]rVþÝ7½Bsš®Yü©Dô ‹Æß³1¸ò†u ZDWµ8'w%x˜ D?jͼi>îƒLïâýƒ¿ù™¿˜u?ÌYWv»[™¢«4ð ‚°(-¢9BºÏð<ì$~ºÅ\Dë]”;Vº ‡,òvÎ[ ¯½5¨9U¨ŠX?{)h½`Ú£~ÊF™Ž-ºwqlX€º\Úmˆ‘¹ö!'ÙÍ‹üµÓ"=¸—‚åsWÇÕ3#dü(›ï.BÎîo“1Ôýn,HòÍ#j =#í*QA‹è7Âüç-f¯Éˆ¢O£À©Ël]l†fä&cfÊÁghvz3¨œ ¬üã(F#1*JÁУ»ÉüüÑ{íÎr•ì Dís<μ%Á‚æñ¸YAÝÖQ±¼KíPK*ÔhjDÃk‘ê+GÁÔéf›0°áÁ 3:M6"ô¨}ఓꈨt×<½IqÔ>BÀž;€L¾”€ýÍϧð÷:l¨`Bt‡— ™~t{@„[÷»K½/|¸…Ðý¨åøÙ].<ŠX¬! 4ÙU ]‹èÒQ»‘¿ãA$¥©ÂÐ耈Œu†ž¹˜ãV¦HØÐÎaúpm¨ÀB¤Ú\¨gª9ÊŽ¼lPXZšéÕ3zv &C´R…H÷  M•u4„ÄFµ=¥Ž·ª£¡³ÂéêëtR`) ,DÆÈÔ üÉ•CWp"wÔÉDkˆŒ(ƒ^$[Š ‘áRè2‰«¡xH$']±÷÷€¶P‹bµF~Ö‚LN]¥RD"‚þžHƒ™ñˆÀngûb±ÉÓσ„=¢Õ "…ûÐy±8i}Ë; ÜÙÔ“7ÆF~™òÒ Z¯ Ì>pÇäE$Ífåå¨w2E üaN'Ñð5 ¢õ b½<ÓåE$“[!’í¾—éf¢£©õKƒh½‚XD WÞm±L'ÕSgÂëVÛëÌÂ<ˆ^W‹(˜)²"ÒxÞø†BLìf¢È’—QÙAJZ>.ÒóÖîtºè:fn‚h¼^Š‚lˆb'±\~z©yœ©ÄèDWŠ‚}Q5:_à= ˆ¼ËŽhCK1Ç»Q^ÿŠÒ¼µLê= #z£óà‚¶È¢ ,AÄÒÏ"D4jíZñÁ·Î–=Š6T°-BDÓ–Öæ·¸ø3ÚzPáKE*X‚ˆUÑ ?i>Â¥øfúªÛ&¿©$¸Q! ò!¢TðRFbq/"¢W&ATˆ‚|ˆh>Bw ÜIéÈfÚHtbêø‹%QTˆ‚|ˆÈÓr²÷Ak æI"‹Û"' .ATˆ‚-©úN®ŒÝ=9e<·-ʆhWÑJ­\©öó\ŠD§|?Ó ]†‚ˆâfºqSŽœ(l\À¦ÆhO±ˆ`sbÜz‘Ó3£Í¾öç;´E Y²©‚XDÁKŽ?)O¼°É`M§{y[@T„ ‘µ-:˜ËD°ÄÞª£ºzýmÛG¬;°Q„ ‘?µ)ÒrÜœ'†‘3M³¢àÎ šµÝO…ÈÂ^OÑ=ó™‹b3ÛF÷ˆ"QKP`"ò¾ {P7s ’€_nz«§î×ÒP ×ð¹[0жio!Dôñ<@¡’/Ûz4B4† ±"e¦cßÁº{q¹-ò€))ãçÖ¡¦egaDXï†3 ÷oulEwÛ§âdÝ dìÒ|õ%Þ¬/íCAΑöW뀆ÎÒƒ8¶%|°Æ¾ ^AÄ5•«ä¤O° t·'@ô“f…hú¯ÉC'°8—T ôLÛUÁ ˆDÒ1¢¬ÿ•BôÇÎAyÈû¦7xzÏŒyQà0€AÌž ÌiT÷ðÝþшàÌ„.7ô¿Rg %çG4sTžú°¹{¦û.#û¨SSÈŸö_Í Ãýøáeù¾ÄÀ_}që¢èboÄ«Òn ‚‹p¦ÞÉOÁ `†vf¶ºÆ•¶ô®ßOuÒÙ1VÝ^ b׋–úžëúsçc&xMlíÉ•Ÿ±t†2÷Q°!¢XßÕû¸*¢âkDETïâ ¬QTïâ ¬QTïâ ¬QTïâ ¬Qô–ˆ^œ¹*^ã›»aøÍe¾³ù"ܸñ΂þží#"šJP[¶¸‹ I¡X$…,Y á[‹`uËtîyî¾óíÜùfæ<çÏÌoæ÷;Âp“ï7óþ}ÎyÎ9ïyß÷Ê×_}¥žƒÂ@a 0P( ìEæeÐ …Â@aà0P“x“XVõ¾­êš¿š¿Â@a EèEè…Â@a 0P8 Ô$Á$fXvUFy…Â@a`ß(B/B/  …ÂÀ` &ñ&±¬ê}[Õ55…Â@ŠÐ‹Ð …Â@a 0p¨I<‚Ḭ쪌ò …ÂÀ¾1P„^„^(  …#À@MâLbYÕû¶ªkþjþ … ¡¡ …Â@aà0P“x“˜aÙUå! …}c ½½0P(  G€šÄÄI¼råʽòœÉó£Þó@Y½û¶zkþjþ …=` =Ð…¼•ç3y¾žxþ ¿€¨6–â*  ûÄ@zÐ…¨—ço3dÞ‘üÍ’} IÍ[Í[a 0° ¡Ç ýc‚Ì;R¿¶PTKy …ýa =@èBä× dRÿa É´Èø<+Ï/œÏ?ÕØîO՜՜ò0P„#ôçŠÐÁxåÊOŒãÙÏYø})†¼¹8¥± `n*gÿÉ)aõu²·¡ À¯Êóp{¾q €Ð•Ayè3”c<‹Ðé1È`FŒ2÷NÊý©"lßö–mù®}‘qfv¡¡gú7HB¯ä·Â]î,ü3êìF ˜Œåë{´ßóÖ-‚vK¶û¼e[¾sú*††¥{x—ô–ŠÐK±¦*VÁÝ_ Rn2Tm\Çù8ô8§ @¿3"?V„áuOç¥Ì§4!ó”ëùFÚÁùÚ_O/BwNúi(# .ù i°W^ŒC¦—œ·S.{IBŸÊnïÎeÙŠi{¿WóÔ4Ãbä÷³S›·ïEèEè^ìD¾+B/ÜEðsˆo!t¬?iÞëãžK¹7"ýµ§\Ï7VB÷ÔQßÀþ£”V3äjNNCÑ¡ŸÆ<“‚DÏ­åzß'Úr×ö5o=§þ]z)ÖCÈ@záԹ¡kaqW†»öˆj‡;¬–uJ´¥Ž&MX_+B/ÅQrÞo‹Ð w^ì껥]»RÔ•á.¦mW¾¸Ö`¡¯#ðEèëŒóœÜÈàç{–”-”/σKÖa)»ýð¸³Ì—õݶ4¼(ÞZgm›÷ýtB—¡¿k­£Þÿ?"+B? ¡7ïâW#8#›0šÁЗ\è´¨÷¤õAê/Bß!Yó:v‚)pžF¸RÁ0¼”ìI­}‘ß— t-Üî"]R©¯v¼ª´çÛFBß„×Ë¡¾%羲ܕï„Bê+'x÷G1!e¼8#GðÖ_ŠÖáý>ƒÐ¥Œ«òàŒx\:Ô=ßÇß¼íªï|.ð*Oç1åb€Žo3PçvyÁ H3úíM%tiä÷H’3wFÊe¯*}-:!Ì÷Bÿ1SîÒïô ” K§dÞ’ÿý‹ö|(ÿþyðükû ï?°t;ïim[K•Smîd~b×¶B!"ïò8 ÐäóþÇ-Þ[tý~lL¤N³‡.ßÜÛdêßå߯}ˆ+Ÿ_:Dÿ4ÓïÀië–T Ü?ãí»|;{I0,Ò#PiŠ¢ è'€ÿÛ3XR.s ãj—%H{¬ú"kmÒŽsbÌ5{~¿-õþRžëžù´|#u0ePzöxÍõƒ$vì's0Èñ™¬g`9ÿEèN2éæAÆ™‚á€Ú]"uÀS‚"ó` J9U&d06æìÏØ ¯°²Q„>‰£;„.ÿiGÏáð§ÌžBIO7¹œª2‹0¾¦M¯Ž“v0úD{µþŒýoZš$æÎZÿQxè„Þ`0=ë©K˜&Ç͇kç×%]É€½‘ëòï y°Îú¹ä;¯Z 3 pͪW™¯.›ž²uœa­B1\—箤7(yÎÚ‰ºÃàì…‚Ó½0ŒÕËò°ýAŸº1‚qK­u6aÆØ¡.,M°óÒ¯KUê­_˜§.Ùù l]S;SnKIo¤bßµ¤ÖŸft^(My[~R_$/¥‡;ÆCŸãϤH ½„üM$Oic‚²A¬§î‘Ì1"™û«¬žj8»!ÿ¢Ÿ,ÎúuYäýBûÐNfÌО)L_ÊùwÏ•>\2Tq£Þ}{Ÿ!uÓÚ=í¡“ f'rKï=‚V!O‹I­ïf´«GX–P ik`ãyR°rÂH‰Iq#á|;“p‰w(b1Q;/aœJ]Ï „zÉÛ&õÂÅòùþó#óïGSÞøýZ–ÜŒ´ÁCèHö{Œi“¼Çf^›¥š!´¡.þ&ÓVEj7kvu>PcÜOñ ðr© ò7ƃ¾0¬È÷ÿyOŒ€¶»G›LŠ“BÙôû-‘5Ó–”PŒ…ÐÍ™|6ÁÉEºÒÖ‹ oדº!vMRþ{Š…\¥Üâ'å0Ç»î:Q ÌÝc23:o#C¶ßkïjÞO47à“¨¬Ì`VB‡ÌQÞt/ ÀŒ%æÈ”7 ï¿N`úVÆØI=sg ôñe›±¶I]gD¿¦x`2¢Cžwta(OFU`$†*ŒéûåÑ–þS!ã;^¼ÎE=Úz:­Û0MðÐÃÑi?c<ÐJžÄY–‘ºuB§/“qÓ–4LSÆ¥Ôs“0TÍËIs„ëZ[ÃòXJ‡úÉOi{X¥,‹²T=M±Y':ì ’ŠsN}ÂSb†»/Ò¶sB¸NÐUyx·)y§IðœPGêùòÍhgHu‡Òh¥ë"åÿ]õ˜˜F^ƒ—£hù‘:®üú¾Yï+Ö1²1áéa›Z&¹·«¡üîØÑþ¿÷¶|‹cß”çºu`´÷„®z"Z}ÖßIv“à@˜ yàE¹¼tRÙ¹ûR„~¡ÀL†g3昄Â1i:2V꺩è7¾‚:m˜‘f,)Ã…”ÿS t³!.cÇÜG2†é,zZêaòœLÆ¢é¤8i@·}é†üoë1‡8(áÌÒáf%³ÖüCy°Bo¿°¶§Ò¶xè‹*£(&äî&Á¡ã&<Æàsyé'Hè“ûs-x5[ÊîÉ'yÖ\—ô‡ÒhºÀ¤PµþJ}Œ‡ž’“Ót£à©-°$ô¿iãÍþNà Ã‰ËÈ“ò=‘'s]RÏ#D_(îBÙAœ…"<Á(r¼c^ãj€&Ä(Û¨\ÞŸ·ÿeé­#ò)Ð)„Þ”‘¶ö„y4+òVö©…ܳ<'‹ÑéÚý!sê QzëÒÖìoFdfø-I覨†b„38‡©?Rþ³pÆ´;S×hœÐýî: \EòZ¿Þº´#Ó).…Л²e¶á¸ÂR2¨Hñ×&ÏRÉz¡‡Ï<í%:SÈ^#æ siOôX~ۃᜑó’¥h-„n6¸{^ú-rîC;+ȱ{È#/ ã¡gúCä8ªä!å0™óY8Û*¡?åÅ‚Œã ô¹ÉU—ÔÃŒjÀe:Úu…¥¤³Zµ9ÌáàëU–)áSk»Iå—IèÚ¥üæ°; zw_ȱš1[˺ùw×%õSŒ‡6Fð;½Um˜Óö¤÷Û`ÖižCRø\Ÿèy2%ÅMtœIäp…ó`e)u…9¬ä8ÑïU”¥·­$IÑ@aÚ!u2ùfË™T”Bä¼d…÷YŒ†ÂÆäüt ËtâÙ©k„—¶Äu Bל˜nguŸ´!$·Ì ‰1RäÇ@è!28l˜¨AÌD¸(Í t €.“AÒÂkiY“ YBîæ³§=#íc<ÁîE`Úí@†»/dù) i£„>zC‹C£ò áR—¦oÌøš‰<0xC#rËäs ÚUµŒ1ç–™zÈp4˜‹Ð2©¨óÝ3ý¦âE»BR¦¶~žÂa×ÄŒÀ¨‚ç©›ù†IŠ@÷]ÃBç]˜–`HÂu÷…ôf\8QØÌÛ)$a Z“5Ò'6™+L¶$®)å§ÉÑÕ2_^3cÔ1Q$“œÎÔÅ_x™Œ”»ð~÷¦d™>¥Ê¸ô©“Þ=C”Zžò™ÈVöm‰ÐÃQ'¡«ÛYG ]&é±F¿³ø)û³ò0“ãy÷µ/潓ŠE ‘XHÚòî9ˆÁ¬@Ö ô&TÚÙ |“12ð,4e½ÖxÊ„–=‚ ©Ð·Qn5£Òt ŒôÉGr‘Rþ9ဥ%ÄÈ/cþ™¾­Ièªat¡CYÉób‚<{ˆo ¡D¦„ÙƒïVþ%0.ZÕC—qf¾€sˆoEBg¼ Óú/zñ!WiëçMñ¡“gLÈf€¥ºàãŒÀê¤q"ï2ë讥ÁÆšŸº~^„>Ÿì;$tÆ"91Gê …IçˆU¾ToËJðk{èR³–†y4+ 1ÒÞ±ÎESB¯ÊúiOÙr½©*ÃC[›Ð™#TÍF„àN#Ý=¸“rŸ&0ýЧì€Þí¸"cþ>ܦ‡nð°"äzˆoS׉!Ô¹þP'ûdƒ½ç®í¡3Þ-ÆëIkŸ×"ôF¼ï+J QuûȰòÍšÇ:6Šâ+}_ºv®äǼì#ß0I̹Ôdí}$Óå¡“º <%ü„EvÒž«“OYׄkªîÅêeÚIzèÔéCd}Œ5k^?o¼ø¶µž!Äx¦ä.™‹¹yƒgË;¬L[Êœ‰0Þ¬iÜ&ÂଞÊðИ>©ëšÌø’ákès„¤JàÏäiÂ0'Ê|Ÿé»õ¢Þ5³ÜMã6#? oªX†Ü5/‚©tKïüE&ÿ~+`Ø÷¥ì9O Ÿ²m2zèt¶¬â "÷B ë¡.i­é¡7BSäð’(\5E­É•Ëë×0Q„n;\h$ª¢áÀþžQèŒÑçŠp5L3·º®§_´Rw[ \3“aÐ1c¶iBÇÁ·I ˆÔC½ƒmloʳèö5)jÒ'(—þôÐSI¸îðY~ÚÒ Y•Ü'e½JÈSØsx³XTŒÛN„ûÉ)ò^8–ìSè°¡A~LàÄíñJÙ׈ò©Ãº¤œg‰²Rˆn× ¯#¡«X»´m $ØÈà+bÒ˜E9غ†-pØ+Þ=ØV†¿3Û4æêù/)ã­²‡3”[†Ôy¿<0úíûgËXê=¡‡“¥®›VÜNl¡7†ñ@f©øF)uí¼GTx:ƒ$ù­IèjxRë÷Š}b–” cÜÛ˦™-l³‡À4ÌÎédèôôµó®Þ9FBWågö`™”ëòüPÖc ïõ^MhÐþžP|ý:rC•Ï´áØÞ1:ÆÕµ«Í“Ýp’Ð¥9±kÿ{($““Û"IÅ{­¿Òfn²Ö™ð´ªˆ>QF 0£•¥ýNê½A cN 1GáÓÖ uŽ]ûþb\Ì;Y´¹è‘9»=6cþω¾º•¾aŒU~Ø“â訧 3ÈIkuü²8Õ÷Œ„Ž1}Ð:VMÀE=2•¹d"%` dŒç„±»+¼+ÿQ.Fñ©jGû³¹>5¬üd\™p/ú&B™£:Ÿb"|Ìr0R¢8À*i¨Þ•ÔdikxŽç0/íXÓ c®O G¥Og$ÖÔDkŠÐ›'Æ(îŽÔ=[ ØòÃkc%¹—o„ÞJ¦y“:±…”‘|Ï™Š>¤'"’ãxAêò>–a2_Tñ5™e !umNQ²Ì˜Ÿð!# ê,å§ô‰¹H§ÓwXž1c\¾a¥ôñ¤^ÆPE½wä©‘9³E-,ëš~5t!'¢õ›1RÃçHŸ˜Ã(ù±úë¤ ¹,)[»‚R- §ò;IDÃ0„û×g•“ü”9Q0joB‚d2èÑ(”ôDHÒp¹-ïá.&|ŠK…Ì`ÁnS¬‘lލ "ì(!r2ö ˜qç²@×ú®“!ÜAñ3GM~þ,?:žˆ 0çT`¾^nr¥…„‘KäofÜš>`¼f´5Õï-]hޤ.¶Oè׬Ck!tƪC…æ0bT 0ø}±D P{yÏIèÝ€œÞ”Ý<,.çÁ¿¸<IL"£[yKùW[ÝP&äC '¨÷CyšE›SˆSʱ*ø©vÂÓIiÓ‹R. ­3y0nÖÜ$­Òc&ï> ÏuyË¢ÍIÿwÌÏ/–Ðä©õ óxÃÑ'¬¥O¨ïdìS¿’oßlcƒòî˜òdòd¹à*íæ½ Rg£ÚüÂ^„Ì!+òtóoÌ?ps1JTæj›#;ŒŽë ’¼ï`Àt×'Öà0Löi B§÷2v—2ÖPÈ‹ÐùØ~—1e,p„”Rèáï·¥\Sø¾)½ (Ò¹6~•1¿Ò6æøÖ¹v¸öákm—výÀ¡€´9Åž ˆ.hßZÿ—‘zÖì“Uy[ûg}íyF›÷ŒßQO;¿éf´eõ9“rAëØÍ½qýÁÖ~ž\Úð#õ,Þ§lBw­'Hç™Ý¢Öj6 ]IèɘÊ$ux†æ»FèŒâð¬ù¾}Ç1^PL‹)h)›žYÇîÒÖ©‹ ã[꺔O°rŸ,m]ú]x•«&ý¢>y¬×VƒÓÏiï9ylò›u>.…Ç¥ç ú¥¥g©cñ>YýE¢Ó®D)—I1ŸŸ¥Ä÷XKèH==æ•«ëïJÈkó„ÞÆ ¡mv ­ËÀaq·2ù¡çzýe®ü°x™¿# Ì.Ÿ`l±«©›Œ-N~=ãá$ Q¶./ZÀ¡âÎŽ€}ßZ½ !eëBX3Äwád4²ÍV‹üÎûKÌ¡”‹È¼ï±6Áƒyi‰zGByå¡ó$;upaŠpp˜r!o)ò“5i "Psy+¸è%=Ä>‚é"tÓQ‹‡ÎX1—ÖÜ @´,fs¢Soäióóò bþ/åo8/´(Ò6CøztG‚´ÉNo¶þ¡/ý§;ñÝW£íÜû÷0dä®ÁÁ{_Ô+ßû¸í¹ý2·]’è™üoÈ’‘,Ú~ 0Eæ pȤUCë옃°[ûú¹XJ[Ô+gÛWïM e!ô›3 x½7i1wP›í–šôv61è`†HHÍCpm1\jl÷\.”]SèßÝs?ªíy§ î},a”Êó<Ïï½/§Ò~ ¡ßRݵG\ÊÔN~r%Ú-5ÒÞse†$z­vE襘—’*·°UØ&,„®y{®Sº„x´Ó¬\‰vKNÚÊÜ<§E¶*iý+BߦÀióV¿×¼ ^ P„.ä %(`­Å•(!ß½«x¼®D;ï€Ì}'íd²ñ‡„~ýóE襖*³pUØ.XB×Âân/ZˆG[›ßÌv5i«–?Å8È:uúv…®bÍMa 0°XB׶ŸÂíèü7·¯õ ÞíÔ@+mZ’(BO¼Öt !¨2K¹ Ç€–Ðçnƒ í’üt&äþ–ÙIèï¢å¡—‚:îªÎÂ]aàp` —ÊwŒ#CC‡òË÷8¨`Ì» Ý–³¨œ„î^Žˆô¡ýpB™·ú¶æ­0Pðb€"ôGؽ#õîÆ¤™·rq=æðáÏäo›;˜CÚd¹æ®3R^óNNä»"ôR üÔ·…ŸÂÀþ0@z#_œ ¤^yèÊ•'íºKO´oH’ìG`øÔ>ôZC7É™†Ãú}Š¶æ¬æl ”¢1 –äÑŽ©ízgß5_u”b- ŽEèFÂBÇu£·gù@è‹^-È€‘Œ&$Ÿi½SJ·0P( Ø0P„n$ô¶ô€ 9º pÑ./ùP\Ѐ¿ß{h JΣc±[ÈÝ÷ªß¦j¼j¼ Ç"t¡oüBäÈuÐÎÞï–Ügl}ª}Ç¡¤jk ŠÐŒÐ…ÈŸG;N·¿Îå\åˆïÂwš—àq‚WãTãT( dc ý½…üçî`Ö.Öfæc á­l°Uy¥À …ÂÀr(Bß9¡·ðº…°Ùwr N ûrÂ^c[c[8n ¡ïŸÐµ›ðX¾W„¾sl”ò>nå]ó[ó;Ä@úΕ¶xèEè;ŸÃRÌ¥˜ … ¡ïœ È&NØË~®f¬Ê(EU( ÖÁÀÿð aJÆIEND®B`‚plumbum-1.7.2/docs/_static/logo3.png0000644000232200023220000001065714161152302017663 0ustar debalancedebalance‰PNG  IHDRc®ù®ÙڴÚÚ‚Ï—*v|  ¢2ͦö£à} `ÖÀQV«ýžý(xq:H½‚::¸ ‚W5±ÞV±«TÞáMû;>Pð œe ï>ø\àÃÍÞ¾)(øÿ¢€Eþ Fl•ç'|ët&N¿ë’â!Úõ-ÏQ”l´L“—³ìÅ¡tö¬Ñ~ Šg|Uü>Éü9£ iêãáPf©ß{iüßÒêй(¨!AIúÛ¤‹¨#ºø‘F Œ—5(øù|½^¯///¯Ÿú${´ç‚Oz…èŒÏ}ŸœxËúù ¥Rq¤´ «HPBoE›ýÉL±£ôi (и¢ 6K°)9Ø‚"¥6E\dkŠY" 9ãq êI0¡ Ûi8gCl™=IÏJ·bü¼/:¼Ðßíór…ÐBõÑÔ½šT)(éL‚0wˆòSúK!ô-QS¨\«PLf¢Å^¿î±•‰› YàZ°L-ZÝÀkSσ$(È*"ã»}*èv¡ø?ÈÜÇçëÛ¥ª€‚R ò‘( a¦„ó }¹àÖ´ˆ¢9 Ž—@¬&9U@i|Œ:•Oª¢€Ù¦ UxiÅAÖ“°Ê-·¢Öæ‚ã%ÈQàgÐ"ØÁÞ¾ú&&§ž DV¿´+e=0žÞ„ZùЊZ­žÀ²UúÑœ Ž— G_>†Rø˜MVTåÖö&Ð궉`‹( £ô @Hm.8^‚N"ØwG“£§dõ%aTTEÁºèf­ì] Ø7£€L#¼Åô%A†ï"ÖÁ!ð4eöºžGX‡âÌ»¨›eC#æT½² ú’ CAÑ!h‡Q'på°2´cÍ\@'[nႾ$ÈPà"ãF‡à3—ø%lWÇÒ ( „/'&¨tUÏ!­€4lAA_HÄÑÅBR¶K¦„ÞOE0 Ùµ]f8›Á¸¸=H0¡ ¦H—«bdÍ·ò€Cxûóš\ßçhþ ÐÜv^¶:)½3.ØŽ˜‚zêJ>SµÃQ»ÑHÍ—`@æ0â-ñ1-n‡ ½ukŠØå|–¯?@é&Ð)½ < °ìp> àwsÍa1ßÛ–œÞÓúÌ9­ ö§» øu]}ëÓî2}#ô qAÁa”zr†¥]긿œŸrâànµ{³tv–Ø0*wÿ T`®â4Š (èIH ló«sR‰S6Qàm«_1º2L µ¢ø&ZˆAl¸íx.€EdÆt=I°óúá](°Wx2i™Œâ$z¢p$Õñ((VUv$ÁÑ(Pfè!ÖÈ~ËdjÆÂ&¢3¸†|†ñŽ$ØŽ‚y.é..H CÒ‘9$ÓÉSô:×FA²S:îp.0ö¢õ#GA©Bh!HH1¸õR„·ˆÒÈî)üÅíðòD›çŽÈG3:4¬YŒkœaÀ‹õ ‰“%ð(˜²ü߯OŠÜð€:èü4]”þ¼[›pÂÏ1“µZŒ‡©†%aŒt(vÑ%m{ ÐèyÑ«Ä#3I†©áÒ}«ÎÝHàQ0c2Á€oÖJ:”G>Å©°ôC)nÞ ÑI]ð9u3gx^=†àç6ô¯|õ"Á4ƒœf· øè–ƒ6häºt2,á€; óð*‘D.þ "Ó1€ |8!îÎÏÓX ›w¥ãKXÐA½OH@«I‰àÃùpQ[J}ɬÇÛ‡ç’N4°ô;©,±1ž-)Æ&«¿¨Ã”œRk2w l^ÔÜüÅû’Ÿ9{/6'[²ºÀY\™k‡_fbØÆ÷Ÿ &Ó~?ósé¦ÛÞ´Û ÐNž}ú-þhÇÏ]bCƨ޴ƒ}OŸ³C_Ëgfž,A´>”ÀÂ!| »Ò3¾žgi4Ð,_s5•+—GìüTáäÅls©à¶Ðyÿܲ\áëtýs%à(ða¤2”E¶„ýPj’›Ä·RŒ5Õeì\Ô®„XvGáU夎øŽ©ÅÂû‚ñö£ÀþyÁS%à(€R3^e˜‚1©àÔ„asœ+JÚ?Rq øöºòÉÚâ¼–ÃP°p®~¶Að` 8 dóê =A$W 7(\¾Zä×hNÉá%“é?fM YK¶3á,´X8º}>½KCåJðªñÌuxž p¬)ì@_FÀÓ‹ö¡À= ?#QåÇX£Ï_tMÕûîû]‹YûS³gK†½ Çà˜KY€:>\R\ZÁõÔ-ùprýôE“.`£î<0ªÈ[÷¶s%¶uJŒ°õ^ NëAÀ‚ª¬Óô¸«8 â “Ñ•A@hdùQwڋȯCt©°s†3ņXVê¾Õ#³H$I´³\“g²>k—WH:š,kà`œ¼î7 kà`]ç°C8ì” þ3ô=jä_ìScèUCwleømr¼ÌQP¨ ,/P¯ñFÚÐH¥:ÒF¯ÍnÓÀ9\ Z¿­ÿã©hŒ‚BuoyY ™F÷j 1 îíθÿ œ¢öÎ^:PЙANéÎ@Á)jïì¥ÿ¦;TÅ[ˆIEND®B`‚plumbum-1.7.2/docs/_static/logo.png0000644000232200023220000002157514161152302017601 0ustar debalancedebalance‰PNG  IHDRb€:WsRGB®ÎégAMA± üa pHYsÃÃÇo¨dtEXtSoftwarePaint.NET v3.5.100ôr¡"ìIDATx^í?ȯŕÇo‘ÂÆp%ŠjvpYEH–¸ !…¬+»EYP°°Y±l‘%•Iaõ&•°—@„bŠˆ…),²…,Áb«`¬n™Î=ß7Ïcž÷÷>Ï3Ÿ3sžï=ÂýÞß©ƒÄ@b 1H $j0$"‰Tb 1H $Õ¨~±†µä;Év‰Ä@b 1pµ0D"Yhb 1H $Õ¨~1åÕb”9ž9ž‰Ä@b 1Pƒ$ÉBwk×®ý‹=?myj ßIÇ™H $Ú0°‹I$±m¯‚þŒ@œÙóEËsô}8†-NÿÛž?,ôükâà8ÈqúË8%‘ȈÄ.0D"瑜²áõ£Ò[x÷Õ#é"Ûš¶»‹I$˜@L"‘8’H"‘x=^—nk‰ŒHìæ˜ÿ­ V»Ò[ÚX²þœñúçšúóÄå1DN>æÞ„¢¿ áá=øÛDõ~ľe›¯ÖÄ`Xý:ÄëG9öWkìïäñL"Á‰„¶-<&}ïNVdß©Þ#ef]éäk0D"qSƒ›£¿“D sÓÉlP.‰Ð-1 ª{RW–IG¿$’H$¾–Ä×^ë^œH˜a}מ›öô×ÉþÆþþ\­BìÝûìyÑžþ» :‚õö,–àèØ÷F,’H$‘Xܾjí(ß[fÂK"±Œ^¯ûÖ뢎ΌJ“üÔvÀË^pX]: 8µÅð®·>Z¾“ëÙÖPÙ$I$µ/Šß,·žN"±ž®×ûÑõbŽÎ ê«3“¾&Ú?y€`å-Ô·Xò’É}–†×sk#ÜT÷aÏ {ú£îÃ?ÿÉþ=oÏ]÷Œj®ú9ðW!$»›¼–¼Ù¾×LA¡Ÿ‚Î=Cd[=ßuÉy}ƒÔWSfD¢s,¾6jÿFþÿa+'§£§ÿì±’îóèïö[åÝî~מÛS[|ÿ{l|àøã­ «Oáq¢;O™{KØZ@¦Ú7¾ êç…1w8¯žPhV½ÕÑÉ{bÀÊ ·á«¾ÒØö¿›ìê+²í]}=T¶¦ÏŠÓãæêïMÙ'mãT9«ãÞ0:ë3LÞ¥¯(´áza~ºÞ*ãRhÿ/ù ¿wŒ›Hoµ?µw%ï}‡<• !õ£>º|# Õ1MbˆM[]t[ÕWÓ_kÃìSßïð£¨&îN¢ï¨2" ºdl½ý7†'‘ÀŸ&wŒ{qÌuyÆj2©×äEôóËHŸŸÓyõýPØõi¯íÉñUʓܗ¼ò"Ê›\7‘°wž³çð¡:¾SÛ{÷£€6¸"Å ùµÙÄû ~ɇ:¶ú”Ÿp»Bw"îü{G‰Í5òšHýª ëà€B='6núð$RcdÀ+N*Þv,dpž ë´¬€¬•b5«¦: ã¯2Žú"&ØS}ÇœöÃYn"amÒ¤þ[gÛ\“ʉsV²Ú0y¬« 3¯FxÕ~û‰N§žU%ÕV»5T 碥›Ô½ßaŽ£0Ž#V–äÎáDmý>õ•´\xŽ„5’$F¢rÐq¹.·¢Ê„+Ï`;ú ?ŠºC"Ñ÷U„âY¯N=å©îiV_ è0;]ÉK$âs8NÅ\&«çöWs±ô{ÿ®&êÕÈ„ƒH|le§U)a(éǵÅcíI"í¢#Âi‰D»@dÂd½ lŒÚ…ûféÕ"ÖIÝfIŒØ°ºh~Dq5H'™ÑýÿÖ꟧M;&ýx/¶úƒ˜ÊˆÄIâYaé*éó±·E#E¦°ß‘ˆ’s–ãÕñÜ•ùC?$´ ™p â'#ÊàÕg 4wõc¢Ý\Äì|åßÙÅð¸òÔ˜ÎbúÙ©ìð-è3C’>ÏI•g2+•µÆo³ì:HOlÐü¼URêC‘ Óö˜þ”`÷M{nØók˜)ð*ßA¹Ïtõ“ Ÿè~û‡ íùá0 *ßQ_Ÿl©½½aOÍþôm{ïm{”Ä*½Í&xu+%³õcwÓþ^š0ÇÆMã¡6k|f<í÷aR©ÆX˜ñÊÕyô¹ íB.B‡™Ò(üÕl“HÄ…Õ›ÕC£K« kÝÚ(…Ÿ•ä¬1»0ÆÝ˜jì?¤v!Û,{®ÄIßµK>EmœMîl¢Ç¨ú*\×Ú¢p-|Kf)ÁózWN}í“Ð=úT_çìâB.‚•%=Õ9ºµŒõ…(¸•?ïHfÈ\F$:e}OOlxBm‹ÝÛà”Þ˜»ÓbÄùRG:t\º@ènâ\:c×}ºP¥4Aèw¦£²;ùD.ŽHLŇýS[d¤Í+\9<{J[þ‡Üg25a#Z¹?M” ÝÒ‚örêøÍ,åDHæ¥h†œ!ìOø–ãˆ=¶ õßjeu_ÇQ½•lÐêVÔgjüOåJFõéœa[„+Ø/µAsDHž–ÕóšCî”^´]u)?Åþ­ÁèõyôÙ{"X¥1¾D’埾ÍU¶6Æ,¥<@LnÇ*øµRµ¿CÇ8lKóÀxÚjíÓ~k lýï/zê>1pq ]ýѾÕö«#+¼ñKÛb}£ÉS¡Q7“{‹êu¢Ü$ÆNìË´±9™g°?‹Ú¦µ¡–H(š÷ÅM_ÎÞ¡ ‰*‚oõ“{ ä[B¿udõ} ÇE·‰^MžÇŒùÛY[ý$§âËh$“2áû!‹¿æˆ„Ø—=Ã+rÉ„ö³ìæÚµ¬Îw †ò´j~€€ÆSÆêüÀÙ–°‰†´SF Û÷©¯06tµ¶úƒ}kŠHx"ÖžKaùZ½vÎ…ØLhœ¸§ÚU ‡vN¬´u£¦VNdÕ5»Pp­$÷ß{ ‘Ðzu„ËÞ¥™Ÿyqê¨û‡Þº ~ægÐîC‰!”9f(êcõ“…ù.í?’ë0{$/æÆÆ…E¦gðM˜.ѯö•t©QKÖñ‡]ÃÛÚ´²&Y®%Ç«Ü'<}+€Ú³Å¢¶­úÁ.Ǥµ1y4w%d‹‡wëx¯%ç$ÊC'ž½ LMŸ$ùòy+WÚ•ý'ZH^dŸ?nÅÊÔû²}Š£®\HôÎê¢> m+ˆ%)Eréѹçí…HàvXßn0"RB nuò"»¨þbu1"!¶GûvŸ9 ¤4Ù¯ñ»Ž~5¯>FzÞ'ÁD”uÎà.+a¢´Ÿ-]È "ô°ÒªKŠïV9•D¢iÅpÚff†º? xúHÙƒÇÎá4õôƒ–u Ùǃ´î‚M’‰Gcù‰Gž•ß;‘½Ñ˜ú€“r®hŒ½« ¬J>­dò¿Ø­,‰ZW/Vf‰DG"<É`%ålñû-ጕu‰O[åyßwL ˜5—ÚàpØx;³ÊC¸)µ¹ô;0îp¢è˜xšõxB`Îhåð„>XÑ*â€Æo¢œ{ïÝê!>+TŸƒþz"¯—0éùÝúýÔõó´Þ‰Ðè/ÔßÏ® }€“š­û¡\×¢Ðú¥§K[ª¿jŽ( ÊF[Åï6]Ÿí$Õ¬ŽwÃê² $äJQ‰kîǶiõG»VãFXl•S‘øä³×‰{BW?»ÅÅɘþÝ'L&]‡D*ÇS}m²‹?@¶’$çJ%‘(’`ׄ>ð5OQ¿6RÎUÈ$‰³p_—y“ ‘#nP`mýk‰P‡O&-ǤF$ºI‚æJ¼Iú‘‰Y§Š+fz›«N 6Y'§ö]åÔ:l’ÕyhD “K#á·ñšž=§‰J"1k“î­¾ÒIO¦œÚEÕwdl,„s¯Ÿ¼ˆMà©à×xoí­Å¹f&Ú3h"AW~ ©áý¼‘•ÂI Yé&UäT®`·$MQ6ûÇ!>Oõ_¬kòÈ–Jø6¤É¥D¢I§3¾€úl”™Db–H ÎŒU-Á®¾3ÉÆ“l©¸r>Î}gH}kC§J"’ý¨qÊ6«f2t¬.£‰„¶7Ⱦ›ô‚÷e7$è>Žš1šq&tâÙ’HT­€NSÍöF‹Ã¤Ø¬Ž´Œéˆ _ËÑËÊÑãÞ¡~ÏáÓ¶Ì‘@QÛ§ ±!ÁFã6#“Ü;â&ØäÔÆs6¨5×–¢Õ]åJe®îÛV§®—ýnË*øÄ zˆDèdM&-‡Ñ…·M ŽaõDë7U}QÄsD®UÎ#Í{ù6†ô^€Þ¶Ñ‡ý ãI2ÕÝ«¯‚LJ —ŠHÐ< é¹xŠ•!OøÊáÓ¶"Íã§ù‰ú¶A¹¦ ¸4æP¦‹`cÇkÂu‡„î1ïï&×wèjT@Sv«¾m :f¿K`¿{V/ý·T7¾öÙ3X½"áO;ö¶µÑmÐPõž0þU'ÑÞÔkõ¸¦W ¼žH4÷×ä™ÍB%1 •Û·ÁúìYåOá€HÉÇÇ:0²…mò¼yˆÍó ”éŠ0a"1ÆÓ·hÂÚï´úhÂŽx]ºÓÜZÖI$š;i›Nœ–ˆHgÝOUg¾©q×èîDȨ[åTN<.Ã.µÑtJ1SuZãT¾cBê±ÒüizǤ6™Ì­#T¾ô\\ÝZrëèf‰ν¿S_cåªNkŒØµÃÞ.ªsÍd“,þ\vßD$º)]­£IÄÜæp+™‡HÐ:#Ë9&…%ˆ }ʪ&jÜ­:¥zl•s0"Ñèü„g‚V¿˜ Ç¢¤¸2§c¾5‘ètM·•G¿6y$|Òq¡å¨¯±rÍ‘n¬í°ûp™=>C"ÏŠl8dº¢S˜u,Öq×"8q NÚ!a%‡s Q—¢aR™žrŽI!t2r:N`UÂ%54ÎÆÊšº:Kð2™4¢:v[`†Ž£&ÿÖ±LHÄv«p9!y Ÿ}¦Ûͨ pÜÂtØù2n¨ý,Á¾Z•m‰Ýƒ±¢ö/ÿYŒ ‘v9È!ö9D‚€±UG(3%¢ØRHnª'Ê’üÒï[L C Ž³Z?k·VßPV$‘ ÇëÂdvŽzõ(–é–ø á$l‚°ºÈi‘HyÔÂdŽD¹È$,=£*ŒêD r;T¦'šSòÉôwӭ玕ªˆî>èi,/‚Hý8A «3:HQåD+>ªm[M "AAY=AÀɽy•@ñ§Ð`Ôø9¢ aÛ)[aÆaGa‘=H²## G"Èfà¸á¨3±hóhJä ü™ÏÞˆ\Gt@íB \ÒOi"% ÄH«d“.Æä§’&2О2ÐYª}8Tå”O@y"Aó=ª’FÇtjcBr‚Â×Àan‘ +å°•¦é÷58)Eåd\E"A¢:(ºAüŠͧ _¸A¬„Ù£c õߊF¾âœŒV"A²ø–,ë9~·g"¶R%·å¤0ldO4ª>…Më÷èlbR_ÝY߈“ÇüäÓd’ ¢„•°•¦#â²uä˜ÂÇtà(aCmX›ìšµ0Ÿ/ŠˆmÓ2TfäüýÐÎŽ(\LŠÜ@‰ÂýVM:Ù-‘¨Õeë{½ƒ§Žó(D‚D"÷ðɶÐk­±ß3èÀÂ0ãÀéD"¤Ÿ;‰HL© ];"!+fùk¾œ)‰ÄügÒ©ß©ŽHÐp—@Hcå^‚ŽMð$©3ªL4{‹j×¶6ŽSãV5AP\Dè:1×e-Sí2Yþ…o™9&õ ¶‹öPò‚d¼«é~:ìa‘‘É÷„ÊQ2ÕI?€’ hòÞvšÜ9𥈄ìÑ2 ¯}›Bòx:[DÛŒ¸´à@ÑÀÆ! ¨tû@õ*t÷º=³ßìðömÆá“¶…­R½íÞbRàF’ª³Ž©¡yõ6VÞdÑ ¢š ›,’c$½…„Œ‡ýÝ3ɤaò*’;²¢¥º¥ˆ•/\ዸ¬ì-`‡ˆ˜”ìÔäDÀ°íÅ»Ø-‘(éÍó»ÃÑv}1"Ñ­šô¡®ŸÚ£Ïr#EÊéÝ©ç?í··íù¼¢Þ¾·íÝ?Ø£ºôá®%Â]„½…eš{á\é…¬ºN :êêd!Š ¯Þ&ˆ]}5¯š­_äÄÆRÎÔkfÎ+R&ÅçU!´¿.{´±#—µá=õ™E¨„a$‰Äìœ^O$ 4÷Øó_öФ/¹X²¼ÈÚN¡t€!Žˆ¶iëÕå "A/qª^Yà ϚvÛ!Žo¶Ê³~‘H×[åL¦$E!ökcJ#KD2á»CåÖ/y|°¯&‡¡°0‰ˆDG"ôÉï%'û¥ëÝf€D"ìn¯!ÂöIçá,Þê$wHvõj…bÑ«·™UÉRW¢quôËÞ}ö+$m$§ ò¢êünmdaõ×í#eó/µØ¡ÕONºÛNÛú‡·ìw.óìk]DÂ*ÿ°4h©?ì¦:ÇÖÁ"Ÿ€qc"AY½õCñHtEʘ<…WI4î)RßDD@9>%Œ‡$uÞá ºBÙsßAD‚œ¤îܾ½C0û^ƒMP» Iꜰ‹’M^"A¢¡xûëRŽ„… @ Îr‰zB«õìe‡'ÄQƒÜ˜H Wc\S@1DõEÊA¢ãs#«Våa‡ÿ:ikMØ¿Ð(ÖF2)‘¨&º'¡ñͶ6L¿tûAãêÞ‚°w…¶Xu'‡È ¨_þf‘mnш棛›‰€n!“Îó(Š=F$Èþ0ê8ÅRõ„—³~”œQ5 ¤ÃA‡2y§ãª6~Š£]öu8úævšV7 ·»=Õ3È‘¹É,Ùn‘ õ÷2^+v2?¸ ¶µIäšl©7ç&Íõ}#_ƒæÁÚ1›Çʼn„Ž¿ÑU&Rœ€rjwèdÙ±Ôg m{>r½u9tHøv0Ù.Z%troLº’ævä0Ï@ß2"áLà5n‘èpE¢\Ó–-9z<ÚE@÷@®·ò5Эy1Éä]?—%G¬sÒnXNÄAG?u4U“ýÝ‘ÊL˜bË¿ègÈÙé–v;ˆÄ'-rFBôŸÀ±¯v\[w'—:ML^M_ä#RÂõbш®oI$.æ¨:"axyÚ¢‹øN¬f ÁVt%#[9$$(t!4Ñ7´8ö£[È\žH &P]>¥äËÚÅM{WwHŠGŽUÚRø•Ø#$0‰Ä¶D",!\¶=”Ô7'…w8&6‰Èô_"÷,íO¡O‹Ž]FË\H œ©&Ñw©’»rŽ,+G/‘¢å¬Ÿ^HG¬bÖDT“y âÁâÜË*a€}­–‘VÄjr&'hÙÖ„€Vr-Xu`¦yüºÜ‚¼ì5GBöøhËôJ¢\’çÎ]˜jŸÕEЋ[cVŸêdmŽ¡O‹žÔï "Ñ…Bé¹÷^)•Ðî'ªN>.ɽj¿;&…~lš.ß1yôjçÇEÛÊ5_[=Adè%9"Û—H€þÍž©­±¡A‘ü:0Ó„•a[5‘Á± ³u“Gï9 ùvƒÉ£ŸÀÖ¸»`ŽD–h”KØ [Ñ;1=ºµiuP»B‘ »@“ºæ® yžÅtØÉ@k?ÙJ’.Ð5êÅ+²OŽöâ­‘¥$Æ~PWY¥Ec‹:“ÂÐHªœµÉºÏ²Bq\N²Ù¶YÑÕ´Ê}I&ìïŠæQ\½EbËÚDC›a7kšLò é!lkÇê"[S’rg‡cr<—Ù2¹ë]{H”klu¤‰ú‚ K{—.Ff#}Á6áYØFm{ˆgqû”è£#”0¡¼/‘u>Ö!z .lUD}´2•DB@ÒWöp½#«:.“In™ì¢¸V;¶ñQ2q™èýïíOb´«aÜÚãY…mvÊŒÚ~£÷uà(j ?ÖOr3ãÂÇ}¥zG"9HÄbز6¶úpKlEeBVc8µºEtZJ㩱üs,‡}øÌÞ}Û´’>ï25áÝ´çv¥¾„WáOõhtJ¦Æ¤ï§Êªµ_Ö{j³úy.Sc>‚×{;=ÜèäI?tÌçÊõzV?†_M–¯.ÏÆÚ¾„O39ŠLx&â9h¢[,au_·ç›~k¿jÝ·_~Juh¬Tçõ »ÊÔ‰?Ùb-^„Qa\˜Ïœ’y÷ Ÿ’éõåÃöõþ´?…yÁŸb"axv'ØY}„U‡íÑÖµSYý—5 ÎÐÐYÓöZ[ÎàØxV25 ¯¾ÒÙчšvéPBjõ‘ë|çÚzÛêx’ŽqM9«Ÿ&³UétÂan!Ó³·]Ó×K‰u´¹yïTÛb ÖV»§}ý¹é-DÚIýeÍ8ŒF€öoS2鉘š~^ð§"A/çAamS,5~´MR ªBèI+2ùŠ5]ºÔÒ~Håäi–·\š¿µR<í– %ã¬]…½oúZÓ‚Ín1©o!“F7k&½³çˆDo‹Íd>›Z`ØóË?/ò#?²èVÆi¿ö5I$ NIGK†ˆí¬.r¾{±D´’ÑXûèå9C`"U’ïùÝa£·ÚûJ’A+Gß.®$ôÿý÷LnØßC’=ý;JÙÎq*B3ÄÄ&Îò(:»“ÚÙáCÉ›}b¨r®”o¥Ä9Ù˜¶%J>VxR‚ß²Û#èÏÚ©èÄißV'ÖGÐÕQÛèÉ‘¸@Žó¬.’¬v“—w€¬}º,¦dÔ—~÷ʉ(ßJ$"ÚuüõË|6úè¥Vc‹çB¤î㾊¸]žŽž|ì.ê@õeýéóÍAy޾—åŽm‘(Müô : !—ÒxÚ4,[C"ôN­¼–÷’HÃØZÆ8ßÍ1N $öŠ‘8+L®®m««t‡Äó[*­’H|ºE›“H¤ƒÙw)3q—H "ñÏ3“ëû^@Y]Ê0žÚ:Àß²÷Ê¥å+‰Ä"'JmN"‘Æ\ÂHþžI $–€‡HhKçßO'%—¹³ní©Î()'äã$-J“1„“M[Úvún‰t‘xʺO‰Ä€˜Hô•Ú¤õ„=Ê6Öõ±MÇÿº„¢þ¶ÈþêM7)ñt˜–-DL¦")‡¸G‚ê Ë¥3I $‰ÜD¢TáUù݈„¾è¹;~µ‹†2"‘†}Uì,û‘XN I$,Qd ÈF$ô¥´ÒÙîÛVæÙ-!·6Žoˆ[â'e'~‰ $‘˜!R¬ò?ìÑ×õ5@]Ó?otÛ;›ŸíN"‘N Å 仉ŸÄ@b I$ D¢E¹k½›D"ÀZXK9‰µÄ@bàÒöz‚âø H"qü1L;Ì1L $ŽŠŒHdD"1p0pT”íÎÉ31p| ä$r&‘ŒHßÓ™æ&GÅ@‰«A$JßAéï½xå¨@Ív§“M $ûÄ@‰ƒ‰îT‰>-L¾Tz–†¸OCÌqÉqI $ŽŠ$Ç'/@!¢!Âñ÷Gk¶;mb 1Ø’HˆHtWŠßßÝkñªýù¡ƒD #ŸÙ{oÛs£«Ku6]wžÆ½?ãÎ1É1I $ÖÀ@‰c š A¶9NËlòåÒ5@ž2Ò™&‰å0D"‰DO(’H é—sŠ©ÛÔmbÀ‡$š™M)S\£GöêùÊÞ ÀÚ€y¦\ª$TP$FI¿/¾ë ž/g}0Û4¹aÎÖDö¢)c[sÇ¿°çFÿnv)7Af„–ù£¸ŒÆêOˆycµD¡z€ 1Pqs°Í½l0Ö¦ÈÇÝØÿæ°´Œ©¾ð!ðh_,| Æ?«(cöϸá# ·w»Ç-F Ô‚„ç¡€Áˆ|rÃ?¤»Ý3˜(íÊ=Â)€–òùi<¢º HÜ™êÖ€`£µOJ¥]u!³Ô Àé/b¬•× ¨C*L÷Ž,N·»·iä/àü(ýhK9ˆRY-û±tZ¬õ—u¤©¨tÛAx6e©…- Iƒ' !ÉMØ™ÀS¥íu€˜sªÞX õºä@1j[D(äJ´$xÒ³à+2µãäFá.ñóRܦ7‘D@èa¡ä…ÞRÇŽºèÔJofžJvŒ["`聆%åÆÒIbNåœÜ0­#KFI>4ï þµÁ†oÆòR°Ã-Œ MŒÃéjÆÜ9“cLöy!åýbU»{ ´*ˆ¹ÄX’[¸;ÆšØd]aä±ÝH·¤[Æ.ã‰C‘ç1×eérÀX•.ëȆ}¶©&ÅÆ*׃ÓS ×IŒ¹š°ìè»qì5æ\±VûL,˜Ø*f±Õ@(m‘‹£°¤9Ù)ãM†žÄ7t”í8½ú”ä™% £#PG¬¬k¿¾ÌX¼²Ì}ÉE„-géW:Šò*uj§þ qÌhã _ºõ­Œ¼K#yû/#\nzÆ~r¶jÞ÷kçá·2[~‘Î)FÃöuƒÜÆðÐè<Ì͘b—HM/ìz=pÍ߃?ø ÷§Í≚·Ÿ+Ƥ„@ëxœ›ÎSDDÐñ°„ÀDA‚ÒÄìàGF@èqã Dg,}Æ’a·“HQ~¹iòá€'M?ÒBØ9±O cÉ/ -Ò|0…©ôž}yØ«°^çCê´—­b¤µlûn‡€ó~q„)\h)x{¹©çÅ¿Ü[¼uü ,ý¸ýhcñ”IEND®B`‚plumbum-1.7.2/docs/_static/placeholder0000644000232200023220000000000014161152302020314 0ustar debalancedebalanceplumbum-1.7.2/docs/_static/logo7.png0000644000232200023220000002274414161152302017667 0ustar debalancedebalance‰PNG  IHDR$hÁöëNsRGB®ÎégAMA± üa pHYsÃÃÇo¨dtEXtSoftwarePaint.NET v3.5.100ôr¡%SIDATx^í]=ÈfÕµž"…ÍÈÇ0¢aƉWA¸QâRµÌÀ…\.(¤°¹b1]$•±°ú&ÕÀ„D %…),’B‚XX‰…¤š2w=ßÝg8ß™sÎ~ÖÚkï}Þ÷]Çy÷ÙkﵟµÖ³×þ9ç¾ûî»sñ„À@` 0è‰ #AÈÀ@` 0èŽî èÉÆBvÌÀ@` 0° ! V À@w to@0Óm0Ó‡‡À@` 0è‰ $ÁŠÀ@` 0èŽî èÉÆBvÌÀ@` 0° ! V À@w toÀ¡2ÓsçÎ}OžGFÏÅCÕEô{³“‡‡À@` '‚4dʼn„Ü”ÿ~#Ïw3Ï]ù·_¢\OP„ìpJÀ@` 0ÐAH!äùjˆLÉÉ;­òÂùÀ@O !iGHNH22“K=±«²EÇÿ)ÏÛ%Ï®ö=ÚÁ$0Øe !i@H$8>£$# %?ße`õj»èíØ ë3ª^m¹‡L«”ç‹JϦS»<æAHÚ’[† ùË]V¯¶! Ü {¹‚×O ¾anÿÙÜ¿…iàß-ãïÌû© $ +ç§ÎÄ06AH‚ì’³BxÝ%¼ÖnkB’N›<)ÿ}Až«û~ªÄèt‚ØÉ§ô÷ÒI¦ìì²¶ÑEý„ }CÃi>ÄàCÂ>ûÙgsB"†ò yþ5É|-ÿÿ„áùÍHäýIžG½dhê1dGàp™88Ñã‘<7ä¹ËŽƒfl£l?絺Lþl]lÄþLƒÓ<ƒ„ÃÏ¡®ëòüÛ>è(úp86Ö”ˆ¬mî<ñžÈùp!ø€ø<è%‡­‡ „“rAHÉh&úSvØqr‡ã([޵àôqyà«ØLÈm){¾eCV`¿Z’“Œ‘“©˜5Cþi E.Õ)m¹¬p,ãv!ñ%$³ãÐ!+û«¸@‘%$WG£}À@3B"Æu‰0°”(UêÇuìßnŒü€èwìw$+ärð%Œw#0x`@|ÆÓ¤ßø—‡¼¨#p» ´$$ ã¿\¢1à—#v۫´UÚc%$q‰3I!°qJX˜q2áÀkb@á7>­Ùލ;pÞ- In¹¦˜é‹3Çknª`Û¥hŒM—–Zꤗ¬ $á\{aO+7I`U‹™}(ß„ˆq=HƒOJJÊh>û± ú>·”„$2$Mì³ÄîâÝ:3I½^·­×& ÊE”?'ÿg­Iö}Ž<ݺ­û.ÀG,Ù8“À}ÇT­þ!Ùvà¬5î‡^o+Bòk"¼Z2Rÿ1!Ãíh1ÛÖBR´Á—mß!•#ð„$IŸ˜³» $AHrÙÇß›Ÿ×—D0øq‰‚¥þÜé›ã–w IÑ_K[÷ýƒAH‚4ñ‰9[ B„$‡‘}ü½ºñ‰a±÷?˜°È`ȽÖzÉ¥¤û–lZ·óä! '¿+8BXݬz¶³!aöv°ãÍ]†6ü_x*©KÚ†k¡©û/Æå˜º£ŒÎi±ãzÕéÕS_2F×äù•<xÖ;W—ÈÀ½Eu£¶,mýAHúaP;V-ʧ8òb#Y‰<|‚à±òÆ2ZfoGé ›ß‘Á¦ùÉ#!‰»*,ûgØZ‰àûSyÿJþþT­v€ðÈóþHÞ_åïÕ’§­7IØ 0#•Øì⽚d6'Ïø³ ìßÓâ×Z¾*!INfú!½¹lAÑÞ‘ÃìÜæEƒlDZ!ÙÎXœ™ÉLLƤÉÅÉÉÏɃ¯ÚD¶$É6±j ´–÷€Eyæâ'Ã5KkïHX͘“[i’-©MHž#ƒ€ù»-R?»ήé¥h‰áZ–lŠš7P÷¥>‹‘!i˜!Zq‚cr‚LÆJ=Ï/8Ý*ò¬mö"$RÏyy®Ês]|xú¼ ÿfÞ¿gí_¼·L¸ÒDþ÷„¿2ÇÍÉ„K—ÌIXy«¤¨&0¤“Ç„R‹n%•úÙý#»”!)º“%Âá 1<—å8/<ÃçÒáÜ>’ç‹ôÌÍ:ï¦ß°&ùFrŽÕ÷Ìé€Ä"MH¤>¤ý-º\{çbÎ&+ÈD{Ó²Ný<3æÉé2Np ˜š—p’¼×Y H9àÖ}šÛáw‘Í~râ¾å]y_ †­}£ìïØ'ÛÆ¥rRÇÅ ]õ"Ïê×Ölñ(ãKJû9­_êÃþ¿)Æ dÝìO“¼òPÖer0ë£KÁ·JÜÎÊ,בÁ\?8µ]Y²©ÆDE_–ŒzS.p™Ÿ PÔÂàŒ±Sý`Û#m·Þ»֎ìþ!B·T?'õ,Ú†S?ïíß‚•Gã‡þÀŸ<ÏŽÏ(¸_0ʃܛZyåE®šÈ;/Êó| ŽYû!ï~êІ)†W÷ÿUòk«™j‘y\ÚÏI¦û7îêQï’w°Ü"¯hrÐ%C¢Hð5¤ÿ™U–õ=F@¬vÈØK€cßA`æjfùìØ°†®¨/ ‚ì4 ÈƒÙ±X(GoN—÷±)p¼IÅå¸Òç͈sÒMH’N-/§ ̾-. i#Ò rÀNàçÆ §3R–9ýº†´Õý°Uö$a•›®da´á6l¼Ê ípµí4¶'ç¸<~1©JÙ ÈêTêÓâÑSÖØ~(ËUÏ ¨däŸd»³6"õ˜w6LH†ñ©6%Œð´ ŒÓ,6 éxy;ÎŒÁéØËÃì ƒßX´ÒÉÃcÍ㘠ÀüZß¡ $9Báõ;=B¢ºw õ Þi&"ÙÅø˜úÒ¸®b”ôÿ°SØá;¤ÏtÛbàFH¤áØT„àË2«±BO GR~|gÖèÀ6q¤ªÅ2…Tßõ.}Çf®'å¹.ÏŸIà-éûAÐϧzÇÅ®¦ßþ®5Q:ý¨À …E}æÖg¥½oÈcY¿‡½¼+6 Co«é¦ÁaìîÈß™`:í;ÆmÆø¬n¤…ŒÆc Ìhåâ>ƒ5ýŸÙ«‘0“/8òû&3òoLfäÌlRÞaÉeR"ía3$¹´:6“cÌÎŒqSŒý_2ã2®?›•áó²'ƒ<´ >m\ÝtdE_k«-×À7dæ6Ò¥rèë°Ù_£OôuÍ.ÎìÕ²L†uΊ…ë3ûf¤ü1ñdº!á à ¼)×›%I@]ÃIŽñ‡£j|Lê*çȆßáÈ+ÏãlÒ–Ã@‘z¯ÍZ奬Cë¤ð<+_Êâ¾ \ìÃŒ•~de'§ÅÈ¥3$s²¥oÿCöm±ÓD|eéÎX“•t¹ñQd"ðS:•rß.”… Î.Õ‘Îpê@±¯,·gd6»¢°ÕêÇò¥-%„ý£o»•²˜œ1㸚•ZÁ'²PKã?• æÓTã6Wd¿Ð,3¹LP¥ž×r—ô‚e¸ûöïÈ¿å2*ƒ>ÏGyD-7Æ÷‘mø'…o¡ëšÏ¦ *lÛ“äÔò÷*K '7ôõKMõ,+mÅz4«ó—­²EÆMRŽël””YJHØo7AÏO[u8³|ÉnR+úLÃŒÜV¯ å˜ÂÞû0§¼sL´gM&ó>Æ®Øù®:f;!AvñQ-®ÐBoè·i¢ ï±G¼ÿ m{F_’ý*úÚü„i|Àœ¿]Õ±ô\Vô^ö$1‹2É÷ÍØ8£»Üà§ÎçflÛz9÷û? „$»©17fÖߥ­ìžœ¬2†÷D;{q›’Ž©ˆh21ÒÓÞ©…Ì ›­pÅI–ì>›æMÎ0ç|?Oä…™®~`SA‚0³u_VÙ‡%C‚=挛ÂWýNkÿŠºoiëÎ’.Ÿa}ÍL9* %ï1S]Êf/Èê¾E’A˜4!ÃLŒxëD³}®·¹* qèƒ[Ö¶"¸¸|5YäÝ" ×å4)«!ÑŽÓŒz+„„ƹŒ³Éõ%)b²æ –Ïl’Aί=Çp2ËÖ—l¢ô‰=!s/+Åè@á³$•‘7"vǤݻf¼H™sX¥Û!2N2r@nž!ÚBùs’¡OÏhÆHCHGàI¶P—ÊÐrŠWâÐ÷jNŽh+k¼ôŽûLÅú$“}ƒaÏF Ã<ƒœžr¿·’c `E3˜iß$vlÛ§œ'ýË‘ fí(Î÷ЦlY‘¯!$°KlÝ›dÆòs<…ìEH\O6²>`RN•’wq‘Z.fæìþ—¶E)ËdÑÍ“žÕ=$Fg“SÐÖïMH\ Ré4XBB³øœ|…ã§gÔK2 ã=BR¬Ç Q`13¶{šŒfºÈ€”øõÞ‘ÇlvÕ稿BòzÎÎ4¿K¿? uý[ï×[¼Iýñ¬"#œÜ6ÈËUM.EÖyrK¨æ=V9B¬=•8‰­½kEfÆ¡=eãìYgÑ3½ v.OŽÅ8)𲆫ÕÛLÖ€Âu©c†Ä5€&-jb€~*q2§õÉ ‘Éf \²ÆñD_‹ìb¿lfœÞK„$K¦UÄ`ä³ÙØÎÙ„*Ë5’ÉlP¦±q÷™`ʜ秜/ :–Ã1+õîô\PQâ Gõì1×öwEpq%M"—ÝKòÛ—¹r,¶Jd¤àIÙD©cëMHÌ'‹8™êßäÓX2Ù× E’ËfH¾õÄQ’­9%B‘1…tÍ+|ZÏ ‰z sbûìI¢©]˜¾Ó$:½DúRý$ fŒ3ä8Ó¾Ç}ABp)ÕÕœ¬¿+ q‹«ahÚ­0^oBÂÎD‘*¤×;{e.Hc•æ-‚\û›ÈdXOBb^[NòiV·“ræMÑR³Tä~T_1žE:]™Èg7·RBá©úXûPø4W¿«ÄiQŸ ˆºù‡Èd–ŠT{bNmœØä޼åÁ¥e¸¬LsoÅ”á&=ú&¾ #dreÞy¸Gåey.—6¥ÎTK6šº½Ë*Œ×›`Ù†Y—ÄxÒëÖ ežã§`= ‰iF6±}vyolû%ޗŦ9ó³Íc f-B¤å¡ãÇRŽ=æ_œglþ˜$= •eZ!ì„nlÔ¸­Èdî­Qu!Y0\š† Ë”ÓÚ%„@8! 7±VË€äŒQ13@[Õ˜“¯ù]¡WWB’Èî é8Ì•¬¿8s!r¨Ù¥flrew„ïu~j'#Ÿät—û]d2' Ô³Á5¹Šñ¬EHØ}$ð[Ùût¤ ÀPס’âñC,e}Û¨\ÑEpsR¦Š¨’Á¨¤qÌE-C+š)•“Êpœ9ç5ÃÐÙýÐM±óÔ¶o2û<î¥W‘ËÎÈÌkædßöxN3æq›`ôUvS¹âþJ=ŒÌâÀ2égï ‰fÖÝ÷¶„$Kª4¾UQ—k;2kTDa!ÁÈTLOBÂPñ®pQ&“1Ϥ5ÀË••¶²zéÚfE[݉éôBkº3€5ØÜ˜æ~gC®Íï"“µ?•ƒÈµAÓéš‚Ïδ¬Ì~''ׯ 9`ƒ³[PSŒ§+M0YW™…Ì¢<Ò-›õ¢û؃Í”Dé̳®}2»Ò×`‘±uf/càÞN…Ã2/m±«ÕÛŒÙÙŠÛÎ~Œ Ù?×±S7¹d?·/K]ÌFe·%W…=¸ÓQÀaï· Ú@Ž››±cÆj¿Æ/}•*}Žþ+ìv‘Íh1íR`”¶}OBÂÎÐLéö.öži3Ú2 ‡  ¸(m;“~YòDŒm‡Ü»@H˜M®ã-úcUºbLo7̈L×@É`Td2§{Ü›ÂÜdÎk&˜ÇÔ"¨½ sßUfò¥Ôrƒ=¦ŒèUsG˲˜ÈdO÷Ðò< ;C£Ó7Ö´43€­Ê(6u|®f»mu .£Ù nèÉä„ gxêDJu¬˜­¸}7Id²Y·e¢^$–ÌV`,³K ìX“¶á™‘a7•šlé·BÏ”Íõ¹ž4$mÞu™¨!añ»p[A õKgd< ãU\Œ©ðjÊñL›Ù¬ƒ›3³´³Wp·•÷.v?Œ9[8ƒ3êx¹Kï‘cæFbÉÀfΤÍõUd¾FâÓkÏ `ªù;…žYBÂd™¨l ƒci?»ßˆžÁ3rw€xÚ"“¢÷¬x&ðÒ [p '„Spý¼: @‡]ͱ°}è\Œ„Ä4Æv¼2$Í¡ôqöîë‘Éì#ÃXºÍ|(—%1‘·K„„•ô©)iyìÄÀ-@2¾­—lXŸƒvyžc–õ(|œ96XåÊI'¦T”*Í× sýÎý®pئ ›“¯ù]ÑÖÌ­4Ï|[’4Cbö9xLÆ1¸c¬f2Ýf¾ BR|áTÂÏ ão錥è= Ⲭˆ¬ióî{I¹Tf‰õá¬L]¶Î\92‹Fß@îBH¤Ql01_­`»nN>7Ìï çYtúˆiK®Œ¢­5 ë€w…0 Ï=ÌŒÌýž›˜QÈìAH\lc#S(Ce¾>Üe¯ ‘Å—„Í,¡RºËùdÍï&$4ñò"$7 e”îaÙµ›Ò€ÁaÉÆík»ŽÞÅéN–k4„Ä4ÆF]–lÒ ·úžªQzø2Ù7·M´#ÙǤl7Ì(pêr fOÖÿ¸ô³7!ùš%jÓ¢ÔÉž’,ʤ°ùM×M´½–lF)„±µÆˆâE-Ÿ²òŠ I>–—SÅ¢W;sM­ÛkV¹r çéâÈríYû½g[E¶æØšÉaõ$$ì1x󩳑ócüº¤Â'DrË„Äͦ;lúßD–g=KÐéõyoP"Ø =™’²'„R'בÃl#¨’Q'úèæk:“ Öþ©e"BÂÌ¡ü"C•÷™5yÈÁçÉ/äÀÚêwE§¬f»musô#cb¾ySVK'¡˜ ÏâE³Y°Vàb’fzàô I{D ìÞs°2e3 ó¦<És—>—®zOÞ}!= 4o¯3ƒd™9È2ÆÁ–é\F3B&p?æ=œB¢˜ P'׫®Ï=g®.RŸî÷CXú¥pô®3 ¥4;‹Ý-½£è›ÚùJÝì2‚:h°:P`†rHŒÜN2‰0Y7øìÛÌx-øE渨š¨'’þ5aëÅ{·2šŠiVý-è´‡Ìz„Dœ±3Uªó0z×C­mYC×_ÐVf6ï––eGª;À^³–$÷„°ղ݆/‹™ $Š¥À^’„+&ë?P²ÔÈ‹Wáf $½—¯!|L}+U »“¼7‰ð”_›0Í'¬äca³ÁÅ5³$†ÄÜcSä{9‰$—u¾411{Ãä¢Zv$õÅŒ*°dfŸ=dD†DðòØTzÁß0DÙj¿Ÿ”» C¦\öùD†.¹â’TÎOO2л.×Àº`t É3cõ$#Êà‚qS/1,è繆›·^“n™S Ù}M(Cf2«/J;zƒ2·JHܾž²#ìä øÔVÂ1c“Ô¾‹Mî©"}š¸Ÿ ô½&\µ ŽÊ’ úé6k+ØCPíè™ì¤AÄ¥Ý"“™%AfñXõp£e)ÌÞÛÂ÷Åaš2kä,ÔÌR‹“qyfŠÇo¤Ë $gOÀ˜¿6K&ë[TïíXñÌ¥~Yû—6±„Ѽ)^c'=|M'™õ–lËÚ÷,É]¸?Éó˜`Ö²iÖ±ˆŠgÖ¶MßS—”]%òØ+Ï] k°R®ø:÷‚ltûþ™HXbîä¡2-¸Q`¦+“ÀÉlˆ„é%°œ.¤.öž—o£ˆ›u¶ÜnÂVbzvÏ &+¤®¨LK Ìïd{0v.[¤öfZÈtËÈK]ÌdRŸX:öû˜Tð¿ò03:ÖhæÊýSdྑo¨‘‡zïÈöü¬¥ÞÆ>á\V& #ìrÍ ³h6.ò~¨Ôë v?ýý\“.1Idm±úr‡´åš¢=&uÛQµœþVðò}…½bÜ ÿ"kðowåYû˜&•ì$iÀgÖŸS„d”že7=I‰‰Jg5i/·àU2H»þ®"À¬91€ýŽ<×å2BoÈß±o‡u~O*Å7ÃÚäÝ«I6>OÀ¦÷—²q Z¨ýAfËå›HR%Эe ]f= ³uG’N1–øü‚f,ÇíÆ2ê»òàóÐç#„LN` زdMÑVàõàSK21&C?QíIJ²E&ÞC›ÑÏS™ó¼"# =\Oò¼–¯=£ƒâ¿£Õåñ\Ûkø;‘ƒL KÀsã‚€Y-3"uɃ¬?ô aÍê£ðS¨u¡ÎÙ ÿDæðI—œ–~Fq`>sIæùQ?!SëËÇòŠ~Bæª%$štéÀþL7kJC5×»¤fkØ.Õ):‡ãaÀ­™Y1õM˘¯:WôÁÒ.¼S|ÿÂd ‚¹æz­­w¥M×jâLê×ìgPëuô©iûzßFà‰´9­,¯òf[´bRôa™}Oûû{©§j¶¼²¯™ÍHu’Éž`²`îŒ?U’”ªùDa@%W k’ËÆ «íË{ °clØh"¸þ°DŸŠ>hÚ5.ëJH’MYg…Ây—è‹y7 EÒ<í!ÁL¶Èl9/• “à%²Ùw+ûš $Ì@(á%¦Î…ËÊ\6ËYÛ¹Oï)ÆöôN ùƒ qÚuÄ9Ç”:öl˜×úGËŠl–‡ seÜ IÒ%–E0«cÛ•]õÂfzLrc· „¶ˆ½xŶè?"y0QÉé¿Ã=ê!—©Cá/™¶OË!!A³ÁË´\“4Xš\tÃèf×Ë( ìÞ%WòÖݱQÕ²ÖŽw@<Ý6*ú`qîK6SÌHûŸÏ8à¿ÊïMîБ{림ÍXp Øô„ûeÆ3ü?žaƒ¨ËæÐ}Ÿä€‘1;².Nwõ»ë}JøÀ&Ùaî°©acÌæJà )߀Ýî‚N¤È–LûÖœ ï‚®vµ–=$,+,J)“ìÞåÏ]<ïv—ïöz}2¸ ³Ãê{E]ßûØÿ yHÌæ² ¬î¥?ÈÈbÒƒŒÎ‹ì{QÎï›95ui!$Læ3;óm—ò.{Iµ£Ž5•¾Õºƒì†Ñn?Ñ®ÀO` 0P‚ !ÁEg¹51¤Íëyò.{°ËG¥J˜{Wúr$î!À}X®žá~€Çsu´ú=I8“VX 9µÀ@``Š!‘€ÅÞCR´”"rn¤çË-:¥J×6bIé0ïþ! ᩨ/0 °Ðöâ ¢¥ ŒÌ×<7ÄçÖ9Ù¡ ÄävÐj• BŽ£¶¢ÞÀV` 0À–°ÇÿŠ–R$02ËB³'=rnñ»´_s4z $ÝO ! ‡ÑÂ>BFà,0˜Ìk€!‹ùºbÑRŠÈx˜X®Aßäq_Eûçöát%YAHÂIhüA” ¼žÐfH˜½·JˆÓ9!)ºã¤¤}¹w¥íšOOOIIÑ×PsmËý„$œK#ñ{`$0¨…-!9&ÈBÑweÈ€^´G¥–2Q¯q¹f &Õ?ñ½Ö÷ $áhjÚFÔø Vc °rw|®©ov éÿ/½É+~¸TN­÷A}®AH„ÔÕ›¨7t ¶‹•ó'N¿g.DFŽ˜¿ ܈…„$–l‚¨l²¦CÆvxŒMŒÍ>a@åü2÷ƒ|ì¡‘KÄÖ2$ÏyÈ©UG!!)&t%ýR´½ëæÛ’>Æ»áÀÀÀ61 %$W$háxê”0|(ÿæò}©ß*øz”t]Ò`@¬êqÊ&²!*ûcðe¶éhc\b\y ¨¢Ükò|$¾*yGžg½-u>$ÏðÅØAÎO¼åÔ¨OÚý*±f)Ôuo ±Ghh÷«5tuæ 6t: öjB²¯Šðê—õ§Œ„¤kö'e¦æ²_›Û|ë5VQO8öÀ@` 0° !©°l`X¶ÁÕ…ž†!òo(ˆˆË¿÷loÈÞŽ‰±ˆ± <0„¤!Á>˜÷Éý7MÉöûÈó–ÛäÁ©¦¿mfK¾‘÷Þ•çzª už÷fÔ.0 ‚T $ƒIp¾*ÏÛò {n°‚ø›ø½‡Á)öŠäûýÓ} ™‡å¸b¼c¼û‡ $ ÉV &ÉþòV±í ¬,‚&!ù̸DÃdL"Cr€˜bN”‹à ,a ÉŠd„%Éb*‚L™À@` AH0x¤ ­Ø€Zã¹X Êx?[` 08< !9@B†~x†cc lÿÌM|ï"zµØIEND®B`‚plumbum-1.7.2/docs/_static/logo4.png0000644000232200023220000001006314161152302017653 0ustar debalancedebalance‰PNG  IHDRý` [“sRGB®ÎégAMA± üaPLTEHHHbbbtttƒƒƒœœœ§§§±±±ºººÃÃÃËËËÒÒÒÚÚÚàààƒ}çÄtRNSÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿS÷% pHYsÃÃÇo¨dtEXtSoftwarePaint.NET v3.5.100ôr¡ ŠIDATx^íÛ‚ê* †uÔ9ëû¿í,µ…ü!°…—]W{Û’|$„ãìþ¶¯kÝ몾iþ·ÑåFÐ!ýïÿw|e>muïþ[B¿CïLR1ÇÿoË«néšVXµ®ÆÕJÛèW3%tÜ|¿…Yµ2;ôýß}‚)[ ¨fòiûùF¿‰}7ßobVVèæû3müò¾ÿ{K}ö_3͘~¾ù~eƒ*ÅÍ÷}JÏ.UÅÝèW5§ZØ|úD©îˆg£ÿôad6¿%¡¾ý' ÿÞ*íÙè¯Dÿ¾Âþ‰•ß»µ_ÇIùªânô«š³¸ßÿ ÿM| ¿høq­£ª¸‹Ð¿µóýY•ûÞ¥1Ð ¸ˆœ¶egi õÕAŸXåïøË‡6hxâÈr–wE¯ÏQ¯ÜªM+õË5Ƹ[EB˜C¬^k`‡”þlMª="Ì?W…~\¶Š• µ8èDþû,÷ïèµ…þ·ØôµÛ½‡îÈ{xÁÜ4t&tsWµC°dpÛ)£ÿ› ¸OBGg(ô£>~la?ù~þ­"­3jQè«-)pó¦EoZ'{HÐÅÓ×yÓàF€j/JãÜX²Ô™’þ%Hã¼hú°|—–•ñüñ±K釀¤Ö6> 8h’n Úþ$P\?r¢^IôOH}›hS $ýh©(õTúÔwº&Úë3Ä¥ôCëQëBÿ8¼–zþí·P„ïáÏÓÇÁ“'¤N¿©’¾`M³yZŠú$Ï ,sEÍ«úãÀ“ÇkÚÃÐeŸSOÒºøA *çû‹Lï/Ñ‘zŽÎ4( Ogèï•çç´/|r¨ ÆZǪп~ǽ8IçiÊY EWÏJ6QŸaÀJP$ý±Zúõ¤ /=]ÇHƒöÚ€mУèzûCƒ’VÕ⺂Œ ÍÁïÝA‘÷_?¡âïkV€_KyYr:”I2¡¤¨_Gú”¨K¾ß×'Æby5MbAˆÇ²ï·–ò@‹rÔPÈø yã·id7—rßgÂZëGA?ŠÖk3Û+Ac`ä(KÞÉIôýÝ€I”Žm‘Z<ÁÍ‘¾ÑC+3±ä8²qé/ªAJŸz«àÉ0jÏø¾±»ƒ”©´,ÑtšÓ fçÍÒ‚Lrª¬ è‘£ýÙ$ô!U M-£èï«Â§mdp€úÒ¦eÑ÷[£õÕ½t£HÅá!diŽyXÐ'ô£ÐÿIçæÞ”¹+17ó}%8ÐçiRmq„Á‚B£œ¾;‡©Ȩ̀â0Î aûÑ`¤¯‡d<„TIÉù5×`í_y’êäicú¼Je±±+õÞ™U.E=èÚ(ã­D¿‚}ÌNuæf~öˆï+æAÒ/4¦Ïý&׊¹—Æ¿c U‰~ ú…s²ƒ¸’3éc_l̯c»tfŠ·y^# #•1Fíq? ,sþΞ[´©>^¸æ[,I?åÈržt¢(ª/Ç¥vë´|2N@Ô¡_CƒŠôõñ°ºT œÁ<ô·üiCd+úiúæ…þŒïCŸûé:ôkh0ÐëßN0HæÐèÍ–jAr0w¹eé³ð—–rB½r=Ÿò¼ ý)Œ½xºùÅ¢ó§ô¦µÆMšX¯ÃlÛ”¾\@m·’3kѯ¢æpÚ² ü¦ïèΰeI…Öï›§U0Äh¥¼Âü\®ØyôIF׿¥êõ£Ïà¥íbГzÊ.N¿t"Üm²—úbQêßïGƒ¾ÜÕeÑì‹>¹•’žƒÕe/½ÀМþ¢ôcÖ¢†µÊ¾Ï˜˜ÍÊko”Pi›q¼/aµY®T;ÖöýE50èÖ±%Zùª‘ŸÅãIôix­@<ú0×ëmiîû‹j¡'AŒ-Ðï[‰A®}€ó°J&ѧIKmR5¼Mîr bmß_T¦ª³«Ë üÄÖÜ™_yÄ÷G³ƒÞè[x2}B"z…)ô»Ñ€Ñ nQÇl·OôóÑAq*–8äÆ#PŠ…èLŽâNÁÈ=/±qM¡ßÌâQ“àÓÒWÞ÷óoT¢OJfv~œ™ìûÔ4Äý$Sèw£¡ùÞ@’´63=*ѧp#Ö•¨µª·ÈT oïƒG™Òùø4 ²²$Nöúаߌ£ÌkÒ(¨µ–‰ËüÎÃ)¾ï$ÂËj00{ÚvÐ1=‘Êæú‹ü°É>‚*N0¥°èÃ"„#ÚÑ´®7úÅ'®´#·}¿lw"•îa¯ùa}µ‰u5¸ÒOv5A(SþÓœí°¯kóš^Ùú¾“»S⢯6¯@ßè,IÖàJ_oöø›ôí{*|úæöfç3 œlÀ›3â›Þïô;ÑàJ_=üjµ“¾}Á•¹ý—íЇh…ßЉ±Ély߇Å\Ù:ÑàJÿ‘ ½öò—™ôA‚민$e{¨hIŸ¾ÈO”±g›¬Ïܵ؇7;”;¿2êWGœ ŠÜ1œ”IÒ_¾º?"sèfÈ.ž´í>4¸{Á¹à†ý·±ÂÇ_z³Ùqìv邼›.ÐÄ{\|„fݲ¶xäwÎlu¡Aˆx˜%Œ¼Ài=ºv̳]bËdül¤Åqм¹¥àž ™¥¸‘ß¡èE6pp±:aa= bZÈÃ˹+ Ý€ dÚAÐ3±€iAÂüKš=,ÈFEÊŽÊ¥ï †GÂJ¥}ÞjDúʺ(¼¹tŸsûûs6|àAD=ä8 \½ÜñæÈ ~ÉèC†÷`mý4—„íBšLh%¦¶ó“¨sîZM¿ðÁ#ú8è/ƒÙ?ÈÇl ðíN ç4D¹D THEíøŒ¨Ë´¾ ýhÉXE.n¾”N„¨‹GH„/²³5ojóãA ±á³"ÓÐÏîRrIqYÒ;c/…shV"²º‘¾<¯=Óü—ï÷ã§—XGð;3ÏåKܤx{õC4í5¾vCêîs(ñ¬x߯‚œ?¸ù«]ìw“tŸ ¡ì»WÖà/Ò'A‚G:ÚØhlŸËÓçæ1*¢‹¨¢ëXÔ ÚÖ S†ð"Ÿq”ôny_W¢Å11þâ~È´Íä K?‰ÂæûRŽd‰Â¸õÓ^ÊY)U>@›OßÜñv/U úÑÆô±ë.ÙÓë^U)bNjy¥²¾Ü¾ñÙZÈjO?s¿{røÂ8ôãˉ{†"âu5YÜ $cµ…J_C¦Šß¸J4&:‡âæùþ©äÔ­§¤YÇ,ØÑ€Ù¦„þõøó?*:=<ûô¬“••6õ­ŸqÅÕ[©vëi æúäÅ1ÈúSíkwKgÍFÖéú÷G>JÁ5¯¥AJŸ&cBžì¥¬}Я ï•JKF|¸-bDëYc£ÿÜm%Ò¿_¥Îüüîü~Þ‚ôÍYßsë¿“^?aQª&ΚßlôK͹ø{óèÃÒZÉÊÚâÚmú˜I<²÷þòÜæûÝ6¹ô ÛèiW6úëØ½Z7ú}pXGŠþ:vï£Ö~Ö‘bQúÎ~¯c‘WªuQú+¯Ã½×2]ÛÓ/š,v{«²šÓggDùŸ#¯¬ÊVÜÃhJ_ߥsìh-þa{ý_4¥ol‹²¯ùø¿lÛ¿6kÐoZgÿ&ïH¦$ ß×.OïÈ$/$JSúÆnÙ9ÇÁ_ͪ6¥¿€ü[s,°ÑŸc½gÿv£ÿìçÈÿ(Ð̃EáIEND®B`‚plumbum-1.7.2/docs/_static/logo6.png0000644000232200023220000022154014161152302017661 0ustar debalancedebalance‰PNG  IHDRî©í=Þ6sRGB®ÎégAMA± üa pHYsÃÃÇo¨dtEXtSoftwarePaint.NET v3.5.100ôr¡ÿIDATx^ìýxÉ–% Oïyóz¦ÿofÞ¼™é×=Ýs»ûú[ÞQU¶ðÞ{'@„¼÷Þ{ƒ$$N8á­$9ä½÷Þ{­EJ%Ð9’ (îÑ÷­ï¤2#####bÅÞ±cïà_(¡¬eP¶eP¶÷µ ü‹_Ø_ï@×w½íÎ}-qýƒmƒÃÿ?ñ }ý]Aü›i¿Îûú¡”åR"Ê6 lÊ6 l¢ ¼­?ŒtüKŒ´ÿOŒ´-ÂPó" 4©£¿åöH_SÆHo}ÆHOmõHw-FºkÀc ÷Õ]ê« ì©v讲è.·è*³ë奄Ý×Uqz°»"`d îîð`cùðP0ÒŒ´v ´]i_Œáöÿ:#ï¢ìÊŽ¡lÊ6 lÊ6ð>·!»q™@ÿËðPëWCM‰Ý¥èëÈGOkº›ÓÑÝ”Šþæ4 6=A]º+ú1ØŒúžŠÞšÇè­ŠGoå}tßB}ÖEÔeœGSÞU´•ÝEW=ïïÌÇÈ@0Ü€‘aùpK†ÛOû}Þç¥,›r0Q¶eP¶e˜.Ñõvõ~6ÔÝöýpOӓ޲Âîšôþ®²xT§žFÍãShy…ÆŒS¨M?…g7½m…›Þjx|YQúÈ»d‰¢{è,¿ŽÎÊÛh)‰C}~,º*®¢¿òšÒÂtêbwà”Å:„›¯Åi§¸è¯ø³vȉ?†Âèå„` »¨cx ¾px¨Ý§¿¯SR¡wu÷þ•Bï¨ìÊN¡lÊ6 lÊ6ð>·…Hm,qOoûìo[52Ðvx ³öpWcigSy:jsî ìa4r¯¹!á”®ú« ÚzBôç @ë8˜‡ßÃW}.Λ­À9Óe¸í¥‚ÇÑh͹€¡jJàå$뜳hÊ:ö¼XÔ'…âÉ)S\rUÁI‹ð×_N,ƒ‡æBØ0/µ¹ˆt=ˆŒ;ÈO>‹Š‚ôtTa¨¿½­¯»}iOO×Qèßç¥,›r0Q¶eP¶ePˆÔ˜¸§¯ëŸûû;ÜF›0ÔU†ÖŠÇ(N9§×ü‘zÙ%·}‘®0“ðÒšƒpãŸì¼çì¶ Ôt‚Ͷ"ÆfnxìÃuÏ}xm†g·y;™·ü‘|ÙcpÚOÏÙ!õ´-îøê Â|¼Ž.‡“êB8œ[•oárx>üôV"ÖÄ炜ûÇ‘•‰¬Ç—ÐÙTŒ¡ôõ¶ÏRè•BÙ)”m@Ù”m@ÙÞç6 ©õôõþ‡Þ¾®7Ô×àÙÙ…âäÓH¥týø¼žÅÙ£à–+Š®9#9ÌW)Ÿ·;ˆÓ6{a¯‚s†H» …ÎâÙ%/”ÞôEGæYôRÊîïyÆIÀ3ŒtfЦ- CíO1Ü‘ê[èÊŒÁÓS†ˆ±ÚÀIÀJøë,„“Ú÷°Ú7&;¾„ù®¯a³÷ØíÿA–›jŒ”;¡¨ÊOÄPoCÆ@[@_Ç–û=ßç¥,›r0Q¶eP¶e›ÐÆöuµ|ÞÕP„²ä ȼáÂ[ÎÈ8gÄuh $©#õ×­Ï[!ç¬5 ÏÙ ç4%ê VHºd„‹®¸~Âgöâ†÷”sm»§ì2»hlÖ”€ÞÚ;论æÂXôU^ÅHÍ5´¥…âa¨& —Àý ZåSX«|ã_ÂpË'p:ø-.ºîÀ“=<‰2Àý“†¸{Æž“„(´Ôça ¿é~ooó_Êýžn"==]ì-˜2œœœXeÊŽ¤¬eP¶exWm@nBcÂÞοìh©ùC}iúHaâä\óÄãäÆš¡ò–#šz£ñ'НXãºû„é~ƒ0OqúØ·ˆ:öÂôÂçÈøþ–ëÛK‘£‡žg¡è/»„’ukf$êÓÃP”È|žDGÎi”ÞõFœ×!øê,ƒíþï`¸ýSíø &;?‡éöO`¾õ÷ˆ¶XŽÇ¡‡ð0ø0’£M©pƳ»QHCUIÖ…žž¦-÷{¾«Š_žsàÀ)“¶Œðß—wQ–ãÃ8§3©œì^e{ùðÚËŸË7•›ÐÄúvû? ô6þ©¥*{¤<ù2Jï…£>áʯº!÷¬ .ØHÄý(ä0NèÏÅq­Y8cº¤='õæ#Âx5‚ôW!Êp-î9ïAÎ)ä]°CY|0 ï‡ û¦žÝðFò¤S_|g×[2 ÷^·%¡‡ÛÂúkîr÷mt¤Gr[‘&k ù#ÜTgÃaß,ØíúŠÒöÇp¥ú<˜{»ƒuæsŸ÷ \ðØ»QÈN8…Æò'ì¯ínýå~ÏíOõ}êëëå–º§ú å}Êuªm`2©züõ©>CyŸ²}¾¯m@nB“ˆ»ûß´Ô–>ʈ?GuvJâÃwÝ7|5)ñªâq¸& /[¡”jëÜ3&$ÜÃôK¾±[C#µÓî{pÜ| Žë/ øb‰¼{³Oq¿ö] UÜDoáEtÄ¢-;½\ëFÕ5t>‹À£0]„.§»ÔY°Þû̹ÌjÇç0Þð;Xnú=óZ‹¬ècÈ:cÄí`FHˆ±Àã8_TäÆÓýiEÏ`ó“û=ß×õs”KÞÁñç(›ò™Þƒª¼mS¤S¶•?ï¶ò!~¹ m,aGcyhÁÓ«È‹?…äóîH9ç†*n媼ãEC5K”ݰEJ¸.Biîºé—<Úp5"¨ê7ÞÂýثࣹ' S¥®ŽÆûþh¥¶úÄZ›»£0Ή[ºlPpÕµü}Þ×=Ôè€e=×ÉçÃ|Ç×0ØÀ}ÜëgÁjËw°Üð5"ŽmDéE”SúϺh‡‡gmr#OïEmEzºê•kÜSi¼òŽSÉ[yr0N·m*‰[ÙΦÓÎÞ×{!îîζϻ[ªv7•Ð?yR ªG!;ΗÜqö2$†il]xü(‚µVÁûÈ j­A îFíÀB—¸Îý0XYçLQöÀ ¹Qž|’xa| ý¸¯›3×¾ ¹î}/@gm¶â„Á*¸š«Ý³¸Îý5LIàÆ«ÿË!Ò`5îx³óöH¿â”ëaÈH¸Æ}Ü-uåÿ·Üïù¾~¨Ÿ£\òŽ?GÙ”Ïüóåm›Jâþón'ê8!7¡1aOW‡Ssm±c~rr¨*tÚV{`K"µÙõ.{lÃÝ€}ˆ2[…`í¸èt÷ƒèõ¦*3Ρºè:JSN#/ÎÑú¨{è‰Þ²‹èoa<î–$ôs[XãôÔÜä>îk¬ˆCKj(·ƒ©#Ì`¼)½;îÿ†¾É¿„õ®Oa¹ýð=ò=.ÚmÂ]OÜpÛ‡;Ú¸N#µ‹þÜlj¾özEÞQ2ÌRb´ä•õ¥l3ïº ÈÛ6•Ä­l›ïºm¾‹ç)BjÝ] jËóî¥?8‹[‘v8ë®Îõm]Fù:"­C§Ò…iñ5KÔÝwEé5[F Û8·åt²’1´W!Ìn-œÞÓáÀ't¦2™g ÑWthz„áª;èÊ?Æß®~ˆî¼hŒ”_Bc¢?øA]ž:Óå©ù6n£Ÿrñk¼Òöæe¼ Ï"t¸Æ} 4|{vÝ•qºÃQüìZŠZº:j”÷T“¼ƒãTòVÞ£P§Óäm›JâV¶³é´³÷õ^…ˆ»»ëÛÎæ ýœÇ—‘~+9ŒðUÆpœÕwýPrà ¹—,èTÅ éѺˆ0YJUù®÷¢¿ÇqͯàÉ !¿†·=™.¦ñ£¨N8Žº”3¨¢Z¼6)€AKœzÞ˜ÛÀ,P~Ç™~Ð-qÓ}?BtIÜ*ßÁbÛç$.ÿÆ?Ɇ¨±U×]ÐðÀéç-”ÛÓè>53éê*³ÐÕQ«\ãžJ”wpœJÞÊ{”êtÚ€¼mSIÜÊv6vö¾Þ«q3¬ç_w¶V?®-zˆÒ§gPšÂ5h®'G˜"㬠ÓLÑè†øãjð£ZÛSí+œÐ™ƒ³VkqÍó„áN°ns-ú Ri žÇÕ™±¨L@yJJ’ïÆ@#î¨MñçÄÀñªÜ ¾!t¡êzh°|mîßÖZõ0Ùø¸ª|H£å4bÛ‡äs¤]ñ@æÝ“ÈNºÎaå#ÝíÍ#÷{¾¯êç(—¼ƒãÏQ6å3ÿ¼dyÛ¦’¸ÿ¼Ûɇ:NÈMhb»»ç×µ•yš. íf=§ÑÑÉ’ìg¤Eâ–ïn\vÝŠóö[çÌía!–ôŽæˆÂë¾hL;‹ž‚ë¨|Jkr=â(R>eÚ©µõ7Õ›²¼¨S…X›‰ë« ®$ÄqûŒ3n×£„k€Œ3f 4žƒÐšr\㾸‘–Kèât.\u~„ùáy°×\ç#s¸ûSZ‰ø 4>¡–üp´=õç¶2'’¹ Ê.£é¾=úңꦮ¹ìƒ—ÝnjòO`°åc’öa²ùw0\÷ܾ…,$éÙìuÙ“b"‘óä6*Jrêë*§LHŠF$Rd`‘'­xþL ÊŠ (ò”K^‰F¨¶('{¶ ‚ôMïó®Ÿ)Ê#Ê5YÙ_w}"5ôTÚxoE¾³,­hçS)û»n—¢¼Š”süûM· Š¥‰éhÞu›ïþ®Ÿ©ì3?iyµ?+BÜ=*͵å›K3âQùôSbÐÈõí*†ß||ÊѶ[à£5¶{þÈP›¿ƒ¯ê'И ª¸-Ô~€“îx,ÉN˜.Cœ÷>d^¶Ae¢Ê®X#ç¤&rÃÕ‘´¹'µPÁ=Ü©Aº¸hµG—Ãqï0Úôô×ÿ‰þÊÿÈ}Ü‚öò_qÛÙ䜵ä>îTp_yön)£Ÿògï  ûY}UYéÿû='ð¦2x*2°(šöâÅ‹S˜Ìå-—<ùÊ›—"é&#(Eò’7훞ù꺛¼yŽO7¾.IL%q¢ÒðtË>Ù·§(’F‘zD2]Â~õyB+1ÙÄq¢÷Q¤Üò¦¬îåÍG‘tÊ~ð/&웓} EÚø›ÒÊMhLØßÝó_jK —å?¹CŸâ§‘~Á Q6;h46 ®jßÓýézÜôUÁ9û5ˆ4]ˆ©Å˜­Á·¸š$|ÏI4>»N?å×ü !ÂŒ{®½ðô²?Ò.z!)–þÎMqï„R¢-QtÕƒÖꎸé©A«òõ”ºÂfë§0#i[÷'è¯ù=L6}ÌŸ_ã”ñZÜò:Œ„¦ôŸ~ÅâP˜šT\_Y±Q‘wœÐÝ©" ú]¦}Ûê;yßEžÆ(o^Ф›ÌPJ‘¼äMû¦gN¶.,Ï3du9Ò–=G^òžŽ–`ü;Éû“yÉûL‘n²‰‹"yÉ›öMÏTd îuÏu9¤-˲:š)ÒVt”§ÍL"qLY!ï·–7"R–¼y*’n²o¬H^ò¦Uöƒ™™,Lµ(Bj}=íË«Š2U.ŸÂcñH¡ÄýˆRsÒ)SßðFó“0\q@N¬ ¯Z#{ª†AJ¤6îûíGvÌ1<:®ŠóeˆuÜ„â{hfüíÖÊÛh¨x(‘vUé]T–ÜBeñ Tä_AeöydßõÇÍ:¸Aj×ßû’ëFœqÜŠhû͸쳉‘ºÜfŽn+£Ú¾àÞqdÜ AæcF+IEC}ñô¶ƒ‰Yµ¼ úçJ7u·×5yßEžÆ'Ôûòæ'O:y¶AÍô3E¹Þ´ž+®M—tåY–iY~]Û·~„±¤€<ßE¤y’·¼eyWéä%oyë\Þr+ûüvR?P„¸EÚöær”›(zƒ¼»AÒì eíÃSh 2©Ú¾ IKó]5[ÍØÛŒÇm±gÌWã‚åjÄ/alî8ÅxÜqî{p;XI1fôºv˜’:·•QÝm³g)½Ÿw݃Xœ4ÝH#·pVýv{¿€™0LÛþ)·„}›ƒß"Är åÔ‘ÓõÜJVþ$y‰Â8í*è=­¶2wzÄý*1‰zºkfbp{  z*jWyyˆV yó“¥›ªd:½©>S  ŠªCeï)¾ïLkkÆK:“m?ø¿úä]ÓOŠHçoÛC^b›,Ý›¾­¢$«hÿ›j›Töƒ—µmŽý@ânÿ­ùÏ^â>éh%„¡ðîq$E1R˜ËQÛF—¤?2äæ·’¸ÕޝîókØíþ ö{¾€ëoàÃ0œÁ4`;AKó3æëq[ÉRh€vÇ™ñº˜Dú9«8o»qô=ëz†›à¡½Vû@sç8°ì78°ø×Ø5ÿ°eöß`Ëÿ,ý5,T~À÷#Œ„ÂÄs¨Ë„ÞÖŠŠÁžÆÿ,÷{*BBŠH!òª._}¾¢ÒþLZøN6èÉ®+RgãIMÞüEº™Ò((òLAºSy·Wï™êýjY'"y%ïñï"ï@'âÕwQ„¼§:á‘§ÎùŽ¥•·=É[W²g(ÚÿÍ_ÞrOV‡ŠÔŸ²¼ý@nBc¡þŽÿÞÚT™“öè*_ Æåø›ì Õøxêl†»æz¡å÷æÏa¾ý 8îûŽ„ý5¤|[º(õ$i{™M/hs¤½€Ä½·]ö 5P÷¹F~Ñb"-ÅYóU8k¹í·á”ùøëm„«úZX©,‡Ö†ï ²ôOØ9ï7Øøí?`ëœÂŽù¿ÅžE¿‡ÚÊy‘ìB´›12î]@kM!»fVâ–u !+Ò¦3)¢‚©5oyßm²AbºªxQŽ©>c:!Eá·¹&û¦²È;1I„Óý®ŠLXg껽š¼ïðjº©h%WEßY‘wQ4ï×¥WD Pöƒ‰·wýœý@âîïëú«žÎVŸ¬ä;‚]ŽÁ…¶“æ:øì„·Á«­£ñïIÜŸÁq²-ö|Æð›Ÿ´?"yÿa9?'qOïj´8×ýŒèuÍi ׃”´wà”ÑÄÐKZ¬Õ&\´ÝÊðŸ›n¸‡W1¯ÅÐZ÷-ö.úÉúØøý?cóœß`ë¼ßc7ÏXþ ´7~³}Ë᪷öH¸z9Oãßq+¢Nž ò‘·ƒ 5þLtpyŸ7ÕgÉ›ÿLÔÝø2ÊûÜ©¾×D÷Éû̉ÒÉ3á“gm\ä-ï`3ÙäOÞ÷Ï›Éz”å%ïóǧST•=¾Üòj6Äó&3›Î$d&ëRÞ:ü9ž©ìoÞ ®q‹´]Úƒ G£üìa­±™Ä½ÎG7ÂR·“ú*mgœlJÛ†[é$…á6Íw.IÜλ¾€¿Ý¡ªÎGˆÖJ„ê­A˜ÑZšíaD/-\t;È5ñ5Y…S+¹½kÎX®E„鄬…§úXîžu_a%î­ó~‹Í”º7›y¼cáïqp%‰{ó÷°T]Ýíp5:ˆ_;”å¥-“û=m¤ò6þ™0ØQdæ?AJÑÁQÑ:S4ÿ?gâV„äm‹“¥“g¢ H[œ)õîT&_²wlË”¢šEÚÆëÊ£È3åUÑÏô÷õ®h}*ò­Þ”VÞç*¢’Wöƒ7o­R¤M*:FþRûÜ„6.aivŠI€ƒ!ìµ¶ÁY÷ÑõðÐÝH‰{”¸ 6Sâ&q[´­÷~›=_Â~ï—pSýîªßÓ›Ú„[ŽH“Õ¸ì°w½ã ­Ç£Ì6 üØJD›+ô5ˆ¦ñZ”Å„m‚çQ§í[­_cß²?¸ƒ-sÿ ÛHÚ;çÿ»þû–| ¡*?°Ž:TÝ›¨"ÊßíMåÙr¿§¢\ÞŽ<™êQ‘çÊûLE‰ž/ïs)ûø´ŠXÎOõ?Ç{Mç™ãë|*mFÞoöºtŠ>SÞç)šïdß[ÞçŠt3)ñ+²K`²w]Wöƒ7÷TÚŽ"íc¢´Š>SÞç)šïëÚÜ„6.aynšÉq'cIâvÑÚJâ^/ýÍp8¼ f»gÃxÛ¨ÄmE)Ûfß×°Øñ)l¨2w;ô Ô~@°þ’ôZÄÒEê 7ÆÛÖÄ 5œ¶ÜÂuïÕ8K#µóVpÎf+ÎÛíF˜É:zYk•…”¸?‡Êò?bÛ¿•ˆ{ëÜæz÷(qï_ö %î`º¥T6W£ˆô³F[CÙÏOÜòvbyÒ)bÙ+O~ohrßMõÊëõÖT—VQç¾:ØHE¿¥¼k¿3½Ö-ï@9ÓÒ¾"Kò,9ˆúVöe?P´ßM…¸+ó3L‚œM%rtàú¶³ú:xÛÇ#Ëa±w Ô¾a¯Ï%‰Û’kÛV»>ç±Oáxðk‰¸´iœFò>#ö_“¸ïùPâvV¡„½ ' V"ʘjt£•8+"9¨ Ìx+÷q¯§Ä½‡WŠý+>ŽſǦ~…M³%Y– ©[e© îÙqÛqBál@)žwkCé‡EÜŠX³OGE/ïà¨h£SJo–0D½Ouf®HÛxõûNU2}ÛídºŒ©¶ÏéNhå)‰ûõýAÙ&6R› q—祛'qÛhl‡í¡ p:²n:›`wh9¢PâÞþ %îÏ`¾“.IIÞ6T—[QâvÜÿ‰{6ü¹ì¸öT‹s¿¶õVÜòP¥ªœkÜ4N #qGS qŸ±Ø„ÓÖ;pBìãÖû¸BsÓ—8°êqoþá±êr!qï]ü'^û5 ÷,­0œ#qGØ¢¹¶èÃ"nE×ø¦:p½íY9`M<`É;àO—X^ý¾Sm'òn-’W•§ò¶Í™–¸ßFßSöe?§ÍO3â.ÍI5ñ³7‚Å‘­°R]OI{#5ÖÃþÈJlûŽ–å_ÈÄm±“ëÛûfQe.Öº¿d<íYp?ô=|5æ!Ps¡$]ÇÚl§Ô}÷ÒmÃ(qÓ -üØ2œ&qG˜mE°Á&8©-…žùÐÜü%Ó~‡- ~ ³ÿ¶ŒIÛ2â>¸òsèl[ku–éØ>„{™£¡*ÿÃ#nE<·)Ú(déå§š¿rÀšxÀš.Á)bì$ûÆÓu²!O[QÔÈèMíJžçÉÒLµ}¾î>EŒåÙ­ìÊ~ h qg?M0ñ¶9&·%‰ÛîÐ:Z—SòÖX ó½ó`²c–¤*·Üõ$q›Ò=©¹Xç¦÷4÷Ã?ÐJ|9÷q/E¤ñîÝÞŠ«ÎôæÀµlÃ5”Äsïöb-ÎXo£»Ó-ðÓaþ$n“ÝspdÝ'ؽ”[Áæÿ36Ž©Ê·‘¼eÆiHÜF{—JÆiΆûqÒÛõ"q+ÒÙ§ª.—wpT´Ñ)UåoV•Oµ>e÷É»î<þûNUMþ®&y¯Ö‰¼mómHÜŠ¬sËc« H_žnÛxEj{«6,?÷LJܦBÜÏžÜ7ñ°Ô…é¡M0W]G©{5׺×ÁîðJnÿǶЪ|3£víø¶*ß’¼¿€É¶ÅëºDý!”ªO’´cÌÖKêðëÜ v™Vå14N 7¤×4«8M,a†kq\=¼5×ÀnÿÜ^ö=Ô׆}Ëÿ0*qÿÒ÷Ž1UùžE„ Ô è¨ÅYoÜiU~ÊÇòÃ$nE©JSòŽSL”ÖÄ>ÕúœÊ •ëòö yêVIÜ?íÊ~0³žÓË'n3µcĽ6”¸õ¶p7×·Mé=Í”·õžYT™Å߯øË-b»?ƒ#=ª¹üÇuã” ãw;ì¢Ä½—$½Ú‹(i¯D Öbøi.cœíð¡s‰¸÷̇úÚO°}á?I÷f:_ÙBÈŒÓÄ>nõõ³`LŸæŽº;$âõP·¤{›ƒÓTò÷È;ØOµü¯+—¼ƒîTßk:*Âéî½WTu=S·¼ÉŸc@žÉï8•úìùÊ~ðSâVöƒ·Kܦ\ã6;¸öêÜg­¶:›¾‚ÞÆa,¬Ê̆F,èAMHܶ*_ÁIõ[8˜ÏÃs$é:Úb#â`$†[¿ÜÍ»ÚløÑA‹ï‘Ü6¶‚ÎWdDw\ŽÐW¹Ê8JUù®E¿¦«Ó’¼¦ ã´mÜ˽çÇß9`¡qÜÞeÒ·—ùaÉ8­¦y''"Ý»&Ó™¨WEÞo&ž7Õ6$ï¤SÞ÷™ÉwyŸŸ©ì3OÜ©IwM\¨*7RÝãkaBOev$n»#«a(‚ŒUùöQu¹•¸Å¶0ú+·Þó9¡OrzM›÷+ªÊÿ¶Ï§ó•‚¸Ë}Ü„úªÊ÷-…“Þxš©!ØÕ5¥™Jâž !¼‹Áä] :ë?‚Þ†?Qêft0a¤Æ-aö¾•Ö·Õ¾£dý=<Ô¾ÅqJݑƫ$⎱Þ_­e4\›/ù3÷×\Ĩ`ëè”e;BÖÓ†o&$ns{çÂÆÖùT“SêÞAÒÞÉõnAÜû–ü G7Ð8NÚ¶žæjq3Buñ3%qOEõô.“wñŒñƒ—¼{p§R_3¡ºž©Y^+oQÿÓÝ ös,{ÈÛn”Ä=±ÊUÙ~JÜ¿¤~0]â>Fâ6RY›#kashôiU®Ë5nýMÓšüKBlûLòYn¹óSØï§a×¹ÝUg!Xg‘ä!M÷é1âö<2JÜÚK!l²l#qoƒ;Õð¦;çJ.O…Ľm ÔæŽJÜ;ˆÝ´2WY*‚Œ|ýKeë â.Q7¦âÔCÞÁq:dó.ž1¾|ò‰˜Š†â}"ny%ª™$6y¿åL ò>k&ßoü÷éçË›ßtúš²¼]ÿ ŠN`§Ó¦BÜãUåúT•ì]ëÃka¥¶z[gAjrÖsó¨ÓíŸsö·p=uR›Còž/M®cÓƒZÝžJþÊ­6Ó8J4®q3M€ÖÒå>o“­tزªKpló78´òOT• ‹òà>îÿÍ}ÜÿÀ #·—Å!ªÊ QYœ¡T•O¥#È;8N%oÙ=òªì¦3;Ф4wšèÞwQ—SYËŸIb“÷[*‰û§ƒ²¼u§ìo&´WûÞÏ1•÷[N§(*q÷övÿôÇ÷%«r]®q âÖݵ„Ô¨ÊÞ·뿂ƺ)ucªÉ…qš%°XîþšæßÀá׷̃ÏÑù9 å§iYj´žê $âöQŸ'íñ>®³L’¸ƒô6Âáàblýê«?Æ®ÿù9qo¡š¸wIÜš›hÕÎ5n!q{[–ˆ»¢(]IÜS!¢wA6òz€›)c±wñN®Ä-ï )|©O¥=Ne6““·ù|e?øp$îwѦ@Ü-#ní=k +ˆ{çb˜rK˜ÑÞÅ4ûšNR>Æ1ú*7Úö÷rÓ_9IÛœG„ÚÜ^å{¸šËm_”ª)q‡ê-Ãs®e3¤§7 Òü´QÒjòeÄJ:hÙ*·£êR’¸ÒËn±[Hܳÿ7·ƒ âþG‰¸eÆiÆ4–Ä-“¸•Äýž:`QD<݈YâYò†¼œ by•˜Þõ„AÞÁc&‰í]>SÞúœÉ÷{›Ä-ò–ç”ýàý—¸ßE?P”¸Eú”‡w¸L qï^I‰{)ŒÛpÏb†Ýœ…£ë>…¾ðWÎuná=ÍœžÓq›r·%²¸¨ÎÛÁHÐK¢³D 6rŠ!=}µ–0ìç¨;TAêŒËm¾ ÁôžæÈ #FŒÅ?•ö\ ¢UeüìëþÍuÜ"ƵoxÄqÿlJÖKnÀøÛÂW¹ázþ¿”jò…’´íuh6¼„*Äí¡Î}âûÂxçTÆÝK~Çè`$nîã{¹…Ú|뼤$Îçp[%àcu”kÜF(/üUåB÷¶;ùÛÎ|ƒ—WM¨ˆ3•5ə꼯æó.ëR<û] ?Ç€õ¡·²ŒëLõõL¸c%âÖÙ·´(פjÚ@…!=éøDuÝ7 ò¬øû—ÿ ªtQºÙHÞ¿‡úš?IÎYDðñÕX,ÅãÄ}’ÄísdI{TMî©ö½DÜ~Œæzx),÷ΧÁÛ·Œ·M—§ôœ¶‘d½ŽVå…ªÍ·Ó êž%„Úš/%â¶&q{[ªã¸³!JóSþ5î©x.› u:à]’¼ßSyy*Ì„Åîë¾Ù»¬K%q¿­E¢^åmKâ{OEÓ¥ìJâ–gÒ2â¾{㢉-UåZ4NÄ}tûJÜ\ëfT.•Õ_bÇ¿ÃÞ%b-úw$ï?â‰û%n JܺtÊ"ˆÛIu.×®—#Üh ·ƒm¤Ä½ªó%âjrÏC$nF ó×Y ·#ô…Nâ֥ź˜ìXôlœÿ+¬å·º…Ä-ˆ{7%nAÜ»GÓ„Ääb„’¼”ŸŸ¸gj²¢ÇtÖ…ß5ÙÈCQ©[Þ÷P4_y:˜¢ßK‘<ÿr­Ï©LôÞT¯òz8›Îs•ý@)qO6L¸/HÄ­¹k%Ž’¸Õ·‘Té=Mƒ¾wåçØ¾à·t’ò[z8û5THÚBâ>$Ö½qSâ¶à~nµyŒ·½Œ1¹W>—¸ýŽ.aT°€d6­Ê`lî94Z[Í#c÷æo%âÞF’·DÜ”ºÅÖ0!uïZô{¨râ ·æ®qkPUnŒâÜ䟟¸§Ó‘_ýˆbV^2šŽ%¶¼Ï˜¬‘É{]iFÞ<©+yóœJºw]—ªŠðç$nE¤áéL˜•ý@IÜ“1S!î[WÏ›X†ÖžÕ8ºãqÙ2»—}ŠmóIÜ G‰›1²IÚû–þWüG(uëoúL"nëݳ%l!‰{N­q§Z\·ëé£>‡NZæÂGc¥óE°ØCUùÖï˜ÏGI¯£¤½úÛÿëÅ^î1âÞ±ð·8°ò3èí\ø|;ÈÅ…ÙOÞ➉Ne½vº1¥ß5Ùˆw”×ßµˆ~5Yã–×JW¼çÛ”¶E9ßu]*‰{æ-”ß…]‰¬M+ûÁÔöPÿœv²g¿‹¾7â¾v)ÆÄR_MZã>ºcÔ)iëÓ8M}ë<ìXü£vý»ü=­¼OÒ*óßRe>j ¦³þËéUX–{ªÿȽڌ¹m²AT‰{ZHi›¾Ì©*wWÝçíE)ÜåÐXîã3¸Æ}€ëæ[ lÍì¿ÃªYƒµ$oáEm ÓdÄ=ºl3|­(q»˜ ?3éý îé©hŠX“O—ŒÞ5Ùˆ÷SDª™Ì\ÞòÏô2ÆD yË2SQÉÞÅàñê{¾Kb“·>gJÓ¥ˆ<Ô²:UöƒúI'æ“MÜÅõµL…¸¯^Œ]ã¦Ä­I¹0NÓÙ½D"n!qoÿÉÊè:7ãdSâ>@‰û0Ót¨*7¡5ûý 2BÒÄ-,Ë4–Ò ¾Êé„Å—{¹Å·¸=,–$nAܺÜ#.ˆ{×Î7Îû?XùÍÿÂ’·0Pä½}Áo¸í i·Õ‘ð³ÑÄ 73äf<|?ˆ[ "“‘ÍdQÞK‰t¦ž5SšYyQo¿Îèï]¬ENVSÑ’L·}¼ËYÿ«ï/oÛœ 2UäYÓÀ*¢1™‰wSöƒé“z?˜ qÇ]Œ–¬Ê5w¯‚Œã4AÜG¹Ö½oåÜcý[ìYDÒ^$ŒÔ(u‹unJܪ«þÍ ÃTHÜÜæ}t Ô–á”áú-_Fi{$q»«Î&¾µ0§ªÜYm1¬¸L· %xaˆ¶žD-“¸e–å;i'7=§Y«o‚ÿsâN|ˆ{ªÖ¦Š¨‘gbðPDÍ<Æt¤·W¥Ty·Ôˆzš) w2——hfJú—Òïϱî,o}ÊÒMö}Þt]^c±·ÑžùŽÊ~ðSõº"õ762•IútÆé)÷…(±Æ-¬ÊIÜÂ8M‡¿š;âÀš¯I¬¿£aÓ„u¹Xã¦e¹ =žZó4ד¸wÍ‚ãÁ¹ (²œkÜkª¿ þc·ÛÁÙôcþ$qû1èˆ Ýñà\ãžG7ª³¤-f‚¸Å· îußý­$m ‡,BU~pÕçÏ÷qØhÑ‹ %î÷Œ¸ÅST"V¤ÎÄÖ3Q>EÇ™jôãó™Ê€©iOe»ÎTÞS‘åétæ©3!‘**•N·*Ò6EZE\Nµ ¾­ö¤ìS_ïV¤ü’úÁTˆûJlÓ=W•KVå{–B‹*êk¾áv°ß?'î}ËþÈ}Ý¿Çþ•4N[÷ ´èQÍ„1º…ªn¡&ÿö.ý¥mA´¿ °âÖ¥ÛS˽ߓŒçÀ—Òt÷oGÓk%nAÜ"j˜l;˜·Ÿ{)cx/xaœF«rá9MlVåBâÄ-0JÜŸA‡.O-o€¿µŒ¸ßC‰[‘™ ¼i§2xˆAL4ðé †¯+ŸØ3ÕÁyªRõß§~0=‰ÂÜ}ù|„‰Õ15†õ\E,£qÚ"SYÅ5î…Ø³œû¸©*ßGÒÞO’ÝCUù¶…ÿÄßßKªr|Ä\9@_äÚˬ· ')qꬂ·úZ”Ó0MíøÑ{𷆉5nîã¶;H·ªÜ¶ŸüîßÄÀ"b÷šoÿvÔõ)ÿŽ_„Ä­½}‰{‰û(B=L‘“þg@ÜS!mÑÞÅ (Ï 4Yšé ¬ãë™R‡MT^EUâŠÌ뼺Íô`>¾L¯“Â)·¢i§cð¦è³f"ýÛ°ïxS_Pöƒe¬žŸªÎ?ô~0Ä}1&ÔÄBgUå˸ÎM0ÈˆÑ tÀ²”X¾Âv:_Ù»Œû¸ d§²ˆÛÃhœvh-%nú*7e¨Oç?H¤b°Átw ·Žjñ•8N2÷¢Ô}üèBZš/–ι©¯€ ƒŒèq[.O÷ÒÑÊÖ9ÿDiûï±ükn cLî í¹}Ñ©’ÿ:Ûæ’¸×RU~ážFÈL¿ŒÓfbÀŸÇtT«3]–‰ò›Œ”å½®¨úr|YDÇ–GE*oY&J7òÉó&z¦<÷M5ÍëHiªùÉsß/‰¸§»¼0Õ¶6v¦ì“G®{µ¾ý`&ˆ;6ꄉ¹ö>JÛË%‰[‹FŒIÜÚ;—cÿꯥ5è=K~Ãm[¿ÁÃ.®q«0àˆ nm®qmþö{¾aô¯¥$Ä}\ ­ÈÃOø'§ç´PF ;ΰžÜßíz„û¸÷Î#qƒC$na­.ˆ{ý÷ÿ€•³þ–†jÿæ·§¿£Jžªò­óa®¶^‡êe‚쌄ú‘áŽÏ††:þÕ¤êE;“<ƒ‘H#òi•¦è„ÓUEË[þé¤S´Nß”~*kÆÓñb¥HÙ§3 ÊS¿JâV|Е§^M3Uí–"mi²´Ê~ð²Ô­è7T$ý‡DÜ–:*ÐÙ»œÛÀ–A{‰›‘´w,¡Äû%v â^LßáüÝËí[»üCeùGŒÓý1´êÓˆ{¹­w|AÕø¨-§´½Rò[î+H[¥ø!Gç"ˆÆi~Œ æÆ½ÜV*ó`¸ý[Yó %nAÜ¿&qÿ «¾ý{‰´7Í¡ÛÓyŒÉ½ŒRýÖ0=¸îæ‡ìe†ìÌ„Á‘ávÕááöý³·èŒBê›®ºK¬½Ì”Ê÷m“ÍT­y'¸ä­Ã™ª§ÉÊ#®Oe0•wy]=Ê[ò>Gže…©ä%ï=ï³Ä-Öáß¶ÖFžv6>¼ß_Ù¦>é{]ÝÉÛ¦§’n²v0)‘Mà|dˆ‰¥îèî[Ém`K¡·{9Œ÷¯…½¨©py/%ì}T•«,%a/ã:7‰{ß²?AmÕÇСšÑ¦O`¹íS:W™'v€î i϶ïá¹ð?ô=|T¾@ú÷Ñõ¬æA5ó½s¡·u}žSUNl[çþëgÓ@êr±¾½Yl ›ûOØ¿ôc¦[Ókáf¦Nâ¶BVF<0ܶšÄ=ùëNVaSX&ÊW¨Úä5d–¢?—jNÑ:yWéß´¦ü>HE杖ÏyYúíBhYäí[oXEï{¿Söƒ©oû¥öÉ™ì§)¢ÃŽ›˜ií§ËÓ5”ºWB›Äm@ã4m†ÓT¡”½4 “¼¥‘¬…oqÉËŠ?1¬ç§Ðct0“ÍŸÀjû'ty:ú”¶õWÓPm)·„- šüøÓÝé -JÜ$nAêžš+`¶û¸iœ&‚ŒìåD@øC_7ûÿвüï°‰^Ô¶Ïióþ qÓHÎôàJÜ´*÷²CÚÓ{õnÿäzWE?¤¼³)EóU¦—¿3¾j`÷.¥ åw’ÿ;½u%–š„ˆ¦Ù¿ï$ý¦:Töƒ_v[T¤ÈEfãõôtôv61Tß]•u$ïU”¼WÀà€¾iU¾ìcªÈé§œjrU¸*·ƒí§º\…kÝ"·Î†`Lâ¶Üù9=¢1˜ˆ‹¬A áZø u9É;€Ñá·îRr ÜÏpì՗ÈNX­ýLÚ¾}Á?K‘ÁVû¿°yC{Îý{JÜ¿Â~ºZÕå¾r3µõp59Œ0o[EѶ¡L¯lÊ6 lo£ LFܽ}_ËÒ`¨íOCým¥ ·/™¨ï¤”½ú{…Eù2˜¨.…î® ¹õ¨¯ÿŒÒ>DZM_Ãhûw0Û3ÜÒåÊ­]î\³5Û„3Ž{pÙë®é#.è.ûkãF âOC|îú«ãv néᢟ6b<Ž"Äv?œtÖÁd?÷ïøš[¾ÄþU¹‹XKç^qºW=¼ê3JæßÃìàr8Ûƒ0zN{šp­pd -|°¯ýâdï«$î ¶Z¼†§ÌS9 )Û€² (ÛÀÔÚÀ›ˆld¨ã¿a¨ý×lÑlŠê­iín)FÞ“8`‹‹–¸n„3NxzÅ O.;#÷Žònù ü~ÚRcК~uO#Ñ’~YçÑ”…®¼X —_GÙMôT=@WuZ o )ózò.¢7çH¿à€äK<Š4Eþ w‰¼{Ëoc°)#ÙnNC;É»3ïÚŸ&ñû¡ä¶' o{£þÙY Ô' §üª‡!õ< :XgœUàÍ=àîÜ î¨»öÚëài°Áæ{å|WŽpba†¼{§P~ õyñ¨Ìæ{'£³¹¨hd°9nx¨m¹¨ƒîÞî{úz¥ð ­T•O­á);¬²Þ”m@Ù”m`jm`E7Õá·Ð[tíéèH C]‚/Õæ¡èxvŠ¿Á¨‰÷DzŒ>ª!Âr½D·hqN7ª‡7}í­_ÃŒ¾Ð­eÌöÀ,8þ>Zsb²éœH”'¡2%u9×PWü•E)hm(ïèéܤ$n¥š\ቛr Q’‰² (ÛÀÛl“fuwvþMNFRIÊýÓx|Ù‹„ë‹Â›þȽâ¼+.¨}„®¬4=Bþ $?€;ž›q×w+Ã")F‰QÆTOã& úþiSdÜtCÊ57Ü=mæ¸m‹'×ýQ›{핉$ìT µdP 欋h¡š¼ƒÊ^(¹á€Â8+4'ù ûYj\QxÍŠFkûqÕw?bÝvã„ù¸i/€Úw0ÚõL÷| ۃߞñ¼õÙ$nºW5YF‰Û%O‚Q™>«¹8­åih«Ëȵü«$n%q+‰[Ù”m@ÙÞ«60q÷vt›Ÿ‘Tö,>ù÷ƒQ|7%·ü)mû“0ÝÐüäZž©a(è‚û^xvÓÙ”ŠKžE Ž’rcÙmÔÞB5ÑZ•€~’òHwFº )]b¨£ƒüEo)†»y®§ƒ]9hJCwu"ÚKnpMû *¨&/¥Z¼ñ)¥ü¬“”¸ÃøNc·´Í)¨NôDÞU¤7AZ¬EÃÍÀÈó9€s.;qÆi;¢ì6#Æzλì@:×àó™géãPÔÓP®µè.:ËSÐÝT ´mT·²Ã¾WömÎâ•y+¥Deøe´Éˆ{ ³Ã¼2÷)ŠžœCÅÃPd]tÄ]nѺà´!Ç–áªÛ.ÔÞsÃ@Ñ9 ÔÝÇPg:†ú Ñ?X†¾¡J Tb˜¿Ãƒå¬±ùÛ€‘¡f`¨•ÇÍÒÿÒù*Þ[Æ´%ìçww†{si—ŽÁæÇÌÿ†ëîb¤ú&FÊã0\r#¥Ñ—½¹‘èÏ>îŒãèJ䄵‰î„'*á‘/êò\¥ö›Ž(¾j‡ÌXsd^±AbŒϘ#íª+ N¢%ÿw¹•³,í™}ý]ÿ^á[¹ÆýËhüÊAJù”m@Ù~‰m`2âîm­[—tóBÔàÂö*³`»ûKXný[Ûí¿Æ›(¤”ÛVÇíU™èé)FÏp#zFZÐ?܆a4‘¼kH„5iÅðH†FÚ18ÜNboÁ@t}x°Cƒ”º‡Š04”Ëó9¼'‡Ò7‰»ý)%pj=×¼ËHÚeW0Ppýy§Ñ•îÌp´*¸è±Qö›å¸1®*w8ˆÓ^ÆÈIºF¹&Õî¾îUJâVJÝ ·_â` ,³’Ä”mà—Ñ&#îúªBóب¯Ó‘EpVý.\/¶ãº±Íî`·ã×pÝûG\vÞ†’Þl¸GÕ7¥î¾ U`p¨ #Ãõ”°)QÕò4bx˜R· {„9†+(QúÎå½$êÞ'èo„¾æxô7ÜAwe: bЙË}Ü$éö”ãhLðDwjjî8£ôª*oX¡ê& û ×Ñ#5‘¥œsxJ«ö¤Ð#¸å½gm×!œkÛá&+n¶!Æ+pÒjιî¡Suœó;†ho3¤=¸ŒÎFýÎî®m ÚJ‰û—Ñø•ƒ”ò;)Û€² üÛÀdÄÝÓÝbTšó°#ÄI îŒí%|0B—ÇÑù$ñYpTù Ž{?‚ÛÏpÍc÷h;¢ƒëÎõ·0ÜÏ5ìT®gçñ·C\¿(U÷PšîÎâšv& :}-Iôé’HíùJÖ·ÐS{=eçÑ]ƒž|n9Ë¢œšº¡1Þ wmQzÉçt‘wFé§Ô%¢~|â’BÔpÏwnzî¤*;"ÍÓ[g|5€·ÆðÕ¦?ôcKqÂt5‚ŒV Œ–èç\÷႟&®„Z#ûá•‘ÎÆ2‡¾Îßuôôü¯·FÜ¿dÈ¿ÄÆ®,³rV¶eøÚÀdÄ‘ŽI6ioÈÇzDs×[OùðÒ]"¸Ë!JàÜ^å´ÿSiÍÂ}¯-(‹3DGŠ7z3C0Àuè¡ê;¬MDwÕ=IrŠêëè.»Œn®Q÷”Ð3Zñnù:¶¬Sh†g$ÿJÓ‰¨¸É½ßœPsÛE PqÕ…çu‘{FYÑG‘vꞆ©á·œ]°ßÀ}ä›qÞn=—0Dèx0ª˜‹Ú7Ò¯›:‰[‡ÆŒV!Èd B-7p?÷&œõ¤De‡ª¼ªæëNö÷´ˆºéìëÿè­·2Z•rùå;(Û±² ¼Û60q‹ë }ù#ƒ-Ë;ësû‚Ôá¬CâÖ[oeŒÒµ.Œ“í¢Î½Ñßáä±y¸ë½ ¹çt¤õææ$4Óê¼ùY$êÓÃÑ–AKðL€qÿuk -ÁŸ¡“{³{3N¡í ­ÃIÁ¨¿çƒ–D_ªÀmP}Ë×­Q>†bªÂ³ÎèqíÚ)T‰§Åhã%íëî»i¶BB„ÙJøëÌ—¤k£s8ɘ onóÔœO­Á4Zƒpë-8e¿Î{p>ÐåÙ7¹¦Þ„îžÎ9ôÇþ¯F‰»O1—§"ª¼ªòQß.ïöƒ+Ÿ§¬oeP¶eøe·Éˆ» ½#Ò45ÖÎéï©O*ɸgýuRô®FéòÑ]?}ÆÏ6X_]J²ú?â²ë6$Gh!ï’)·ŒÙÒÛ™/÷[{¢þ¡7ªî¸ ò¶3jï{ öŽ;ZüÑôÀM÷}ÐüÀ­ è$q·Äû£ŽÖê5wQwŸ€‡^ªï8¡ì†04£Eø9C<ÒA×°oûí£×¶T×ïÂÛõ8Iâ#B¹Ží«÷#˸¾ú‹áo°”e]`c¦±Ü„‡íˆñRCüeôw—¡·¯=·«·ëpGïÀ_tõõÿ7…¶ƒ‰p€Š¶H+bj·µµ) \9QX³£|Ùƒ¯òû)¿ßTÛ€Ä=[–f°¯uKKMÎðI×#0Ù ‘_à±å&†›¬B=‘"Yž¶Y‹[¾{éîT Y±(»e‹âë–$l;Tßt@ímGÔÜr$Y»£-чdín×j}è‹ö¤t<9޶Gþh}â‹znéj|ĵíÇ^üŸûÇoØ£”“’8kJõ&x} i‘º¸ã½÷h%~Õu;Γ¸£,W#Üt9"­Ö’À©7âƒDZ×7ßH/ki·§=TñìA8†z«ÚG†ÛÿïQI{ðßuööÿæÄ}ûöm…‰Zbwr¢Y|y¹’Ì•d®$seP¶eøI˜Œ¸»Ñ£!Ò´uuÍéêîØØÛZ–wÒ![pÒb3Nš®A¼ÎZ¯Çe§Ítuº•ëÌkqÛ{2Ok#;ö*nY£ú®¥g;ÔÝuD㪿í¤ß†xZ‡»¢#Ù‡†gÜΕäA¢&Yó¸+M·Z{JFiUwPtÉ %ÍPvÁÅgé*5\ 'ŽâI€*øìÃU§-¸â¸¸ÆAâ>ÉÉD˜15ÔøÁ<Ž°Ú€H®mŸ³ßŠóT“ŸqWENb­Ùkr^­žŽï^Ûqä!ᩦä=ÕÙ˜ò>åL^Ù”m@Ù>Ü6 qÿYš¾îöíýmÕñ·Ï¸!Ðt‚¨.4_‡hóµ$ëMˆåV«óÖ«pÆ|n¹oÆãàýÈ>£‹òk$l{nÛ¢ñ×-þÚK{­ãÑôˆÞ×$´…Å8ÑþÔÝéþ”¼½¹ÎMu9%urîË®¿í€ª8K”_0Eé9#< UGöIM<òSÁ]Ï=¸á¶ƒ¤½g¬Ö°\«¤µî“¦« »Ç-‘¶EZoÄ)÷yîݾìqçi˜–ÃÈfC}õ!²wíú«çuóº0UR–÷>eÇûp;žòÛ*¿­² (ÛÀTÛÀdÄýêõƲܯoñFùv3|f„ nn§Šá^hA”g¬FqÉa=øïCÞY}”]1CÕuî±&„Ä]ÏIúmN I'qí:ÁÍñ\ï¦Z\@œkLòNöãZ·êïPµ~“äMTs}»è‚ Š/š";FÏ"µ‘~‰´(à@2P‹¥C•X‡-ܳ½‘7׺W"˜^ÞÄoq’*r±¶}žQÌ®zA¬÷Qä&žåÞñFÿ ëãç î(%n¥ŠL©&U¶eP¶…Uå÷ükQ·’©r¶àh»-8Mâ>oKõ³ÍªÍWã"‰û¾ß^«¦DÞW)uSÒ®¥º[¨ÉîSÚŽwåZ¶' ÔÜÑJפÍ$ênÇIÞܯÍm`$ìæôˆF¯ £AÚù4LËŠÖ•ˆ;ƒ†pô†v—kÜ—·H¤}ÆfNS}Á€#¡b¯¶1×¹¹¾}œFtÜþuÊ~}•ïÁuߣ8ï¥N‰›ñ½{ê¤-`?ù{q u¶¼Ò³¢éÒÓÓ•Ä­ì°ÊA[Ù”m@Ùž·E$í!tüƒ,}CyÞÚ ¡ð7ٌƬc r¢HB=-H;Ƃ۱Œá–'×¹£´(!¡–jtÎ"H[HÛ‚¸…Ä-ÐúP7×¶…õøJá´>o§y ¼ñž³„V’»ø­ºnƒœÓúiçž9&IÝIAªH Ä}“îKãèÁMHÚ§LW"Ê‚ëï´*?e¶F"î$ð`"Òz3"wâ®ÜÄíIu{üZ•×lSˆ¸§ªæPÞ§T‘)Û€² (Û€² (Ú$î¿|¾öÛPîjOâÞD╸#HŒBU+­-¯F˜ÁB„[@Cµ-H‹Ð@÷[×ЉJ=%n!e ²®¹m/¸€ îQi›jr’vë¹í«ŽëÙBÊnºO#6JÞâ!}—5Bö“C #º“Ø…Û^{IÜÛ%i[¶ ï0õIá!Ò¶ î´~?E‰;Ö‹ju¿Ã¸¤Ëã£R¬ñî: %q+g·J GÙ”m@ÙÞË6 qOÛÝZጠ‹­”^×IÒlŒ°Òæ–«Ó–kEç'1+c¾W]6ãQ°*2£µ%UyµØvÃæ¹Ä-¬ÊÅzv'·}µ‰}Ú$n!u׋½Û$î¦1õxí-³‘À+¯Y£”û·…ª¼œjBEþ8X w½v»¹Æ}q.Û©ºZn #y ô†Ü²F§ÁT“‡’Ä…ªüŒû>Äy©â^°.ûÓú=á »ëM•Ä­ì°ïe‡Utf®L¯”æ”màÃkS%î®æÊØ¸H'[nåþhª¤IÜÂ8-B¨¦MWHÄa²ѦËpÑ~|.q×ݶ•Ö¸a‹_!u c4™qšŒ´ÅoÃ=¦ÑÒüž+–³–ÞÓ„^vÙ’kæ†(Š5AÆ)m¤…kRU.È{´ûºÛNœ¥Ä-4‚¸ÃhE.‘6ÓBHàBâ7§1ËnÜ>Îà#¦¸ä§%wG•RU®ìì^gÿ¥}Ó©NÊû”5ð!Ö@OO×o§û^½í5±×¢]à-<§é¯3üZ!©¥#u+Ì`N-F” o³¥’Ôý4ü Π’R·p]*H[¦&—¬ÈyQº¦Q×¹…Ô-ÔæBê¤Ýò€Ä~ß µtÜÒÀ(`Ut¾RzÉ‚¾ÊM¥‡Ô0 ‰ϽÜw¼÷á ¥íHóÕ´(¤Í-`”´ÃM¨Â'i â5]+§Ý9®‰Ô3Ö’ÄC«òþöJ%qÿÒyey?¼‰Æt)åýÊø%×ÀÈ`Ç¿åèøŸC}mtYÚ.ýß×ßõy/§}NüqOokuâõxꯄçQ:4áÚq˜°Ô&IFÒá‰Xߎ0Y"w$ƒ|âN!qçœÖ¥ºÜœa7m%òÆi2#5AÜ‚´yK„Í_!q7 PÚ.H»5žix\rÑœQÁ3;æ˜DÚBêN>¡N+öý¸á¾Ñk©X+IÜ"|§Ø&<»‰5n¡*÷I»­¸æ{)§­HC·GçƳZIÜJ"üðˆð—öMœ”i•5ðK®¡þŽ×ÝÞèP_UTX[š[ØP–SØT™]ÙR[ØZWŒþ®†sÝõ…}}íŸw÷´ŸRä]IÚ †Ññ_†zºþ†ÄýäÖi7ø®e¥ð;:¡$ïpú=ö£DÚ'á”á"‰¸ãœ71̦*÷ZaøM=ÆÊ6GÖi=”]¥3îç5V£Jœd-$m™uy Uã-$jAÜut*$îº;­dÐ!mçÓkZv´>ÒOjá)IûIð7ÙGƒ¸Òú¶ n¡·´L¨ ׬fë¤}ÜçœvãÓ„Ä•x}mJâþ¥ òÊò~x E'eZe ü’k`¸¿}CKm12^CÚÝó(xt E/¢<õ*h„¡,?¹sx° ƒýíF}}óyW’¶$©vwÍî¨/K¿zÊÇMEØÌµÐ\€ÝE$ïÅ$îQ5y¸áˆ1[.IÝ1‹šçvª²÷‘¼#A7;ç,]¡Ròx•p¬r“ç$o$ œkÝ2ÒÄ]}é¡*Ä-TåÉ!G$â¾G_åb}û*ÓÎÓ8-‚ÆrBU …P•ê.–ŒÓÄv°`†ô ·Ý‚H›-4P;L‰[¢ÑÛ^=ñ>nE*K™öE (IõÃ#ÕwñM•}HY.50Ô×zº¾ôÒîœFÊÕ({t•O΢&õ"îœöB¸/cZWçah -½¿¿ý¯úû:ÿ“¢uƒ¾öÿP•ŸÚ~Ò] aVôS.âYSŠ !)žÐ#ySâ>¡¿€˜ðcT™“ÄO’Ä/ÑH-Þo¥cî—æÞkAÞÂÂ\ †Vã•×l¸‡Ûýyˆ¤î6Ý¢RâÒvù+‰¸‹cMQ0FÜ)¡G%âÆiñ\ç¾ë»—¸%,Ôp©DÞb[§rr$~‰–û-Ë£l·âŠç!Ü1@ò­“èi«VH¡hýIé{úºÿ‚ø¿ˆ3¥ ~A7½‹A^ùŒorð>5ñ±þúÂÿñûT8eY>ˆ¨ÈyÒU’pÍ)±¨}ƒŠ‡¨yz©—}áylÂu(ŸECeæÃžöJ›¡–O ë?ŽŒ´ÿ«´ÿÇáᶯG[ça¨]â”ÞÞîÿ<8Ðùë¡ÞÆeM¥jò·_ ²¡Ã-œvÜÍýÒ›q–åQÂ3#o âÆiaKpJ쟦w(C} éû÷tgDE÷_Ws?v)·sq[WÉ%sºD„̨a·œhœæ‰Š›N¨¸MkòxzNcøÏ¦ *¢‰ÝuCi¬9òc P$ˆ;RiaZxrüÒ²œªr!q_qb¨N–ë”ÙZºeF–À_o1‚x,\ž œ°Ø€–?Ö•ëâAÇðì^”pÀ2±ËÓ¢uü /¡$ÕTßÅ7ýšªò‘ÊøYjÿ­"ëaoU¾MÉgP~/ñ¡¨O9ƒÜ8„qM:DŸû¬mþ2Âé7ƒ‘é/àɨ¢gbñŒ6ÒîGâÑÍðЇ×O¤>ºu*?ùþéΔ{§ð Ö—ƒŒK/c×}Žà†— Uàtxâ°±ÜÇMµ´ í y8®=_RŸ‡q V8%\¡2¿ç³‹ÞÎ4QuÃ’°"qs?öeЫZ¥îŠë¶$t{i½»ŒûµËè!­üš*®‰ýÛb}[HàÎtuj‚"É¢\—]zL;(©É“‚èL…Ä}›e:GOn"bÙ ÃaË Ü òˆ°Û†Sô ã@åºH¥ÄÝÛYe=­××Óûï‰'ˤ¯·û_ÿ^žL{:ÿ’øŸýCÿEžô¿„4ïbW>ãۼ˶Íþú’4ÍþúÜÛÔ›ÊÁ¾ú¿ØWÿû»,«òY^ `°]¯2;Å"Ñú4†á1ÃPDé; ¹NuöiJÁ­6â²ãvÜñUÃí ÜÐÁÝmÜóÓ$ñ&ñQ PÁŸ=¸à½‡8Të±g])aS’½î}·|öãžï>ÜgÍÛî\[vÞ‰XîŽænAÞÁ:  1AÚ?"„’øIª]v\‡”“‡QÅ(a"¶v׳˸/»â½¢1&wÅm7Rê.äZv>ÕæUôY^zË©ÑFH‹6@ÞEnãî”HdÐÝir¸:÷ˆ«!!ð ì§E9÷p»î`t2î)çÞmá1í¸Þ¨¤í£½ž,—æ|øqBÈ E0 ÔNQâ¾à~·B ‘|=½Õ·¦Ý20Üþ—#ƒm;ÜßöÉ@oÇì¾Þ®å²Lû;ç÷öwþÐÛßõ? I­14Ôño‡:þ}ß`ÇgD@ÿPÇÁÉ ÁµŽÛ××öW}½mÿ©¯§ý?O–þ纮$ÕTßÅ7n{}eü%=cºõ©¼ÿíÕZã*²î£<‘FVY±èË<ƒîg§Ñ}¥·}I¨ÜE";Ëœ7Ip‰Œ¤•DKì‡Áôn¦ŽG”Xªâ#z%úîD¼/=‘‘ãIŽ·¹Íê*ý€_æ}W„˜ÇN^; Ý“t”~W¹®|Þ–¤É½Üšsá}è{x©}µYð92‹jôyÈ:§‡Î®%ŸAgI,ÚJ. ­ü ºªo¢¥ä ÚJ¯ µ(M9ÑhΉDMòq4¦† #ë$û£Žk9T¯Ñ"=ÿ’ R£4‘B—ªO#4ñ˜á=‡p­›NX.;nÇm8)Tå$po­p=<[‚ q¡*³Ü„³tÂrƒ“—»a¦Èz3}UùÐ`ë?÷õÔötVö¶Wµ÷6Þîo îi­ëmoŒh¹9ØÓx­«¥æ~wKýùþÎfþÎ:kº£{ÜÝV—>Ô×ôh¨¿)±§½¦ižôw¶,ìîXÞÓÖt¼§­>n §)v¨¯Å±«­Æ³­±âqs]éâ܌¤ä‡÷ï%Þ½yýÞ«qn^‹¿÷ ·ãâï\{üàæOð„‰Ó’îÇe<‰Kò`ÉñqÏž&Æe¦>z Y©Iqù™Oã ³Sã sÒ$ŹqeYqU%¹D^\uižtN†’¼gqe™qå…Ùq•Å9ciEúÑ{d÷¾@>_ ¦,?®¦¼€(Œ«­(Š««,Ž««*‰«¯.•ÐPS×X[>âÿ‰ÑTW×\_NTĵ4T>Gs}%Ïý- UL3LÓ2­¼ÖÖX×ÞTý<þ škâ:[j_Fkm\W[„îöú¸žŽ ½ãÐÄcÆŸù¸iú»šãº[â{Zã†zÚ^Â`wkÜ@W‹‘ޏØßÝ8ÐÓj=Ô×~„XBü3ñÁÛa¼½!ùçÏy ·ûßÿ™ø‰ÿ‡ø+â¥oÊÿÿ5ñ×Äß¿!þñŸˆ¿øùßàÃ(Áð@ë¿&þ#ñ_É¿hÖ$® 4÷ Ôä>@cÆ% •Þ@_Ñô—ßÀ@Ù äÞðF¦œ2g jZRߢ”,Húé‰#xÆ=С:xª”šxJòëʼnþD¥Z:)¹A)ûŠÛ\u§ôÍÖW=öáºç>’··zEÒ‰£’aØMúYÆãÒ™ ¯C³`¿óXlú-Ì6þ#÷ü ñÁÑYƒ‘Öû”Ka¤'#]©DF:Óy.#m0Üx#·0Òpý¥çЛÏÉHÎ) 䆣‘ÑÄêâ]$µzã|]6Eá%SäÑ“Z:Uçi§tIÞG ´ÆMâdí¦>.œL¸ùA"ò`®ÍÓ8í’§*Uå4˜KŠEgõN…ZJOoÇ_÷÷uüÛòâŒÙmÍÅÁÝí¥5 •©ÈO»†Â”«h(|„¦’$ß‘ÐR‘Іâdä'ßBqê}4—fðÿ'(J½…ÒŒü?-å)(J»AÜAsIZJs™6ùOï¡®( eÏ—zIwbqÿÚiD{⸻á !ØÝÁ®¶„„7Ù±5ÿEˆ› Nðw[œðr@¨·#Nx; ÄËÁD'á!À-ž/#€÷ø3qè!À2Œ¥ ò²‡€8'Ãqæ7ŠŸæ'ÎÉî™ü×iGL„Œá„Ë/ƒ·ßeâ½äÁK÷Ë+TÊcbœêìB}˜Î×a~.ÏÊãW!®‡û¹¾„“ü_†SþnaüyyÃÅóYQ–—àÇÿÅ5>;àU¸ñœ;¸®ö Â=!ƒtÍ߃ù¸K8A„øº½„P^ôÂÉ  áǽ /„òÞþãÒûñ>Bœ÷…xŽB<“÷ŒÇÉ oæ7ŠSãÌÿ_ADˆ"OøJˆ õCt˜ÿKˆ Àéð@â8Μ RgOc<ÎE„à|ä ÄF…âBtØs\Œ ‡ —NŸÄå3§pålâÎEJ¸z> ×b£Gq!f §ù+Ãܸt7/Ÿ— ޝ]8ƒ¸ó1¸x&ç¢ÂpúdÈK8ŠóQá¸s ±Ñ'óÏFœˆŠ: è½ó„¯û† OçåžÃƒÇÄqO§EA^΋ø-—ó;¬æ÷ÛÀï°†ßwEËâ o—E2gº‰ »ìãÂ|\ñÛ.:!àï.÷v^àéøZòÚsx9- |âþW1> ˹„ífeDˆïÚÈ~ëy¼Šç–ù»Û-CT°Ïú‹Ñ¡{®_ˆ>zûÊY½»Wcܸdó’ <ž·x^Âe ©ïFe¥É I¾T©;¡ˆmµ 6",Òs*4Ænb¸p«zÝu;nò½¤í`6Œ·Í5n?­…p'a;©~+IÛBUî«ó£d¬v܈û¼Å²—®7Duî= u×þA.âîëkù â¿ôµ¸v4—Ý/ȸ×V‘s w¹ÿ.¹BQœx µ©P|÷¢Q̸¡Y—Q™|¹w£¤ÿë3®Òì?"˜~ôÿ†Ì8äÐ@!ï~Ê^FmòuÇŸGöh<µfs‹Ø<®uÏÇm× Ü‡mŒ¦zC£ô\•àM#:_4< "IûrmÞ5$çFk}ä‡úž(¿á@Ø#ŸRuΆ Õ¡3–;Ró;‰“‡ËöùÌMÄf®µÓÝ©°7Xþ|[HÙb½{Ô@.ZmhyN·§×üÔq'ÌíÕiÀ@Ó§r÷ð`ó¿$þq°«²¶$ó.2iXŸ‡†gPñ(•ñÁ\Ä¢•`*œ ¥à 4>9άs(Aþõ ¦‰@ãÓhT&!ïºÊîq¶’ËÿO 5Öi±®<Çmñ§h]èƒì«~Ì;šÏ¸‚’Ggq+Ê•@ƒ}ÞæGàe6 o x™Ã豇é ^fêÏáÃóÞ<ïmz˜8/c5x«Â݈Ǽænr®ü_Ϲ™‚³¡*\W#5éØÙà tÝùxš«ÃÇJ“M¦98Šq÷Š4,§€HëeÁ2XjH÷È î•`­Å‰†öKç|Øè¼­ø~lø^VÌg¼EŒÁdžï9<ï3ïq÷xZ²L,›Å!¸›«ÁÝŒïjÊw4á».&ÆAüÏó¼î&Òîf¼Ïì01Z×2ˆú{^7…ëáµ`>nã ò”òe}y²¾¼X7Þ¢X7¾¶:ð·Ó} bpÁN œ¬þœ°ø pÂ2!d×9A“<NNÄdN@ Ü̵ÙÞ4ádtG`ì𠆣¡:œ5¤4nfÚ,³˜8srk­ÏIç1 >o€H'Ò{Xèð~-¶k æ§Ã#°ã³lôe8Âã#°=¦>âqþ0¬õ=‡•®ÆÃRG“ÁB[2˜k„€™Öbÿka®½:˜÷>[•e9{#u8šðL5áÊ÷qç{ ˆ÷ÿ½=,D}Éðâ{ËÚÀO~Ç·•±c/K~O K>¬C!8 øY °Ÿ±mø±ûqŒöåáczž†{ᪿÎ:Û$ÜŽäh}Œš{ÞhN AKr8­òQuß›/Š–'´"dšZÎLªïÒç룓h'Ñ—ÝtE:gAég­xOJ®»#'ÖŽÄÅþ èÈ½Š¼»'qÆÇ˜¤ÌF`z¾$@ 9è!P †}8z’¬Q"ñå (ð@1‹AœiüI~þ$0_óC$oUx â¤|ì œ€Ó±ýp2ØÇc*pÐßݬµFa#Àÿíõ÷Jéœ àIh&j°ÓÛ#Á^o¯t¯ÈC¤‘A¤p1" òĽ2¸òø%HéDzñœý ÃÅH…Ï…³á¾ ád Þe/Ë»‡åÞ-Á^o×k ® ð=u÷J°Ó™²´²_QGcõõcy¾HÇzÔgòÛˆ “˜@¹ú dß\|^žPŒ›DLpÿó<9Q“-w1?BÞlW>lK2x‹‰Ä¼8 “ 1É“WæáÂIŸø –ÉòèsˆÿÝ 7n®¼.à‰¤€+ñbÃë,‡ 'Cãál|„D3 '£Ãc8DBS“ ª$ğž}@QØé€ ¶zû!ƒ® &Âø4âØFWàÀò÷eX ‚z¶$,¸ ËÙøèKç^… e“Qˆ:s0`Õ?ȼU`©µfGwa7öü¦ê»ard—ãÃ;'„ìº,Ñ¡0TÛþªÛ0ŽÜŠ×a|z‘—x¶È_”ÑRKÔóA¾‹ßI´Ã|×—ád4®mð»;ŽÁß{2ÈÒ¾¸‡u¯/ÛŸëD8óœ³û(¿©û°“Î.8iÖت¯‡¥Ú*˜X&án¤-¢ÜÔ¡·s6ôwÎÖæï »õ[ÓË·eÅ/Æ=÷MH؉œS‡PD¤å"`ÇEâ93ª ÍHÞÜW}ÙŒR1·]1ŽvÁ}Rº- Fq-ø$ýz‡k#ŸkÉzÈ>©ƒt®qgžÒD ƒˆ ${¢ÿ ¹‰ŽRžÂUÇ­tIJ„ŽZæ#š^Ô.Ø®Ãc®­—S:¯£T]Ë"UôO^F§tºRK_ä•ô¢VEÔ w§T‘‹ÜõÜû]Hokyœ ä’¸‹¹%ìÙ)-¤PÝWxM£áÜeÇÍÄVÉÝ©ð–æO«vápEHÛš\¶—û¤ˆ!Îõí‹Â">Æm—=è§¼Cƒíjo”¸G†HÚCm;2Ô’2ØQ6\þä *ÃÑL‚m$9×Ý÷¥t= @ÇC_†?sF%7§·$ù¡‘¦óµœQ-ßóGÇ’w¢7¶;H›×»’B9ó9Η·¥ ½rø1:RN ˜>^ËYé¡èÊ:ƒæg±H¿ˆpG-x›„…%g5JÒ)y«Râ& Iªâ÷ű¶4àú’¬Å +Xñ+]ç±ïÅQ’8¥:ÒÒ@Ïßñƒ²ˆÅ ,ˆ…$øê91@‡,ý«¿¯¦{Óÿ²{)½;“èGÁŽ!ƒ¡˜4üNb²ðŽÇö±sÊ@²<6£ç™^'æù¯äõR¾Òäf$Ö‰àH²p"áJšŠqìB Æ«x‘FÜ3£÷Ë´B"ƒ87Æ?[”GÀÑàߟe'ìõUÞY:Þã(ÊÅ •‹˜pIšjHÚ2ˆÿŸ“/ÉY´ j’÷«çe$.Ò½ u’:aúz¼Lè?´eƒ¸¼eOðã üu¤=žœÅñëÚZg‰pVÚ{%Xj r…øÿE‰4­uöóü( ½ q^· ëQr·˜°Œôxâÿ‹k¯ÂÉH¤… 6ûcjÌO¶xæ>‰¨MŽì„Ñá08´ÇÔ¶I0äÿFÒÿ£ªv¡ÂöªKª&ŸC¨ñKêÚÑßQÕ¦øÏêÎQˆs¯BR+Ž]ie›ìWÜ?Z–QÉÎSHo Àƒd0²{½H"2¼ âœH#îU…O q]¤“ÕûD¿ã%SñMFA͈®½Ãózy®&~ñÎ2iVö;Z/0þ»û uú=|™&<Ë6/Ôò~vTÿÁ—kuÞ¼ö*|xÞ—iü쩃/}ÆÁ—Ú!?GªˆÇ ;'Òyóþñðb¾ž|Ö×Ë  ”u'àIM’ ?M7ѽ¯?çζ?ÜØÞ_śҹ²­º°? 8³ïÉ þ×ÜØ¦Üù½=¨‚õ šÕ“êWdªo_aËBUº€?×üÄ9™ª[¦¿²{äQ“‹ç¸S%/Tå.BÍLÉ݉R¹#µ„#µN¦âDyÅûk‚}ø9dç&øuãÁóoÄ÷ⲉ.LÙuþzp)Amm$°nÆ¿ç ø‹ú­7‘N›ïÅï(-ŸŒ.¯¸™¥°!´A@\#^¿óêP¯³L´-ð°Ðçø Gí—IJëÏ“q7 îÔzšìãRän¸èm…ƒæ:Ø^)áé%wœ÷áš·Ù6ZìD€ñv7Ü„85®1/cŒìåHðÝ…ŒP5äGë0`‡ n+ª¢íHâÖ¨¸l² T›3òVéE3TP]rÁ •ü-¥åvY9æ.UÖt¢RB~)‰3AñFéŠÔFfèa¤ê÷£¨ ¤^Ëüê®Ù£–ÎSÒi¹~Ýe+É›ÑÄt~ ·µetžrqÆT‰[‘Ÿ„ÐIç+‰\Û~à„ö'¾T¡ žϲ!‡Y3˜¾4‹åΣ&ùIÈai ØZÀ?à¶´t¼rÕe­Ú×Iw¨ðæf<Æ3Œ[ày µ¹´Œ1ÄÏ0Àˆ î«\ãN¿î‡áîr®q·üÝ›‰{¨ý 49 v¢9ó ãꌪí¦Ä.î;¢ê²-Znð%®Z¡„3“2΂IÜåq¬H#”œ×GIº™ªñzΘ*.ê£ú*‰™/Xv™‚3©âX]®[h£‚^I7s±úŒÆâÈušÓ' ø¦/7ŸkÁRe)¬TWRå"°‚³¸å¬¬…åáugvjkˆÕ0W] ³ƒTÍ–‡ÖÀаƒÕ¡µœn€úfØrVh#a‹[Ímœ5nƒ•ÆVXðºù‘MTó­­°ÔÜBðÜÑM0SßÄßÍüŸéµ9ÃÔÚ KíÒùQˆt[`Á|D+­íL³CJ+ƒ¸ÇJs‡tŸ€•æhš7aüý6TCIù¾¶¼>6,Çë`ËkvL/ƒ-ó|v,¿U`vºB>ªRwƒ½®¸Æû_{ÝT«¿€ƒÞNªÊwIÍc'j\ UhCÀeB¨÷Å9™úÞ–ùÛhó=ÆAœ³ãóEX¶ñpd9yÿkaÀåƒqÏ–Á•Çn,ƒ;—)ܹ\!à&–-¨õqÎÝD•ÚIpý_w.ë¸ÑVÀ“Nj‰<©%Ç xš©râ¨FíÑ!Ó~€önbi„ÏqiÉ„Ïqãsdµ5ø)<Ì©¢›\É&|2»¡¾m¼„ „-',„7—Ÿ¼¬99´bZKjX.¾‡ÉöaÔÖÁ]”•idé¤ü¤g¾ž,Ã(Æì„ŠX×çúº7Ig"ø$Ä|h<ú&x ÃR¦Oæ+Dý‚èIvÞbm\¸ŠõqÉ0U‘ŠÉ'ábÒO¸s’(ƒŸƒ“\wNz=8Ù“ÁÝšéx~BÈ®ó×cû£Ö&ØæXÌõlô8/'ŸDIòY4ÝGEzJ’bP–Š“›hœ¶ýv‘d¢„a6›oÙQÐ#ÏPÚm¤”ÜH¢lÊ(Šà"v#}ÔRM]}Ë–êm!!ÓóÙmú§ðXAÒ¯$'•’¨³(m? 9ˆòóÇPÍŠ¼ÃØÛ—D䪪èÒ4'R ×Ö"âØˆµXŒž›)Íks\v׆©Ð(‹5náöÔg’öhÐRF« ^¬»ç u991™êödªÉ“(}ß§–ž»hU¾—(ÝÇRê>Mïi§HС¦ (Bâö¡S˜@á·Üˆ¡GÍéAކi—=¹ÿœ[ÞRh÷5Ô]Á5îIˆ»¯¯ñ?õu–oëkH‘¼Ãä5DÕuóUg.ê› 0Æå§‘v˜Öª\ˆ7$iÛ"“æö|ö"/BÕWì¨î ë7 DbE£ßWZFh!;’‹ÿiåÇJNciT¤†©Ò}œ÷»Ñ×ë[z 1£*a 7}£mßÀhû70ä¯ÁÖopl3±eôø«Ï5=ënüê| ½_À`Ë×¼ñ-ï}ý-ßBàØ–ï$è‹ßm³™×÷ÐÚðMæ) µùkèlå3˜‡îÖï ½ù[hˆë¾ÅÑõ|þ¶ï%ˆk2ˆ4Z›fIédÐÜ0KºGcì>q¯Æ†ïø >óˆsš›F¡Áµ çØÄ{6¾€&µ6±\„¡»™ey ÄõñÐÝÂõ¦­?@Û z[çð~ 3m¦Ñb~Ú„ÿ÷é²®´Yñ^ë¿~ ¾âùÐÜ êôk‚uÌúÑâý¢ì"oÄ9 æ{”ùއTï_ܧ%Ž™—ææ9âœó—A›éFÁgmuÀ÷mÖ· Zü&ZüFÏÁôÚ›f¿„W¿×øÿEŒ>÷©mÛö v|£]?¼Ó=ó`®²–û„…ÊB˜ï]³= `º{>Õˆó`LíœÃóxÿ<Û>—߇ßEÔëK|sM–íØ^D»!46Œƒø_œ÷máýâ;‹ü„ sÏ"í]Ìã…Ò9qM›Ïi5˜¿€8ž Ú›g³]ˆö#Ê81ty]G<ŸåЃ&ïÕdý¾qŽ×_`Çc.´¶Îƒö¶ùÐÝÁ²ïúÇöð=ýÝã°g Ží]*AŸÇz¼¦»kÑKÐÙù#tvÒPjç‚Q°.$ìæù1hï^-B“u6 qü£ ޔǚ»A›åÐÙ»º<ôö/ƒ>×xŽX ƒq0FZc0:°öÇRþ¿ŒXþ†*Ë aß+PYÎóÃHed0Ü·¼×`Ÿ¨—%¬—Ŭ¿E£`ÙÅÿâ¼€ëMÀ0Ú3 Ãݼ¶‹u¹s tv,‚Ö6ÖÅ–ÐØ2¿óø]ømøí޲¯Zó9TW~$!‹*ß¾š$î.ÁÈ`=‰¨#}åè®zˆP«ŒG½wÝ· -h/ê¸ÿ¹ƒÚÜ)Þ5Ãkr©µUÄ¿¾Éuå›$o· íf:=iŒ§¦—haØÍf†Úl£*»%Þ‹÷¹£kÔM4&Ë=¥,Z“7\c¸N®_Wsi¶"ŽË³$í6æÓI£³Bªço»mÆ%ë¸b³ ¹QZôQnMm2' T“WÑX­Šy•Å‘ø¯ØJVê" h¥î²+äF®m ,±ÆäBª¸Éuéœ$îÇm¯]’%ûEZ— ©;Ò|5÷­¯‘ˆZHÚîê?Ð@m!Uç4–ãµh:h¹(yMÓÆÓ«þì®—¯_/q÷ôôü랎ÚÿÝÓ”gÖQtÉ\¸Á™B=Ø<<®ÅMì¹&°×m·á6 rÓa=®2vèUWú…u¡œÝÜcÌÑÛ®ÜOqÿ®ËÜg°ò»´Ö»G_­÷é9æ–ëVÜtãÿtKwÝi3î»nF¢÷v½.ÓÓͳcúxÕF€Ö XïúÖ{¾…Åî¯a±ë˜ïü†[>'¾„1‰Üb÷l &ÛgÁxë׌¶ìÅuÂ|Ç,Xí™Í<¾‡õN¦ßöÌ·~3æa±ý+ØíýNæñw6Ì990Þü%L¸þbJÂ7Ýö̘FÀtÛ—̶3Ïí_ÃŒÏÅwÒ¹M/ÒÈð"­ìžY¼ïÛÑ{Ç`NëÊñ0ÛÎk\ÿ0Ýù-LvÎÅŽo~Sž3ãûË`ÎãñÏ7å„GÀ„ï5¦œÔ˜ogýîüþ9ÌwŠç~ Sæ#êÕˆujÀ:=ƺ1 Y?FœÌŒ‡87Dc>SÀ„ù˜Š:“ÊÊ÷'ÄóM8)2&ÁIØü dç'ø}¾(‹(ÓÛÅ8mù‚íàU|)µã-üÖã`Âüex^ž3âäp/¿¯8'Ò™p2hÊo8ŠÑ÷1ÝÆ÷mfû0ÝñÌv²Í»¾‚åžo`½w¬÷}Ëv9 –»Ù&Ù¦%ì…Ô¾Æ`Îok¹û;Øìû¶ûçÂþà|8¨.€=aµoóþnôû°ô9QÕÛð9ÅwýdÖ(XÖW!ÞÝD´AN(Ì ñ 71NT¤ 3óçÅuËÝsXþ¤ôâ>ñ.ÒÛðå(ÖñüßPä3#æ-`,ÕÁ¶hÌz2f3bÿyö1£1nå7% Ø__@üÏóLcÈ:0f¿6ÙÅzßÍúßÃï0Ö›ÇCÖá1Ö§€>Ÿ'`ÀsF¢_íþž¤ïaÈz4`_Óç÷Óeù´ùþZã ¹éKN~¾€úúÏpd Ÿóø‹QðÝÔùÞGùÎ|W-Ö‘ëJu¦Oƒ¬c„Á®Ù$¾$Æ›FÁ‰(ïX™EÙŒY.öOSÞ+ƒ yΘyË`Ä1DÀB„€86ÞÁ±qçדŸCü/Î31¡ä{²/Ê þy‹g™ïáwß;—ívÛÀ°çxŸ)ó5"Ž …}VLšÕ×~ë?…êŠ?áÀ²ßcÿÒßBeɯ¡²øŸ%<á¢îê§aÓ.†{02Ò‰¡Ö|ÆŸ>€£¥¸ê´–‚Ü^4ÑB»›6Qí$âÖDÈÙ6U>üß]OŽ£ó‰0ަ ÕÓãèÞËøÛýÕâ¸+9ÝÉÁ´ Ám¦:Iäù\oÎ9y”$Î(_$à®O·%ÒX:Á‹ù{¡û‰Ÿ_»„¤ü€îI/Ù¬¥—¶ýtú¢.´ØŸ]D齂DžT×瞦fù²9¥m Z—;Hd]Âí`™4H“ýf“ÏRÃ’ËTÈuäL×m 0²•û¸×K1¹Ã„e9ÃzŠ-`£{¸…×´8A)<šn_qߥûÔ8?®qW4 t|òZUyOO׿éë¨ù¾§¹°¾ŽÛ¸®‘x£hmwǯ o/¦ëH¨tÌNuW­V!Öd!Nè,@¸éZz¯Ù‹X³eˆÑg r}ñvâgçá,˜Ç9“°é½æœÅrD/¥ÿØÍ¸ã±wœÖ#3­Tº¯KÑBV„ Iþ(uVÁñÀ\8š ›ƒ$ÞýßÃJå;X"gc·Ü7vª?Â^m1l,„µÊ|fóžÃ–ÇN¼îrh œU’ ¹ÙpQOõÅðÓ^_­ep?Ì4j÷Ïý¬8Øî› ;•žŸ×DyœTçÃI  H™çsäÿÅs™F¦ué9AÝ#~íUæ0ÿ9|eÂŽç¤ðÜ ˆÿí̇Ý~N„žƒßˆïcÇz°éþùRzûý¼‡ßNÀv vûE^ ùŒŸÃQußýF¯‰4 ™vôÙ?Ë)ÈΞes`pí@íG8Z4ŠÃ£p:ô£tMÜ/ÞC¤q9¼®G–ЫgÄK᡹ žZËá¥MoGº«^‚¯»°Ö ëHu üfcp:ÌzU§ÓEÌk1ÜŽ²<çtˆõK8š? 5ñ|Ö«À¡…Rù\XW>ÃÏ÷äó=uVÀC{Ü4—,ãD—£cà}.GKï"ÞW¼— ÎâyN@˾¥ìÝÝÔùÎËiõº’ƒÊjzxZņÏ=Ê>Ãkn¬WQG¼¢-|gN.D?qd»ßöTØžÆàÄ_œÙndpâ±€£ ü6ÏËÈo%+¿(ƒ(‹€»:¿‘†p#¹’XÏ£+à~dËÆzà8áÌöã4q,à¢Æ÷8Äz;$Ò-c}¼€ÛÞt•7ÂE}%œyΉiT—°M/Åþq;gp1ÛÝ(T >ßñÐb8æxIJŠo*Ú–~#W~gÙ7qaZW¦u;Ìú>²ü%¸‰wⶪ—!ÎM ¾Ÿ‹Ú Hï;î~iŒõ3VG²º‘ý:‰~#Æ$Ž;öû)íýæb¢Ä‰‘±˜I ás"d²÷˜ pŒ6åDDšP Ü·ÝUû #ÃÄàð(F¢¿±ˆÄ­F•ñ*Ü÷ÝN-­ª´ Û“àƒÞä@´?ö&Qû çiúSÃ0”‰¾g'‰SèLå®&uWz=—E£‹çÛx®›¿=„H“DÃ6SZy“¸ïØrà‰¾”@Jè^\–åíGhæ>í¶”0t0}1ÕñhPvƒNSR¸N]Ç­_õôa^Â5tpDl+¢f¸„K¼b X×à…%y* ѲOQÒÓàv4:¡ºü1“¸-ì–ÇnÛLÉ{ƒŒ¬GŒÕ:„’¤'\X•{‰ýäT•Ÿ0YÉeƒµ8Mî¼àF¿ëBâŽóGGyE_û·¯%îá¡Öß õÖé©I)¸î…³´€»ê´ ^$a»µ´¼[Jék()S ÷ÚŠV qRoÎR´¢hÍÙ8ÏX§Wm7IáÌnP¿dº ×íÖ!‰*ƒGô/®5—éôý>ÉÅÒÌ?â2O¨Qí~„³kn^·ÅmDXì¢+¼Íð3Þ£ ð2\o£„ø|¶#Ðt7‚-ö!Äj?c˜îG°¥ Íö0ún‰k<1߇`ó=6ÝÁã8i³gÝŽâ j£‚ ó]̃×-ö"ÄrNX«0/ÞcÆ<‘×qSæi¶WBùhºKñûâ¼ìº,ÈO–çhzÙ=£÷[ˆ²âùDß7Ôj7¬÷ŒAóœõNbÇ+؉0›]Âmvã¤íb/NÙíC„ý~ ‘œÅJ°?Àh3ûqÊVEz÷p뽌W»¡–»%œ°Ü%A<7œÑyNÙ©"ÞGDrïe”£*¢œTÉß“vY–Âl"Ìö ÂíTÇÐnbœr8„ÇÈt:‚(gu⨄H'uœ´?„Ì/X|G"h Á|NÏŸ°UC¨€Ý!„á‡qÒñNñÞ‘Ç+ˆp: ⼋ÆKç$ˆ{ qo´›N{h!Æ]Q®âú â8Æ]g.p/Þç­óN{ió¼6b<µ™Fë%ÄðQæ³c8Ãw;-ÞsĈ÷$ĵ³¼ï¼—Ž„Xo]\ðÑ“~Ïñ²oäV1a½žFH§¤ï¹IÜÃý€Q„ñ› î[\ë>cµ†ªò5¢´@Krù$nn ¡=¥5îµl» H⬂8®q§^ @_GYÅÀ`çÄÄ=4Ðñ×Ü»½s¨»ª³£ð> â\@gïÜ螬‚Ά„ºìs´îãFø²˜CÈÙÆÍí´¤£ÁY_âŽíJ±:õMPFÕÁc_úõÞ…”\+×¢%9ÝÔqPÊ5†‚K´ØKd%>`¥\ãûóƜѸ";Ö‰á–xpÒ÷#q'Ê7£q-ÊW#œpå¤#nG»áV¤ nÓAËÝwÜ?ã…g½Î Ä£X<<ï÷ü?áb âÇp9.ǽ ¸ñ8â/#áÊ <¼†‡q¡H$IqaHºzI×"ðèªÀ)<ºv OnF0¸ù)5ñRn†âɵ <º€„ ¾ßéâ(/ùò¼?Ç"™iž^Fʤއ0 „ÿ)×ÃðôZ¨„d´‚G¬‹‡Ä£+Á|ß<çÇá‰HÏüRn2?–=õVÉ2G=G ÿ*êäëæúI ¯…³îÂXcuz% —O°®_ çÅy"þRî_â÷fšP~ Þ/Õa$ë/J‚øÿᵓH`ž qáHä·yÈï‘t=y=ùVÌs<¹Í{qîöéQÜ:Ís1Ì3ZBÒ æÉü“øžƒeȲ'^eÙˆ‡×ø×Ã%ˆÿâXþ+!/AœOºq’Ïy±|¬¿{—qïb\fûŠ æ½¬kFùIb]>ßÿëñ~4Ò@@ õþîñw¤ÜÁx¤ÞaÏNŒç˜ç9¤Óañl <'ƒ8—™+A§3/Ƨ“§Ý;ƒÔ»§‘r'OoG³.Å7ßæĹçàùäq×Äw”A|¯G×"ù#Ù×"ùý#XŸ/Às‰W¹³äµˆæw‰a}žæw?Ëú<‡dºG~zçÂs$ß¾ÀïñS<¹Ëô/ð˜÷>¾u–8ƒ$¶‘¤Û1?Á£[Ñø ˜îñ×ønñl›â8‘mQœ×ÇCœ×&ÂC¶UGcÚ&ë)‰õ”Äz’áÅxþw ¯œBâe¶¹KáßÂðàB(îÇž ØŸÆáÁ…xp‘ýl"\bºÌ6ÎqìÉõhöeöÑG¤gó9WÂGûî%Ñ?ƒØ®ƒp‡¸Íþú±Çq;6À­s~¯Ýq9Ü‚­$&c(mîGh£¤=ˆþ>þt“¸ ¥É¿ î›\cΉT§´ÍýÖTw÷¤@_Vz2CÑžŒ¡¼³¦ÛÑÁ¼tgE¬IÚùç0P|®TÏÓH4úùÛ_t=ÐC—¥½Å—PtË…?4¹ßÚŽ»šœ¸MÙB­žMb/»†Úûè­ºƒþšxºa£“°“ô3nôp nÓ£aµ:’ÄZ:±p ˜p¼ROõ¹ºÅªzneÒ¶ ì:}yBßè¨rÄ}Ë]¸;Ý.÷j c,× œ’µXߦ¹—Öð¦¿rAÞA´6ç„qÚ9§} Szi7ŽËˆû?N(q÷¶ÿï¡Ó¡Žb´çÄQ½`Ç‚qqž†eg4QvZ \whI¥:â‘/Ún™¢õ¶1ºÓ‚%…ê†$3Mg*+%…û»ù2g¹iÝ™Î×iý÷€y‘¤‹¹àßÍRsj8gGáèJ¡{¹DGª(h¡ÇífOÏ;#é¼r9<ºŒœÇqÈHºŠ´‡×E_èYÉw‘÷ô&ry.‹îR³]AÞãkô“~E)·Qš~%DQê=ú?EQúäÓwnVÚd¦ÜGNFò²";#™i÷ñŒi²éG½(ï±ô¿@vZ<òès7?3 ùYOxO2éO='ƒç²I(É}$“!+M<#‘y=Dî³G|Æhºñ(äÿE9PœÃ|èÃ7‡þÚ3ù>YcÈHŠCJÂE<¹wÉDj|,2^DVÒä<¡sš§×Qz…i·øn·Qœq—à»fÜ—ü¥sâE_ïyOï0ý-d>ºŽŒÄ«xÆ:ÇÙoJçsžÞFn Ó¥ÝC¾¸‡uPÈûůø_œÏe>2ä¤ÜA6ïù XïÙÏÁ4"‘Åçg&߯3"ƒÏÇ™â;>½‹ú²Ïå7È!²ù¿øߪ0+‰uôDúÍçwÈá÷È–Ò<@–”nôûòÛd>F¿ON¿)¿‡„” 9<ÎåùÜô¤çÈI{Ä´"_‘>Y)lô•?Yl+âYÒs¥¶så¿Ã÷ïóYw ¹™‰R[“!7“e!òÙ6 ²é×9—~ü‰âÜd %yO'Di>EóS%”䥲 =Eß=Ÿï.~Åÿâühš4”¤IÇE̯€ù¾ž+d~Å…ô5=†¢‚Té\~N2r³#›íW ‡í_ /›ÏâµÂ<ÆùŽAü_ûyüV"Íë òý"+ß…È!òˆ|Ö™ yéü¦cmüÎ웯B´«g¢=%ÝijÇlßüÙ¢=‰ï0†\Ѷ¤öÅvÌþ/ϼeÈßK<ƒí)—ß_ ç)ÛA2ûÿc¶ƒ— α-@kÁu VÆa°( íÏHô=Q–è‡ÇÝðäj AqðÏaÇÊÉà ’ARMM³Àü_ î¹ìŒ¹ìŒ™$Q*Å<Šù+²Ò‚t %b+â/©B1hb *ç1`åe?FYqJ‹y”?CEI&ï÷ñ˜PJ‹3QV’êò,T—¦£ª<_‚¸V^’Í<²¤4¥¼¿”ù‰<ËKŸ¡R¤`•j^©*á3 Å@„Bƪ-"‘‹‰@ÿ/ãoiNJ³ˆIÈéw¤ -E$ëâ4NNHÔeϸ¥"+"ÎmÓWæ&¡"÷1ñå/!|ßJ¾cßYñ¿8_ÆW†rðâÜ«i+ FQ‘Ï믢€çˆJ¢ªÏC%+x_ùç* Y.~‡jÖUµ¨[¢Š¨ä·¨äù ~7q,®Õ°>kK3‰,"›È!r‰<^CI.óË‘P5ÕŹ¨)ašq÷Օ壞߱¾\‹¼²™W–â(cY*rQW•/¡¶2OB ÏU—ç ’y T•eKÿ×VðzÓVH¨¯*|ކê"4Õ” ¹¶-uehm¨@[c¥q,ΉkÕŨ¯,D-Ë_Wñ"Ÿºª"Ô5â—i꘶¡¡hjªDss5ZZ^@üÿ*ZDq¾©Š÷U¢¾¾\B#ŸßÄÿÇCœ›L'ž[__F”%ü¿„iKÐ4õÅh¨#jùî ÑXS ¡¡&_B}5¿Që¸2GB=e¨­àw“úY愨bÛyÕlKÕÒ·ß<‡à·*á·*Êb»eÎO—~+Ä8P”ùå<.ã¹^/Îã$ˆ(á¤I@6öŒþr\‘À1¡€÷å…Ì{ âÿç(yEÏž£´0Å|FóŸÅ|Þ«ã–¸OäSÁ@N•|·*¶Íñçĵ2¾G©x–Q<§˜Àñ(ÉM嘔Êq…¿9œfó:QÄÀ"…YO³81ä2Ÿ31ñý RÙ(\%^–™p]Í¥è†iCàúö0×»‡{Êq’ËKÁ$î+ëðŒ>Ê…¥÷`IûY†‹b(iG`07Š8Câ>aª·‹.H’qw&ÕåÄ@!‰šÒv¹¨-3¥q¨b¯ºû¨|H­b:-Ñ]h¸æ‹N®— ‰½!ý:J®2 ÈmôTÜA_õôWÜBA,º)áwÐR]hƒ«ˆÆûŒÕMU{Û#Z³»Kf±g»˜Úå<¿ ‰;Y„õUA"ƒÜ#í«4ξÀ¥âKŽ[(m¯¥fa%‰{‰DÜ"2˜ûÑù´¥™#EÄB£µhªÊ/¸2Š˜?'Bâî,¯ëèúÝ„ÄÝ7Ô~h` ¾ ) -)átºâÅ5JÆŒ;*,ëè¶´³$]wÑ•Ï Ê Qc‘4Æ£‹•ÔÎÙQUƒõ7Y B݆Fz[ë,¹I“ÿ«hϧ‹ÔlVUý ©Œoš€¡†ÛœÅR­AjY!¨J:Ž”+žxF5bE攑LKr8èç³³äò8[BYþ3©#•³q ˆ×\]ˆ–ÚbˆÁ®¡¦b°ƒ ·FþßD´rÀho,E{S)ÚˆÖÆ¢Íu0+³8H¨¯ÊB]y&9à6õ•bÀ(BcI‘”æÓIÊ&FÒµZFâù"}M¡4ð´p jk(•îéà3;x_GC!:ê ÐQ—‡öš´ñ™-h*KE3ÑZž†öŠ4:ÁIeô´§hb„µQ$)ŒªÆóeéL÷ ­,»„l"­•¹Ì/mÕùÌ›ÏåeÝ´­¬§æª4ñI&õ$zr?ß¿žh¨>«a õe¼çeˆkãÐPÎÈp?ÁX,oS¥øn¹šˆFÀ ˆøõ|Äq#Ï5s°~ùh"16Š÷‡ñ­ˆzb]9 ˜ƒ•hÕ jˆZN®d¨á$«šƒsÊJª•lS¯¢ŠšH'î©'¡70ß&QãAÂm”ÄÛ$#\Aºch"qJ ‰¾ $àÆŠQ4ÍU⛕ …hªd›*gûä$£Ž“Z19)ÊFy\¢ÔP<㤕ƒiqv û‰˜ñ}91}B´ÓVv;ÉZ†¶æ*ŒG;Iº£¥­µèj«Ew{z:ê¥_ñg«¸VÃã:ž«çµ†QtŽÓ‹{zy¾¯«½ã Òvóž®WÐÉô˜oGÛ ´·Ö±¼tÕ\#¡µ‰“‰Æ*¢R‚x'¶æho©DGk5ºÚYÖv¾ ;Ûøn­U?…8ß^¶Væ×ľ<6hm.c9*y¯¸§íü_Œmümo)ç9ñŒho.çuÖ1ûy‹˜Ôñ{±ÿ7pÒQOÔ±?ŽG-ûh5ûj%ûly‰\€“ˆòA¶lD!&5¼V[Îo9†ö[qn<ê™WŸÓÊò·ñù2´ò¸…çšY1‰4,OË î«c¤üųơÇÍì‹íD+ÇŠ&N~8É®NªEdǺ¢Ô$£*'å:J(l¤r©§›÷ÀPz©*ïá:÷ðH/Fz«F[AdçmVPjUE+=q¦…£œÒ•Aëð<4ÃajjG ÎQÍ͵oŸõRUÞIž¤ÞÇõêN—u“¸[… êó²8ŒTߦçÏ@Ú`¥JÛA²ïJ a,ðHtS*(»ŽÊÛè Š¼«ˆdOŽ“ƒ¾´PºG¥&™ã¶Dú1á^î*:d©¿ï$m+¥•yÑEÉÃ[-È…_ò?±sjp‰x”¸E¨ÒÕÜFX­¥Ä½ÑôŒL‚!=}ui€xtž7®uûÐyɘÄí¼—}q;]•jÿÇ ‰{p¸õÇÁ¾êg}µI¨#–Ñ3ZÛ5q= ’:þv†1미ÉYÉ=®œ#is ¢€³ –ôpöÓLwpBêÄÝÅ@ä-ôYÞüä$Å=×ЙLr÷Goé5ô×§¡µ$ži¯a¨:m¾œ,0À]©&Ÿw¢·˜`”¤\Ga*C…RÝU@uZ!gt£ DÉ¡@I6)éUŽT–äŠÁŠd.ÍŠ³)±±CˆY-eAðl\2I®\Hr”誄„GU8ÕÂ9iw%©·”Ro5Ï ©N4jÑÀë«EçÒ€€Ä L €äSÇAZ þù„AtX¦‘i›IœÍ‚ÄyDN$#qo' -UÙ$Xne&;G:; YvÕ3t×f¡».û9ºx,Cg]:ˆöº\‚€:u]!ÚØ9[ÇÐÂg¿@1Ù‰8Ñh¥ôÓ.M(8@=Çø †èüELWÀôù¼O€ÇÄDi­|f+…6í$d¨p‚ÃÁN<ÚŤI ’Dõ2ÄĨ…“¬V–­Mšhq ü Ê9ŠÁ”ƒu¥OJtM|'ANÍ<È«¤ßJ˜âú«Rëx)VH²íLßAòê"9u â’@’j%y¡«…$F0Þøst‘\$p&Bw{z;9{nblÝ&®[‘Ìx®›éEþ‚:Hªm‚ˆÄ;í,{ËÕI‰·K”‹×»Y¶^’QI®¿s}uÌÿeô0M7 ëUpË'Iµ–¤Êôcÿ‹t]cäÖNBj$Çÿ;y^ü¶²¾›ÙNZšXŸ„8×ÖÂï ¬M¤ÿ)F óÅd¡“õÔ-ꢫ•ÏoeYZøÜ&Nùþ ü¦õ/Aœ{ ÍüŸiÚ›Hêchk¬e‘ØëYV¢hçÿûÄ÷#zÚXçü¦¢Ý‰IÛ²@ëXLöǰ8‚G''ݬï^æ%À@z˜¿…¸Þ!ú ÓŠ{ZëÙŸØG›ÙoG!ŽŠöáQÒ¼·}¿£Fœlsìj æ°ŽšCQÕ$k:  ÛÜXÂ:) &0ûÊ3®KH£0Ö^ŸCy úFzˆ>JÜíê*Á ºBõÒ]Ȱž+¸ë]“Ú£/)䜎Ô¢Ñ:üi ºŸ†¢‡k½\f¥ÄÜüПZ_Æ·àÖ¯6±&NÂî£ ½•¡7»HÌÔ ÷]FÝÚ—•…õ0ŸÁ¼hôѸ­;û ×ǯ ‹FiCå×1\Nž¢´Þ“Á ­Õ{ÉaíTÛ7swU幋ðžÖ@òæÅô›.¯<ã¾í$F'Kðçº6oÓmê wVñØNc´•ôƒ.TåkèRu¢,×á¸$qÿOÍÜ­ v›Ì•àIÿå'̹ϛÆÞçHÜBUþ”Æi ôõú}܃Ãíì*ñ颻‚¤M±ÝEçèÑŒþb/hÓ"Ï…ªˆs ‚>*m·?uãK‘”¯’°ƒ¹ðoÉõj W_$£…‹ø­ñ4ã—ÖË4̓&øö’•__ãr3üg_1­úò¨’§wœqÂã(+$qC¡ÒîD!3µôûg$C›ô$<ãŠPÃä$]CîãëRìï|®{‰5Ý<®Krí¬ˆj›R®Õ‚k€$|‰Ü©.âºi!×Åzb)ÕÃ¥9T“s}»¼à ÕcTçSåL5v ½”iÊ…ª–ªïr¡Vê'ª¦ªùP§ u!®•SE.PV$Ôbœp°Œ“†Jª¬Ä=2Ôp¶*:|~]Q*ê¨R®åLµ†Ï®)xŒÚÂ'ɹ¿³ö9ddÝÍI˜N†I‹‰ö 4ÕäQø ÀPEm!ŽÅ9Äÿ2LxždÝHÁ¢‘Á¢¾â™„Z U%b9LŒ\®#ª ¢¦0AŠ!]•y•éW%<ºäCáä Fj¸—»‘ÎWšèŒ…ÒÊ$}ÚÈ­ŽspÚf£jíE=I±“®°[¹ÏZpÊ`¦Ø“‚Átjx)…w0¶ØËÝMmðÉZÚ»Mc³nJÉ#%çÉ9Qt}‚‘+¹Ÿ;÷,jéõ󾟪ä:µ+™÷R ÞÎ5ô.Z©Pºm”æ…5ºà»¹œ;”yŠÛѸþ$€DÍøÜV{ûCWæÁðžBê¦[Ö\zJ{E59ý”ÇsÏöm_ ÜäMâ¾@_'§)uŸ¡Ä}âØbœäö ªËEP!q I[&q‹cIâªrîã¾B«ò´[A\N¨~=qs…þŸûÛò}ÚJn¡ü¾;2Î3(UµôùZ}›â їεVdó]+4Ü0àv{VR8éì]çm9“ •Öê鯴j„NVnïk¸eŒª‹Z4ðFoÖi®PŠ÷d F»iJïl–È¢G¶»ÚˆóÑÁ\¡5âåÖ¸b…‹<Ž µ“p%ÄWCpã¤3nGºáN´'-ͽp3Ê ·¢}p÷\î;ŽÿÿöÞ¸Ž,K¬\)$­FiwfšÐJZí®BÓ3Ý]í}wµ©®ªvå½÷ÞÒ{]Ñ‚ÞôÞ€ C‚$H‚ Hxï½·ß»»ç¾Ìüø@ÕU­™Ö´ˆ8‘™/_šŸø?ï}÷]soñÓ»ñˆÜ÷ã~¹˜Œ‡äù8}©ã×iÉ¿’‚ã\I–[YÉ8p¥â¼uF Q ²NË-æf sÎÊ­œs8~\0·  G25TI¸…³†rÓvظ‰Ã†ö)¢M:L_»¿S8Aé±UP êHgqÉPƒ˜*#yÌA)+ÌÓ®RÅ8㔪cqˆÂɪÇ8–â[6…ÙR\xMŠ)l_‚U¢”y¨RL¨±”±]n`}JpF*FáÑe©:©cÓêx„‚S…R¾êÈdQÍÜ×xª°~Äö1ëÚ‹H ZLÔõXD0ù*õÌ9×1ÿ<LÁõºÓp#ŒöÕ~åR[Wf¨S…„¹é&”¥e¤«K¦ï–µ(-õ˜¹-¸ŽM-s™5[Áý”TÜ’[%7¤s%¦óz”ƒ&i3&ê„yK{S”æ¶Fö5HcK½44×t]Ñöf›Vúµw¨b€휣•}-ôÓeûÛÚÌgÑÿË ‡ŠÕÙ å°EÌîºlÁÔ­ŠJfóúwržÎöféê`Î:ÛümÍÌe ìïèhå>Ú ºÞF›Òι:ô<­Ì?·¡8´3wÝaÐõî®véÒã8ÆôÓëÚýºYêv[ϧE?4óÌ ­ň}Ö9ÚFAàö p{0{+Ýü.Lß(,±t±Ý­óã:·ŽuD­v„[›*_γAl±iEIthÁÜ„²ÙˆI¹¥TÑõFÑ1hŠ«£Ü:KõWiáûÒ¦>:õ‚¢©ÓeÝL5µr®:òJ±Të @(úM(æÚ¯?†N]õihc Gçšu~¹‰i3Ç ªük»úCtrÿjésFɽÌí÷0]Ô­óù:·Ï¿³µŠ;£ß¶zµ Þd€ßèz›M«nÛ8mÖRÛñMà üNò˜žd•X!+²ô\Å"‰ïMå5æäñÇ©bpT‘E;N±eøÝ”_ÂÇæ’´”gHs1#í›)Òxã¤áÄ–Ò[ÃÚWÛtß¹}„¯Ñ_9¿ß:ÎA·/Ø`È8hsh#¡n›`³dÝ*Ç·IƉx¹˜”0ÊÉréÔNB3vrD¨!TY)„u¤îgËnã„Ø\;CÈÉÂT@ׯ¦&‡p–´-›}Ù„Ü\#ä&Ê9¶c¸NMîE¡Ûq)IòbÉ$<%)e©àjªÜÄ õÞ¨…×ÒQÎTIº ùx«ÞPoÕ«g%7ë ¤±Ž×*} ´Å·ð(.Ä£X¹…§üÍëçØ—NËÃUѶBõfÇ{Þ¡¥§€cs¹vvfŠ\!Œè Ý…³ÇÌú Î_¦ÞÎ(-J%–š ,6åÅx[ã½^Šs¤R†R¤èz êÅa žíôÑcª˜Ê©AÉ©Cá©Åa²ë*Yªl…KÁ™§ˆ{É»’*W54)%R=ÿ¹_Ë#Z=­Õó:[ªQ¨Ô ¨š{©‚Ê((Uþ*†”ʲ[6…x{Û ˜TŽ£ª²EªÈ ÊTmÓ}†[QjÔAÊF5K1‹Qæø¼ú™ýücÑçhQB¨Q(‰e(š¥xt£Ü:”¨³zúÛhE=Ñ7:ϰÚX͘&ÓÿΩÚfEðŒböÕ`M«ÅisõäWˆîPˆð¨ÕH¨Qo¢ô™—£P—ä]´¾“|•B]òÌ%ô,‹ßÇÅÓûäRò~Éä÷vEòŸÊá÷‘—qœð´DÉ¿t‚> ]×6Ý—KHâu­rÒ5‘°+¶c .'Êw’äX¼³ó/éùe$¼ÌA· ´Ý ÌLÃòr Ǽn‡aj(¦†dæƘG¨†uæÝãï.É#<Ñb‡áä§%D¹‘J¸iÊvÉKÞ[%‡°Ôì“›%+ICn7ÂÕDBqÅÉå#k`µd•£k,ŽÈäÐr¹rp™áø”¯¼ˆ jUg·J õ(ê2(Ø‘³›^ÈÆ·~h }dm}êZïSzs‰ô ‡:Ò©ðîb„;LäÒÐ5 \1øë'gøNbºÞË~Í`Ö¯ÞÞ:Mó^Jpödâ§Åà³öôRæœ_DÆLÅÑŒXvÿn⯇®‘*•ZÛC×7Rrz eª9çí&Ū:[kæ¶Ajx·_XBŠSdY^ägæ““|ª©±Qƒ<Ì'êµø—åâÆgÈŽö¨œ]ûóÚTúšw¯ýè78¦=*©k—S«—#K)4B]nMoºâ" Ó‰å~ç§x•«ðþù<!GÁKrzË»rj×L<úIZÓwì¶ÉWû…g°º¼³2YJ1œ¡ ÙÒ—æ1Ñž¿‘„)+~)Y«•œ ÏÉÕ¸ÇäÒ²_È•÷KAüKRLÐ|æÆÇåüª%kÓ³’µþq¹´ü7rvÑ/$“ؼܭ/JÆŠ$eÞÏ8ǃ’¿ý%¹À‡¸÷¸ä¬ O¼g$kó³à“èeí $_yL6“òtã‡`÷P¶ÍxLâg<ÊÜÀ#|Pª©LQXŸù$ÞˆO£Á dþsxä=K,Õgh‹Ÿ¥hšgh£}ÉN41˼ç%aîsQt[µè¾Ý$WQö´E•…½$YÙMâœ[“¸ì"©J‚*,÷“tä ‰Dö,yÕ  VvÐG÷+š¼EѾ»¿B²’×Ì1û—“˜ƒz¬rH‘¬!¡‰CÉ€£kIÂ`£Û‡I ¢Œ&ÑÄ!$Ià<ûIj²ä%£8„ûW¾Kr’÷àý>`݆$¢LaÝâ઩cжý+>äüpÞ8/ǯþ€Ä"ÊA’J(‡ÖN‰r˜õÃ릢M$8eiÃLÃQ¦c(LÇ6Î!¡Èy¬¶)y7×b˘m3‹¥Eœ‚Ó¼™ûR6ÍäÓm¦I ‰wbIfû´Í)’ðœ"¡r’$DI$:÷Žœ éP,IqoËɵo“ºóéµoIÒ’¹Ì{BW½*Iq¯RHƒêY»gÊŽ™¼ë܇ɰy™ô×yäý¨¦·ð43Y;ÎÑ]1Ug,£èÇ*ÖÉWŽ ÖeÎcº®Ë­“ÂYó™w_^ÏI²2knûWíÇŒàN$ÿùqFß ;DÚSMwªóÜ›Ößÿ9ÿî’õ’‰tÑ£Äo¿Å@ò59½gŠ$váþ”Û n\¼æ¬Ìï(c.™Š*§Ý/g˜PÏ'ÛYÖš$mþOå4éKÏ¢1œûøIžûS²¤Ý)gÐ,´ŽjâÂ»åØœ»$™¾¸úaÉ`¾âÔŽYð 9Ë §, õé´ïKê¼»äüòä´¦L]p¯¤r\æÚÇ$sÃSr‰dìgÖ<'»gý†Ô¤š‚SÓT’úQÓ'ÂZÒ*®ƒ ¤]ÜH:F]j Ƥ9Üðî¯Iëç¤u$} i×iÊCïMúÆ ¤Bܨ©u 4…#ífëN»µïô¿—þ¿ «ÍodÏ‚ÇxàÏȾž@xHvÎ{+ÀSr뀢û.yÚ }´ÿι›¾Öõ˜]´ï^ø Á“drã\lÇÓ¾}ÎòuÖƒ²yé4§ß‡éˆÔš¤¶Ü[§Ý'Ûi‹g_ÂÌȺ£™w8§MÂÌM»îŸaõå~Öˆ²mÚhzÈVRÊn™ÂuPÆ@JÍ-Sïãš÷}§£8ÙlŸñ iúl¸æv‡œ ’Âï¡(ÛY7Ì)tg(Ð÷1¾Ì£‰òLlÈœ7Êãf,;(D£ìœó„±Ð|*óžB±sxšõQvÌyï ½Ç º½sîSü¿ž6ì0 *óŸ1YøbÑö›x–Ûcˆ§Â‹x®¿ûÜÎýÆÏå³ñÛa“@†@™G¡ý•»“síZ€‚ª™ö šy¥Ó@6+“%,W²ÏA‚Wàå1ì Ÿá#úa­rÐ „š=pšùO³ jF@®g² Úå7ªø’Ípï§B¬J±²oñ(ûÉXx€¬[—’¹Ùpÿ²ýÑg/·=díÚMöÃ] °ÈÙè3™þwóõÿöÔ(óy† ÆÃÿSíÎüßç3¢>´an sxÑ’¹1žß›¢ë;hÛ ñü¶ñûÜLúäͤƒvØò!H6Û¦þRâ§ÿʨ]³ï'Ý¥&áx˜ùÎGÌhs/ë{x?ì†]úž0à}Ìwcgìý³î|âõ;ä ß)òïÃvÖ ³ƒG÷Ì,~oÎo‘ßõve¿WÐAÑ~»f=B‘'Þod¬Ü »8&ß÷VÞIÊ6ûý¯ï$¶ã§Q,c*#È)¿”­¤óU¶ñÙ·“–7ýJúF2å}üïûxgßKzÚ»Í;ãèÇÏËê×$k_ýŽìšúcBÂ$ÑY5)“Ù‰Àî%¯x3Ü*<»(Û©BZ‹z8B[ëa+mܪsˆ¹&§8sÙZNºíü*ò‘,Æ”MF;RªŽ´þôBs¼Éz†`V¯±Ù*¼µˆH3#ü.®©HÕ´Žà`Ž»«qCÊ\Ž[È~ˆËËÉÏâš„‚‘„¬`/£î„Wåâ&jic&Wá´Œô¥‹î“˜Ê,ú5ÿožÉtÍ’v7Ïðä×Od«&`Ap¯}ç'² ¹·cÁC îž'Óâ«röYÚZrqèëôxÝߨTè|ÁÝÓ‰§]iò,Iãb©ó#ùĦe||BûÇrrÎÝ’Œ0×ê)§çã¾?ë§lß'kž”#óî–#³~FóŸ‘â”,8î³î”ã³ï’3œëì’_ÓÿNÎóI]òKIBÀ{—œ˜ó¹²îqÉ$_yÕÅ26ðC}ŸÄ‘“{ý»|8r/¯&öJÍÿlrq“[›|Ы4‡6íqïhæ-HŽ A@)šW:î=Í1PG8m¡m ÂwÓTÞä|&_´¢ù¢ã`­æ~ÿ^#¬×Íò½Ÿyd?°µ!4¢-Syð34óÝìÃ}ªæš½Ç°y _V~œñü8µÏÖiüˆiÓ}zí¯l`]×c6i?ØhúÞm4°zmÕÆÞýi”M¬;lfÝ ÷1íã·/Äú·GÙøîÏÐòî’Í|£õ’°ÏÆõÄ*ëÞ¾Óâ­±¬ëDzüñßæãÐöXÖ“§[Y÷&ç‹¢Û´“Ÿ{¹º7¾Íçt W÷ò²g#}dzÁœ›{b]kƒöå¾6½Ãsy—ÉûüH> Þ- ˜Ã62e«Ífúlâÿ½cÖs¬Ã:Î¥¬÷½6†uÜ¿ÃÚ·ù:k|í[ªpbÓð|gW“G~5yãW“óy5ù÷W¿úƒ±¼ò}Yõò÷bÐm O¿9F×ï¿?^óÚ[èoD•\+'ûJòô[yÉ5?»Åj›5šWÜÆiûmKÍE¾Ž¼ÚøM)ëɇ¾žßØzòþ;¬{‡ß•ÍZr·+q< žÁè³YÇóRÖó[V6èïÞF·uŸó ãø~Äb)ø<Ð\Ú&‡?ÏuÏÕâûQV¾öúLņ+õÝÂÿÆaëŠ>ó5<ïµüÿýÿ­$çÿÇ/|O–Qühé³ß–%KQ–>û-Y?÷mYñüwd%¬¦¸ÌŠË¬å»ž{XÏ=lxã‡Ôaжu6k_û¡(q|}ÆñùÌg¤ö€¢Ÿq,´±ݶpžËÖV¿Áç‰AÛ×êo”ï¸õ›ä÷Ãï@Ñß²ßüÚ7øî½þ}Ú(ß“5¯ñÙºk^ù¶ÅËq¯|aÌçÕÏ—·’R—ξùÁÇwcïlæË^ø¶¬~ù[ÿÁåÄâ_KVÜšDJ=#ŒˆénÁWª…ùäV¦aMÒ­ð¥Ëqw¨°%·¹ b%×R¤™b!)sn…”à7uy J,ÎoW/ÕwÜÆÕÊ1zœÖÕ®£Ò—æ4×"":"oâøf=}û©PÖÁµÛ¡1eŠ×:ËÈ;Eí¢ƒT£º˜ ¸! 0È=¼à—Œ¼‰Ëfä½Ù¹ÓùÖ©Ôƒx‹gøÖà‡²Y勾×y÷[‚ûaRß¾†eä=9shN‰Uê‰?âõ ÿé„‚;ìoÄ;PÞÓ‰ãXUÚŠ<,)”ZË£JÊåÕÈÙÅŒ¶ÒgÈ3žº„%fðt5,ø5UÁ°ËÏù¹žq§ìŸö#Ií?mñ/ä#ò£÷Ô¥ôGp'Îú¡œœðþènFè¿’cûc³~DÀúÓÔ@}F.o|<´kâÜ–¿ø- èCH?²X?äÇ¥X?2Mx¿B\|¡µ0† zýRh¡îüÐßá‡ÏK|5_À|‰bYÉ—xÕ›|™Ù·§¬£ï&„÷ºwôe£…7ôE©E@ôÚwòBâ\æå©EAô~dö9èöj~Lq"í«KóÒÖBöqcŠ“Ä*Ñb(öT¨úò~À ?æ‰à3Äœúèu íkøÑ)«ùì«ôåõÚ÷y¦ß㾿Ç3ü>nÏr}ñqŒ>w}¹9/ ëÞøŒ|.}¹Å2ú²±>ÓØÏe½Ì9ØgàÙÆñ¿e-ŸwŸËA·£˜—ŽEœù¼Üçjî×Àº¶Yûyùòâ_Çÿ#m[­ß3þ‡+^æe?mãû¦ÅXlVQ”e5ESÖðÝRâ((ã-Ê¢rè§ßO^”• /ý•z—x¶¼ÐÇ€Ð^áðÒwÙ|…Þ£ý3ß;Îm¡Er(ä¢Åo´¨‡ÍrŠtÜŽ)ð±Œ*cK) ±˜‹(±ˆ‚Qmë#­¦ƒVæ‹ÅÙ·ˆ¾Êb„˜Vòå¬SÙ –Q…OÑ¢6V›îåcžÉÄè±£,Õëp½ÅÍEÜ×(T`C`ÞŽÅÑ%Td‹e)UÚ–rK©¸´š ²ˆçñÏcçS9Ð@E³yOiu9ª˜Q‘nÎãßÃ<ªÑ-xŠJpô[DÿÅ6K¨úfà^cYŒÀå[¬#üm–RYné³ß0,yæë²èé¯ÊGO}Å‚õEÏ|þ_ÿã¹oÈbXKŸû¦,ƒŸÿ–,G®x¥ƒïÞ*XùÒ·y³ï…¯¡¨|•¾_1,yöîí˲äi‹¥°ì™;äãg¿_•åÏ~MV<÷uÃ…õ¯Én¬;}¯Óle udésæ7ºÁ½ùïI²%wÇ«ÆA­ÁÜÍȹ‘Ô¢MŒºë’ç™Ä'¬kæ2Ú­p-­©Õºj¨ÒU uô×:ÙšZ»äø\FÀXe¦ü3üó”Þ¤’çiL]`xUÒæ«e-¥§õx=fCkCèä̯ܽ7rœ®wž_fŽ­Jš†_dFáåÔ/>ô¾Ü¤î÷éåãˆö°eïcÎÞŠáÆÂtê3’²ýC¦5¦È|¾V ]î÷é#nweê,ÉZ÷˜¤SÉëÖl÷k–ôEwÉ9êÊyžYò+c:?·àn9óÑ/åüʇåøü»ä8B8SøYæ·SÝͶ îŸÈ)ú¤"ÈOÍû±¤-¼ ÁÏ:#îS‹~%‰s~,¹Û^¤ˆúK’Å|z&AìG?¢²ZɆ·Œð5nÔ87F‚º­##}ië‹ íÕh…:Šá‹ÇèO…ž£]®a¿Vh‚ºTÍ{µjß1m[þòw *Ì´¯¾Ø×3RPâô­/j^†kÕ¨¹~-£]×6‡8F=ж›ý }œ6Ý7æ¸O…Ä9çz* 9¬c=k´ãŒü8åÁbt¤‡f­¬‰!íZœñ¬c4ºN­˜·Vqžåªa9÷³œÏx;Vðì´Â•²ÊTº¢z#(E-%QhSì™Þ«*M멦5mWô³8J•*V¡ûGÑþkÔŠ ÙQASåÍV¤tT‹ àå^"ºŒ e# -–kE9RËTx!d–±þ1Çâ;¼?ÉÇ—ñ˜‘Í–Š ®Å*4¶‹©Ä´˜ò·C…’–]ʽ)K¸7í«eC?âøé±P^Aã0!ã0‘e, Ø·{1¥wù< žc±Î£û¢ØŠ@l¿ù-%öÜóµl.çE÷Ï¥Üçœ'¿>ms˜£ûa6}Æãì›K™ß9 eCgPêwú_§ŽúרÍþ5ê±U>xä+òþCwŒåá;d û¦=þu™©eGí£sÌZft‚{&åegpžiô›úÈc˜öè´ßA Vxì+c˜Áö,˜ sÿŠÌ{üY`ø²ÌüKl‰öQæ°®Ì~ì‹2ëÑ¿5Ì|ôo 3ù‚Lø¯eÚCÓmf>ò7ôç¸'î@á:º­p=E¯=ÿ‰¯È˜oÚ¹Îãk˜ûÄ)ƒûEJ‰ÒÄ_¦í”IF¡€EÊÓwÈÂ'¾$󹟹Á¸äI‰Çª¸+ÈÇ(+±>¨e1aæý(–úFp¿÷}ãY®Ž^Õd%ÓùíFLßU‰³ÎÌ33Ç­£c­ƒ­B¶6EC²ŠÉ*=6Ý$FQñÓ*;1Wr÷~€àfŠlÚOPž6B» ¥ ù Š€ÖÏÜÕd@ÓóVž˜é›ÚÚÔÎÐäCÄpw¤3ÚfÄßÄ58¾‡ëòcr<™Ó(B¢Ê€ nU,òpTË#4,8nà©«ÕlδȜ{Üj™ÅJø>‰w„u Â{;ÞåÛgþš*iÏàOô,ÎÒoIâÖi8( tÃOÜ~Ž;Ðù ß@EygY"ECfÉÕ R4ä7R‡_.Žg—Ý-çýRÒãtÆèû¢ŸIæÒ{åâÒ_KVžt8«[v¯¤#´3q8»¸š9ò%÷J fñsÔY½°ò~Ž»[.®ø‚ž9òeH=å£{Éíú²äâBŸ»ƒäì ÚÊ–w¿‡©ó˜†Ä?ôN´‹Í|è-ü„6àºïÇÖúÛÛ71C½‰iæMr¿bâU6`î]oL¾vö+ëèc mÃ;œ3ñ&£ÐÖ£0lÄÓOM¹j>²Pó.¬}£ÛN{ìR÷+Ú×ÁiÓ~ëP8> 5«9XMÄ›6›j›n›Ê[0G:lÖ©ÚcÙȶÃŽÛ &h5ƒÙ¦05‡™û‡µ|®‰pF°ÎHUG«–åá¤Ö¶°K‡ê4ÆDXåH­²•¦t¥–¶T+† ^µ‚ØÄ©UÂbµRX÷³N-(fÊZ5Õñ¿ü,8ÇL¼Ôsò¿T“x Úû Ös/¸ÇM([ÔÔŽI};/­Lcè´„5]¡SüOltÛüŸÕÔh›ö-“?ßó¿´Øhà{f›H×bíˆ{YM©jRËF;qŒ¾ãxÉ)«u¤#!Qß[ͱÊrâ FŽã­3¢×¥Ž0-!o¡#Ô8n!Br!uÃÇÀHï#F^‹y^°øèyú12[À(o>£½X0²ZȨÊA·cÑv=. #È6óŸþ£X *HbAp=õUF¹œ›c?âÚ‹¸e1÷ã`ݧծ}ô¼óŸþ:Ç~ óUͪˆ³c˜ñèeʃ-ïÝ÷ßá¿Eyÿþÿ.Sú+„쬧¾,sJóŸã³;<Ïgf$ºÓðÒWQ¬”W¾cXüÏìãŸÇøíùÏèçäÜÊSL›,- 6óyFóÌsâ~b˜Ãº2{B¾Dö/ÊÌǰ¯Ïœç;æ¹óüð•…Œúõ¿ðy>ŸÙ|~þÇæ¾Ç±€íXæ¢l¨’0g«œ\ñ%¡)ËsYÅi-ƒ£­8cZò¿}Êþ¾üMcßü|/ñÐ/Ye3ñª3Y­BpÖŸa|aŽPn<»ØZÜ5d0+#,«êÔ\Šƒ0fY‚/¤Üæ*|]$›ÙÁ¹÷2 û ö#¼Up«Ù]·šÇk¸žQ8¯¶WC¸c2ïæzm\o<#=ÔíPs}3¡Ë­çò‰S9£sFà*¸ËqR»Æ@7‡Ð°Ìm/§´”Uãpý4!aÊ…L£R=s+óÜÛâ:×O2–³™ÿ^ü£mü/–༷õ]IŒŸ%¥W"aÿÐ²Û mÝ t=âªîé&ÁzMú|FÜâ”öˆÔýPr7<‚Wù=p¿\Á³<¡œµâ^Éþ/@沯Æ=ļö=Ì…ßÃ|õ£ÔÖ~QnlyR.­º_2ð4¿†×øõÍOIÖ*Ì x¨gmxœì¸Ç/þ•d¬~HJHäžOiÏìøWäæŒSLì«iáèÂûLrö#|àƒó~Åܦ²Ï]ðÎÄÇQ»T׸ýè‚i{ˆuʦ‘ïõ€Ö6%^NÙ­µNaeÔ̾½¸ãGÑäî<À]$Wœ~;§Ý-;HŧËÓŽ™‡yØ9ÝfÚOY¿Ëà쿌öuŽÑ%Çí˜úI@ûŠÿðNÙN9ÔñÄp's>wJŠJ K¦•((*;ÐÔFá|(’Õ'ÄO¿x”e;– ‡mÄ­°è Û[Pv”Í$Àß„’³ñ —C1‚uŠ£üÄ.Q„TéqP¥i Q‹‰*Zc÷md{“^O)ØÂçÝÊçvÐmÊš²EmóD›°Ì(›ßþ÷ûÃ1lfÿ†×¿+ë^ý6ó‹ß²xõ[Ì'~›Ïò]sÌVÃmœ×a+çŽÂ¾íïÿX>ä¹NÁœ…¸]ïé½ׯ䵑ó¬ç|ë8ïÚW¾)ky­ƒõÊKÊ7 ë^üº¬ÅÔ‡©q fÆÕŒRV1ZYɨE—kØŽÃĸþ…¯ËFŽÛ̼áæ·Æ m™GÜ€Yq#޾kx‘FAP¬¡ÝÀ Ò™oÔåjX¥ÓO/|s,ô]i³‚ãWp¾å¼˜?~î«cXþ ïw[p8±äAÖq† ®îØGƳ/ -è„=ݶ۴]ûå®®ýIl'á”wbÉoLNÝ$³Çq yå$V‚$<ìO0 }L_Ž9²¢Çÿèr³Ð14Ì( YmO´9Wþ œN*‘8!¿dŽ€ µÏ“°Œ!q>×B :>÷—xý5Õ•£|i”#pxÖ½dö94ã›»YÞ-Ñ ö“–pß´»dÊK,{Q\”}3Øo£ÛãûMtœöß?óîQt{çBÁ1Ìøûî’³~.™3Rav2̾=ÚOÑã÷R3~Ï´GÙËúþ?åøŸËœHŽá4rÜ~¶ÇÑÌrìa®yhæÏ¢dýÇìŸþÙËoÏ”;eÎ4;ßÿìÄÄ·ó½ïY¼û]ÙùÎwdçÛ·gÇ[ß–o~[ÞøV”¬+;ßø¶ìzó;²û­ïvÙì|ë;²Ã&¥¯p‡í¬å»l[lã<ÛÞŒá­ïÑþ}‰ç†„whÐum7ýa«ƒžƒ9Èmï~e†þ(›;x†9ömû~”ø÷9ÿ{zÌwP¿mñŽÅ–·1‹¾ÅëÍo6ñÂÞÌþ-ôÝò×dä…knáÚ›¹M<e#Ïh"Ö¿†âô Š JªîÀ1ìË̳~Q>~æoǰâ¹/™}«Q"âP*Ö½‚r…²A:Ý^‡"÷Ò×eõ _•Õ(«.ŸÆJæ|oÇ ö-×ù_”´ñhû ”¶1 8­PeI)UTP¦–Ò¶„~‹Õ4Íy”E¬/Ö6ö)*sÏK8ÇR”‡e/~ƒé §Mû8,e}|¬×U%I•%»(l+PFUI[Î3Yþ"ŠèöJž“bö¡ ®DQÝ€e3ž”žû0+gÖ¼À`ê~,U(É(Á»)k¹A’:e©5gŸ!îoJK:jU“¸ÆewjÈ XºH9:@iÍn·†r7›Xë~b°UH·0ïÜÊȸ‰³ òrLç:â.eyuç›Pœ§yÞØ÷¶´âX6H"ÆZ¢³ sxsÝšÂTMñµ§ç0Ÿ¾PúP ZqrS6u‚ëEaè'†\¯£Jƒ& «=5‹ù𙄃M5á`¹È®,¬Çj&?MÆ4ܧW]p»©¹6Ø|N:r×I%6üÚcS¤ £îèÛR”ð¤”íyd+S¤lÿëó§¥z÷‹RCÒ•úÄw%×SrkçSRyøm²­ñá½'Å;Ÿ—â=/QÓô]©?þ¦”±¿˜~E»^–[{ÞëÛž—¶”™Ò¯á,•Z´˜â#Hùj¯ï|³ùËxê½$ÙÛ^’«Ì_ÃQ.GK¦Å¿.…{Þ¥äÚ‡f™·ãMÃu<à³·âL‡“›†–i-TåòæçÉœóœ\Úü Š€*Oá¶ÿŒ\áúWñ`ÌDÉDIÑíË[ž5ý.o&ms"ÖÑE"K@Ö–ðL$Ž>YêPÇy²™›W®²/‹ëè~%‹ë)Ú×BcÕŸ&ÎÝ:.sÓëŸø$´_Ùˆ—=\ÝôÔ²8ö ¡s£pNÿ®näÞ¸ß+Ü{æxx™QžeÖ?'—áâZžMŸ—ù %Ãæ"m—Øçp1N÷=I_,*|A3×?˽ò™yÞY›x ë™Ää_&åßE4OíÍ3/ñ™å|¡™óQtý,mé«“ó|É38÷EÎiàÞ3ìç~í“£çx8»ú1ä­|ÄTJAKOþøAÊ’¡åj"´d Å}LÓÜ')Êvš”iŠÒ•‚gk áÖµM÷)gè¯8}S™:Ò~ÉøkLŒîÓóèy-tûôG„K.ü¥ ‰<1WýAÈ5ã'rxÚrpÊeÿ‡ß—}|Oö¾ÿÝ(º½öýS~ ¦ÿPâüypÖ `?Ûʾ™?²˜ñ# Ú¦³…ã Œr…è0ÊÑ^º­í™ Ü?õNÙûáư¡¼ a¼“ûÙÉ}ìæ>öLý¡ìÅU¯q`¦Þ×°Ñme¿:¬Ú8}Ðg?ûö™{Ut]ï_Û$žÇíØÇ~‡½<³=|¾Ý(c»T‰@™ŠG±ˆï¬ÿHvbÑÚuËB¸qоËל†”½Qî”=X¼v¿ÿ£1ìze)¼ï ô LlG‘P¶¡LlGSe*Ec ‡C‚*HX…¶¿© ÅU(KQVœXâÙŽWEŠëm‡m|e+ŠÓPG®M±¼Ív ¹· Ü‹²6©òCƒ®£ämBT6¾ù-¡Q6±¾™Ï²•ã¶Ñ7^? ×Ü¥ *JÕ”«xŽSt]ÛtŸÃnîs÷û?”Ýúü>ä¹Ãå/3˜Ap!°ã±`&`ýTqáqj¶_‚’¡‚;yÕ#Rpà]c¶ö‘Á¬Ÿ(š¹¬Ÿ,›Cù›¤“ð­>ÚTpëÈ{ˆ‘°îב¸ _MCª‚ܬsŽÅÌsTÈC® sæTpgªÀE8_Q! ª¨’`”ƒxmÚ'{û ÷š\¥<Û• T{l®s}ó+\G:„øuæM49MÞÖ× ¹”s»NÿœM(¦Ï ¦ŸCÛzŒ’;ŽëºrxÊ5a6øª dôª$@&Ó —ä—ÞÊeöe¢d\Q%åã궸w”Utl²H~se pÍL®q™óE•¼ù7 è ÀÕº¡KÝV.ªÄ繬ÇÛçÈTÁ<ôW%àç0ŠÀ8. œçË{~õSø= ŒIò“¾’/7f$Ýw‘/¹Þ›Þk–*CüßôÞÕô"©• ”å? ‡s(gqIC³MEˆEÛΨҀbàpŽX.°r J†*#QؾH{†î·¹Èµ2TÙM&tåãÊ„ÅâÜœ;á,SKÊ^Hi8qª’‘‚’q{Ôÿ%á”Zwò'òŠ®ŸD‘ÐöSXqNモœÄYôQÇ>ÂzGmŽá?’ˆ‰{M_s'¤,}…§F)K"oJN¡'™–R«M"Vå8£ƒcó8ï|¬ŒZÌùPRNš{ LTï•QÒéåJò (HT>kš~v,kçõÔ¥nk»îO¡ÿ(º­èó±HF±;…õë$–® ‰XÀÆ£íÚç4Ÿ'™Mê þß|¯Îòý:‹e.Kœ¢V¹äeñüæ9c '>z%Ÿ›ã“y¡¦ ø¥ò¿IãÞÎp¡ûRô3ëç×ÿÅ8´]ÿwÎÿOÿ_I<¯<7µ¤9¨EMÛô¹Pæ „9çÿ V¡(ºM Ëjwt>ÿw¢tŽÎýù(óX‡cóQ 5Ö<å8}©¥‰¥X޳'ا$a}RNs½®“ÆwìÌ¢{£¤ñ=KYðsößeHf=eá=’ I„ú#¢è0J•RB¦Ì$¾{{±öYæá_«Nmê9Ê œØ>Â"±e"…ßL –W˜C×ÈfÆèº?g#•À¨‘·QXï"¶ÚK=Œ²§ “ŠÔ[˜ÀH|àºT¡­&óœÊ”k$t9µóÆþ·¹½j_Þ›[M]íNb¶UÐî FÐ-ȽVhfN}€óé<ºæ)ïF1èdäßN]v]Zn°¨à.>ôÅE^EŽ 0ó_MÀ¡›¼åj.?ÏæïÄÔ¸g$™÷aêæ¨·Mœ>ÖÑ¢¶’V?+éÛÞ’T*%ogÄàN?¹’úUâvõÜãuÿøS7±b_ô{[÷xú e¸ö8•½4ÝÛ*ª©¨ÇMþèëd°™/ƒh>íä*¯=õ¡tž™'#<ÈAL ä2oH™NQêvçñÏÎ&r“wœ_@p;nô š'_y B¾ã^zä{-ÚûëóÄW±› ›¥ójœ1eÔPqE=ûJN“²ãjFŸ…9}¦Tãv_E¦š*ª±Tš*¥Ô"¤o"À wšuÏRyp:÷:KÊO7T#E+õRKð:,ã|œ¯3Jç¯:ŽCƒnkû‘©Rrà)Ú÷.5aß—Šƒ3¸Çœ)eû¦“²•k˜!µ‡gIíŽ#}^åþ)0ÝPu€ërLÍ¡™vígõ­9<ƒ}züÔèqû>”Š}H9Ÿ¡lÏ{RJöâX6ø<·P@n&¼.| T‹ãËps7 VŠBÐbóÅ1i»îç zs׫†[($…XH I5E·mtýóQŽòv¼"¹ (&X®}¬ø(×¹u ÌcÎè&¨ü]oqŽ· ù;ß²'U®°˜Å镱ðe¾Ž¢u$ÈF™P®!¬•”!‡k(dÙ( WQ®"еŸî»Nä…®k=Š‹*;XÆ@[6I®a qÐþ±\ãǤÇgs«üвP ²P® Ä•L›+´ei.b½V Ú¦û¬~XC˜â±,"£•Ëä*¸D¢¡‹8rfà;¢\ ZCɈ{áQöãÊú[‰ßÉ(Î"u„¯KmÓ}zŽËš´ˆi¦+š¥ã28ϹUôÃ(Iƒ^‹>ø•\\÷ “žDÁR«Zøœ6º}‰L‡ô=ϵ½æxÎ*j×âÛgéçÎö9®©×º‚eë åU”K]7‰–¢×àxùy,'Ê9^hš¬)•)§d å4SUÝfÊ*å#~ÁžŽÅæJÙ%æ¯hFGýé÷g+ß­-/³²©Š$/Ì (‡éDĤ!<”T¦Î”³Œ~ôÚ—ìgªÏÕ Ûp‰ÿ“&”º°’é½åüP”Îૹ(Rˆ”ÑõôåDÛ°_ûi=N}wÒÉ‘ºø—Üÿ=(E³1 Q¬,اÂ.„q’M¢*MXeŽr«ëɌӹgU&UQ<‹Õ)¥ã$ Úq„÷a˜ y5”£äÒ8ƱÇêÇ' ‘sŸ@9KâÜ'QÎ,KB…í$„üI„þiî3y±:ÿÂD)il;œa=gr~Ù¯äÂ2ž ¾Oi|ö¦•œ¿’V †âšëâТǦOˇº…L]l`å Y9‹}hò‘{)¿(=DÉNJp–ü­ÔÏØ†œX+Òaêf¸ )6R”@žñÍÒ… Ü,‘#=¤)í´݇¼ÊÝûºœÆ!º€‘±žÃEQ,?¾³û ¹Ï»¯m’Út2§1òÖ‘ü0ÊB§¦EUÁÎR³©™Ñ6ƒÖÎË„¡!»:Îá ‡Y½1y¶’94GÞ­E8sç0èTç´K žRQôÓø­Ûò´¤óÎJÛüº_ý‚]Éè{ñƒrâ'g1“_ÚCöº„)˜ÉgSÏý £íÎúûÛÿÌãòÿɧ î@pèO‚¾ö7ÃUþ‘Ƴ2TLe>œŸ:§Ch:-ésø +(ç¹›ê_ñf½ä+¡Vjé>麾AÚÈ&3Èè9P³_ó˜È^É9(åYºGF(42€2ÐÃ1÷(ñI*:FÝ-—‰¿þˆ Uì½d·ŒÜL áû&*±¬•¶LZWôRÔ\óʪ{¾å÷]‚‹üƒ.òÏCÍiÛ~–À|Ðí.²ætg`¹HNÛ,ÍG»É*½¬C‰@{º0JÇ’ØŸW4mÞjÒæQÆí<1„ðw¦“™‡xÀ6²ñ´iÐ>ó!­x¶¦`U`þ¿ S‹&«‰ ôuÐcØ×ãZø§+Í8D4‘©Îb–¡%¥žµx)ÖÄ‚DZ^}ÑÑöuûÄtúO%G¼No|0†zòË7`i2yágp͙ҒlÑÌz#ùvÇ¢mŠž_¯3{ÿ!é@3´b&jOà çŒNèHÏ´‡’ásqŒæ´oÂw¡‘{kä¾0C)ºÝäÀv“¶1GÔÀ´L=¦¬:³:¾üu‡ß“ÚCïJÍÁw¤š<öUûß2èºRcxËP½ÿM©Ú÷†T¡¤Tòã±x-J?Üjö;ÔÐ_©åXÃÁQj¼iúé1•(–P¾÷UCSD•L UâºL ÕyGꔣïFÑ6ÝW}ˆ{æ¼ûß ßÿë(~¯¡X¾*%»_fºˆ4ÁŸBÑÎQD_‡ó Þ«fʪœû«à>­ Pˆõ*ë³ø_?÷zK? 5Џ¥xç+QŠh¿‰åÌy&Ö5?‰ÞOþ¶g¥`ûs†||s>ûolåyPáúæ'ùÜ<‡õ8Ù®}Äp‡\ݾ÷°d®yˆüâ û€‚ü2JY&J–¢N¼J&ý/£t]$òFÉJ–š æsŒôϪç° œÇ" íÊy¬QtÛnŸh¿›Ž¨I²bQ¥Äá<é©c±Úïƒß˜è Ë|¾«|rìÿGÿ×›ú?á{S ÿsýÞl|šï S~(iŠÎUŸZ«© $#ïùc¢è^åê±ÿÑ _&sÛLY¾`<½]üðï§j×q ÕŸ’árÊ‚–ì‘`Íaé£ÂW fìþ<92j¨p‡x1ÃŒÂ{ó¶ œ·Qñë°ô#Kzs7Ê­ÃïJ2ÿâS3e°˜ª`¥ÄskÕÆÅ#kZ21½ST¤?w+Æv¡$P%l§´"+¸öêÈŸ‘ý@Ög–íVÞ}-LíÖÏ]Ì;ä¿›œ²U¦Ý`P¥æò,]):¥È`èüV¦4÷¾-Ù ð.`O&ÑXÖÙ‹Û4íëTIGhg&.'ÿÚÏÁôw฾è¡v=gæœ^ÊÇõ`ÒQEGço,hGÛÔ*5öõa¶é»Â¼O ½˜st§‡2t·óI!Hn^Ð\ÀÝø3ô0ÓÃ|N/&'EkÑ:ôÒ®çëÆÃ³›´=ß }ÐOÛÕâbDSu Y? ú¨õÞËLÏÙT“›']$ò·@QL™mè “_ŠÆíèÄO"Jò é4L·!û»SGéaêGé=3Ë ë]X‹:NO•vŠÞŒò!ëj=²éLžÂ¹GébÈ¡SÏ“2ƒ©¡™ÒÆ=·žFYb:¨ù$óg1h›îÓ>J{Ž==ÝÐÆ´SëI”Û¤)ch=1…©(@)k¡ O3ÊZã‘÷¤þJŠNÝA”ŠÅF•›ºÃïHÓñ8ß4Ϋç¶Ðí¦ÄQœP–޼‹µÅ¥£¥Ã ŠTª’€²PŽBT†òPŠÀ/Qa‰€4 D•ÖKi/gýTREH—»_¡ýe|]^¤ÑsR’@xN«EÛñ{±Ñmm/Ýñì(;Ÿ•²]ÏIùîç¥bÏ (idÀbZ®æÀ+(]$ë°ÑíšýÕû^æš/r͸æó]×¶X*´~8{^îyÊõ¾mʸÿâ”*ôŒ¢Æ€"RÃMößDàßBð+ê°[?–Âíº_Ïñ…6#Q|>¡ô €¨rÄtY,y(YF‰QTÙ¹Žruë…*F9*|oƒîÏå8½Î …õñ ÝâZE­¦õ®J³*Ðå,K°è1úTºÈ1žLr­¦(¶SÚrÏGËé¸jï™ð¹q*܆߄ú©yz8?zÙG$P›,¾†dqÕ%Ê@9¥6kŽQ?ûx©©í¯¦'%<½UGÄ]AyOö»Ùv!ä½U‡¥—RŸÝ×7K!¿ÔuK=¢]â­Kb°y@‚e(ôí¥ŠX ´ûoR×›¶aª„5g!°‹tQ"ËòJ¾'ÛÒIÁJêÓ&*Õ Š øn_Uå½2 k.…´®ïF˜)uE8Þì—y>Y{Þ”ÂÑ®ì}W2±²fî~¡ý¾dc©Í<¼@®§o§ÂÛu ûf|.Á ÿQ`¤ã×þÆ·£P¼ð°«Jü}7e¤9C|½×%ä)£|‘¸:rÄÛ•#¡‘2ê…ÖP“¶T†Ú3)ú]L¶—JŽÉ_wŽ‹$ì&m›«Œóyâjç˜ábê×J_MštT¢ åQP½ˆ¾…T½)¡ž| t剿û†û Äß{C¼Ùø«âo½„"p…B9oðµ¥ÃYÚY¶žOs*K”Ö•@û9ƒŸ6Ýï×6êˆ+þV…–T ´¤Iõ û̲9%âT”`Ói µ$K¨ù4û¨1Þt‚:ã‰VÛxLŸSpÒêë ÇØÇ(âÞ€RSO±÷ñÔÑæPK¾¬Š¯ú(eS)¯_Výòòeõê—¹|¿Á~]–í¦¥Qt›âóúEÁK›§„ú³ÖK÷S“mTáËkà:¾ÊCÔO?$ÃA´ác®çE)óðÃ0 üxЄ½6>–>%/Ê‘eÉ‹¢äàAqòòáA«õ 9xQ†|8¦Äâ½A埼 âÉ]ëFAññÄÂ>oÞzñ¡xZèöZƒ¥Qq__«£xr×ÐÎ<S>Š/oµørWÁJñ^_!žœqPOÞcãÂÊ4ru• ã³1Œ#ÌDŒ lY¬6¸Àmãá8o¶SUW9·–ÏÁõ±¸¯@æ²±\!E"û0tœž˜Îd¦íR¦p߀B¥tª’Å1mIïÚORqŠí.úŽá4ýQغôó™Ï<ÕÐqrŠt 0µŸøßî9‘{;ν{WšŽ¾ƒ¢„¿–¥ +‹ƒn7`‘‰¥ñЛÒxð i<ðÚ'hØÿªÔï{Eêö¾,µ(Õ»_’*¬1U(U( UXi*Ui R§…¡$þyFvl«"äP©ýbPIûiÿb”Œ"UP"]×6ÝK1¸ˆ‘³¢ëz½2çZ(c•P…U£†i¶Zsº3±R5Ì¡¥±²†¹ß,hÊ¿ñ‹«qÏÊÁµäóŽ 6EÒ·¼I" ²µ½ú 2Ý…cÚÛÒ†cØ@þÞ§x·ò~oáÝÞ~Vx†:õŸ"Þ;ÆÓ¼;O\UÇh‘`KŠøØ(EøR_ÛÅ{íŽÖ‰+--LýŽÔ•ÁjÞÌ⃜σðªbÐY|@z ÞXênÚ]•G°&ðÎåFà#7·3MÌÀ…00-6R…å°*i*¦ý·-ÿ#™2é „w-ÖÕ|Bà ˜¢½ÅTìUBÄ2yvYû^¦‰1¿qtŽäŃüðL¹vj9Õ玊×Ý: ýús n¿ÏõϾÁ¿ úŽE|ÝžˆÞa ž‡z$àm‘P°U"‘N ‡º)†Þ ²ìe½Ÿ>m¬÷HˆýA›„دÛáPô’s•vŽ“ˆž³[<•2Ò•/!_-ÛÍœ£Õ¤x‹p†àˆÂë-ö7IØÛ@áõzƒ (DÜ•(6•lW±¯%¡Ú( º?Â~ƒ®{èë.q•Kd¤TÂá¡ ¡dX܈|‡< Ñ;„nHx%c€õ¾ ÷¢„ÐeuÐ>¦¯ÒÒÃ1=®çšM¶»¯Úd±¼(@]—%ØiÓqI‚í((,X(‚¬ZÓ%A¾èA¬QZ´M÷CíýGÑsê¹cAAjÓkŽ‹Áޏë~´´r\ çPˆN4sn…k+Ö}¡5¡5¥F 5¦ˆÍ:XÒSµü°béx (EhÎJ åÏ¡6-¥…Ç!Èz,š£¨FcGñW¢€Ä  ‰ƒ¿ŤRÍu(>å{Å[¶ååKâµñ•íB!ÚmÐ/X¾öKÅÆA·­ö½à<,CþbŽC¡Ñù6?‹íˆ²UüL=ùó7ŽcJ µ„U ‰Á‡2âcŠÊ‡2âSÅeÄ›³ VÚŠ‚üdÅ‹Òᣢëºßmãôõf£8¨a””‚(¶2BáB¡m#´0•fÀ3ÊRÖ-†/.‘¡ŒÅ(8Á á3Œuf Œœœc\œÏMøjݾŒ¢rIÑãržf9œñ‘Œ€.‡Ù6ûb _?޵J_ú\¬1sD•KY—.,!е¢ƒ©§NËItÓÞðïÆJ¢ô²Þ‡BЇҢô&Ó†¢`@‰é9ù¡t£ÄtáÈÛyü]é8öŽ´¡´âôÛŠІBÐÆ4L,íDët¢Ht£Tôp.–(íÇQp Í¦•~­(J ŠF3çuha]iåü±´pÍf”Žñ´`…iÅÓvô=î‘{ÅBÓŧ¡ÕËsècÊ­«ØV²Á´¹2HÙË>è†N›”õ+§R*ùu9¯O6þ?W©Öu†2ÎK5×^ðêlWBÔR§:!xM¼/:yveŠ¯ë¢Œè`¬“Xï™Îó˜H—!Þ# §ÅÝÄȼ‘wHïB©ÁjÌy9Ó“(*mX@Uè»xyšÎ0’?-Þ,º¼w†y/ ð‡jŒ¡$èàÎGÛpU¢ò#*Ð 0§3×ݵ³ Ëcæò*ã Fu0”™|¦ßòuJ“|)áÍÙÄuß ‡y>S9Œ²uÔ¹ó9¶ñ:ð¾%-'ÈÕÄùr%ëm_õ°Ï?ü¸ÇçþtOòX©îö¸¾ ¿„‡ÿe0<ø£Px @ІÂÃ{„Ѹ‚J0ì2mº/ì¿¡îŸH‚!· ÎzÐem‡½é1úÒTà#ÜÃ(‘-_Ãñý0~@Û!dÃ5‘¤( º=À’>ÁA„<ë Û‘`Ÿéc¡ë Ç³Œ¸–åÃgálS°\‹–³ûñµ£@´ø¢H4#ü›B•à¿nÚG÷5²_Q%ÃÆîë3z, ˆwb"^/ʉW• ¬¥X(аLܲ@©P‚C(Ã(#0ÌúP l Æ‚ÕeÂA÷‡‡ $BÿQn±n3RˆB£Y`A‰¸JÇ]&!”Ÿ÷òTÆPź JRe)8R.A"‹C¥È?P(þþ[†ÀÀ- ŽÏe[X|=–ÅÆâÖ‹€MÅ'ˆì»U¬T¹ `éñÓîëΆ«£teñã¿™üø/ÁEñtdˆË¥Ç…£?hÅmãiÁz^ða‘14+Xt h÷ / ÕþõE ø!ÁXØ„–›0‹SfÚ(Üx‚y<æòêQ@Ø&T{X‚Õª< 8TìAy`š å@?ëAœ;ÃU{%ÂTT¤z¯YW1XŽNévñm1ø‹c(ÜLB‰cðßÚ„±IEø©[ø‹¬6ÿ-”‡›&`£nn’à­-†ÀMŽ)Pë燫I@Á’âDz⧖²O=†s6ÄÀqÚE·Çâ'Èo£ûõœ‘ܼTÝX:ÜX.ÔŠácêÉÏ‹;€…ÆEÆË¶ˆáZ,X9®­â<øç`µp©"bc,àBYÉDɸŒbp ‹EÆwa†a$ƒyÚ‹ Äí1OÜæˆëülIŸ)Ã8ï™e˜ueäÌŒ(Ãi3DJ&ý~¬Ê‚íX´­Ÿ)>,½X)Æ€µ¡WÛ¡«Ã„è>›^Ó×˵û˜ÐûJçsœ›+.>«ùœ<7–7ŽËjÝFQ¼²œšÖ+ n”ã«8êžbN÷ܾir6Ṅ™ø<Õ+I,´cêÏL ëÁ9,Ø”‚ð¼` œô7>p÷Å5ñö]w¿á^¸ú®ŠÁîep£¿c/ƒ ¯þ¦@èÀÇß~™Ñx²”Û}‰Ðâýÿ3b÷òöda¡¥_Çñ黀S?ƒ‡`JBgï‡ ŽgÉh¨ò¸x«y}¸ ÿ+âºùž•!œkðWªÔ0¦­ªP`êqØ®GY)aßMœ©sÐY„,çìÃ$®u»u´"”=ÇéÜ‹$óÄri*G õy½C?Bpÿ“Ï7â–‘?óËðƒÈà¸O¼Á 2ÂÉ€„#ƒÞ.ñ‡:Yö!ÀûÅǺ?¢}‘#ÐýŒª½Œ°u=FX÷Cô ç‚ØC›ŸÑv ÔŽ@o·QüÉB5A!ˆø!ˆ0HDüàY†ÙV"ìGbCXHæ:ìúØD$ÀýøÆŽp>” çRÜà2DÂC\ÅAt‰RF9ŠšÛV, |Vm*ja0Ö¬aÓ†Ò ð,"á®q <¨2cˆ–ˆZ;P$Â(”‰0Ê‚ R€ûA¡°‰„°RLÃ[…cƒœÃ&Ìr,\#ÄuÃz},*ÜOˆÿyHô{aŽ!Â>÷·–C+ÿc¬66aµ°ØX×Öû@Ù r¿:”*¥« Ö5Ö…©—qXíÚg”é_)Ao…P:n 7 „›©zbp3Uã*4„Üa÷-  è`2–UPúQbõg£Úô^Å’’E•\å ëa^4cÉdû2û.Ñº.`<XEx¹(áγpÒ$Ô‘ÊH€i|LB­X”åä(LÉ„šu:†QBÓq, ês‚óg°%À&ÄR 7‘ˆƒn׺-¡ºƒX-˜Tp>u±®„!Rs J˜õp5ûªö‚•XP>å€Å!ˆ3‘RÊöJkDÕXBj½0}wB(-J˜s„Ù§D°†8„+hÓýXBBø¾„ŠÕ)>Æ‚kͰÖu°8óÓ·4e…þøêøQB|(!”S-k=1Ó+f:‹†ÂÞÆËÒ§0âÍfÚâêR„=Âì £üËõKó¢ ]dtš1ëÂl87 ËÀl„>Â=c®¸ÁCŽá7Û\Ó«?( ž‹*œ¹w¹¼Ð|oö2>ç ”§U(b«QȰð€›u× –(zž’m(œ(™à¯?!©Ë%qÓ›r#»§Éùí„ý˜E*kb½Iz‘ÈÂc„ãHìg´‹ã™>õ3X1¿MÓ·üÝC7Ä­VP,/>µ„2ÑéYwW6ÖÎ0¿ å¿é¼Tãh|‘p­~¾nFè6~ö‡Äø{˜~íå÷Ìy½(üÊ`–àLjÅ¢ˆàÅjèª9I˜ô>FÞû¤“8ò&œíZñ+êÁ)»ŸŸFuXÃïç™F5¬¬ˆh¥üÃS%‡\#¹‡™ó>4M²÷ 7ŽÍÀInfò)Rp…âè©.Àúèéð3˜ý¿>—Àv:‡dè‡A¬ H¹ !¬/[Ì×¢#ÕPH_Ƽ¨1uÁÏËÙæe¬æóHŸ-¤1•#¸Ã==.VÁÄÈý~FÏ>E´ A.w½x]˜Õ{9ŽQzáóòW‚Fx3üe]zÂk¿%´#h öŽÀW¡ï€ÐFðâán Qea„s0Ú7 ¬aÂFéÜ»¢&~AI>ƒ…¶;ût¿ÝGû9}µÿ˜cœcí¥0} Ï^&¤“v‡ÖÚYÿ @åèq±çн ÷…{ŒBYeB N±þ— R$ŠNŒ!Fɰ ›¨¡=Jë6olß1Ô°~;ª;8_¦H|L•x™ñ”Æ€Á3/Û@À(S1ÅBŒ‰77EøQG†o`Ò)\`š$†Èà5q3*!Ü•‰„¼%ð™éÃg!ïWÍÁî×B7#„n|8l,ƒ*Ø!Ü{q”>ÖL{ ø{t¤Á‹ ïïHcÄ€ï†M¥ƒ*aˆ8°/‚i8”„p ª(¨ÏFë@ éˆ(ê«’¡F%+ ‰ D8Ê…°í¡OÿŽHÝ cUU£T¡Œå JAè#„e!J ßVÌÒXð•X"Ê™†(c«C,²x¦0vH¸r' ä±X¨Õ"ˆbábÝÁÚ8ÛpÐ ä%ŽG@Ð ·¢l•ëáb(b«F«F°`½ Öa‰`Ô_°&Š »(*üü) ×WJ€éŠ@Ö2ñ_Y*>Fø±h›î‹Â¶_û0âõ^\$¦ÜL¸1ÿÇâ¡M÷´Ÿ£exñ0×ÑóéµÁwD§N˜Fq«‚‚`öÞÄì\ˆ€-Á2SÎç¯Ào¥|;e8(—mO»îæn¦¥À‡ÏP~ê*Iß=SÎïš)vb^>4ç­É÷OÒ2?^ʼn­ç¯Î<>£aµ™Ò `áó¡t{FTÑÆâ§Š8@µþu Blëï?K}¦£pOìpŒÏÖ&ddkâò£$EÑm/| BõOÐsòÿCÌuÔÇ!€0ö32ö12ö16`Ú7`ÞC5Û10ŽL£çˆ×òCˆðc4¨B ¸Šò¯%‚³£CX}Fø!‚:>†pž4~ ê§ÀôA?S}hï5½¡­AS;þŒ¦ÃQºJ¸ï ­<Œ€uˆ hc cºs²PALÇMŒîsà¥@Ç™ûû²xaà÷0À(¥ m!öéc®£ž¹?5ñ3æ?Ž›^œ2½D†¨ó¦ƒß8rÁ¡0oèש|¼˜ s~Ìü§à$Ñ!I£àOàÁ—ÀPsÜGuöqÀÏÀ3öák xkŽ`zÄÁíÁ‡ÀÅz˜QòpéÎ(#HÅ…¹ß͈Í^ÂÖ_‰?™™©@h‡ª¾ÕìCpøÎÞr"ܱøÜ~ÚìT ˆU 3 a¬6~Fó>Fý^õm DÖÃ(ÞÁ˺â³ñ3r÷3bW|{ §K|ÜL¸¹»nl£wN“.Lö®ëq°F\9ŒTÁm£æz/æzßՕ⇎UJ³¾ÊY3mÓý>ÌÒ^Bu=™ø!0÷ÿIÔaÑ›¥ŒÀù}z,øõ:z.…k(~îQý&¼øOxQB|:u´‹*@”ŸDþZœ[yöÃüOQrFP¢ÜXz|X€rº•¾ 3ùBÉ;½Nª/ó,kR¤áòFÙ;—$CÄïg“2´á×WÌÿEÏ2Ëo)Јl©¿NÇñ¾ ³d:1dÀª*Ü#~,sôÑ)F/>KÁ¡2FÝ×¥5w‡œ#„ˆ…†®‹wG>A¯CpuŒö3%B)ªeN§Qú}C…üs1­g‰ó»»>Ç5¼Õ wK¡b=ùñRIøq£ïNBÅ3>6ŠGiQÈÞÖ@\xÉÉùR7)dR”¼LÊR×HÙ™õRraƒ”]'Bª¯¼?è¼›Ô¦ÿÚëuÿ;¯wä¯>·ðf¤ùo‘ÇÑþP˜Ñb0؆vR.¾á^ðoæƒC#¼ì‡y@Ìûª³ZÐÐƳ\Åüíð´ !Ä}b~Á ŽeAÍJc>W!áåÀ?!ˆq”óOAÀ`ž6&ípAÏ2„Ù:„¹:¤¦jÄ:RV­K) g5k“¶1ëc!P+A„3óéQ|ã\§ŽuŠ:Ñ¡\ (êXô7âkâÝú9êyuÆ¡N¿8¦m<ôÑ~ѾÚß9Æ>ÎúÒMLÐSËsŒ¥ŽmÀ:átq~›‹ó8Ÿ[áÿ5A_ð‘ªt»¸õ\c,ÚfãâGÄñ‘a~$CV~h»„çïx¦aÅW²6ªõê‡þŠjÉæÀ-¬9p5™'¤QFH;DÖæÊÃ}Ìç#¬ÃD3„ùZ  ™%Â|y¤'Û‚yñfn%Œ¦f-ÜqõFa”Ë|šCˆy5‡ ëðƒï· óq^G‹Á‡`,^œ=àf¾Ï‡­Ç¿X<´9xy¹Ä¢£–(8"ú˜Çwð«s#‚]çøB¬;„™3´`îMû©g¯ wbñá¸ã‡€z鯍ctƒ'ÂQµc: 4X£w?sú>¼}½„Z Ôk‰š°ñÕd?`~ÿ˜ã}ŒÈ}øx1Ç{*q DxŒÁüUÊv0W©a£€€u!tÝŒÄ<Ä{5ƒˆ +òßÑ^º@ ¸ 2 LU#)^’ƒ˜{uð‘„Dñ#T&—Ãþ ß×Çñ^îÁ˵¼„ÓúÉÉ(aje'Ès7~5ÅÅdˆ©‹¦=˜Ó$ fˆ0.ÅËo§êú!É?Ÿ µyÇ¥½ ¿ÅÞâ#²BQZ=ëqÎM„£5Æ"ÅÐÖjj bÙ …˜ÆÃQ9äÓwn=ë u¿¾+Õ)YßÇê¸Ì»×7Ì‚9ŒoPsÁn)¤Ò— ¥1ÞYA¢ŸÔÇ&ÈûÀ‡É܇ã¬zžó}¨8,Ý7¶KóÞ-$si Û[9FºÉ7Ò@ñªTÌàg—KyÚÇRš¾BÊÉRš¾^JÏo•Æ›I˜ãKû"þž»Ü®á{=®?÷y]ÿ ¾þ¹·€ðü3F£¯0§{#ì «CÑ0áYÞ!Ä‚£ÔÍ6/Ì€—ŠVäçÅçÇHƒâê/×€Î+ò0Ey^ðn·ŒØžjÌjšàeÌ9<\Ã5XÁ\7s§F ÛÂY白c´æ–£œšÓÕñ‘3B=„PÁ®óÏê¬fy°«u Îha?çPg4/Û‹BáU4LÄꄆ÷|Ø`yµ‡ÔƒG´€«Á†Ï=‚àDaQ²° HYvÅÙç,ƒB¤V_Ý¶Ž‹EÛb±Î˳FÈòÜýúlxž±ôùÚûÑ 80#}Vè?Èä³ }õ¼} ·;ž~F»6A–ü hÁ6¡Žw0mÌ;Ó×Oæ¾ÏB€~>›^|Žmìf~ËÀ(»‹6a…:×¥š÷í±/léfÄí@¸cÄÀÈ›pÄp'#ïæ®;˜·6d‚8ÁÛÕãß"ÄKÊ‚¹kñmiÃC_çÑ”–Oh¾€°<E·Ú¯•snC¡°Ñíñ„Û´Eˆc¬k ¨9Ï(åf›&¼q‰Pðì¥ð² 72ïîÐÆ\yšDê1·×¥D «„ðì¢ÛÚ®}9W¤)Ýæ,K='çR0å7ðb7pŒ:ù©r Š@-Qt¨aN? sûx[`¾· Uc· 6$Ì'@èP€äRºŽ@ùQÌã8’ô#\‰Éb%CG% „çç ó27í+L8å(lÓRØÄä@! Š±¢ øQ|„ùn2Š' ˆƒŸu‡IEbñ#4|$ ñ’”d<Ún¡},¼/‰K¼Ä${p‹B¼³çvän1Çø¸†Ÿ{ ¢À„PjB„Š O :åçYûÔú‚ÆEf„ÿÕ 9F©JgÚî†,éiÈ–¾fvsŽq´­ººCvS|äÒv2>’Ѳž¤.þ·LØc#H4a] Øþ2?ƒEÞÍAd‰}ùgíâ¥5ÀËÊÍ\@Ç›jL•ÁL}ýìï¹!ý×ÅÕÇ ”޳ä@;ÿ°Nœ ‹Ðwó2jG‹Á‘À‡V3ÈË×¥!B`S¶šÜÕ<Ïh?¬¦ôB™°³ éÁ Îof©ꌲupãY9O„vØ‹àö ¸ÝœÇàv#¸Ý˜žÝhrn¾ . ¬ cÆqn¸ÁBxrƒÞ!´¼!–¬µ}tŸîW‚6V_=Ö:îÓ ´ûqÀêGYêCñéC(£0˜QzY· õ"céA þ½Œ€mB,• ÂqŸF »˜ÿ÷88Æoà~Ö_”Ö-üŸ ëxsŽæ«nƒ¿aß§ºÒ…“ŠøB '„ï¡ß@®¥OôöëcðÝ‹¶£œ‚¯ oõVýÇÂ÷¸s·Âœš¿™MÊÆÏ@ÂÔ€0G¨‘‘üØœ3Ü„06àÜÁF®×pñÓ©ÇP‡à!Èzˆv‡0ë‘ú c…¹F¥î\ Ü:‹p-t6¡„;«GÑíÑ>ÚŸóÔr>›p ÊBóò•äU¨L¥ŠupÎ¥ëþJ<+“Ç -hªÂ/ á…U@ÿPÊÃm“h*J^þ•xûÛ„X:+PHL¬@9‡ |‡`9 ÂH*å'™”¯32fW ¡‰J1á‰EX ± ‚Hl±ç#|’‰8è¶¶ûа8Ð×g ¼Úg"nÒι,H¨5IHü˜‚•ç±À±Ð­`ÉA”#|VQb(9*U¨ˆ¦@) yÓÛˆe¨ñ‚Œð;PŒÉÛ§¡½ ”˜*E 6VÈÆ¢D9¼”BDÔ{(ÜܞÍÔ‚^¦Õ¨#¦-ƒXDÃX€ñÚÄïEÃpaÁ "ÄÃX€FpsFÜÌ;…¡ð&øåLñ}r¡ä3µP‹*‘?a¢wB(A,¡A½3 ¨“l##o¬È˜Þ=È3?Ê|H#SP8PðQ\ɃÑCˆXWÞvêH“zólJ²lËÚÊü÷f©¿´Yš¯íêìx©!;[K9ÉÇ…}½YžÁ{bås9nøW¿‹ÌŽÃPêŸÂÿò¶¶{ú«e¸³XF:ñêë-Äuþ‰T² §a”ù1ØsS^¦!Ыžz7e¸õºŒ´ñD ¹;òL›§]?íÚæëãA2"î-AÖ=:ïÀC «¹š‘oÀÝŒ¹ a#b(*l=m,eЋ‰[ñŒ¢íú†]ü#G8¯1ùø#°‡lጀö# ƒWøçò BÓ1 `?Ñ6¾Ïm·õØÛ¡×Á„awÁmèånêA´ v#Ä Œ\!Ð…ðìÂìl`Tꀀ ØY~^ÆßF°‡ÿ·Í§õ r®M¸ S·ÒI[,lÛÛMC ½À‡’vKÀÏ÷È_¥ó6 Œý Ú׆ç(x[ /øÙ°/¨°®h´°¯aÝ„ nÌ"ðÊ'Ñv?û ŒÂ!C°k¨CÐÚøk™§Vj>I –cê8„êáGÑm‹`ö¹Ìù€ó|m=9×mj?ç©AˆÁ_鿊Й*F·Á[É|ú8ýýÕX P™^…uæw¶¯‚»qh»ƒ³Ï[Ž©ɘÅO}Os÷à-'Œ¯e J^ñ•“àÃÆ[v’~ O)sý%P̼íxJN`ROúṮ6nŽ7 ¼\EÇmHüQD–È"’‡12+"#X,Å´Ù¸Y:xJs^¦P]×}Ÿ8žsi»Óßé3Bûíp¡L¸ÖŠ›¸h/‰Mü¯ŠJZF©©B9Bi V©Ò†2Æ÷;؈ŸF ò˜ï1Ó øpöã©äfZ³WºùM_Mí­÷pz‘¹ßpË%3§Ó–~¬­!ão¤><껣+}xb¿úP4g‚=hü¬4Â?„¯‘µúÉôC˜âÏ€¿G0ÀšèŸÊ¦?5y˜Îwû˜Á@™h8.DZ Gê: 2ÝÀºæÅÒäÆÃ|€)•¢ \äjèÃJÒ~¢%7ö‘‰óôäí–ÆÌmÒ˜½CêTh_ß)•7÷JÖ,Ÿ*0áž,ÆÿñH@ßî`†Œÿþ8è»pwx\õ˜Ì+ÅÝËœ7ƒ§óv7®øà0£è& /¦I]1íz º™ù)†ù𜲱yztN ‡ä+.‹›‘ZÀÌ£—ÉPO¡ P\C˜Ò½˜:Ü 2<ˆÒÐÇu`®íáurãÝ4¨‰²°Œ‘ž;€‡¼dP~¦|5IxÑóì™GWŧçÆéÌÝ-mù»¥ ÞRxXÚ*ø.j–QU*B½»±ÿ?/B;ö¤‘àð?c¢û—_ï ¿§ûfÀÛu-äïÊ£ I{@Ð@“•„ÉtPs3#àˆ¿«9ìël xÚ½AoG3·‹ªnßH s :—ÌÃfžÙÏÈÙ;Òâ¢ÏyηÃ=Ü|}d°¹Ý3ÒÖé÷tÖ»GZjû{jºÛʤ»µnIô5cŠoÊ#`>®K_ã5È–^4¼^Gá¥KwÝU骽¹Qºëò¤'–úã/½ôíã‡Þæeªs }5y´ß¾:úÙm·[š¾zŒÇêñJ}Áúêio /ô6rŸ|NCÓ(½¬’ǺÇ4ÒC{ô8žUÏD4Ñ~[rØgÑÛ˜Ã=åÈ€R ² ý<_‡¾Ú«|Ɖ鯣¿Í Ë¡ñÔÒÆ±1XsUb¨É""S6Jí(ý5™òÛ¹LŸªYw¨º$ý•”¸ˆ ÚÇÁ¶M厷©f9}Œ:-ÎI/#Ï^„b/#•O£áÛ‡Uú¼ýÛš Qt[Ïg¨²°ÎmÃq½6Îyúj8¦k ·é<Šnß½¾Þs£ânö禜ãÊ,ºJ;%i2ç¸î²³œÇBÛL¿b(š˜ŽÂTK ÛÉpš¹ÐSp’¤ç!‡6ÖZož¥eœ}Ÿ¶l)H‡æüãåÆ1iVòlr’0Ä¢Ua{ º/çH”æk‡1ÉŽÒD˜Ñm¹Æ>hΡ¿žK¯Ó–{LÚÕyL¹á@{(ïí¤3…¢¥åd ˆ:(JC8› §H_Q ùÀS ½J—ž² ¾#™†KCGIÆENŽ€ñc1¡³µE§%…|å·ˆi®N'ã:jr"þ)Aæ™Ãµâ 0uĉajEÈhÄ ŽÄÃè[s]h~ˆ€š¸#oòV¨“™ iuZÄ<=\‚ÿyü„ŒyÉ…Ð|ëˆz£«S×ø™–$|TC΂šÚ›)Ü &¸ÒŒ˜„yjG¸ pÚÔ)¬¿/S1¾ÊSX#(Æ…ÒÑÃÔEJMSþ~iºu\º±Zùð;Š:|÷#Oÿ÷¿w¡{@pä_Á?·ØþsÐ_‚§ »î …îž…à0öUÐåÏá;8}“åOÙwÂö=h†@`àÛðGz^¯gøÏ|žáÿðýÒ®þ¥ßÛû ×pÇ/G†Úg¸GÚÜ#­'ÉåÚãn)ñ 6e{¯ú•¦2osƒ§¯ñŠ»¯A¹êék*ñö·„|xºÆÂÜÇ Ùèú@³pŽ(îÞ¬ õ£ô°Þ]• oÀ:bïj nm·÷é~§ösújÿØcœco·쬑Žjüª¤¯½RzÛ+¤ç3ÒÛQ!±èqÝmåP&]m¥ŸŸÖé²én)–žæ"K ©0JO# •Òp ÓsJ6]($c¨cÛ¡6%Ê¢³æF””œ ©¦} ¹l[tV_ÒQ•#£\cÝ¢½*ÛÐf¸ú™h¯¼*ÑV‘…˶-Z+.Ã%¸(­}‡¶šË3Ä¡©ì‚J-?çégÑPrÎP_œ.uEg?A=m ÐX”¥©øœ84—œ‡Öšéói4q>¥Ñœû“4ž‘1Üb{ Ü×-%}BêÙå&럺‚3%?Mê& öFšÔÞH•š<%…õŽIåü)¬k[,Él[ÔæŸf®ò´Tçú57N™}Jm¾öµpŽuŽ×sXçÑþ6y'¹ŸQjs“¤öú C] õ´;ĶÚzmN¢Ä2¾om×±©»®ç?9î!÷8£ý˜4 Ô›òQ4’¤µeç&JϬ Zy–­7ÏHGq†tñÑñiškLå:ê&ú‡cæ«»˜ó=GB–ÙRšJ“Ë„Åá™êÂYуS©Ë)‚Û«É•Èá ŽÌ&Z‡ð0+$WsG¨ðVs¦<Ü!ÌÝ:2×dMA/QÂ:uNz§ÀYלӥB">Íï€3­íÃzÉuÔI-h²Br-²>4r…(ö1b÷¶0…ÁF|YÔ²†ï…¯«Qy¢tí—nüºQ:±: ·¨ÐoÆ×ºg=ƒßoý^ö?¦‹ñŸbz`øKð]øø§ÿ˜>ÿoû¬Øþ9ükøSøOðWð ø~”ðÐ÷‰»ßdßðø/ð§`”ÁÏûGF»ÿþ?ø:ÙOà=T왡ýv„8Æ&Ô?ç‚ `h`6Ìažg¬ìzx¼ôG‰zÓH¡ Ý·ÇϾ(]¬ÇàëL#í®è‡(!OGšCÐÝžfÑ–pÝž «-+8Ò^õ¡‘Žvè¡àp{wp¨­:0Ør#0Ð|.@º¿¿)Mñõ7¦yû”³p.xzëÏ»{êι»kÓݘ°PVËPnÛ ÕÕ]W2ÒU“9ÒY}ÎÃE¶¯ŽtÕ–Žt×µ¸zêGXzÀ!ÖðPWMd¨«§×ÛÐYê¬ Cp°³Ê^pÃÈ`g%ËJ¶+ÁþöŠ0D@~}(±±ô¶–JOK `ÑC!íB!íjºu[:QPG¹Éú( ÒŽµ¬ +ÚíhÅâæÐR›'Qjr¥%”Ñ”ЖÊkãÈfû·Óвêp»þÍW%J9ë6MeY(“£4—_AéÌD Í”¶XTYeß'¹Š²¬Î|”ù"ƒ¨ãf @vMÍ iòg¨ Á]2wi‰K“깞æ ïÊ`$LkˆÑ³Pذ†€QƒB=¿uNÚh!Äu c¢1s“œÅOv$,š”EëBôáèÂŽºÎär‡h¦#IÖaT6™†ÔpZÂcM½ Lìa´aÊÕƒÍsöÅÉ(,Ǥ[§biP$Ë…c¦«Ž(øUtáOÑ‹l¸•0Ô‘¦óÉ É‡¿ôºÿóç}¿MöŸ|“O`ò üNO âù—ðÀ¿‡??‚ßI™å¸?†?‡ÿ ÿ%&ÿaöeÞ(rò%Úÿþøð'`,s8ÑüK@±üðÿÂ_Àqž¹ãs¼åëDýw|í¢¿ƒ¥„Ž»o¢uŽŸð¼£×º“: ÷Á³(ˆSa%&n€85›`?®ÜDþܧé ßo¥÷JçNØk` 솣a2 …ݵ–òÙ¨»"Zd å3n`TÙõPñöäAìx{×*aÿÀ·‰Úâ'ò' Ù3 Ùò¹¹ä‚dZ.5¶P‰k¡dD|%‚05™u¤M8­æ¬"«ÉQLÖ4öé¨ZSk ä€QMvRaLˆ05†²®j<Õ5‡¹&"Äo‡5÷™ÕÂDI…4Ç&sÚ"š[‚NAÒÂDEÀ‡¤ gÆfüj®íÃâpHZK¥ g¼n¦zê/ÊS‰#øX 5·ó¹ ¼¼ãwúÑMôûy¤jûß&™|Ÿ÷;ðûùvN^eò üþŸEª¾ÂÔèWÈ 2f>—ÌœÈÇÄL4uz¥¾0]²,Uç7IgÁqiÌ7™…°­¦nÜþ1g«w7â­æk­æˆ9[CµtÔ$*¬‡i³ªBÚ•™ßö‘w¡£¯rLÙ^"¡¼êЌ㴉ŽB(»Ú¯SMŽs"a¼D68F 8yTàÞÙ[wž)¡D©É=(U9û™Ú`þºøü*š®ÉÓøvùBžî¦ »ÿenî÷;‡ýûÿ÷þ¯qÅÏûžì?)äõ;0ù7ùþ1=`hø¿“jº"¨É±LÙgâ±I3]]pZ²/•*â»ËHàB†@¯–òÔâäøð#DýŒˆdj jÖGLØþ!Ìጾý˜µý$Yñ W“૊v„ø Þàš˜Šˆ+ÌîHSÉqœ—‰¤ “¢‡hOw%Ëbü—òñ=B@áâ"ÚÈÛW…_R© ¶áÐÖ’/ƒÍœÏhZ ™«o¼Á>¢©úù{ZªÉ1rœ„_ß&eä¿ ¸‡þÜëòÝÿéúýY'ñ¤ þ]¾Ð_úÉ›Ÿ|Ÿó 0¿]¤™-µ&B„Ú¤„(‰\†'û´8iÄ »«:Uú›©ÊEÂ/7™‡2Bì."LŠÐ!,XÃa¨¿F†É³1<€#0y8<$Êò’ ËKÍ=2ÒE¿ŽR­V4É_âÃ÷‡†ºï »ÚÇÓ{(¢ ^|™aO÷tLÜKƒ®ö-ŽÄ.‡†Û—†Ý=—#îÞ>Ìý'"þ¾c_ )+ߢ¤ôŸ|ÎG0ÙýÒø]^Ú“ÇL ûHßáÉ{™|ŸO†ê÷ô¸È~„¹›4£ZˆŒeòf”ki1™KOÆÃ1Ú!-dé%‹JùoqüŒôo§ZãÌHd`1۔ȡ.ÅqêSÌþ7¬ßcÖ#Cw±D&˜UÖ®H`o¶Á.jpœð…‡Þñ\º|Þ¨†?0ü§DJý‡¿ÏÏ>yî O`RO áßå;ðôëÏñ¨7ý^wC µN$‚é©"Þ¾Ôļ·ðw·aâžÏh{2IÉçx®¦ëäKû÷÷Òž|Ö“Ïúóþ>'ûO>?ä'àv÷­éj¯¹gx°uŠ×Õ±Ñ3ÒáêoÞîn›JðôWBþß)?Àò3ù;¹÷Ia2)L&¿¿¿ïÀßÉvò$“Oàä øü®«·ê ºÌÜp 0üŸ=Þá?ÿúòþaÞæäKû÷÷Òž|Ö“Ïúæ[`ò®&ŸÀßïp{<ß÷ø¼&žË3ô¯áÏÿ~¯ø¿øÙ'…ɤ0™üüþ¾ÿ‹¿N&?Þä˜ð x¼Þßÿìv{'=¶?ã÷çÿÄ*M…¾ÄIEND®B`‚plumbum-1.7.2/docs/_static/github-logo.png0000644000232200023220000000540414161152302021052 0ustar debalancedebalance‰PNG  IHDRd-XµÎðsRGB®ÎégAMA± üa pHYsttÞfxtEXtSoftwarePaint.NET v3.5.87;€] tIDATx^í› °UUÇAîE‚„|0>p uT¨é5“šâ«|t)²4GQ EÓLÒy1”¾0‹H m”ñM‰ 2™ÊG™š"™Oýÿ¶ßÚ¬³îZûìsîáÞã̹3ÿ¹{ïõúÖ÷_ëû¾õ8}ú4ø¯[[»0Uø£ð p¦Ð¿ͨžw#èlDÝÝ­£ie“`s"ÂÍén‡)ß´nVÙ¤°q ¥1²Çu—”!5jP ›V@Èô«ë’½EH”Â., 䢫kÒ…}©€ñ ¨¿åÔkQ¢ÈØ@¸;BÊ=úÖ¯–ºby[&« Jiƒ…«„ç a³HPõaÊ›·r}ýú‹Ê¼V ÛëJ;® )Rì†ÂÂ*D‘DZoRD¦Kƒ´Š‘¯÷ž ¤Œlä9¾‚)óŠn’Ñì„Ðé#¾™yCòí”É'"ƘŸgÀ}_$\/,NäñË4ó ·…<ó×L„ ßMŽË"³ã)}{Ÿ1ûÓûç"~BN†–ô!j\“°­.Ýýo/áa— G 8wœ1sñ“^ äMÉr¹ÉÖ©ÿÿIÈö޾d†,r`Ìû+ß!‘¼¿ LA¡S÷rrL°dÔ¡„DGþ¡ï;2ìšp¦D`YÔÕC>ä)µ36m }ûg¢/ß7%eµ%iSQ–oªVô2!1YÕá‹ÎÈë!BR²œm„¼(xuÑ(U^Ö&>!5)!ŸJtúÐ& „MؘéZ!OGÌÐv‰²m$ïü&%¤‚Óz›_û*„̉(yž¾åΔ ôÊZåÖHÞ¬ƒžoèi5 Öé˜sÏò÷¦É²öõ¡Rð%»èiªÒ& çODòoÕ"¤ö "ãÒ!}¥Tö­ª­Äcé3CÓ¦FZ3d]È]4{¯‹²&Ó§ÈÀ7<_#)˔И õŸÊ[Ï^V!ì±…k¥gr}J¹; -IÊ=Ê×eîÀY囋ž¯iÔ:¤^²W‚ÝcM# Q] l,†„,¬h[J>Jx»€”G”6IHž×`²ŽO(dHL!FvM—T‘Sß6ÑþÔ dr¢íþ Ù\Љø%B‡0"¥¨::g1ïÓC„°x9"«ëŠA¡wfÓK‘¼ÕŽžV™OúÙMßV'ú¾‡OÈ‘™ñ("¦†²gB¨eú¾ƒ0L@øYž™kØ ±÷»„ ëû ÈŸ ì3ÅÚ®FeÞf ÜÝRP[@ëÓ¤x6ÃHjq™YQç žèd—ޝGB/+C7){&rRè?ÆGa[å µRv†ØåD­ªÀë‘V󩾪r©l™R¦¬BÅBœÐw°ÀÙy8K8S_ Lö²mö;_jb„œØ›„˜ _” kËÈÉÓBØþXTŸRö Çî“Ĭ¹S88ÔBH›„áð¿p­¯âÕû-³õ)9þ¤ôŸÕAÈt•y¥ SZŹ“ïÔýKK®CA7(ÅqÌy-b:{Èo„$*fã ›"¾Bþ¯÷Û…#<Å…õò>±`¶Þȑ̯|W›*UÝIûŸìW$¼Û [ ©/Ï …„©™ÁµLS=Û'×Ôâcª˜»¡r«3}U晴°o„½÷ œ­?.„g'‘Œ|îø¦îDmª´J'æU@š°Iò6¬–§Jydèrgmw¹‹\–T•RÔ· "¨¼…ÑÕmBîÓ·1¡úö!¶]ÿϬpDÉ…5ÌSô»æ<ûêùTá_–†Yø_¼Óó‡bv·­ðéÂïAÐ|fÍ¿Hw±Þ‰ç˜±ã”΂ ù¸Ïu¬ÉG;ȃ\¤=#°Éfªþßkuc÷ÿ Lðú|£Þ±|”ÈÊY?r}ÆòŒÕ3¿ ˜üóýA-¿²šˆ+Õ!¥q¡îå L®,Uìœv“‹Ùuôßýº ’niøF+#žûStÛÍÅ”2F ,\æ)`K½»ÅšïcÜà˜ü*ÀpÊF6ì<Ûà´ÂHC.äCNÞ§™ü¤¿(p#’Ȉ´NK£žU!¤MöäuûvŸ×÷Ñ;ÿf ".Óñm ýÆ£Õ¦§Ê„g#~ø‡òh ¹úÏW=yÄüÃ=AgZÇøÕî)öœÿÒJïLfMH×:¹ÖC'xõ% Q>VþÈ…bóQ=¾omçÇ zÇqõ¹y¦\6(è›À¨FwŒfù.–ß'ä—ÖÖÞžÜéÛYÂ@ 79tÚµ`†Ä|ί½ÊÝõ:s’0@øº ‘™///#sÀ|î°çØoIBB–+/Û×´…R¶°Žò5“á”°oúŽù`‹£Âoéý{V†²9!ÖÖW->†„0ÃW Ü_$ø„03–'½”¿"2K0[3…Ľ„q{]\š{+’ÿTOÉ›¨AŽ'ÀÖ#4ÓøËe0ý3… Ï&LMNˆò¸Ë „Ê\˜¦Þ)%qJéˆÈm[õ~Åêg²»¥uF™§oŸ˜]×z„ìcenñôå|õßÈ ™Qp-áïk*¿e¤“›©?›Îvg6×S†S»M€8üB—]eG˜)ÝE_§ïç[ù‡Jâ”{VDVœ4rT´­w” áBX—Æ¢2œ!óL7ð¼øÿ +=`.gYúl*<Ù RN”ÌÕûM FB3ZX±"ÌhKcAèì)‹¤oZÞ_鎒W úq3óG˜—Uw»|'½™,ÂpÊPþ£ž ŸÕû·­í_èŽ[ÛäÅÄPÖ÷!Ûéh‹( ÿ’"„È’q&ýp”s,}böMdŒª‘”UÊL@ÆFª˜ûFü¾Å2e±£8öc­Q”ɪ•<Éèœ~+s¾áð”Ï£,=»c×üIßöµ2è¿#äQ=Câž|œÁ0 1§ø Â_¢,ˆv£ùùýr2k&Aùø/ü¾ÏÝñŠbåJ\¨!Dˆ¬Ö©ƒˆíVky×ízH¹D]mäpÆî¯ÞqöÜù½Y8QøP8õMF*NŽÑX¾q¦g¢ „‡ :Fhì¯#P S˜]`„ž+°MÿC~ûœ›H=C$ë€ ø*Ò>1O{+PüA&7‘¤"iÈ™¥Yú•^½˜>zæ¬c¶å£íÓƒêÖ![ë3Å€ ò¤îa”y *²ý.IEND®B`‚plumbum-1.7.2/docs/changelog.rst0000644000232200023220000000006514161152302017155 0ustar debalancedebalanceChange Log ========== .. include:: ../CHANGELOG.rst plumbum-1.7.2/docs/quickref.rst0000644000232200023220000002323614161152302017044 0ustar debalancedebalance.. _guide-quickref: Quick reference guide --------------------- This is a cheatsheet for common tasks in Plumbum. CLI === Optional arguments ****************** ================ ========================= Utility Usage ================ ========================= ``Flag`` True or False descriptor ``SwitchAttr`` A value as a descriptor ``CountOf`` Counting version of ``Flag`` ``@switch`` A function that runs when passed ``@autoswitch`` A switch that gets its name from the function decorated ``@validator`` A positional argument validator on main (or use Py3 attributes) ================ ========================= Validators ********** Anything that produces a ``ValueError`` or ``TypeError``, is applied to argument. Some special ones included: ======================= ========================= Validator Usage ======================= ========================= ``Range`` A number in some range ``Set`` A choice in a set ``ExistingFile`` A file (converts to Path) ``ExistingDirectory`` A directory ``NonexistentPath`` Not a file or directory ======================= ========================= Common options ************** ================== ============================ ================== Option Used in Usage ================== ============================ ================== First argument Non-auto The name, or list of names, includes dash(es) Second argument All The validator docstring ``switch``, ``Application`` The help message ``help=`` All The help message ``list=True`` ``switch`` Allow multiple times (passed as list) ``requires=`` All A list of optional arguments to require ``excludes=`` All A list of optional arguments to exclude ``group=`` All The name of a group ``default=`` All The default if not given ``envname=`` ``SwitchAttr`` The name of an environment variable to check ``mandatory=True`` Switches Require this argument to be passed ================== ============================ ================== Special member variables ************************ ====================== ===================================== Utility Usage ====================== ===================================== ``PROGNAME=`` Custom program name and/or color ``VERSION=`` Custom version ``DESCRIPTION=`` Custom description (or use docstring) ``DESCRIPTION_MORE=`` Custom description with whitespace ``ALLOW_ABREV=True`` Allow argparse style abbreviations ``COLOR_USAGE=`` Custom color for usage statement ``COLOR_USAGE_TITLE=`` Custom color for usage statement's title ``COLOR_GROUPS=`` Colors of groups (dictionary) ``COLOR_GROUP_TITLES=`` Colors of group titles (dictionary) ====================== ===================================== Paths ===== ================= ============================= Idiom Description ================= ============================= ``local.cwd`` Common way to make paths ``/`` Construct Composition of parts ``//`` Construct Grep for files Sorting Alphabetical Iteration By parts To str Canonical full path Subtraction Relative path ``in`` Check for file in folder ================= ============================= .. The main difference is the loss of relative files =================================================== =========================== ================== Property Description Compare to Pathlib =================================================== =========================== ================== ``.name`` The file name ✓ ``.basename`` DEPRECATED ``.stem`` Name without extension ✓ ``.dirname`` Directory name ✗ ``.root`` The file tree root ✓ ``.drive`` Drive letter (Windows) ✓ ``.suffix`` The suffix ✓ ``.suffixes`` A list of suffixes ✓ ``.uid`` User ID ✗ ``.gid`` Group ID ✗ ``.parts`` Tuple of ``split`` ✓ ``.parents`` The ancestors of the path ✓ ``.parent`` The ancestor of the path ✓ =================================================== =========================== ================== .. Missing: .anchor =================================================== =========================== ================== Method Description Compare to Pathlib =================================================== =========================== ================== ``.up(count = 1)`` Go up count directories ✗ ``.walk(filter=*, dir_filter=*)`` Traverse directories ✗ ``.as_uri(scheme=None)`` Universal Resource ID ✓ ``.join(part, ...)`` Put together paths (``/``) ``.joinpath`` ``.list()`` Files in directory ✗ (shortcut) ``.iterdir()`` Fast iterator over dir ✓ ``.is_dir()`` If path is dir ✓ ``.isdir()`` DEPRECATED ``.is_file()`` If is file ✓ ``.isfile()`` DEPRECATED ``.is_symlink()`` If is symlink ✓ ``.islink()`` DEPRECATED ``.exists()`` If file exists ✓ ``.stat()`` Return OS stats ✓ ``.with_name(name)`` Replace filename ✓ ``.with_suffix(suffix, depth=1)`` Replace suffix ✓ (no depth) ``.preferred_suffix(suffix)`` Replace suffix if no suffix ✗ ``.glob(pattern)`` Search for pattern ✓ ``.split()`` Split into directories ``.parts`` ``.relative_to(source)`` Relative path (``-``) ✓ ``.resolve(strict=False)`` Does nothing ✓ ``.access(mode = 0)`` Check access permissions ✗ =================================================== =========================== ================== .. Missing: .match(pattern) .is_reserved() .is_absolute() .as_posix() .is_symlink() .is_fifo() .is_block_device() .is_char_device() .lchmod(mode) .lstat() =================================================== =========================== ================== Method (changes files) Description Compare to Pathlib =================================================== =========================== ================== ``.link(dst)`` Make a hard link ✗ ``.symlink(dst)`` Make a symlink ``.symlink_to`` ``.unlink()`` Unlink a file (delete) ✓ ``.delete()`` Delete file ``.unlink`` ``.move(dst)`` Move file ✗ ``.rename(newname)`` Change the file name ✓ ``.copy(dst, override=False)`` Copy a file ✗ ``.mkdir()`` Make a directory ✓ (+ more args) ``.open(mode="r")`` Open a file for reading ✓ (+ more args) ``.read(encoding=None)`` Read a file to text ``.read_text`` ``.write(data, encoding=None)`` Write to a file ``.write_text`` ``.touch()`` Touch a file ✓ (+ more args) ``.chown(owner=None, group=None, recursive=None)`` Change owner ✗ ``.chmod(mode)`` Change permissions ✓ =================================================== =========================== ================== .. Missing: .group() .owner() .read_bytes() .write_bytes() .replace(target) .rglob(pattern) .rmdir() .samefile() Colors ====== You pick colors from ``fg`` or ``bg``, also can ``reset`` Main colors: ``black`` ``red`` ``green`` ``yellow`` ``blue`` ``magenta`` ``cyan`` ``white`` Default styles: ``warn`` ``title`` ``fatal`` ``highlight`` ``info`` ``success`` Attrs: ``bold`` ``dim`` ``underline`` ``italics`` ``reverse`` ``strikeout`` ``hidden`` plumbum-1.7.2/docs/_cheatsheet.rst0000644000232200023220000001316414161152302017506 0ustar debalancedebalance Basics ------ .. code-block:: python >>> from plumbum import local >>> ls = local["ls"] >>> ls LocalCommand() >>> ls() u'build.py\ndist\ndocs\nLICENSE\nplumbum\nREADME.rst\nsetup.py\ntests\ntodo.txt\n' >>> notepad = local["c:\\windows\\notepad.exe"] >>> notepad() # Notepad window pops up u'' # Notepad window is closed by user, command returns Instead of writing ``xxx = local["xxx"]`` for every program you wish to use, you can also :ref:`import commands `: >>> from plumbum.cmd import grep, wc, cat, head >>> grep LocalCommand() Or, use the ``local.cmd`` syntactic-sugar: .. code-block:: python >>> local.cmd.ls LocalCommand() >>> local.cmd.ls() u'build.py\ndist\ndocs\nLICENSE\nplumbum\nREADME.rst\nsetup.py\ntests\ntodo.txt\n' See :ref:`guide-local-commands`. Piping ------ .. code-block:: python >>> chain = ls["-a"] | grep["-v", "\\.py"] | wc["-l"] >>> print chain /bin/ls -a | /bin/grep -v '\.py' | /usr/bin/wc -l >>> chain() u'13\n' See :ref:`guide-local-commands-pipelining`. Redirection ----------- .. code-block:: python >>> ((cat < "setup.py") | head["-n", 4])() u'#!/usr/bin/env python\nimport os\n\ntry:\n' >>> (ls["-a"] > "file.list")() u'' >>> (cat["file.list"] | wc["-l"])() u'17\n' See :ref:`guide-local-commands-redir`. Working-directory manipulation ------------------------------ .. code-block:: python >>> local.cwd >>> with local.cwd(local.cwd / "docs"): ... chain() ... u'15\n' A more explicit, and thread-safe way of running a command in a differet directory is using the ``.with_cwd()`` method: .. code-block:: python >>> ls_in_docs = local.cmd.ls.with_cwd("docs") >>> ls_in_docs() 'api\nchangelog.rst\n_cheatsheet.rst\ncli.rst\ncolorlib.rst\n_color_list.html\ncolors.rst\nconf.py\nindex.rst\nlocal_commands.rst\nlocal_machine.rst\nmake.bat\nMakefile\n_news.rst\npaths.rst\nquickref.rst\nremote.rst\n_static\n_templates\ntyped_env.rst\nutils.rst\n' See :ref:`guide-paths` and :ref:`guide-local-machine`. Foreground and background execution ----------------------------------- .. code-block:: python >>> from plumbum import FG, BG >>> (ls["-a"] | grep["\\.py"]) & FG # The output is printed to stdout directly build.py .pydevproject setup.py >>> (ls["-a"] | grep["\\.py"]) & BG # The process runs "in the background" See :ref:`guide-local-commands-bgfg`. Command nesting --------------- .. code-block:: python >>> from plumbum.cmd import sudo >>> print sudo[ifconfig["-a"]] /usr/bin/sudo /sbin/ifconfig -a >>> (sudo[ifconfig["-a"]] | grep["-i", "loop"]) & FG lo Link encap:Local Loopback UP LOOPBACK RUNNING MTU:16436 Metric:1 See :ref:`guide-local-commands-nesting`. Remote commands (over SSH) -------------------------- Supports `openSSH `_-compatible clients, `PuTTY `_ (on Windows) and `Paramiko `_ (a pure-Python implementation of SSH2) .. code-block:: python >>> from plumbum import SshMachine >>> remote = SshMachine("somehost", user = "john", keyfile = "/path/to/idrsa") >>> r_ls = remote["ls"] >>> with remote.cwd("/lib"): ... (r_ls | grep["0.so.0"])() ... u'libusb-1.0.so.0\nlibusb-1.0.so.0.0.0\n' See :ref:`guide-remote`. CLI applications ---------------- .. code-block:: python import logging from plumbum import cli class MyCompiler(cli.Application): verbose = cli.Flag(["-v", "--verbose"], help = "Enable verbose mode") include_dirs = cli.SwitchAttr("-I", list = True, help = "Specify include directories") @cli.switch("-loglevel", int) def set_log_level(self, level): """Sets the log-level of the logger""" logging.root.setLevel(level) def main(self, *srcfiles): print "Verbose:", self.verbose print "Include dirs:", self.include_dirs print "Compiling:", srcfiles if __name__ == "__main__": MyCompiler.run() Sample output +++++++++++++ :: $ python simple_cli.py -v -I foo/bar -Ispam/eggs x.cpp y.cpp z.cpp Verbose: True Include dirs: ['foo/bar', 'spam/eggs'] Compiling: ('x.cpp', 'y.cpp', 'z.cpp') See :ref:`guide-cli`. Colors and Styles ----------------- .. code-block:: python from plumbum import colors with colors.red: print("This library provides safe, flexible color access.") print(colors.bold | "(and styles in general)", "are easy!") print("The simple 16 colors or", colors.orchid & colors.underline | '256 named colors,', colors.rgb(18, 146, 64) | "or full rgb colors" , 'can be used.') print("Unsafe " + colors.bg.dark_khaki + "color access" + colors.bg.reset + " is available too.") Sample output +++++++++++++ .. raw:: html
This library provides safe color access.
    Color (and styles in general) are easy!
    The simple 16 colors, 256 named colors, or full hex colors can be used.
    Unsafe color access is available too.
See :ref:`guide-colors`. plumbum-1.7.2/docs/cli.rst0000644000232200023220000006212714161152302016004 0ustar debalancedebalance.. _guide-cli: Command-Line Interface (CLI) ============================ The other side of *executing programs* with ease is **writing CLI programs** with ease. Python scripts normally use ``optparse`` or the more recent ``argparse``, and their `derivatives `_; but all of these are somewhat limited in their expressive power, and are quite **unintuitive** (and even **unpythonic**). Plumbum's CLI toolkit offers a **programmatic approach** to building command-line applications; instead of creating a parser object and populating it with a series of "options", the CLI toolkit translates these primitives into Pythonic constructs and relies on introspection. From a bird's eye view, CLI applications are classes that extend :class:`plumbum.cli.Application`. They define a ``main()`` method and optionally expose methods and attributes as command-line :func:`switches `. Switches may take arguments, and any remaining positional arguments are given to the ``main`` method, according to its signature. A simple CLI application might look like this:: from plumbum import cli class MyApp(cli.Application): verbose = cli.Flag(["v", "verbose"], help = "If given, I will be very talkative") def main(self, filename): print("I will now read {0}".format(filename)) if self.verbose: print("Yadda " * 200) if __name__ == "__main__": MyApp.run() And you can run it:: $ python example.py foo I will now read foo $ python example.py --help example.py v1.0 Usage: example.py [SWITCHES] filename Meta-switches: -h, --help Prints this help message and quits --version Prints the program's version and quits Switches: -v, --verbose If given, I will be very talkative So far you've only seen the very basic usage. We'll now start to explore the library. .. versionadded:: 1.6.1 You can also directly run the app, as ``MyApp()``, without arguments, instead of calling ``.main()``. Application ----------- The :class:`Application ` class is the "container" of your application. It consists of the ``main()`` method, which you should implement, and any number of CLI-exposed switch functions or attributes. The entry-point for your application is the classmethod ``run``, which instantiates your class, parses the arguments, invokes all switch functions, and then calls ``main()`` with the given positional arguments. In order to run your application from the command-line, all you have to do is :: if __name__ == "__main__": MyApp.run() Aside from ``run()`` and ``main()``, the ``Application`` class exposes two built-in switch functions: ``help()`` and ``version()`` which take care of displaying the help and program's version, respectively. By default, ``--help`` and ``-h`` invoke ``help()``, and ``--version`` and ``-v`` invoke ``version()``; if any of these functions is called, the application will display the message and quit (without processing any other switch). You can customize the information displayed by ``help()`` and ``version`` by defining class-level attributes, such as ``PROGNAME``, ``VERSION`` and ``DESCRIPTION``. For instance, :: class MyApp(cli.Application): PROGNAME = "Foobar" VERSION = "7.3" Colors ^^^^^^ .. versionadded:: 1.6 Colors are supported. You can use a colored string on ``PROGNAME``, ``VERSION`` and ``DESCRIPTION`` directly. If you set ``PROGNAME`` to a color, you can get auto-naming and color. The color of the usage string is available as ``COLOR_USAGE``. The color of ``Usage:`` line itself may be specified using ``COLOR_USAGE_TITLE``, otherwise it defaults to ``COLOR_USAGE``. Different groups can be colored with a dictionaries ``COLOR_GROUPS`` and ``COLOR_GROUP_TITLES``. For instance, the following is valid:: class MyApp(cli.Application): PROGNAME = colors.green VERSION = colors.blue | "1.0.2" COLOR_GROUPS = {"Switches": colors.blue | "Meta-switches" : colors.yellow} COLOR_GROUP_TITLES = {"Switches": colors.bold | colors.blue, "Meta-switches" : colors.bold & colors.yellow} opts = cli.Flag("--ops", help=colors.magenta | "This is help") .. raw:: html
    SimpleColorCLI.py 1.0.2

    Usage:
        SimpleColorCLI.py [SWITCHES] 

    Meta-switches
        -h, --help         Prints this help message and quits
        --help-all         Print help messages of all subcommands and quit
        -v, --version      Prints the program's version and quits
    

    Switches:
        --ops              This is help
    
Switch Functions ---------------- The decorator :func:`switch ` can be seen as the "heart and soul" of the CLI toolkit; it exposes methods of your CLI application as CLI-switches, allowing them to be invoked from the command line. Let's examine the following toy application:: class MyApp(cli.Application): _allow_root = False # provide a default @cli.switch("--log-to-file", str) def log_to_file(self, filename): """Sets the file into which logs will be emitted""" logger.addHandler(FileHandle(filename)) @cli.switch(["-r", "--root"]) def allow_as_root(self): """If given, allow running as root""" self._allow_root = True def main(self): if os.geteuid() == 0 and not self._allow_root: raise ValueError("cannot run as root") When the program is run, the switch functions are invoked with their appropriate arguments; for instance, ``$ ./myapp.py --log-to-file=/tmp/log`` would translate to a call to ``app.log_to_file("/tmp/log")``. After all switches were processed, control passes to ``main``. .. note:: Methods' docstrings and argument names will be used to render the help message, keeping your code as `DRY `_ as possible. There's also :func:`autoswitch `, which infers the name of the switch from the function's name, e.g. :: @cli.autoswitch(str) def log_to_file(self, filename): pass Will bind the switch function to ``--log-to-file``. Arguments ^^^^^^^^^ As demonstrated in the example above, switch functions may take no arguments (not counting ``self``) or a single argument argument. If a switch function accepts an argument, it must specify the argument's *type*. If you require no special validation, simply pass ``str``; otherwise, you may pass any type (or any callable, in fact) that will take a string and convert it to a meaningful object. If conversion is not possible, the type (or callable) is expected to raise either ``TypeError`` or ``ValueError``. For instance :: class MyApp(cli.Application): _port = 8080 @cli.switch(["-p"], int) def server_port(self, port): self._port = port def main(self): print(self._port) :: $ ./example.py -p 17 17 $ ./example.py -p foo Argument of -p expected to be , not 'foo': ValueError("invalid literal for int() with base 10: 'foo'",) The toolkit includes two additional "types" (or rather, *validators*): ``Range`` and ``Set``. ``Range`` takes a minimal value and a maximal value and expects an integer in that range (inclusive). ``Set`` takes a set of allowed values, and expects the argument to match one of these values. Here's an example :: class MyApp(cli.Application): _port = 8080 _mode = "TCP" @cli.switch("-p", cli.Range(1024,65535)) def server_port(self, port): self._port = port @cli.switch("-m", cli.Set("TCP", "UDP", case_sensitive = False)) def server_mode(self, mode): self._mode = mode def main(self): print(self._port, self._mode) :: $ ./example.py -p 17 Argument of -p expected to be [1024..65535], not '17': ValueError('Not in range [1024..65535]',) $ ./example.py -m foo Argument of -m expected to be Set('udp', 'tcp'), not 'foo': ValueError("Expected one of ['UDP', 'TCP']",) .. note:: The toolkit also provides some other useful validators: `ExistingFile` (ensures the given argument is an existing file), `ExistingDirectory` (ensures the given argument is an existing directory), and `NonexistentPath` (ensures the given argument is not an existing path). All of these convert the argument to a :ref:`local path `. Repeatable Switches ^^^^^^^^^^^^^^^^^^^ Many times, you would like to allow a certain switch to be given multiple times. For instance, in ``gcc``, you may give several include directories using ``-I``. By default, switches may only be given once, unless you allow multiple occurrences by passing ``list = True`` to the ``switch`` decorator :: class MyApp(cli.Application): _dirs = [] @cli.switch("-I", str, list = True) def include_dirs(self, dirs): self._dirs = dirs def main(self): print(self._dirs) :: $ ./example.py -I/foo/bar -I/usr/include ['/foo/bar', '/usr/include'] .. note:: The switch function will be called **only once**, and its argument will be a list of items Mandatory Switches ^^^^^^^^^^^^^^^^^^ If a certain switch is required, you can specify this by passing ``mandatory = True`` to the ``switch`` decorator. The user will not be able to run the program without specifying a value for this switch. Dependencies ^^^^^^^^^^^^ Many time, the occurrence of a certain switch depends on the occurrence of another, e..g, it may not be possible to give ``-x`` without also giving ``-y``. This constraint can be achieved by specifying the ``requires`` keyword argument to the ``switch`` decorator; it is a list of switch names that this switch depends on. If the required switches are missing, the user will not be able to run the program. :: class MyApp(cli.Application): @cli.switch("--log-to-file", str) def log_to_file(self, filename): logger.addHandler(logging.FileHandler(filename)) @cli.switch("--verbose", requires = ["--log-to-file"]) def verbose(self): logger.setLevel(logging.DEBUG) :: $ ./example --verbose Given --verbose, the following are missing ['log-to-file'] .. warning:: The toolkit invokes the switch functions in the same order in which the switches were given on the command line. It doesn't go as far as computing a topological order on the fly, but this will change in the future. Mutual Exclusion ^^^^^^^^^^^^^^^^^ Just as some switches may depend on others, some switches mutually-exclude others. For instance, it does not make sense to allow ``--verbose`` and ``--terse``. For this purpose, you can set the ``excludes`` list in the ``switch`` decorator. :: class MyApp(cli.Application): @cli.switch("--log-to-file", str) def log_to_file(self, filename): logger.addHandler(logging.FileHandler(filename)) @cli.switch("--verbose", requires = ["--log-to-file"], excludes = ["--terse"]) def verbose(self): logger.setLevel(logging.DEBUG) @cli.switch("--terse", requires = ["--log-to-file"], excludes = ["--verbose"]) def terse(self): logger.setLevel(logging.WARNING) :: $ ./example --log-to-file=log.txt --verbose --terse Given --verbose, the following are invalid ['--terse'] Grouping ^^^^^^^^ If you wish to group certain switches together in the help message, you can specify ``group = "Group Name"``, where ``Group Name`` is any string. When the help message is rendered, all the switches that belong to the same group will be grouped together. Note that grouping has no other effects on the way switches are processed, but it can help improve the readability of the help message. Switch Attributes ----------------- Many times it's desired to simply store a switch's argument in an attribute, or set a flag if a certain switch is given. For this purpose, the toolkit provides :class:`SwitchAttr `, which is `data descriptor `_ that stores the argument in an instance attribute. There are two additional "flavors" of ``SwitchAttr``: ``Flag`` (which toggles its default value if the switch is given) and ``CountOf`` (which counts the number of occurrences of the switch) :: class MyApp(cli.Application): log_file = cli.SwitchAttr("--log-file", str, default = None) enable_logging = cli.Flag("--no-log", default = True) verbosity_level = cli.CountOf("-v") def main(self): print(self.log_file, self.enable_logging, self.verbosity_level) .. code-block:: bash $ ./example.py -v --log-file=log.txt -v --no-log -vvv log.txt False 5 Environment Variables ^^^^^^^^^^^^^^^^^^^^^ .. versionadded:: 1.6 You can also set a ``SwitchAttr`` to take an environment variable as an input using the envname parameter. For example:: class MyApp(cli.Application): log_file = cli.SwitchAttr("--log-file", str, envname="MY_LOG_FILE") def main(self): print(self.log_file) .. code-block:: bash $ MY_LOG_FILE=this.log ./example.py this.log Giving the switch on the command line will override the environment variable value. Main ---- The ``main()`` method takes control once all the command-line switches have been processed. It may take any number of *positional argument*; for instance, in ``cp -r /foo /bar``, ``/foo`` and ``/bar`` are the *positional arguments*. The number of positional arguments that the program would accept depends on the signature of the method: if the method takes 5 arguments, 2 of which have default values, then at least 3 positional arguments must be supplied by the user and at most 5. If the method also takes varargs (``*args``), the number of arguments that may be given is unbound :: class MyApp(cli.Application): def main(self, src, dst, mode = "normal"): print(src, dst, mode) :: $ ./example.py /foo /bar /foo /bar normal $ ./example.py /foo /bar spam /foo /bar spam $ ./example.py /foo Expected at least 2 positional arguments, got ['/foo'] $ ./example.py /foo /bar spam bacon Expected at most 3 positional arguments, got ['/foo', '/bar', 'spam', 'bacon'] .. note:: The method's signature is also used to generate the help message, e.g. :: Usage: [SWITCHES] src dst [mode='normal'] With varargs:: class MyApp(cli.Application): def main(self, src, dst, *eggs): print(src, dst, eggs) :: $ ./example.py a b c d a b ('c', 'd') $ ./example.py --help Usage: [SWITCHES] src dst eggs... Meta-switches: -h, --help Prints this help message and quits -v, --version Prints the program's version and quits Positional argument validation ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. versionadded:: 1.6 You can supply positional argument validators using the ``cli.positional`` decorator. Simply pass the validators in the decorator matching the names in the main function. For example:: class MyApp(cli.Application): @cli.positional(cli.ExistingFile, cli.NonexistentPath) def main(self, infile, *outfiles): "infile is a path, outfiles are a list of paths, proper errors are given" If you only want to run your application in Python 3, you can also use annotations to specify the validators. For example:: class MyApp(cli.Application): def main(self, infile : cli.ExistingFile, *outfiles : cli.NonexistentPath): "Identical to above MyApp" Annotations are ignored if the positional decorator is present. Switch Abbreviations ^^^^^^^^^^^^^^^^^^^^ The cli supports switches which have been abbreviated by the user, for example, "--h", "--he", or "--hel" would all match an actual switch name of"--help", as long as no ambiguity arises from multiple switches that might match the same abbreviation. This behavior is disabled by default but can be enabled by defining the class-level attribute ``ALLOW_ABBREV`` to True. For example:: class MyApp(cli.Application): ALLOW_ABBREV = True cheese = cli.Flag(["cheese"], help = "cheese, please") chives = cli.Flag(["chives"], help = "chives, instead") With the above definition, running the following will raise an error due to ambiguity:: $ python example.py --ch # error! matches --cheese and --chives However, the following two lines are equivalent:: $ python example.py --che $ python example.py --cheese .. _guide-subcommands: Sub-commands ------------ .. versionadded:: 1.1 A common practice of CLI applications, as they span out and get larger, is to split their logic into multiple, pluggable *sub-applications* (or *sub-commands*). A classic example is version control systems, such as `git `_, where ``git`` is the *root* command, under which sub-commands such as ``commit`` or ``push`` are nested. Git even supports ``alias``-ing, which creates allows users to create custom sub-commands. Plumbum makes writing such applications really easy. Before we get to the code, it is important to stress out two things: * Under Plumbum, each sub-command is a full-fledged ``cli.Application`` on its own; if you wish, you can execute it separately, detached from its so-called root application. When an application is run independently, its ``parent`` attribute is ``None``; when it is run as a sub-command, its ``parent`` attribute points to its parent application. Likewise, when an parent application is executed with a sub-command, its ``nested_command`` is set to the nested application; otherwise it's ``None``. * Each sub-command is responsible of **all** arguments that follow it (up to the next sub-command). This allows applications to process their own switches and positional arguments before the nested application is invoked. Take, for instance, ``git --foo=bar spam push origin --tags``: the root application, ``git``, is in charge of the switch ``--foo`` and the positional argument ``spam``, and the nested application ``push`` is in charge of the arguments that follow it. In theory, you can nest several sub-applications one into the other; in practice, only a single level is normally used. Here is an example of a mock version control system, called ``geet``. We're going to have a root application ``Geet``, which has two sub-commands - ``GeetCommit`` and ``GeetPush``: these are attached to the root application using the ``subcommand`` decorator :: class Geet(cli.Application): """The l33t version control""" VERSION = "1.7.2" def main(self, *args): if args: print("Unknown command {0!r}".format(args[0])) return 1 # error exit code if not self.nested_command: # will be ``None`` if no sub-command follows print("No command given") return 1 # error exit code @Geet.subcommand("commit") # attach 'geet commit' class GeetCommit(cli.Application): """creates a new commit in the current branch""" auto_add = cli.Flag("-a", help = "automatically add changed files") message = cli.SwitchAttr("-m", str, mandatory = True, help = "sets the commit message") def main(self): print("doing the commit...") @Geet.subcommand("push") # attach 'geet push' class GeetPush(cli.Application): """pushes the current local branch to the remote one""" def main(self, remote, branch = None): print("doing the push...") if __name__ == "__main__": Geet.run() .. note:: * Since ``GeetCommit`` is a ``cli.Application`` on its own right, you may invoke ``GeetCommit.run()`` directly (should that make sense in the context of your application) * You can also attach sub-commands "imperatively", using ``subcommand`` as a method instead of a decorator: ``Geet.subcommand("push", GeetPush)`` Here's an example of running this application:: $ python geet.py --help geet v1.7.2 The l33t version control Usage: geet.py [SWITCHES] [SUBCOMMAND [SWITCHES]] args... Meta-switches: -h, --help Prints this help message and quits -v, --version Prints the program's version and quits Subcommands: commit creates a new commit in the current branch; see 'geet commit --help' for more info push pushes the current local branch to the remote one; see 'geet push --help' for more info $ python geet.py commit --help geet commit v1.7.2 creates a new commit in the current branch Usage: geet commit [SWITCHES] Meta-switches: -h, --help Prints this help message and quits -v, --version Prints the program's version and quits Switches: -a automatically add changed files -m VALUE:str sets the commit message; required $ python geet.py commit -m "foo" committing... Configuration parser -------------------- Another common task of a cli application is provided by a configuration parser, with an INI backend: ``Config`` (or ``ConfigINI`` to explicitly request the INI backend). An example of it's use:: from plumbum import cli with cli.Config('~/.myapp_rc') as conf: one = conf.get('one', '1') two = conf.get('two', '2') If no configuration file is present, this will create one and each call to ``.get`` will set the value with the given default. The file is created when the context manager exits. If the file is present, it is read and the values from the file are selected, and nothing is changed. You can also use ``[]`` syntax to forcably set a value, or to get a value with a standard ``ValueError`` if not present. If you want to avoid the context manager, you can use ``.read`` and ``.write`` as well. The ini parser will default to using the ``[DEFAULT]`` section for values, just like Python's ConfigParser on which it is based. If you want to use a different section, simply seperate section and heading with a ``.`` in the key. ``conf['section.item']`` would place ``item`` under ``[section]``. All items stored in an ``ConfigINI`` are converted to ``str``, and ``str`` is always returned. Terminal Utilities ------------------ Several terminal utilities are available in ``plumbum.cli.terminal`` to assist in making terminal applications. ``get_terminal_size(default=(80,25))`` allows cross platform access to the terminal size as a tuple ``(width, height)``. Several methods to ask the user for input, such as ``readline``, ``ask``, ``choose``, and ``prompt`` are available. ``Progress(iterator)`` allows you to quickly create a progress bar from an iterator. Simply wrap a slow iterator with this and iterate over it, and it will produce a nice text progress bar based on the user's screen width, with estimated time remaining displayed. If you need to create a progress bar for a fast iterator but with a loop containing code, use ``Progress.wrap`` or ``Progress.range``. For example:: for i in Progress.range(10): time.sleep(1) If you have something that produces output, but still needs a progress bar, pass ``has_output=True`` to force the bar not to try to erase the old one each time. A command line image plotter (``Image``) is provided in ``plumbum.cli.image``. It can plot a PIL-like image ``im`` using:: Image().show_pil(im) The Image constructor can take an optional size (defaults to the current terminal size if None), and a `char_ratio`, a height to width measure for your current font. It defaults to a common value of 2.45. If set to None, the ratio is ignored and the image will no longer be constrained to scale proportionately. To directly plot an image, the ``show`` method takes a filename and a double parameter, which doubles the vertical resolution on some fonts. The `show_pil` and `show_pil_double` methods directly take a PIL-like object. To plot an image from the command line, the module can be run directly: ``python -m plumbum.cli.image myimage.png``. For the full list of helpers or more information, see the :ref:`api docs `. See Also -------- * `filecopy.py `_ example * `geet.py `_ - a runnable example of using sub-commands * `RPyC `_ has changed it bash-based build script to Plumbum CLI. Notice `how short and readable `_ it is. * A `blog post `_ describing the philosophy of the CLI module plumbum-1.7.2/docs/colorlib.rst0000644000232200023220000003311014161152302017030 0ustar debalancedebalance.. _guide-colorlib: Colorlib design --------------- .. versionadded:: 1.6 The purpose of this document is to describe the system that plumbum.colors implements. This system was designed to be flexible and to allow implementing new color backends. Hopefully this document will allow future work on colorlib to be as simple as possible. .. note:: Enabling color ``plumbum.colors`` tries to guess the color output settings of your system. You can force the use of color globally by setting ``colors.use_color=True`` See :ref:`guide-colorlist` for more options. Generating colors ================= Styles are accessed through the ``colors`` object, which is an instance of a StyleFactory. The ``colors`` object is actually an imitation module that wraps ``plumbum.colorlib.ansicolors`` with module-like access. Thus, things like from ``plumbum.colors.bg import red`` work also. The library actually lives in ``plumbum.colorlib``. Style Factory ^^^^^^^^^^^^^ The ``colors`` object has the following available objects: ``fg`` and ``bg`` The foreground and background colors, reset to default with ``colors.fg.reset`` or ``~colors.fg`` and likewise for ``bg``. These are ``ColorFactory`` instances. ``bold``, ``dim``, ``underline``, ``italics``, ``reverse``, ``strikeout``, and ``hidden`` All the `ANSI` modifiers are available, as well as their negations, such as ``~colors.bold`` or ``colors.bold.reset``, etc. (These are generated automatically based on the Style attached to the factory.) ``reset`` The global reset will restore all properties at once. ``do_nothing`` Does nothing at all, but otherwise acts like any ``Style`` object. It is its own inverse. Useful for ``cli`` properties. The ``colors`` object can be used in a with statement, which resets all styles on leaving the statement body. Although factories do support some of the same methods as a Style, their primary purpose is to generate Styles. The colors object has a ``use_color`` property that can be set to force the use of color. A ``stdout`` property is provided to make changing the output of color statement easier. A ``colors.from_ansi(code)`` method allows you to create a Style from any ansi sequence, even complex or combined ones. Color Factories ^^^^^^^^^^^^^^^ The ``colors.fg`` and ``colors.bg`` are ``ColorFactory``'s. In fact, the colors object itself acts exactly like the ``colors.fg`` object, with the exception of the properties listed above. Named foreground colors are available directly as methods. The first 16 primary colors, ``black``, ``red``, ``green``, ``yellow``, ``blue``, ``magenta``, ``cyan``, etc, as well as ``reset``, are available. All 256 color names are available, but do not populate factory directly, so that auto-completion gives reasonable results. You can also access colors using strings and do ``colors[string]``. Capitalization, underscores, and spaces (for strings) will be ignored. You can also access colors numerically with ``colors(n)`` or ``colors[n]`` with the extended 256 color codes. The former will default to simple versions of colors for the first 16 values. The later notation can also be used to slice. Full hex codes can be used, too. If no match is found, these will be the true 24 bit color value. The ``fg`` and ``bg`` also can be put in with statements, and they will restore the foreground and background color only, respectively. ``colors.rgb(r,g,b)`` will create a color from an input red, green, and blue values (integers from 0-255). ``colors.rgb(code)`` will allow you to input an html style hex sequence. These work on ``fg`` and ``bg`` too. The ``repr`` of styles is smart and will show you the closest color to the one you selected if you didn't exactly select a color through RGB. Style manipulations =================== Safe color manipulations refer to changes that reset themselves at some point. Unsafe manipulations must be manually reset, and can leave your terminal color in an unreadable state if you forget to reset the color or encounter an exception. If you do get the color unset on a terminal, the following, typed into the command line, will restore it: .. code:: bash $ python -m plumbum.colors This also supports command line access to unsafe color manipulations, such as .. code:: bash $ python -m plumbum.colors blue $ python -m plumbum.colors bg red $ python -m plumbum.colors fg 123 $ python -m plumbum.colors bg reset $ python -m plumbum.colors underline You can use any path or number available as a style. Unsafe Manipulation ^^^^^^^^^^^^^^^^^^^ Styles have two unsafe operations: Concatenation (with ``+`` and a string) and calling ``.now()`` without arguments (directly calling a style without arguments is also a shortcut for ``.now()``). These two operations do not restore normal color to the terminal by themselves. To protect their use, you should always use a context manager around any unsafe operation. An example of the usage of unsafe ``colors`` manipulations inside a context manager:: from plumbum import colors with colors: colors.fg.red.now() print('This is in red') colors.green.now() print('This is green ' + colors.underline + 'and now also underlined!') print('Underlined' + colors.underline.reset + ' and not underlined but still red') print('This is completly restored, even if an exception is thrown!') Output: .. raw:: html

This is in red
This is in green and now also underlined!
Underlined and not underlined but still green.
This is completly restored, even if an exception is thrown!

We can use ``colors`` instead of ``colors.fg`` for foreground colors. If we had used ``colors.fg`` as the context manager, then non-foreground properties, such as ``colors.underline`` or ``colors.bg.yellow``, would not have reset those properties. Each attribute, as well as ``fg``, ``bg``, and ``colors`` all have inverses in the ANSI standard. They are accessed with ``~`` or ``.reset``, and can be used to manually make these operations safer, but there is a better way. Safe Manipulation ^^^^^^^^^^^^^^^^^ All other operations are safe; they restore the color automatically. The first, and hopefully already obvious one, is using a Style rather than a ``colors`` or ``colors.fg`` object in a ``with`` statement. This will set the color (using sys.stdout by default) to that color, and restore color on leaving. The second method is to manually wrap a string. This can be done with ``color.wrap("string")`` or ``color["string"]``. These produce strings that can be further manipulated or printed. Finally, you can also print a color to stdout directly using ``color.print("string")``. This has the same syntax as the Python 3 print function. In Python 2, if you do not have ``from __future__ import print_function`` enabled, ``color.print_("string")`` is provided as an alternative, following the PyQT convention for method names that match reserved Python syntax. An example of safe manipulations:: colors.fg.yellow('This is yellow', end='') print(' And this is normal again.') with colors.red: print('Red color!') with colors.bold: print("This is red and bold.") print("Not bold, but still red.") print("Not red color or bold.") print((colors.magenta & colors.bold)["This is bold and colorful!"], "And this is not.") Output: .. raw:: html

This is yellow And this is normal again.
Red color!
This is red and bold.
Not bold, but still red.
Not red color or bold.
This is bold and colorful! And this is not.

Style Combinations ^^^^^^^^^^^^^^^^^^ You can combine styles with ``&`` and they will create a new combined Style object. Colors will not be "summed" or otherwise combined; the rightmost color will be used (this matches the expected effect of applying the Styles individually to the strings). However, combined Styles are intelligent and know how to reset just the properties that they contain. As you have seen in the example above, the combined style ``(colors.magenta & colors.bold)`` can be used in any way a normal Style can. Since wrapping is done with ``|``, the Python order of operations causes styles to be combined first, then wrapping is done last. .. _guide-colorlist: 256 Color Support ================= While this library supports full 24 bit colors through escape sequences, the library has special support for the "full" 256 colorset through numbers, names or HEX html codes. Even if you use 24 bit color, the closest name is displayed in the ``repr``. You can access the colors as as ``colors.fg.Light_Blue``, ``colors.fg.lightblue``, ``colors.fg[12]``, ``colors.fg('Light_Blue')``, ``colors.fg('LightBlue')``, or ``colors.fg('#0000FF')``. You can also iterate or slice the ``colors``, ``colors.fg``, or ``colors.bg`` objects. Slicing even intelligently downgrades to the simple version of the codes if it is within the first 16 elements. The supported colors are: .. raw:: html :file: _color_list.html If you want to enforce a specific representation, you can use ``.basic`` (8 color), ``.simple`` (16 color), ``.full`` (256 color), or ``.true`` (24 bit color) on a Style, and the colors in that Style will conform to the output representation and name of the best match color. The internal RGB colors are remembered, so this is a non-destructive operation. To limit the use of color to one of these styles, set ``colors.use_color`` to 1 for 8 colors, 2 for 16 colors, 3 for 256 colors, or 4 for true color. It will be guessed based on your system on initialisation. The Classes =========== The library consists of three primary classes, the ``Color`` class, the ``Style`` class, and the ``StyleFactory`` class. The following portion of this document is primarily dealing with the working of the system, and is meant to facilitate extensions or work on the system. The ``Color`` class provides meaning to the concept of color, and can provide a variety of representations for any color. It can be initialised from r,g,b values, or hex codes, 256 color names, or the simple color names via classmethods. If initialized without arguments, it is the reset color. It also takes an fg True/False argument to indicate which color it is. You probably will not be interacting with the Color class directly, and you probably will not need to subclass it, though new extensions to the representations it can produce are welcome. The ``Style`` class hold two colors and a dictionary of attributes. It is the workhorse of the system and is what is produced by the ``colors`` factory. It holds ``Color`` as ``.color_class``, which can be overridden by subclasses (again, this usually is not needed). To create a color representation, you need to subclass ``Style`` and give it a working ``__str__`` definition. ``ANSIStyle`` is derived from ``Style`` in this way. The factories, ``ColorFactory`` and ``StyleFactory``, are factory classes that are meant to provide simple access to 1 style Style classes. To use, you need to initialize an object of ``StyleFactory`` with your intended Style. For example, ``colors`` is created by:: colors = StyleFactory(ANSIStyle) Subclassing Style ^^^^^^^^^^^^^^^^^ For example, if you wanted to create an HTMLStyle and HTMLcolors, you could do:: class HTMLStyle(Style): attribute_names = dict(bold='b', li='li', code='code') end = '
\n' def __str__(self): result = '' if self.bg and not self.bg.reset: result += ''.format(self.bg.hex_code) if self.fg and not self.fg.reset: result += ''.format(self.fg.hex_code) for attr in sorted(self.attributes): if self.attributes[attr]: result += '<' + self.attribute_names[attr] + '>' for attr in reversed(sorted(self.attributes)): if not self.attributes[attr]: result += '' if self.fg and self.fg.reset: result += '' if self.bg and self.bg.reset: result += '' return result htmlcolors = StyleFactory(HTMLStyle) This doesn't support global resets, since that's not how HTML works, but otherwise is a working implementation. This is an example of how easy it is to add support for other output formats. An example of usage:: >>> htmlcolors.bold & htmlcolors.red | "This is colored text" 'This is colored text' The above color table can be generated with:: for color in htmlcolors: htmlcolors.li( "■" | color, color.fg.hex_code | htmlcolors.code, color.fg.name_camelcase) .. note:: ``HTMLStyle`` is implemented in the library, as well, with the ``htmlcolors`` object available in ``plumbum.colorlib``. It was used to create the colored output in this document, with small changes because ``colors.reset`` cannot be supported with HTML. See Also ======== * `colored `_ Another library with 256 color support * `colorama `_ A library that supports colored text on Windows, can be combined with Plumbum.colors (if you force ``use_color``, doesn't support all extended colors) plumbum-1.7.2/docs/make.bat0000644000232200023220000001201214161152302016074 0ustar debalancedebalance@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\PlumbumShellCombinators.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\PlumbumShellCombinators.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) :end plumbum-1.7.2/docs/local_machine.rst0000644000232200023220000001010714161152302020002 0ustar debalancedebalance.. _guide-local-machine: The Local Object ================ So far we've only seen running local commands, but there's more to the ``local`` object than this; it aims to "fully represent" the *local machine*. First, you should get acquainted with ``which``, which performs program name resolution in the system ``PATH`` and returns the first match (or raises an exception if no match is found):: >>> local.which("ls") >>> local.which("nonexistent") Traceback (most recent call last): [...] plumbum.commands.CommandNotFound: ('nonexistent', [...]) Another member is ``python``, which is a command object that points to the current interpreter (``sys.executable``):: >>> local.python >>> local.python("-c", "import sys;print sys.version") '2.7.2 (default, Jun 12 2011, 15:08:59) [MSC v.1500 32 bit (Intel)]\r\n' Working Directory ----------------- The ``local.cwd`` attribute represents the current working directory. You can change it like so:: >>> local.cwd >>> local.cwd.chdir("d:\\workspace\\plumbum\\docs") >>> local.cwd You can also use it as a *context manager*, so it behaves like ``pushd``/``popd``:: >>> with local.cwd("c:\\windows"): ... print "%s:%s" % (local.cwd, (ls | wc["-l"])()) ... with local.cwd("c:\\windows\\system32"): ... print "%s:%s" % (local.cwd, (ls | wc["-l"])()) ... c:\windows: 105 c:\windows\system32: 3013 >>> print "%s:%s" % (local.cwd, (ls | wc["-l"])()) d:\workspace\plumbum: 9 Finally, A more explicit and thread-safe way of running a command in a different directory is using the ``.with_cwd()`` method: >>> ls_in_docs = local.cmd.ls.with_cwd("docs") >>> ls_in_docs() 'api\nchangelog.rst\n_cheatsheet.rst\ncli.rst\ncolorlib.rst\n_color_list.html\ncolors.rst\nconf.py\nindex.rst\nlocal_commands.rst\nlocal_machine.rst\nmake.bat\nMakefile\n_news.rst\npaths.rst\nquickref.rst\nremote.rst\n_static\n_templates\ntyped_env.rst\nutils.rst\n' Environment ----------- Much like ``cwd``, ``local.env`` represents the *local environment*. It is a dictionary-like object that holds **environment variables**, which you can get/set intuitively:: >>> local.env["JAVA_HOME"] 'C:\\Program Files\\Java\\jdk1.6.0_20' >>> local.env["JAVA_HOME"] = "foo" And similarity to ``cwd`` is the context-manager nature of ``env``; each level would have it's own private copy of the environment:: >>> with local.env(FOO="BAR"): ... local.python("-c", "import os;print os.environ['FOO']") ... with local.env(FOO="SPAM"): ... local.python("-c", "import os;print os.environ['FOO']") ... local.python("-c", "import os;print os.environ['FOO']") ... 'BAR\r\n' 'SPAM\r\n' 'BAR\r\n' >>> local.python("-c", "import os;print(os.environ['FOO'])") Traceback (most recent call last): [...] ProcessExecutionError: Unexpected exit code: 1 Command line: | /usr/bin/python -c "import os;print(os.environ['FOO'])" Stderr: | Traceback (most recent call last): | File "", line 1, in | File "/usr/lib/python3.5/os.py", line 725, in __getitem__ | raise KeyError(key) from None | KeyError: 'FOO' In order to make cross-platform-ness easier, the ``local.env`` object provides some convenience properties for getting the username (``.user``), the home path (``.home``), and the executable path (``path``) as a list. For instance:: >>> local.env.user 'sebulba' >>> local.env.home >>> local.env.path [, , ...] >>> >>> local.which("python") >>> local.env.path.insert(0, "c:\\python32") >>> local.which("python") For further information, see the :ref:`api docs `. plumbum-1.7.2/docs/paths.rst0000644000232200023220000001016214161152302016344 0ustar debalancedebalance .. _guide-paths: Paths ===== Apart from commands, Plumbum provides an easy to use path class that represents file system paths. Paths are returned from several plumbum commands, and local paths can be directly created by :func:`local.path() `. Paths are always absolute and are immutable, may refer to a remote machine, and can be used like a ``str``. In many respects, paths provide a similar API to pathlib in the Python 3.4+ standard library, with a few improvements and extra features. .. versionadded:: 1.6 Paths now support more pathlib like syntax, several old names have been depreciated, like ``.basename`` The primary ways to create paths are from ``.cwd``, ``.env.home``, or ``.path(...)`` on a local or remote machine, with ``/``, ``//`` or ``[]`` for composition. .. note:: The path returned from ``.cwd`` can also be used in a context manager and has a ``.chdir(path)`` function. See :ref:`guide-local-machine` for an example. Paths provide a variety of functions that allow you to check the status of a file:: >>> p = local.path("c:\\windows") >>> p.exists() True >>> p.is_dir() True >>> p.is_file() False Besides checking to see if a file exists, you can check the type of file using ``.is_dir()``, ``is_file()``, or ``is_symlink()``. You can access details about the file using the properties ``.dirname``, ``.drive``, ``.root``, ``.name``, ``.suffix``, and ``.stem`` (all suffixes). General stats can be obtained with ``.stat()``. You can use ``.with_suffix(suffix, depth=1)`` to replace the last ``depth`` suffixes with a new suffix. If you specify None for the depth, it will replace all suffixes (for example, ``.tar.gz`` is two suffixes). Note that a name like ``file.name.10.15.tar.gz`` will have "5" suffixes. Also available is ``.with_name(name)``, which will will replace the entire name. ``preferred_suffix(suffix)`` will add a suffix if one does not exist (for default suffix situations). Paths can be composed using ``/`` or ``[]``:: >>> p / "notepad.exe" >>> (p / "notepad.exe").is_file() True >>> (p / "notepad.exe").with_suffix(".dll") >>> p["notepad.exe"].is_file() True >>> p["../some/path"]["notepad.exe"].with_suffix(".dll") You can also iterate over directories to get the contents:: >>> for p2 in p: ... print p2 ... c:\windows\addins c:\windows\appcompat c:\windows\apppatch ... Paths also supply ``.iterdir()``, which may be faster on Python 3.5. Globing can be easily performed using ``//`` (floor division):: >>> p // "*.dll" [, ...] >>> p // "*/*.dll" [, ...] >>> local.cwd / "docs" // "*.rst" [, ...] .. versionadded:: 1.6 Globing a tuple will glob for each of the items in the tuple, and return the aggregated result. Files can be opened and read directly:: >>> with(open(local.cwd / "docs" / "index.rst")) as f: ... print(read(f)) <...output...> .. versionadded:: 1.6 Support for treating a path exactly like a ``str``, so they can be used directly in ``open()``. Paths also supply ``.delete()``, ``.copy(destination, override=False)``, and ``.move(destination)``. On systems that support it, you can also use ``.symlink(destination)``, ``.link(destination)``, and ``.unlink()``. You can change permissions with ``.chmod(mode)``, and change owners with ``.chown(owner=None, group=None, recursive=None)``. If ``recursive`` is ``None``, this will be recursive only if the path is a directory. For **copy**, **move**, or **delete** in a more general helper function form, see the :ref:`utils modules `. Relative paths can be computed using ``.relative_to(source)`` or ``mypath - basepath``, though it should be noted that relative paths are not as powerful as absolute paths, and are primarily for recording a path or printing. For further information, see the :ref:`api docs `. plumbum-1.7.2/docs/_color_list.html0000644000232200023220000005430214161152302017675 0ustar debalancedebalance
  1. #000000 Black

  2. #C00000 Red

  3. #00C000 Green

  4. #C0C000 Yellow

  5. #0000C0 Blue

  6. #C000C0 Magenta

  7. #00C0C0 Cyan

  8. #C0C0C0 LightGray

  9. #808080 DarkGray

  10. #FF0000 LightRed

  11. #00FF00 LightGreen

  12. #FFFF00 LightYellow

  13. #0000FF LightBlue

  14. #FF00FF LightMagenta

  15. #00FFFF LightCyan

  16. #FFFFFF White

  17. #000000 Grey0

  18. #00005F NavyBlue

  19. #000087 DarkBlue

  20. #0000AF Blue3

  21. #0000D7 Blue3A

  22. #0000FF Blue1

  23. #005F00 DarkGreen

  24. #005F5F DeepSkyBlue4

  25. #005F87 DeepSkyBlue4A

  26. #005FAF DeepSkyBlue4B

  27. #005FD7 DodgerBlue3

  28. #005FFF DodgerBlue2

  29. #008700 Green4

  30. #00875F SpringGreen4

  31. #008787 Turquoise4

  32. #0087AF DeepSkyBlue3

  33. #0087D7 DeepSkyBlue3A

  34. #0087FF DodgerBlue1

  35. #00AF00 Green3

  36. #00AF5F SpringGreen3

  37. #00AF87 DarkCyan

  38. #00AFAF LightSeaGreen

  39. #00AFD7 DeepSkyBlue2

  40. #00AFFF DeepSkyBlue1

  41. #00D700 Green3A

  42. #00D75F SpringGreen3A

  43. #00D787 SpringGreen2

  44. #00D7AF Cyan3

  45. #00D7D7 DarkTurquoise

  46. #00D7FF Turquoise2

  47. #00FF00 Green1

  48. #00FF5F SpringGreen2A

  49. #00FF87 SpringGreen1

  50. #00FFAF MediumSpringGreen

  51. #00FFD7 Cyan2

  52. #00FFFF Cyan1

  53. #5F0000 DarkRed

  54. #5F005F DeepPink4

  55. #5F0087 Purple4

  56. #5F00AF Purple4A

  57. #5F00D7 Purple3

  58. #5F00FF BlueViolet

  59. #5F5F00 Orange4

  60. #5F5F5F Grey37

  61. #5F5F87 MediumPurple4

  62. #5F5FAF SlateBlue3

  63. #5F5FD7 SlateBlue3A

  64. #5F5FFF RoyalBlue1

  65. #5F8700 Chartreuse4

  66. #5F875F DarkSeaGreen4

  67. #5F8787 PaleTurquoise4

  68. #5F87AF SteelBlue

  69. #5F87D7 SteelBlue3

  70. #5F87FF CornflowerBlue

  71. #5FAF00 Chartreuse3

  72. #5FAF5F DarkSeaGreen4A

  73. #5FAF87 CadetBlue

  74. #5FAFAF CadetBlueA

  75. #5FAFD7 SkyBlue3

  76. #5FAFFF SteelBlue1

  77. #5FD700 Chartreuse3A

  78. #5FD75F PaleGreen3

  79. #5FD787 SeaGreen3

  80. #5FD7AF Aquamarine3

  81. #5FD7D7 MediumTurquoise

  82. #5FD7FF SteelBlue1A

  83. #5FFF00 Chartreuse2A

  84. #5FFF5F SeaGreen2

  85. #5FFF87 SeaGreen1

  86. #5FFFAF SeaGreen1A

  87. #5FFFD7 Aquamarine1

  88. #5FFFFF DarkSlateGray2

  89. #870000 DarkRedA

  90. #87005F DeepPink4A

  91. #870087 DarkMagenta

  92. #8700AF DarkMagentaA

  93. #8700D7 DarkViolet

  94. #8700FF Purple

  95. #875F00 Orange4A

  96. #875F5F LightPink4

  97. #875F87 Plum4

  98. #875FAF MediumPurple3

  99. #875FD7 MediumPurple3A

  100. #875FFF SlateBlue1

  101. #878700 Yellow4

  102. #87875F Wheat4

  103. #878787 Grey53

  104. #8787AF LightSlateGrey

  105. #8787D7 MediumPurple

  106. #8787FF LightSlateBlue

  107. #87AF00 Yellow4A

  108. #87AF5F DarkOliveGreen3

  109. #87AF87 DarkSeaGreen

  110. #87AFAF LightSkyBlue3

  111. #87AFD7 LightSkyBlue3A

  112. #87AFFF SkyBlue2

  113. #87D700 Chartreuse2

  114. #87D75F DarkOliveGreen3A

  115. #87D787 PaleGreen3A

  116. #87D7AF DarkSeaGreen3

  117. #87D7D7 DarkSlateGray3

  118. #87D7FF SkyBlue1

  119. #87FF00 Chartreuse1

  120. #87FF5F LightGreenA

  121. #87FF87 LightGreenB

  122. #87FFAF PaleGreen1

  123. #87FFD7 Aquamarine1A

  124. #87FFFF DarkSlateGray1

  125. #AF0000 Red3

  126. #AF005F DeepPink4B

  127. #AF0087 MediumVioletRed

  128. #AF00AF Magenta3

  129. #AF00D7 DarkVioletA

  130. #AF00FF PurpleA

  131. #AF5F00 DarkOrange3

  132. #AF5F5F IndianRed

  133. #AF5F87 HotPink3

  134. #AF5FAF MediumOrchid3

  135. #AF5FD7 MediumOrchid

  136. #AF5FFF MediumPurple2

  137. #AF8700 DarkGoldenrod

  138. #AF875F LightSalmon3

  139. #AF8787 RosyBrown

  140. #AF87AF Grey63

  141. #AF87D7 MediumPurple2A

  142. #AF87FF MediumPurple1

  143. #AFAF00 Gold3

  144. #AFAF5F DarkKhaki

  145. #AFAF87 NavajoWhite3

  146. #AFAFAF Grey69

  147. #AFAFD7 LightSteelBlue3

  148. #AFAFFF LightSteelBlue

  149. #AFD700 Yellow3

  150. #AFD75F DarkOliveGreen3B

  151. #AFD787 DarkSeaGreen3A

  152. #AFD7AF DarkSeaGreen2

  153. #AFD7D7 LightCyan3

  154. #AFD7FF LightSkyBlue1

  155. #AFFF00 GreenYellow

  156. #AFFF5F DarkOliveGreen2

  157. #AFFF87 PaleGreen1A

  158. #AFFFAF DarkSeaGreen2A

  159. #AFFFD7 DarkSeaGreen1

  160. #AFFFFF PaleTurquoise1

  161. #D70000 Red3A

  162. #D7005F DeepPink3

  163. #D70087 DeepPink3A

  164. #D700AF Magenta3A

  165. #D700D7 Magenta3B

  166. #D700FF Magenta2

  167. #D75F00 DarkOrange3A

  168. #D75F5F IndianRedA

  169. #D75F87 HotPink3A

  170. #D75FAF HotPink2

  171. #D75FD7 Orchid

  172. #D75FFF MediumOrchid1

  173. #D78700 Orange3

  174. #D7875F LightSalmon3A

  175. #D78787 LightPink3

  176. #D787AF Pink3

  177. #D787D7 Plum3

  178. #D787FF Violet

  179. #D7AF00 Gold3A

  180. #D7AF5F LightGoldenrod3

  181. #D7AF87 Tan

  182. #D7AFAF MistyRose3

  183. #D7AFD7 Thistle3

  184. #D7AFFF Plum2

  185. #D7D700 Yellow3A

  186. #D7D75F Khaki3

  187. #D7D787 LightGoldenrod2

  188. #D7D7AF LightYellow3

  189. #D7D7D7 Grey84

  190. #D7D7FF LightSteelBlue1

  191. #D7FF00 Yellow2

  192. #D7FF5F DarkOliveGreen1

  193. #D7FF87 DarkOliveGreen1A

  194. #D7FFAF DarkSeaGreen1A

  195. #D7FFD7 Honeydew2

  196. #D7FFFF LightCyan1

  197. #FF0000 Red1

  198. #FF005F DeepPink2

  199. #FF0087 DeepPink1

  200. #FF00AF DeepPink1A

  201. #FF00D7 Magenta2A

  202. #FF00FF Magenta1

  203. #FF5F00 OrangeRed1

  204. #FF5F5F IndianRed1

  205. #FF5F87 IndianRed1A

  206. #FF5FAF HotPink

  207. #FF5FD7 HotPinkA

  208. #FF5FFF MediumOrchid1A

  209. #FF8700 DarkOrange

  210. #FF875F Salmon1

  211. #FF8787 LightCoral

  212. #FF87AF PaleVioletRed1

  213. #FF87D7 Orchid2

  214. #FF87FF Orchid1

  215. #FFAF00 Orange1

  216. #FFAF5F SandyBrown

  217. #FFAF87 LightSalmon1

  218. #FFAFAF LightPink1

  219. #FFAFD7 Pink1

  220. #FFAFFF Plum1

  221. #FFD700 Gold1

  222. #FFD75F LightGoldenrod2A

  223. #FFD787 LightGoldenrod2B

  224. #FFD7AF NavajoWhite1

  225. #FFD7D7 MistyRose1

  226. #FFD7FF Thistle1

  227. #FFFF00 Yellow1

  228. #FFFF5F LightGoldenrod1

  229. #FFFF87 Khaki1

  230. #FFFFAF Wheat1

  231. #FFFFD7 Cornsilk1

  232. #FFFFFF Grey100

  233. #080808 Grey3

  234. #121212 Grey7

  235. #1C1C1C Grey11

  236. #262626 Grey15

  237. #303030 Grey19

  238. #3A3A3A Grey23

  239. #444444 Grey27

  240. #4E4E4E Grey30

  241. #585858 Grey35

  242. #626262 Grey39

  243. #6C6C6C Grey42

  244. #767676 Grey46

  245. #808080 Grey50

  246. #8A8A8A Grey54

  247. #949494 Grey58

  248. #9E9E9E Grey62

  249. #A8A8A8 Grey66

  250. #B2B2B2 Grey70

  251. #BCBCBC Grey74

  252. #C6C6C6 Grey78

  253. #D0D0D0 Grey82

  254. #DADADA Grey85

  255. #E4E4E4 Grey89

  256. #EEEEEE Grey93

plumbum-1.7.2/docs/_news.rst0000644000232200023220000000420714161152302016343 0ustar debalancedebalance* **2021.02.08**: Version 1.7.0 released with a few new features like ``.with_cwd``, some useful bugfixes, and lots of cleanup. * **2020.03.23**: Version 1.6.9 released with several Path fixes, final version to support Python 2.6. * **2019.10.30**: Version 1.6.8 released with ``local.cmd``, a few command updates, ``Set`` improvements, and ``TypedEnv``. * **2018.08.10**: Version 1.6.7 released with several minor additions, mostly to CLI apps, and ``run_*`` modifiers added. * **2018.02.12**: Version 1.6.6 released with one more critical bugfix for a error message regression in 1.6.5. * **2017.12.29**: Version 1.6.5 released with mostly bugfixes, including a critical one that could break pip installs on some platforms. English cli apps now load as fast as before the localization update. * **2017.11.27**: Version 1.6.4 released with new CLI localization support. Several bugfixes and better pathlib compatibility, along with better separation between Plumbum's internal packages. * **2016.12.31**: Version 1.6.3 released to provide Python 3.6 compatibility. Mostly bugfixes, several smaller improvements to paths, and a provisional config parser added. * **2016.12.3**: Version 1.6.2 is now available through `conda-forge `_, as well. * **2016.6.25**: Version 1.6.2 released. This is mostly a bug fix release, but a few new features are included. Modifiers allow some new arguments, and ``Progress`` is improved. Better support for SunOS and other OS's. * **2015.12.18**: Version 1.6.1 released. The release mostly contains smaller fixes for CLI, 2.6/3.5 support, and colors. PyTest is now used for tests, and Conda is supported. * **2015.10.16**: Version 1.6.0 released. Highlights include Python 3.5 compatibility, the ``plumbum.colors`` package, ``Path`` becoming a subclass of ``str`` and a host of bugfixes. Special thanks go to Henry for his efforts. * **2015.07.17**: Version 1.5.0 released. This release brings a host of bug fixes, code cleanups and some experimental new features (be sure to check the changelog). Also, say hi to `Henry Schreiner `_, who has joined as a member of the project. plumbum-1.7.2/docs/Makefile0000644000232200023220000001300014161152302016125 0ustar debalancedebalance# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/PlumbumShellCombinators.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PlumbumShellCombinators.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/PlumbumShellCombinators" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PlumbumShellCombinators" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." plumbum-1.7.2/docs/conf.py0000644000232200023220000002033414161152302015774 0ustar debalancedebalance# -*- coding: utf-8 -*- # # Plumbum Shell Combinators documentation build configuration file, created by # sphinx-quickstart on Sun Apr 29 16:24:32 2012. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import os import sys import time # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # sys.path.insert(0, os.path.abspath('.')) # -- 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.viewcode"] # 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 = u"Plumbum Shell Combinators" copyright = u"%d, Tomer Filiba, licensed under MIT" % (time.gmtime().tm_year,) # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version is release. from plumbum.version import version as release from plumbum.version import version_tuple version = ".".join(str(v) for v in version_tuple[:2]) autodoc_member_order = "bysource" # 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", "_news.rst", "_cheatsheet.rst"] # 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 = [] # -- 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 = 'default' html_theme = "haiku" # 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 = {"full_logo": True} # 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 = "Plumbum: Shell Combinators" # A shorter title for the navigation bar. Default is the same as html_title. html_short_title = "" # The name of an image file (relative to this directory) to place at the top # of the sidebar. html_logo = "_static/logo8.png" # 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 = "PlumbumShellCombinatorsdoc" # -- 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", "PlumbumShellCombinators.tex", u"Plumbum Shell Combinators Documentation", u"Tomer Filiba", "manual", ), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ( "index", "plumbumshellcombinators", u"Plumbum Shell Combinators Documentation", [u"Tomer Filiba"], 1, ) ] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( "index", "PlumbumShellCombinators", u"Plumbum Shell Combinators Documentation", u"Tomer Filiba", "PlumbumShellCombinators", "One line description of project.", "Miscellaneous", ), ] # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' plumbum-1.7.2/docs/api/0000755000232200023220000000000014161152315015250 5ustar debalancedebalanceplumbum-1.7.2/docs/api/path.rst0000644000232200023220000000053414161152302016734 0ustar debalancedebalance.. _api-path: Package plumbum.path ==================== .. automodule:: plumbum.path.base :members: :special-members: .. automodule:: plumbum.path.local :members: :special-members: .. automodule:: plumbum.path.remote :members: :special-members: Utils ----- .. automodule:: plumbum.path.utils :members: :special-members: plumbum-1.7.2/docs/api/cli.rst0000644000232200023220000000046714161152302016554 0ustar debalancedebalance.. _api-cli: Package plumbum.cli =================== .. automodule:: plumbum.cli.application :members: .. automodule:: plumbum.cli.switches :members: .. automodule:: plumbum.cli.terminal :members: .. automodule:: plumbum.cli.termsize :members: .. automodule:: plumbum.cli.progress :members: plumbum-1.7.2/docs/api/commands.rst0000644000232200023220000000056514161152302017605 0ustar debalancedebalance.. _api-commands: Package plumbum.commands ======================== .. automodule:: plumbum.commands.base :members: :special-members: .. automodule:: plumbum.commands.daemons :members: :special-members: .. automodule:: plumbum.commands.modifiers :members: :special-members: .. automodule:: plumbum.commands.processes :members: :special-members: plumbum-1.7.2/docs/api/colors.rst0000644000232200023220000000112114161152302017272 0ustar debalancedebalancePackage plumbum.colors ====================== .. automodule:: plumbum.colors :members: :special-members: plumbum.colorlib ---------------- .. automodule:: plumbum.colorlib :members: :special-members: plumbum.colorlib.styles ----------------------- .. automodule:: plumbum.colorlib.styles :members: :special-members: plumbum.colorlib.factories -------------------------- .. automodule:: plumbum.colorlib.factories :members: :special-members: plumbum.colorlib.names ---------------------- .. automodule:: plumbum.colorlib.names :members: :special-members: plumbum-1.7.2/docs/api/fs.rst0000644000232200023220000000023414161152302016405 0ustar debalancedebalancePackage plumbum.fs ================== File system utilities .. automodule:: plumbum.fs.atomic :members: .. automodule:: plumbum.fs.mounts :members: plumbum-1.7.2/docs/api/machines.rst0000644000232200023220000000112314161152302017562 0ustar debalancedebalance.. _api-local-machine: Package plumbum.machines ======================== .. automodule:: plumbum.machines.env :members: :special-members: .. automodule:: plumbum.machines.local :members: :special-members: .. automodule:: plumbum.machines.session :members: :special-members: .. _api-remote-machines: Remote Machines --------------- .. automodule:: plumbum.machines.remote :members: :special-members: .. automodule:: plumbum.machines.ssh_machine :members: :special-members: .. automodule:: plumbum.machines.paramiko_machine :members: :special-members: plumbum-1.7.2/docs/index.rst0000644000232200023220000001736414161152302016347 0ustar debalancedebalance.. raw:: html Plumbum: Shell Combinators and More =================================== .. comment raw:: html
Sticky
Version 3.2.3 was released on December 2nd
Please use the mailing list to ask questions and use github issues to report problems. Please do not email me directly.
Ever wished the compactness of shell scripts be put into a **real** programming language? Say hello to *Plumbum Shell Combinators*. Plumbum (Latin for *lead*, which was used to create pipes back in the day) is a small yet feature-rich library for shell script-like programs in Python. The motto of the library is **"Never write shell scripts again"**, and thus it attempts to mimic the **shell syntax** (*shell combinators*) where it makes sense, while keeping it all **Pythonic and cross-platform**. Apart from :ref:`shell-like syntax ` and :ref:`handy shortcuts `, the library provides local and :ref:`remote ` command execution (over SSH), local and remote file-system :ref:`paths `, easy working-directory and environment :ref:`manipulation `, quick access to ANSI :ref:`colors `, and a programmatic :ref:`guide-cli` application toolkit. Now let's see some code! News ==== .. include:: _news.rst * :doc:`changelog` * :doc:`quickref` Cheat Sheet =========== .. include:: _cheatsheet.rst Development and Installation ============================ The library is developed on `GitHub `_, and will happily accept `patches `_ from users. Please use the GitHub's built-in `issue tracker `_ to report any problem you encounter or to request features. The library is released under the permissive `MIT license `_. Requirements ------------ Plumbum supports **Python 2.7-3.9** and **PyPy** and is continually tested on **Linux**, **Mac**, and **Windows** machines through `GitHub Actions `_. Any Unix-like machine should work fine out of the box, but on Windows, you'll probably want to install a decent `coreutils `_ environment and add it to your ``PATH``, or use WSL(2). I can recommend `mingw `_ (which comes bundled with `Git for Windows `_), but `cygwin `_ should work too. If you only wish to use Plumbum as a Popen-replacement to run Windows programs, then there's no need for the Unix tools. Note that for remote command execution, an **openSSH-compatible** client is required (also bundled with *Git for Windows*), and a ``bash``-compatible shell and a coreutils environment is also expected on the host machine. This project uses ``setuptools`` to build wheels; and ``setuptools_scm`` is required for building SDists. These dependencies will be handled for you by PEP 518 compatible builders, like `build `_ and ``pip 10+``. Download -------- You can **download** the library from the `Python Package Index `_ (in a variety of formats), or run ``pip install plumbum`` directly. If you use Anaconda, you can also get it from the ``conda-forge`` channel with ``conda install -c conda-forge plumbum``. User Guide ========== The user guide covers most of the features of Plumbum, with lots of code-snippets to get you swimming in no time. It introduces the concepts and "syntax" gradually, so it's recommended you read it in order. A quick :ref:`reference guide is available `. .. toctree:: :maxdepth: 2 local_commands paths local_machine remote utils cli typed_env colors changelog quickref API Reference ============= The API reference (generated from the *docstrings* within the library) covers all of the exposed APIs of the library. Note that some "advanced" features and some function parameters are missing from the guide, so you might want to consult with the API reference in these cases. .. toctree:: :maxdepth: 2 api/cli api/commands api/machines api/path api/fs api/colors colorlib .. note:: The ``local`` object is an instance of a ``machine``. About ===== The original purpose of Plumbum was to enable local and remote program execution with ease, assuming nothing fancier than good-old SSH. On top of this, a file-system abstraction layer was devised, so that working with local and remote files would be seamless. I've toyed with this idea for some time now, but it wasn't until I had to write build scripts for a project I've been working on that I decided I've had it with shell scripts and it's time to make it happen. Plumbum was born from the scraps of the ``Path`` class, which I wrote for the aforementioned build system, and the ``SshContext`` and ``SshTunnel`` classes that I wrote for `RPyC `_. When I combined the two with *shell combinators* (because shell scripts do have an edge there) the magic happened and here we are. Credits ======= The project has been inspired by **PBS** (now called `sh `_) of `Andrew Moffat `_, and has borrowed some of his ideas (namely treating programs like functions and the nice trick for importing commands). However, I felt there was too much magic going on in PBS, and that the syntax wasn't what I had in mind when I came to write shell-like programs. I contacted Andrew about these issues, but he wanted to keep PBS this way. Other than that, the two libraries go in different directions, where Plumbum attempts to provide a more wholesome approach. Plumbum also pays tribute to `Rotem Yaari `_ who suggested a library code-named ``pyplatform`` for that very purpose, but which had never materialized. plumbum-1.7.2/docs/colors.rst0000644000232200023220000002716514161152302016541 0ustar debalancedebalance.. _guide-colors: Colors ------ .. versionadded:: 1.6 The purpose of the `plumbum.colors` library is to make adding text styles (such as color) to Python easy and safe. Color is often a great addition to shell scripts, but not a necessity, and implementing it properly is tricky. It is easy to end up with an unreadable color stuck on your terminal or with random unreadable symbols around your text. With the color module, you get quick, safe access to ANSI colors and attributes for your scripts. The module also provides an API for creating other color schemes for other systems using escapes. .. note:: Enabling color ``ANSIStyle`` assumes that only a terminal can display color, and looks at the value of the environment variable ``TERM``. You can force the use of color globally by setting ``colors.use_color=4`` (The levels 0-4 are available, with 0 being off). See this :ref:`note ` for more options. Quick start =========== Colors (``red``, ``green``, etc.), attributes (``bold``, ``underline``, etc.) and general styles (``warn``, ``info``, etc.) are in ``plumbum.colors``. Combine styles with ``&``, apply to a string with ``|``. So, to output a warning you would do .. code-block:: python from plumbum.colors import warn print(warn | "This is a warning.") .. raw:: html

This is a warning. To create a custom style you would do .. code-block:: python from plumbum import colors print(colors.green & colors.bold | "This is green and bold.") .. raw:: html This is green and bold. You can use rgb colors, too: .. code-block:: python print(colors.rgb(0,255,0) | "This is also green.") .. raw:: html This is also green Generating Styles ================= Styles are accessed through the ``plumbum.colors`` object. This has the following available objects: ``fg`` and ``bg`` The foreground and background colors, reset to default with ``colors.fg.reset`` or ``~colors.fg`` and likewise for ``bg``. ``bold``, ``dim``, ``underline``, ``italics``, ``reverse``, ``strikeout``, and ``hidden`` All the `ANSI` modifiers are available, as well as their negations, such as ``~colors.bold`` or ``colors.bold.reset``, etc. ``reset`` The global reset will restore all properties at once. ``do_nothing`` Does nothing at all, but otherwise acts like any ``Style`` object. It is its own inverse. Useful for ``cli`` properties. Styles loaded from a stylesheet dictionary, such as ``warn`` and ``info``. These allow you to set standard styles based on behavior rather than colors, and you can load a new stylesheet with ``colors.load_stylesheet(...)``. Recreating and loading the default stylesheet would look like this: .. code-block:: python >>> default_styles = dict( ... warn="fg red", ... title="fg cyan underline bold", ... fatal="fg red bold", ... highlight="bg yellow", ... info="fg blue", ... success="fg green") >>> colors.load_stylesheet(default_styles) The ``colors.from_ansi(code)`` method allows you to create a Style from any ansi sequence, even complex or combined ones. Colors ^^^^^^ The ``colors.fg`` and ``colors.bg`` allow you to access and generate colors. Named foreground colors are available directly as methods. The first 16 primary colors, ``black``, ``red``, ``green``, ``yellow``, ``blue``, ``magenta``, ``cyan``, etc, as well as ``reset``, are available. All 256 color names are available, but do not populate directly, so that auto-completion gives reasonable results. You can also access colors using strings and do ``colors.fg[string]``. Capitalization, underscores, and spaces (for strings) will be ignored. You can also access colors numerically with ``colors.fg[n]`` for the extended 256 color codes. ``colors.fg.rgb(r,g,b)`` will create a color from an input red, green, and blue values (integers from 0-255). ``colors.fg.rgb(code)`` will allow you to input an html style hex sequence. Anything you can access from ``colors.fg`` can also be accessed directly from ``colors``. 256 Color Support ================= While this library supports full 24 bit colors through escape sequences, the library has special support for the "full" 256 colorset through numbers, names or HEX html codes. Even if you use 24 bit color, the closest name is displayed in the ``repr``. You can access the colors as as ``colors.fg.Light_Blue``, ``colors.fg.lightblue``, ``colors.fg[12]``, ``colors.fg('Light_Blue')``, ``colors.fg('LightBlue')``, or ``colors.fg('#0000FF')``. You can also iterate or slice the ``colors``, ``colors.fg``, or ``colors.bg`` objects. Slicing even intelligently downgrades to the simple version of the codes if it is within the first 16 elements. The supported colors are: .. raw:: html :file: _color_list.html If you want to enforce a specific representation, you can use ``.basic`` (8 color), ``.simple`` (16 color), ``.full`` (256 color), or ``.true`` (24 bit color) on a style, and the colors in that Style will conform to the output representation and name of the best match color. The internal RGB colors are remembered, so this is a non-destructive operation. .. _guide-usecolors: .. note:: Some terminals only support a subset of colors, so keep this in mind when using a larger color set. The standard Ubuntu terminal handles 24 bit color, the Mac terminal only handles 256 colors, and Colorama on Windows only handles 8. See `this gist `_ for information about support in terminals. If you need to limit the output color, you can set ``colors.use_color`` to 0 (no colors), 1 (8 colors), 2 (16 colors), or 3 (256 colors), or 4 (24-bit colors). This option will be automatically guessed for you on initialization. Style manipulations =================== Safe color manipulations refer to changes that reset themselves at some point. Unsafe manipulations must be manually reset, and can leave your terminal color in an unreadable state if you forget to reset the color or encounter an exception. The library is smart and will try to restore the color when Python exits. .. note:: If you do get the color unset on a terminal, the following, typed into the command line, will restore it: .. code:: bash $ python -m plumbum.colors This also supports command line access to unsafe color manipulations, such as .. code:: bash $ python -m plumbum.colors blue $ python -m plumbum.colors bg red $ python -m plumbum.colors fg 123 $ python -m plumbum.colors bg reset $ python -m plumbum.colors underline You can use any path or number available as a style. Unsafe Manipulation ^^^^^^^^^^^^^^^^^^^ Styles have two unsafe operations: Concatenation (with ``+`` and a string) and calling ``.now()`` without arguments (directly calling a style without arguments is also a shortcut for ``.now()``). These two operations do not restore normal color to the terminal by themselves. To protect their use, you can use a context manager around any unsafe operation. An example of the usage of unsafe ``colors`` manipulations inside a context manager:: from plumbum import colors with colors: colors.fg.red.now() print('This is in red') .. raw:: html

This is in red
This is in green and now also underlined!
Underlined and not underlined but still green.
This is completly restored, even if an exception is thrown!

colors.green.now() print('This is green ' + colors.underline + 'and now also underlined!') print('Underlined' + colors.underline.reset + ' and not underlined but still red') print('This is completly restored, even if an exception is thrown!') Output: .. raw:: html

This is in red
This is in green and now also underlined!
Underlined and not underlined but still green.
This is completly restored, even if an exception is thrown!

We can use ``colors`` instead of ``colors.fg`` for foreground colors. If we had used ``colors.fg`` as the context manager, then non-foreground properties, such as ``colors.underline`` or ``colors.bg.yellow``, would not have been reset. Each attribute, as well as ``fg``, ``bg``, and ``colors`` all have inverses in the ANSI standard. They are accessed with ``~`` or ``.reset``, and can be used to manually make these operations safer, but there is a better way. Safe Manipulation ^^^^^^^^^^^^^^^^^ All other operations are safe; they restore the color automatically. The first, and hopefully already obvious one, is using a specific style rather than a ``colors`` or ``colors.fg`` object in a ``with`` statement. This will set the color (using ``sys.stdout`` by default) to that color, and restore color on leaving. The second method is to manually wrap a string. This can be done with ``color | "string"`` or ``color["string"]``. These produce strings that can be further manipulated or printed. Finally, you can also print a color to stdout directly using ``color.print("string")``. This has the same syntax as the Python 3 print function. In Python 2, if you do not have ``from __future__ import print_function`` enabled, ``color.print_("string")`` is provided as an alternative, following the PyQT convention for method names that match reserved Python syntax. An example of safe manipulations:: colors.fg.yellow('This is yellow', end='') print(' And this is normal again.') with colors.red: print('Red color!') with colors.bold: print("This is red and bold.") print("Not bold, but still red.") print("Not red color or bold.") print(colors.magenta & colors.bold | "This is bold and colorful!", "And this is not.") Output: .. raw:: html

This is yellow And this is normal again.
Red color!
This is red and bold.
Not bold, but still red.
Not red color or bold.
This is bold and colorful! And this is not.

Style Combinations ^^^^^^^^^^^^^^^^^^ You can combine styles with ``&`` and they will create a new combined style. Colors will not be "summed" or otherwise combined; the rightmost color will be used (this matches the expected effect of applying the styles individually to the strings). However, combined styles are intelligent and know how to reset just the properties that they contain. As you have seen in the example above, the combined style ``(colors.magenta & colors.bold)`` can be used in any way a normal style can. New color systems ================= The library was written primarily for ANSI color sequences, but can also easily be subclassed to create new color systems. See :ref:`guide-colorlib` for information on how the system works. An HTML version is available as ``plumbum.colorlib.htmlcolors``. See Also ======== * `colored `_ Another library with 256 color support * `colorful `_ A fairly new libary with a similar feature set * `colorama `_ A library that supports colored text on Windows, can be combined with Plumbum.colors (if you force ``use_color``, doesn't support all extended colors) plumbum-1.7.2/docs/typed_env.rst0000644000232200023220000000561414161152302017230 0ustar debalancedebalance.. _guide-typed-env: TypedEnv ======== Plumbum provides this utility class to facilitate working with environment variables. Similar to how :class:`plumbum.cli.Application` parses command line arguments into pythonic data types, :class:`plumbum.typed_env.TypedEnv` parses environment variables: class MyEnv(TypedEnv): username = TypedEnv.Str("USER", default='anonymous') path = TypedEnv.CSV("PATH", separator=":", type=local.path) tmp = TypedEnv.Str(["TMP", "TEMP"]) # support 'fallback' var-names is_travis = TypedEnv.Bool("TRAVIS", default=False) # True is 'yes/true/1' (case-insensitive) We can now instantiate this class to access its attributes:: >>> env = MyEnv() >>> env.username 'ofer' >>> env.path [, , , , , , ] >>> env.tmp Traceback (most recent call last): [...] KeyError: 'TMP' >>> env.is_travis False Finally, our ``TypedEnv`` object allows us ad-hoc access to the rest of the environment variables, using dot-notation:: >>> env.HOME '/home/ofer' We can also update the environment via our ``TypedEnv`` object: >>> env.tmp = "/tmp" >>> env.tmp '/tmp' >>> from os import environ >>> env.TMP '/tmp' >>> env.is_travis = True >>> env.TRAVIS 'yes' >>> env.path = [local.path("/a"), local.path("/b")] >>> env.PATH '/a:/b' TypedEnv as an Abstraction Layer ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The ``TypedEnv`` class is very useful for separating your application from the actual environment variables. It provides a layer where parsing and normalizing can take place in a centralized fashion. For example, you might start with this simple implementation:: class CiBuildEnv(TypedEnv): job_id = TypedEnv.Str("BUILD_ID") Later, as the application gets more complicated, you may expand your implementation like so:: class CiBuildEnv(TypedEnv): is_travis = TypedEnv.Bool("TRAVIS", default=False) _travis_job_id = TypedEnv.Str("TRAVIS_JOB_ID") _jenkins_job_id = TypedEnv.Str("BUILD_ID") @property def job_id(self): return self._travis_job_id if self.is_travis else self._jenkins_job_id TypedEnv vs. local.env ^^^^^^^^^^^^^^^^^^^^^^ It is important to note that ``TypedEnv`` is separate and unrelated to the ``LocalEnv`` object that is provided via ``local.env``. While ``TypedEnv`` reads and writes directly to ``os.environ``, ``local.env`` is a frozen copy taken at the start of the python session. While ``TypedEnv`` is focused on parsing environment variables to be used by the current process, ``local.env``'s primary purpose is to manipulate the environment for child processes that are spawned via plumbum's :ref:`local commands `. plumbum-1.7.2/docs/remote.rst0000644000232200023220000002375414161152302016533 0ustar debalancedebalance.. _guide-remote: Remote ====== Just like running local commands, Plumbum supports running commands on remote systems, by executing them over SSH. .. _guide-remote-machines: Remote Machines --------------- Forming a connection to a remote machine is very straight forward:: >>> from plumbum import SshMachine >>> rem = SshMachine("hostname", user = "john", keyfile = "/path/to/idrsa") >>> # ... >>> rem.close() Or as a context-manager:: >>> with SshMachine("hostname", user = "john", keyfile = "/path/to/idrsa") as rem: ... pass .. note:: ``SshMachine`` requires ``ssh`` (``openSSH`` or compatible) installed on your system in order to connect to remote machines. The remote machine must have bash as the default shell (or any shell that supports the ``2>&1`` syntax for stderr redirection). Alternatively, you can use the pure-Python implementation of :ref:`ParamikoMachine `. Only the ``hostname`` parameter is required, all other parameters are optional. If the host has your ``id-rsa.pub`` key in its ``authorized_keys`` file, or if you've set up your ``~/.ssh/config`` to login with some user and ``keyfile``, you can simply use ``rem = SshMachine("hostname")``. Much like the :ref:`local object `, remote machines expose ``which()``, ``path()``, ``python``, ``cwd`` and ``env``. You can also run remote commands, create SSH tunnels, upload/download files, etc. You may also refer to :class:`the full API `, as this guide will only survey the features. .. note:: `PuTTY `_ users on Windows should use the dedicated :class:`PuttyMachine ` instead of ``SshMachine``. See also :ref:`ParamikoMachine `. .. versionadded:: 1.0.1 Working Directory and Environment ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The ``cwd`` and ``env`` attributes represent the remote machine's working directory and environment variables, respectively, and can be used to inspect or manipulate them. Much like their local counterparts, they can be used as context managers, so their effects can be contained. :: >>> rem.cwd >>> with rem.cwd(rem.cwd / "Desktop"): ... print rem.cwd /home/john/Desktop >>> rem.env["PATH"] /bin:/sbin:/usr/bin:/usr/local/bin >>> rem.which("ls") Tunneling ^^^^^^^^^ SSH tunneling is a very useful feature of the SSH protocol. It allows you to connect from your machine to a remote server process, while having your connection authenticated and encrypted out-of-the-box. Say you run on ``machine-A``, and you wish to connect to a server program running on ``machine-B``. That server program binds to ``localhost:8888`` (where ``localhost`` refers naturally to to ``machine-B``). Using Plumbum, you can easily set up a tunnel from port 6666 on ``machine-A`` to port 8888 on ``machine-B``:: >>> tun = rem.tunnel(6666, 8888) >>> # ... >>> tun.close() Or as a context manager:: >>> with rem.tunnel(6666, 8888): ... pass You can now connect a socket to ``machine-A:6666``, and it will be securely forwarded over SSH to ``machine-B:8888``. When the tunnel object is closed, all active connections will be dropped. .. _guide-remote-commands: Remote Commands --------------- Like local commands, remote commands are created using indexing (``[]``) on a remote machine object. You can either pass the command's name, in which case it will be resolved by through ``which``, or the path to the program. :: >>> rem["ls"] , '/bin/ls')> >>> rem["/usr/local/bin/python3.2"] , '/usr/local/bin/python3.2')> >>> r_ls = rem["ls"] >>> r_grep = rem["grep"] >>> r_ls() u'foo\nbar\spam\n' Nesting Commands ^^^^^^^^^^^^^^^^ Remote commands can be nested just like local ones. In fact, that's how the ``SshMachine`` operates behind the scenes - it nests each command inside ``ssh``. Here are some examples:: >>> r_sudo = rem["sudo"] >>> r_ifconfig = rem["ifconfig"] >>> print r_sudo[r_ifconfig["-a"]]() eth0 Link encap:Ethernet HWaddr ... [...] You can nest multiple commands, one within another. For instance, you can connect to some machine over SSH and use that machine's SSH client to connect to yet another machine. Here's a sketch:: >>> from plumbum.cmd import ssh >>> print ssh["localhost", ssh["localhost", "ls"]] /usr/bin/ssh localhost /usr/bin/ssh localhost ls >>> >>> ssh["localhost", ssh["localhost", "ls"]]() u'bin\nDesktop\nDocuments\n...' Piping ^^^^^^ Piping works for remote commands as well, but there's a caveat to note here: the plumbing takes place on the local machine! Consider this code for instance :: >>> r_grep = rem["grep"] >>> r_ls = rem["ls"] >>> (r_ls | r_grep["b"])() u'bin\nPublic\n' Although ``r_ls`` and ``r_grep`` are remote commands, the data is sent from ``r_ls`` to the local machine, which then sends it to the remote one for running ``grep``. This will be fixed in a future version of Plumbum. It should be noted, however, that piping remote commands into local ones is perfectly fine. For example, the previous code can be written as :: >>> from plumbum.cmd import grep >>> (r_ls | grep["b"])() u'bin\nPublic\n' Which is even more efficient (no need to send data back and forth over SSH). .. _guide-paramiko-machine: Redirection ^^^^^^^^^^^ Redirection to and from remote paths is not currently supported, but you can redirect to and from local paths, with the familiar syntax explained in :ref:`the corresponding section for local commands `. Note that if the redirection target/source is given as a string, it is automatically interpreted as a path on the local machine. Paramiko Machine ---------------- .. versionadded:: 1.1 ``SshMachine`` relies on the system's ``ssh`` client to run commands; this means that for each remote command you run, a local process is spawned and an SSH connection is established. While relying on a well-known and trusted SSH client is the most stable option, the incurred overhead of creating a separate SSH connection for each command may be too high. In order to overcome this, Plumbum provides integration for `paramiko `_, an open-source, pure-Python implementation of the SSH2 protocol. This is the ``ParamikoMachine``, and it works along the lines of the ``SshMachine``:: >>> from plumbum.machines.paramiko_machine import ParamikoMachine >>> rem = ParamikoMachine("192.168.1.143") >>> rem["ls"] RemoteCommand(, ) >>> r_ls = rem["ls"] >>> r_ls() u'bin\nDesktop\nDocuments\nDownloads\nexamples.desktop\nMusic\nPictures\n...' >>> r_ls("-a") u'.\n..\n.adobe\n.bash_history\n.bash_logout\n.bashrc\nbin...' .. note:: Using ``ParamikoMachine`` requires paramiko to be installed on your system. Also, you have to explicitly import it (``from plumbum.machines.paramiko_machine import ParamikoMachine``) as paramiko is quite heavy. Refer to :class:`the API docs ` for more details. The main advantage of using ``ParamikoMachine`` is that only a single, persistent SSH connection is created, over which commands execute. Moreover, paramiko has a built-in SFTP client, which is used instead of ``scp`` to copy files (employed by the ``.download()``/``.upload()`` methods), and tunneling is much more light weight: In the ``SshMachine``, a tunnel is created by an external process that lives for as long as the tunnel is to remain active. The ``ParamikoMachine``, however, can simply create an extra *channel* on top of the same underlying connection with ease; this is exposed by ``connect_sock()``, which creates a tunneled TCP connection and returns a socket-like object .. warning:: Piping and input/output redirection don't really work with ``ParamikoMachine`` commands. You'll get all kinds of errors, like ``'ChannelFile' object has no attribute 'fileno'`` or ``I/O operation on closed file`` -- this is due to the fact that Paramiko's channels are not real, OS-level files, so they can't interact with ``subprocess.Popen``. This will be solved in a future release; in the meanwhile, you can use the machine's ``.session()`` method, like so :: >>> s = mach.session() >>> s.run("ls | grep b") (0, u'bin\nPublic\n', u'') Tunneling Example ^^^^^^^^^^^^^^^^^ On ``192.168.1.143``, I ran the following sophisticated server (notice it's bound to ``localhost``):: >>> import socket >>> s=socket.socket() >>> s.bind(("localhost", 12345)) >>> s.listen(1) >>> s2,_=s.accept() >>> while True: ... data = s2.recv(1000) ... if not data: ... break ... s2.send("I eat " + data) ... On my other machine, I connect (over SSH) to this host and then create a tunneled connection to port 12345, getting back a socket-like object:: >>> rem = ParamikoMachine("192.168.1.143") >>> s = rem.connect_sock(12345) >>> s.send("carrot") 6 >>> s.recv(1000) 'I eat carrot' >>> s.send("babies") 6 >>> s.recv(1000) 'I eat babies' >>> s.close() .. _guide-remote-paths: Remote Paths ------------ Analogous to local paths, remote paths represent a file-system path of a remote system, and expose a set of utility functions for iterating over subpaths, creating subpaths, moving/copying/ renaming paths, etc. :: >>> p = rem.path("/bin") >>> p / "ls" >>> (p / "ls").is_file() True >>> rem.path("/dev") // "sd*" [, < RemotePath /dev/sdb>, , ] .. note:: See the :ref:`guide-utils` guide for copying, moving and deleting remote paths For futher information, see the :ref:`api docs `. plumbum-1.7.2/translations.py0000755000232200023220000000210114161152302016633 0ustar debalancedebalance#!/usr/bin/env python # -*- coding: utf-8 -*- # If you are on macOS and using brew, you might need the following first: # export PATH="/usr/local/opt/gettext/bin:$PATH" from plumbum import FG, local from plumbum.cmd import msgfmt, msgmerge, xgettext translation_dir = local.cwd / "plumbum/cli/i18n" template = translation_dir / "messages.pot" ( xgettext[ "--from-code", "utf-8", "-L", "python", "--keyword=T_", "--package-name=Plumbum.cli", "-o", template, sorted(x - local.cwd for x in local.cwd / "plumbum/cli" // "*.py"), ] & FG ) for translation in translation_dir // "*.po": lang = translation.stem new_tfile = translation.with_suffix(".po.new") # Merge changes to new file (msgmerge[translation, template] > new_tfile) & FG new_tfile.move(translation) # Render new file into runtime output local_dir = translation_dir / lang / "LC_MESSAGES" if not local_dir.exists(): local_dir.mkdir() msgfmt["-o", local_dir / "plumbum.cli.mo", translation] & FG